前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分布式全站爬虫——以"搜狗电视剧"为例

分布式全站爬虫——以"搜狗电视剧"为例

作者头像
小歪
发布2020-04-27 12:33:31
5880
发布2020-04-27 12:33:31
举报

先看看robots协议

User-agent: *
Allow: / 
Sitemap: http://kan.sogou.com/sitemap/sitemap.xml

牛皮!

分析

打开一个具体的影视:http://kan.sogou.com/player/181171191/,网址中有具体数字ID,我们假设数字ID就是递增的,即从1开始,那么我们可以拼接url:

  • http://kan.sogou.com/player/1/
  • http://kan.sogou.com/player/2/
  • http://kan.sogou.com/player/3/
  • http://kan.sogou.com/player/4/

有两个问题:

  1. ID上限是多少,我不可能永不止境的往上加吧
  2. 抓取效率问题,如果有10亿,那么我就需要发送10亿个HTTP请求,如果你开启了1000个线程,0.3秒能处理完一个ID,一天可以抓取:1000 * 0.3 * 60 * 60 * 24 = 25920000 个,需要约38天才能抓完,这肯定是不合乎要求的

针对这两个问题,可以用如下方法:

  1. 使用采样,比如我们确定间隔是1000,我们在1~1000中随机取数,在1001~2000中再随机取一个数,这样10亿数就被缩短为一百万了,这个数字就小多了
  2. 凭什么说上限是10亿呢,我们在真正爬虫之前还需要一次调研,调研的时候可以把间隔调大,比如5000,这次抓取只是为了评估ID分布范围,比如第一段是[1, 10000],第二段是[1000000, 9000000],最后一段是[10000000000, 10090000000]。确定ID分布范围后就可以在指定区间内采样抓取

代码

核心代码参考:generate_uid.py,该函数是主节点开启的一个线程,该线程会监控redis中爬虫start_urls队列,如果小于预期,调用生成器生成ID列表,加入到队列中,同时更新此时的ID到redis。

运行爬虫,命令是:scrapy crawl sougou -a master=True,日志样例如下:

2020-04-07 22:05:06 [scrapy.core.engine] INFO: Spider opened
2020-04-07 22:05:06 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2020-04-07 22:05:06 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2020-04-07 22:05:09 [sougou] INFO: spider sougou gen uid 40000001
2020-04-07 22:06:06 [scrapy.extensions.logstats] INFO: Crawled 3569 pages (at 3569 pages/min), scraped 0 items (at 0 items/min)
2020-04-07 22:07:06 [scrapy.extensions.logstats] INFO: Crawled 7034 pages (at 3465 pages/min), scraped 0 items (at 0 items/min)
2020-04-07 22:08:06 [scrapy.extensions.logstats] INFO: Crawled 10521 pages (at 3487 pages/min), scraped 0 items (at 0 items/min)
2020-04-07 22:09:06 [scrapy.extensions.logstats] INFO: Crawled 13849 pages (at 3328 pages/min), scraped 0 items (at 0 items/min)
2020-04-07 22:09:39 [sougou] INFO: success get useful id http://kan.sogou.com/updown.php?gid=181159677&op=get
2020-04-07 22:10:06 [scrapy.extensions.logstats] INFO: Crawled 17292 pages (at 3443 pages/min), scraped 0 items (at 0 items/min)

分布式

这是MS设计,master负责去向队列里添加任务,slave负责消费。(这里的master同样有消费)

开启master只需要向启动命令添加额外参数,slave启动方式正常即可。

注意:master只能开启一个,否则会有重复任务,slave开启多少个取决于机器、网络、代理条件。

思考

这种全量抓取方式只适合ID是数字的,这种网站还挺多的,淘宝、京东、知乎、美团等等。这些ID并不是递增,而是分布在一块块区域,先宏观上调查出大体范围,再缩小ID间隔。

但是有些网站,比如优酷的:https://v.youku.com/v_show/id_XNDU4OTM3NzM0NA==.html,id明显就是混淆过的,想要全量抓取只能通过分类接口去抓。这个有时间再聊聊。

线程安全

我们想想,这样设计会不会有线程安全?

先看看slave端,对redis的操作只有一个就是获取redis里的id,我们看看代码,scrapy_redis.spiders.RedisMixin.next_requests

    def next_requests(self):
        """Returns a request to be scheduled or none."""
        use_set = self.settings.getbool('REDIS_START_URLS_AS_SET', defaults.START_URLS_AS_SET)
        fetch_one = self.server.spop if use_set else self.server.lpop
        # XXX: Do we need to use a timeout here?
        found = 0
        # TODO: Use redis pipeline execution.
        while found < self.redis_batch_size:
            data = fetch_one(self.redis_key)
            if not data:
                # Queue empty.
                break
            req = self.make_request_from_data(data)
            if req:
                yield req
                found += 1
            else:
                self.logger.debug("Request not made from data: %r", data)

scrapy-redis使用使用list结构,所以这里我们用到的是lpop命令,多次去redis中获取request,直到found = self.redis_batch_size,每次从redis中获取request的数量如果没有设置,默认就是settings中的CONCURRENT_REQUESTS,一般就是16。

        if self.redis_batch_size is None:
            # TODO: Deprecate this setting (REDIS_START_URLS_BATCH_SIZE).
            self.redis_batch_size = settings.getint(
                'REDIS_START_URLS_BATCH_SIZE',
                settings.getint('CONCURRENT_REQUESTS'),
            )

每次从redis中获取到request后,会直接调用self.make_request_from_data(data)方法。作者在注释里也说到了这里的两个改进方法:

  1. Use timeout。redis的lpop是阻塞操作,所以理论上需要加上超时
  2. Use redis pipeline。如果不了解的可以看看文档,Redis 管道(Pipelining),

有心可以去提个pr,以后就可以吹牛说自己是scrapy-redis的contributors了,有人已经行动了,read start_urls use redis pipeline

因为lpop是原子操作,任何时候只会有单一线程从redis中拿到request,所以在获取request这一步是线程安全的。对这块不熟悉的可以阅读Redis 和 I/O 多路复用

再看看master端,有两个redis操作,

  1. 查询spider种子数量,使用llen
  2. 如果数量小于预期,生成任务ID,使用lpush插入数据

线程安全一般出现多线程之间的共享变量,这个场景下共享变量是什么,redis中的request列表吗,我仔细想了下,因为我们对redis的操作都保证原子性,并且插入的id保证不重复,所以不会出现问题。可以改进的地方,就是对master端使用redis pipeline操作。

欢迎交流想法。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python爬虫与算法进阶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先看看robots协议
    • 分析
      • 代码
        • 分布式
          • 思考
            • 线程安全
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档