㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:数据模型与清洗(Models & Utils)](#6️⃣ 核心实现:数据模型与清洗(Models & Utils))
- [7️⃣ 核心实现:请求与解析层(Fetcher & Parser)](#7️⃣ 核心实现:请求与解析层(Fetcher & Parser))
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
- [🔟 常见问题与排错(实战必读)](#🔟 常见问题与排错(实战必读))
- [1️⃣1️⃣ 进阶优化(可选但加分)](#1️⃣1️⃣ 进阶优化(可选但加分))
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
- [✅ 免责声明](#✅ 免责声明)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
本文将演示如何构建一个健壮的爬虫系统,针对主流菜谱网站的"分类/列表页"(例如"家常菜"或"烘焙"频道)。我们将使用 requests 获取网页,配合 BeautifulSoup 进行 DOM 解析。
本项目的核心亮点在于:引入 Python 3.7+ 的 Dataclasses 进行强类型数据建模,并编写专门的 清洗管道(Cleaning Pipeline),将五花八门的"烹饪时长"和"评分"标准化为可分析的数值格式,最终产出高质量的 CSV 数据集。
读完这篇你能获得什么?
- 🏗️ 工程规范 :学会使用
dataclass定义爬虫的数据模型,告别杂乱的字典(Dict)。 - 🧹 清洗神技 :掌握利用
re正则表达式处理复杂的"时间字符串"(如将 "1小时20分" 自动转为80分钟)。 - 📊 容错设计:学会处理网页中"评分缺失"或"字段隐藏"的常见坑点。
2️⃣ 背景与需求(Why)
为什么要爬?
- 营养与健康分析:通过分析"家常菜"分类下的平均用时和食材,制定更合理的膳食计划。
- 爆款预测:分析高分菜谱的标题关键词(如"快手"、"下饭"、"减脂"),找出流量密码。
- 个人私厨 App:为自己开发的"今天吃什么"小程序提供基础数据库。
目标站点 :示例某大型中文菜谱网 https://www.example-cook.com/category/home-cooking/
目标字段清单:
| 字段名 (Code) | 字段名 (CN) | 原始值示例 | 清洗后目标值 |
|---|---|---|---|
name |
菜名 | 🥒 响油黄瓜(脆爽) | 响油黄瓜 |
rating |
评分 | 4.8分 (100人评) | 4.8 (float) |
duration |
烹饪用时 | 1小时10分钟 | 70 (int, 分钟) |
detail_url |
详情链接 | /recipe/123456/ | 完整 URL |
3️⃣ 合规与注意事项(必写)
美食虽好,吃相要雅:
- Robots.txt:大部分菜谱网站允许抓取公开列表,但严禁抓取用户隐私数据。
- 并发控制 :菜谱网站图片多,带宽压力大。请务必使用单线程 或低并发(Max 3 workers) ,并设置
sleep间隔。 - 版权问题:菜谱的图片和具体做法步骤受版权保护。我们本次只采集"元数据"(标题、链接、评分),不批量搬运正文图片和步骤文字。
4️⃣ 技术选型与整体流程(What/How)
技术选型:Requests + BeautifulSoup + Dataclasses
- Dataclasses :相比普通的 Dictionary,它能提供字段类型提示,防止写错 Key,还能自动生成
__repr__,调试打印时非常清晰。 - Regex (re):处理"时间"字段的必备工具。菜谱网站的时间描述是最不规范的,必须用正则提取数字。
流程图:
入口: 分类列表页
下载 HTML
BS4: 定位菜谱卡片 List
遍历卡片
提取: 原始文本字段
清洗管道: 文本 -> 数值
封装: 生成 Recipe 对象
校验: 过滤无效数据
存储: 导出 CSV
5️⃣ 环境准备与依赖安装(可复现)
Python 版本:3.8+
依赖安装:
bash
pip install requests beautifulsoup4 pandas
项目目录结构:
text
recipe_spider/
├── data/ # 存放结果
├── models.py # 定义数据结构 (Dataclass)
├── utils.py # 清洗工具函数
└── spider.py # 主逻辑
6️⃣ 核心实现:数据模型与清洗(Models & Utils)
这是体现"代码详细解析"的部分。我们不直接把逻辑写在爬虫里,而是拆分出来。
1. 定义数据结构 (models.py)
python
from dataclasses import dataclass
@dataclass
class Recipe:
"""定义菜谱的数据模型"""
name: str
rating: float # 清洗后存浮点数
duration_min: int # 清洗后存分钟数
url: str
def is_valid(self):
"""简单校验:没有名字或链接的视为无效"""
return bool(self.name and self.url)
2. 编写清洗逻辑 (utils.py)
处理时间字符串是最头疼的,比如 "1.5小时"、"30分钟"、"10-20分钟"。
python
import re
def clean_duration(text):
"""
将各种时间字符串转换为分钟数 (int)
示例: "1小时30分" -> 90
"45分钟" -> 45
"10-20分钟" -> 15 (取平均)
"""
if not text:
return 0
text = text.strip()
# 模式1: 同时包含小时和分钟 (如 1小时30分)
match_hm = re.search(r'(\d+)[\u5c0f\u65f6h](\d+)[\u5206m]', text)
if match_hm:
h = int(match_hm.group(1))
m = int(match_hm.group(2))
return h * 60 + m
# 模式2: 只有小时 (如 1.5小时)
match_h = re.search(r'(\d+\.?\d*)[\u5c0f\u65f6h]', text)
if match_h:
return int(float(match_h.group(1)) * 60)
# 模式3: 只有分钟 (包含区间 10-20分)
# 提取所有数字,取平均值
nums = re.findall(r'\d+', text)
if nums:
nums = [int(n) for n in nums]
return sum(nums) // len(nums)
return 0
def clean_rating(text):
"""
清洗评分,如 "4.5分" -> 4.5
"""
if not text:
return 0.0
# 提取第一个浮点数
match = re.search(r'(\d+\.?\d*)', text)
if match:
return float(match.group(1))
return 0.0
7️⃣ 核心实现:请求与解析层(Fetcher & Parser)
python
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from models import Recipe
from utils import clean_duration, clean_rating
import time
import random
class RecipeFetcher:
def __init__(self, base_url):
self.base_url = base_url
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
def get_html(self, url):
try:
# 模拟随机思考时间
time.sleep(random.uniform(1, 2))
resp = requests.get(url, headers=self.headers, timeout=10)
resp.raise_for_status()
return resp.text
except Exception as e:
print(f"❌ 抓取失败: {url} | Error: {e}")
return None
class RecipeParser:
def parse_list(self, html, base_domain):
"""解析列表页 HTML,返回 Recipe 对象列表"""
soup = BeautifulSoup(html, 'html.parser')
recipes = []
# 假设菜谱卡片是 li class="recipe-item"
# ⚠️ 实际开发请根据 F12 调整 CSS 选择器
items = soup.select('li.recipe-item')
print(f" 🔍 分析 DOM 结构,找到 {len(items)} 个潜在菜谱...")
for item in items:
try:
# 1. 提取原始文本
# 标题通常在 h2 或 p class="name"
name_tag = item.select_one('.name a')
name_raw = name_tag.get_text(strip=True) if name_tag else ""
href_raw = name_tag['href'] if name_tag else ""
# 评分通常在 class="score" 或 "stats"
score_tag = item.select_one('.score')
score_raw = score_tag.get_text(strip=True) if score_tag else "0"
# 时间通常在 class="info" 或 span 标签里
# 假设页面显示: "评分 4.5 | 30分钟"
info_tag = item.select_one('.info')
info_text = info_tag.get_text(strip=True) if info_tag else ""
# 2. 调用 utils 进行清洗
full_url = urljoin(base_domain, href_raw)
# 假设 info_text 里包含时间,我们需要从中识别
# 简单粗暴的做法:直接丢给 clean_duration,正则会自动找数字
duration_val = clean_duration(info_text)
rating_val = clean_rating(score_raw)
# 3. 实例化 Dataclass
recipe = Recipe(
name=name_raw,
rating=rating_val,
duration_min=duration_val,
url=full_url
)
if recipe.is_valid():
recipes.append(recipe)
except AttributeError as e:
# 某个字段缺失导致报错,跳过该条
continue
return recipes
8️⃣ 数据存储与导出(Storage)
这里我们将 Recipe 对象列表转换为 Pandas DataFrame,然后存为 CSV。
python
import pandas as pd
import os
from dataclasses import asdict
class DataStorage:
def save(self, recipe_objects, filename="recipes.csv"):
if not recipe_objects:
print("⚠️ 无数据可保存")
return
# 核心技巧:利用 asdict 将 dataclass 转为字典
data_list = [asdict(r) for r in recipe_objects]
df = pd.DataFrame(data_list)
# 增加一列:可读性更好的时间显示 (可选)
# df['duration_desc'] = df['duration_min'].apply(lambda x: f"{x}分钟")
output_path = os.path.join("data", filename)
os.makedirs("data", exist_ok=True)
df.to_csv(output_path, index=False, encoding='utf-8-sig')
print(f"💾 成功导出 {len(df)} 条数据至: {output_path}")
9️⃣ 运行方式与结果展示(必写)
python
# spider.py
# 导入上面所有的类和函数
def main():
print("🚀 启动美食采集器 (GourmetSpider)...")
# 配置
base_domain = "https://www.example-cook.com"
start_url = "https://www.example-cook.com/category/hot?page={}"
fetcher = RecipeFetcher(base_domain)
parser = RecipeParser()
storage = DataStorage()
all_recipes = []
# 抓取前 3 页
for page in range(1, 4):
target = start_url.format(page)
print(f"\n🥘 正在"烹饪"第 {page} 页: {target}")
html = fetcher.get_html(target)
if not html:
continue
batch_recipes = parser.parse_list(html, base_domain)
all_recipes.extend(batch_recipes)
print(f" ✅ 本页收获: {len(batch_recipes)} 道菜")
# 保存结果
storage.save(all_recipes, filename="recipe_dataset.csv")
if __name__ == "__main__":
main()
示例运行结果:
text
🚀 启动美食采集器 (GourmetSpider)...
🥘 正在"烹饪"第 1 页: https://www.example-cook.com/category/hot?page=1
🔍 分析 DOM 结构,找到 20 个潜在菜谱...
✅ 本页收获: 18 道菜 (过滤掉 2 个无效数据)
🥘 正在"烹饪"第 2 页: https://www.example-cook.com/category/hot?page=2
🔍 分析 DOM 结构,找到 20 个潜在菜谱...
✅ 本页收获: 20 道菜
...
💾 成功导出 58 条数据至: data/recipe_dataset.csv
生成的 CSV 数据预览:
| name | rating | duration_min | url |
|---|---|---|---|
| 鱼香肉丝 | 4.8 | 25 | https://.../1001 |
| 慢炖牛腩 | 4.9 | 120 | https://.../1002 |
| 凉拌皮蛋 | 0.0 | 5 | https://.../1003 |
🔟 常见问题与排错(实战必读)
-
图片懒加载导致解析不到
- 现象 :有些网站的信息藏在图片的
alt属性里,但图片因为 lazy loading,src属性是空的或者是占位符。 - 解法 :查看 HTML 源码(不是 Elements 面板),通常真实链接在
data-src属性里。BS4 可以通过img['data-src']获取。
- 现象 :有些网站的信息藏在图片的
-
正则匹配不到时间
- 原因:网站写的是"半小时"而不是"30分钟",或者"数小时"。
- 解法 :完善
clean_duration函数,建立一个映射字典{'半小时': 30, '一天': 1440},先把中文词汇替换成数字再跑正则。
-
列表页没有评分/时间
- 现象:列表页只显示图和标题,点进去才有时间和评分。
- 解法 :这就需要修改架构了。解析列表拿到
url后,再遍历url发起二次请求(Nested Scraping)。如果量大,建议使用concurrent.futures.ThreadPoolExecutor并发访问详情页。
1️⃣1️⃣ 进阶优化(可选但加分)
-
多线程加速 (Concurrency) :
如果是上面的"二次请求"场景(List -> Detail),单线程会非常慢。
pythonfrom concurrent.futures import ThreadPoolExecutor def process_detail(url): # 请求详情页并解析的逻辑... pass with ThreadPoolExecutor(max_workers=5) as executor: results = list(executor.map(process_detail, url_list)) -
IP 代理池 :
菜谱网站为了防爬,可能会限制单 IP 的访问频率。接入一个简单的代理池(Proxy Pool),每次请求
proxies={"http": "http://1.2.3.4:8080"},能显著提高稳定性。 -
数据可视化 :
利用
matplotlib或seaborn,画一个"烹饪时长分布直方图 ",或者"评分 vs 难度的散点图",让你的数据瞬间高大上。
1️⃣2️⃣ 总结与延伸阅读
复盘:
在这个项目中,我们没有满足于简单的"抓下来就行",而是引入了 Data Cleaning (数据清洗) 的概念。通过 Dataclasses 和 Regex,我们将杂乱无章的原始网页文本,转化为了干净、结构化、可直接用于数据分析的"黄金数据"。这才是数据工程师的核心价值所在。
下一步:
- 尝试 Scrapy :如果你要爬取全站 10 万道菜谱,
requests可能会力不从心。Scrapy 框架提供了更完善的调度、去重和异步下载机制。 - 食材提取:挑战更难的字段------"食材清单"。通常它是一个列表,包含"食材名"和"用量"(如:猪肉 500g)。如何把它们解析成标准化的 JSON?试试看吧!
厨房里有烟火气,代码里有逻辑美。愿你的爬虫既稳又快!
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
