Nstproxy 现已升级为 Nstdata。了解此次变化。
周一至周五 09:00 - 18:00(UTC+08:00) 公司地址
19H MAXGRAND PLAZA NO.3 TAI YAU STREET SAN PO KONG, KOWLOON HONG KONG ©2026 NST LABS TECH LTD. 保留所有权利。
使用 Python 和 Aiohttp 的异步网页爬虫
Ivy Lin Community & Content Lead
Asyncio用于网页抓取:正确处理并发请求 TL;DR
asyncio 提高了许多独立网络等待的吞吐量;它并不会加快 HTML 解析速度,也不允许提高站点的请求速率。
重用一个 aiohttp.ClientSession,设置总超时时间和连接超时时间,并限制全局和每个主机的并发性。
信号量限制同时进行的任务,而不是每秒请求数。当源需要时,添加明确的每主机节奏。
仅对瞬态失败如 429、502、503 和 504 进行重试,需设置延迟并处理 Retry-After;不要盲目重试每个 4xx 响应。
对于 JavaScript 渲染的网站或递归发现,调用受管理的渲染和爬取 API,而不是期望 aiohttp 执行 JavaScript。
何时应使用 Asyncio 进行网络爬取?
当抓取器大部分时间都在等待许多允许的 HTTP 响应时,使用 asyncio。一个工作线程可以管理多个正在进行的请求,而无需为每个请求开启一个线程。收益取决于源的延迟、允许的请求速率、解析成本和下游存储。如果源只允许每几秒发出一个请求,则异步客户端无法合理地将其转换为高频率爬取。 Nstdata Crawl 产品 是处理这里讨论的获取和转换步骤的受管理选项。
Python asyncio 任务文档 解释了协程如何被调度。aiohttp 客户端指南 建议重用一个会话,而不是为每个请求创建一个。会话保持一个连接池和共享配置。仅对您被授权收集的源使用此教程。
大规模渲染指南 涵盖了与 HTTP 并发不同的瓶颈。
开始前需要准备什么?
在虚拟环境中安装 Python 3 和 aiohttp。示例还使用 Beautiful Soup 从返回的 HTML 中提取标题:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install aiohttp beautifulsoup4
创建一份短的允许列表,记录每个主机的许可请求速率,并选择一个可联系的用户代理。对于 Nstdata Crawl,获取一个账户和 API 密钥;记录的身份验证头为 x-api-key。对于标准的受管理爬取请求,您不需要代理主机、端口、用户名、密码或协议值,因为 Nstdata 处理代理路由。
并发与速率限制有何不同? 并发是同时进行的请求数。速率是新请求开始的频率。一个信号量的值为三,可能在同一时刻产生三个请求,然后在响应完成后再产生三个请求。尽管只有三个请求处于活动状态,这可能会违反源的预期节奏。当主机需要最大速率时,使用每主机的启动间隔或令牌桶。
在三个级别上设置限制:活跃任务的总数、连接池的 limit_per_host 和每个主机的启动间隔。在试点期间保持保守,并根据源政策、429 响应、延迟和错误率进行调整。下面的示例在每个主机之间使用一秒的间隔,最多三个连接;这些是说明性设置,而不是通用的安全速率。
如何使用 Aiohttp 并发抓取页面?
方法 1:运行一个有限的 HTML 抓取器 **最佳用于:**几乎所有数据出现在初始 HTTP 响应中的静态页面。
步骤 1. 将以下脚本保存为 async_scrape.py。将 URLS 替换为批准的 URL,并将 MIN_HOST_INTERVAL 调整为实际源政策。
import asyncio
import time
from collections import defaultdict
from urllib . parse import urlparse
import aiohttp
from bs4 import BeautifulSoup
URLS = [ "https://example.com/" ] # 替换为批准的 URL。
MAX_CONCURRENT = 8
MAX_PER_HOST = 3
MIN_HOST_INTERVAL = 1.0
RETRYABLE = { 429 , 502 , 503 , 504 }
class HostPacer :
def __init__ ( self , interval : float ) :
self . interval = interval
self . locks = defaultdict ( asyncio . Lock )
self . next_start = defaultdict ( float )
async def wait ( self , host : str ) :
async with self . locks [ host ] :
delay = self . next_start [ host ] - time . monotonic ( )
if delay > 0 :
await asyncio . sleep ( delay )
self . next_start [ host ] = time . monotonic ( ) + self . interval
def retry_delay ( header , attempt : int ) - > float :
if header and header . isdigit ( ) :
return min ( float ( header ) , 60.0 )
return min ( 2 ** attempt , 30.0 )
async def fetch ( session , pacer , semaphore , url ) :
host = urlparse ( url ) . hostname
如果不提供主机或 urlparse ( url ) . scheme 不在 { "http" , "https" } 中 :
raise ValueError ( f"不支持的 URL: { url } " )
对于尝试次数在范围 3 内 :
await pacer . wait ( host )
try :
async with semaphore :
async with session . get ( url ) as response :
if response . status in RETRYABLE :
delay = retry_delay ( response . headers . get ( "Retry-After" ) , attempt )
else :
response . raise_for_status ( )
html = await response . text ( )
soup = BeautifulSoup ( html , "html.parser" )
title = soup . title . get_text ( " " , strip = True ) if soup . title else None
if not title :
raise ValueError ( f"缺少标题: { url } " )
return { "url" : str ( response . url ) , "status" : response . status , "title" : title }
except ( aiohttp . ClientError , asyncio . TimeoutError ) :
if attempt == 2 :
raise
delay = min ( 2 ** attempt , 30.0 )
if attempt == 2 :
raise RuntimeError ( f"重试限制已达到: { url } " )
await asyncio . sleep ( delay )
async def main ( ) :
timeout = aiohttp . ClientTimeout ( total = 30 , connect = 10 )
connector = aiohttp . TCPConnector ( limit = MAX_CONCURRENT , limit_per_host = MAX_PER_HOST )
headers = { "User-Agent" : "ExampleResearchBot/1.0 (contact: team@example.org)" }
pacer = HostPacer ( MIN_HOST_INTERVAL )
semaphore = asyncio . Semaphore ( MAX_CONCURRENT )
async with aiohttp . ClientSession ( timeout = timeout , connector = connector , headers = headers ) as session :
results = await asyncio . gather (
* ( fetch ( session , pacer , semaphore , url ) for url in URLS ) ,
return_exceptions = True ,
)
for url , result in zip ( URLS , results ) :
print ( url , result )
if __name__ == "__main__" :
asyncio . run ( main ( ) )
步骤 2. 运行 python async_scrape.py 并检查每个结果或异常。成功的 example.com 响应应该包含其文档标题;该脚本并不声称每个目标页面都有一个 <title> 元素。
步骤 3. 仅在验证提取字段后添加持久性。存储 URL、获取时间、解析器版本和拒绝原因。用与规范 URL 或源 ID 键控的幂等写入替换 print。
限制: 上面的 Retry-After 帮助程序处理数字秒。HTTP 日期值需要在生产使用之前进行日期解析。示例还在等待时持有每个主机的速率限制锁,故意串行化该主机的请求开始。
如何处理数千个 URL 而不同时创建数千个任务?
方法 2: 使用有界工作队列 最佳适用范围: 大型 URL 列表或持续发现的页面。
步骤 1. 将已批准的 URL 放入一个持久化队列或数据库中,带有唯一键、尝试计数和下一个可获取时间。内存中的 asyncio.Queue 在进程内很有用,但无法恢复机器崩溃。
步骤 2. 启动固定数量的工作者,每个工作者提取一个 URL,调用有界获取器,验证记录,并在写入持久输出后确认任务。
步骤 3. 记录失败及原因和重试时间。对耗尽重试或违反政策的死信 URL。对解析器或存储层落后的情况施加反压。
限制: asyncio.gather在一百万个 URL 协程上会消耗内存,即使使用了信号量。队列限制任务创建并提供检查点工作的地方。
何时使用 Nstdata 爬虫而不是 Aiohttp?
方法 3: 委托渲染和发现 最佳适用范围: JavaScript 生成的内容或经过审核站点上的链接页面。
步骤 1. 获取 Nstdata API 密钥并定义批准的 URL 范围。 爬虫 API 记录 x-api-key 验证和具有 maxDepth、maxPages、includeUrls 和 excludeUrls 的站点工作。
步骤 2. 提交一个有界作业到 Nstdata Crawl 。轮询状态并通过文档化的端点检索已完成的页面。Nstdata 处理页面发现、递归抓取、JavaScript 渲染和代理路由。
步骤 3. 根据您的应用需求解析返回的 Markdown、HTML、原始数据、链接、截图或 PDF。在索引或训练之前验证内容并存储来源。
限制: 管理的 API 不会替代您的源权限、跨网站调度程序或任意产品架构提取。请勿使用它来规避访问限制。
为什么并发请求会失败或返回坏数据? 429 表示源正在进行速率限制;减少每个主机的启动次数并遵循响应的重试指导。超时可能表示网络问题或源响应缓慢;重试少量次数并记录失败,而不是无限重试。带有空标题的 200 可能是一个 JavaScript shell、同意页面或解析器不匹配。在更改并发度之前,检查返回的 HTML 和浏览器网络行为。
监控每秒接受的记录数、中位数和尾部延迟、429 速率、重试次数和按主机的队列年龄。高请求数与低接受记录产出是一个质量问题。如果一个域开始失败,请暂停该域,而其他批准的源继续。
如何使示例具有生产安全性? 将 URL 输入移动到持久队列中,并为每个 URL 存储一个终端结果。大量的 gather 调用在信号量运行之前为每个输入创建一个协程,因此不适合非常大的清单。一个固定的工作池从队列中读取可以保持内存限制,并允许崩溃的工作者的租约到期。确认应在接受记录或失败结果持久存储之后进行。
添加源特定的策略记录。示例中的一秒间隔和每主机三个连接是示意性的。实际设置应来自网站允许的访问模式和试点。不同的主机可能需要不同的启动间隔。一个全局信号量保护您的流程,但它并不防止多个分布式工作者共同超出主机预算。对于分布式爬虫,将速率状态放入共享调度程序或将每个主机分配给一个协调的工作组。
在大规模运行之前改善重试处理。示例中支持数字 Retry-After 值;HTTP 日期值需要解析。添加一个抖动的退避,以避免多个工作者在同一时刻重试。设置最大经过时间和尝试次数,然后对无法成功的任务进行死信处理。不要像处理 401、403 或格式错误的 URL 那样重试,因为更多尝试不会授予访问权限。
最后,如果 HTML 解析变得 CPU 密集,请将其保持在事件循环之外。小的 Beautiful Soup 操作可能是可以接受的,但在同一循环中解析非常大的页面或运行复杂提取可能会延迟所有请求。测量事件循环延迟,并在必要时将重解析移动到工作进程或单独阶段。异步网络 I/O 和并行 CPU 工作解决不同的瓶颈。
在并发抓取器中应该监控什么? 按主机记录延迟和结果。全局平均值隐藏了一个源未响应或返回软错误页面的事实。跟踪在途请求、每主机的请求启动、429 速率、超时、响应字节、接受记录产出、重试尝试和队列年龄。对持续的质量下降发出警报,而不仅仅是处理崩溃。
在更改并发度后使用接受样本。更高的吞吐量可能会增加不完整的响应、挑战页面或源负载。比较每秒接受的记录与每单位成本,并验证源侧规则仍然受到尊重。如果接受的产出下降而请求计数上升,则降低速率并在进一步扩展之前检查样本。
在仪表板中保持原始计数和接受计数分开。如果大多数页面包含错误模板,突然出现的 200 响应并不算成功。在将记录计数为接受之前,验证预期页面标记、标题、内容类型和必填字段。这向运营团队发出信号,在其余队列继续时暂停一个源。
常见问题解答 不。信号量限制同时进行的工作。使用每主机节奏机制控制随时间的请求启动。
问:aiohttp 能否抓取 JavaScript 渲染的页面?
aiohttp 获取 HTTP 响应,但不执行页面 JavaScript。对于加载后生成的内容,请使用浏览器渲染或管理的爬网路径。
问:我应该为一百万个 URL 使用 asyncio.gather 吗?
不。通过持久化队列提供有限数量的工作者,以便内存使用和任务恢复保持受控。
支持 JavaScript 渲染,成功率达 99.8%
将任意网站转换为 Markdown、HTML、JSON、链接、PDF 等格式,无需管理爬取基础设施。 开始免费爬取 →