0
点赞
收藏
分享

微信扫一扫

基于生成器的异步爬虫框架

要实现一个基于生成器的异步爬虫框架,我们可以利用 Python 的 select 模块来模拟非阻塞 I/O,而无需使用 asyncio。生成器(yield)可以帮助我们保持任务的状态,并在请求完成时立即处理返回的数据,避免传统爬虫的等待耗时。

思路:

  1. 使用 select 模块进行多路复用
    select.select() 允许你同时监听多个 I/O 通道(在这个场景下是多个 HTTP 请求)。当某个请求的响应数据准备好时,select 会通知我们,进而处理该请求的结果。
  2. 使用生成器
    通过生成器来管理多个请求的状态,每次请求发起后会 yield 控制权,等待 I/O 操作完成后再继续执行。
  3. 非阻塞 I/O
    我们的目标是尽量不阻塞其他请求的处理。通过 select 来实现非阻塞 I/O,保证在发起多个请求后,可以并行地等待它们的响应。

步骤:

  1. 发起多个 HTTP 请求:使用 urllibrequests 等库。
  2. 模拟非阻塞 I/O:使用 select.select() 来监控多个请求。
  3. 处理请求结果:一旦某个请求完成,立即提取数据(如标题)。

示例代码:

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)

代码解析:

  1. 生成器 fetch_url
  • fetch_url 用于发送 HTTP 请求,获取页面内容,并提取 <title> 标签中的信息。
  • 通过 urllib.request.urlopen 来发送 HTTP 请求,获取响应内容。
  • extract_title 用来从 HTML 内容中提取 <title> 标签的内容。
  1. 非阻塞 I/O 使用 select
  • async_crawl 函数中,我们利用 select.select() 来监听多个连接(即多个网页请求)。select 会阻塞,直到有一个连接准备好读取数据。
  • 通过 readable, _, _ = select.select(connections, [], []),我们获得所有已经准备好的连接,这些连接可以立即读取数据。
  • 一旦一个连接的数据准备好,我们立即处理该连接并提取标题信息。
  1. 循环处理多个请求
  • 程序首先发起多个 HTTP 请求并将连接保存在 connections 列表中。
  • 使用 select.select() 来监控这些连接,处理已经返回数据的连接,提取页面标题。
  • 每当一个连接的响应完成后,即时关闭该连接。

优缺点:

优点

  • 非阻塞:使用 select.select() 进行多路复用,可以并行处理多个请求,而不需要等待每个请求完成。
  • 简单:代码相对简洁,使用生成器和 select 实现异步,避免了使用复杂的 asyncio 库。

缺点

  • 仅限于网络 I/O:这种方法只能应用于等待 I/O 操作的场景(如网络请求),不能处理 CPU 密集型任务。
  • 连接数限制select.select() 有最大文件描述符数量的限制,适用于小规模并发请求(如同时 5 个请求),如果请求量大,需要考虑其他并发模型(如使用 asyncio)。

扩展功能:

  • 错误处理:增强错误处理功能,比如处理请求超时、网络不可达等情况。
  • 支持更多协议:除了 HTTP 请求,还可以支持更多协议或处理文件 I/O、数据库查询等。
  • 并发量提升:对于更高并发的爬取需求,可以使用其他技术(如 asynciogevent)来进一步提升性能。

这个示例使用了生成器和 select 模块实现了一个简单的异步爬虫框架,虽然它不如 asyncio 那样强大,但在理解非阻塞 I/O 和事件驱动的基础概念时,这种实现方式非常直观。

举报

相关推荐

0 条评论