python爬虫与数据分析实战
scrapy多爬虫与数据分析实战
本次爬虫选择的网站是博主一直在玩的一个网页游戏的论坛,爬取游戏相关的所有玩家交流贴子和所有参与用户的信息.
- 比较适合新手入门scrapy练手, 一共爬取了50000多篇帖子,和5000多名玩家信息.
- 网站没有做一些反爬措施,(如果你看了这篇文章,想要尝试一下,也要稍微设置一下延迟啦~~~)
爬虫思路
- 分析目标网址结构和网站源代码
- 创建爬虫项目,编写爬虫程序
1. 这次实战有2个scrapy爬虫,一个爬取用户信息,一个爬取帖子内容
2. 分别设置不同的ITEM_PIPELINES, 确保爬取的数据存放在不同的数据表中
3. 连接mysql数据库,设置相关配置
4. 设置爬虫全部同时启动命令 - 使用pandas和matplotlib进行数据分析
1. 制作帖子内容的词云图
2. 绘制玩家在线时长条形图
3. 绘制玩家创建账号时间与最后登录时间的折线图
4. 绘制玩家论坛等级的条形分布图
一.分析目标网站
目标网站
通过观察,可以看出
所有的帖子url都在每一页的源代码中, 所以可以直接通过解析函数爬取第一个start_url获取帖子url,和下一页的url,然后遍历每一个帖子url进行内容的抓取,完成后再把下一页的url传给解析函数.
点击其中的一个用户信息,我们发现每一个用户的个人信息url 都是由/space-uid-XXXXX.html结尾,很明显xxxx就是该用户的唯一标识uid,这样的话我们就可以使用scrapy中的crawl.spider进行url的正则匹配只要满足上诉条件都进行抓取(最开始想用requests做这个项目的时候,在这里本来打算构造url进行for循环,不幸的是uid从1到3000多w都有,而且部分uid的用户还不存在,果断放弃选择使用scrapy,虽然不能抓到所有的用户,但是活跃的用户都可以获取到)
二.创建爬虫项目,编写爬虫程序
通过第一步的观察,然后创建一个项目, 两个爬虫
scrapy startproject lequ
scrapy genspider LqSpider bbs.lequ.com
scrapy genspider -t crawl ContentSpider bbs.lequ.com 注: 指定-t参数为crawl 匹配特定url爬虫
接下来开始编写项目,我使用的是pycharm+python3
-
修改一些必要的配置项(其他的配置在写爬虫的时候再一起说)
a.关闭robots协议: 在settings.py中将 ROBOTSTXT_OBEY = True 的值改为False
b.设置访问延迟:在settings.py中将 #DOWNLOAD_DELAY = 3 取消注释并改为0.5
c.设置随机请求头: 在middlewares.py中 编写如下代码from fake_useragent import UserAgent # 随机请求头第三方库,pip install fake-useragent安装 class RandomUserAgent(object): def process_request(self, request, spider): """重写process_request方法,添加请求头信息 该方法是在爬虫访问url的时候调用, """ # 创建一个随机请求头类的对象, 他的random属性返回一个随机请求头信息 ua = UserAgent() request.headers['User-Agent'] = ua.random request.headers['Host'] = 'bbs.lequ.com'
d.打开下载中间件配置: 在settings.py中将 #DOWNLOADER_MIDDLEWARES 这个字典 取消注释
并将键值对改为’lequ.middlewares.RandomUserAgent’: 543 即使用我们自己重写的请求头类 -
编写第一个用户信息爬虫
经过第一步的分析,我们可以直接对start_url的解析结果进行xpath提取用户信息url和下一页url
然后对每一页的用户信息url进行遍历,将遍历每一个url传递给一个解析详情页面的函数
最后在把下一页的url传递给parse解析函数. 注:scrapy框架的爬虫默认第一个url即start_url的解析函数是parse方法
关于xpath语法这里就不多介绍了,如果有同学比较迷惑,可以私信或者留言,注: 在命令行执行scrapy shell url, 可以启动scrapy交互模式,可以测试xpath语法的正确或者错误,url为要爬取的网页地址
既然思路已经明确,那让我们愉快的开始吧import scrapy from scrapy.spiders import Spider from lequ.items import ContentItem import re class ContentSpider(Spider): name = 'content' allowed_domains = ['bbs.lequ.com'] # 改为网站第一页的url start_urls = ['http://bbs.lequ.com/forum-173-1.html'] # pipeline配置项,用来区分不同爬虫使用的pipeline的类 custom_settings = { 'ITEM_PIPELINES': {'lequ.pipelines.ContentPipeline': 300,} } def parse(self, response): # 提取当前页的所有帖子url存放在urls列表中, 注意当爬虫启动的时候首先会调用这个方法对start_url列表中的url进行爬取 # 所以如果爬取的网站需要登录, 需要在这个方法中实现 urls = response.xpath('//th[@class="common"]/a[1]/@href|//th[@class="new"]/a[1]/@href').getall() # 抓取下一页的url next_page = response.xpath('//a[@class="nxt"]/@href').get() for url in urls: # 遍历urls,对每一个帖子链接传递给parse_detail函数解析 yield scrapy.Request(url, callback=self.parse_detail) # 判断是否存在下一页url,一般情况下最后一页是没有下一页链接的 if next_page: # 将下一页的url返回给parse函数进行解析,可以当作是递归的思想 yield scrapy.Request(next_page, callback=self.parse) def parse_detail(self, response): name = response.xpath('//div[@class="authi"]/a/text()').get() content = response.xpath('//div[@id="postlist"]/div[@id][1]//td[@class="t_f"]/text()').getall() content = re.sub(r'\s', '', ''.join(content)) public_time = response.xpath('//em[@id][1]/text()').get().replace('发表于 ', '') read_count = int(response.xpath('//div[@class="hm ptn"]/span[2]/text()').get()) reply_count = int(response.xpath('//div[@class="hm ptn"]/span[5]/text()').get()) # 创建Item对象进行存放数据,博主一般喜欢先用xpath提取数据,在编写Item类, # 因为在提取的过程中可能还会有其他想法, 所以下面我们开始写Item类 item = ContentItem(name=name, content=content, public_time=public_time, read_count=read_count, reply_count=reply_count) yield item if __name__ == '__main__': from scrapy.cmdline import execute # 创建执行爬虫命令, execute需要传入一个列表,即['scrapy', 'crawl', ContentSpider.name] # 直接执行这个文件,就等于在命令行启动这个爬虫, 方便我们来测试爬虫 execute('scrapy crawl {}'.format(ContentSpider.name).split())
class ContentItem(scrapy.Item): # 这里定义scrapy存储字段,即想要存储哪些数据,就需要定义一个Fied(),要与爬虫文件中使用的变量名一致 # Item类似字典结构,可以使用字典的方法存储或者获取 name = scrapy.Field() content = scrapy.Field() public_time = scrapy.Field() read_count = scrapy.Field() reply_count = scrapy.Field()
-
编写第二个爬虫程序
经过第一步的分析得知,我们可以通过匹配每一页中所有符合条件(用户信息url规则)的url进行爬取
LqSpider.pyfrom scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from lequ.items import LequItem import re class LqspiderSpider(CrawlSpider): name = 'LqSpider' allowed_domains = ['bbs.lequ.com'] # 改为网站第一页的url start_urls = ['http://bbs.lequ.com/forum-173-1.html'] # pipeline配置项,用来区分不同爬虫使用的pipeline的类 custom_settings = { 'ITEM_PIPELINES': {'lequ.pipelines.LequPipeline': 300, } } # 将正则表达式的字符串形式编译为Pattern实例, 提高效率 pattern = re.compile(r'[\(\)();;“”\s]') # url匹配规则 rules = ( # 在start_url中爬取满足.*forum-173-\d+.html的url, follow=True为跟进匹配,即当爬取的url中还存在满足表达式的url时继续爬取 # 在这个规则中没有回调函数,是因为我们不需要解析页面内容,只需要获取到网页源码从中找出满足匹配规则的url即可 Rule(LinkExtractor(allow=r'.*forum-173-\d+.html'), follow=True), # 在上面的每一个url中爬取满足.*space-uid-\d+.html的url,并传递给parse_html解析 Rule(LinkExtractor(allow=r'.*space-uid-\d+.html'), callback='parse_html', follow=True), ) # 注意,在写上一个爬虫中讲过,爬虫开始的时候默认调用parse方法爬取start_url,CrawlSpider爬虫模板已经实现了该方法,并对rules规则匹配 def parse_html(self, response): name = response.xpath('//h2[@class="mt"]/text()').get() # 获取用户名 name = self.pattern.sub('', name) active_time = int(response.xpath('//ul[@id="pbbs"]/li[1]/text()').get().split()[0]) # 获取在线时长 create_time = response.xpath('//ul[@id="pbbs"]/li[2]/text()').get() # 获取创建时间 last_login = response.xpath('//ul[@id="pbbs"]/li[3]/text()').get() # 获取最后登录时间 last_activity = response.xpath('//ul[@id="pbbs"]/li[4]/text()').get() # 获取最后活跃时间 area = response.xpath('//ul[@id="pbbs"]/li[6]/text()').get() # 获取地址信息 area = self.pattern.sub('', area) identity = response.xpath('//span[contains(@style, "color")]//text()').getall()[-1] # 获取论坛等级 uid_str = response.xpath('//span[@class="xw0"]/text()').get() # 获取uid uid = int(re.search(r'UID: (\d+)', uid_str).group(1)) signature_info = response.xpath('//div[@class="pbm mbm bbda cl"]/ul[2]//td//text()').getall() # 获取个性签名 signature = ''.join(signature_info) if signature_info else '' signature = self.pattern.sub('', signature) infos = response.xpath('//ul[@class="cl bbda pbm mbm"]//a/text()').getall() friend_nums = int(infos[0].split()[-1]) # 获取好友数 reply_times = int(infos[1].split()[-1]) # 获取回复数 theme_nums = int(infos[2].split()[-1]) # 获取发帖数 forum_money = int(response.xpath('//div[@id="psts"]/ul[@class="pf_l"]/li[last()]/text()').get().strip()) # 获取论坛币数量 # 构造Item对象,存储数据 item = LequItem(name=name, active_time=active_time, create_time=create_time, last_login=last_login, last_activity=last_activity, area=area, identity=identity, uid=uid, signature=signature, friend_nums=friend_nums, reply_times=reply_times, theme_nums=theme_nums, forum_money=forum_money) yield item print('{}的用户信息已爬取'.format(name)) if __name__ == '__main__': from scrapy.cmdline import execute # 创建执行爬虫命令, execute需要传入一个列表,即['scrapy', 'crawl', LqspiderSpider.name] # 直接执行这个文件,就等于在命令行启动这个爬虫, 方便我们来测试爬虫 execute('scrapy crawl {}'.format(LqspiderSpider.name).split())
-
将数据存入数据库
我们需要保存数据到文本或者数据库中,方便我们后续的数据分析,这里使用的是存入mysql数据库
既然要保存数据,就需要用到pipeline
pipeline.py# 导入python与mysql交互的第三方库 import pymysql class MyPipeline(object): """ 因为两个爬虫需要存入到不同的表中,那么就需要两个pipeline类, 但是可以发现,两个pipeline有一些方法是相同的,比如连接数据库,创建游标,关闭数据库等, 那么可以自定义pipeline父类来实现这些操作,让这两个pipeline类继承于自定义的父类,简化代码 pipeline需要实现3个方法,open_spider用于爬虫启动的时候执行,process_item接收到数据的时候执行,close_spider爬虫停止的时候执行 """ def __init__(self): # 连接mysql数据库 self.conn = pymysql.Connect( host='localhost', # 数据库ip port=3306, # 数据库端口号 user='root', # 数据库用户名 passwd='XXXXX', # 数据库密码 db='lequ', # 要连接的数据库名 charset='utf8', # 编码 ) # 创建数据库游标操作数据库语句 self.cursor = self.conn.cursor() def open_spider(self, spider): pass def close_spider(self, spider): # 爬虫停止的时候关闭数据库 self.conn.close() print('{}爬虫已停止'.format(self.__class__)) class LequPipeline(MyPipeline): """用户信息内容爬虫Pipeline类""" def process_item(self, item, spider): # 取出item中的数据 name = item['name'] create_time = item['create_time'] active_time = item['active_time'] last_login = item['last_login'] last_activity = item['last_activity'] area = item['area'] signature = item['signature'] friend_nums = item['friend_nums'] reply_times = item['reply_times'] theme_nums = item['theme_nums'] identity = item['identity'] uid = item['uid'] forum_money = item['forum_money'] # 注意sql语句的写法,格式化的时候字符串类型的数据需要在{}外面加上引号(我不会告诉你,我之前没加,然后搞了几个小时才发现,哭....) sql = """insert into lequ_user (name, create_time, active_time, last_login, last_activity, area, signature, reply_times, theme_nums, identity, uid, forum_money, friend_nums) values('{}', '{}', {}, '{}', '{}', '{}', '{}', {}, {}, '{}', {}, {}, {})""" self.cursor.execute(sql.format(name, create_time, active_time, last_login, last_activity, area, signature, reply_times, theme_nums, identity, uid, forum_money, friend_nums)) self.conn.commit() class ContentPipeline(MyPipeline): """帖子内容爬虫Pipeline类""" def process_item(self, item, spider): name = item['name'] content = item['content'] public_time = item['public_time'] read_count = item['read_count'] reply_count = item['reply_count'] sql = """ insert into content (name, content, public_time, read_count, reply_count) values('{}', '{}', '{}', {}, {})""" self.cursor.execute(sql.format(name, content, public_time, read_count, reply_count)) self.conn.commit()
在settings.py中修改ITEM_PIPELINES = {
‘lequ.pipelines.LequPipeline’: 300,
‘lequ.pipelines.ContentPipeline’: 800,
} 即 使用自己定义的pipeline类保存数据,要于爬虫文件中的类属性custom_settings的值保持一致,这样才能保证数据存入不同的数据表中
OK, 到目前为止,我们已经写好两个scrapy爬虫,但是我们可能会想到一个问题,这样一个项目俩爬虫,还是要分开运行,和两个项目有什么区别阿,对哦, 我们还要写一个scrapy命令,让所有爬虫同时执行
首先在爬虫文件夹同级目录下创建commands的python包
然后创建crawlall.py的程序,可以直接把scrapy源代码中的commands代码复制进来,只改写run()即可
import os
from scrapy.commands import ScrapyCommand
from scrapy.utils.conf import arglist_to_dict
from scrapy.utils.python import without_none_values
from scrapy.exceptions import UsageError
class Command(ScrapyCommand):
requires_project = True
def syntax(self):
return "[options] <spider>"
def short_desc(self):
# 命令行执行scrapy -h 显示的描述
return "Run all spider"
def add_options(self, parser):
ScrapyCommand.add_options(self, parser)
parser.add_option("-a", dest="spargs", action="append", default=[], metavar="NAME=VALUE",
help="set spider argument (may be repeated)")
parser.add_option("-o", "--output", metavar="FILE",
help="dump scraped items into FILE (use - for stdout)")
parser.add_option("-t", "--output-format", metavar="FORMAT",
help="format to use for dumping items with -o")
def process_options(self, args, opts):
ScrapyCommand.process_options(self, args, opts)
try:
opts.spargs = arglist_to_dict(opts.spargs)
except ValueError:
raise UsageError("Invalid -a value, use -a NAME=VALUE", print_help=False)
if opts.output:
if opts.output == '-':
self.settings.set('FEED_URI', 'stdout:', priority='cmdline')
else:
self.settings.set('FEED_URI', opts.output, priority='cmdline')
feed_exporters = without_none_values(
self.settings.getwithbase('FEED_EXPORTERS'))
valid_output_formats = feed_exporters.keys()
if not opts.output_format:
opts.output_format = os.path.splitext(opts.output)[1].replace(".", "")
if opts.output_format not in valid_output_formats:
raise UsageError("Unrecognized output format '%s', set one"
" using the '-t' switch or as a file extension"
" from the supported list %s" % (opts.output_format,
tuple(valid_output_formats)))
self.settings.set('FEED_FORMAT', opts.output_format, priority='cmdline')
def run(self, args, opts):
# 获取爬虫列表
spd_loader_list = self.crawler_process.spider_loader.list() # 获取所有的爬虫文件。
print(spd_loader_list)
# 遍历各爬虫
for spname in spd_loader_list or args:
self.crawler_process.crawl(spname, **opts.spargs)
print('此时启动的爬虫为:' + spname)
self.crawler_process.start()
接下来新建cmd.py文件用来执行crawlall命令,或者可以直接再命令行下执行 scrapy crawlall
from scrapy.cmdline import execute
#启动这个文件就可以执行scrapy crawlall命令
execute('scrapy crawlall'.split())
最后还需要在settings.py中添加COMMANDS_MODULE = ‘lequ.commands’, 告诉scrapy自定义的命令在个目录
OK, 爬虫全部完成,可以开始愉快的抓取数据了,顺便提一下博主用了11个小时才爬取完所有内容,鉴于篇幅问题,数据分析放到下一章中讲解.