从抓取豆瓣电影聊高性能爬虫思路

简书不维护了,欢迎关注我的知乎:波罗学的个人主页

知乎原文

本篇文章将以抓取豆瓣电影信息为例来一步步介绍开发一个高性能爬虫的常见思路。

寻找数据地址

爬虫的第一步,首先我们要找到获取数据的地址。可以先到豆瓣电影首页 https://movie.douban.com/ 去看看。

顶部导航为提供了很多种类型的入口,其中和电影有关的有:排行榜、选电影和分类。为了便于后续更精细的分析,这里选择进入分类页面,它的地址为https://movie.douban.com/tag/。通过浏览的开发工具,我们最终能确认数据来源是的 https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0 接口。

注意:如果有朋友熟悉前端并装有vue浏览器插件,就会发现豆瓣电影站点是vue开发的。这些基本web开发技能对于我们平时开发爬虫都是很有帮助的。

爬取首页数据

用浏览器打开上面的接口地址,我们就会发现它的返回数据为json格式。利用python的requests和json库,就可以把数据获取下来了。

这里我们只获取电影的标题、导演、评分和演员四个字段,代码如下:

import json
import requests

def crawl(url):
    response = requests.get(url)
    if response.status_code != 200:
        raise Exception('http status code is {}'.format(response.status_code))

    data = response.json()['data']

    items = []
    for v in  data:
        items.append({
            'title': v['title'],
            'drectors': v['directors'],
            'rate': v['rate'],
            'casts': v['casts']
        })

    return items

def main():
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0'
    for item in crawl(url):
        print(item)

if __name__ == "__main__":
    main()

代码执行得到如下这些数据:

{'title': '绿皮书', 'drectors': ['彼得·法雷里'], 'rate': '8.9', 'casts': ['维果·莫腾森', '马赫沙拉·阿里', '琳达·卡德里尼', '塞巴斯蒂安·马尼斯科', '迪米特·D·马里诺夫']}
{'title': '惊奇队长', 'drectors': ['安娜·波顿', '瑞安·弗雷克'], 'rate': '7.0', 'casts': ['布丽·拉尔森', '裘德·洛', '塞缪尔·杰克逊', '本·门德尔森', '安妮特·贝宁']}
 ...
{'title': '这个杀手不太冷', 'drectors': ['吕克·贝松'], 'rate': '9.4', 'casts': ['让·雷诺', '娜塔莉·波特曼', '加里·奥德曼', '丹尼·爱罗', '彼得·阿佩尔']}
{'title': '新喜剧之王', 'drectors': ['周星驰', '邱礼涛', '黄骁鹏', '肖鹤'], 'rate': '5.8', 'casts': ['王宝强', '鄂靖文', '张全蛋', '景如洋', '张琪']}

仔细观察,我们会发现仅仅抓到了20条数据。电影数据才这么点,这是不可能的,这是因为正常网站展示信息都会采用分页方式。再来看下电影的分类页面,我们把滚动条拉到底部就会发现底部有个 "加载更多" 的提示按钮。点击之后,会加载出更多的电影。

分页抓取

对于各位来说,分页应该是很好理解的。就像书本一样,包含信息多了自然就需要分页,网站也是如此。不过站点根据场景不同,分页规则也会有些不同。下面来具体说说:

先说说分页的参数,通常会涉及三个参数,分别是:

  • 具体页码,url中的常见名称有 page、p、n 等,起始页码通常为1,有些情况为0;
  • 每页数量,url中的常见名称有 limit、size、pagesize(page_size pageSize)等;
  • 起始位置,url中的常见名称有start、offset等,主要说明从什么位置开始获取数据,;

分页主要通过这三种参数的两种组合实现,哪两种组合?继续往下看:

  • 具体页码 + 每页数量,这种规则主要用在分页器的情况下,而且返回数据需包含总条数;
  • 起始位置 + 每页数量,这种规则主要用在下拉场景,豆瓣的例子就是用下拉来分页,这种情况下的url返回数据可不包含总数,前端以下页是否还有数据就可判定分页是否完成。

介绍完了常见的两种分页规则,来看看我们的的url:

https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0

该页面通过下拉方式实现翻页,那么我们就会想url中是否有起始位置信息。果然在找到了start参数,此处为0。然后点击下拉,通过浏览器开发工具监控得到了新的url,如下:

https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=20

start的值变成了20,这说明起始位置参数就是start。依照分页的规则,我们把main函数修改下,加个while循环就可以获取全部电影数据了,代码如下:

def main():
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start={}'

    start = 0
    total = 0
    while True:
        items = crawl(url.format(start))
        if len(items) <= 0:
            break

        for item in items:
            print(item)

        start += 20
        total += len(items)
        print('已抓取了{}条电影信息'.format(total))

    print('共抓取了{}条电影信息'.format(total))

到这里工作基本完成!把print改为入库操作把抓取的数据入库,一个爬虫就真正完成了。

进一步优化

不知大家注意到没有,这里的请求每次只能获取20条数据,这必然到导致数据请求次数增加。这有什么问题吗?三个问题:

  • 网络资源浪费严重;
  • 获取数据速度太慢;
  • 容易触发发爬机制;

那有没有办法使请求返回数据量增加?当然是有的。

前面说过分页规则有两个,分别是 具体页码 + 每页大小 和 起始位置 + 每页大小。这两种规则都和每页大小,即每页数量有关。我们知道上面的接口默认每页大小为20。根据前面介绍的分页规则,我们分别尝试在url加上limit和size参数。验证后发现,limit可用来改变每次请求获取数量。修改一下代码,在url上增加参数limit,使其等于100:

def main():
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start={}&limit=100'

只是增加了一个limit参数就可以帮助我们大大减少接口请求次数,提高数据获取速度。要说明一下,不是每次我们都有这样好的运气,有时候每页数量是固定的,我们没有办法修改,这点我们需要知道。

高性能爬虫

经过上面的优化,我们的爬虫性能已经有了一定提升,但是好像还是很慢。执行它并观察打印信息,我们会发现每个请求之间的延迟很大,必须等待上一个请求响应并处理完成,才能继续发出下一个请求。如果大家有网络监控工具,你会发现此时网络带宽的利用率很低。因为大部分的时间都被IO请求阻塞了。有什么办法可以解决这个问题?那么必然要提的就是并发编程。

并发编程是个很大的话题,涉及多线程、多进程以及异步io等,这篇文章的重点不在此。这里使用python的asyncio来帮助我们提升高爬虫性能。我们来看实现代码吧。

此处要说明一个问题,因为豆瓣用下拉的方式获取数据,正如上面介绍的那样,这是一种不需要提供数据总数的就可以分页的方式。但是这种方式会导致我就没有办法事先根据limit和total确定请求的总数,在请求总数未知的情况下,我们的请求只能顺序执行。所以这里我们为了案例能够继续,假设获取数据最多1万条,代码如下:

import json
import asyncio
import aiohttp

async def crawl(url):
    data = None
    async with aiohttp.ClientSession() as s:
        async with s.get(url) as r:
            if r.status != 200:
                raise Exception('http status code is {}'.format(r.status))
            data = json.loads(await r.text())['data']

    items = []
    for v in  data:
        items.append({
            'title': v['title'],
            'drectors': v['directors'],
            'rate': v['rate'],
            'casts': v['casts']
        })

    return items

async def main():
    limit = 100
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start={}&limit={}'
    start = 0
    total = 10000

    crawl_total = 0
    tasks = [crawl(url.format(start + i * limit, limit)) for i in range(total // limit)]
    for r in asyncio.as_completed(tasks):
        items = await r
        for item in items:
            print(item)
        crawl_total += len(items)

    print('共抓取了{}条电影信息'.format(crawl_total))

if __name__ == "__main__":
    ioloop = asyncio.get_event_loop()
    ioloop.run_until_complete(main())

最终结果显示获取了9900条,感觉是豆瓣限制了翻页的数量,最多只能获取9900条数据。

最终的代码使用了asyncio的异步并发编程来实现爬虫性能的提高,而且还用到了aiohttp这个库来实现http的异步请求。跳跃有点大,有一种学会了1+1就可以去做微积分题目的感觉。淡然,这里也使用多进程或多线程来重写。

总结

本文从提高爬虫抓取速度与减少资源消耗两个角度介绍了开发一个高性能爬虫的一些技巧:

  • 有效利用分页减少网络请求减少资源消耗;
  • 并发编程实现带宽高效利用提高爬虫速度;

最后,大家如果有兴趣可以去看看tornado文档中实现的一个高并发爬虫。如果不懂异步io的话,或许会觉得这个代码很诡异。
tornado高性能爬虫

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

推荐阅读更多精彩内容

  • width: 65%;border: 1px solid #ddd;outline: 1300px solid #...
    邵胜奥阅读 4,764评论 0 1
  • 前言 爬虫就是请求网站并提取数据的自动化程序,其中请求,提取,自动化是爬虫的关键。Python作为一款出色的胶水语...
    王奥OX阅读 3,337评论 1 8
  • 上网原理 1、爬虫概念 爬虫是什麽? 蜘蛛,蛆,代码中,就是写了一段代码,代码的功能从互联网中提取数据 互联网: ...
    riverstation阅读 8,030评论 1 2
  • 基础知识 HTTP协议 我们浏览网页的浏览器和手机应用客户端与服务器通信几乎都是基于HTTP协议,而爬虫可以看作是...
    腩啵兔子阅读 1,464评论 0 17
  • 如上图所示,屏幕正中间有个元素A,随着屏幕宽度的增加,始终需要满足以下条件: A元素垂直居中于屏幕中央; A元素距...
    独爱一乐拉面阅读 616评论 0 1