文章

网络爬虫

网络爬虫

总览

一个最基本的爬虫流程通常是:

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"
}

如果需要保持登录态,应使用 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 客户端

httpxrequests 很像,也是 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.parserPython 内置,无需安装初学者、简单任务
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. 核心特性

  1. 自动等待元素 传统爬虫或自动化脚本常因页面加载延迟导致元素未找到而报错,需要使用 time.sleep() 硬等待。现代框架(如 Playwright)引入自动等待机制

    • 元素已附加到 DOM 树
    • 可见性(Visibility)
    • 稳定性(不再移动或改变大小)
    • 可接收事件(未被遮挡)

    自动等待机制大幅提高了脚本健壮性和执行效率。

  2. 多内核支持 Playwright 原生支持三大主流浏览器内核:

    • Chromium(Chrome / Edge)
    • Firefox
    • WebKit(Safari)

    可以确保爬虫或自动化脚本在不同平台的兼容性。

  3. 网络请求拦截与 Mock 支持拦截、修改或模拟 HTTP 请求:

    • 阻止广告或大文件资源,提升加载速度
    • Mock 后端接口数据,便于前端开发或测试
    • 监控请求参数和响应,便于调试
  4. 多上下文隔离(Browser Contexts) 每个 BrowserContext 拥有独立 Cookie、LocalStorage、SessionStorage 和缓存,相当于轻量级的无痕模式。适用于:

    • 同时登录多个账号
    • 多任务执行
    • 测试隔离环境
  5. 录制功能(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:port
  • https://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]

本文内容仅用于技术学习与合法授权场景,请严格遵守相关法律法规与平台协议。

warning

本文由作者按照 CC BY 4.0 进行授权