news 2026/10/13 10:33:36

python如何提升爬虫效率

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python如何提升爬虫效率

前言


「提升爬虫效率」这个题目最容易滑向两个误区。第一个是上来就写并发、写代理池、写分布式,却不先量一下瓶颈在哪儿;第二个是把「效率」等同于「单位时间发更多请求」,于是把并发开到很大——这在技术上或许痛快,但对方是个小站的话,你等于在替它做压力测试。效率的目标应该是在你的合规约束下,把等待时间压到最小,而不是「把对方打满」。


本文按这条思路展开:先讲怎么定位瓶颈,再逐个维度讲可以优化的地方(I/O、解析、调度、存储),最后给一段可运行的代码,用time.perf_counter()让你自己测出提速效果。本文不会给出任何「提速 N 倍」的数字——那类百分比要么有特定测试条件,要么纯属拍脑袋,都不应该被当成结论。所有示例只用标准库,Python 3.8+ 可直接运行。


先交代一句版本常识:Python 2.7 已于 2020 年 1 月 1 日停止维护。老教程里用xrange省内存、用dict.iteritems()遍历的写法,在 Python 3 里分别对应range(本身就是惰性的)和dict.items()。


一、先测量,再优化


优化前先问:慢在哪里?最常见的四类瓶颈:




现象大概率瓶颈先做什么



每页耗时都在 1 秒上下、很稳定对方响应慢或网络延迟检查限速设置,别着急加并发

少数请求特别慢,多数很快超时/重试没设好设 timeout、分类重试

抓下来的解析阶段 CPU 吃满解析或字符串处理换解析策略、减少重复拼接

内存一路上涨把所有数据堆在内存里流式处理、边抓边写



测量方法很朴素:用time.perf_counter()给每个阶段打点。顺序抓 5 个页面,把「请求耗时」和「解析耗时」分开打印,你立刻就知道该优化哪一段。盲目加并发是治不了 CPU 瓶颈的。


二、I/O 维度:爬虫的时间都花在等网络


爬虫是典型的 I/O 密集任务。要压缩这部分时间,有三个杠杆:


第一,减少固定开销。每次新连接都要做 DNS 解析和 TLS 握手。如果同一个站点要抓很多页,复用连接能省掉这部分开销——标准库的urllib.request每次urlopen走的是默认 opener,连接复用需要显式构造 opener 或改用支持连接池的 HTTP 客户端。第三方客户端库在这点上通常更方便,具体用法以该库官方文档为准。


第二,让等待重叠起来。一个请求发出后,大量时间花在「等服务器回话」上。这段时间里程序什么也做不了,除非你用多线程或异步 I/O。CPython 有全局解释器锁(GIL,Global Interpreter Lock),同一进程内同一时刻只有一个线程执行字节码;但线程在等待网络 I/O 时会释放 GIL,所以少量多线程对爬虫确实有效。反之,CPU 密集的任务用多线程没有意义,要用多进程(multiprocessing)或把重活交给 C 扩展。


第三,把超时和重试设对。urllib.request.urlopen的timeout不写就不超时;重试要区分错误类型——404 立即放弃,403 停下来反思,429 退避,网络层错误才值得指数退避重试。一个卡住 60 秒的请求,比六个 10 秒的请求还浪费。


一个必须强调的边界:这些手段都服务于你自己的合规额度。对同一域名,并发上限要设得很小,并且要有全局最小请求间隔。下面的示例就是这么做的。


三、解析维度:少做无用功


解析阶段是纯 CPU 工作,几个常见浪费:



  • 重复解析同一段 HTML。同一页里抽多个字段,就一次遍历抽完,而不是对每个字段各扫一遍。

  • 在循环里用+拼长字符串。s = s + piece每轮都新建一个字符串对象,整体接近平方级开销;正确做法是累积到列表里,最后"".join(parts)。

  • 用正则去解析 HTML。正则对结构化的 HTML 很脆弱,遇到嵌套结构还可能出现灾难性回溯,把 CPU 拖死。整页解析应该交给解析器,正则只用来处理解析之后的、规则明确的小片段(比如从一段文本里抽日期)。

  • 把整页内容都留在内存。只需要几个字段,就不要把整个站点缓存起来。


四、调度维度:去重、上限与自适应


调度做不好,前面的优化都会被浪费:



  • 去重:用set记已访问的 URL,是成本最低的收益。少了它,环形链接会让你把同一页抓无数遍。

  • 硬上限:页数或条数一定要设上限,防止失控。

  • 自适应限速:与其固定sleep,不如根据对方响应变慢就自动放大间隔、变快就略微缩小。完整框架里通常内置了这类节流器,自己写的话用「最近 N 次请求的平均耗时」做简单反馈即可。

  • 尊重 robots:urllib.robotparser.RobotFileParser.can_fetch(useragent, url)要在入队前调用,而不是抓完才发现不该抓。


五、存储维度:别让写文件成为瓶颈



  • 用open()打开一次、写一批、再关闭,而不是每条记录开关一次文件。

  • CSV 用csv.writer一次writerow一行,它自己处理转义,别手工拼逗号。

  • 写文本文件显式指定encoding="utf-8";写 CSV 还要加newline="",否则 Windows 上会多出空行。

  • 数据量大时,边抓边追加写,别全堆在内存里等最后一起写。


实战:小并发 + 全局限速 + 去重


下面这段代码只用标准库。它做了三件事:把 5 个页面的请求提交给一个只有 3 个线程的线程池;用一个带锁的全局节流器保证任意两次请求的发起间隔不小于 0.5 秒;用time.perf_counter()量总耗时。


# 适用于 Python 3.8+
import threading
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed

UA = "throughput-bot/0.2 (+mailto: ops@example.org)"
WORKERS = 3 # 并发上限,刻意保持很小
DELAY = 0.5 # 任意两次请求发起之间的最小间隔(秒)

_rate_lock = threading.Lock()
_last_request = 0.0


def polite_sleep():
"""全局节流:任意两次请求的发起时间间隔不小于 DELAY。"""
global _last_request
with _rate_lock:
now = time.perf_counter()
wait = _last_request + DELAY - now
if wait > 0:
time.sleep(wait)
now = time.perf_counter()
_last_request = now


def fetch(url, retries=2):
last_error = None
for attempt in range(1, retries + 1):
polite_sleep()
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=15) as resp:
return url, len(resp.read())
except urllib.error.HTTPError as exc:
if exc.code in (403, 404): # 明确答复,不重试
raise
last_error = exc
except urllib.error.URLError as exc:
last_error = exc
time.sleep(2 ** attempt) # 指数退避
raise last_error if last_error else RuntimeError("请求失败")


def crawl(urls, workers=WORKERS):
results = {}
with ThreadPoolExecutor(max_workers=workers) as pool:
future_to_url = {pool.submit(fetch, u): u for u in urls}
for future in as_completed(future_to_url):
url = future_to_url[future]
try:
_url, size = future.result()
results[url] = size
except Exception as exc:
results[url] = "失败: %s" % exc
return results


def main():
urls = ["https://quotes.toscrape.com/page/%d/" % n for n in range(1, 6)]
t0 = time.perf_counter()
results = crawl(urls)
elapsed = time.perf_counter() - t0
for url, value in results.items():
print(url, "->", value)
print("总耗时 %.2f 秒" % elapsed)


if __name__ == "__main__":
main()

这段代码里有一个值得想清楚的取舍:全局节流器把请求的发起强制串成了每 0.5 秒一次,那并发到底还有什么用?答案是——被压掉的是「发起」这一瞬间,而每个请求等待响应的那段时间是重叠的。所以并发优化的是「等服务器回话」的等待,而不是「发出去」的动作。这也解释了为什么在严格的礼貌限速下,并发带来的收益有上限:上限由你的限速决定,而不是由线程数决定。


想验证这一点,把DELAY改成 0,再跑一次,看看总耗时怎么变;再把WORKERS从 3 改成 1,看看又怎么变。用你自己的机器和网络测出来的数字,才是你可以引用的数字。


常见坑点


1. 不测量就加并发


❌ 上来就把线程数从 1 调到 50,结果耗时几乎没变——因为瓶颈其实在解析或对方响应。 ✅ 先用time.perf_counter()分段计时,找出真正的瓶颈。


2. 以为多线程能加速 CPU 密集环节


❌ 把解析、正则匹配这类 CPU 活丢进线程池,期待变快。 ✅ 记住 GIL:线程只在等待 I/O 时释放锁;CPU 密集要用多进程或 C 扩展。


3. 忘了并发要配限速


❌ 开了 20 个线程,还去掉了time.sleep,指向一个小站。 ✅ 并发上限保持很小,并加全局最小间隔;这不是「能不能」的问题,是「该不该」的问题。


4. 在循环里用+拼字符串


❌text = text + data逐段拼接长文本。 ✅parts.append(data),最后"".join(parts)。


5. 同一页反复解析


❌ 抽标题扫一遍 HTML,抽作者又扫一遍。 ✅ 一次遍历把需要的字段全抽出来。


6. 每次请求都重新建立连接


❌ 同一个站点抓几百页,每次都走一遍 DNS 与 TLS 握手。 ✅ 复用连接(构造带连接池的 opener,或用支持它的 HTTP 客户端库)。


7. 去掉重试,或对所有错误都无脑重试


❌ 403、404 也重试三遍 —— 既没意义,又加重对方负担。 ✅ 按状态码分类:404 放弃、403 停止、429 退避、网络错误才退避重试。


8. 把结果全堆内存,最后才写


❌ 抓几万条时才写文件,中途崩了一次全没了。 ✅ 边抓边追加写,或分批落盘。


总结




维度优化点主要代价 / 边界



I/O连接复用、超时重试、少量并发并发上限受你的礼貌限速约束

解析一次遍历、join代替+、别用正则啃 HTML正则回溯可能拖死 CPU

调度去重、硬上限、自适应限速、查 robots宁可慢,不打满对方

存储批量写、显式编码CSV 需newline=""



提升爬虫效率的路线图其实很清晰:先测量找瓶颈,再在 I/O、解析、调度、存储四个维度里挑对症的手段,最后用一个自测脚本验证效果。唯一不能碰的捷径是「把对方压垮」——那不是效率,是把项目做死。所有百分比都请自己跑出来再说。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 10:33:12

信号完整性与电源完整性学习笔记 Day 5:R、L、C 的物理基础

这是30天学习计划的第5天。前四天我们建立了阻抗的统一框架,从 Day 5 开始逐个深入 R、L、C 三种基本元件的物理本质。从物理尺寸和材料参数出发,理解互连线的电阻、电容和电感。一、为什么从物理基础讲起?在 Day 4 中,我们建立了…

作者头像 李华
网站建设 2026/10/13 10:33:09

BUCK-BOOST

目录 BUCK-BOOST BUCK-BOOST的原理 BUCK-BOOST的电感选型 BUCK-BOOST的输出电容选型 BUCK-BOOST的输入电容选型 BUCK-BOOST的MOSFET和二极管 损耗计算 BUCKBOOST电路器件计算文章 (qq.com)https://mp.weixin.qq.com/s/ZxlcMmxAdy4gl0fr6OUSEA BUCK-BOOST Buck-Boost 是…

作者头像 李华
网站建设 2026/10/13 10:31:10

地图+列表联动交互:高德首页手势冲突与状态机解析

新版高德地图首页那个效果,我盯了很久。首页是一整块地图,你手指往上轻轻一推,地图区域跟着往上缩,同时底部的卡片列表被顶出来,滑到某个位置以后就变成一个偏向列表的页面,地图只剩一小条贴在顶部。再往下…

作者头像 李华
网站建设 2026/10/13 10:30:55

MySQL 5.7数据完整性实战:五类约束添加与避坑指南

1. 为什么“数据完整性”不是一句空话,而是MySQL里每天都在发生的生死线你有没有遇到过这样的情况:业务系统里突然冒出一批“张三”用户,身份证号全是123456789012345678,注册时间统一显示为1970-01-01?或者财务报表里…

作者头像 李华
网站建设 2026/10/13 10:28:30

地热井供暖毕设别急着“一键生成”:按环节选 AI,反而更快[特殊字符]

如果你读的是能源动力与材料大类 / 热能与发电工程类 / 地热开发技术,大概率会遇到一类很典型的毕业任务:给某个园区、学校、小区或县城片区做一套地热供暖方案设计。 比如题目可以具体到:《某园区中深层地热井换热供暖系统方案设计与经济性分…

作者头像 李华
网站建设 2026/10/13 10:27:47

Oracle rownum与rowid底层原理与实战避坑指南

1. 为什么刚学Oracle的人总在rownum上栽跟头?刚接触Oracle的开发者,十有八九会在rownum上卡住至少三天。我见过某高校数据库课程的期末项目里,A同学用SELECT * FROM emp WHERE rownum > 5想跳过前5条查后面的数据,结果返回空集…

作者头像 李华