Python爬虫实战:采集菜谱网站的“分类/列表页”(例如“家常菜”或“烘焙”频道)数据,构建高可用的美食菜谱数据采集流水线(附CSV导出)!

㊗️本期内容已收录至专栏《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 数据集。

读完这篇你能获得什么?

  1. 🏗️ 工程规范 :学会使用 dataclass 定义爬虫的数据模型,告别杂乱的字典(Dict)。
  2. 🧹 清洗神技 :掌握利用 re 正则表达式处理复杂的"时间字符串"(如将 "1小时20分" 自动转为 80 分钟)。
  3. 📊 容错设计:学会处理网页中"评分缺失"或"字段隐藏"的常见坑点。

2️⃣ 背景与需求(Why)

为什么要爬?

  1. 营养与健康分析:通过分析"家常菜"分类下的平均用时和食材,制定更合理的膳食计划。
  2. 爆款预测:分析高分菜谱的标题关键词(如"快手"、"下饭"、"减脂"),找出流量密码。
  3. 个人私厨 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️⃣ 合规与注意事项(必写)

美食虽好,吃相要雅:

  1. Robots.txt:大部分菜谱网站允许抓取公开列表,但严禁抓取用户隐私数据。
  2. 并发控制 :菜谱网站图片多,带宽压力大。请务必使用单线程低并发(Max 3 workers) ,并设置 sleep 间隔。
  3. 版权问题:菜谱的图片和具体做法步骤受版权保护。我们本次只采集"元数据"(标题、链接、评分),不批量搬运正文图片和步骤文字。

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

🔟 常见问题与排错(实战必读)

  1. 图片懒加载导致解析不到

    • 现象 :有些网站的信息藏在图片的 alt 属性里,但图片因为 lazy loading,src 属性是空的或者是占位符。
    • 解法 :查看 HTML 源码(不是 Elements 面板),通常真实链接在 data-src 属性里。BS4 可以通过 img['data-src'] 获取。
  2. 正则匹配不到时间

    • 原因:网站写的是"半小时"而不是"30分钟",或者"数小时"。
    • 解法 :完善 clean_duration 函数,建立一个映射字典 {'半小时': 30, '一天': 1440},先把中文词汇替换成数字再跑正则。
  3. 列表页没有评分/时间

    • 现象:列表页只显示图和标题,点进去才有时间和评分。
    • 解法 :这就需要修改架构了。解析列表拿到 url 后,再遍历 url 发起二次请求(Nested Scraping)。如果量大,建议使用 concurrent.futures.ThreadPoolExecutor 并发访问详情页。

1️⃣1️⃣ 进阶优化(可选但加分)

  • 多线程加速 (Concurrency)

    如果是上面的"二次请求"场景(List -> Detail),单线程会非常慢。

    python 复制代码
    from 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"},能显著提高稳定性。

  • 数据可视化

    利用 matplotlibseaborn,画一个"烹饪时长分布直方图 ",或者"评分 vs 难度的散点图",让你的数据瞬间高大上。

1️⃣2️⃣ 总结与延伸阅读

复盘

在这个项目中,我们没有满足于简单的"抓下来就行",而是引入了 Data Cleaning (数据清洗) 的概念。通过 DataclassesRegex,我们将杂乱无章的原始网页文本,转化为了干净、结构化、可直接用于数据分析的"黄金数据"。这才是数据工程师的核心价值所在。

下一步

  • 尝试 Scrapy :如果你要爬取全站 10 万道菜谱,requests 可能会力不从心。Scrapy 框架提供了更完善的调度、去重和异步下载机制。
  • 食材提取:挑战更难的字段------"食材清单"。通常它是一个列表,包含"食材名"和"用量"(如:猪肉 500g)。如何把它们解析成标准化的 JSON?试试看吧!

厨房里有烟火气,代码里有逻辑美。愿你的爬虫既稳又快!

🌟 文末

好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

✅ 专栏持续更新中|建议收藏 + 订阅

墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?

评论区留言告诉我你的需求,我会优先安排实现(更新)哒~


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


✅ 免责声明

本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。

使用或者参考本项目即表示您已阅读并同意以下条款:

  • 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
  • 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
  • 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
  • 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
相关推荐
喵手6 小时前
Python爬虫实战:硬核解析 Google Chrome 官方更新日志(正则+文本清洗篇)(附 CSV 导出)!
爬虫·python·爬虫实战·零基础python爬虫教学·csv导出·监控谷歌版本发布历史·获取稳定版更新日志
小邓睡不饱耶6 小时前
实战|W餐饮平台智能化菜品推荐方案(含Spark实操+算法选型+完整流程)
python·ai·ai编程·ai写作
草莓熊Lotso6 小时前
Qt 主窗口核心组件实战:菜单栏、工具栏、状态栏、浮动窗口全攻略
运维·开发语言·人工智能·python·qt·ui
aiguangyuan6 小时前
基于BiLSTM-CRF的命名实体识别模型:原理剖析与实现详解
人工智能·python·nlp
禹凕6 小时前
Python编程——进阶知识(MYSQL引导入门)
开发语言·python·mysql
阿钱真强道6 小时前
13 JetLinks MQTT:网关设备与网关子设备 - 温控设备场景
python·网络协议·harmonyos
我的xiaodoujiao6 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 47--设置Selenium以无头模式运行代码
python·学习·selenium·测试工具·pytest
寻星探路12 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
ValhallaCoder15 小时前
hot100-二叉树I
数据结构·python·算法·二叉树