asyncio实现代理池

从一个代理池讲起?

搞爬虫的一般都有自己的代理池,代理池的结构一般分为抓取模块,存储模块,检测模块,api模块。
抓取模块本身也是一个爬虫,它会爬取个大免费代理网站的页面,解析。最后把数据交给存储模块。
假设现在我们要爬取一个代理网站http://www.website1.com/free 的前10页。对应第n页的URL应该是这样的
http://www.website1.com.free/n/ 。这是一个简单的爬虫,我们可以轻松的实现:

import time


# 计算时间的装饰器
def costtime(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        func(*args, **kwargs)
        print(time.perf_counter() - start)

    return wrapper


# 获取页面
def getPage(url):
    print(f"crawling: {url}")
    time.sleep(2)
    return url


# 解析页面
def parsePage(page):
    print(f'parsing {page} done!')
    time.sleep(0.2)
    return page


# 单个url的调度函数
def schedule(url):
    page = getPage(url)
    res = parsePage(page)


@costtime
def main():
    start_url = 'http://www.website1.com/{}/'
    for i in range(5):
        schedule(start_url.format(i))


if __name__ == '__main__':
    main()

这里我们使用time.sleep(2)模仿等待响应的过程过程。整个程序运行下来的时间等于:

总时间 = 当个页面时间 * 页面数
整个程序跑下来花了2.2s * 6 = 13.2s

可以优化么?

让我们思考以下几个问题:

  • 不同页之间的爬虫线程有数据关联么?爬取第一页的数据会影响第二页的数据么?
  • 不同页之间的爬虫线程之间有优先级么?比如一定要爬取了第一页才能爬取第二页?

很明显,答案是否定的。
所以我们尝试引入asyncio异步库打乱线程的优先级:

import time
import asyncio


# 计算时间的装饰器
def costtime(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        func(*args, **kwargs)
        print(time.perf_counter() - start)

    return wrapper


# 获取页面
async def getPage(url):
    print(f"crawling: {url}")
    await asyncio.sleep(2)
    return url


# 解析页面
def parsePage(page):
    print(f'parsing {page} done!')
    time.sleep(0.2)
    return page


# 单个url的调度函数
async def crawlSingleUrl(url):
    page = await getPage(url)
    res = parsePage(page)


async def schedule(start_url, page):
    tasks = []
    for i in range(page):
        tasks.append(crawlSingleUrl(start_url.format(i)))
    await asyncio.gather(*tasks)

@costtime
def main():
    start_url = 'http://www.website1.com/{}/'
    page = 5
    asyncio.run(schedule(start_url, page))

if __name__ == '__main__':
    main()

这里我们使用asyncio进行并发处理:

  1. 协程化: async语句会将指定函数封装成coroutine协程对象 。协程对象的特性是可以将处理机自由让出。
  2. 挂起协程: await语句会将协程挂起,等到合适的时机重运行进程。
  3. 收集协程: asyncio.gather方法会将协程对象自动封装成task,押入运行loop中,一个个的执行。
  4. 启动协程集: asyncio.run运行loop中的coroutine对象,等同于之前的语法:
loop = asyncio.get_loop_event()
loop.run_until_complete(asyncio.wait(task1,task2....))

现在回到了我们之前的程序, 我们把会阻塞的地方---getPage函数定义为协程,在请求代码前面加上了await,让其让出处理机。
那么处理机就运行其他协程的getPage代码了。
这样我们就实现了在某页等待response时,去发起下一页的requests或者页面解析。
整个程序跑下来只花了3.1s左右。


还可以再优化么?

之前我们说过代理池的抓取模块是从各大免费代理网站抓取的。那么每个网站的抓取除了页面解析规则不同之外,其他的都一样。
让我们进一步思考,如果把不同网站比做上面同一网站的不同页的话,是否可以得出如下结论:

  • 不同网站的数据获取没有先后关系

所以我们也给每个网站的获取加上异步并发:

import time
import asyncio


# 计算时间的装饰器
def costtime(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        func(*args, **kwargs)
        print(time.perf_counter() - start)

    return wrapper


# 获取页面
async def getPage(url):
    print(f"crawling: {url}")
    await asyncio.sleep(2)
    return url


# 解析页面
def parsePage(page):
    print(f'parsing {page} done!')
    time.sleep(0.2)
    return page


# 单个url的调度函数
async def crawlSingleUrl(url):
    page = await getPage(url)
    res = parsePage(page)


# 爬取单个网站的所有页面
async def crawlWebsie(start_url, page):
    tasks = []
    for i in range(page):
        tasks.append(crawlSingleUrl(start_url.format(i)))
    await asyncio.gather(*tasks)


async def schedule():
    start_url1 = 'http://www.website1.com/{}/'
    start_url2 = 'http://www.website2.com/{}'
    start_url3 = 'http://www.website3.com/{}'
    page = 5
    await asyncio.gather(crawlWebsie(start_url1, page), crawlWebsie(start_url2, page), crawlWebsie(start_url3, page))


@costtime
def main():
    asyncio.run(schedule())


if __name__ == '__main__':
    main()

这里我们加多了一个crawlWebsite函数,用来爬取整个网站的所有页面。然后在schedule调度方法里把不同的crawlWebsite收集到(gather)loop里面。
在main函数中运行loop。
这样我们就实现了不同网页间的并发,假设A站点响应速度比较缓慢的话,程序会把A挂起,运行B,C站点的协程。
整个程序跑下来花费了5s左右,很出乎意料。
这样我们的就实现了两个层级的并发,一个是同一网页间的不同页面的并发。另一个是不同网站之间的并发。

最后

异步的本质是打破线程之间的优先级,即让线程共同去竞争GIL,这点有点类似于多线程模块threading。但是不同的是,协程之间的切换消耗会比线程之间的切换消耗小。
如果把抓取模块中的最小单位规定为每一页的话。那么抛开网站而言,所有代理网站的请求集合都是页的集合,而我们所做的只是消除这些不相关页运行的先后顺序。
一句话:众页平等


基于上面的异步编程我写了一个代理池,可用于爬取各大网站,效率还是挺不错的。
基于异步的代理池

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350