要实现一个基于生成器的异步爬虫框架,我们可以利用 Python 的 select
模块来模拟非阻塞 I/O,而无需使用 asyncio
。生成器(yield
)可以帮助我们保持任务的状态,并在请求完成时立即处理返回的数据,避免传统爬虫的等待耗时。
思路:
- 使用
select
模块进行多路复用:select.select()
允许你同时监听多个 I/O 通道(在这个场景下是多个 HTTP 请求)。当某个请求的响应数据准备好时,select
会通知我们,进而处理该请求的结果。 - 使用生成器:
通过生成器来管理多个请求的状态,每次请求发起后会yield
控制权,等待 I/O 操作完成后再继续执行。 - 非阻塞 I/O:
我们的目标是尽量不阻塞其他请求的处理。通过select
来实现非阻塞 I/O,保证在发起多个请求后,可以并行地等待它们的响应。
步骤:
- 发起多个 HTTP 请求:使用
urllib
或requests
等库。 - 模拟非阻塞 I/O:使用
select.select()
来监控多个请求。 - 处理请求结果:一旦某个请求完成,立即提取数据(如标题)。
示例代码:
import socket
import select
import urllib.request
from io import BytesIO
# 发送 HTTP 请求的生成器
def fetch_url(url):
# 模拟一个 HTTP 请求,返回一个简单的 HTTP 响应(仅包含标题)
req = urllib.request.Request(url)
try:
with urllib.request.urlopen(req) as response:
content = response.read()
# 假设页面返回内容中包含 <title> 标签
title = extract_title(content)
print(f"网页标题: {title} (来自 {url})")
except Exception as e:
print(f"无法获取 {url}:{e}")
# 从网页内容中提取 <title> 标签内容
def extract_title(content):
start_index = content.find(b"<title>")
end_index = content.find(b"</title>")
if start_index != -1 and end_index != -1:
title = content[start_index + len(b"<title>"):end_index]
return title.decode('utf-8')
return "无标题"
# 使用 select 模拟异步请求
def async_crawl(urls):
# 创建一个 socket 连接池,模拟发起多个 HTTP 请求
connections = []
for url in urls:
# 通过 urllib 请求每个页面
req = urllib.request.Request(url)
conn = urllib.request.urlopen(req)
connections.append(conn)
# 使用 select 监听这些连接的响应
while connections:
# 创建读取集合,用于 select 监听
readable, _, _ = select.select(connections, [], [])
for conn in readable:
try:
# 读取每个响应的内容(页面)
content = conn.read()
title = extract_title(content)
print(f"网页标题: {title} (来自 {conn.geturl()})")
except Exception as e:
print(f"读取 {conn.geturl()} 错误:{e}")
finally:
# 处理完后关闭连接
conn.close()
connections.remove(conn)
# 主程序
if __name__ == "__main__":
urls = [
"http://example.com",
"https://www.python.org",
"https://www.wikipedia.org",
"https://www.github.com",
"https://www.stackoverflow.com"
]
async_crawl(urls)
代码解析:
- 生成器
fetch_url
:
fetch_url
用于发送 HTTP 请求,获取页面内容,并提取<title>
标签中的信息。- 通过
urllib.request.urlopen
来发送 HTTP 请求,获取响应内容。 extract_title
用来从 HTML 内容中提取<title>
标签的内容。
- 非阻塞 I/O 使用
select
:
- 在
async_crawl
函数中,我们利用select.select()
来监听多个连接(即多个网页请求)。select
会阻塞,直到有一个连接准备好读取数据。 - 通过
readable, _, _ = select.select(connections, [], [])
,我们获得所有已经准备好的连接,这些连接可以立即读取数据。 - 一旦一个连接的数据准备好,我们立即处理该连接并提取标题信息。
- 循环处理多个请求:
- 程序首先发起多个 HTTP 请求并将连接保存在
connections
列表中。 - 使用
select.select()
来监控这些连接,处理已经返回数据的连接,提取页面标题。 - 每当一个连接的响应完成后,即时关闭该连接。
优缺点:
优点:
- 非阻塞:使用
select.select()
进行多路复用,可以并行处理多个请求,而不需要等待每个请求完成。 - 简单:代码相对简洁,使用生成器和
select
实现异步,避免了使用复杂的asyncio
库。
缺点:
- 仅限于网络 I/O:这种方法只能应用于等待 I/O 操作的场景(如网络请求),不能处理 CPU 密集型任务。
- 连接数限制:
select.select()
有最大文件描述符数量的限制,适用于小规模并发请求(如同时 5 个请求),如果请求量大,需要考虑其他并发模型(如使用asyncio
)。
扩展功能:
- 错误处理:增强错误处理功能,比如处理请求超时、网络不可达等情况。
- 支持更多协议:除了 HTTP 请求,还可以支持更多协议或处理文件 I/O、数据库查询等。
- 并发量提升:对于更高并发的爬取需求,可以使用其他技术(如
asyncio
或gevent
)来进一步提升性能。
这个示例使用了生成器和 select
模块实现了一个简单的异步爬虫框架,虽然它不如 asyncio
那样强大,但在理解非阻塞 I/O 和事件驱动的基础概念时,这种实现方式非常直观。