[关闭]
@wade123 2019-04-29T01:07:42.000000Z 字数 12237 阅读 1162

实战项目 9:Scrapy 框架爬取分析酷安网 6000 款App

Python入门爬虫与数据分析


摘要: 使用 Scrapy 框架爬取分析酷安网 App 信息。

上一个教程,我们使用 pyspider 框架爬取了虎嗅文章,它的整体功能还是单薄,更强大常用的框架是 Scrapy,本文使用该框架爬取分析著名手机应用市场——酷安网 6000 款App 信息。

本文知识点:

1. Scrapy介绍

相对于 Pyspider ,Scrapy 框架要复杂一些,有不同的处理模块,项目文件也由好几个程序组成,不同的爬虫模块需要放在不同的程序中去,所以刚开始入门会觉得程序七零八散,容易把人搞晕,先大概了解一下 Scrapy 的架构和代码编写方式

Scrapy 框架的架构,如下图所示:

img

它可以分为如下的几个部分。

项目文件结构如下:

  1. project/
  2. spiders/
  3. __init__.py
  4. spiders.py
  5. __init__.py
  6. items.py
  7. pipelines.py
  8. settings.py
  9. middlewares.py

最重要的文件是下面这几个:

经过上面的介绍,你可能还是不太明白 Scarpy 怎么使用,下面我们就来实战, 抓取酷安网页端 6000 App 信息。

2. 目标网站

观察目标网站可以发现两点有用信息:

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

通过上述分析,我们就可以确定抓取流程了,首先遍历主页面 ,抓取 10 个 App 的详情页 URL,然后详情页再抓取每个 App 的指标,如此遍历下来,我们需要抓取 6000 个左右网页内容,抓取工作量不算小,所以,我们接下来尝试使用 Scrapy 框架进行抓取。

3. 数据抓取

首先安装好 Scrapy 框架,如果是 Windwos 系统,且已经安装了 Anaconda,那么安装 Scrapy 框架就非常简单,只需打开 Anaconda Prompt 命令窗口,输入下面一句命令即可,会自动帮我们安装好 Scrapy 所有需要安装和依赖的库。

  1. conda pip scrapy

3.1. 创建项目

接着,我们需要创建一个爬虫项目,所以我们先从根目录切换到需要放置项目的工作路径,比如我这里设置的存放路径为:E:\my_Python\training\kuan,接着继续输入下面一行代码即可创建 kuan 爬虫项目:

  1. # 切换工作路径
  2. e:
  3. cd E:\my_Python\training\kuan
  4. # 生成项目
  5. scrapy startproject kuspider

执行上面的命令后,就会生成一个名为 kuan 的 scrapy 爬虫项目,包含以下几个文件:

  1. scrapy. cfg # Scrapy 部署时的配置文件
  2. kuan # 项目的模块,需要从这里引入
  3. _init__.py
  4. items.py # 定义爬取的数据结构
  5. middlewares.py # Middlewares 中间件
  6. pipelines.py # 数据管道文件,可用于后续存储
  7. settings.py # 配置文件
  8. spiders # 爬取主程序文件夹
  9. _init_.py

下面,我们需要再 spiders 文件夹中创建一个爬取主程序:kuan.py,接着运行下面两行命令即可:

  1. cd kuan # 进入刚才生成的 kuan 项目文件夹
  2. scrapy genspider kuan www.coolapk.com # 生成爬虫主程序文件 kuan.py

3.2. 声明 item

项目文件创建好以后,我们就可以开始写爬虫程序了。

首先,需要在 items.py 文件中,预先定义好要爬取的字段信息名称,如下所示:

  1. class KuanItem(scrapy.Item):
  2. # define the fields for your item here like:
  3. name = scrapy.Field()
  4. volume = scrapy.Field()
  5. download = scrapy.Field()
  6. follow = scrapy.Field()
  7. comment = scrapy.Field()
  8. tags = scrapy.Field()
  9. score = scrapy.Field()
  10. num_score = scrapy.Field()

这里的字段信息就是我们前面在网页中定位的 8 个字段信息,包括:name 表示 App 名称、volume 表示体积、download 表示 下载数量。在这里定义好之后,我们在后续的爬取主程序中会利用到这些字段信息。

3.3. 爬取主程序

创建好 kuan 项目后,Scrapy 框架会自动生成爬取的部分代码,我们接下来就需要在 parse 方法中增加网页抓取的字段解析内容。

  1. class KuspiderSpider(scrapy.Spider):
  2. name = 'kuan'
  3. allowed_domains = ['www.coolapk.com']
  4. start_urls = ['http://www.coolapk.com/']
  5. def parse(self, response):
  6. 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 中的写法:

  1. # 常规写法
  2. url = item('.app_left_list>a').attr('href')
  3. name = item('.list_app_title').text()
  4. # Scrapy 写法
  5. url = item.css('::attr("href")').extract_first()
  6. name = item.css('.detail_app_title::text').extract_first()

可以看到,要获取 href 或者 text 属性,需要用 :: 表示,比如获取 text,则用 ::text。extract_first() 表示提取第一个元素,如果有多个元素,则用 extract() 。接着,我们就可以参照写出 8 个字段信息的解析代码。

首先,我们需要在主页提取 App 的 URL 列表,然后再进入每个 App 的详情页进一步提取 8 个字段信息。

  1. def parse(self, response):
  2. contents = response.css('.app_left_list>a')
  3. for content in contents:
  4. url = content.css('::attr("href")').extract_first()
  5. url = response.urljoin(url) # 拼接相对 url 为绝对 url
  6. yield scrapy.Request(url,callback=self.parse_url)

这里,利用 response.urljoin() 方法将提取出的相对 URL 拼接为完整的 URL,然后利用 scrapy.Request() 方法构造每个 App 详情页的请求,这里我们传递两个参数:url 和 callback,url 为详情页 URL,callback 是回调函数,它将主页 URL 请求返回的响应 response 传给专门用来解析字段内容的 parse_url() 方法,如下所示:

  1. def parse_url(self,response):
  2. item = KuanItem()
  3. item['name'] = response.css('.detail_app_title::text').extract_first()
  4. results = self.get_comment(response)
  5. item['volume'] = results[0]
  6. item['download'] = results[1]
  7. item['follow'] = results[2]
  8. item['comment'] = results[3]
  9. item['tags'] = self.get_tags(response)
  10. item['score'] = response.css('.rank_num::text').extract_first()
  11. num_score = response.css('.apk_rank_p1::text').extract_first()
  12. item['num_score'] = re.search('共(.*?)个评分',num_score).group(1)
  13. yield item
  14. def get_comment(self,response):
  15. messages = response.css('.apk_topba_message::text').extract_first()
  16. result = re.findall(r'\s+(.*?)\s+/\s+(.*?)下载\s+/\s+(.*?)人关注\s+/\s+(.*?)个评论.*?',messages) # \s+ 表示匹配任意空白字符一次以上
  17. if result: # 不为空
  18. results = list(result[0]) # 提取出 list 中第一个元素
  19. return results
  20. def get_tags(self,response):
  21. data = response.css('.apk_left_span2')
  22. tags = [item.css('::text').extract_first() for item in data]
  23. return tags

这里,单独定义了 get_comment() 和 get_tags() 两个方法.

get_comment() 方法通过正则匹配提取 volume、download、follow、comment 四个字段信息,正则匹配结果如下:

  1. result = re.findall(r'\s+(.*?)\s+/\s+(.*?)下载\s+/\s+(.*?)人关注\s+/\s+(.*?)个评论.*?',messages)
  2. print(result) # 输出第一页的结果信息
  3. # 结果如下:
  4. [('21.74M', '5218 万', '2.4 万', '5.4 万')]
  5. [('75.53M', '2768 万', '2.3 万', '3.0 万')]
  6. [('46.21M', '1686 万', '2.3 万', '3.4 万')]
  7. [('54.77M', '1603 万', '3.8 万', '4.9 万')]
  8. [('3.32M', '1530 万', '1.5 万', '3343')]
  9. [('75.07M', '1127 万', '1.6 万', '2.2 万')]
  10. [('92.70M', '1108 万', '9167', '1.3 万')]
  11. [('68.94M', '1072 万', '5718', '9869')]
  12. [('61.45M', '935 万', '1.1 万', '1.6 万')]
  13. [('23.96M', '925 万', '4157', '1956')]

然后利用 result[0]、result[1] 等分别提取出四项信息,以 volume 为例,输出第一页的提取结果:

  1. item['volume'] = results[0]
  2. print(item['volume'])
  3. 21.74M
  4. 75.53M
  5. 46.21M
  6. 54.77M
  7. 3.32M
  8. 75.07M
  9. 92.70M
  10. 68.94M
  11. 61.45M
  12. 23.96M

这样一来,第一页 10 款 App 的所有字段信息都被成功提取出来,然后返回到 yied item 生成器中,我们输出一下它的内容:

  1. [
  2. {'name': '酷安', 'volume': '21.74M', 'download': '5218 万', 'follow': '2.4 万', 'comment': '5.4 万', 'tags': "['酷市场', '酷安', '市场', 'coolapk', '装机必备']", 'score': '4.4', 'num_score': '1.4 万'},
  3. {'name': '微信', 'volume': '75.53M', 'download': '2768 万', 'follow': '2.3 万', 'comment': '3.0 万', 'tags': "['微信', 'qq', '腾讯', 'tencent', '即时聊天', '装机必备']",'score': '2.3', 'num_score': '1.1 万'},
  4. ...
  5. ]

3.4. 分页爬取

以上,我们爬取了第一页内容,接下去需要遍历爬取全部 610 页的内容,这里有两种思路:

这里,我们分别写出两种方法的解析代码。

第一种方法很简单,直接接着 parse 方法继续添加以下几行代码即可:

  1. def parse(self, response):
  2. contents = response.css('.app_left_list>a')
  3. for content in contents:
  4. ...
  5. next_page = response.css('.pagination li:nth-child(8) a::attr(href)').extract_first()
  6. url = response.urljoin(next_page)
  7. yield scrapy.Request(url,callback=self.parse )

第二种方法,我们在最开头的 parse() 方法前,定义一个 start_requests() 方法,用来批量生成 610 页的 URL,然后通过 scrapy.Request() 方法中的 callback 参数,传递给下面的 parse() 方法进行解析。

  1. def start_requests(self):
  2. pages = []
  3. for page in range(1,610): # 一共有 610 页
  4. url = 'https://www.coolapk.com/apk/?page=%s'%page
  5. page = scrapy.Request(url,callback=self.parse)
  6. pages.append(page)
  7. return pages

以上就是全部页面的爬取思路,爬取成功后,我们需要存储下来。这里,我面选择存储到 MongoDB 中,不得不说,相比 MySQL,MongoDB 要方便省事很多。

3.5. 存储结果

我们在 pipelines.py 程序中,定义数据存储方法,MongoDB 的一些参数,比如地址和数据库名称,需单独存放在 settings.py 设置文件中去,然后在 pipelines 程序中进行调用即可。

  1. import pymongo
  2. class MongoPipeline(object):
  3. def __init__(self,mongo_url,mongo_db):
  4. self.mongo_url = mongo_url
  5. self.mongo_db = mongo_db
  6. @classmethod
  7. def from_crawler(cls,crawler):
  8. return cls(
  9. mongo_url = crawler.settings.get('MONGO_URL'),
  10. mongo_db = crawler.settings.get('MONGO_DB')
  11. )
  12. def open_spider(self,spider):
  13. self.client = pymongo.MongoClient(self.mongo_url)
  14. self.db = self.client[self.mongo_db]
  15. def process_item(self,item,spider):
  16. name = item.__class__.__name__
  17. self.db[name].insert(dict(item))
  18. return item
  19. def close_spider(self,spider):
  20. self.client.close()

首先,我们定义一个 MongoPipeline()存储类,里面定义了几个方法,简单进行一下说明:

from crawler() 是一个类方法,用 @class method 标识,这个方法的作用主要是用来获取我们在 settings.py 中设置的这几项参数:

  1. MONGO_URL = 'localhost'
  2. MONGO_DB = 'KuAn'
  3. ITEM_PIPELINES = {
  4. 'kuan.pipelines.MongoPipeline': 300,
  5. }

open_spider() 方法主要进行一些初始化操作 ,在 Spider 开启时,这个方法就会被调用 。

process_item() 方法是最重要的方法,实现插入数据到 MongoDB 中。

完成上述代码以后,输入下面一行命令就可以开始整个爬虫的抓取和存储过程了,单机跑的话,6000 个网页需要不少时间才能完成,保持耐心。

  1. scrapy crawl kuan

这里,还有两点补充:

第一,为了减轻网站压力,我们最好在每个请求之间设置几秒延时,可以在 KuspiderSpider() 方法开头出,加入以下几行代码:

  1. custom_settings = {
  2. "DOWNLOAD_DELAY": 3, # 延迟 3s,默认是 0,即不延迟
  3. "CONCURRENT_REQUESTS_PER_DOMAIN": 8 # 每秒默认并发 8 次,可适当降低
  4. }

第二,为了更好监控爬虫程序运行,有必要 设置输出日志文件,可以通过 Python 自带的 logging 包实现:

  1. import logging
  2. logging.basicConfig(filename='kuan.log',filemode='w',level=logging.WARNING,format='%(asctime)s %(message)s',datefmt='%Y/%m/%d %I:%M:%S %p')
  3. logging.warning("warn message")
  4. logging.error("error message")

这里的 level 参数表示警告级别,严重程度从低到高分别是:DEBUG < INFO < WARNING < ERROR < CRITICAL,如果想日志文件不要记录太多内容,可以设置高一点的级别,这里设置为 WARNING,意味着只有 WARNING 级别以上的信息才会输出到日志中去。

添加 datefmt 参数是为了在每条日志前面加具体的时间,这点很有用处。

以上,我们就完成了整个数据的抓取,有了数据我们就可以着手进行分析,不过这之前还需简单地对数据做一下清洗和处理。

4. 数据清洗处理

首先,我们从 MongoDB 中读取数据并转化为 DataFrame,然后查看一下数据的基本情况。

  1. def parse_kuan():
  2. client = pymongo.MongoClient(host='localhost', port=27017)
  3. db = client['KuAn']
  4. collection = db['KuAnItem']
  5. # 将数据库数据转为 DataFrame
  6. data = pd.DataFrame(list(collection.find()))
  7. print(data.head())
  8. print(df.shape)
  9. print(df.info())
  10. 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 分。

下面,我们将以上几列文本型数据转换为数值型数据,代码实现如下:

  1. def data_processing(df):
  2. #处理'comment','download','follow','num_score','volume' 5 列数据,将单位万转换为单位 1,再转换为数值型
  3. str = '_ori'
  4. cols = ['comment','download','follow','num_score','volume']
  5. for col in cols:
  6. colori = col+str
  7. df[colori] = df[col] # 复制保留原始列
  8. if not (col == 'volume'):
  9. df[col] = clean_symbol(df,col)# 处理原始列生成新列
  10. else:
  11. df[col] = clean_symbol2(df,col)# 处理原始列生成新列
  12. # 将 download 单独转换为万单位
  13. df['download'] = df['download'].apply(lambda x:x/10000)
  14. # 批量转为数值型
  15. df = df.apply(pd.to_numeric,errors='ignore')
  16. def clean_symbol(df,col):
  17. # 将字符“万”替换为空
  18. con = df[col].str.contains('万$')
  19. df.loc[con,col] = pd.to_numeric(df.loc[con,col].str.replace('万','')) * 10000
  20. df[col] = pd.to_numeric(df[col])
  21. return df[col]
  22. def clean_symbol2(df,col):
  23. # 字符 M 替换为空
  24. df[col] = df[col].str.replace('M$','')
  25. # 体积为 K 的除以 1024 转换为 M
  26. con = df[col].str.contains('K$')
  27. df.loc[con,col] = pd.to_numeric(df.loc[con,col].str.replace('K$',''))/1024
  28. df[col] = pd.to_numeric(df[col])
  29. 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

从中可以看出以下几点信息:

以上,就完成了基本的数据清洗处理过程,下面一篇文章我们将对数据进行探索性分析。

5. 数据分析

我们主要从总体和分类两个维度对 App 下载量、评分、体积等指标进行分析。

5.1. 下载量排名

首先来看一下 App 的下载量情况,很多时候我们下载一个 App ,下载量是一个非常重要的参考指标,由于绝大多数 App 的下载量都相对较少,直方图无法看出趋势,所以我们择将数据进行分段,离散化为柱状图,绘图工具采用的是 Pyecharts。

可以看到多达 5517 款(占总数 84%)App 的下载量不到 10 万, 而下载量超过 500 万的仅有 20 款,开发一个要想盈利的 App ,用户下载量尤为重要,从这一点来看,大部分 App 的处境都比较尴尬,至少是在酷安平台上。

代码实现如下:

  1. from pyecharts import Bar
  2. # 下载量分布
  3. bins = [0,10,100,500,10000]
  4. group_names = ['<=10万','10-100万','100-500万','>500万']
  5. cats = pd.cut(df['download'],bins,labels=group_names) # 用 pd.cut() 方法进行分段
  6. cats = pd.value_counts(cats)
  7. bar = Bar('App 下载数量区间分布','绝大部分 App 下载量低于 10 万')
  8. # bar.use_theme('macarons')
  9. bar.add(
  10. 'App 数量 (个)',
  11. list(cats.index),
  12. list(cats.values),
  13. is_label_show = True,
  14. is_splitline_show = False,
  15. )
  16. bar.render(path='download_interval.png',pixel_ration=1)

接下来,我们看看 **下载量最多的 20 款 App **是哪些:

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

作为对比,我们再看一下 下载量最少的 20 个 App

可以看到,与上面的那些下载量多的 App 相比,这些就相形见绌了,下载量最少的 「广州限行通」更是只有 63 次下载

这也不奇怪,可能是 App 没有宣传、也可能是刚开发出来,这么少的下载量评分还不错,也还能继续更新,为这些开发者点赞。

其实,这类 App 不算囧,真正囧的应该是那些 下载量很多、评分却低到不能再低 的 App,给人的感觉是:「我就这么烂,爱咋咋地有本事别用」。

5.2. 评分排名

接下来,我们看看 App 的总体得分情况。这里,将得分分为了以下 4 个区间段,并且为不同分数定义了相应的等级。

可以发现这么几点有意思的现象:

接下来,我们看看评分最高的 20 款 App 有哪些,很多时候我们下载 App 都是跟着「哪个评分高,下载哪个」这种感觉走。

可以看到,评分最高的 20 个 App,它们都得到了 4.8 分 ,包括:RE 管理器(再次出现)、Pure 轻雨图标包等,还有一些不太常见,可能这些都是不错的 App,不过我们还需要结合看一下下载量,它们的下载量都在 1 万以上,有了一定的下载量,评分才算比较可靠,我们就能放心的下载下来体验一下了。

经过上面的总体分析,可以发现一些不错的 App ,还可以通过类别细分发现每个类别下的精品 APP,受篇幅限制不再罗列。

文中完整代码和素材,可以在这里获取:

https://github.com/makcyun/web_scraping_with_python/tree/master/Scrapy%20%E6%A1%86%E6%9E%B6%E7%88%AC%E5%8F%96%E5%88%86%E6%9E%90%E9%85%B7%E5%AE%89%E7%BD%91%206000%20%E6%AC%BEApp

本文初步使用 Scrapy 框架完成了一个爬虫项目,接下来我们会通过另一个实战项目来详细了解 Scrapy 各个功能模块的使用。

本文完。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注