4. 静态网页抓取:requests + BeautifulSoup 实战

静态网页抓取,说白了就是去拿那些「已经写死在 HTML 里」的数据。你打开浏览器,右键查看源代码,能看到的内容,基本都能用 requests 拿下来,再用 BeautifulSoup 解析出来。

我刚开始做采集的时候,总觉得这步太简单,结果踩了不少坑。嗯,今天咱们就把这块彻底捋清楚。

4.1 环境准备与核心库

先装两个库,一个负责「拿」,一个负责「拆」:

pip install requests beautifulsoup4 lxml

我个人习惯把 lxml 也装上。为什么?因为 BeautifulSoup 默认的解析器是 Python 内置的 html.parser,速度慢,容错也一般。lxml 是 C 扩展,快得多,而且对「不规范」的 HTML 容忍度更高。

小提示: 如果你在公司内网,记得配代理。requests 的 proxies 参数可以直接搞定。

4.2 requests 基础用法

拿一个页面,核心就三步:

  1. 构造请求 —— 告诉服务器你要什么
  2. 发送请求 —— 等服务器回你
  3. 处理响应 —— 拿到 HTML 字符串

看个最简单的例子:

import requests

url = 'https://example.com/products'
response = requests.get(url)

# 检查状态码
if response.status_code == 200:
    html = response.text
    print('页面获取成功,长度:', len(html))
else:
    print('请求失败,状态码:', response.status_code)

这里有个坑。我曾经遇到一个网站,明明浏览器能打开,requests 却返回 403。为什么?因为服务器检查了 User-Agent。浏览器会带上一串标识,而 requests 默认的 User-Agent 是 python-requests/2.x.x,服务器一看就知道你是爬虫。

解决办法很简单,加个请求头:

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers)
注意: 别把 headers 搞得太复杂。我见过有人把几十个请求头全复制进去,其实大部分都没用。核心就三个:User-Agent、Referer、Cookie。够用就行。

4.3 BeautifulSoup 解析与 CSS 选择器

拿到 HTML 之后,就该 BeautifulSoup 上场了。它能把 HTML 字符串变成一棵「标签树」,然后你用选择器去树上摘果子。

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'lxml')

# 用 CSS 选择器找元素
titles = soup.select('h2.product-title')
prices = soup.select('span.price')

for title, price in zip(titles, prices):
    print(title.get_text(strip=True), price.get_text(strip=True))

CSS 选择器是我最常用的方式。你想想看,前端写页面用的就是 CSS,你用同样的语法去选元素,天然顺手。

常用的 CSS 选择器规则:

语法 含义 示例
tag 标签选择器 soup.select('div')
.class 类选择器 soup.select('.price')
#id ID 选择器 soup.select('#main')
parent > child 直接子元素 soup.select('ul > li')
tag[attr] 属性选择器 soup.select('a[href]')
tag[attr="value"] 属性精确匹配 soup.select('a[href="/login"]')
核心原则: 选择器越短越好。能用 class 就别用复杂的层级。页面结构一变,长选择器就断了。

4.4 XPath 定位元素

CSS 选择器虽然好用,但有些场景它搞不定。比如你想选「第三个 li 里的第二个 a 标签」,或者「包含某个文本的 div」。这时候 XPath 就派上用场了。

XPath 是另一种路径语言,比 CSS 选择器更强大,但也更啰嗦。BeautifulSoup 本身不支持 XPath,需要配合 lxml 的 etree 使用:

from lxml import etree

# 把 HTML 转成 lxml 的 Element 对象
tree = etree.HTML(html)

# XPath 定位
titles = tree.xpath('//h2[@class="product-title"]/text()')
prices = tree.xpath('//span[@class="price"]/text()')

for title, price in zip(titles, prices):
    print(title.strip(), price.strip())

常用的 XPath 表达式:

表达式 含义 示例
//tag 任意位置的 tag //div
/tag 直接子元素 /div
@attr 属性 //a[@href]
text() 文本内容 //h1/text()
position() 位置 //li[position()=3]
contains() 包含 //div[contains(@class, "price")]
我的习惯: 简单的选择用 CSS,复杂的用 XPath。比如选所有链接用 soup.select('a'),但选「第三个表格里的第五行」就用 XPath。两者互补,别死磕一种。

4.5 CSS 选择器 vs XPath:怎么选?

很多新手会纠结这个问题。我直接给你一个对比表:

维度 CSS 选择器 XPath
语法简洁度 ⭐⭐⭐⭐⭐ ⭐⭐
表达能力 ⭐⭐⭐ ⭐⭐⭐⭐⭐
按文本筛选 不支持 支持
按位置筛选 有限支持 完全支持
性能 快 稍慢
学习曲线 平缓 陡峭

我个人建议:80% 的场景用 CSS 选择器,剩下 20% 用 XPath。别为了炫技用 XPath 写一个超长表达式,维护起来你自己都看不懂。

4.6 实战:抓取商品列表

咱们来一个完整的例子。假设要抓取一个图书列表页,提取书名、价格和链接:

import requests
from bs4 import BeautifulSoup

url = 'https://books.toscrape.com/'
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(url, headers=headers)

if response.status_code != 200:
    print('页面加载失败')
    exit()

soup = BeautifulSoup(response.text, 'lxml')

books = []
for article in soup.select('article.product_pod'):
    title = article.h3.a.get('title')
    price = article.select_one('p.price_color').get_text(strip=True)
    link = article.h3.a.get('href')
    books.append({'title': title, 'price': price, 'link': link})

for book in books[:5]:
    print(f'{book["title"]} | {book["price"]} | {book["link"]}')

这段代码里有个细节:article.h3.a 这种链式写法,是 BeautifulSoup 的快捷方式。它等价于 article.find('h3').find('a')。但注意,如果中间某个标签不存在,会报 AttributeError。稳妥的做法是用 select_one 加判空。

避坑指南: 我曾经在抓一个电商网站时,发现某些商品没有价格(比如「已售罄」的商品不显示价格标签)。直接用 select_one 返回了 None,然后 .get_text() 直接崩了。后来我加了个判断:price_tag = article.select_one('p.price_color'); price = price_tag.get_text(strip=True) if price_tag else 'N/A'。记住,永远假设数据可能缺失。

4.7 本章知识体系

下面这张图把本章的核心逻辑串起来了:

静态网页抓取核心流程 1. requests 请求 2. BeautifulSoup 解析 CSS 选择器 XPath 3. 提取数据 4. 存储 / 输出 关键点: • 请求阶段注意 User-Agent 和状态码 • 解析阶段 CSS 与 XPath 互补使用 • 提取阶段永远假设数据可能缺失

4.8 常见问题与避坑

  • 中文乱码: 有些网站返回的编码不是 UTF-8。可以用 response.encoding = response.apparent_encoding 自动检测,或者直接指定 response.encoding = 'utf-8'。
  • 动态加载的内容: 如果页面里有些数据是 JavaScript 渲染的,requests 拿不到。这时候需要 Selenium 或直接找接口。别在静态抓取上死磕。
  • 反爬虫: 除了 User-Agent,有些网站还会检查 Cookie、Referer、请求频率。我建议每次请求间隔 1-3 秒,别把人家服务器打崩了。
  • 选择器失效: 网站改版是常事。我一般会在代码里加个日志,每次抓取记录成功提取的数量。如果数量突然变少,大概率是选择器需要更新了。
一句话总结: 静态网页抓取 = requests 拿页面 + BeautifulSoup 拆页面 + 选择器取数据。这三步走稳了,80% 的采集需求都能搞定。