1 简介

最近接了个私活, 一同学在爬雪球网的数据时, 经常被雪球网以IP短时间内过多次访问为由要求输入验证码, 导致爬虫无法长时间运行. 一开始他的想法是用python的一些库自动识别验证码, 或者网上找一些人工输入验证码的服务来解决这个问题. 但是我看了一下雪球网的验证码还是挺复杂的, 肉眼看都不太好认, 自动识别应该是不行了. 至于人工打码服务, 看了一下他们的网站觉得整个一副”我有病毒, 我很山寨”的样子, 就完全不想用了. 于是我决定用HTTP代理来避免IP被禁就无法继续爬的问题.

经过一番调研, 国内提供稳定可靠的HTTP代理的服务基本没有. 国外倒是找到两个, 其中一个提供试用, 但是不知道为什么连不上他们的代理. 另一个最便宜也要$25/月, 钱倒是小事(反正不是我出), 但是不试用过不知道效果怎么样, 最终都放弃了. 另外还有个tor, 提供不定时的切换IP, 理论上来说是最适用我的需求的, 奈何一来连tor需要翻墙, 二来网速实在是慢. 最终决定试用网上免费提供的大陆高匿代理.

2 设计

scrapy提供下载中间件机制, 可以在请求队列与下载请求之间做一些动作. scrapy本身也提供了一个ProxyMiddleware, 但是它只能使用固定的IP地址, 由于免费的代理相当不稳定, 很多代理其实根本不能用. 因此需要对ProxyMiddleware改造使得这个middleware能够发现代理不可用, 并且在发现不可用的时候切换到另一个代理.

scrapy提供的ProxyMiddleware相当简单, 对其改造基本上等同于重新写一个了…首先就是如何发现一个代理不可用. 最常见的不可用代理的症状就是超时或者拒绝服务, 完全连不上, 为了处理这个问题, 新的ProxyMiddleware就要在process_exception里捕捉超时和ConnectionRefused异常. 还有些代理的问题是直接返回403或者407. 于是还要在process_response中检查response的status.

发现异常之后要换新的代理, 所以在新的ProxyMiddleware中需要一个list来保存所有代理, 每个代理还要有些属性, 例如valid, 这样当发现一个代理根本连不上时将valid属性设为False, 下次需要换代理时就忽略这些valid为False的代理, 避免再经历一遍超时.

另外就是发现IP被ban了之后更换代理, 一开始我是直接在middleware里处理这种情况, 但是IP被ban的检查各个网站都不一样, 写死在middleware里不太好. 于是我在spider里检查是否被ban, 如果被ban了则重新yield一个请求, 并设置meta["change_proxy"]=True, 然后在middleware里的process_request处检查这个属性, 如果为True则更换代理.

还有个问题就是代理数量不足的问题. 由于免费代理一般都是临时性的, 随着运行时间的增长, 有些代理会失效, 这样到最后就没有可用的代理了. 于是我在新的ProxyMiddleware里, 每次换新代理时检查代理列表中有效代理的数量, 如果小于一个阈值, 则从网上抓新的免费代理扩充代理列表.

3 实现

抓免费代理的脚本

这个脚本用于为了获取免费代理, 由于是为雪球网设计的, 主要抓的是大陆的高匿代理. 代码见https://github.com/kohn/HttpProxyMiddleware/blob/master/fetch_free_proxyes.py

抓代理的时候会简单筛选一下响应时间, 这个数据是提供免费代理的网站提供的.

4

光写个middleware其实一天就写好了, 接着又花了一天调参数. 然后在用到实际项目中去的时候就是与坑作斗争的血泪史.

有些代理根本不按常理出牌, 简单归纳一下:

  1. 将URL重定向到一个格式不合法的URL, 例如http://xueqiu.com/ 它给重定向到了http://xueqiu.com/http://xueqiu.com/. 通过设置request的dont_redirect属性以及检查302来解决
  2. 返回404, 500等各种各样的status. 解决方法是一个个加到检查列表里去(直接检查不等于200就换代理理论上应该可行)
  3. 最坑的就是虽然不好用, 但是返回的status是200! 只不过内容是一行错误信息. 这种错误在middleware里根本没法检查, 因为有些response的内容是一行错误信息说”… was not found on this server”(没找到你倒是返回404 啊), 而有些返回的内容是一个广告网站…这种代理只有在spider的parse里检查了, 如果找不到需要的内容, 就新建一个request, 设置meta["change_proxy"]=True来要求middleware换代理.
  4. 各种坑层出不穷, 防不胜防, 最终决定在获取免费代理时, 首先使用免费代理爬取一个测试用的网页, 如果能够爬下来并且比较过内容之后发现确实是目标网页, 则认为这个代理可用.

除了可用性之外, 在实际应用过程中还有一些效率上的问题需要优化.

  1. 使用代理总是比不使用代理要慢, 因此实现时用到了一个定时器, 当发现已经两小时没有用原生的连接(就是不用代理)去爬网页时, 自动切换到不用代理的情况.
  2. 运行过程中可能会出现”抖动”的问题, 当已有的n个代理都能正常使用, 但是被ban了, 就会不断的在这n个代理之间切换而实际上什么都没抓下来, 解决方法时定时(如半小时)抓取新的代理.

5 想法

写爬虫绕过网站的反爬虫机制应该是一个比较通用的需求, 但是国内竟然没有针对这一需求提供商业解决方案的公司. 如果能实现一个中间代理, 使得用户只要简单的将他的爬虫的代理设置为我的中间代理的URL, 然后由这个中间代理再去使用另一个代理根据用户需求抓数据, 并且能根据需要或者定时更换代理, 这样用户只要简单的设置一个代理, 就能实现IP的不断切换了, 这种服务应该能有盈利空间吧.

http_proxy_middleware_idea.png

  1. 博主你好,我尝试着使用你的代理去爬亚马逊的内容,你博文里面说的,如果发现被banned可以yield一个新的request,我不知道具体代码怎么写,请教一下,感谢!

      • 太感谢了,我看到您的方法中设置了change_proxy,想再问您一下:change_proxy后,之前使用的代理是否会被设置为invalid。
        您做的这个项目真的很棒!

      • 您好,博主,我看到您的代码中将代理 invalid了,解决了我的疑惑。
        另外我在使用过程中发现,在HttpProxyMiddleware.py中的198行(def process_request(self, request, spider) 方法中)的self.invalid_proxy(request.meta["proxy_index"]) 好像有问题,我测试发现会报错提示keyerror: ‘proxy_index’。
        我将request.meta["proxy_index"]改为self.proxy_index后可以正常运行了。

      • 再使用过程中发现,如果yield生成的request和response url相同的话不会发起新的request,爬虫好像忽略了这个request,我不知道是什么原因,我尝试着在response url后面增加了时间戳参数,此时request成功发起了。

        • 不好意思最近没怎么看博客.

          报错可能是因为你使用错误, 一个request发起之后, 首先经过我的这个middleware的process_request, 会向meta中增加一个属性”proxy_index”, 然后会进入你的spider的parse中, 解析response, 如果发现被ban, 那么再发起一个request, 同时设置change_proxy, 此时这个request既有proxy_index又有change_proxy, 再经过middleware时就会到198行调用invalid_proxy.

          但是如果你一开始发起的request就设置了change_proxy, 此时这个request第一次进入middleware, 还没设置好proxy_index, 于是就出错了. 所以change_proxy是当parse过程中发现被ban了, 新发起的request才能设置.

          你将request.meta["proxy_index"]改为self.proxy_index会有问题, 因为scrapy是异步, 当scrapy处理到这个request时, 有可能self.proxy_index为10, 而这个request之前使用的代理的proxy_index是9, 于是你就把明明好用的10给禁用了.

          最后url相同不会发起新的request, 是因为你设置了scrapy忽略相同的request, 所以要增加属性dont_filter=True. 这样就不会被忽略了.

  2. def process_request(self, request, spider):
    """
    将request设置为使用代理
    """
    if self.proxy_index > 0 and datetime.now() > (self.last_no_proxy_time + timedelta(minutes=self.recover_interval)):
    logger.info("After %d minutes later, recover from using proxy" % self.recover_interval)
    self.last_no_proxy_time = datetime.now()
    self.proxy_index = 0
    request.meta["dont_redirect"] = True

    博主我想问下这段代码中self.proxy_index = 0是什么意思呢?如果是将request设置为使用代理的话,设置self.proxy_index = 0,不就是使用零号代理(无代理)了吗?

  3. 您好,博主,我想请问一下,您这个的爬取速率大概是多少呢,因为我现在有个千万页面的需求,我稍微测试了一下,发现使用这个代理的时候平均速率在30个页面每分钟。
    还有您这个在监测代理是否可用的时候(就是在fetch_free_proxy里面的chenck,花费时间非常久。)

    • 你好, 速率要看你网速以及爬的页面, 我之前爬雪球网的时候是不用代理爬取效率的60%左右. check确实花费很长时间, 因为抓下来的代理有好几十个, 要一个个切换到这个代理然后测试能不能用. 但是这个check只有在代理不足需要抓取新的代理时才会使用, 使用频率很低, 所以对吞吐量不会影响太多.

    • 这些代码是边写边发现问题边改的, 可能确实绕了点, 主要是遇到的问题比较多, 很难一开始就做好一个设计能解决所有问题.

  4. 最坑的就是虽然不好用, 但是返回的status是200! 只不过内容是一行错误信息. 这种错误在middleware里根本没法检查, 因为有些response的内容是一行错误信息说”… was not found on this server”(没找到你倒是返回404 啊), 而有些返回的内容是一个广告网站…这种代理只有在spider的parse里检查了, 如果找不到需要的内容, 就新建一个request, 设置meta["change_proxy"]=True来要求middleware换代理.
    放到process_response里面检查就好了。没有必要放在parse里面。

    整体思路就是在中间件里面检查好process_response和process_exception就可以了。无他。

    • 我一开始也跟你一样想把所有异常处理做到中间件里, 这样的话spider就不用改任何东西, 但是在使用过程中发现这样不行. 有些问题是无法在中间件里面检查的, 比如我博客里提到的, 有些代理返回一个页面, 状态码是200, 内容却是一个广告页面, 那中间件就无法知道得到的response到底是本来就是这样的还是被proxy做了手脚得到了错误的response.

      归根到底是proxy的来源不可信任, 而且你无法穷举所有可能出现的问题. 一开始全做到中间件里能运行一段时间, 后来发现有些代理不超时, 但是返回乱七八糟的状态码, 于是我在中间件里加入处理这些状态码的逻辑, 后来跑着跑着又发现状态码是对的, 但是内容不对, 比如”was not found on this server”. 于是我又加入检查response是不是这段话的逻辑. 后来又发现response是一个广告, 你当然可以再在中间件里检查response是不是广告, 可是接下去呢? 跑着跑着又发现其他proxy引起的问题, 难道再改中间件吗? 而且如我刚刚所说, 不知道爬虫爬的是什么页面的情况下, 中间件无法判断获得的某个response是不是正确的response.

        • 前面说过了, 有些代理返回的是广告页面, 有着正常网页都有的元素. 如果你的意思是比较关键词, 例如我爬雪球网就检查response中有没有雪球两个字, 这当然是可以的, 但是把这个做到middleware里就不通用了, 只能雪球网用.

  5. 您好,你的分享很好用,谢谢了。
    有个问题:我修改了你的事例,去爬虫一个动态网页(lagou.com),在修改 yield.req 时出现问题,请问怎么修改这块的? 有点不太懂,谢谢。

    • 出了什么问题? 一般流程是爬网页, 解析错误抛出exception, 异常处理中yield一个meta["change_proxy"]=True的request. 就可以了

  6. 你好,
    1、我参考了:http://www.cnblogs.com/voidsky/p/5492184.html 这个爬虫拉钩网的信息,然后综合你那个,把你那个 test.py 修改成lagou那个;
    2、拉勾网是动态网页,所以原来爬虫 的yield是用这个的:
    yield scrapy.http.FormRequest(self.myurl,
    formdata = {‘pn': str(i), ‘kd': self.kd},callback=self.parse)
    就这块不懂怎么修改了。
    3、还有,拉钩网,返回错误信息 不是 403之类的,多次爬虫后会提示输入验证码。
    4、昨天修改了一下,直接全部用代理ip就行了,不用本地电脑ip。后面发现用代理,一个代理ipdownload 4次就被禁了。
    初步学习scrapy,所以很多不懂。希望指点一下,谢谢。

    • 2. 就这样改不行吗?
      3. 你要在parse里面检查response有没有让你输验证码, 如果有就切换代理.
      4. 用本地电脑的ip会快一点, 毕竟用代理总是比不用慢, 当然全用代理也完全没问题. ip下四次就被禁是有可能的, 因为抓来的代理是公共的, 可能别人也正好用了那个代理去爬了拉勾网, 总之发现被禁了就切换.

  7. 楼主您好,非常感谢您的大作,想请教一个问题: 您在前面的doc中有提到“website_possible_httpstatus_list”的使用方法, 大致原文是:
    Your spider should specify an array of status code where your spider may encouter during crawling. Any status code that is not 200 nor in the array would be treated as a result of invalid proxy and the proxy would be discarded. For example:

    website_possible_httpstatus_list = [404]

    This line tolds the middleware that the website you’re crawling would possibly return a response whose status code is 404, and do not discard the proxy that this request is using.
    我理解的是:一旦出现某种status被放到这个list里面,那么接下里就不会ban这个代理; 但不应该是ban这个代理么? 因为出现这种status!=200的,比如403或者404,意味着这次抓取失败,如果这个代理继续被保留使用,那不是仍然抓不到数据么?
    非常期待您的交流,拜谢!

    • 你好, 这里放的状态码是网站可能返回的状态码, 也就是说, 不在这里面的状态码肯定是代理引起的问题, 需要丢弃代理. 而在这里面的状态码可能是代理引起的, 也可能代理正常而由网站引起的, 需要由你写的爬虫来判断到底是属于哪种情况. 例如我爬雪球网, 根据用户id拼出一个url, 然后从雪球网抓数据, 但是这个id对应的用户可能被删除了, 这时候这个url就会由雪球网返回一个404. 这种情况下, 虽然返回的不是200, 但是问题不是由代理引起的, 如果丢弃这个代理就会冤枉了这个无辜的代理, 所以需要一个website_possible_httpstatus_list来避免这种情况.

  8. 博主您好,再请教一个问题:您这边实现的是用代理去抓取数据?有没有实现多个代理同时抓取呢?这样可以提高效率;

  9. 博主好,感谢您的代码,试了一下代码相当好用!不过有个问题,就是文章第4部分提到的“抖动”问题,在抖动发生的时候,程序还没运行完,已经爬到的数据就也没有保存到文件中。这时候我如果想看已经爬取到的数据信息,应该怎么处理比较好?求指教!

    • 正常情况下应该是爬多少就写多少到数据库中吧, 为什么会出现没运行完就没保存到文件这种情况? 你是整个运行完最后一次性写入到一个文件里吗? 如果是这样的话还是建议你爬下来一条就写到数据库了, 或者数据量不大的话也可以省事点爬一条就写一条到某个文件里

  10. 大佬,请问invalid_proxy函数中
    if index == self.proxy_index这个判断是其什么作用的呢,我认为传进去的index和现在的proxy_index不会出现不一致的情况啊,希望你能解答一下,谢谢

    • scrapy是异步的, 比如同时发起三个请求, 使用的都是同样的proxy以及index(比如都是1), 当第一个request返回时, 发现需要invalid, 于是就把self.proxy_index增加了1(self.proxy_index变成了2), 第二个请求返回时, 仍然需要invalid, 但是这时候这第二个请求用的仍然是1, 而self.proxy_index已经是2了, 因此需要判断一下. 不然的话会把可能是好用的2号proxy也给invalid了

  11. 博主,能请教一个问题吗?
    spider:
    def prase(self, response):
    req = response.request
    req.meta['change_proxy'] = True
    yield FormRequest(……..)
    这样在DownloaderMiddleware中的request接收不到change_proxy的状态(没有这个key)是怎么回事啊。

Leave a Reply to Chris Cancel Reply

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>