几乎每个学 Python 的人都会被"爬虫"这两个字吸引:几行代码就能把网页上的数据批量搬回来,听起来像魔法。但 2026 年的爬虫早已不是 requests 加正则就能打天下的时代——TLS 指纹、行为检测、AI 抽取、合规红线,每一条都值得认真了解。
把"Python 爬虫从入门到精通"当成一条可以逐段验证的路线,会比背一堆库名字有用得多:先弄清它做什么、边界在哪,再依次走过请求、解析、动态渲染、工程化和规模化五个台阶。这篇文章写给两类人——刚装好 Python、想知道第一个爬虫怎么写的新手,以及已经能跑脚本、却总卡在反爬与工程化上的进阶者。每一节都尽量留下能自己改两行就跑起来的代码,读到结尾你可以按最后那份阶段清单逐项打勾。
爬虫到底在做什么
爬虫听着玄,本质只有三步:拿到网页、从网页里挑出你要的数据、把数据存下来。你打开浏览器、搜索、复制文字、粘进表格,这套动作重复一千遍是人力采集;把它写成程序自动跑,就是爬虫。
换个更形象的比喻:浏览器打开一个网页,本质上是一次"取快递"——浏览器(取件人)向服务器(仓库)发出 HTTP 请求(取件单),服务器把 HTML、CSS、JS 等文件(包裹)发回来,浏览器再把这些零件组装渲染成你看到的页面。爬虫做的事情,就是写一段程序代替浏览器去取包裹,然后从包裹里把有用的数据挑出来。
看清楚这三步,坑基本也分三层:网页拿不到,是请求层的问题;数据挑不准,是解析层的问题;存下来又乱又重、跑一半就断,是工程层的问题。新手最常见的失误,是找一段"万能爬虫代码"贴上去,结果三层同时出错,连坏在哪里都看不出来。
静态页面与动态页面
这是新手必须分清的第一对概念,也是后面所有技术选型的起点。静态页面的数据直接写在 HTML 里,请求一次就能拿到全部内容;动态页面的 HTML 只是个空架子,真正的数据由 JavaScript 在浏览器里再发请求、再渲染填进去。
判断方法很简单:在浏览器里右键"查看网页源代码",能搜到目标数据就是静态,搜不到就是动态。两者要用不同的工具对付。
三条不能碰的红线
在写第一行请求代码之前,先把边界讲清楚,这比任何技巧都重要。
- 遵守 robots.txt 和平台用户协议。robots.txt 放在站点根目录,用很简单的指令说明哪些路径可以被抓、哪些不行。它不是法律文件,但 2026 年的司法实践里,"是否违反 robots.txt 与平台服务协议中的反爬条款"已经是认定行为正当与否的关键事实之一。
- 不抓取个人信息与非公开数据。手机号、身份证号、订单、行踪、征信这类能识别到自然人的信息,受《个人信息保护法》约束;放在后台、登录后才可见的数据,即便屏幕上"看得见",也不等于"可以随便抓"。相关行为可能触及《刑法》第二百五十三条之一侵犯公民个人信息罪。
- 不破解网站的技术保护措施。逆向破解接口签名、暴力绕过验证码、伪造登录态,这类操作在司法上很容易被认定为"非法获取计算机信息系统数据"(《刑法》第二百八十五条)或"破坏计算机信息系统"(第二百八十六条)。2025 年修订的《反不正当竞争法》第十三条也明确禁止"避开技术措施获取、使用他人数据"。
一个务实的自我判断标准是两句问话:你访问的频率会不会让对方服务器难受?你的用途会不会实质性替代对方提供的服务?前者关乎《网络安全法》下的义务,后者是反不正当竞争案件里最常出现的"实质性替代"标准。技术本身中立,使用技术的行为不中立。
第一站:把环境搭对
Python 版本与虚拟环境
2026 年做爬虫,建议 Python 3.11 起步;想追新也可以用 3.14(3.14.0 于 2025 年 10 月发布,目前已有多个维护版本)。但真正要紧的不是版本号,而是别往系统全局环境里装包:爬虫依赖更新很快,几个项目之间的 requests、httpx 版本经常打架,用虚拟环境隔离是最省事的做法。
python -m venv crawler-env # 创建干净的虚拟环境
source crawler-env/bin/activate # 激活,Windows 用 crawler-env\Scripts\activate
pip install requests beautifulsoup4 lxml # 基础组合
pip install httpx parsel selectolax curl_cffi # 现代一点的组合装库的原则:按需,而不是按清单
不要一次性装三十个库。推荐的推进顺序是:先用 requests 加 BeautifulSoup 加 lxml 把流程走通;确认要做并发再上 httpx 或 asyncio;确认被 TLS 指纹拦截再考虑 curl_cffi;确认页面确实靠 JavaScript 渲染才动用 Playwright。
新手容易犯的错是上来就囤一堆库。实际上入门阶段只要 requests 加 BeautifulSoup 加 lxml,就能解决八成静态页面的采集需求。库里多一个,出问题时你要排查的面就多一层。
先把整个工具链摆出来,建立全局印象,再逐一站过去:
| 工具 | 定位 | 一句话说明 |
|---|---|---|
| requests / httpx | 发请求 | 最经典的 HTTP 库;httpx 是其现代化、支持异步的同类库 |
| BeautifulSoup / lxml / parsel / selectolax | 解析 HTML | 把网页源码变成可按标签、按路径查找的结构化对象 |
| curl_cffi | 发请求(伪装版) | 能模拟浏览器 TLS 指纹,对付指纹级反爬 |
| Playwright / DrissionPage | 浏览器自动化 | 驱动真实浏览器渲染页面,动态网站的万能钥匙 |
| Scrapy | 爬虫框架 | 大规模、工程化采集的标准框架 |
| Crawl4AI / Firecrawl | AI 爬虫 | 用大模型直接从页面抽取结构化数据的新范式 |
第二站:发起请求
第一个能跑的请求
import requests
url = "https://httpbin.org/get"
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
),
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
}
resp = requests.get(url, headers=headers, timeout=10)
print(resp.status_code)
print(resp.text[:200])三个细节值得记住:一定要写 timeout,否则对方不响应时你的脚本会挂在那里;状态码是 200 也不代表成功,很多反爬系统会返回一个状态码正常、内容却是验证提示页的响应;带上正常的浏览器标识是基本礼仪,也是降低误封概率的第一步。
请求头不是随便写写
2026 年的请求头校验,已经不只看 User-Agent 是不是浏览器,还会看它和其它字段是否自洽。你换一个 Chrome 的 UA,却带着操作系统语言对不上的 Accept-Language,或者缺少浏览器本该发送的客户端提示字段,反而更容易被判定为脚本。
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"Referer": "https://example.com/",
}核心思路是让一组特征逻辑自洽,而不是把每个字段都随机填一遍。同一个出口 IP 一秒内发出十几个互不相同的 UA,比固定一个正常 UA 更像异常流量。
同步还是异步:httpx 的取舍
requests 简单好用,但它是同步阻塞的:发一个请求,线程就停在那里等回包。需要同时处理成百上千个地址时,httpx 的异步模式会明显快一截。
import asyncio
import httpx
async def fetch(client, url):
resp = await client.get(url, timeout=10)
return resp.status_code, len(resp.text)
async def main():
urls = [f"https://example.com/page/{i}" for i in range(1, 21)]
async with httpx.AsyncClient(http2=True) as client:
tasks = [fetch(client, u) for u in urls]
for result in await asyncio.gather(*tasks):
print(result)
asyncio.run(main())有个容易踩的版本坑:httpx 在 0.28 之后把代理参数从 proxies= 改成了单数的 proxy=,照旧写法会直接报错。另外 httpx 原生支持 HTTP/2,对于默认走 HTTP/2 的站点,连接复用与头部压缩都能省下不少时间。
TLS 指纹:requests 为什么被一眼认出
有些站点不看 User-Agent,而是看更底层的东西。你的客户端在 HTTPS 握手时会交换一串参数——加密套件顺序、扩展列表、椭圆曲线偏好,再加上 HTTP/2 的帧参数,这些组合起来就是所谓的 TLS 指纹(常说的 JA3)。真实浏览器和 requests 底层生成的指纹差异很大,服务器拿指纹一比对,就知道来的不是浏览器。
curl_cffi 是目前解决这个问题最省事的库:它基于 libcurl 做了浏览器指纹模拟,一个参数就能让请求在握手层更接近 Chrome 或 Firefox。
from curl_cffi import requests as crequests
resp = crequests.get(
"https://example.com/",
impersonate="chrome",
timeout=10,
)
print(resp.status_code)
print(resp.text[:200])这里有个安全提醒:curl_cffi 在 0.15.0 之前的版本存在一个 SSRF 漏洞(CVE-2026-33752),会把请求重定向到内网或云元数据地址,务必使用 0.15.0 及以上(当前稳定版已到 0.16.x)。
还有一句题外话——指纹模拟只能让合规的请求不被误伤,它不是突破网站技术保护措施的通行证。它只解决传输层的伪装,不执行 JavaScript,也没有内置重试。行为层的检测要靠控制频率、随机间隔来化解,JS 挑战层的检测还是要回到浏览器方案。攻防是动态博弈,任何"一劳永逸绕过一切反爬"的说法都不可信。
会话、Cookie 与登录态
需要保持登录状态时,用 Session 复用连接与 Cookie,比每次手动拼 Cookie 靠谱。
import requests
session = requests.Session()
session.headers.update({"User-Agent": "YourBot/1.0"})
session.get("https://example.com/login-page", timeout=10)
resp = session.get("https://example.com/profile", timeout=10)
print(resp.status_code)再强调一次:能拿到登录态,不代表有权限抓登录后的内容。把自动化脚本套在自己的账号上,和替平台批量采集其它用户的数据,是性质完全不同的两件事。
第三站:把网页变成结构化的行
解析器怎么选
| 工具 | 定位 | 适合的场景 |
|---|---|---|
| BeautifulSoup | 容错好、API 友好,需搭配解析器 | 一次性脚本、结构不规整的页面 |
| lxml | 速度快,支持 XPath | 结构规整、数据量大 |
| parsel | 同时支持 CSS 与 XPath | Scrapy 项目、两种选择器混用 |
| selectolax | 基于 C 引擎,解析快、内存低 | 高并发、超大 HTML |
它们不是竞争关系。常见组合是 BeautifulSoup 搭配 lxml 作为底层解析器:比内置解析器快,又能修一些残缺的标签。别在开发时用内置解析器、上线换成 lxml,不同解析器对同一段不规范 HTML 的修复结果可能不同,这种切换会引入"换了解析器就少了几条数据"的诡异问题。全程统一一种。
三十行代码爬下第一个页面
练习爬虫有专门的"合法靶场",比如专门供练习的名言采集站。下面这段完整代码做三件事:请求页面、解析数据、存成文件:
import csv
import requests
from bs4 import BeautifulSoup
# 1. 发请求:带上正常的浏览器标识是基本礼仪
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
resp = requests.get("https://quotes.toscrape.com/", headers=headers, timeout=10)
resp.raise_for_status()
# 2. 解析:把 HTML 变成可以按选择器查找的对象
soup = BeautifulSoup(resp.text, "lxml")
quotes = soup.select("div.quote")
rows = []
for q in quotes:
text = q.select_one("span.text").get_text(strip=True)
author = q.select_one("small.author").get_text(strip=True)
rows.append([text, author])
# 3. 存储:写入 CSV 文件
with open("quotes.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["quote", "author"])
writer.writerows(rows)
print(f"共采集 {len(rows)} 条名言")这段代码里藏着三个好习惯:带 User-Agent 表明正常身份、设置 timeout 防止无限等待、用 utf-8-sig 编码避免 Excel 打开 CSV 时中文乱码。入门阶段把这三件事刻进肌肉记忆,能避开一半的"莫名其妙报错"。
CSS 选择器还是 XPath
两者都能定位元素。CSS 更短、读起来更顺,但它不能按文本内容选,也没有父节点、祖先节点这类轴;XPath 能做这些事,代价是写起来更长、更易错。日常解析优先用 CSS,遇到"按文字找节点""从子节点反查父节点"再换 XPath。
CSS 选择器是最直观的定位语言:div.quote 表示找 class 为 quote 的 div,div.quote span 表示找它内部的 span,div.quote > span 表示只找直接子级,#content 按 id 找,ul li:nth-child(2) 按位置找。日常八成的解析需求,掌握这五六个语法就够了。
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")
for card in soup.select("div.product-list article.product"):
link = card.select_one("h3 a")
price = card.select_one("span.price")
if not link or not price:
continue
print(link.get("title"), price.get_text(strip=True))这里的 html 就是前面请求拿到的页面文本。注意代码里那条 if not link or not price: continue——页面上少一个节点就整段报错,是新手脚本最常见的崩溃原因。
让解析代码抗改版
选择器写得越贴近页面结构,越容易碎。div:nth-child(3) 这种写法,对方一改版就全废;尽量抓语义化的锚点,比如带 id 或者带 data 属性的容器。也不要照抄浏览器复制的完整路径(那种从 html 一路写到目标元素的长串),正确做法是找目标数据最近的"稳定锚点",从锚点往下走一两级,路径越短越抗改版。
文本取值也有讲究:
import re
def clean(text):
if text is None:
return ""
return re.sub(r"\s+", " ", text).strip()
raw = " 12.30\n元 "
print(clean(raw))get_text(strip=True) 只去掉首尾空白,不会合并中间多余的换行和空格,稳妥做法是再用正则压一遍。
把结果存好
抓完就 print 等于白抓。小规模存 CSV 或 JSON 最方便,注意用 utf-8-sig 编码,否则 Excel 打开会乱码。
import csv
rows = [
{"title": "示例一", "price": "12.30"},
{"title": "示例二", "price": "45.00"},
]
with open("books.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["title", "price"])
writer.writeheader()
writer.writerows(rows)数据量再大就该换数据库了。写入前顺手做一次去重和字段校验,比事后清洗省力得多。
第四站:动态页面
什么时候必须动用浏览器
如果数据是打开页面时由 JavaScript 拼出来的,你直接请求 HTML 只会拿到一个空壳。判断方法前面已经讲过:禁用 JavaScript 刷新页面,内容还在,就说明请求层能搞定;内容没了,才需要上浏览器。
但先别急着上。打开开发者工具的网络面板,刷新页面,在 XHR 或 Fetch 分类下找到返回 JSON 数据的那个请求——很多情况下,你可以直接模拟这个接口请求,拿到干净的 JSON 数据,连解析 HTML 都省了。能用接口解决的,就不要渲染整页:更快、更省资源,也更不容易被识别。这是效率最高的路径,值得每次动手前都先试一遍。
Playwright 起步
需要真正驱动浏览器时,Playwright 是当前的首选。它由微软维护,原生支持 Chromium、Firefox、WebKit,内置自动等待,比早期的 Selenium 稳不少。
pip install playwright
playwright install chromiumfrom playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com/", wait_until="networkidle")
print(page.title())
links = page.query_selector_all("a")
print(len(links))
browser.close()两个 2026 年的实操要点:Playwright 现在要求 Python 3.10 及以上(1.62 与 1.63 分别在 2026 年 7 月、9 月发布);playwright install chrome 装的是带品牌的 Chrome,不会装 Chromium 本身,如果你装完直接以默认渠道启动,会报"找不到可执行文件"。另外,工作目录里放一个叫 playwright.py 的文件,会把真正的包遮蔽掉,这是很多人遇到过又想不到的导入错误。
自动化特征与它带来的问题
Playwright 默认识别特征比较明显:浏览器暴露的 navigator.webdriver 标记、通过 CDP 协议通信留下的痕迹,以及 WebGL、Canvas 渲染上的细微差异,都可能被检测出来。社区因此有了 stealth 类补丁,在页面加载前注入脚本,把这些特征改得更像一台普通浏览器。
需要说清楚的是:这类补丁的正当用途,是让合规采集不被误伤,而不是用来绕过网站的技术保护措施。绕过验证码、破解接口签名、伪造他人登录态,前面已经讲过它们的法律性质。指纹伪装能降低误封概率,但改变不了你行为的性质。
动态页面的性能代价
一个浏览器实例动辄占用几百兆内存,几十个并发一开,普通机器就撑不住了。经验法则是:能用接口就不用渲染,能渲染一个页面就不渲染十个——浏览器只用来解决"第一公里"的登录和渲染,后续翻页能回退到接口就回退。实践上还要控制并发数、复用浏览器上下文(context)、用完及时关闭。
第五站:工程化,交给 Scrapy
2026 年的 Scrapy 长什么样
当采集从"几百条"变成"几十万条",脚本式的写法会迅速失控:断点续爬、去重、限速、重试,全都得自己写。这时候该上框架。Scrapy 依然是这套场景里的主力,目前稳定版是 2.19.0(2026 年 9 月发布)。
近几个版本的几个变化值得留意:2.16.0 起官方支持 Python 3.14;2.18.0 起内置了基于 httpx2 的下载处理器,Twisted 版的 HTTP/2 处理器不再是实验特性;2.19 新增了 RemoteControl 扩展,可以通过 HTTP 检查和控制正在运行的爬虫。另外,老的 start_requests() 方法已经被 start() 取代。
scrapy startproject blog_spider
cd blog_spider
scrapy genspider example example.comimport scrapy
class ExampleSpider(scrapy.Spider):
name = "example"
start_urls = ["https://example.com/"]
async def start(self):
for url in self.start_urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
for item in response.css("article.product_pod"):
yield {
"title": item.css("h3 a::attr(title)").get(),
"price": item.css("p.price_color::text").get(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)框架帮你把请求调度、并发控制、去重、重试这些"脏活"都接管了,你只需要写好"从哪开始爬"和"怎么解析"两件事。如果你还在用更早的版本,start() 需要换回 start_requests()。
中间件与管道
Scrapy 的扩展点主要就三处:爬虫中间件管请求与结果的加工,下载器中间件管出网请求(比如换 UA、加代理),Item 管道管数据的清洗与落库。
DOWNLOADER_MIDDLEWARES = {
"blog_spider.middlewares.RotateUserAgentMiddleware": 543,
}
ITEM_PIPELINES = {
"blog_spider.pipelines.CleanPipeline": 300,
"blog_spider.pipelines.CsvPipeline": 800,
}
ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 1.5
CONCURRENT_REQUESTS_PER_DOMAIN = 4注意 ROBOTSTXT_OBEY = True 是新建项目时的默认值,别顺手改成 False——很多项目出事,就是从这个开关开始的。
去重、限速与重试
Scrapy 内置了基于请求指纹的去重,但限速和重试建议显式配置。别用固定延时硬等,用自动限速让它根据对方响应时间自己调节,比人手拍一个数字更友好,也更不容易触发风控。
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 10.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0
RETRY_ENABLED = True
RETRY_TIMES = 3第六站:规模化
并发模型怎么选
简单说:IO 密集的采集任务,瓶颈几乎都在网络等待上,多开进程是浪费内存。中小规模用线程池,大规模用 asyncio 配合 httpx 或 aiohttp。但并发数不是越高越好——并发拉满换来的是更快的封禁,而不是更快的采集。把并发控制在对方能接受的范围内,是长期跑得稳的前提。
代理是怎么一回事
代理的作用是分散出口 IP,避免单个 IP 的请求量过大。常见的几类差别很大:
| 类型 | 来源 | 特点 | 常见用途 |
|---|---|---|---|
| 数据中心代理 | 云机房 IP 段 | 快、便宜,但容易被标记 | 公开数据、低防护站点 |
| 动态住宅代理 | 真实家庭宽带 | 更像普通用户,按流量计费 | 防护较严站点的常规采集 |
| 静态住宅代理 | 固定分配的住宅 IP | 稳定、可长期保持会话 | 需要登录态的长期任务 |
| 移动代理 | 4G 与 5G 基站 | 隐蔽性最好,也最贵 | 前几类都过不去的场景 |
有两个常见误用:一是全量上住宅代理,成本翻好几倍却未必需要;二是在需要保持会话的任务里用"每请求换一次 IP"的轮换,导致登录态频繁失效。选型前先想清楚你的任务是高频短请求,还是需要连续会话。
还要提醒一句:代理是让你分散压力、避免误伤正常服务,不是用来攒一堆 IP 对某个站点发起高频冲击的。后者既有封禁风险,也可能触碰前面讲过的法律边界。
增量采集与缓存
长期运行的项目,最省成本的一步是别重复抓已经抓过的页面。记录已处理的 URL,或者用响应头里的 ETag、Last-Modified 判断内容有没有变;开发阶段还可以把响应落盘做本地缓存,避免反复打扰目标站点。
import hashlib
import json
from pathlib import Path
CACHE = Path("cache")
CACHE.mkdir(exist_ok=True)
def cache_key(url: str) -> str:
return hashlib.sha1(url.encode("utf-8")).hexdigest()
def save(url: str, text: str) -> None:
payload = {"url": url, "text": text}
(CACHE / f"{cache_key(url)}.json").write_text(
json.dumps(payload, ensure_ascii=False),
encoding="utf-8",
)第七站:AI 时代的新范式
不写选择器的爬虫
2025 年以来兴起了一类新工具,代表是开源的 Crawl4AI 和云服务 Firecrawl。它们的思路是把大模型引入采集流程:Crawl4AI 负责渲染页面并转成干净的 Markdown,再按你给的提示词让大模型直接抽出结构化字段;Firecrawl 则以 API 服务的形式提供类似能力,抓取、渲染、反爬都托管在云端。
最大的好处是免去了写选择器、适配改版的重复劳动,页面结构变了,提示词通常不用变。对于原型验证、低频采集、结构多变的页面,这条路线能省下大量时间。
新范式不是银弹
代价同样明显:
- LLM 抽取按调用计费,大规模采集的成本远高于传统方案
- 抽取结果存在小概率的幻觉和漏字段,关键业务需要校验
- 把页面内容发给第三方模型还涉及数据出域问题
合理的定位是:原型验证、低频采集、结构多变的页面用 AI 爬虫快速搞定;高频、大规模、字段固定的采集,依然回归传统方案的成本优势。
学习路线:分三阶段打勾
入门阶段,一到两周
目标是能把一个静态页面从头抓到尾,不追求性能。
- 会用 requests 发请求、设请求头、处理超时与状态码
- 会用 BeautifulSoup 加 lxml 提取标题、价格这类单层数据
- 会把结果存成 CSV 或 JSON,并处理中文编码
- 会看 robots.txt,知道哪些路径不该碰
- 在练习站上完成"请求、解析、存储"的完整闭环
进阶阶段,一到两个月
目标是能应对动态页面和中等规模任务,代码不再一碰就碎。
- 会看浏览器的网络面板,判断数据来自接口还是渲染
- 会用 asyncio 加 httpx 做并发请求,理解并发与频率控制的关系
- 会用 Playwright 处理 JavaScript 渲染的页面
- 会用 Scrapy 搭一个带中间件和管道的完整项目
- 会写健壮的解析代码:缺字段跳过、正则清洗、异常捕获
- 掌握 CSS 选择器,能处理翻页、登录 Cookie、简单加密参数
精通阶段,持续投入
目标是从"能抓"走到"长期稳定、合规、可维护"。
- 能设计去重、断点续爬、增量更新的完整调度
- 理解 TLS 指纹、请求特征一致性这类底层原理,知道什么该用、什么不该用
- 能搭代理调度与监控,出错能定位到请求层还是解析层
- 熟悉《个人信息保护法》《反不正当竞争法》和刑事红线的边界,能在动手前做合规判断
- 会把采集结果接到下游的数据分析流程里,而不只是停在原始表格
常见坑排查清单
| 现象 | 常见原因 | 应对方向 |
|---|---|---|
| 返回 403 或 503 | 请求头不完整、频率过高 | 检查请求头自洽性,降低并发、加自动限速 |
| 状态码 200 但内容为空 | 数据由 JavaScript 渲染 | 找接口,或改用 Playwright |
| 页面是验证提示 | 被风控识别 | 停下检查频率与合规性,不要硬闯 |
| 中文乱码 | 编码判断错误 | 显式设置 encoding,或直接解析原始字节 |
| 数据缺字段 | 页面改版或选择器过脆 | 用语义化选择器,解析时做空值判断 |
| 跑一段时间就断 | 内存堆积、未做重试 | 分批处理、及时释放、配置重试与超时 |
| 明明换了 IP 还被封 | 请求特征与行为模式被关联 | 让整组特征自洽,而不是只换 IP |
法律与合规:技术无罪但边界清晰
司法实践的态度可以概括为:网络爬虫本身并不违法,其合法性取决于数据公开程度、抓取手段、使用目的及对平台运行和竞争利益的影响。公开的、无防护的数据以人类等效的频率采集,风险很低;而绕过验证码和登录墙、打爆对方服务器、把人家的核心数据搬来做同类生意,每一条都有真实判例。
近年公开的判例里,有企业因搬运数千万条电商商品数据被行政处罚,有平台因后台数据被技术手段穿透获取获赔五百余万元,也有因全文截取他站评论、实质替代原站服务被判赔三百万元的案例。
robots.txt 是网站声明"哪些路径不希望被抓"的约定文件。它不是法律条文,但无视它在诉讼中会被当作过错的佐证,养成动手前先看一下的习惯。真正的三条法律红线是:
- 个人信息:刑法规定五十条敏感个人信息即可入刑,且需遵守个人信息保护法的明示同意要求
- 著作权内容:未经授权的规模化复制和替代性使用
- 计算机系统安全:绕过或破坏技术防护措施可能构成刑事犯罪
一份可执行的合规清单,每次动手前过一遍:
- 数据是否公开可访问、是否需要登录或绕过验证
- 请求频率是否控制在人类水平
- 是否只取需要的字段而非整站镜像
- 采集结果的用途是否与原站构成直接竞争
- 涉及个人信息是否已获授权
- 目标网站有没有官方开放 API 可以优先使用
清单任何一项亮起红灯,就该停下来重新评估,而不是想办法"绕过去"。
结语
爬虫技术这两年最大的变化,不是"怎么抓得更快",而是"哪些能抓、怎么抓才站得住"。库在迭代,反爬在升级,法律边界也在收紧,唯一不变的是那三步基本功:请求、解析、存储。把这三步练扎实,再往上叠加框架、并发和调度,你会发现自己不再需要到处找"万能代码"——因为每一个问题出在哪一层,你心里都有数。
技术能带你走多远,取决于你愿不愿意在动手前先问一句:这件事,该不该我做。
参考资料与延伸阅读
- requests 官方文档:https://requests.readthedocs.io/
- BeautifulSoup 官方文档:https://www.crummy.com/software/BeautifulSoup/bs4/doc/
- curl_cffi 项目主页:https://github.com/lexiforest/curl_cffi
- Playwright Python 文档:https://playwright.dev/python/
- Scrapy 官方文档:https://docs.scrapy.org/
- Crawl4AI 项目主页:https://github.com/unclecode/crawl4ai