V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
caneman
V2EX  ›  Python

Scrapy 效率瓶颈

  •  
  •   caneman · 2019-05-05 14:53:07 +08:00 · 5073 次点击
    这是一个创建于 2029 天前的主题,其中的信息可能已经有所发展或是发生改变。

    现在在爬的一个站点,有一个起始链接,后续所有的链接都是通过上一链接返回的 response 来产生的, (可以理解为从返回的 response 里面提取到下一页的链接)

    现在的问题是这样写好的爬虫,感觉是不是硬生生把并发搞成了单线程一样 我并发和线程数都调的很大,但是仍感觉速度很慢,大概每秒处理 2-5 个页面,一天也就只能抓 10-15W 的样子 感觉明显有问题

    我想问一下怎么样我才能提高我的抓取效率呢?(单机的情况下)

    这是我的一些配置 RETRY_ENABLED = 1 RETRY_TIMES = 2 DOWNLOAD_TIMEOUT = 15

    DOWNLOAD_DELAY = 0 CONCURRENT_REQUESTS = 100 CONCURRENT_REQUESTS_PER_DOMAIN = 100 CONCURRENT_REQUESTS_PER_IP = 100

    27 条回复    2019-05-06 20:41:48 +08:00
    InternetExplorer
        1
    InternetExplorer  
       2019-05-05 15:00:41 +08:00
    要考虑对方服务器的性能的呀,网站服务器的性能可能还没你的爬虫机器高。。。。
    caneman
        2
    caneman  
    OP
       2019-05-05 15:11:07 +08:00
    @InternetExplorer 我试了调并发数并没有显著的影响到我的抓取速率,而且对方的站是绝对扛得住的(是个大站)。
    这里我虽然写的很大,但是其实抓取频率并没有很高(所以才一直没改),而且抓取到的页面都是按顺序的,我觉得是不是我的抓取逻辑有问题,应该怎么样改善呢?
    locoz
        3
    locoz  
       2019-05-05 15:27:27 +08:00
    如果别人网站原本的翻页逻辑就是 [要根据上一页内容来得到下一页内容] 的,那你并发再高都没有用,跟 Scrapy 没有关系,如果要并发你只能是从分类之类的地方入手。(不过一般不都是这样爬么,直接计算页数爬的话很容易出现漏数据的情况)
    locoz
        4
    locoz  
       2019-05-05 15:28:15 +08:00
    @locoz #3 想了想这么说不太准确,“如果要并发” 换成“如果要提高效率”
    caneman
        5
    caneman  
    OP
       2019-05-05 15:37:55 +08:00
    @locoz 总共大概有 4000W 页面,如果我找到了这 4000W 页面的列表,我要写在 start_urls 里面才能实现高并发吗?
    之前没有接触过 Scrapy-redis,目前的情况是,单机,带宽还可以,IP/Cookie 等所有反爬措施均已解决,可以理解为网站无反爬站措施,这样的话,我该怎么样实现日抓百万呢?

    想到的一种可行的方案是,把所有的 url 写入 redis,然后所有的请求从 redis 里面去取 url, 但是单机的情况下,如何实现并发?(就是不是一个请求结束后再去 redis 取下一个,而是多个线程(并发数)同时连接 redis 去取 url,然后这些个线程同时进行抓取)不太清楚 scrapy-redis 有没有解决这个问题。。。
    snal123
        6
    snal123  
       2019-05-05 15:42:07 +08:00 via iPhone
    timeout_delay 调小 改成 10s 左右,默认 180s 很多时间浪费可能是代理质量很低造成的。
    snal123
        7
    snal123  
       2019-05-05 15:44:07 +08:00 via iPhone
    @snal123 说错了 看到你已经是 15 了 你没用 redis 的话我觉得还是这里的问题
    tozp
        8
    tozp  
       2019-05-05 15:46:10 +08:00
    首先爬虫不必过于追求效率;其次 Scrapy 执行效率是个问题,我现在都是用 Go 框架爬的;最后,Scrapy 你再怎么改也就那样。
    dingyaguang117
        9
    dingyaguang117  
       2019-05-05 15:57:39 +08:00
    LZ 你自己不是很清楚吗,下一页的 URL 是上一个的 response 里面读到的,这个肯定是串行啊。你得改变这种串行获取 url 的方式才行
    caneman
        10
    caneman  
    OP
       2019-05-05 15:57:44 +08:00
    @tozp 现在日抓 10-20W 级,有点跟不上需求,需求大概是日抓百万,但是不能分布式。。。不是不能用,是现在的问题是单机的性能远远的浪费了,无论是带宽还是性能,都远远的没有用到。
    tozp
        11
    tozp  
       2019-05-05 16:00:21 +08:00
    @caneman 和我之前差不多,用 Go 之后,每天差不多提升了 10 倍,2M-
    caneman
        12
    caneman  
    OP
       2019-05-05 16:03:04 +08:00
    @dingyaguang117 改变串行后呢,怎么提高效率,我总不能把 4000W 页面连接都写道 start_urls 里面吧?

    其实我现在是有点不太明白 scrapy 是实现并发的原理,网上也没有找到很好的解释文档。按我的理解,它是通过 start_urls 来实现并发的,任何在 parse 里面写的 yield 都会存在上面的串行问题。

    我能想到的是把 scrapy 和 redis 对接(单机对接),然后多个线程同时去取 url,然后同时去抓,关键是我不知道 scrapy 支不支持这种操作,也不知道能不能实现或者有没有现成的解决方案,以免重复造轮子或者根本就此路不通。。。

    不过好像上面这种想法又回到了 scrapy 是如何实现并发的问题上了。。。。
    dingyaguang117
        13
    dingyaguang117  
       2019-05-05 16:14:06 +08:00
    @caneman 当然要保证 队列里有足够的 url 够下载器消费啦,你可以 按照某些固定的规则放进去,保证足够的数量就行了
    你现在是每次队列里只有 1 个,你 100 个并发下载啥?
    caneman
        14
    caneman  
    OP
       2019-05-05 16:18:45 +08:00
    @dingyaguang117 谢谢,我觉得问题在这儿,但是这个规则怎么建立没想好,4000W 级别,还要涉及到失效错误链接的处理,请问 scrapy-redis 是不是能解决我的问题?
    AlloVince
        15
    AlloVince  
       2019-05-05 17:02:54 +08:00   ❤️ 1
    Scrapy 底层是 Twisted,Twisted 通过事件循环+线程池来实现异步 IO 的效果,LZ 所说的“并发数”,在 Scrapy 中是 CONCURRENT_REQUESTS, 其实只是传给 Twisted 的 Deferred 对象数量。由于 Twisted 只适用于单机环境,如果要增大 LZ 所说的“并发”数,可以调大 CONCURRENT_REQUESTS, 但显然“并发”数不可能无限增大,因为 Twisted 本身也存在限制

    一方面 Twisted 本身有 Queue 和线程池,在 Scrapy 中可以通过设置 Twisted 的 REACTOR_THREADPOOL_MAXSIZE 增大线程池线程数。

    另外 Twisted 主线程是单线程的,主线程达到瓶颈的话,再扩大线程池也没有意义。

    因此你可以认为单机环境下 Scrapy 的瓶颈 == Twisted 主线程处理上限。
    AlloVince
        16
    AlloVince  
       2019-05-05 17:14:51 +08:00   ❤️ 4
    关于 4000W url 如何调用 scrapy 爬取的问题,简单说可以将已知的 url 构建为`Request`, 然后`Spider.parse_start_url()` 中 `yield Request` 即可,所有待处理的 Request 会存入 Scheduler,Scheduler 的数据都存在内存,可以提前评估一下内存是否够存放所有的 url。

    scrapy-redis 实现的是将 Scheduler 的数据从内存改为 Redis, 一方面 redis 在进程崩溃后数据不会丢失,另一方面可以突破单机的限制,理论上有足够多的机器的话,再多的 URL 也可以同时请求。此时的瓶颈在 url -> Scheduler 生产者的生产速度
    caneman
        17
    caneman  
    OP
       2019-05-05 17:26:03 +08:00
    @AlloVince 非常感谢!您的这一番讲解能让我少走很多弯路,再次感谢!
    renmu123
        18
    renmu123  
       2019-05-05 22:22:32 +08:00 via Android
    先把所有的 URL 爬到手,一些能看出规律的就手动构造,然后就多多进程 /多线程 /异步,随便玩
    zhijiansha
        19
    zhijiansha  
       2019-05-05 23:23:26 +08:00
    @caneman #10 其实,上分布式,可以把 worker 都放在一台机子上的
    caneman
        20
    caneman  
    OP
       2019-05-06 10:30:29 +08:00
    @zhijiansha 这个思路挺好的,谢谢啊
    caneman
        21
    caneman  
    OP
       2019-05-06 10:34:07 +08:00
    @renmu123 现在能得到所有的 url 了,我想着怎么能用 scrapy 高效抓取,scrapy 这么多年了 这样一个成熟的框架应该不至于解决不了这种问题。想先单机把 scrapy 性能发挥到极致,了解他的极限和瓶颈在哪里,然后再上分布式再接着进一步优化,计划的学习路线是这样的。
    rocketman13
        22
    rocketman13  
       2019-05-06 11:45:04 +08:00
    scrapy 的 CrawlSpider 类?
    cxh116
        23
    cxh116  
       2019-05-06 15:50:16 +08:00
    数据是怎么保存的? 用的是同步还是异步调用.在 pipline 用同步阻塞方式去保存数据的话,会阻塞整个抓取调度的.

    https://leehodgkinson.com/blog/scrapy-pipelines/
    caneman
        24
    caneman  
    OP
       2019-05-06 17:54:27 +08:00
    @cxh116 是采用的异步 MySQL 存储的,很多页面是空数据的,所以瓶颈不在存储这一块,下面是主要代码。


    def start_requests(self):
    url = 'https://www.xxxx.com/'
    longitude, latitude = get_next_coordinate( self.start_longitude, self.start_latitude)
    data = get_form(longitude, latitude)
    proxy = 'http://' + get_proxy()
    yield FormRequest(url, method='POST', formdata=data, callback=self.parse, dont_filter=True, meta={'proxy':proxy,'download_timeout':3,'longitude':data['longitude'], 'latitude':data['latitude']})

    def parse(self, response):
    info_list = json.loads(response.text)
    if info_list['Count']:
    for item in info_list['list']:
    item_loader = QiyeItemloader(item=QiyeItem())
    item_loader.add_value('hash', item['Key'])
    item_loader.add_value('name', item['Name'])
    item_loader.add_value('longitude', response.meta['longitude'])
    item_loader.add_value('latitude', response.meta['latitude'])
    qiye_item= item_loader.load_item()
    yield qiye_item
    longitude, latitude = get_next_coordinate(response.meta['longitude'], response.meta['latitude'])
    next_data = get_form(longitude, latitude)
    yield FormRequest(response.url, method='POST', formdata = next_data, callback=self.parse, dont_filter=True, meta={'proxy':response.meta['proxy'],'download_timeout':3,'longitude':next_data['longitude'], 'latitude':next_data['latitude']})

    我想的一种解决方案是把所有 URL 放在 redis 里面,然后在 start_requests 里面 while True:yield Request()
    这样的问题我不知道我这样一直写会不会时间长了我的电脑就崩了。
    我如何控制这个被 yield 的 Request 的数量?比如,在队列里面一直有 100 个 Request,每少一个就添一个,始终保持 Start_url 里面有 100 个待爬 URL,这样的情况下,我调 CONCURRENT_REQUESTS 的值,是不是就能真正的控制并发数了?
    caneman
        25
    caneman  
    OP
       2019-05-06 18:02:54 +08:00
    cxh116
        26
    cxh116  
       2019-05-06 20:14:19 +08:00
    @caneman 你得确认瓶颈在什么地方?
    假如网页通过代理访问,60 秒才返回一个页面.这样就算你 1000 个并发. 1000 / 60 = 16.6 .这样算每秒最多也就是 16 个而已.

    假如网页解析比较费时,这个问题就更加不好解决.因为毕竟这种类似于阻塞的调用.


    你可以登录 telnet 用 est() 查看一下状态,分析一下原因 https://docs.scrapy.org/en/latest/topics/telnetconsole.html
    可以看一下 engine.scraper.slot.queue 的实现,这里应该可以取到你要的队列大小值.

    你还可以尝试用你自己的 redis 这种方案,启用多个进程,看看有没有提升.
    caneman
        27
    caneman  
    OP
       2019-05-06 20:41:48 +08:00 via iPhone
    @cxh116 谢谢,代理好像不是瓶颈,不加代理提升的速率也非常有限(大概就是去除了代理延迟级别的速度提升) redis 的那种方案确实提高了速率,是我之前写法太蠢了,所有的下一个页面链接都得等我上一个页面请求完毕才能获取,生生的变成了同步。(可是书上和网上都是这样来写的啊,寻找下一页的链接然后 yield ),不知道是我的理解问题,还是这样写本身就存在这种问题,我再多尝试尝试改一改,谢谢啦🙏。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2665 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 05:33 · PVG 13:33 · LAX 21:33 · JFK 00:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.