【Easy Python】第四话:爬虫初探——玩转豆瓣二百五(下)
前言
在上篇,我们获得了豆瓣二百五的电影URL,然后存储在了一个文件里。接下来,我们要访问每一个电影URL,深入敌后,获取情报~
所有的代码都已存储在我的Github仓库:Douban_250当中~
设置爬取规则
对于每一个电影,我们选择爬取如下内容(虽然电影列表页就能爬得到= =):
标题(title)、年份(year)、时长(time)、
导演(director)、类型(genre)、评分(score)
随意点击一个电影页面,用上篇所说的提取CSS选择器的方法,我们可以制作出每一种内容与内容提取规则的映射。如下所示:
# sp就是上篇所说的soup解析器
# 每一种内容都可以在解析器中按特定的规则提取加工得到
content_func_map = {'title': lambda sp: sp.select_one('#content > h1 > span:nth-child(1)').get_text(),'year': lambda sp: sp.select_one('#content > h1 > span.year').get_text().replace('(', '').replace(')', ''),'time': lambda sp: sp.select_one('#info > span[property="v:runtime"]').get_text(),'director': lambda sp: sp.select_one('#info > span:nth-child(1) > span.attrs > a').get_text(),'genre': lambda sp: list(map(lambda item: item.get_text(), sp.select('#info > span[property="v:genre"]'))),'score': lambda sp: sp.select_one('#interest_sectl > div.rating_wrap.clearbox > div.rating_self.clearfix > strong').get_text()
}
设置完规则,大问题来了= =250个链接,一个个访问,应该会很慢吧!而且,豆瓣还有反爬虫机制,如果访问间隔太快,就会暂时封ip——这,这可怎么办呀!
人海战术,代理爬虫
一个个访问慢的话,一起访问,似乎会更快的吧= =
豆瓣封ip的话,如果有多个ip,效率也会增加的吧= =
效率调度
理想状态下,我们希望爬取每一个url的过程,都能够被当作是一个任务(task)。就像在M记一样,前台每收到一个客人的订单,都会把订单任务直接扔给厨师们,然后处理下一个客人的请求。那么,我们的厨师在哪里?
得益于python内置的asyncio
库,我们可以模拟厨师们的工作,调度每一个爬虫任务。
然而,即使有了asyncio
库,我们也只是通过其中的调度机制,重构了CPU指令而已,对于某些阻塞(block)任务进度的过程,并不能妥善解决。在爬取url内容的过程中,最阻碍我们任务进度的,当属请求——响应的阶段。对于调度器来讲,一个任务发送请求出去,调度器并不需要让它傻等响应回来,而是可以让这个任务歇一下,把执行权让(yield)给其它任务,直到响应回来后,再通知这个任务继续原来的工作,这样才够效率。上一章我们用的requests
库不支持这一种机制,没关系,我们可以利用第三方的aiohttp
库,完成这个需求。
获取代理池
HTTP请求支持我们通过代理发送数据,使得目标识别发送源为代理服务器。要短时间获取大量的代理服务器地址,很简单,随便找个比如西刺代理或者66代理,利用内置查询,或者HTML解析爬取一堆,搞个一两千个就好。
代理服务器分配
在我们的人海战术里,每一个爬虫任务开始前,都需要分配到一个代理服务器。所以问题来了——代理服务器,该怎么分配给每一个任务呢?我们希望,每一个任务都像被等待点名一样,如果有可用的代理服务器就挑几个任务去用,如果暂时能用的代理服务器都在用的话,就歇一会儿。
当然,如果没有代理服务器能用,就凉凉了= =
得益于asyncio
库提供条件变量(Condition Variable)的机制,可以满足我们的需求。需要获得资源的任务,就去等待(wait)资源分配器的通知,资源分配器发现资源可用,则去通知(notify)那些迫不及待的任务,叫他们赶紧获取资源去。具体如何操作?且让我徐徐道来~
首先,在爬虫任务开始之前,我们得把“获取代理池”一步拿到的代理服务器列表,放到咱们内存里。
# 初始化条件变量
cond = None
# 代理队列采用deque()数据结构,头尾都可以添加/删除,便于代理重复利用
proxies = deque()
# 存放当前正在使用的代理,如果同时被多个任务用的话,小心被豆瓣封= =
proxies_used = set() def get_proxies():"""获取存好的代理服务器列表"""contents = open('proxies.txt', encoding='utf-8').read().splitlines()for i in range(len(contents)):if not contents[i].startswith('http'):contents[i] = 'http://' + contents[i]proxies.extend(set(contents))
然后,开始添加我们的任务啦
# 创建用于发送HTTP请求的客户端
async with aiohttp.ClientSession() as session:# 电影数量num_urls = len(movie_urls)# 初始化条件变量Condition Variableglobal condcond = asyncio.Condition()# 添加任务啦~tasks = list()for i in range(num_urls):# crawl_movie_url就是我们爬虫任务的模版啦,具体见下面tasks.append(crawl_movie_url(session, movie_urls[i], i + 1))# 最后一个启动的任务是咱们的代理分配模块:allocate_proxytasks.append(allocate_proxy(len(tasks)))# 开始把这堆任务跑起来,走你~await asyncio.gather(*tasks)
对于每一个任务,我们都做成一个循环(loop),直到任务完成或者没有代理服务器可用,才退出不干。因此,爬虫任务的初始逻辑如下:
# 加了async,表示这个函数上升为一个可被asyncio模块调度的任务
async def crawl_movie_url(session, url, movie_num):while True:# 加了await,该任务就歇了,保存该任务状态,CPU执行权交给其它任务# 直到get_proxy有结果才被asyncio调度器唤醒proxy = await get_proxy()if not proxy:log(movie_num, 'TMD代理全部挂了,凉凉= =')return# 开始执行爬虫任务啦,具体后面说= =await set_proxy_in_use(proxy)pass
对于每一个任务来讲,该如何获取代理服务器呢?咱们的get_proxy
任务代码如下:
async def get_proxy():# cond.acquire()跟cond.release(),可以实现对一段命令的加解锁(Lock)# 就如同上厕所一样,一个任务执行到加锁的一段,如果另一个任务也执行到这一段,就会暂停挂起,等待排队# proxies跟proxies_used是每个任务共享的全局变量(Global Variable),对于共享变量的操作,是要加锁的# 如果代理就剩一个,然后两个任务都跑到这一段,那就出事了= =await cond.acquire()proxy = ''try:# 等待代理分配器通知= =# 代理分配器会在有可用代理,或者代理全部凉凉的时候唤醒这些等待者await cond.wait()# 如果有可用代理,就挑一个,没有就直接返回空字符串# 可以看上面的crawl_movie_url任务,直接凉凉= =if len(proxies) > 0:# 从代理队列头部取出一个代理proxy = proxies.popleft()# 把这个代理加到正在使用的代理集合中proxies_used.add(proxy)finally:cond.release()return proxy
面对这群嗷嗷待哺的任务们,我们的代理服务器分配任务allocate_proxy
,就可以这样设计了
async def allocate_proxy(max_tasks):# 代理分配任务,本身就相当于一个服务,因此这里采用循环的方式来设计while True:# 上锁,劳资要给别的任务分配代理了# 而且还得看代理队列呢,其他任务先别着急= =await cond.acquire()will_break = False # 是否退出代理分配任务will_delay = False # 下一个循环是否要延迟长一点try:if task_count == max_tasks:# task_count指当前完成的爬虫任务数# 如果到了最大值,说明全部任务都结束了# 这个时候代理分配任务也就完成任务了will_break = True# 看代理队列还有木有可用的 len_proxies = len(proxies)if len_proxies == 0:if len(proxies_used) == 0:# 代理队列没代理,连tm正在用的代理也是空的,岂不是凉凉= =# 把这个可怕的信息告诉给所有任务吧= =然后劳资也跑路= =cond.notify_all()will_break = Trueelse:# 代理都在用着呢,等多一会儿再给大家分配吧= =will_delay = Trueelse:# 代理队列有多少代理,最多通知多少个任务去拿代理去cond.notify(len_proxies)finally:cond.release()if will_break:breakelif will_delay:# 歇多几秒,再分配代理给其它嗷嗷待哺的任务# 相当于爬虫访问网页的时间间隔设置# 豆瓣白天2s左右晚上3~4s左右差不多await asyncio.sleep(get_proxy_delay_time())else:# 歇一丢丢时间,把自己排在任务调度后面# 不然while True死循环,别的任务就跑不了了await asyncio.sleep(proxy_search_period)
爬虫主任务
有了代理分配这个强劲的后盾,我们的爬虫任务就可以顺利进行啦!
免费的代理好多都没法用的,需要在爬虫的过程中不断舍弃。废话不多说,直接上代码!
async def crawl_movie_url(session, url, movie_num):while True:# 这段上面讲了= =proxy = await get_proxy()if not proxy:log(movie_num, 'TMD代理全部挂了,凉凉= =')return# result存储爬取的数据,log函数打印日志(得自定义实现= =)result = {'number': movie_num, 'url': url, 'proxy': proxy}log(movie_num, '代理%s正在访问%s...' % (proxy, url))success = True # 是否爬取成功的标志try:# 用get获取数据,判断状态码,而后解析数据response = await session.get(url, proxy=proxy, headers=headers, timeout=proxy_connection_timeout)status_code = response.statusif status_code == 200:html = await response.text()soup = BeautifulSoup(html, html_parser)for k in content_func_map.keys():try:# 按最开始设的爬取规则抓数据,存到result里content = content_func_map[k](soup)result[k] = contentexcept Exception as e:# 如果爬不了,很有可能get到的网页变了!# 比如叫你登录啥啥的,这说明代理被豆瓣临时小黑屋了,果断放弃这个代理log(movie_num, '代理%s爬取%s信息失败!果断放弃掉!错误信息:%s\n' % (proxy, k, e))success = Falsebreakelse:# 如果状态码不对,很有可能是400啥的,说明也被豆瓣小黑屋了,果断放弃掉这个代理log(movie_num, '代理%s获取数据失败!果断放弃掉!状态码: %d!' % (proxy, status_code))success = Falseexcept Exception as e:# 代理链接或者代理发送请求都有问题,果断不要了 log(movie_num, '代理%s连接出错,果断放弃掉!!!错误信息:%s!' % (proxy, e))success = Falsefinally:if success:# 成功爬取数据,把爬取结果加上,把任务完成数加上global resultsglobal task_countresults.append(result)task_count = task_count + 1log(movie_num, '当前爬到信息的电影数: %d,爬到信息:%s' % (task_count, str(result)))# 这个代理还能用,给力!proxies_used里删掉它# 然后把它放到proxies的尾部(append)await recycle_proxy(proxy)breakelse:# 这个代理不给力,直接从proxies_used删掉吧~await remove_proxy(proxy)
试试看吧~
总结
豆瓣Top 250爬虫,其实更多的难点,在于如何组织、调度你的资源,更有效率地处理数据。爬虫的工具、软件,其实都已经烂大街
对于软件,集搜客、八爪鱼之类的就能完成需求
对于代理池,github上就有许多项目可以clone下来去获得
对于爬虫框架,其实可以踩踩scrapy
库的坑,这是一个非常成熟的爬虫框架
对于获取异步加载(不是当即就在response里,而是后面才加载到)的数据,可以使用PhantomJS一类的工具,或者利用Chrome开发者工具,采用抓包+模拟HTTP请求的方式,获取相应数据。
最后介绍一个大杀器——Selenium,作为一款浏览器测试驱动,selenium甚至可以模拟浏览器操作,百试不爽,谁用谁知道!
【Easy Python】第四话:爬虫初探——玩转豆瓣二百五(下)相关推荐
- 爬虫初探:把豆瓣读书主页上书的URL、书名、作者、出版时间、出版社全部爬下来
import requests import re #进入豆瓣读书主页,把网页源代码打出来 content = requests.get('https://book.douban.com/').tex ...
- Python爬虫初探(九)——爬虫之Beautifulsoup4实战(爬取豆瓣信息)
前面两章咱们介绍了Beautifuisoup4模块的简单使用,今天就用它来爬取豆瓣信息.话不多说,咱们开始吧. 一.拿到url地址 二.获取豆瓣数据 三.保存文件 需求: 爬取标题.评分.详情页的地址 ...
- python中的网页解析器_python爬虫初探(三):HTML解析器
爬虫初探系列一共4篇,耐心看完,我相信你就能基本了解爬虫是怎样工作的了,目录如下: 代码已上传至github,在python2.7下测试成功(请原谅我浓浓的乡村非主流代码风格)summerliehu/ ...
- python爬虫详细步骤-Python爬虫的两套解析方法和四种爬虫实现过程
对于大多数朋友而言,爬虫绝对是学习 python 的最好的起手和入门方式.因为爬虫思维模式固定,编程模式也相对简单,一般在细节处理上积累一些经验都可以成功入门.本文想针对某一网页对 python 基础 ...
- [Python从零到壹] 四.网络爬虫之入门基础及正则表达式抓取博客案例
欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都将结合案例.代码和作者的经验讲 ...
- c#使用正则表达式获取TR中的多个TD_[Python从零到壹] 四.网络爬虫之入门基础及正则表达式抓取博客案例...
首先祝大家中秋节和国庆节快乐,欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都 ...
- Python 爬虫瞎玩系列(1) —— Bilibili的前100个上古巨坟考古
Python 爬虫瞎玩系列(1) -- Bilibili的前100个上古巨坟考古 现在是2017年5月25日13:29:56,嗯,神志正常. Python爬虫很难?不存在的. 只要学习我的课程< ...
- 【Python笔记】网络爬虫——常用框架介绍以及 Scrapy 框架使用
网络爬虫开发常用框架 Scrapy 爬虫框架 Crawley 爬虫框架 PySpider 爬虫框架 Scrapy 爬虫框架的使用 搭建 Scrapy 爬虫框架 1. 安装 Twisted 模块 2. ...
- python最简单的爬虫代码,python小实例一简单爬虫
python新手求助 关于爬虫的简单例子 #coding=utf-8from bs4 import BeautifulSoupwith open('', 'r') as file: fcontent ...
最新文章
- [Delphi] Webbroker ISAPI 示例说明
- Intel Realsense D435 pyrealsense set_option() rs.option 可配置参数翻译
- mysql 密码字段加密_phpmyadmin密码字段加密方法
- 最小二乘算法MATLAB代码实现
- 用户认证-什么是认证
- Ubuntu 16.04工具栏靠下设置
- Hadoop Balancer运行速度优化
- Android系统性能优化(44)---全面详细的内存优化指南
- 集合Collection总览
- 背靠 Google 的 Go 语言,就不会失败?
- Nginx+Tomcat+SSL 识别 https还是http
- .NET:持续进化的统一开发平台
- 查找Excel最后一个非空单元格的值,你会吗?(适用于数据加行时,一直引用最后一个非空单元格的值)
- IP被反垃圾邮件组织列入SBL,发送邮件被退回的解决方法
- 八:微服务调用组件Dubbo
- JavaSE方法(构造方法)与方法重载基础练习题
- Sentiment140数据集
- win10 GTX1060 安装CUDA+PyTorch GPU
- 【原创纯手打】如何用微信小程序写留言板(附代码)
- plt.plot() marker 一览表 (散点图)
热门文章
- 分析化学在计算机行业的应用,计算机在分析化学的应用,Computer Application in Analytical Chemistry,音标,读音,翻译,英文例句,英语词典...
- C语言实现推箱子(从1.0到3.1版本详解)
- 桌面计算机回收站打不开,电脑桌面上只有回收站怎么办 电脑只显示回收站的两种解决方法...
- 微软发布会前新平板Xbox Surface规格泄露
- 两数求最大公约数的三种方法的C语言实现
- OfficeClickToRun.exe禁了后word就打不开了
- 第一份工作(二)编写中
- Java 变量运算符
- 网络同步在游戏历史中的发展变化(四)—— 状态同步的发展历程与基本原理(下)...
- 腾讯miniStation评测 打造完美安卓游戏主机