知其然,知其所以然
爬虫工程 2026-10-12

连接异常:RemoteDisconnected、ConnectionReset 分别由什么触发

用纯套接字服务器制造十几种连接异常,看 requests 抛什么:FIN 关闭是 RemoteDisconnected、RST 是 ConnectionResetError、没人监听是 ConnectionRefusedError,全部包成 ConnectionError;响应体阶段的断开是 ChunkedEncodingError,不是 ConnectionError;只发半截响应头就关闭时 requests 不报错,返回 200、空头、空体。

爬虫 requests 异常 ConnectionError RemoteDisconnected ConnectionResetError ChunkedEncodingError SSLError Python 真实验证 动画演示
爬虫工程 2026-10-12

请求头顺序与大小写:requests、curl、浏览器实际发出什么

用套接字服务器抓 9 个客户端的原始请求头:requests 5 个、Node fetch 7 个且全小写、真实 Chrome 154 发 14 个;头名顺序有 8 种彼此不同;requests 里想去掉 User-Agent 会被 urllib3 补回 python-urllib3/2.7.0,想改默认头的顺序也改不了。

爬虫 请求头 Header 头顺序 大小写 requests curl Chrome Python 真实验证 动画演示
爬虫工程 2026-10-12

编码识别:Content-Type、meta、chardet 谁说了算,乱码从哪来

实测爬虫里乱码的来源:text/* 没写 charset 时 requests 默认 ISO-8859-1(r.apparent_encoding 其实能猜对 utf-8);requests 不看 meta,lxml 不看响应头;页面声明 gb2312 但含“镕”时 Python 解码替换、lxml 截断;UTF-8 BOM 变成 r.text 开头的 \ufeff;charset_normalizer 36 个组合里有 3 个解错文字。

爬虫 编码 charset requests apparent_encoding GBK 乱码 lxml Python 真实验证 动画演示
爬虫工程 2026-10-12

线程池与 asyncio:抓取吞吐在什么并发量下拉开差距

在本机测试服务器(每个请求停 0.1 秒)上对比线程池 requests 与 asyncio aiohttp:并发 10 到 100 时都紧贴理论下限,并发 200 时 698 ms 对 615 ms;无延迟时每请求 0.331 ms 对 0.109 ms;并发 200 的峰值内存比起点多 18.8 MiB 对 3.3 MiB。

爬虫 并发 线程池 asyncio aiohttp requests 吞吐 内存 Python 真实验证 动画演示
爬虫工程 2026-10-12

asyncio 信号量限制并发

实测 asyncio 信号量限制爬虫并发:Semaphore(N) 让服务器看到的并发峰值恰好是 N;但只包住 session.get() 而不包住读取响应体时,Semaphore(5) 的实际并发是 50;TCPConnector(limit=10) 加 total 超时让 200 个请求中 110 个超时,改用 Semaphore 排队则 0 个;20 万个任务 gather 要 310.8 MiB,worker 加队列 23.5 MiB。

爬虫 asyncio Semaphore aiohttp 并发控制 TCPConnector 超时 Python 真实验证 动画演示
爬虫工程 2026-10-12

aiohttp 的 limit 与 limit_per_host

实测 aiohttp 的 TCPConnector:默认 limit=100、limit_per_host=0,对单个站点最多同时开 100 条连接;limit 是总数上限、limit_per_host 是每个主机的上限,两个都设时更紧的生效;“主机”按主机名区分(4 个域名指向同一台服务器时对方看到 20 个并发);多个 ClientSession 的 limit 会叠加(3 个会话峰值 30)。

爬虫 aiohttp TCPConnector limit limit_per_host 并发控制 ClientSession Python 真实验证 动画演示
爬虫工程 2026-10-12

事件循环里的阻塞调用:解析 HTML 把整个爬虫卡住多久

用心跳任务测 asyncio 事件循环被卡的时间:20 个页面抓完后直接在协程里用 lxml 解析,事件循环最大延迟 1293 ms;放进线程池后降到 15 ms 但总耗时几乎不变(1518 ms),进程池才真正并行(557 ms);协程里直接 requests.get 让 10 个“并发”请求变成串行(1079 ms,服务器峰值 1)。

爬虫 asyncio 事件循环 阻塞调用 lxml run_in_executor 进程池 Python 真实验证 动画演示
爬虫工程 2026-10-12

解析放进进程池:什么时候值得

实测把 HTML 解析放进进程池:池冷启动 83 ms、预热后一个任务来回 0.141 ms;6 KB 到 2.5 MB 的页面 8 个进程都比直接解析快 5.3–6.2 倍,8 个线程基本不加速;传输页面的成本远小于解析;lxml 树不能返回;map 的 chunksize=1 只快 1.29 倍,批量后 6.15 倍。

爬虫 进程池 ProcessPoolExecutor lxml 解析 chunksize GIL Python 真实验证 动画演示
爬虫工程 2026-10-12

背压:有界队列如何让生产者慢下来

实测有界队列的背压:2000 个 100 KB 条目、消费者慢时,无界队列生产者 42 ms 发完、峰值内存 242 MiB,maxsize=10 只要 26 MiB,而全部处理完的时间都在 11.2–11.4 秒;三段流水线里积压总出现在最慢一段的前面;忘了 task_done 会让 join() 永远等。

爬虫 asyncio 背压 Queue 有界队列 生产者消费者 流水线 Python 真实验证 动画演示
爬虫工程 2026-10-12

asyncio 里的取消与超时:wait_for 与 asyncio.timeout 的差别

实测 asyncio 的取消与超时:wait_for 和 asyncio.timeout 都在约 200 ms 抛 TimeoutError;协程吞掉 CancelledError 时超时失效(1202 ms 后正常返回);shield 让任务在超时后继续跑完;gather 一个任务失败后其它任务仍在后台运行,TaskGroup 会取消它们;被取消的 aiohttp 请求会让连接关闭。

爬虫 asyncio 超时 取消 wait_for timeout TaskGroup gather shield Python 真实验证 动画演示
爬虫工程 2026-10-12

lxml 解析与 GIL:多线程能不能并行

在本机 10 核上量 lxml 各操作的多线程加速比(8 线程):HTML 解析 0.68–1.15 倍(不能并行,哪怕直接用 C 层的 etree.HTML),XML 解析约 2 倍,xpath 约 1 倍,序列化 tostring 约 4.1 倍;对照 sha256 5.61 倍、zlib 5.63 倍、纯 Python 与正则约 1 倍。

爬虫 lxml GIL 多线程 解析 并行 etree Python 真实验证 动画演示
爬虫工程 2026-10-12

文件描述符上限:并发连接撞到 ulimit -n 时的报错

实测文件描述符上限:软限制压到 100 时 300 个并发连接只有约 94–97 个成功,其余是 OSError errno 24;每条连接约占 1 个描述符;更隐蔽的是保留 requests.get 返回的 Response 对象——97 个就耗尽描述符,而只保留 .content 或用同一个 Session 则全部成功。

爬虫 文件描述符 ulimit Too many open files EMFILE aiohttp requests Python 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号