哪个修手机店装修效果图比较好的

Python 异步网络爬虫(1) - Python - 伯乐在线
& Python 异步网络爬虫(1)
本文主要讨论下面几个问题:
什么是异步(Asynchronous)编程?
为什么要使用异步编程?
在 Python 中有哪些实现异步编程的方法?
Python 3.5 如何使用 async/await 实现异步网络爬虫?
所谓异步是相对于同步(Synchronous)的概念来说的,之所以容易造成混乱,是因为刚开始接触这两个概念时容易把同步看做是同时,而同时不是意味着并行(Parallel)吗?然而实际上同步或者异步是针对于时间轴的概念,同步意味着顺序、统一的时间轴,而异步则意味着乱序、效率优先的时间轴。比如在爬虫运行时,先抓取 A 页面,然后从中提取下一层页面 B 的链接,此时的爬虫程序的运行只能是同步的,B 页面只能等到 A 页面处理完成之后才能抓取;然而对于独立的两个页面 A1 和 A2,在处理 A1 网络请求的时间里,与其让 CPU 空闲而 A2 等在后面,不如先处理 A2,等到谁先完成网络请求谁就先来进行处理,这样可以更加充分地利用 CPU,但是 A1 和 A2 的执行顺序则是不确定的,也就是异步的。
很显然,在某些情况下采用异步编程可以提高程序运行效率,减少不必要的等待时间,而之所以能够做到这一点,是因为计算机的 CPU 与其它设备是独立运作的,同时 CPU 的运行效率远高于其他设备的读写(I/O)效率。为了利用异步编程的优势,人们想出了很多方法来重新安排、调度(Schedule)程序的运行顺序,从而最大化 CPU 的使用率,其中包括进程、线程、协程等(具体可参考《》)。在 Python 3.5 以前通过 @types.coroutine 作为修饰器的方式将一个生成器(Generator)转化为一个协程,而在 Python 3.5 中则通过关键词 async/await 来定义一个协程,同时也将 asyncio 纳入为标准库,用于实现基于协程的异步编程。
要使用 asyncio 需要理解下面几个概念:
Event loop
Future & Task
Event loop
了解 JavaScript 或 Node.js 肯定对事件循环不陌生,我们可以把它看作是一种循环式(loop)的调度机制,它可以安排需要 CPU 执行的操作优先执行,而会被 I/O 阻塞的行为则进入等待队列:
asyncio 自带了事件循环:
import asyncio
loop = anscio.get_event_loop()
# loop.run_until_complete(coro())
loop.close()
import asyncio&loop = anscio.get_event_loop()&&# loop.run_until_complete(coro())loop.close()&&
当然你也可以选择其它的实现形式,例如 Sanic 框架采用的 ,用起来也非常简单( 至于性能上是否更优我没有验证过,但至少在 Jupyter Notebook 上 uvloop 用起来更方便):
import asyncio
import uvloop
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
import asyncio&&import uvloop&loop = uvloop.new_event_loop()&&asyncio.set_event_loop(loop)&&
Python 3.5 以后推荐使用 async/await 关键词来定义协程,它具有如下特性:
通过 await 将可能阻塞的行为挂起,直到有结果之后继续执行,Event loop 也是据此来对多个协程的执行进行调度的;
协程并不像一般的函数一样,通过 coro() 进行调用并不会执行它,而只有将它放入 Event loop 进行调度才能执行。
一个简单的例子:
import uvloop
import asyncio
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
async def compute(a, b):
print("Computing {} + {}...".format(a, b))
await asyncio.sleep(a+b)
return a + b
tasks = []
for i, j in zip(range(3), range(3)):
print(i, j)
tasks.append(compute(i, j))
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
### OUTPUT
Computing 0 + 0...
Computing 1 + 1...
Computing 2 + 2...
CPU times: user 1.05 ms, sys: 1.21 ms, total: 2.26 ms
Wall time: 4 s
12345678910111213141516171819202122232425262728
import uvloop&&import asyncio&loop = uvloop.new_event_loop()&&asyncio.set_event_loop(loop)&async def compute(a, b):&&&&&&print("Computing {} + {}...".format(a, b))&&&&await asyncio.sleep(a+b)&&&&return a + btasks = []&&for i, j in zip(range(3), range(3)):&&&&&&print(i, j)&&&&tasks.append(compute(i, j))loop.run_until_complete(asyncio.gather(*tasks))&&loop.close()&### OUTPUT"""0 0&&1 1&&2 2&&Computing 0 + 0...&&Computing 1 + 1...&&Computing 2 + 2...&&CPU times: user 1.05 ms, sys: 1.21 ms, total: 2.26 ms&&Wall time: 4 s&&"""
由于我们没办法知道协程将在什么时候调用及返回,asyncio 中提供了 Future 这一对象来追踪它的执行结果。
Future & Task
Future 相当于 JavaScript 中的 Promise,用于保存未来可能返回的结果。而 Task 则是 Future 的子类,与 Future 不同的是它包含了一个将要执行的协程( 从而组成一个需要被调度的任务)。还以上面的程序为例,如果想要知道计算结果,可以通过 asyncio.ensure_future() 方法将协程包裹成 Task,最后再来读取结果:
import uvloop
import asyncio
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
async def compute(a, b):
print("Computing {} + {}...".format(a, b))
await asyncio.sleep(a+b)
return a + b
tasks = []
for i, j in zip(range(3), range(3)):
print(i, j)
tasks.append(asyncio.ensure_future(compute(i, j)))
loop.run_until_complete(asyncio.gather(*tasks))
for t in tasks:
print(t.result())
loop.close()
### OUTPUT
Computing 0 + 0...
Computing 1 + 1...
Computing 2 + 2...
CPU times: user 1.62 ms, sys: 1.86 ms, total: 3.49 ms
Wall time: 4.01 s
123456789101112131415161718192021222324252627282930313233
import uvloop&&import asyncio&loop = uvloop.new_event_loop()&&asyncio.set_event_loop(loop)&async def compute(a, b):&&&&&&print("Computing {} + {}...".format(a, b))&&&&await asyncio.sleep(a+b)&&&&return a + btasks = []&&for i, j in zip(range(3), range(3)):&&&&&&print(i, j)&&&&tasks.append(asyncio.ensure_future(compute(i, j)))loop.run_until_complete(asyncio.gather(*tasks))&&for t in tasks:&&&&&&print(t.result())loop.close()&### OUTPUT"""0 0&&1 1&&2 2&&Computing 0 + 0...&&Computing 1 + 1...&&Computing 2 + 2...&&0&&2&&4&&CPU times: user 1.62 ms, sys: 1.86 ms, total: 3.49 ms&&Wall time: 4.01 s&&"""
异步网络请求
Python 处理网络请求最好用的库就是 (应该没有之一),但由于它的请求过程是同步阻塞的,因此只好选用 。为了对比同步与异步情况下的差异,先伪造一个假的异步处理服务器:
from sanic import Sanic
from sanic.response import text
import asyncio
app = Sanic(__name__)
@app.route("/")
@app.route("/")
async def index(req, word=""):
t = len(word) / 10
await asyncio.sleep(t)
return text("It costs {}s to process `{}`!".format(t, word))
123456789101112
from sanic import Sanic&&from sanic.response import text&&import asyncio&&app = Sanic(__name__)&@app.route("/")@app.route("/")async def index(req, word=""):&&&&&&t = len(word) / 10&&&&await asyncio.sleep(t)&&&&return text("It costs {}s to process `{}`!".format(t, word))app.run()&&
服务器处理耗时与请求参数(word)长度成正比,采用同步请求方式,运行结果如下:
import requests as req
URL = "http://127.0.0.1:8000/{}"
words = ["Hello", "Python", "Fans", "!"]
for word in words:
resp = req.get(URL.format(word))
print(resp.text)
### OUTPUT
It costs 0.5s to process `Hello`!
It costs 0.6s to process `Python`!
It costs 0.4s to process `Fans`!
It costs 0.1s to process `!`!
CPU times: user 18.5 ms, sys: 2.98 ms, total: 21.4 ms
Wall time: 1.64 s
123456789101112131415161718
import requests as req&URL = "http://127.0.0.1:8000/{}"&&words = ["Hello", "Python", "Fans", "!"]&for word in words:&&&&&&resp = req.get(URL.format(word))&&&&print(resp.text)&### OUTPUT"""It costs 0.5s to process `Hello`!&&It costs 0.6s to process `Python`!&&It costs 0.4s to process `Fans`!&&It costs 0.1s to process `!`!&&CPU times: user 18.5 ms, sys: 2.98 ms, total: 21.4 ms&&Wall time: 1.64 s&&"""
采用异步请求,运行结果如下:
import asyncio
import aiohttp
import uvloop
URL = "http://127.0.0.1:8000/{}"
words = ["Hello", "Python", "Fans", "!"]
async def getPage(session, word):
with aiohttp.Timeout(10):
async with session.get(URL.format(word)) as resp:
print(await resp.text())
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
session = aiohttp.ClientSession(loop=loop)
tasks = []
for word in words:
tasks.append(getPage(session, word))
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
session.close()
### OUTPUT
It costs 0.1s to process `!`!
It costs 0.4s to process `Fans`!
It costs 0.5s to process `Hello`!
It costs 0.6s to process `Python`!
CPU times: user 61.2 ms, sys: 18.2 ms, total: 79.3 ms
Wall time: 732 ms
12345678910111213141516171819202122232425262728293031323334
import asyncio&&import aiohttp&&import uvloop&URL = "http://127.0.0.1:8000/{}"&&words = ["Hello", "Python", "Fans", "!"]&async def getPage(session, word):&&&&&&with aiohttp.Timeout(10):&&&&&&&&async with session.get(URL.format(word)) as resp:&&&&&&&&&&&&print(await resp.text())&loop = uvloop.new_event_loop()&&asyncio.set_event_loop(loop)&&session = aiohttp.ClientSession(loop=loop)&tasks = []&&for word in words:&&&&&&tasks.append(getPage(session, word))&loop.run_until_complete(asyncio.gather(*tasks))&loop.close()&&session.close()&### OUTPUT"""It costs 0.1s to process `!`!&&It costs 0.4s to process `Fans`!&&It costs 0.5s to process `Hello`!&&It costs 0.6s to process `Python`!&&CPU times: user 61.2 ms, sys: 18.2 ms, total: 79.3 ms&&Wall time: 732 ms&&"""
从运行时间上来看效果是很明显的。
### 未完待续
接下来将对 aiohttp 进行简单封装,更有利于伪装成普通浏览器用户访问,从而服务于爬虫发送网络请求。
– END –
打赏支持我写出更多好文章,谢谢!
打赏支持我写出更多好文章,谢谢!
关于作者:
可能感兴趣的话题
写的都挺好
o 224 回复
关于 Python 频道
Python频道分享 Python 开发技术、相关的行业动态。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2017 伯乐在线> 博客详情
& 用python也差不多一年多了,python应用最多的场景还是web快速开发、爬虫、自动化运维:写过简单网站、写过自动发帖脚本、写过收发邮件脚本、写过简单验证码识别脚本。
爬虫在开发过程中也有很多复用的过程,这里总结一下,以后也能省些事情。
1、基本抓取网页
import&urllib2
response&=&urllib2.urlopen(url)
print&response.read()
import&urllib
import&urllib2
form&=&{'name':'abc','password':'1234'}
form_data&=&urllib.urlencode(form)
request&=&urllib2.Request(url,form_data)
response&=&urllib2.urlopen(request)
print&response.read()
2、使用代理IP
& & 在开发爬虫过程中经常会遇到IP被封掉的情况,这时就需要用到代理IP;
在urllib2包中有ProxyHandler类,通过此类可以设置代理访问网页,如下代码片段:
import&urllib2
proxy&=&urllib2.ProxyHandler({'http':&'127.0.0.1:8087'})
opener&=&urllib2.build_opener(proxy)
urllib2.install_opener(opener)
response&=&urllib2.urlopen('')
print&response.read()
3、Cookies处理
& & cookies是某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据(通常经过加密),python提供了cookielib模块用于处理cookies,cookielib模块的主要作用是提供可存储cookie的对象,以便于与urllib2模块配合使用来访问Internet资源.
代码片段:
import&urllib2,&cookielib
cookie_support=&urllib2.HTTPCookieProcessor(cookielib.CookieJar())
opener&=&urllib2.build_opener(cookie_support)
urllib2.install_opener(opener)
content&=&urllib2.urlopen('http://XXXX').read()
& & 关键在于CookieJar(),它用于管理HTTP cookie值、存储HTTP请求生成的cookie、向传出的HTTP请求添加cookie的对象。整个cookie都存储在内存中,对CookieJar实例进行垃圾回收后cookie也将丢失,所有过程都不需要单独去操作。
& 手动添加cookie
cookie&=&"PHPSESSID=91rurfqm2329bopnosfu4fvmu7;&kmsign=55d2c12c9b1e3;&KMUID=b6Ejc1XSwPq9o756AxnBAg="
request.add_header("Cookie",&cookie)
4、伪装成浏览器
& & 某些网站反感爬虫的到访,于是对爬虫一律拒绝请求。所以用urllib2直接访问网站经常会出现HTTP Error 403: Forbidden的情况
对有些 header 要特别留意,Server 端会针对这些 header 做检查
& 1.User-Agent 有些 Server 或 Proxy 会检查该值,用来判断是否是浏览器发起的 Request
& 2.Content-Type 在使用 REST 接口时,Server 会检查该值,用来确定 HTTP Body 中的内容该怎样解析。
这时可以通过修改http包中的header来实现,代码片段如下:
import&urllib2
headers&=&{
&&&&'User-Agent':'Mozilla/5.0&(W&U;&Windows&NT&6.1;&en-US;&rv:1.9.1.6)&Gecko/&Firefox/3.5.6'
request&=&urllib2.Request(
&&&&url&=&'http://my.oschina.net/jhao104/blog?catalog=3463517',
&&&&headers&=&headers
print&urllib2.urlopen(request).read()
5、页面解析
& & 对于页面解析最强大的当然是正则表达式,这个对于不同网站不同的使用者都不一样,就不用过多的说明,附两个比较好的网址:
正则表达式入门:&
正则表达式在线测试:&
其次就是解析库了,常用的有两个lxml和BeautifulSoup,对于这两个的使用介绍两个比较好的网站:
BeautifulSoup:&
对于这两个库,我的评价是,都是HTML/XML的处理库,Beautifulsoup纯python实现,效率低,但是功能实用,比如能用通过结果搜索获得某个HTML节点的源码;lxmlC语言编码,高效,支持Xpath
6、验证码的处理
对于一些简单的验证码,可以进行简单的识别。本人也只进行过一些简单的验证码识别。但是有些反人类的验证码,比如12306,可以通过打码平台进行人工打码,当然这是要付费的。
7、gzip压缩
& & 有没有遇到过某些网页,不论怎么转码都是一团乱码。哈哈,那说明你还不知道许多web服务具有发送压缩数据的能力,这可以将网络线路上传输的大量数据消减 60% 以上。这尤其适用于 XML web 服务,因为 XML 数据 的压缩率可以很高。
但是一般服务器不会为你发送压缩数据,除非你告诉服务器你可以处理压缩数据。
于是需要这样修改代码:
import&urllib2,&httplib
request&=&urllib2.Request('')
request.add_header('Accept-encoding',&'gzip')&&&&&&&&1
opener&=&urllib2.build_opener()
f&=&opener.open(request)
这是关键:创建Request对象,添加一个 Accept-encoding 头信息告诉服务器你能接受 gzip 压缩数据
然后就是解压缩数据:
import&StringIO
import&gzip
compresseddata&=&f.read()&
compressedstream&=&StringIO.StringIO(compresseddata)
gzipper&=&gzip.GzipFile(fileobj=compressedstream)&
print&gzipper.read()
8、多线程并发抓取
& & 单线程太慢的话,就需要多线程了,这里给个简单的线程池模板 这个程序只是简单地打印了1-10,但是可以看出是并发的。
虽然说python的多线程很鸡肋,但是对于爬虫这种网络频繁型,还是能一定程度提高效率的。
from&threading&import&Thread
from&Queue&import&Queue
from&time&import&sleep
#&q是任务队列
#NUM是并发线程总数
#JOBS是有多少任务
q&=&Queue()
#具体的处理函数,负责处理单个任务
def&do_somthing_using(arguments):
&&&&print&arguments
#这个是工作进程,负责不断从队列取数据并处理
def&working():
&&&&while&True:
&&&&&&&&arguments&=&q.get()
&&&&&&&&do_somthing_using(arguments)
&&&&&&&&sleep(1)
&&&&&&&&q.task_done()
#fork&NUM个线程等待队列
for&i&in&range(NUM):
&&&&t&=&Thread(target=working)
&&&&t.setDaemon(True)
&&&&t.start()
#把JOBS排入队列
for&i&in&range(JOBS):
&&&&q.put(i)
#等待所有JOBS完成
转载请注明来源:开源中国&&
欢迎关注微信公众号: Pythoner每日一报
人打赏支持
码字总数 58301
可以利用Twisted模块,http://blog.csdn.net/hanhuili/article/details/9389433
或者利用框架 scrapy,封装了异步功能
个人习惯问题,我觉得requests和urllib2相比 并没有太大建设性的好处
支付宝支付
微信扫码支付
打赏金额: ¥
已支付成功
打赏金额: ¥问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
请看这个网站,翻页是通过ajax的,并没有刷新界面。我做了爬虫想爬去上面的content,按照网上的教程,发送一个xmlhttprequest:放一个data和一个header:
#-*- coding: UTF-8 -*-
import sys
import time
from HTMLParser import HTMLParser
import requests
import random
from bs4 import BeautifulSoup
reload(sys)
sys.setdefaultencoding('utf8')
def get_info(url):
info_list=[]
headers={"User-Agent":"Mozilla/5.0 (M Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 QQBrowser/3.9."
'X-Requested-With': 'XMLHttpRequest'}
"searchKey":"虚拟化",
"bidType":"0",
"bidWay":"0",
"region":"0",
"solrType":"0",
"frontMobanType":"1",
"pageNum":"2",#应该就是通过传输这个pageNum给服务器实现翻页
"pageCount":"30"
content = requests.post(url,data=data,headers=headers).content#就是这里
#t = session.post(url,data,headers)
print content#无法print出内容,说是HTTP Status 405 - Request method 'POST' not supported
except Exception,e:
get_info('http://www./cggg?pageNum=1&pageCount=30&searchKey=%E5%AD%98%E5%82%A8&bidType=0&bidWay=0&region=0')
请各位分析一波,是哪里有误?
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
很明显,post地址错了,真正的地址是你截图选中的那条。
(回复里面插不了图,所以在这补充)浏览器没显示完整URL信息,你需要自己看下完整地址
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
你post的地址错了
url 应该是请求 问号后面的去掉
同步到新浪微博
分享到微博?
你好!看起来你挺喜欢这个内容,但是你还没有注册帐号。 当你创建了帐号,我们能准确地追踪你关注的问题,在有新答案或内容的时候收到网页和邮件通知。还能直接向作者咨询更多细节。如果上面的内容有帮助,记得点赞 (????)? 表示感谢。
明天提醒我
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:
扫扫下载 App「总结」Python抓取Ajax返回内容【python吧】_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0成为超级会员,使用一键签到本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:142,321贴子:
「总结」Python抓取Ajax返回内容收藏
今天做一个类似爬虫的工具的时候,遇到了Ajax的问题,经过一番努力,我做到了……哈哈,慢点把我的心得发给大家!
mark,顺便插楼
楼楼当初自学Python就是为了为塞班手机的基友能方便的查询到自己的英雄联盟战斗力,没想到做到做到就喜欢上了Python,于是楼楼做了一个网站专门为木有盒子的手机用户提供查询战斗力服务,Python做的,PHP改的。
应部分用户的要求,我准备加一个获取详细比赛信息的功能,也就是这个。如图!
为了这个,我从今天中午一直钻研到5点,虽然很累很累,但是其乐无穷……————不废话了!开门见山!
正文:&br/&大家在使用爬虫的时候是不是经常遇到要抓取的内容是动态调用的,我这里是Ajax,先来说说Ajax是什么吧!—————AJAX 指异步 JavaS cript 及 XML (Asynchronous JavaScript And XML)。—————楼楼的理解是,也就是相当于你访问一次网页,这时需要发送一个Http请求到服务端,然后服务端返回相应数据。但是你每次请求就需要刷新一次……||使用了Ajax的页面就能很好的解决这些,它能发送相应的Http请求,然后在网页上显示(不会刷新整个网页,只会刷新要显示的内容)。
怒插,顺便把程序发给我,我也试试查下我的战绩~~~
当你抓取一个页面的时候,如果是Ajax的页面,你并不能在返回的网页源码中看到你要抓取的内容,而是这样的数据:图!
楼楼当初做的时候,天真的以为我成功了,但是:你直接访问这个网站: 你会发现返回的网页源码为99,这可怎么办?
怎样解决这个问题:其实很简单,我们只需要模拟正常的浏览器请求就行,也就是加上一个Header……这里正常的Header还不行。因为是Ajax,我们在这里只在Header,看图中Mark的行……也就是加上一个这个,其他有无都无所谓就能获取Ajax返回的内容了。
最后,告诉新人一个分析网页请求的方法,也就是猎豹浏览器或者google浏览器中的审查元素!这两个是一样的……在网页右键—审查元素—Networks,然后刷新你的页面就能截取到他的网页请求……然后用Python模拟出来就行。
这里如果有什么不恰当的地方,请大神指正!
直接用headless browser这种类型的模块访问
楼主 这个东西很简单的,
ajax请求就是普通的请求, 没什么不一样,
直接使用requests或pycurl会比urllib2更简单一点难点:
当使用爬虫怕网页的时候, 会遇到不能执行页面中的javascript, 这一直是爬虫的难点之一 解决方法是使用webkit访问页面, 然后取其内容
这个要支持一下
重点是分析,而不是发请求,可以抓包分析所有tcp的80,也可以用httpwatch等
登录百度帐号推荐应用Python实现基于协程的异步爬虫 - 吃咯 - 博客园
随笔 - 150, 文章 - 18, 评论 - 2, 引用 - 0
Python实现基于协程的异步爬虫
一、课程介绍
1. 课程来源
本课程核心部分来自项目,作者是来自 MongoDB 的工程师 A. Jesse Jiryu Davis 与 Python 之父 Guido van Rossum。项目代码使用 MIT 协议,项目文档使用&&协议。
课程内容在原文档基础上做了稍许修改,增加了部分原理介绍,步骤的拆解分析及源代码注释。
2. 内容简介
传统计算机科学往往将大量精力放在如何追求更有效率的算法上。但如今大部分涉及网络的程序,它们的时间开销主要并不是在计算上,而是在维持多个Socket连接上。亦或是它们的事件循环处理的不够高效导致了更多的时间开销。对于这些程序来说,它们面临的挑战是如何更高效地等待大量的网络事件并进行调度。目前流行的解决方式就是使用异步I/O。
本课程将探讨几种实现爬虫的方法,从传统的线程池到使用协程,每节课实现一个小爬虫。另外学习协程的时候,我们会从原理入手,以ayncio协程库为原型,实现一个简单的异步编程模型。
本课程实现的爬虫为爬一个整站的爬虫,不会爬到站点外面去,且功能较简单,主要目的在于学习原理,提供实现并发与异步的思路,并不适合直接改写作为日常工具使用。
3. 课程知识点
本课程项目完成过程中,我们将学习:
线程池实现并发爬虫
回调方法实现异步爬虫
协程技术的介绍
一个基于协程的异步编程模型
协程实现异步爬虫
二、实验环境
本课程使用Python 3.4,所以本课程内运行py脚本都是使用python3命令。
打开终端,进入&Code&目录,创建&crawler&文件夹, 并将其作为我们的工作目录。
$ mkdir crawler && cd crawler
环保起见,测试爬虫的网站在本地搭建。
我们使用 Python 2.7 版本官方文档作为测试爬虫用的网站
wget http://labfile./courses/574/python-doc.zip
unzip python-doc.zip
安装serve,一个用起来很方便的静态文件服务器:
sudo npm install -g serve
启动服务器:
serve python-doc
如果访问不了npm的资源,也可以用以下方式开启服务器:
ruby -run -ehttpd python-doc -p 3000
访问localhost:3000查看网站:
三、实验原理
什么是爬虫?
网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
爬虫的工作流程
网络爬虫基本的工作流程是从一个根URL开始,抓取页面,解析页面中所有的URL,将还没有抓取过的URL放入工作队列中,之后继续抓取工作队列中的URL,重复抓取、解析,将解析到的url放入工作队列的步骤,直到工作队列为空为止。
线程池、回调、协程
我们希望通过并发执行来加快爬虫抓取页面的速度。一般的实现方式有三种:
线程池方式:开一个线程池,每当爬虫发现一个新链接,就将链接放入任务队列中,线程池中的线程从任务队列获取一个链接,之后建立socket,完成抓取页面、解析、将新连接放入工作队列的步骤。
回调方式:程序会有一个主循环叫做事件循环,在事件循环中会不断获得事件,通过在事件上注册解除回调函数来达到多任务并发执行的效果。缺点是一旦需要的回调操作变多,代码就会非常散,变得难以维护。
协程方式:同样通过事件循环执行程序,利用了Python&的生成器特性,生成器函数能够中途停止并在之后恢复,那么原本不得不分开写的回调函数就能够写在一个生成器函数中了,这也就实现了协程。
四、实验一:线程池实现爬虫
使用socket抓取页面需要先建立连接,之后发送GET类型的HTTP报文,等待读入,将读到的所有内容存入响应缓存。
def fetch(url):
sock = socket.socket()
sock.connect(('', 3000))
request = 'GET {} HTTP/1.0\r\nHost: localhost\r\n\r\n'.format(url)
sock.send(request.encode('ascii'))
response = b''
chunk = sock.recv(4096)
while chunk:
response += chunk
chunk = sock.recv(4096)
links = parse_links(response)
q.add(links)
默认的socket连接与读写是阻塞式的,在等待读入的这段时间的CPU占用是被完全浪费的。
默认这部分同学们都是学过的,所以就粗略记几个重点,没学过的同学可以直接参考廖雪峰的教程:
导入线程库:
import threading
开启一个线程的方法:
t = 你新建的线程
#开始运行线程
#你的当前函数就阻塞在这一步直到线程运行完
建立线程的两种方式:
线程同时操作一个全局变量时会产生线程竞争所以需要锁:
lock = threading.Lock()
lock.acquire()
#..操作全局变量..
lock.release()
多线程同步-队列
默认这部分同学们都是学过的,所以就粗略记几个重点,没学过的同学可以直接参考:
多线程同步就是多个线程竞争一个全局变量时按顺序读写,一般情况下要用锁,但是使用标准库里的Queue的时候它内部已经实现了锁,不用程序员自己写了。
导入队列类:
from queue import Queue
创建一个队列:
q = Queue(maxsize=0)
maxsize为队列大小,为0默认队列大小可无穷大。
队列是先进先出的数据结构:
q.put(item) #往队列添加一个item,队列满了则阻塞
q.get(item) #从队列得到一个item,队列为空则阻塞
还有相应的不等待的版本,这里略过。
队列不为空,或者为空但是取得item的线程没有告知任务完成时都是处于阻塞状态
#阻塞直到所有任务完成
线程告知任务完成使用task_done
q.task_done()
实现线程池
创建thread.py文件作为爬虫程序的文件。
我们使用seen_urls来记录已经解析到的url地址:
seen_urls = set(['/'])
创建Fetcher类:
class Fetcher(Thread):
def __init__(self, tasks):
Thread.__init__(self)
使用正则库与url解析库来解析抓取的页面,这里图方便用了正则,同学也可以用Beautifulsoup等专门用来解析页面的Python库:
import urllib.parse
在Fetcher中实现parse_links解析页面:
def parse_links(self, fetched_url, response):
if not response:
print('error: {}'.format(fetched_url))
return set()
if not self._is_html(response):
return set()
实现线程池类与main的部分:
class ThreadPool:
def __init__(self, num_threads):
self.tasks = Queue()
for _ in range(num_threads):
Fetcher(self.tasks)
def add_task(self, url):
self.tasks.put(url)
def wait_completion(self):
self.tasks.join()
if __name__ == '__main__':
start = time.time()
这里先贴出完整代码:
from queue import Queue
from threading import Thread, Lock
import urllib.parse
import socket
import time
seen_urls = set(['/'])
lock = Lock()
class Fetcher(Thread):
def __init__(self, tasks):
Thread.__init__(self)
self.tasks = tasks
self.daemon = True
self.start()
def run(self):
while True:
url = self.tasks.get()
print(url)
sock = socket.socket()
sock.connect(('localhost', 3000))
get = 'GET {} HTTP/1.0\r\nHost: localhost\r\n\r\n'.format(url)
sock.send(get.encode('ascii'))
response = b''
chunk = sock.recv(4096)
while chunk:
response += chunk
chunk = sock.recv(4096)
links = self.parse_links(url, response)
lock.acquire()
for link in links.difference(seen_urls):
self.tasks.put(link)
seen_urls.update(links)
lock.release()
self.tasks.task_done()
def parse_links(self, fetched_url, response):
if not response:
print('error: {}'.format(fetched_url))
return set()
if not self._is_html(response):
return set()
urls = set(re.findall(r'''(?i)href=["']?([^\s"'&&]+)''',
self.body(response)))
links = set()
for url in urls:
normalized = urllib.parse.urljoin(fetched_url, url)
parts = urllib.parse.urlparse(normalized)
if parts.scheme not in ('', 'http', 'https'):
host, port = urllib.parse.splitport(parts.netloc)
if host and host.lower() not in ('localhost'):
defragmented, frag = urllib.parse.urldefrag(parts.path)
links.add(defragmented)
return links
def body(self, response):
body = response.split(b'\r\n\r\n', 1)[1]
return body.decode('utf-8')
def _is_html(self, response):
head, body = response.split(b'\r\n\r\n', 1)
headers = dict(h.split(': ') for h in head.decode().split('\r\n')[1:])
return headers.get('Content-Type', '').startswith('text/html')
class ThreadPool:
def __init__(self, num_threads):
self.tasks = Queue()
for _ in range(num_threads):
Fetcher(self.tasks)
def add_task(self, url):
self.tasks.put(url)
def wait_completion(self):
self.tasks.join()
if __name__ == '__main__':
start = time.time()
pool = ThreadPool(4)
pool.add_task("/")
pool.wait_completion()
print('{} URLs fetched in {:.1f} seconds'.format(len(seen_urls),time.time() - start))
运行python3 thread.py命令查看效果(记得先开网站服务器):
使用标准库中的线程池
线程池直接使用multiprocessing.pool中的ThreadPool:
代码更改如下:
from multiprocessing.pool import ThreadPool
使用ThreadPool时,它处理的对象可以不是线程对象,实际上Fetcher的线程部分ThreadPool根本用不到。因为它自己内部已开了几个线程在等待任务输入。这里偷个懒就只把self.start()去掉了。可以把Fetcher的线程部分全去掉,效果是一样的。
ThreadPool活用了map函数,这里它将每一个Fetcher对象分配给线程池中的一个线程,线程调用了Fetcher的run函数。这里使用map_async是因为不希望它在那一步阻塞,我们希望在任务队列join的地方阻塞,那么到队列为空且任务全部处理完时程序就会继续执行了。
运行python3 thread.py命令查看效果:
线程池实现的缺陷
我们希望爬虫的性能能够进一步提升,但是我们没办法开太多的线程,因为线程的内存开销很大,每创建一个线程可能需要占用50k的内存。以及还有一点,网络程序的时间开销往往花在I/O上,socket I/O 阻塞时的那段时间是完全被浪费了的。那么要如何解决这个问题呢?
下节课你就知道啦,下节课见~

我要回帖

更多关于 手机店装修效果图 的文章

 

随机推荐