Trip: 协程 Requests 实战,获取免费代理

2017-12-14 16:42:13 +08:00
 NxnXgpuPSfsIT

本文使用开源协程网络库 Trip,解决验证代理时耗时的问题。

简介

验证大量的网上代理一直是爬虫很麻烦的一个工作。 例如我要发出十万份请求,十个请求一个代理,每个代理验证的延时五秒钟。 那么需要的时间为:

100000 / 10 * 5 = 50000

这就是半天时间了,当然,我们可以开很多的线程。 这里我们开二十个线程运行这样的验证,那么需要的时间为:

100000 / 10 * 5 / 20 = 2500

2500 秒即四十分钟多,不包括抓取和处理,仍旧要消耗大量的时间。

而换用了协程框架后,你可以同时验证大量代理。 即使在一次验证中仅验证一百个代理,需要的时间也会变为:

100000 / 10 * 5 / 100 = 500

十万请求需要的代理在九分钟内就可以完成验证,这还仅是一个线程。 如果开多个线程同时处理还可以达到更快的速度。

另外,等待代理返回请求的过程当中完全不影响处理别的内容。 例如,这一个线程在验证的八分钟内完全可以继续充当公众号服务器继续回复消息,完全不会影响。

开始之前

本文基于依赖库 Trip,请保证更新至最新版本:

python -m pip install trip -U

另外,本文涉及协程,建议使用之前基本了解协程是什么,可以写出一个合格的协程程序。

Trip 兼容 Python 2、3,所以建议使用同样兼容的 Tornado 进行入门了解。

具体实现

由于 Trip 想要在协程的基础上充分还原 Requests 的操作,所以就有一个很简单的转换方式。

先用 Requests 的写法写好,之后将所有的网络连接操作加上yield,就基本没有问题了。

所以,下面的代码应该都不难理解。

获取代理

现在进入正文,首先是一个仅方便测试的代码段。

由于大家都有自己的获取代理的方式,网上免费的代理也多如牛毛,我这里随便就挑了一个做测试:

import re

import trip

def get_proxies(number=10):
    r = yield trip.get('http://www.89ip.cn/apijk/' +
        '?&tqsl=%s&sxa=&sxb=&tta=&ports=&ktip=&cf=1' % number)
    p = re.findall('((?:\d{1,3}.){3}\d{1,3}:\d+)', r.text)
    print(p)

trip.run(get_proxies)

这段代码没什么要说的,89ip是我一分钟前搜来的网站,之后使用正则从中提取代理地址。

验证代理

代理的验证用一个很简单的思路:

所以,这个思路下代码是这样的:

from functools import partial
import trip

def test_proxy(proxy):
    try:
        r = yield trip.get('http://httpbin.org/get', timeout=5,
            proxies={ 'http': proxy, 'https': proxy })
        if 'httpbin.org' not in r.text:
            raise Exception('Invalid reply')
    except Exception as e:
        print('[%s]: FAIL %s' % (proxy, e))
    else:
        print('[%s]: SUCC %s' % (proxy, r.elapsed))
        raise trip.Return(proxy)

trip.run(partial(test_proxy, '58.251.249.152:8118'))

我们这里用了五秒的延时,五秒内没有反应的代理就舍弃。 当然你可以根据自己的需求改为十秒、十五秒。

运行后你就会发现该代理完成了验证,显示了是否验证成功。

你可能会问,这个代码的确跑了五秒钟,和普通的代码有什么区别呢?

区别在于,有了协程,一个线程在这五秒钟里可以跑成百上千个这段代码,详见下面一节。

充分利用这五秒钟

如果你使用过 Tornado 或者一些其他的协程框架一定很熟悉这个操作:

r = yield [future1, future2, future3]

没错,这样三个操作就同时跑了起来 (我知道这不严谨,但看出这不严谨的也不需要我给你夸耀协程的好处了吧)。

所以最简单的,我们的主程序可以这样写:(这段代码不能单独跑,注意)

def main():
    proxies = yield get_proxies(100)
    r = yield [test_proxy(p) for p in proxies]
    print(filter(lambda x: x, r))

第一句话获取 ip,第二句话让他们同时检测,第三句话过滤结果。

完整的代码

所以完整的代码是这样的(这段可以跑了,请随意):

import re, time

import requests, trip

@trip.coroutine
def get_proxies(number=10):
    r = yield trip.get('http://www.89ip.cn/apijk/' +
        '?&tqsl=%s&sxa=&sxb=&tta=&ports=&ktip=&cf=1' % number)
    p = re.findall('((?:\d{1,3}.){3}\d{1,3}:\d+)', r.text)
    raise trip.Return(p)

@trip.coroutine
def test_proxy(proxy):
    try:
        r = yield trip.get('http://httpbin.org/get', timeout=5,
            proxies={ 'http': proxy, 'https': proxy })
        if 'httpbin.org' not in r.text:
            raise Exception('Invalid reply')
    except Exception as e:
        pass
    else:
        raise trip.Return(proxy)

def main():
    proxies = yield get_proxies(100)
    r = yield [test_proxy(p) for p in proxies]
    print(filter(lambda x: x, r))

start_time = time.time()
trip.run(main)
print(time.time() - start_time)

你可以运行一下,还是五秒钟,一百个代理的检测完成了。

之后的话

这里用了最简单的一种主程序的方法,你当然也可以使用更高级些的:

@trip.coroutine
def worker(wl, pl):
    while wl:
        p = wl.pop()
        r = yield test_proxy(p)
        if r:
            pl.append(r)

def main():
    waiting_list, proxies_list = yield get_proxies(100), []
    yield [worker(waiting_list, proxies_list) for i in range(10)]
    print(proxies_list)

这就看你喜欢了。

你如果使用的是 Windows 系统,你可能会发现没法同时进行很多的请求。 这是很正常的问题,Windows 系统使用的是 select,没法同时处理很多请求。

另外,剩下来的抓取应该就不需要我多说了。 Requests 怎么抓,这里也怎么抓,Session、timeout 什么的也全都支持。

最后,有什么我没讲清楚的、想法、建议都可以与我邮件联系或者在文末回复。

望本文能够有所帮助!

6508 次点击
所在节点    Python
29 条回复
NxnXgpuPSfsIT
2017-12-15 17:04:07 +08:00
贴出最基本的协程的使用,只是用过远没有精通,更不会自视甚高的傲视他人却给出可笑的代码和观点:
![]( http://7xrip4.com1.z0.glb.clouddn.com/v2ex/gevent-trip.png)

Result:
55200
0.718000173569
55200
12.2539999485
55200
0.746000051498

另外,一个最基本的页面,送给需入门的程序员们:docs.python.org/3/library/asyncio-task.html#coroutine
2225377fjs
2017-12-15 21:14:12 +08:00
@guyskk0x0 几年前就已经完全一行一行的理解了 greenlet 的实现原理,然后你说我不了解协程,现在还迫于无赖,不得不转 go 来做一些业务支持,你要是说我不懂协程,我也没有办法了,我当年看 greenlet,gevent 的文章记录都贴在前面,麻烦你去翻翻,看看我到底懂不懂。

```
关于隐患,先上一个特别简单的代码:
import multiprocessing, grequests; manager = multiprocessing.Manager()
他报错了,如果不是这段代码非常简单的只有一个引用,你需要多久要怎么发现是 gevent 的问题?
另外,这个问题你需要怎么修复?
```
你认为我需要解释这种东西来证明我懂?而且我也不用 grequests,也不用 multiprocessing。


我现在维护的业务框架是一个分布式服务框架,gevent 环境能非常贴合的满足我们的需求,性能也非常的好,最近公司内部引入了 go 做一些尝试,比较尴尬的是,在类似于基于 mongodb 的登录,基于 redis 的排行榜这种比较简单的服务,在 pypy 跑下来,吞吐数据已经和 go 不相上下了,在 gevent 环境下很简单的就把这些库拿进来用就行了,而且有项目组使用的 redis 的 pub-sub 功能,还有用 kafka 的项目组,你觉得现在 Python 环境下还有什么框架能更适合开发这么一个业务环境?(不过即使是 pypy 在带复杂业务逻辑的情况下执行效率比起 go 还是差一些,而且 go 还有多线程支持,可以很简单就能实现一些并行任务的开发,这个 Python 环境下很难做啊,哎,仿佛已经看到了我们的 Python 框架的落幕日了)

既然要提到 multiprocessing,我不知道你们在协程环境下想用 multiprocessing 做啥,我告诉你一个我们的用例:
登录验证环境,需要对用户数据进行 基于 rsa 的签名,但是 rsa 太慢,导致即使在 pypy 环境下单进程吞吐也只有几百不到一千,部分原因也是因为 cryptograph 库退化掉了,所以 pypy 和 cpython 没啥差,会严重影响 servcie_gate 的响应,这种场景,想要提高单节点的吞吐只有用并行多线程或者 multiprocessing,不过 multiprocessing 其实也搓的一逼,延迟还大,我们最终的做法是用 cffi 加 boost 库包装了一个多线程扩展库,同时兼容 cpython 和 pypy,业务提交加密签名的任务到库,算完之后,通知 libev 的 loop 唤醒挂起的 greenlet 协程提交结果。这种情况,我们的 servcie_gate 即使全部跑授权签名,也能跑 1W+的吞吐,不过代价是进程 CPU 会跑到百分之好几百。

说了一个我遇到的业务场景,那么礼尚往来,你告诉我一个你遇到的类似的业务场景你是怎么处理的呢?既然你评价我不懂协程。你要是质疑这个业务环境的真实性,那确实没有办法,我还要吃饭,这些东西没法放出来,公司比较保守,不怎么搞开源,曾经有同事放了一点带公司内部邮箱的代码到 github 都被劝退了,太吓人,而且也深知水平有限,不敢大张旗鼓宣传开源自己的东西。

不过可以给你贴一部分代码: https://github.com/2225377fjs/weblogviewer 这个是很多年前做的一个业务系统,基于 web 页面实时 tail 服务器集群各个机器的 log 文件,server 部分是一个很稚嫩的服务框架原型,还在读书的时候模仿支付宝的那套框架写的东西,基本已经废弃了,不过一些 IO 层的东西现在一直沿用。


@NxnXgpuPSfsIT 你贴出来不就完了么,那么说明在 200 个这种情况下还是可以的嘛,我已经说过没有兴趣去了解你的框架的 API,以及他是怎么并发的,我能做的就是按照最通俗的 API 和原语去完成功能,另外不要秀代码了,没啥意义,这些语法糖根本不屑于去用,代码是写给别人看的,用最通俗的代码写给所有可能会跟你配合做业务,写代码的人,我们公司有大量的业务逻辑是用 python 写的,这种时候你就能体会那些看起来很简练的语法糖是多么让人讨厌了,宁愿让代码稍微长一些,也要让代码一眼就能看懂。

另外你很喜欢用 tornado 嘛,同样做 hello world 的 http 服务器,把 wsgi server 换成 gevent 的实现,吞吐还能往上涨额,你可以试试。

你最终还是没有回答我的问题啊,既然 gevent 很好很方便实现的功能,你自己再撸一个功能还远不如别人的轮子请问意义何在?仅仅是秀?

不要随便上网搜搜,然后贴一下,就说你看,gevent 这么多坑,这么多人说他挫,我看到是用 gevent 非常方便的能实现你框架展示的各种功能啊,至于说坑,你举例子说说你展示的这些功能在 gevent 的环境下实现能有什么坑啊?

你是公开环境推广自己库的人,也要经得起质疑吧?人家说没啥鸟用,你说别人自视甚高,这种环境下好像推广者的姿态更高一些吧。

不要纠结各种协程定义概念了,没有标准的概念,各种各样不同的实现,我所能看到的 go 实现的协程模型现在发展势头很好,大家接受度都很高,这种概念与 gevent 的模式非常相似。

我也不说什么半吊子框架了,收回这句话,是不太尊重,理性讨论。
2225377fjs
2017-12-15 22:16:25 +08:00
@guyskk0x0
```
关于隐患,先上一个特别简单的代码:
import multiprocessing, grequests; manager = multiprocessing.Manager()
他报错了,如果不是这段代码非常简单的只有一个引用,你需要多久要怎么发现是 gevent 的问题?
另外,这个问题你需要怎么修复?
```

只有你这种类似的的渣渣才会说上面的东西简单,我告诉你,我理解的上面的一套东西,以及没有贴出来的可能的业务上下文一点也不简单。

说我不懂装懂,我看你才是不懂装懂吧,渣渣。

你眼睛瞎啦,不知道把上面的帖子看完?


@NxnXgpuPSfsIT
另外,我是不是已经入门的程序员你还没有资格来评价,我从来不敢说自己精通了什么东西,我能做的就是把好用的东西在自己水平范围内把它用好。不敢随意撸轮子,更不敢泛泛的评价各种有一定知名度的轮子。

我一开始的回复就是因为在我看来你的框架没有任何存在意义,恕我直言,真的是没有任何意义。与其重复的撸这些轮子,不如好好去了解你觉得不行的 gevent,看看 gevent 实现你这轮子的功能是不是很简单,到底会有什么坑。而不是在这种公开环境下泛泛而谈的说 gevent 这不行,那也坑啥的,然后随便上网搜一下找一堆东西 copy 一下来证明你的观点,你不具备这个资格这么来评价 gevent,你一开始对 gevent 的评价给我的第一感觉就是半罐子水,所以需要怼一下才行。

看得出你很用心的维护你的框架,但是自己的框架不是通过随便泛泛的怼怼别的框架就能牛逼了。

自己前后都矛盾,一会普通程序员,一会又大局,细节,还撤出各种东西来背书,有毛用啊,还给协程下定义,非得要基于 yield yield from 这些生成器实现的功能都不齐全的环境才叫协程,那 greenlet,go 的协程,甚至包括 boost 库里面实现的协程他们都时啥东西啊。

Python 生成器的效率问题,我不想去证明了,gevent 的有一个协程 benchmark,你有精力的话可以去试试,不过好像生成器的要实现这种调度的 benchmark 逻辑都很麻烦。(忘记了是 greenlet 还是 gevent 的了,反正肯定有一个会有 10W 并发协程的调度 benchmark 代码)

不过你要是有精力实现出来,而且实测生成器的调度效率也不差的话,非常希望你能告诉我结果。
NxnXgpuPSfsIT
2017-12-15 23:07:03 +08:00
@2225377fjs
很棒!尊重是交流的基础。


所以我们达成共识了,

由于其独特的切入点,gevent 在根源上存在问题,这是劣势。
* patch 会给项目带来毁灭性的隐患,且无法预料
* 不区分协程函数和普通函数(不使用 yield )是一个很糟糕的习惯
* 侵入式的兼容只能产生一个不可知的过渡用品,而不是一个有保障的产品

而你说的优势,你举出的观点最好的性能是源于你当时对于主流协程的不了解。
至于解释,你说了你不用,你不愿意解释。但你也认识到了那些问题都是普通用户在日常使用可能中会碰到的。

我们于是能清楚的看到大牛所说的希望可以有项目出现的空白,
你想要使用协程网络库,现在有了不存在根本层面问题的选择。


所以最终回到我的观点,gevent 是一个没有办法的将就。
Requests 的作者说他想要一个这样库却没有时间,我们有。
不敢说有什么水平,起码达到了高效、可用,
起码可以完美的完成就像文中介绍的操作。
我们才分享了项目的介绍,给那些也曾经意识到这块空白的程序员。
我们可以一起使用,提意见,志同道合可以一起开发。

而对于那些幸运的能一直处在 gevent 安全区中的用户或者项目已经基于 gevent 的用户,
这个项目只是给你们多一个选择,即使你换用官方的协程,依旧可以使用到协程 Requests。
lolizeppelin
2017-12-16 00:51:57 +08:00
总体上来说 openstack 都在用 eventlet 所以稳定性上还是有保证的

猴子补丁确实坑 假的 socket timout 问题找了半天
这还是我比较熟悉 eventlet 的情况


如果有选择的话 我宁愿不使用猴子补丁

但是有选择么?你能写一个 request 的携程库 其他的咋办
redis mysql mq 你全写一份?这做法你可以换 go 了性能更好

所以 要携程老老实实用猴子补丁咯
guyskk0x0
2017-12-16 12:47:06 +08:00
@NxnXgpuPSfsIT
1. 你说你懂协程,却写了个错误的测试代码,来喷 “ trip 翔一样的性能”,你在 #16 楼贴的代码足以证明你不懂协程。

2. 你说你对 gevent 了如指掌,遇到的问题都能解决,那为何不能回答 #20 楼的问题?你那几篇博客,顶多说明你看过源码。你解决过官方仓库的 issue 吗?你给上游贡献过代码吗?你能质疑这个项目,我就不能质疑你吗?

3. gevent 暗坑很多,这是事实。Python 核心开发者,包括 Python 之父 Guido 和 Requests 作者都表态了 gevent 不行,asyncio 以及 async/await 语法都已经加到 Python 中了,协程是解决问题的方向,是未来。你连协程的基本原理都不懂,没资格说这个项目没鸟用。

4. gevent 是当下的 “将就” 解决方案,大部分项目确实是用 的 gevent,因为协程并不成熟,生态并不完善,我们没得选
。但是 gevent 问题很多,这是众所周知的事,我们尝试新的解决方案很有意义啊,你看不懂不代表别人也看不懂。
lolizeppelin
2017-12-16 14:31:03 +08:00
https://wiki.openstack.org/wiki/Oslo/blueprints/asyncio

看上面, 跟着 openstack 走就好, 起码目前 eventlet 的方式是可行的

gevent 是将就,但是目前确实不是能很好替代的,等 openstack 用上 asyncio 基本就有一系列可靠方案了
sunwei0325
2017-12-17 00:27:50 +08:00
gevent 迟早和今天的 python2 一样被打上末日标记
mmz0707
2017-12-20 11:47:50 +08:00
有好的代理推介吗

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/414753

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX