@wade123
2019-04-29T01:07:42.000000Z
字数 12237
阅读 1314
Python入门爬虫与数据分析
摘要: 使用 Scrapy 框架爬取分析酷安网 App 信息。
上一个教程,我们使用 pyspider 框架爬取了虎嗅文章,它的整体功能还是单薄,更强大常用的框架是 Scrapy,本文使用该框架爬取分析著名手机应用市场——酷安网 6000 款App 信息。
本文知识点:
相对于 Pyspider ,Scrapy 框架要复杂一些,有不同的处理模块,项目文件也由好几个程序组成,不同的爬虫模块需要放在不同的程序中去,所以刚开始入门会觉得程序七零八散,容易把人搞晕,先大概了解一下 Scrapy 的架构和代码编写方式
Scrapy 框架的架构,如下图所示:

它可以分为如下的几个部分。
项目文件结构如下:
project/spiders/__init__.pyspiders.py__init__.pyitems.pypipelines.pysettings.pymiddlewares.py
最重要的文件是下面这几个:
经过上面的介绍,你可能还是不太明白 Scarpy 怎么使用,下面我们就来实战, 抓取酷安网页端 6000 App 信息。
观察目标网站可以发现两点有用信息:

接下来看看选择抓取哪些信息,可以看到,主页面内显示了 App 名称、下载量、评分等信息,我们再点击 App 图标进入详情页,可以看到提供了更齐全的信息,包括:分类标签、评分人数、关注人数等。由于,我们后续需要对 App 进行分类筛选,故分类标签很有用,所以这里我们选择进入每个 App 主页抓取所需信息指标。

通过上述分析,我们就可以确定抓取流程了,首先遍历主页面 ,抓取 10 个 App 的详情页 URL,然后详情页再抓取每个 App 的指标,如此遍历下来,我们需要抓取 6000 个左右网页内容,抓取工作量不算小,所以,我们接下来尝试使用 Scrapy 框架进行抓取。
首先安装好 Scrapy 框架,如果是 Windwos 系统,且已经安装了 Anaconda,那么安装 Scrapy 框架就非常简单,只需打开 Anaconda Prompt 命令窗口,输入下面一句命令即可,会自动帮我们安装好 Scrapy 所有需要安装和依赖的库。
conda pip scrapy
接着,我们需要创建一个爬虫项目,所以我们先从根目录切换到需要放置项目的工作路径,比如我这里设置的存放路径为:E:\my_Python\training\kuan,接着继续输入下面一行代码即可创建 kuan 爬虫项目:
# 切换工作路径e:cd E:\my_Python\training\kuan# 生成项目scrapy startproject kuspider
执行上面的命令后,就会生成一个名为 kuan 的 scrapy 爬虫项目,包含以下几个文件:
scrapy. cfg # Scrapy 部署时的配置文件kuan # 项目的模块,需要从这里引入_init__.pyitems.py # 定义爬取的数据结构middlewares.py # Middlewares 中间件pipelines.py # 数据管道文件,可用于后续存储settings.py # 配置文件spiders # 爬取主程序文件夹_init_.py
下面,我们需要再 spiders 文件夹中创建一个爬取主程序:kuan.py,接着运行下面两行命令即可:
cd kuan # 进入刚才生成的 kuan 项目文件夹scrapy genspider kuan www.coolapk.com # 生成爬虫主程序文件 kuan.py
项目文件创建好以后,我们就可以开始写爬虫程序了。
首先,需要在 items.py 文件中,预先定义好要爬取的字段信息名称,如下所示:
class KuanItem(scrapy.Item):# define the fields for your item here like:name = scrapy.Field()volume = scrapy.Field()download = scrapy.Field()follow = scrapy.Field()comment = scrapy.Field()tags = scrapy.Field()score = scrapy.Field()num_score = scrapy.Field()
这里的字段信息就是我们前面在网页中定位的 8 个字段信息,包括:name 表示 App 名称、volume 表示体积、download 表示 下载数量。在这里定义好之后,我们在后续的爬取主程序中会利用到这些字段信息。
创建好 kuan 项目后,Scrapy 框架会自动生成爬取的部分代码,我们接下来就需要在 parse 方法中增加网页抓取的字段解析内容。
class KuspiderSpider(scrapy.Spider):name = 'kuan'allowed_domains = ['www.coolapk.com']start_urls = ['http://www.coolapk.com/']def parse(self, response):pass
打开主页 Dev Tools,找到每项抓取指标的节点位置,然后可以采用 CSS、Xpath、正则等方法进行提取解析,这些方法 Scrapy 都支持,可随意选择,这里我们选用 CSS 语法来定位节点,不过需要注意的是,Scrapy 的 CSS 语法和之前我们利用 pyquery 使用的 CSS 语法稍有不同,举几个例子,对比说明一下。

首先,我们定位到第一个 APP 的主页 URL 节点,可以看到 URL 节点位于 class 属性为 app_left_list 的 div 节点下的 a 节点中,其 href 属性就是我们需要的 URL 信息,这里是相对地址,拼接后就是完整的 URL :www.coolapk.com/apk/com.coolapk.market。
接着我们进入酷安详情页,选择 App 名称并进行定位,可以看到 App 名称节点位于 class 属性为 .detail_app_title 的 p 节点的文本中。

定位到这两个节点之后,我们就可以使用 CSS 提取字段信息了,这里对比一下常规写法和 Scrapy 中的写法:
# 常规写法url = item('.app_left_list>a').attr('href')name = item('.list_app_title').text()# Scrapy 写法url = item.css('::attr("href")').extract_first()name = item.css('.detail_app_title::text').extract_first()
可以看到,要获取 href 或者 text 属性,需要用 :: 表示,比如获取 text,则用 ::text。extract_first() 表示提取第一个元素,如果有多个元素,则用 extract() 。接着,我们就可以参照写出 8 个字段信息的解析代码。
首先,我们需要在主页提取 App 的 URL 列表,然后再进入每个 App 的详情页进一步提取 8 个字段信息。
def parse(self, response):contents = response.css('.app_left_list>a')for content in contents:url = content.css('::attr("href")').extract_first()url = response.urljoin(url) # 拼接相对 url 为绝对 urlyield scrapy.Request(url,callback=self.parse_url)
这里,利用 response.urljoin() 方法将提取出的相对 URL 拼接为完整的 URL,然后利用 scrapy.Request() 方法构造每个 App 详情页的请求,这里我们传递两个参数:url 和 callback,url 为详情页 URL,callback 是回调函数,它将主页 URL 请求返回的响应 response 传给专门用来解析字段内容的 parse_url() 方法,如下所示:
def parse_url(self,response):item = KuanItem()item['name'] = response.css('.detail_app_title::text').extract_first()results = self.get_comment(response)item['volume'] = results[0]item['download'] = results[1]item['follow'] = results[2]item['comment'] = results[3]item['tags'] = self.get_tags(response)item['score'] = response.css('.rank_num::text').extract_first()num_score = response.css('.apk_rank_p1::text').extract_first()item['num_score'] = re.search('共(.*?)个评分',num_score).group(1)yield itemdef get_comment(self,response):messages = response.css('.apk_topba_message::text').extract_first()result = re.findall(r'\s+(.*?)\s+/\s+(.*?)下载\s+/\s+(.*?)人关注\s+/\s+(.*?)个评论.*?',messages) # \s+ 表示匹配任意空白字符一次以上if result: # 不为空results = list(result[0]) # 提取出 list 中第一个元素return resultsdef get_tags(self,response):data = response.css('.apk_left_span2')tags = [item.css('::text').extract_first() for item in data]return tags
这里,单独定义了 get_comment() 和 get_tags() 两个方法.
get_comment() 方法通过正则匹配提取 volume、download、follow、comment 四个字段信息,正则匹配结果如下:
result = re.findall(r'\s+(.*?)\s+/\s+(.*?)下载\s+/\s+(.*?)人关注\s+/\s+(.*?)个评论.*?',messages)print(result) # 输出第一页的结果信息# 结果如下:[('21.74M', '5218 万', '2.4 万', '5.4 万')][('75.53M', '2768 万', '2.3 万', '3.0 万')][('46.21M', '1686 万', '2.3 万', '3.4 万')][('54.77M', '1603 万', '3.8 万', '4.9 万')][('3.32M', '1530 万', '1.5 万', '3343')][('75.07M', '1127 万', '1.6 万', '2.2 万')][('92.70M', '1108 万', '9167', '1.3 万')][('68.94M', '1072 万', '5718', '9869')][('61.45M', '935 万', '1.1 万', '1.6 万')][('23.96M', '925 万', '4157', '1956')]
然后利用 result[0]、result[1] 等分别提取出四项信息,以 volume 为例,输出第一页的提取结果:
item['volume'] = results[0]print(item['volume'])21.74M75.53M46.21M54.77M3.32M75.07M92.70M68.94M61.45M23.96M
这样一来,第一页 10 款 App 的所有字段信息都被成功提取出来,然后返回到 yied item 生成器中,我们输出一下它的内容:
[{'name': '酷安', 'volume': '21.74M', 'download': '5218 万', 'follow': '2.4 万', 'comment': '5.4 万', 'tags': "['酷市场', '酷安', '市场', 'coolapk', '装机必备']", 'score': '4.4', 'num_score': '1.4 万'},{'name': '微信', 'volume': '75.53M', 'download': '2768 万', 'follow': '2.3 万', 'comment': '3.0 万', 'tags': "['微信', 'qq', '腾讯', 'tencent', '即时聊天', '装机必备']",'score': '2.3', 'num_score': '1.1 万'},...]
以上,我们爬取了第一页内容,接下去需要遍历爬取全部 610 页的内容,这里有两种思路:
这里,我们分别写出两种方法的解析代码。
第一种方法很简单,直接接着 parse 方法继续添加以下几行代码即可:
def parse(self, response):contents = response.css('.app_left_list>a')for content in contents:...next_page = response.css('.pagination li:nth-child(8) a::attr(href)').extract_first()url = response.urljoin(next_page)yield scrapy.Request(url,callback=self.parse )
第二种方法,我们在最开头的 parse() 方法前,定义一个 start_requests() 方法,用来批量生成 610 页的 URL,然后通过 scrapy.Request() 方法中的 callback 参数,传递给下面的 parse() 方法进行解析。
def start_requests(self):pages = []for page in range(1,610): # 一共有 610 页url = 'https://www.coolapk.com/apk/?page=%s'%pagepage = scrapy.Request(url,callback=self.parse)pages.append(page)return pages
以上就是全部页面的爬取思路,爬取成功后,我们需要存储下来。这里,我面选择存储到 MongoDB 中,不得不说,相比 MySQL,MongoDB 要方便省事很多。
我们在 pipelines.py 程序中,定义数据存储方法,MongoDB 的一些参数,比如地址和数据库名称,需单独存放在 settings.py 设置文件中去,然后在 pipelines 程序中进行调用即可。
import pymongoclass MongoPipeline(object):def __init__(self,mongo_url,mongo_db):self.mongo_url = mongo_urlself.mongo_db = mongo_db@classmethoddef from_crawler(cls,crawler):return cls(mongo_url = crawler.settings.get('MONGO_URL'),mongo_db = crawler.settings.get('MONGO_DB'))def open_spider(self,spider):self.client = pymongo.MongoClient(self.mongo_url)self.db = self.client[self.mongo_db]def process_item(self,item,spider):name = item.__class__.__name__self.db[name].insert(dict(item))return itemdef close_spider(self,spider):self.client.close()
首先,我们定义一个 MongoPipeline()存储类,里面定义了几个方法,简单进行一下说明:
from crawler() 是一个类方法,用 @class method 标识,这个方法的作用主要是用来获取我们在 settings.py 中设置的这几项参数:
MONGO_URL = 'localhost'MONGO_DB = 'KuAn'ITEM_PIPELINES = {'kuan.pipelines.MongoPipeline': 300,}
open_spider() 方法主要进行一些初始化操作 ,在 Spider 开启时,这个方法就会被调用 。
process_item() 方法是最重要的方法,实现插入数据到 MongoDB 中。

完成上述代码以后,输入下面一行命令就可以开始整个爬虫的抓取和存储过程了,单机跑的话,6000 个网页需要不少时间才能完成,保持耐心。
scrapy crawl kuan
这里,还有两点补充:
第一,为了减轻网站压力,我们最好在每个请求之间设置几秒延时,可以在 KuspiderSpider() 方法开头出,加入以下几行代码:
custom_settings = {"DOWNLOAD_DELAY": 3, # 延迟 3s,默认是 0,即不延迟"CONCURRENT_REQUESTS_PER_DOMAIN": 8 # 每秒默认并发 8 次,可适当降低}
第二,为了更好监控爬虫程序运行,有必要 设置输出日志文件,可以通过 Python 自带的 logging 包实现:
import logginglogging.basicConfig(filename='kuan.log',filemode='w',level=logging.WARNING,format='%(asctime)s %(message)s',datefmt='%Y/%m/%d %I:%M:%S %p')logging.warning("warn message")logging.error("error message")
这里的 level 参数表示警告级别,严重程度从低到高分别是:DEBUG < INFO < WARNING < ERROR < CRITICAL,如果想日志文件不要记录太多内容,可以设置高一点的级别,这里设置为 WARNING,意味着只有 WARNING 级别以上的信息才会输出到日志中去。
添加 datefmt 参数是为了在每条日志前面加具体的时间,这点很有用处。

以上,我们就完成了整个数据的抓取,有了数据我们就可以着手进行分析,不过这之前还需简单地对数据做一下清洗和处理。
首先,我们从 MongoDB 中读取数据并转化为 DataFrame,然后查看一下数据的基本情况。
def parse_kuan():client = pymongo.MongoClient(host='localhost', port=27017)db = client['KuAn']collection = db['KuAnItem']# 将数据库数据转为 DataFramedata = pd.DataFrame(list(collection.find()))print(data.head())print(df.shape)print(df.info())print(df.describe())

从 data.head() 输出的前 5 行数据中可以看到,除了 score 列是 float 格式以外,其他列都是 object 文本类型。
comment、download、follow、num_score 这 5 列数据中部分行带有「万」字后缀,需要将字符去掉再转换为数值型;volume 体积列,则分别带有「M」和「K」后缀,为了统一大小,则需将「K」除以 1024,转换为 「M」体积。
整个数据一共有 6086 行 x 8 列,每列均没有缺失值。
df.describe() 方法对 score 列做了基本统计,可以看到,所有 App 的平均得分是 3.9 分(5 分制),最低得分 1.6 分,最高得分 4.8 分。
下面,我们将以上几列文本型数据转换为数值型数据,代码实现如下:
def data_processing(df):#处理'comment','download','follow','num_score','volume' 5 列数据,将单位万转换为单位 1,再转换为数值型str = '_ori'cols = ['comment','download','follow','num_score','volume']for col in cols:colori = col+strdf[colori] = df[col] # 复制保留原始列if not (col == 'volume'):df[col] = clean_symbol(df,col)# 处理原始列生成新列else:df[col] = clean_symbol2(df,col)# 处理原始列生成新列# 将 download 单独转换为万单位df['download'] = df['download'].apply(lambda x:x/10000)# 批量转为数值型df = df.apply(pd.to_numeric,errors='ignore')def clean_symbol(df,col):# 将字符“万”替换为空con = df[col].str.contains('万$')df.loc[con,col] = pd.to_numeric(df.loc[con,col].str.replace('万','')) * 10000df[col] = pd.to_numeric(df[col])return df[col]def clean_symbol2(df,col):# 字符 M 替换为空df[col] = df[col].str.replace('M$','')# 体积为 K 的除以 1024 转换为 Mcon = df[col].str.contains('K$')df.loc[con,col] = pd.to_numeric(df.loc[con,col].str.replace('K$',''))/1024df[col] = pd.to_numeric(df[col])return df[col]
以上,就完成了几列文本型数据的转换,我们再来查看一下基本情况:
| comment | download | follow | num_score | score | volume |
|---|---|---|---|---|---|
| count | 6086 | 6086 | 6086 | 6086 | 6086 |
| mean | 255.5 | 13.7 | 729.3 | 133.1 | 3.9 |
| std | 1437.3 | 98 | 1893.7 | 595.4 | 0.6 |
| min | 0 | 0 | 0 | 1 | 1.6 |
| 25% | 16 | 0.2 | 65 | 5.2 | 3.7 |
| 50% | 38 | 0.8 | 180 | 17 | 4 |
| 75% | 119 | 4.5 | 573.8 | 68 | 4.3 |
| max | 53000 | 5190 | 38000 | 17000 | 4.8 |
从中可以看出以下几点信息:
以上,就完成了基本的数据清洗处理过程,下面一篇文章我们将对数据进行探索性分析。
我们主要从总体和分类两个维度对 App 下载量、评分、体积等指标进行分析。
首先来看一下 App 的下载量情况,很多时候我们下载一个 App ,下载量是一个非常重要的参考指标,由于绝大多数 App 的下载量都相对较少,直方图无法看出趋势,所以我们择将数据进行分段,离散化为柱状图,绘图工具采用的是 Pyecharts。
可以看到多达 5517 款(占总数 84%)App 的下载量不到 10 万, 而下载量超过 500 万的仅有 20 款,开发一个要想盈利的 App ,用户下载量尤为重要,从这一点来看,大部分 App 的处境都比较尴尬,至少是在酷安平台上。
代码实现如下:
from pyecharts import Bar# 下载量分布bins = [0,10,100,500,10000]group_names = ['<=10万','10-100万','100-500万','>500万']cats = pd.cut(df['download'],bins,labels=group_names) # 用 pd.cut() 方法进行分段cats = pd.value_counts(cats)bar = Bar('App 下载数量区间分布','绝大部分 App 下载量低于 10 万')# bar.use_theme('macarons')bar.add('App 数量 (个)',list(cats.index),list(cats.values),is_label_show = True,is_splitline_show = False,)bar.render(path='download_interval.png',pixel_ration=1)
接下来,我们看看 **下载量最多的 20 款 App **是哪些:

可以看到,这里「酷安」App 以 5000 万+ 次的下载量遥遥领先,是第二名微信 2700 万下载量的近两倍,这么巨大的优势也很容易理解,毕竟是自家的 App,如果你手机上没有「酷安」,说明你还不算是一个真正的「搞机爱好者」,从图中我们还可以看出以下几点信息:
作为对比,我们再看一下 下载量最少的 20 个 App。

可以看到,与上面的那些下载量多的 App 相比,这些就相形见绌了,下载量最少的 「广州限行通」更是只有 63 次下载。
这也不奇怪,可能是 App 没有宣传、也可能是刚开发出来,这么少的下载量评分还不错,也还能继续更新,为这些开发者点赞。
其实,这类 App 不算囧,真正囧的应该是那些 下载量很多、评分却低到不能再低 的 App,给人的感觉是:「我就这么烂,爱咋咋地有本事别用」。
接下来,我们看看 App 的总体得分情况。这里,将得分分为了以下 4 个区间段,并且为不同分数定义了相应的等级。
可以发现这么几点有意思的现象:
接下来,我们看看评分最高的 20 款 App 有哪些,很多时候我们下载 App 都是跟着「哪个评分高,下载哪个」这种感觉走。

可以看到,评分最高的 20 个 App,它们都得到了 4.8 分 ,包括:RE 管理器(再次出现)、Pure 轻雨图标包等,还有一些不太常见,可能这些都是不错的 App,不过我们还需要结合看一下下载量,它们的下载量都在 1 万以上,有了一定的下载量,评分才算比较可靠,我们就能放心的下载下来体验一下了。
经过上面的总体分析,可以发现一些不错的 App ,还可以通过类别细分发现每个类别下的精品 APP,受篇幅限制不再罗列。
文中完整代码和素材,可以在这里获取:
本文初步使用 Scrapy 框架完成了一个爬虫项目,接下来我们会通过另一个实战项目来详细了解 Scrapy 各个功能模块的使用。
本文完。