网络爬虫
总览
一个最基本的爬虫流程通常是:
1
发送请求 → 获取响应 → 解析内容 → 提取数据 → 清洗数据 → 存储结果
根据网页形态和数据来源不同,爬虫技术路线通常分为三类:
| 场景 | 推荐方案 |
|---|---|
| 数据来自接口 JSON | 直接调用接口 |
| 目标数据直接在 HTML 中 | requests/httpx + BeautifulSoup |
| 页面依赖 JavaScript、登录、点击、滚动 | 无头浏览器,如 Playwright |
这三者都可以“获取网页相关内容”,但它们的对象和成本不同。
1. 直接调用接口
现代网站通常是前后端分离的。浏览器打开网页后,前端 JavaScript 会请求后端接口,接口返回 JSON 数据,然后前端再把 JSON 渲染成页面。
例如页面地址是:
1
https://example.com/products
但真实数据可能来自:
1
https://example.com/api/products?page=1
直接调用接口时,程序拿到的是结构化数据:
1
2
3
4
5
6
7
8
9
{
"products": [
{
"name": "Keyboard",
"price": 99,
"stock": 12
}
]
}
这种方式通常最快、最干净、最稳定,因为不需要加载 HTML、CSS、图片,也不需要执行 JavaScript。
2. 使用 requests/httpx 抓 HTML
抓 HTML 指的是请求网页文档本身。例如:
1
2
3
4
import requests
html = requests.get("https://example.com/products").text
print(html)
如果目标数据已经写在 HTML 里,就可以直接解析:
1
2
3
4
<div class="product">
<h2>Keyboard</h2>
<span class="price">$99</span>
</div>
但如果页面只是一个前端应用壳:
1
2
<div id="app"></div>
<script src="main.js"></script>
那么单纯抓 HTML 就拿不到商品数据,因为数据是 JavaScript 后续请求接口后才渲染出来的。
3. 使用无头浏览器
无头浏览器是没有图形界面的真实浏览器。它可以加载 HTML、执行 JavaScript、处理 Cookie、渲染 DOM、点击按钮、滚动页面、截图或生成 PDF。
它适合处理:
- JavaScript 动态渲染页面
- 需要登录后访问的页面
- 需要点击、输入、滚动等交互的页面
- 需要截图或导出 PDF 的页面
- 接口参数复杂、难以直接复现的页面
但无头浏览器资源消耗最高。它需要启动浏览器内核,执行完整页面逻辑,占用更多 CPU 和内存。因此,只有在 HTTP 客户端和接口请求无法满足需求时,才应该使用无头浏览器。
requests:经典的 Python HTTP 请求库
requests 是 Python 生态中最常用的 HTTP 请求库之一。它的核心作用是让 Python 程序向服务器发起 HTTP 请求。
1. GET 请求
GET 通常用于获取页面或接口数据:
1
2
3
4
5
import requests
resp = requests.get("https://example.com")
print(resp.status_code)
print(resp.text)
2. 查询参数
不要手动拼接 URL 参数,推荐使用 params:
1
2
3
4
5
6
7
8
9
10
11
import requests
resp = requests.get(
"https://example.com/search",
params={
"q": "python",
"page": 2
}
)
print(resp.url)
这样可以自动处理 URL 编码,尤其适合中文、空格和特殊字符。
3. POST 请求
POST 通常用于提交数据。提交表单可以用 data:
1
2
3
4
5
6
7
8
9
import requests
resp = requests.post(
"https://example.com/login",
data={
"username": "alice",
"password": "123456"
}
)
提交 JSON 可以用 json:
1
2
3
4
5
6
7
resp = requests.post(
"https://example.com/api/users",
json={
"name": "alice",
"role": "admin"
}
)
json= 会自动序列化数据,并设置 Content-Type: application/json。
4. 响应对象
requests.get() 返回的是一个 Response 对象,常用属性包括:
1
2
3
4
5
6
resp.status_code # 状态码
resp.text # 文本内容
resp.content # 原始 bytes
resp.headers # 响应头
resp.cookies # Cookie
resp.url # 最终请求 URL
如果接口返回 JSON,可以使用:
1
data = resp.json()
但要注意,如果响应内容不是合法 JSON,resp.json() 会报错。
5. 请求头 headers
很多网站会根据请求头判断客户端类型。最常见的是 User-Agent:
1
2
3
4
5
headers = {
"User-Agent": "Mozilla/5.0"
}
resp = requests.get("https://example.com", headers=headers)
接口请求中也常用请求头传递认证信息:
1
2
3
4
headers = {
"Authorization": "Bearer YOUR_TOKEN",
"Accept": "application/json"
}
6. Session 与 Cookie
如果需要保持登录态,应使用 Session:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
session = requests.Session()
session.post(
"https://example.com/login",
data={
"username": "alice",
"password": "123456"
}
)
resp = session.get("https://example.com/profile")
print(resp.text)
Session 会自动保存并复用 Cookie,适合登录后连续访问多个页面。
7. timeout 与异常处理
生产代码不应省略 timeout:
1
resp = requests.get("https://example.com", timeout=10)
更稳健的写法是:
1
2
3
4
5
6
7
8
9
import requests
try:
resp = requests.get("https://example.com", timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
print("请求失败:", e)
else:
print(resp.text)
raise_for_status() 会在状态码为 4xx 或 5xx 时抛出异常。
httpx:更现代的 HTTP 客户端
httpx 和 requests 很像,也是 HTTP 客户端库。
它的优势是同时支持同步和异步请求,并支持 HTTP/2。
1. 同步用法
1
2
3
4
5
import httpx
resp = httpx.get("https://example.com")
print(resp.status_code)
print(resp.text)
POST JSON:
1
2
3
4
resp = httpx.post(
"https://example.com/api/users",
json={"name": "alice"}
)
2. Client 复用连接
如果要连续请求同一个网站,推荐使用 Client:
1
2
3
4
5
import httpx
with httpx.Client(timeout=10) as client:
resp1 = client.get("https://example.com/page1")
resp2 = client.get("https://example.com/page2")
这样可以复用连接、Cookie 和统一配置,性能更好。
3. 异步请求
httpx 的重要优势是原生支持异步:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import asyncio
import httpx
import time
# 模拟大量 URL
urls = [f"https://httpbin.org/delay/1" for _ in range(50)]
async def fetch_with_semaphore(client, url, semaphore):
# acquire: 获取令牌,如果达到上限则等待直到有令牌释放
async with semaphore:
try:
# 设置超时,避免单个请求卡死整个程序
resp = await client.get(url)
resp.raise_for_status()
return {"url": url, "status": resp.status_code, "len": len(resp.text)}
except httpx.HTTPError as exc:
return {"url": url, "error": str(exc)}
async def main():
# 限制最大并发数为 10
semaphore = asyncio.Semaphore(10)
# 使用 AsyncClient 连接池,复用 TCP 连接
async with httpx.AsyncClient(timeout=10.0) as client:
tasks = []
for url in urls:
task = asyncio.create_task(fetch_with_semaphore(client, url, semaphore))
tasks.append(task)
start_time = time.time()
# gather 等待所有任务完成
results = await asyncio.gather(*tasks)
end_time = time.time()
# 处理结果
success_count = sum(1 for r in results if "error" not in r)
print(f"完成 {success_count}/{len(results)} 个请求")
print(f"耗时: {end_time - start_time:.2f} 秒")
failed_url = [r["url"] for r in results if "error" in r]
if failed_url:
print("失败的 URL:")
for url in failed_url:
print(f" {url}")
# 打印前几个成功结果
for r in results[:3]:
print(r)
if __name__ == "__main__":
asyncio.run(main())
异步请求适合大量接口或页面并发访问,但要注意控制并发,避免给目标服务造成压力,也避免自己的机器资源耗尽。
BeautifulSoup
BeautifulSoup,通常通过 bs4 导入,是一个 HTML/XML 解析库。它不负责发送请求,只负责解析已经拿到的 HTML 内容。
解析器选择:
| Parser | 特点 | 推荐场景 |
|---|---|---|
html.parser | Python 内置,无需安装 | 初学者、简单任务 |
lxml | 速度快,容错好 | 生产环境、大批量解析 |
html5lib | 最接近浏览器行为 | 解析畸形 HTML |
1. 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from bs4 import BeautifulSoup
html = """
<html>
<body>
<h1 class="title">Hello</h1>
<a href="/about">About</a>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
print(soup.h1.text)
print(soup.a["href"])
2. find 与 find_all
find() 用于查找第一个匹配元素:
1
2
title = soup.find("h1")
print(title.get_text(strip=True))
按 class 查找:
1
item = soup.find("div", class_="product")
注意,Python 中 class 是关键字,所以 BeautifulSoup 使用 class_。
find_all() 用于查找所有匹配元素:
1
2
3
4
links = soup.find_all("a")
for link in links:
print(link.get_text(strip=True), link.get("href"))
3. CSS 选择器 select
select() 支持 CSS 选择器,使用起来很直观:
1
2
3
4
soup.select("a") # 所有 a 标签
soup.select(".product") # class 为 product 的元素
soup.select("#main") # id 为 main 的元素
soup.select("div.product .price")# 商品价格
取第一个匹配元素:
1
title = soup.select_one("h1.title")
4. 提取文本和属性
提取文本:
1
text = tag.get_text(strip=True)
多个子节点可以指定分隔符:
1
text = tag.get_text(" ", strip=True)
提取属性时推荐用 .get():
1
href = link.get("href")
这样属性不存在时会返回 None,不会直接抛异常。
5. 处理相对链接
网页中经常出现相对链接:
1
<a href="/article/1">文章</a>
需要使用 urljoin 转为完整 URL:
1
2
3
4
5
from urllib.parse import urljoin
base_url = "https://example.com/news/"
href = "/article/1"
full_url = urljoin(base_url, href)
6. 一个完整 HTML 解析例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
url = "https://example.com"
resp = requests.get(
url,
headers={"User-Agent": "Mozilla/5.0"},
timeout=10
)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
for a in soup.select("a[href]"):
text = a.get_text(strip=True)
href = urljoin(url, a.get("href"))
if text:
print(text, href)
我已经查看了你上传的笔记内容,关于“无头浏览器”的章节。我可以帮你优化和补充,使内容更加系统化、适合有基础的计算机专业学生学习,同时加入一些关键概念、实践建议和注意事项。下面是优化后的版本示例:
无头浏览器(Headless Browser)
无头浏览器是一类没有图形用户界面(GUI)的浏览器。它与普通浏览器功能一致,可以加载网页、执行 JavaScript、处理 Cookie、构建 DOM、计算布局并渲染页面,但不在屏幕上显示渲染结果。
这种特性使无头浏览器非常适合以下场景:
- 自动化测试
- 网络爬虫与数据采集
- 持续集成 / 持续部署(CI/CD)环境中的浏览器操作
1. 核心特性
自动等待元素 传统爬虫或自动化脚本常因页面加载延迟导致元素未找到而报错,需要使用
time.sleep()硬等待。现代框架(如 Playwright)引入自动等待机制:- 元素已附加到 DOM 树
- 可见性(Visibility)
- 稳定性(不再移动或改变大小)
- 可接收事件(未被遮挡)
自动等待机制大幅提高了脚本健壮性和执行效率。
多内核支持 Playwright 原生支持三大主流浏览器内核:
- Chromium(Chrome / Edge)
- Firefox
- WebKit(Safari)
可以确保爬虫或自动化脚本在不同平台的兼容性。
网络请求拦截与 Mock 支持拦截、修改或模拟 HTTP 请求:
- 阻止广告或大文件资源,提升加载速度
- Mock 后端接口数据,便于前端开发或测试
- 监控请求参数和响应,便于调试
多上下文隔离(Browser Contexts) 每个
BrowserContext拥有独立 Cookie、LocalStorage、SessionStorage 和缓存,相当于轻量级的无痕模式。适用于:- 同时登录多个账号
- 多任务执行
- 测试隔离环境
录制功能(playwright codegen) Playwright 提供代码生成工具,可记录用户操作生成高质量测试或爬虫脚本,降低学习成本。
2. 启动无头浏览器
1
2
3
4
5
6
7
8
9
10
11
12
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # 无头模式
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
viewport={"width": 1280, "height": 720}
)
page = context.new_page()
page.goto("https://example.com")
page.wait_for_selector("#main-content") # 自动等待
browser.close()
3. 网络拦截与 Mock
1
2
3
4
5
6
7
8
9
# 拦截图片请求
page.route("**/*.{png,jpg}", lambda route: route.abort())
# Mock API
page.route("**/api/user/info", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"name": "Test User", "id": 123}'
))
4. 多上下文多账号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
context_a = browser.new_context()
context_b = browser.new_context()
page_a = context_a.new_page()
page_b = context_b.new_page()
# 登录不同账号
page_a.goto("https://example.com/login")
page_a.fill("#username", "user_a")
page_a.click("#login-btn")
page_b.goto("https://example.com/login")
page_b.fill("#username", "user_b")
page_b.click("#login-btn")
5. 登录态保持与复用
1
2
3
4
context.storage_state(path="auth_state.json") # 保存状态
# 复用登录态
context = browser.new_context(storage_state="auth_state.json")
注意事项:
- Cookie / Token 可能过期,需要定期刷新
storage_state.json包含敏感数据,应加密存储并加入.gitignore- IndexedDB 数据通常不包含在
storage_state中
登录态管理策略:
- 检查账号是否已登录
- 登录态失效后重新登录
- 定期刷新 Token
- 异常账号冷却
- 二次验证时转人工
- 安全加密存储凭证
- 日志脱敏
代理 IP 管理
1. 代理格式
常见代理格式包括:
HTTP / HTTPS:
http://username:password@ip:porthttps://username:password@ip:port
SOCKS5:
socks5://username:password@ip:port
2. Python 示例(requests + 代理池)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
from random import choice
import time
# 代理池示例
proxy_pool = [
{"http": "http://user:pass@1.2.3.4:8080", "https": "https://user:pass@1.2.3.4:8080"},
{"http": "http://user:pass@5.6.7.8:3128", "https": "https://user:pass@5.6.7.8:3128"}
]
def get(url):
for _ in range(5): # 尝试最多 5 次
proxy = choice(proxy_pool)
try:
resp = requests.get(url, proxies=proxy, timeout=5)
if resp.status_code == 200:
return resp.text
except Exception as e:
print(f"代理 {proxy} 请求失败: {e}")
continue
return None
html = get("https://example.com")
3. 高级爬虫与自动化场景
- 与无头浏览器结合,代理配置可直接注入浏览器上下文:
1
2
3
4
5
6
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={"server": "http://1.2.3.4:8080"})
page = browser.new_page()
page.goto("https://example.com")
4. 安全与合规
用于模拟多源访问、测试网络服务等合法用途。常见场景包括:
- 验证 CDN 分发策略
- 监控自有服务在不同地区的可用性
- 企业合规的数据采集
- 广告落地页区域测试
风险提示:
- 不得用于规避封禁或进行未授权访问
- 不得用于隐藏身份进行攻击
- 企业合规采集数据时,必须遵循目标网站政策
- 对代理认证信息和日志进行加密存储
验证码与人工介入
验证码用于区分人类和自动化系统。合规系统遇到验证码或二次验证时,应该将其视为“需要人工确认”的任务状态,而不是尝试强行绕过。
安全隔离
在安全方面,浏览器会执行第三方页面 JavaScript,因此必须隔离运行环境。
尤其要防范 SSRF 类风险,例如页面诱导浏览器访问内网地址、云元数据服务或本地文件。
安全策略包括:
1
2
3
4
5
禁止访问 127.0.0.1
禁止访问内网网段
禁止访问云 metadata 地址
限制 DNS
限制出站网络
完整系统架构示例
一个较完整的爬虫或浏览器自动化平台可以设计为:
flowchart TD
subgraph 管理与调度
A[管理后台 / API] --> B[任务队列<br/>Redis / MQ / Kafka]
B --> C[调度器 Scheduler]
end
subgraph 资源池
C --> D[账号池<br/>Account Manager]
C --> E[代理池<br/>Proxy Manager]
end
D & E --> F[Worker 集群<br/>Playwright / HTTPX]
F --> G[浏览器实例池<br/>Browser / Context]
G --> H[数据提取 / 解析层<br/>DOM / JSON / bs4]
H --> I[数据存储 / 日志<br/>DB / S3 / ES]
本文内容仅用于技术学习与合法授权场景,请严格遵守相关法律法规与平台协议。
