
本文详解如何使用 beautifulsoup 准确提取 scraping club 练习页面中所有商品的名称和价格,解决因 html 结构变化导致仅获取首项的常见问题,并提供健壮、可复用的 css 选择器方案。
本文详解如何使用 beautifulsoup 准确提取 scraping club 练习页面中所有商品的名称和价格,解决因 html 结构变化导致仅获取首项的常见问题,并提供健壮、可复用的 css 选择器方案。
你遇到的问题非常典型:原始代码中使用了 soup.find_all('div', class_='grid grid-cols-1 gap-4 sm:grid-cols-3'),但该 class 实际作用于整个商品列表容器(即父级 <div>),而非每个独立商品项。因此 items 只返回一个包含全部商品的顶层 div 对象,后续循环 for i in items: 自然只执行一次——这正是“只打印第一个商品”的根本原因。
正确的思路是:定位每个商品的最小独立包装单元。观察当前页面 HTML 结构可知,每个商品均包裹在 <div> 中,且该 <div> 直接包含 <h4>(商品名)和 <h5>(价格)子元素。此时最可靠、语义清晰的选择器是 CSS 的 :has() 伪类:div:has(>h4),它精准匹配“直接子元素含 <h4> 的 <div>”,完美对应每个商品卡片。
以下是优化后的完整实现:
import requests
from bs4 import BeautifulSoup
url = "https://scrapingclub.com/exercise/list_basic/?page=1"
response = requests.get(url)
response.raise_for_status() # 确保请求成功,抛出异常便于调试
soup = BeautifulSoup(response.content, "lxml")
# 使用 :has() 选择器精准定位每个商品卡片(推荐方式)
for i, card in enumerate(soup.select("div:has(>h4)"), 1):
try:
name = card.h4.text.strip()
price = card.h5.text.strip()
print(f"{i:^5} {name:<40} {price:<10}")
except AttributeError as e:
print(f"警告:第 {i} 个商品缺少 h4 或 h5 标签 —— {e}")✅ 关键改进说明:
- soup.select("div:has(>h4)") 替代过时的 class 匹配,不受 CSS 类名更新影响,稳定性高;
- enumerate(..., 1) 直接生成从 1 开始的序号,简洁且不易出错;
- 添加 try-except 异常处理,避免因个别商品结构异常导致整个爬虫中断;
- 使用 response.content(而非 .text)配合 lxml 解析器,更准确处理编码与 HTML 解析。
⚠️ 注意事项:
- :has() 是较新的 CSS 选择器,在极旧版 BeautifulSoup(如 < 4.12.0)或某些解析器中可能不支持。若报错,可降级为 soup.find_all("div", recursive=False) 配合手动校验 card.find("h4") 是否存在;
- 该页面共 10 件商品,若需翻页抓取多页,请在循环外嵌套页码逻辑(如 for page in range(1, 4):),并动态构造 URL;
- 生产环境务必添加 time.sleep() 和 User-Agent 头,遵守 robots.txt 并尊重网站反爬策略。
掌握基于语义结构(而非脆弱的 class 名)的选择器思维,是应对网页频繁改版的核心能力。此方法不仅适用于本练习,更是构建鲁棒爬虫的基础实践。

















