scrapy爬虫与数据分析实战

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为要爬取的网页地址
    既然思路已经明确,那让我们愉快的开始吧

    ContentSpider.py

    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())
    

    Items.py

    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.py

    from 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个小时才爬取完所有内容,鉴于篇幅问题,数据分析放到下一章中讲解.

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值