← 返回作品集 2026 · COMPLETE EDITION

Python 爬虫
系统实战教程

一套覆盖 14 章的完整学习体系:从环境搭建与 HTTP 原理,到请求库、解析库、并发、动态渲染、反爬对抗、Scrapy 框架、接口逆向,再到工程化与多个完整实战项目。图文结合,代码全部可直接运行。

适合:有 Python 基础的开发者 环境:Python 3.12+(3.11 亦可) 章节:14 章 + 附录 更新:2026-09

课程目录

  1. 爬虫基础认知与合规边界
  2. 开发环境搭建
  3. HTTP 协议深度解析
  4. HTML 与选择器
  5. 请求库详解
  6. 解析库详解
  7. 数据存储方案
  8. 并发与异步提速
  9. 动态页面渲染
  10. 反爬对抗体系
  11. Scrapy 框架
  12. 接口抓取与逆向
  13. 工程化与部署
  14. 实战项目集
PART 1 · 基础篇
认知 · 环境 · 协议 · 选择器

01爬虫基础认知与合规边界 入门

爬虫(Web Crawler / Spider)的本质只有一句话:用代码模拟浏览器发出 HTTP 请求,把服务器返回的数据结构化地保存下来。搜索引擎、价格监控、舆情分析、数据日报,背后都是爬虫。它不神秘,本身也不违法——关键在于爬什么、怎么爬、用来做什么。

1.1 爬虫的四个基本环节

爬虫基本流程请求、响应、解析、存储四环节 ① 发送请求 GET /anime/3373 ② 获取响应 HTML / JSON ③ 解析提取 CSS / XPath / 正则 ④ 存储分析 CSV / SQLite
图 1-1 爬虫的标准工作流

这四个环节循环往复。一个成熟的爬虫,就是在每个环节上不断加固:请求要能突破拦截,解析要能从脏 HTML 里精准提取,存储要能扛住数据量和中断。

1.2 爬虫的分类

类型目标典型场景技术重点
通用爬虫全网抓取搜索引擎调度、去重、分布式
聚焦爬虫特定站点/主题竞品监控、数据日报解析规则、字段抽取
增量爬虫只抓新增/变更新闻订阅、价格变动去重、断点、时间戳
深层爬虫登录后/动态内容后台数据、SPA 应用会话、渲染、逆向

本教程聚焦聚焦爬虫与深层爬虫——这是个人和中小团队最常用的两类,也是投入产出比最高的方向。

1.3 合规边界(务必先读)

三条红线,违反即可能违法:
  1. robots.txt:网站声明禁止爬取的部分不碰。虽然不是法律强制,但越过了容易惹麻烦。
  2. 控制频率:别把对方服务器打爆。礼貌限速(每秒 1–2 次起)是职业底线,也是防止被封的关键。
  3. 不越权、不传播:不爬登录后的私密数据,不抓取受版权保护的内容(影视、音乐、付费课程)用于二次分发。
别把「公开」当免罪符
① 公开的个人信息照样受法律保护——《个人信息保护法》规制的是「处理行为」,与是否公开无关;抓下来做画像、营销、二次传播都可能违法。
② robots 协议是行业惯例,不是免责条款。
③ 绕过技术防护(破解接口、强行过验证码/盾、抓取登录后数据)批量取数,可能触犯《刑法》第 285 条非法获取计算机信息系统数据罪,已有实际判例。
④ 涉及行业数据、重要数据的,还要对照《数据安全法》。
练手请用官方示例站、自己搭的站或明确开放的 API,别拿真站点试手。

合法场景依然很宽广:公开数据采集、学术研究、市场分析、自用工具、SEO 监测。把力气花在技术上、把边界记在心里,才是这一行能长期做下去的前提。

爬虫工程师能力模型——学习这条线:理解 HTTP → 会用请求库 → 精通选择器 → 会存数据 → 会提速 → 能对付反爬 → 能工程化部署。本教程的章节顺序正是按这条成长路径设计的。

02开发环境搭建 入门

工欲善其事,必先利其器。本章搭建一套可长期使用的爬虫开发环境。

2.1 Python 与包管理

推荐使用 Python 3.12+(3.11 仍受支持,也够用)。包管理用 venv、conda 或 uv 均可,关键是把爬虫依赖装进隔离环境,避免污染系统 Python。uv 是近两年最流行的新选择,装库快很多。

装库报错 externally-managed-environment?这不是故障——是 PEP 668 在提醒你「别直接往系统环境装包」,按下面命令先建虚拟环境即可。确需强装才加 --break-system-packages,不推荐。
# 查看 Python 版本(建议 3.11+)
python --version

# 创建隔离环境(conda 示例)
conda create -n crawl python=3.11 -y
conda activate crawl

2.2 安装核心库

# 一站式安装(按需求选择)
pip install scrapling           # 一体化抓取框架(本教程主力)
pip install requests            # 经典请求库
pip install httpx               # 现代请求库(HTTP/2 + 异步)
pip install beautifulsoup4 lxml # 解析库
pip install parsel              # Scrapy 同款选择器库
pip install playwright          # 浏览器渲染
pip install scrapy              # 爬虫框架(进阶)
浏览器二进制:Playwright / patchright 需要单独下载浏览器,首次使用前执行:
python -m playwright install chromium
python -m patchright install chromium
scrapling 的 DynamicFetcher 走 playwright,StealthyFetcher 走 patchright,两套浏览器是分开的,建议都装上。

2.3 开发工具清单

代码编辑器

VS Code / PyCharm。装 Python 扩展,开启类型提示,写爬虫效率高很多。

浏览器 + DevTools

Chrome/Edge。F12 打开开发者工具:Elements 看结构、Network 看请求、Console 调试 JS。

抓包工具

进阶用 mitmproxy / Charles / Fiddler 抓 App 或复杂网页请求。

数据库工具

DB Browser for SQLite(看 .db 文件)、Navicat(看 MySQL)。

2.4 验证环境

跑通下面这段最小示例,环境就算就绪:

from scrapling.fetchers import Fetcher

page = Fetcher.get('https://example.com')
print('状态码:', page.status)          # 200
print('标题:', page.css('h1::text').get())  # Example Domain
成功标志:输出 200 和 Example Domain。如果报 AttributeError: ... no attribute 'fetch',说明你用了 Fetcher.fetch()——静态抓取请用 .get()(详见第 5 章)。

03HTTP 协议深度解析 入门

爬虫工程师 90% 的时间不是在写代码,而是在理解"数据是怎么从服务器到页面的"。HTTP 是这一切的底层,必须吃透。

3.1 一次请求的生命周期

HTTP 请求生命周期客户端与服务器之间的请求响应往返 客户端 浏览器 / 爬虫 服务器 Web 服务 ① 请求:方法 + 路径 + 头 + 体 GET /anime/3373 HTTP/2 · UA · Cookie ② 响应:状态码 + 头 + 体 200 OK · Content-Type · HTML/JSON
图 3-1 HTTP 请求-响应往返

3.2 URL 的结构

一个 URL 拆解后,每一段对爬虫都有意义:

https://demo-site.com:443/browse?finished=1&sort=release_date#page2
└─┬─┘   └──────┬──────┘└┬┘└─┬──┘└──────────┬──────────┘└─┬──┘
 协议        域名      端口  路径        查询参数         锚点
部分含义爬虫视角
协议http / httpshttps 需注意证书与 TLS 指纹
域名目标主机子域名常有独立数据接口
路径资源位置/anime/{id} 这类可遍历
查询参数?key=val&key=val分页、筛选、排序都藏在参数里,改参数=改数据
锚点#xxx只在前端生效,服务器收不到,抓取时可忽略
实战技巧:翻页、筛选、排序,绝大多数是通过修改 URL 查询参数实现的。看到 ?page=2、?sort=price、?category=tech 这类参数,直接改它们就能批量抓不同数据。

3.3 请求方法

方法用途是否带请求体爬虫常见度
GET获取资源否(参数在 URL)★★★★★
POST提交数据是★★★(登录、表单、接口)
PUT / PATCH更新资源是★(多见于 API)
DELETE删除资源视情况★(少见)

爬虫主要用 GET。遇到登录、搜索、复杂查询时才会用 POST(数据放在请求体里,常见 JSON 或表单格式)。

3.4 请求头详解

请求头是反爬攻防的第一战场。关键字段逐个说:

请求头作用反爬相关性
User-Agent声明客户端身份最高——裸 requests 的 UA 一眼被识破
Referer从哪个页面跳转来高——防盗链、图片、视频常校验
Cookie登录态/会话标识高——登录后数据全靠它
Accept期望的响应格式中——application/json vs text/html
Accept-Language语言偏好低——部分站点据此返回中英文
Content-Type请求体格式(POST)中——POST 时后端会校验
X-Requested-With标识 AJAX 请求中——XMLHttpRequest 常见于接口
import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Referer': 'https://www.google.com/',
    'Accept': 'text/html,application/xhtml+xml',
    'Accept-Language': 'zh-CN,zh;q=0.9',
}
r = requests.get('https://example.com', headers=headers, timeout=10)
print(r.status_code)
最实用的伪装法:在浏览器里 F12 → Network → 右键真实请求 → Copy as cURL,再用工具(如 curlconverter)转成 Python 代码,请求头一字不差,伪装度最高。

3.5 状态码大全

状态码含义爬虫应对策略
200成功正常解析响应体
301 / 302永久 / 临时重定向requests 默认跟随;注意最终 URL
304未修改(缓存)配合 If-Modified-Since 做增量
400请求参数错误检查 URL 参数、请求体格式
401未授权需要登录/Token
403禁止访问被反爬拦了:查 UA、加 Referer、换代理
404资源不存在链接失效,记录跳过
429请求过于频繁立即限速:加长随机延时、退避重试
500 / 502 / 503服务器错误稍后重试;503 常是 WAF 拦截

3.6 Cookie 与 Session 原理

HTTP 本身无状态,服务器靠 Cookie 记住"你是谁"。理解这个机制,才能处理登录态。

Cookie 会话机制服务器种 Cookie,客户端后续请求自动携带 浏览器 服务器 ① 首次请求(无 Cookie) ② 响应头 Set-Cookie: session_id=abc123 ③ 后续请求自动带 Cookie: session_id=abc123
图 3-2 Cookie 维持会话的原理

对应到爬虫:登录一次拿到 Cookie 后,用会话对象保持它,后续请求就都是"已登录"状态。requests 的 Session、scrapling 的会话都自动处理了 Cookie 的存取。

3.7 HTTPS 与 TLS 指纹

现在的网站基本都是 HTTPS。加密握手时,客户端会暴露一组特征(支持的加密套件、扩展、顺序等),即 TLS 指纹。普通 Python 请求的 TLS 指纹和真实浏览器完全不同——这就是为什么有些站点 requests 一请求就 403,而浏览器正常。

对策:用 curl_cffi 或 scrapling 这类能模拟浏览器 TLS 指纹的库。这是 2024 年后反爬对抗的关键技术点,也是本教程推荐 scrapling 的重要原因之一。

3.8 实战:看清一个请求的完整面貌

import requests

r = requests.get('https://httpbin.org/headers', headers={'User-Agent': 'MyBot/1.0'})
print('请求头回显:')
print(r.json())                      # 服务器收到的所有请求头
print('响应状态:', r.status_code)
print('响应头:', dict(r.headers))
print('编码:', r.encoding)

httpbin.org 是一个专门用于测试 HTTP 的公开服务,会把你的请求头原样返回——用它练习观察请求头是绝佳方式。

04HTML 与选择器 入门

拿到 HTML 后,怎么精准定位到想要的那一小块数据?靠选择器。这是爬虫的"手术刀"。

4.1 HTML 是一棵树(DOM)

HTML 本质是嵌套的标签树。浏览器 F12 → Elements 面板看到的就是这棵树的实时渲染。爬虫要做的就是:从根节点出发,用选择器定位到目标节点,取出它的文本或属性。

4.2 CSS 选择器完全指南

需求写法说明
按标签名a所有 a 标签
按 class.titleclass 含 title
按 id#mainid 为 main(唯一)
按属性a[href]、img[data-src]有某属性即可
属性值a[href="/login"]属性等于某值
属性开头a[href^="/anime/"]href 以 /anime/ 开头
属性结尾img[src$=".jpg"]src 以 .jpg 结尾
属性包含div[class*="list"]class 包含 list
后代div pdiv 内所有 p(任意层级)
直接子级div > pdiv 的直接子 p
第 n 个li:nth-child(2)父级下第 2 个 li
取文本h1::text伪选择器,取文本
取属性img::attr(src)伪选择器,取属性值
注意:::text、::attr(x) 是 parsel / scrapling 的扩展伪选择器,不是标准 CSS。在 BeautifulSoup 里要用 .get_text() 和 ['src'] 取。

4.3 XPath 完全指南

XPath 是另一种定位语言,在处理按文本内容匹配、向上查找父节点、复杂条件时比 CSS 更强大。

需求XPath说明
所有 a//a双斜杠=任意层级
按 class//*[@class="title"]@取属性
按 id//*[@id="main"]
按文本//a[text()="登录"]CSS 做不到
文本包含//a[contains(text(),"下载")]模糊匹配
取属性//img/@src@前缀
取文本//h1/text()
父节点//a/..向上回溯
兄弟节点//h2/following-sibling::p同级之后
多条件//div[@class="a" and @data-x="1"]and/or 组合

4.4 CSS vs XPath 怎么选

用 CSS 当主力

短、快、可读性好。80% 的场景 CSS 就够了。

这些场景换 XPath

按文本内容定位(如"找文字含'价格'的标签")、需要向上找父节点、复杂多条件。

4.5 正则表达式(兜底方案)

当数据混在 JS 代码里、或 HTML 结构极不规则时,正则是最后的兜底手段。

import re

html = '价格:¥1299 元,库存:42 件'
price = re.search(r'¥(\d+)', html).group(1)     # 1299
stock = re.search(r'库存:(\d+)', html).group(1)   # 42

# 常用正则片段
# \d+ 数字   \w+ 字母数字   .* 任意   \s+ 空白   [^"]+ 非引号
建议:能不用正则就不用——正则解析 HTML 脆弱易错。优先 CSS/XPath,正则只用于"提取节点内的局部文本"或"从 JS 里抠数据"。

4.6 DevTools 实战技巧

1
定位元素

F12 → 左上角箭头图标 → 点击页面上的目标,自动跳到对应 HTML 节点。

2
验证选择器

Elements 面板按 Ctrl+F,输入选择器表达式,实时高亮匹配结果,确认选对再写进代码。

3
复制选择器

右键节点 → Copy → Copy selector / Copy XPath,直接得到可用表达式(但常偏长,需精简)。

4
判断静态/动态

右键 → View page source 看源码。目标数据在源码里=静态可直抓;不在=JS 渲染,转第 9 章。

PART 2 · 核心抓取
请求 · 解析 · 存储 · 提速

05请求库详解 核心

请求库负责"发出请求、拿回响应"。选对库、用对方法,能省掉一大半反爬的麻烦。

5.1 四大请求库怎么选

库定位优势短板何时用
urllib标准库无需安装API 繁琐基本不用,了解即可
requests经典同步简单直观、生态最好TLS 指纹易被识别学习原理、简单静态站
httpx现代同步/异步HTTP/2、异步、API 像 requests同样有指纹问题要 HTTP/2 或异步时
curl_cffi指纹伪装完美模拟浏览器 TLS 指纹稍重反爬严格的静态站
scrapling一体化框架请求+解析+动态+反爬一体较新本教程主力

5.2 requests 全面用法

import requests

# GET 带参数(自动拼到 URL)
r = requests.get('https://httpbin.org/get',
                 params={'page': 2, 'sort': 'date'},
                 headers={'User-Agent': 'Mozilla/5.0'},
                 timeout=10)
print(r.url)          # https://httpbin.org/get?page=2&sort=date
print(r.status_code)  # 200
print(r.text[:100])   # 文本内容
print(r.json())       # 若响应是 JSON 直接转字典

# POST 提交表单 / JSON
requests.post(url, data={'user': 'a'})        # 表单格式
requests.post(url, json={'user': 'a'})        # JSON 格式

5.3 会话保持(Session)—— 处理登录态

用 Session 对象,它会自动在多次请求间保持 Cookie:

s = requests.Session()

# 第一步:登录(Cookie 自动存入 Session)
s.post('https://site.com/login', data={'user': 'a', 'pwd': 'b'})

# 第二步:之后的请求都带着登录态
r = s.get('https://site.com/profile')   # 已登录,能看到私密数据

5.4 超时与重试

from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

s = requests.Session()
retries = Retry(total=3, backoff_factor=1,
                status_forcelist=[429, 500, 502, 503])
s.mount('https://', HTTPAdapter(max_retries=retries))
r = s.get(url, timeout=10)   # 超 10 秒自动放弃,遇 5xx/429 自动重试 3 次

5.5 代理配置

proxies = {
    'http':  'http://127.0.0.1:7890',
    'https': 'http://127.0.0.1:7890',
}
r = requests.get(url, proxies=proxies, timeout=10)
真实踩坑:如果系统开了全局代理(Clash、VPN 等,监听 127.0.0.1),requests / curl_cffi 会自动走系统代理,可能导致部分站点连不上或走弯路。遇到诡异的 over proxy 127.0.0.1 连接错误,先检查系统代理,必要时显式传 proxies={} 禁用,或换用浏览器模式。

5.6 scrapling 请求(本教程推荐)

scrapling 把请求和解析合在一起,且自动处理 TLS 指纹。三种 Fetcher 对应三种场景:

Fetcher方法底层适用
Fetcher.get() / .post()curl_cffi静态页面,最快
DynamicFetcher.fetch()PlaywrightJS 渲染页
StealthyFetcher.fetch()patchright强反爬(Cloudflare)
from scrapling.fetchers import Fetcher, DynamicFetcher, StealthyFetcher

# ① 静态:用 get,不是 fetch!
page = Fetcher.get('https://example.com')

# ② 动态渲染
page = DynamicFetcher.fetch('https://spa-site.com', headless=True)

# ③ 强反爬
page = StealthyFetcher.fetch('https://protected.com', headless=True,
                             network_idle=True)
最常见的入门坑:Fetcher(静态)只有 .get()/.post(),没有 .fetch();两个浏览器 Fetcher 才有 .fetch()。写错就报 AttributeError: type object 'Fetcher' has no attribute 'fetch'。记住:静态用 get,浏览器用 fetch。

5.7 curl_cffi 指纹伪装

from curl_cffi import requests as creq

# impersonate 指定模拟哪个浏览器的 TLS 指纹
r = creq.get('https://example.com', impersonate='chrome')
print(r.status_code)

06解析库详解 核心

拿到 HTML 后,用解析库把数据"抠"出来。主流四个:BeautifulSoup、lxml、parsel、正则。

6.1 BeautifulSoup(最易上手)

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'lxml')
title = soup.find('h1').get_text()                 # 按标签找
items = soup.select('.item a')                     # CSS 选择器
for a in items:
    print(a.get_text(), a['href'])               # 文本 + 属性

# 按文本找(BeautifulSoup 也支持)
tag = soup.find('a', string='登录')

6.2 lxml(速度最快)

from lxml import etree

tree = etree.HTML(html)
titles = tree.xpath('//h1/text()')                # XPath
links = tree.xpath('//a[@class="item"]/@href')    # 取属性

6.3 parsel(Scrapy 同款,功能最全)

from parsel import Selector

sel = Selector(html)
sel.css('h1::text').get()                        # CSS 取文本
sel.xpath('//a/@href').getall()                  # XPath 取属性
sel.css('a').re(r'href="(.*?)"')                # 甚至能套正则

6.4 JSON 解析(接口数据)

import json

data = r.json()                    # requests 直接转
# 或
data = json.loads(text)            # 字符串转字典
title = data['data']['list'][0]['title']   # 按层级取值

# 嵌套深时用 jmespath 更优雅
import jmespath
titles = jmespath.search('data.list[*].title', data)

6.5 四大解析方式对比

方式上手难度速度功能推荐场景
BeautifulSoup★ 最简单中CSS + 部分文本查找入门、快速验证
lxml★★最快纯 XPath追求性能
parsel★★快CSS+XPath+正则Scrapy 生态
正则★★★快任意文本JS 抠数据、兜底
推荐路径:入门用 BeautifulSoup 找感觉,主力用 parsel(或 scrapling 自带的解析,本质就是 parsel)——它 CSS/XPath/正则三合一,一个库走天下。

6.6 实战:解析一个列表页

from scrapling.fetchers import Fetcher

page = Fetcher.get('https://demo-site.com/')
# 一次抓取多个字段,组合成结构化数据
for card in page.css('a[href^="/anime/"]'):
    href = card.attrib.get('href')
    name = card.css('img::attr(alt)').get()
    if name:
        print(name, href)

07数据存储方案 核心

数据抓下来要落地。按数据量和查询需求选存储方式。

7.1 文件类(轻量数据)

import csv, json

# CSV:表格数据,Excel 直接打开(utf-8-sig 防乱码)
with open('data.csv', 'w', newline='', encoding='utf-8-sig') as f:
    w = csv.DictWriter(f, fieldnames=['title', 'price'])
    w.writeheader()
    w.writerows(rows)

# JSON:结构化、嵌套数据
json.dump(data, open('data.json', 'w', encoding='utf-8'),
          ensure_ascii=False, indent=2)

# JSONL:一行一条,适合大数据量逐条追加
with open('data.jsonl', 'a', encoding='utf-8') as f:
    f.write(json.dumps(item, ensure_ascii=False) + '\n')

7.2 SQLite(单机爬虫最佳归宿)

零配置、单文件、支持 SQL,个人爬虫首选。

import sqlite3

conn = sqlite3.connect('data.db')
conn.execute('''CREATE TABLE IF NOT EXISTS anime(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT, url TEXT UNIQUE, meta TEXT)''')

# 插入(UNIQUE 约束 + INSERT OR IGNORE 自动去重)
conn.execute('INSERT OR IGNORE INTO anime(title,url,meta) VALUES (?,?,?)',
             (title, url, meta))
conn.commit()

# 查询
for row in conn.execute('SELECT title, url FROM anime LIMIT 5'):
    print(row)

7.3 其他数据库对比

存储适用规模特点何时用
CSV / JSON< 10 万条最简单、可读一次性小任务
SQLite< 百万条零配置、单文件个人项目首选
MySQL百万+关系型、多表关联结构化、需复杂查询
MongoDB百万+文档型、灵活字段不固定、JSON 类数据
Redis内存级极快、支持队列/集合去重集合、任务队列
建议:90% 的个人爬虫,SQLite 就够了。只有数据量大到单机扛不住,或需要多进程共享任务队列时,才上 MySQL / Redis。

7.4 Redis 做去重与队列(进阶)

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 集合去重:sadd 返回 1=新增,0=已存在
if r.sadd('crawled', url):
    # 新 URL,加入待抓队列
    r.lpush('todo', url)

# 从队列取任务
task = r.rpop('todo')

08并发与异步提速 进阶

串行抓 1 万条可能要几小时,并发能压到几分钟。但并发是把双刃剑——提速的同时也更容易被封。

8.1 并发模型总览

并发模型对比串行、多线程、协程三种模式 串行 一个一个抓,慢但稳 多线程 ThreadPool,I/O 密集适用 协程异步 asyncio,单线程高并发 简单可控 · 适合入门与小任务 提速明显 · 代码改动小 性能最强 · 学习曲线陡 共同前提:控制并发数,别把对方服务器打爆(详见第 10 章频率控制)
图 8-1 三种并发模型对比

8.2 多线程(ThreadPoolExecutor,最常用)

网络请求是 I/O 密集型,多线程提速明显、代码改动最小:

from concurrent.futures import ThreadPoolExecutor
from scrapling.fetchers import Fetcher

urls = [f'https://site.com/anime/{i}' for i in range(100, 130)]

def fetch(url):
    try:
        return url, Fetcher.get(url).css('title::text').get()
    except Exception as e:
        return url, f'ERR: {e}'

with ThreadPoolExecutor(max_workers=5) as ex:   # 5 并发,别贪多
    results = list(ex.map(fetch, urls))

for url, title in results:
    print(title)

8.3 协程异步(asyncio + aiohttp,性能最强)

import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        return url, await resp.text()

async def main():
    async with aiohttp.ClientSession() as s:
        tasks = [fetch(s, u) for u in urls]
        return await asyncio.gather(*tasks)

results = asyncio.run(main())

8.4 并发控制:信号量限速

# 限制同时只有 5 个请求在飞,避免瞬间打满
sem = asyncio.Semaphore(5)

async def fetch(session, url):
    async with sem:                      # 拿到令牌才执行
        async with session.get(url) as resp:
            return await resp.text()

8.5 选型建议

场景推荐理由
入门 / 小任务(< 1 千)串行简单、不易被封、好调试
中等(1 千–10 万)多线程提速明显、代码改动小
大规模(10 万+)协程 / Scrapy性能与可维护性
黄金法则:先把串行逻辑写对、写稳,再加并发。并发数从小(3–5)开始试,观察是否触发 429/封 IP,再逐步上调。绝大多数封禁都是并发太猛导致的,不是反爬多强。
PART 3 · 进阶对抗
动态渲染 · 反爬 · 框架 · 逆向

09动态页面渲染 进阶

当数据由 JavaScript 动态生成(源码里看不到、Network 里有 XHR),就进入动态渲染的领域。

9.1 第一原则:先找接口,别急着开浏览器

很多"动态站"其实是前端渲染 + 后端 JSON 接口。直接请求 JSON 接口,比开浏览器快一个数量级、省资源、还稳定。

1
判断静态/动态

右键 → View page source,搜目标数据。源码里有=静态直抓;没有=动态。

2
找接口

F12 → Network → 刷新 → 筛选 Fetch/XHR,逐个看响应,找到返回目标数据的那个请求。

3
复制接口

右键该请求 → Copy as cURL → 转 Python。直接请求这个接口拿 JSON,绕过渲染。

4
实在没有/接口加密

才用浏览器渲染(Playwright / DynamicFetcher)。

动态页面决策树优先接口,其次浏览器渲染 页面数据哪来的? 有 JSON 接口 requests 直连(最快) 无接口 / 接口加密 浏览器渲染 JSON 响应 json() 直接解析 渲染后 HTML 再走 CSS / XPath
图 9-1 动态页面处理决策树

9.2 Playwright 详解

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto('https://spa-site.com')

    # 等待关键元素加载完成(动态页必须)
    page.wait_for_selector('.product-list', timeout=10000)

    # 滚动加载更多(无限滚动页面)
    page.mouse.wheel(0, 3000)
    page.wait_for_timeout(2000)

    html = page.content()              # 拿渲染后的完整 HTML
    page.screenshot(path='shot.png')    # 截图调试用
    browser.close()

9.3 scrapling 动态抓取(更简洁)

from scrapling.fetchers import DynamicFetcher, StealthyFetcher

# Dynamic:JS 渲染后拿页面
page = DynamicFetcher.fetch(
    'https://spa-site.com',
    headless=True,
    network_idle=True,          # 等网络空闲,确保 JS 加载完
    wait_selector='.product',   # 等特定元素出现
)
print(page.css('h1::text').get())

# Stealthy:强反爬(Cloudflare 盾等),真实浏览器指纹
page = StealthyFetcher.fetch(
    'https://protected.com',
    headless=True,
    solve_cloudflare=True,     # 自动过 Turnstile 盾
)
关于「过盾」的边界:solve_cloudflare 这类能力只应用于你自有或已获授权的站点(比如测试自己站点的防护配置)。把它用在他人站点上绕过安全防护,很可能踩中第 1.3 节讲的法律红线——技术上能做 ≠ 做了没事。学习中遇到强防护站点,正确做法是换数据源或用官方 API,而不是硬破。

9.4 截获 XHR:绕过渲染直拿数据

scrapling 的 capture_xhr 能拦截页面发出的接口请求——既不用解析渲染后 HTML,也不用自己逆向接口:

page = DynamicFetcher.fetch(
    'https://spa-site.com/products',
    capture_xhr='/api/products',    # 拦截含此关键字的请求
)
for req in page.captured_xhr:
    data = req.json()               # 直接拿到接口返回的 JSON
    print(data)
注意:capture_xhr 是"路径关键字匹配拦截",不是"强制抓取"。只有页面真实发出了含该关键字的请求才会被拦下。匹配不到就返回空(结果里不会出现该字段)——纯 SSR 站点(无 XHR)传这个参数没用。

9.5 Selenium(老方案,了解即可)

Selenium 是浏览器自动化的老前辈,语法啰嗦、速度慢,现在基本被 Playwright 取代。除非维护老项目,否则直接学 Playwright。

10反爬对抗体系 进阶

反爬是猫鼠游戏。理解对方的检测手段,才知道怎么应对。本章按"从轻到重"梳理完整对抗体系。

10.1 反爬对抗金字塔

反爬对抗金字塔从请求头到验证码的六层对抗 6 滑块/验证码/强风控 5 TLS 指纹 / 浏览器指纹 4 Cookie 登录态 / IP 代理池 1-3 请求头 / 频率 / Referer
图 10-1 反爬对抗层级(越往上越难、成本越高)

10.2 各层对抗手段

层级反爬手段你的应对
1检查 User-Agent设置真实浏览器 UA
2校验 Referer(防盗链)补全 Referer,模拟站内跳转
3频率限制 / IP 封禁随机延时、指数退避、限速
4需要 Cookie / 登录Session 保持登录态
5TLS / 浏览器指纹检测curl_cffi / StealthyFetcher 模拟指纹
6滑块、验证码、人机验证成本极高,建议换数据源

10.3 频率控制(最重要的自律)

import time, random

for url in urls:
    fetch(url)
    time.sleep(random.uniform(1, 3))   # 随机 1-3 秒,模拟人类
再强调一次:绝大多数被封 IP 是自己抓太快,不是对方反爬多强。先限速,再谈对抗。

10.4 IP 代理池

proxy_list = [
    'http://1.2.3.4:8080',
    'http://5.6.7.8:8080',
]
proxy = random.choice(proxy_list)
r = requests.get(url, proxies={'http': proxy, 'https': proxy})

自建代理池需维护可用性检测(定期测速、剔除失效 IP),也可用付费代理服务。小规模抓取用本地代理(如 Clash)即可。

10.5 JS 混淆与字体反爬

10.6 AI 辅助反爬分析(2026 新趋势)

把抓包结果、混淆 JS、异常响应喂给大模型,让它帮你分析"参数怎么生成的""哪里被拦了"。这是近两年爬虫工作流的重大变化——逆向分析的效率被 AI 大幅拉高。

务实忠告:第 5 层以下 90% 能自己解决;第 6 层(滑块、人机验证、强风控)投入产出比极低。专业场景用打码平台,学习场景换一个数据源远比硬刚划算。爬虫的精髓是"找到能拿到的数据",不是"攻破最难的站"。

11Scrapy 框架 进阶

当爬虫需要规模化、规范化、长期维护时,用 Scrapy 框架。它把"请求调度、解析、存储、去重、中间件"都标准化了。

11.1 Scrapy 架构

Scrapy 架构Engine 居中调度,Spider、Scheduler、Downloader、Pipeline 协作 Engine 引擎·总调度 Scheduler 调度器·去重队列 Spider 爬虫·解析规则 Downloader 下载器·发请求 Pipeline 管道·存数据
图 11-1 Scrapy 五大组件协作

11.2 创建项目与 Spider

# 命令行创建项目
scrapy startproject myspider
cd myspider
scrapy genspider anime demo-site.com   # 生成一个 spider
scrapy crawl anime                          # 运行

11.3 Spider 核心代码

import scrapy

class AnimeSpider(scrapy.Spider):
    name = 'anime'
    start_urls = ['https://demo-site.com/']

    def parse(self, response):
        # 解析列表页,提取详情链接并跟进
        for href in response.css('a[href^="/anime/"]::attr(href)').getall():
            yield response.follow(href, self.parse_detail)

    def parse_detail(self, response):
        yield {
            'title': response.css('h1::text').get(),
            'url': response.url,
            'meta': response.css('meta[name="description"]::attr(content)').get(),
        }

11.4 Item 与 Pipeline(存数据)

# pipelines.py · 存入 SQLite
import sqlite3

class SQLitePipeline:
    def open_spider(self, spider):
        self.conn = sqlite3.connect('anime.db')
        self.conn.execute('CREATE TABLE IF NOT EXISTS anime(title TEXT,url TEXT,meta TEXT)')

    def process_item(self, item, spider):
        self.conn.execute('INSERT INTO anime VALUES (?,?,?)',
                          (item['title'], item['url'], item['meta']))
        self.conn.commit()
        return item

11.5 常用配置(settings.py)

USER_AGENT = 'Mozilla/5.0 ...'
DOWNLOAD_DELAY = 1                # 请求间隔(秒),礼貌限速
CONCURRENT_REQUESTS = 8           # 全局并发数
ROBOTSTXT_OBEY = True             # 遵守 robots.txt
ITEM_PIPELINES = {'myspider.pipelines.SQLitePipeline': 300}
何时上 Scrapy:数据量 > 10 万、多站点长期维护、需要定时和告警。小规模一次性任务,用 scrapling / requests 更快——别为了用框架而用框架。

12接口抓取与逆向 进阶

很多高价值数据藏在接口里。学会抓包分析接口,是爬虫进阶的分水岭。

12.1 抓包工具

工具特点适用
浏览器 DevTools内置、零安装网页接口分析(首选)
mitmproxy开源、可脚本化App / 复杂网页抓包
Charles / FiddlerGUI 友好桌面端抓包

12.2 接口分析四步法

1
定位接口

Network → Fetch/XHR,逐个看响应,找到返回目标数据的请求。

2
分析参数

看 Query 参数和请求体:哪些是固定的(如页码),哪些是动态生成的(如 sign、token、timestamp)。

3
处理签名

固定参数直接复制;动态签名看 JS 怎么生成(可用 LLM 辅助读混淆代码)。

4
代码复现

Copy as cURL → 转 Python → 替换动态参数 → 循环调用。

12.3 接口签名(sign)的常见套路

# 常见签名 = md5(参数拼接 + 密钥 + 时间戳)
import hashlib, time

def make_sign(params, secret):
    s = ''.join(f'{k}={v}' for k, v in sorted(params.items()))
    s += secret + str(int(time.time()))
    return hashlib.md5(s.encode()).hexdigest()

params = {'page': 1, 'size': 20}
params['sign'] = make_sign(params, '网站JS里抠出来的密钥')
r = requests.get('https://api.site.com/list', params=params)
提示:密钥和盐值通常藏在网站的 JS 文件里。搜 sign、md5、encrypt 等关键词定位生成逻辑,或直接把 JS 片段交给 LLM 分析。

12.4 移动端抓取

App 的数据接口往往比网页更干净。用 mitmproxy 抓手机流量(手机设代理指向电脑),分析 App 的接口。注意:现代 App 多有证书绑定(SSL Pinning),需额外手段绕过,门槛较高,学习阶段建议先用网页版练手。

PART 4 · 工程化与实战
断点 · 部署 · 完整项目

13工程化与部署 进阶

抓几十条不需要工程化;抓几万条还要长期稳定跑,就需要下面这些"基础设施"。

13.1 断点续爬

核心思想:已抓的 URL 存进数据库,启动时先读出来跳过。配合异常捕获,就能跑几天几夜不中断、崩了也不白干。

import sqlite3, time, random

conn = sqlite3.connect('crawl.db')
conn.execute('CREATE TABLE IF NOT EXISTS done(url TEXT PRIMARY KEY)')
conn.execute('CREATE TABLE IF NOT EXISTS anime(url TEXT, title TEXT)')

done = {r[0] for r in conn.execute('SELECT url FROM done')}

for url in urls:
    if url in done: continue            # 断点跳过
    try:
        title = Fetcher.get(url).css('title::text').get()
        conn.execute('INSERT INTO anime VALUES (?,?)', (url, title))
        conn.execute('INSERT INTO done VALUES (?)', (url,))
        conn.commit()                        # 每成功一条立即落库
    except Exception as e:
        print('失败跳过:', url, e)
    time.sleep(random.uniform(0.5, 1.5))

13.2 增量爬取

只抓新增/变更数据,不重复抓全量。常用两种策略:

13.3 日志与监控

import logging

logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] %(levelname)s: %(message)s',
    handlers=[logging.FileHandler('crawl.log', encoding='utf-8'),
              logging.StreamHandler()]
)
log = logging.getLogger('crawl')
log.info('抓取成功 %s', url)
log.warning('第 %d 次重试', attempt)
log.error('失败 %s: %s', url, e)

13.4 定时调度

方式命令/配置适用
Windows 任务计划taskschd.msc 新建任务Windows 本机定时
Linux cron0 8 * * * python crawl.py服务器定时
云函数腾讯云函数等定时触发免维护、按需付费
APScheduler代码内定时需常驻进程
# APScheduler 示例(常驻进程内定时)
from apscheduler.schedulers.blocking import BlockingScheduler

sched = BlockingScheduler()
sched.add_job(crawl_task, 'cron', hour=8)   # 每天 8 点跑
sched.start()

13.5 爬虫系统总体架构

爬虫系统架构调度、抓取、解析、存储、监控五层 调度层 · 定时触发 / 任务队列 / 断点恢复 抓取层 · 请求库 / 浏览器渲染 / 代理池 / 限速重试 解析层 · CSS / XPath / JSON / 正则 · 字段抽取与清洗 存储层 · SQLite / MySQL / Redis · 去重与持久化 横向贯穿:日志 · 监控 · 告警(每一层都要可观测)
图 13-1 一个可长期运行的爬虫系统

14实战项目集 实战

四个由浅入深的完整项目,覆盖本教程所有核心技能。

项目一:动漫站全目录抓取(静态 + 存储 + 断点)

目标:稀饭动漫 Next,抓首页番剧链接 → 详情页 → 存 SQLite。用到:静态抓取、列表解析、翻页、断点续爬、限速。

# crawl_anime.py
import sqlite3, time, random
from scrapling.fetchers import Fetcher

BASE = 'https://demo-site.com'
conn = sqlite3.connect('anime.db')
conn.execute('CREATE TABLE IF NOT EXISTS anime(id INTEGER PRIMARY KEY,title TEXT,url TEXT,meta TEXT)')
conn.execute('CREATE TABLE IF NOT EXISTS done(url TEXT PRIMARY KEY)')

def links_of(page):
    return set(page.css('a[href^="/anime/"]::attr(href)').getall())

def save(href):
    if conn.execute('SELECT 1 FROM done WHERE url=?',(href,)).fetchone(): return
    p = Fetcher.get(BASE+href)
    conn.execute('INSERT INTO anime(title,url,meta) VALUES (?,?,?)',
        (p.css('h1::text').get(), href,
         p.css('meta[name="description"]::attr(content)').get()))
    conn.execute('INSERT INTO done VALUES (?)',(href,)); conn.commit()

links = set()
for i in range(1,4):
    links |= links_of(Fetcher.get(f'{BASE}/browse?page={i}'))
    time.sleep(random.uniform(1,2))
for h in sorted(links):
    try: save(h)
    except Exception as e: print('跳过', h, e)
print('入库:', conn.execute('SELECT COUNT(*) FROM anime').fetchone()[0])
使用提醒:本脚本仅用于学习抓取流程。请勿用于批量下载或传播动漫内容(版权风险)。抓取时保持低频、遵守站点规则。

项目二:电商商品价格监控(并发 + 定时)

目标:定时抓商品页价格,入库对比涨跌。用到:多线程、SQLite、定时调度、增量。

import sqlite3, time
from concurrent.futures import ThreadPoolExecutor
from scrapling.fetchers import Fetcher

URLS = ['https://shop.com/item/1', 'https://shop.com/item/2']
conn = sqlite3.connect('price.db')
conn.execute('CREATE TABLE IF NOT EXISTS price(ts REAL,item TEXT,price REAL)')

def grab(u):
    p = Fetcher.get(u)
    price = p.css('.price::text').get()
    conn.execute('INSERT INTO price VALUES (?,?,?)',(time.time(),u,price)); conn.commit()
    return u, price

with ThreadPoolExecutor(3) as ex:
    for u, price in ex.map(grab, URLS): print(u, price)

项目三:图片批量下载(接口 + 文件流)

目标:抓页面所有图片地址并下载。用到:属性提取、文件流写入、并发下载。

import os, requests
from concurrent.futures import ThreadPoolExecutor
from scrapling.fetchers import Fetcher

os.makedirs('imgs', exist_ok=True)
page = Fetcher.get('https://site.com')
imgs = page.css('img::attr(src)').getall()

def dl(src):
    if not src.startswith('http'): return
    name = src.split('/')[-1]
    r = requests.get(src, timeout=15)
    open(f'imgs/{name}', 'wb').write(r.content)

with ThreadPoolExecutor(8) as ex:
    ex.map(dl, imgs)

项目四:动态 SPA 数据抓取(渲染 + XHR 截获)

目标:抓一个前端渲染的列表页,直接截获其接口 JSON。用到:DynamicFetcher、capture_xhr。

from scrapling.fetchers import DynamicFetcher

page = DynamicFetcher.fetch('https://spa-site.com/list',
    headless=True, capture_xhr='/api/list')
for req in page.captured_xhr:
    for item in req.json().get('data', []):
        print(item)

四个项目的技能覆盖

项目请求解析存储并发进阶点
动漫目录静态CSSSQLite—断点续爬
价格监控静态CSSSQLite多线程定时增量
图片下载静态属性文件多线程文件流
SPA 数据渲染JSON——XHR 截获

附录常见问题与资源

A. 常见问题排查

问题原因解决
AttributeError: no attribute 'fetch'静态 Fetcher 用了 fetch改用 .get()
over proxy 127.0.0.1 连接失败系统代理拦截显式 proxies={} 或换浏览器模式
抓到的数据是空的JS 渲染,源码无数据转第 9 章动态渲染
突然 403 / 429触发频率限制加长延时、降并发、换 IP
中文乱码编码问题设 r.encoding 或用 utf-8-sig 存 CSV
capture_xhr 为空站点无 XHR(纯 SSR)该站静态抓即可

B. 学习资源

入门书

《Python 3 网络爬虫开发实战(第2版)》——覆盖全流程,中文经典。

官方文档

Playwright Docs、Scrapy 教程、parsel、curl_cffi、scrapling README。

练手素材

httpbin.org(测请求)、公开 API 列表、GitHub 高星爬虫项目。

工具

curlconverter(cURL 转代码)、DB Browser for SQLite、mitmproxy。

C. 工具速查

scraplingrequestshttpxcurl_cffi parselBeautifulSouplxmljmespath playwrightscrapyaiohttpsqlite3 redisapschedulermitmproxy

结语:爬虫是"项目驱动"的技能。找一个真实目标,从抓 10 条到抓 1 万条,每个环节踩坑再补课,比读十本书都快。记住三个词:合法、节制、可持续——做到这三点,就是好爬虫工程师。
↑