python异步爬虫
本文主要包括以下内容
- 线程池实现并发爬虫
- 回调方法实现异步爬虫
- 协程技术的介绍
- 一个基于协程的异步编程模型
- 协程实现异步爬虫
线程池、回调、协程
我们希望通过并发执行来加快爬虫抓取页面的速度。一般的实现方式有三种:
- 线程池方式:开一个线程池,每当爬虫发现一个新链接,就将链接放入任务队列中,线程池中的线程从任务队列获取一个链接,之后建立socket,完成抓取页面、解析、将新连接放入工作队列的步骤。
- 回调方式:程序会有一个主循环叫做事件循环,在事件循环中会不断获得事件,通过在事件上注册解除回调函数来达到多任务并发执行的效果。缺点是一旦需要的回调操作变多,代码就会非常散,变得难以维护。
- 协程方式:同样通过事件循环执行程序,利用了Python 的生成器特性,生成器函数能够中途停止并在之后恢复,那么原本不得不分开写的回调函数就能够写在一个生成器函数中了,这也就实现了协程。
线程池实现爬虫
python多线程建立线程的两种方式
#第一种:通过函数创建线程
def 函数a():
pass
t = threading.Thread(target=函数a,name=自己随便取的线程名字)
#第二种:继承线程类
class Fetcher(threading.Thread):
def __init__(self):
Thread.__init__(self):
#加这一步后主程序中断退出后子线程也会跟着中断退出
self.daemon = True
def run(self):
#线程运行的函数
pass
t = Fetcher()
多线程同步-队列
多线程同步就是多个线程竞争一个全局变量时按顺序读写,一般情况下要用锁,但是使用标准库里的Queue的时候它内部已经实现了锁,不用程序员自己写了。
导入队列类:
from queue import Queue
创建一个队列:
q = Queue(maxsize=0)
maxsize为队列大小,为0默认队列大小可无穷大。
队列是先进先出的数据结构:
q.put(item) #往队列添加一个item,队列满了则阻塞
q.get(item) #从队列得到一个item,队列为空则阻塞
还有相应的不等待的版本,这里略过。
队列不为空,或者为空但是取得item的线程没有告知任务完成时都是处于阻塞状态
q.join() #阻塞直到所有任务完成
线程告知任务完成使用task_done
q.task_done() #在线程内调用
完整代码
from queue import Queue
from threading import Thread, Lock
import urllib.parse
import socket
import re
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'):
continue
host, port = urllib.parse.splitport(parts.netloc)
if host and host.lower() not in ('localhost'):
continue
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))
事件驱动-回调函数实现爬虫
非阻塞I/O
如果使用非阻塞I/O,程序就不会傻傻地等在那里(比如等连接、等读取),而是会返回一个错误信息,虽然说是说错误信息,它其实就是叫你过一会再来的意思,编程的时候都不把它当错误看。
非阻塞I/O代码如下:
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect(('xkcd.com', 80))
except BlockingIOError:
pass
单线程上的多I/O
有了非阻塞I/O这个特性,我们就能够实现单线程上多个sockets的处理了,学过C语言网络编程的同学应该都认识select这个函数吧?不认识也不要紧,select函数如果你不设置它的超时时间它就是默认一直阻塞的,只有当有I/O事件发生时它才会被激活,然后告诉你哪个socket上发生了什么事件(读|写|异常),在Python中也有select,还有跟select功能相同但是更高效的poll,它们都是底层C函数的Python实现。
不过这里我们不使用select,而是用更简单好用的DefaultSelector,是Python 3.4后才出现的一个模块里的类,你只需要在非阻塞socket和事件上绑定回调函数就可以了。
代码如下:
from selectors import DefaultSelector, EVENT_WRITE
selector = DefaultSelector()
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect(('localhost', 3000))
except BlockingIOError:
pass
def connected():
selector.unregister(sock.fileno())
print('connected!')
selector.register(sock.fileno(), EVENT_WRITE, connected)
这里看一下selector.register的原型
register(fileobj, events, data=None)
其中fileobj可以是文件描述符也可以是文件对象(通过fileno得到),events是位掩码,指明发生的是什么事件,data 则是与指定文件(也就是我们的socket)与指定事件绑定在一起的数据。
如代码所示,selector.register 在该socket的写事件上绑定了回调函数connected(这里作为数据绑定)。在该socket上第一次发生的写事件意味着连接的建立,connected函数在连接建立成功后再解除了该socket上所有绑定的数据。
事件驱动
看了以上selector的使用方式,我想你会发现它很适合写成事件驱动的形式。
我们可以创建一个事件循环,在循环中不断获得I/O事件:
def loop():
while True:
events = selector.select()
#遍历事件并调用相应的处理
for event_key, event_mask in events:
callback = event_key.data
callback()
完整代码
from selectors import *
import socket
import re
import urllib.parse
import time
urls_todo = set(['/'])
seen_urls = set(['/'])
#追加了一个可以看最高并发数的变量
concurrency_achieved = 0
selector = DefaultSelector()
stopped = False
class Fetcher:
def __init__(self, url):
self.response = b''
self.url = url
self.sock = None
def fetch(self):
global concurrency_achieved
concurrency_achieved = max(concurrency_achieved, len(urls_todo))
self.sock = socket.socket()
self.sock.setblocking(False)
try:
self.sock.connect(('localhost', 3000))
except BlockingIOError:
pass
selector.register(self.sock.fileno(), EVENT_WRITE, self.connected)
def connected(self, key, mask):
selector.unregister(key.fd)
get = 'GET {} HTTP/1.0\r\nHost: localhost\r\n\r\n'.format(self.url)
self.sock.send(get.encode('ascii'))
selector.register(key.fd, EVENT_READ, self.read_response)
def read_response(self, key, mask):
global stopped
chunk = self.sock.recv(4096) # 4k chunk size.
if chunk:
self.response += chunk
else:
selector.unregister(key.fd) # Done reading.
links = self.parse_links()
for link in links.difference(seen_urls):
urls_todo.add(link)
Fetcher(link).fetch()
seen_urls.update(links)
urls_todo.remove(self.url)
if not urls_todo:
stopped = True
print(self.url)
def body(self):
body = self.response.split(b'\r\n\r\n', 1)[1]
return body.decode('utf-8')
def parse_links(self):
if not self.response:
print('error: {}'.format(self.url))
return set()
if not self._is_html():
return set()
urls = set(re.findall(r'''(?i)href=["']?([^\s"'<>]+)''',
self.body()))
links = set()
for url in urls:
normalized = urllib.parse.urljoin(self.url, url)
parts = urllib.parse.urlparse(normalized)
if parts.scheme not in ('', 'http', 'https'):
continue
host, port = urllib.parse.splitport(parts.netloc)
if host and host.lower() not in ('localhost'):
continue
defragmented, frag = urllib.parse.urldefrag(parts.path)
links.add(defragmented)
return links
def _is_html(self):
head, body = self.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')
start = time.time()
fetcher = Fetcher('/')
fetcher.fetch()
while not stopped:
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback(event_key, event_mask)
print('{} URLs fetched in {:.1f} seconds, achieved concurrency = {}'.format(
len(seen_urls), time.time() - start, concurrency_achieved))
事件驱动-协程实现爬虫
什么是协程?
协程其实是比起一般的子例程而言更宽泛的存在,子例程是协程的一种特例。
子例程的起始处是惟一的入口点,一旦退出即完成了子例程的执行,子例程的一个实例只会返回一次。
协程可以通过yield来调用其它协程。通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。
协程的起始处是第一个入口点,在协程里,返回点之后是接下来的入口点。子例程的生命期遵循后进先出(最后一个被调用的子例程最先返回);相反,协程的生命期完全由他们的使用的需要决定。
还记得我们什么时候会用到yield吗,就是在生成器(generator)里,在迭代的时候每次执行next(generator)生成器都会执行到下一次yield的位置并返回,可以说生成器就是例程。
生成器实现协程模型
虽然生成器拥有一个协程该有的特性,但光这样是不够的,做异步编程仍是困难的,我们需要先用生成器实现一个协程异步编程的简单模型,它同时也是Python标准库asyncio的简化版,正如asyncio的实现,我们会用到生成器,Future类,以及yield from语句。
首先实现Future类, Future类可以认为是专门用来存储将要发送给协程的信息的类。
class Future:
def __init__(self):
self.result = None
self._callbacks = []
def add_done_callback(self, fn):
self._callbacks.append(fn)
def set_result(self, result):
self.result = result
for fn in self._callbacks:
fn(self)
Future对象最开始处在挂起状态,当调用set_result时被激活,并运行注册的回调函数,该回调函数多半是对协程发送信息让协程继续运行下去的函数。
我们改造一下之前从fetch到connected的函数,加入Future与yield。
这是之前回调实现的fetch:
class Fetcher:
def fetch(self):
self.sock = socket.socket()
self.sock.setblocking(False)
try:
self.sock.connect(('localhost', 3000))
except BlockingIOError:
pass
selector.register(self.sock.fileno(),
EVENT_WRITE,
self.connected)
def connected(self, key, mask):
print('connected!')
# ...后面省略...
改造后,我们将连接建立后的部分也放到了fetch中。
class Fetcher:
def fetch(self):
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect(('localhost', 3000))
except BlockingIOError:
pass
f = Future()
def on_connected():
#连接建立后通过set_result协程继续从yield的地方往下运行
f.set_result(None)
selector.register(sock.fileno(),
EVENT_WRITE,
on_connected)
yield f
selector.unregister(sock.fileno())
print('connected!')
fetcher是一个生成器函数,我们创建一个Future实例,yield它来暂停fetch的运行直到连接建立f.set_result(None)的时候,生成器才继续运行。那set_result时运行的回调函数是哪来的呢?这里引入Task类:
class Task:
def __init__(self, coro):
#协程
self.coro = coro
#创建并初始化一个为None的Future对象
f = Future()
f.set_result(None)
#步进一次(发送一次信息)
#在初始化的时候发送是为了协程到达第一个yield的位置,也是为了注册下一次的步进
self.step(f)
def step(self, future):
try:
#向协程发送消息并得到下一个从协程那yield到的Future对象
next_future = self.coro.send(future.result)
except StopIteration:
return
next_future.add_done_callback(self.step)
fetcher = Fetcher('/')
Task(fetcher.fetch())
loop()
流程大致是这样的,首先Task初始化,向fetch生成器发送None信息(也可以想象成step调用了fetch,参数是None),fetch得以从开头运行到第一个yield的地方并返回了一个Future对象给step的next_future,然后step就在这个得到的Future对象注册了step。当连接建立时on_connected就会被调用,再一次向协程发送信息,协程就会继续往下执行了。
使用yield from分解协程
一旦socket连接建立成功,我们发送HTTP GET请求到服务器并在之后读取服务器响应。现在这些步骤不用再分散在不同的回调函数里了,我们可以将其放在同一个生成器函数中:
def fetch(self):
# ... 省略连接的代码
sock.send(request.encode('ascii'))
while True:
f = Future()
def on_readable():
f.set_result(sock.recv(4096))
selector.register(sock.fileno(),
EVENT_READ,
on_readable)
chunk = yield f
selector.unregister(sock.fileno())
if chunk:
self.response += chunk
else:
# 完成读取
break
但是这样代码也会越积越多,可不可以分解生成器函数的代码呢,从协程中提取出子协程?Python 3 的yield from能帮助我们完成这部分工作。:
>>> def gen_fn():
... result = yield 1
... print('result of yield: {}'.format(result))
... result2 = yield 2
... print('result of 2nd yield: {}'.format(result2))
... return 'done'
...
yield from得到的子协程最后return的返回值
完整代码
from selectors import *
import socket
import re
import urllib.parse
import time
class Future:
def __init__(self):
self.result = None
self._callbacks = []
def result(self):
return self.result
def add_done_callback(self, fn):
self._callbacks.append(fn)
def set_result(self, result):
self.result = result
for fn in self._callbacks:
fn(self)
def __iter__(self):
yield self
return self.result
class Task:
def __init__(self, coro):
self.coro = coro
f = Future()
f.set_result(None)
self.step(f)
def step(self, future):
try:
next_future = self.coro.send(future.result)
except StopIteration:
return
next_future.add_done_callback(self.step)
urls_seen = set(['/'])
urls_todo = set(['/'])
#追加了一个可以看最高并发数的变量
concurrency_achieved = 0
selector = DefaultSelector()
stopped = False
def connect(sock, address):
f = Future()
sock.setblocking(False)
try:
sock.connect(address)
except BlockingIOError:
pass
def on_connected():
f.set_result(None)
selector.register(sock.fileno(), EVENT_WRITE, on_connected)
yield from f
selector.unregister(sock.fileno())
def read(sock):
f = Future()
def on_readable():
f.set_result(sock.recv(4096)) # Read 4k at a time.
selector.register(sock.fileno(), EVENT_READ, on_readable)
chunk = yield from f
selector.unregister(sock.fileno())
return chunk
def read_all(sock):
response = []
chunk = yield from read(sock)
while chunk:
response.append(chunk)
chunk = yield from read(sock)
return b''.join(response)
class Fetcher:
def __init__(self, url):
self.response = b''
self.url = url
def fetch(self):
global concurrency_achieved, stopped
concurrency_achieved = max(concurrency_achieved, len(urls_todo))
sock = socket.socket()
yield from connect(sock, ('localhost', 3000))
get = 'GET {} HTTP/1.0\r\nHost: localhost\r\n\r\n'.format(self.url)
sock.send(get.encode('ascii'))
self.response = yield from read_all(sock)
self._process_response()
urls_todo.remove(self.url)
if not urls_todo:
stopped = True
print(self.url)
def body(self):
body = self.response.split(b'\r\n\r\n', 1)[1]
return body.decode('utf-8')
def _process_response(self):
if not self.response:
print('error: {}'.format(self.url))
return
if not self._is_html():
return
urls = set(re.findall(r'''(?i)href=["']?([^\s"'<>]+)''',
self.body()))
for url in urls:
normalized = urllib.parse.urljoin(self.url, url)
parts = urllib.parse.urlparse(normalized)
if parts.scheme not in ('', 'http', 'https'):
continue
host, port = urllib.parse.splitport(parts.netloc)
if host and host.lower() not in ('localhost'):
continue
defragmented, frag = urllib.parse.urldefrag(parts.path)
if defragmented not in urls_seen:
urls_todo.add(defragmented)
urls_seen.add(defragmented)
Task(Fetcher(defragmented).fetch())
def _is_html(self):
head, body = self.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')
start = time.time()
fetcher = Fetcher('/')
Task(fetcher.fetch())
while not stopped:
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback()
print('{} URLs fetched in {:.1f} seconds, achieved concurrency = {}'.format(
len(urls_seen), time.time() - start, concurrency_achieved))
总结
至此,我们在学习的过程中掌握了:
- 线程池实现并发爬虫
- 回调方法实现异步爬虫
- 协程技术的介绍
- 一个基于协程的异步编程模型
- 协程实现异步爬虫
三种爬虫的实现方式中线程池是最坏的选择,因为它既占用内存,又有线程竞争的危险需要程序员自己编程解决,而且产生的I/O阻塞也浪费了CPU占用时间。再来看看回调方式,它是一种异步方法,所以I/O阻塞的问题解决了,而且它是单线程的不会产生竞争,问题好像都解决了。然而它引入了新的问题,它的问题在于以这种方式编写的代码不好维护,也不容易debug。看来协程才是最好的选择,我们实现的协程异步编程模型使得一个单线程能够很容易地改写为协程。那是不是每一次做异步编程都要实现Task、Future呢?不是的,你可以直接使用asyncio官方标准协程库,它已经帮你把Task、Future封装好了,你根本不会感受到它们的存在,是不是很棒呢?如果你使用Python 3.5那更好,已经可以用原生的协程了,Python 3.5追加了async def,await等协程相关的关键词。
python异步爬虫的更多相关文章
- [python]新手写爬虫v2.5(使用代理的异步爬虫)
开始 开篇:爬代理ip v2.0(未完待续),实现了获取代理ips,并把这些代理持久化(存在本地).同时使用的是tornado的HTTPClient的库爬取内容. 中篇:开篇主要是获取代理ip:中篇打 ...
- Python实现基于协程的异步爬虫
一.课程介绍 1. 课程来源 本课程核心部分来自<500 lines or less>项目,作者是来自 MongoDB 的工程师 A. Jesse Jiryu Davis 与 Python ...
- python 基于aiohttp的异步爬虫实战
钢铁知识库,一个学习python爬虫.数据分析的知识库.人生苦短,快用python. 之前我们使用requests库爬取某个站点的时候,每发出一个请求,程序必须等待网站返回响应才能接着运行,而在整个爬 ...
- (转)新手写爬虫v2.5(使用代理的异步爬虫)
开始 开篇:爬代理ip v2.0(未完待续),实现了获取代理ips,并把这些代理持久化(存在本地).同时使用的是tornado的HTTPClient的库爬取内容. 中篇:开篇主要是获取代理ip:中篇打 ...
- 深入理解 Python 异步编程(上)
http://python.jobbole.com/88291/ 前言 很多朋友对异步编程都处于"听说很强大"的认知状态.鲜有在生产项目中使用它.而使用它的同学,则大多数都停留在知 ...
- 利用aiohttp制作异步爬虫
asyncio可以实现单线程并发IO操作,是Python中常用的异步处理模块.关于asyncio模块的介绍,笔者会在后续的文章中加以介绍,本文将会讲述一个基于asyncio实现的HTTP框架--a ...
- Python网络爬虫学习总结
1.检查robots.txt 让爬虫了解爬取该网站时存在哪些限制. 最小化爬虫被封禁的可能,而且还能发现和网站结构相关的线索. 2.检查网站地图(robots.txt文件中发现的Sitemap文件) ...
- python异步初步窥探
1.异步之难:因为其执行吮吸不可预料,当下正要发生什么事件不可预料. 程序下一步行为往往依赖上一步值执行结果,如何知晓上次异步调用已完成并获取结果, 回调成了必然选择,那又 ...
- Python 网络爬虫干货总结
Python 网络爬虫干货总结 爬取 对于爬取来说,我们需要学会使用不同的方法来应对不同情景下的数据抓取任务. 爬取的目标绝大多数情况下要么是网页,要么是 App,所以这里就分为这两个大类别来进行了介 ...
随机推荐
- 关于ListView中notifyDataSetChanged()刷新数据不更新原因
使用Listview的时候: 当要动态显示更改后的数据(例如数据库改动), 很多人应该都用过notifyDataSetChanged();这个方法来刷新Listview,显示改后的数据. 这时候就要注 ...
- MongoDB的基本使用(二)
上一个文档说明了如何搭建一个Windows端MongoDB服务器,下面将简单介绍MongoDB的基本操作命令. 1. show dbs : 显示所有数据库 2. use bochao : 使用boch ...
- BZOJ1212——L语言
题目大意:每一个字符串都可以分解成一些个单词组成,现在给你一些单词,再给你一个字符串, dp吧,设f[i]为从0开始,到i结束的字符串前缀是否可以被分解,因为单词长度很小,所以,这就T了, (什么逻辑 ...
- iOS 不规则的ImageView
http://blog.csdn.net/kevinpake/article/details/41205715 我们在做iOS开发的时候,往往需要实现不规则形状的头像,如: 那如何去实现? 通常图片都 ...
- eclipse中整合springMvc和velocity
1.项目所需要的jar包(有些可能多余) 2.在src目录下创建一个bean 一个一个controller ,路径如下 person代码: package com.test.bean; import ...
- Kali Linux渗透基础知识整理(四):维持访问
Kali Linux渗透基础知识整理系列文章回顾 维持访问 在获得了目标系统的访问权之后,攻击者需要进一步维持这一访问权限.使用木马程序.后门程序和rootkit来达到这一目的.维持访问是一种艺术形式 ...
- 40 网络相关函数(八)——live555源码阅读(四)网络
40 网络相关函数(八)——live555源码阅读(四)网络 40 网络相关函数(八)——live555源码阅读(四)网络 简介 15)writeSocket向套接口写数据 TTL的概念 函数send ...
- dp题目列表
此文转载别人,希望自己能够做完这些题目! 1.POJ动态规划题目列表 容易:1018, 1050, 1083, 1088, 1125, 1143, 1157, 1163, 1178, 1179, 11 ...
- phpcms后台进入地址(包含No permission resources错误)
安装phpcms后却不知道怎么进入后台,实际上输入如下地址即可进入后台登陆界面: http://你的域名/admin.php 如果出现No permission resources.错误,可能是之前修 ...
- POJ 1068
http://poj.org/problem?id=1068 这道题是一道模拟的题目 题目大意呢,p代表前面的'('的个数,而w代表这个括号所包括的括号的个数: 给你p,要你求w: 解题思路: 首先, ...