前阵子在整理数据分析练手项目时看到一份很有意思的甜品数据集里面反复出现马卡龙相关的配方和评分数据正好朋友推荐我做一个能锻炼爬虫、清洗和可视化能力的完整案例。于是就有了这个代号为“辉煌の马卡龙”的 Python 实战项目。本文会从零开始拆解整个流程包括模拟页面数据、爬虫解析、数据清洗、存储到最后的可视化分析完整代码都会贴在对应位置方便你跟着一起跑通。如果你是刚接触 Python 爬虫和数据清洗的读者这篇文章可以帮你理清一套规范的操作流程如果你已经有基础也可以直接跳到后面的清洗和可视化部分查看工程化的处理思路。我会尽量把每一步的“为什么”也讲清楚而不只是丢代码。1. 项目背景与需求拆解1.1 为什么选择“马卡龙”作为分析对象马卡龙是一款外观讨喜、配料结构相对标准的小甜点很适合作为数据分析的入门主题。一方面它的食谱变量比较清晰常见字段包括原材料、制作耗时、烤箱温度、热量、评分等另一方面不同配方之间的差异也够大能够让我们练习非数值字段的处理比如配料表清洗、文本长度统计、热量区间划分等。在“辉煌の马卡龙”项目中我不会直接去爬某个现成的在线食谱平台因为很多站点有严格的 robots 协议和反爬策略直接抓取容易遇到法律和合规风险。更稳妥的做法是先构造一个本地 HTML 页面作为演示数据源先跑通完整链路等掌握了思路再在遵守目标站点规则的前提下替换成真实的公开数据。1.2 项目功能拆解整个项目围绕马卡龙食谱数据的采集与分析展开主要包含四块功能采集层读取本地 HTML 页面使用 requests 和 BeautifulSoup 解析菜品卡片的字段。存储层把解析结果整理成结构化数据保存到 SQLite 和 CSV 文件。清洗层处理缺失值、单位统一、热量区间划分、文本归一化。分析层使用 pandas 做统计用 matplotlib 画分布图用词频分析查看高频配料。从学习角度来看这个项目覆盖了爬虫解析、数据表格处理、可视化和 SQLite 操作四个关键能力但又不会涉及过于复杂的分布式爬虫和消息队列对新手足够友好对进阶读者也保留了扩展空间。1.3 技术选型说明项目使用 Python 3.10 作为主语言核心依赖库如下requests用于请求 HTML 页面内容。尽管本文演示的是本地文件但把请求逻辑写成 requests 风格后后续换成线上 URL 也很方便。BeautifulSoup4解析 HTML 卡片结构。pandas清洗和统计分析。matplotlib绘制可视化图表。sqlite3Python 内置数据库驱动把清洗后的数据落库。这里有一点要提前说明使用的版本需要根据你的实际环境调整。比如 BeautifulSoup 在部分服务器上要求 4.9 以上版本才支持某些解析方法而 matplotlib 的中文显示在 Windows 和 Linux 下也有不同的字体配置。本文示例以常见环境为主重点演示处理思路。2. 环境准备与项目结构2.1 安装 Python 与依赖库建议先创建独立的虚拟环境避免污染系统 Python。下面是在命令行中的操作python -m venv macaron_env在 Windows 下激活虚拟环境macaron_env\Scripts\activate在 macOS 或 Linux 下激活虚拟环境source macaron_env/bin/activate接着安装依赖库pip install requests beautifulsoup4 lxml pandas matplotlib如果网络安装较慢可以使用国内镜像源例如pip install requests beautifulsoup4 lxml pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可以快速验证版本号python -c import pandas, bs4, requests; print(pandas.__version__, bs4.__version__, requests.__version__)正常情况下会输出三个版本号说明环境已经就绪。如果你的环境中已经存在老版本 pandas建议升级到较新的稳定版避免 API 差异导致代码报错。2.2 项目目录结构为了让代码思路更清晰我会把项目组织成以下结构macaron_project/ ├── data/ │ ├── macaron_page.html │ ├── macaron_raw.csv │ └── macaron_clean.db ├── src/ │ ├── spider.py │ ├── clean.py │ ├── analyze.py │ └── utils.py ├── output/ │ └── charts/ └── README.md其中data目录保存原始 HTML 和采集结果src目录放 Python 源码output目录保存可视化图表文件。这样分层的好处是数据、代码、输出产物互不干扰后续如果要扩展成真实爬虫也不需要改动主体结构。3. 构建演示页面马卡龙食谱卡片3.1 为什么要先构造本地 HTML直接在真实食谱网站上爬取数据首先需要面对反爬策略、页面结构变化和合规问题。对初学者来说写爬虫时最怕的不是解析代码复杂而是页面结构频繁变动导致无法复现。先用一个固定的本地 HTML 页面可以保证解析逻辑稳定运行方便排查每一步的问题。这个思路在真实工作中也很常见拿到接口或页面后先保存一份静态 HTML 快照然后离线调试解析代码等稳定后再挂到线上请求流程中。3.2 创建示例 HTML 页面在data目录下新建文件macaron_page.html内容如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 title马卡龙食谱列表/title /head body div classrecipe-list div classfood-card># src/spider.py from bs4 import BeautifulSoup class MacaronSpider: def __init__(self, html_content: str): 传入 HTML 字符串初始化 BeautifulSoup 对象。 self.soup BeautifulSoup(html_content, html.parser) def extract_recipes(self): 提取所有马卡龙食谱卡片返回列表[dict]。 cards self.soup.select(div.food-card) recipes [] for card in cards: name_node card.select_one(.name) author_node card.select_one(.author) time_node card.select_one(.time) calorie_node card.select_one(.calorie) score_node card.select_one(.score) ingredients_node card.select_one(.ingredients) recipe { name: name_node.get_text(stripTrue) if name_node else , author: author_node.get_text(stripTrue) if author_node else , time: time_node.get_text(stripTrue) if time_node else , calorie: calorie_node.get_text(stripTrue) if calorie_node else , score: score_node.get_text(stripTrue) if score_node else , ingredients: ingredients_node.get_text(stripTrue) if ingredients_node else , } recipes.append(recipe) return recipes这里的select方法接收 CSS 选择器select_one返回第一个匹配节点。使用get_text(stripTrue)可以去掉文本两端的空白和换行避免导入 Excel 后出现多余空格。4.2 读取本地 HTML 并采集数据接下来写一个入口脚本读取macaron_page.html中的内容调用爬虫类并把结果保存为 CSV。# src/run_spider.py import csv from pathlib import Path from spider import MacaronSpider BASE_DIR Path(__file__).resolve().parent.parent HTML_PATH BASE_DIR / data / macaron_page.html CSV_PATH BASE_DIR / data / macaron_raw.csv def load_html(path: Path) - str: 读取 HTML 文件内容。 with open(path, r, encodingutf-8) as f: return f.read() def save_to_csv(recipes: list, output_path: Path) - None: 将食谱字典列表写入 CSV 文件。 if not recipes: print(没有采集到任何数据) return fieldnames list(recipes[0].keys()) # 指定 encoding 和 newline 参数避免 CSV 出现空行 with open(output_path, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(recipes) print(f已保存 {len(recipes)} 条数据到 {output_path}) if __name__ __main__: html_text load_html(HTML_PATH) spider MacaronSpider(html_text) result spider.extract_recipes() for r in result: print(r) save_to_csv(result, CSV_PATH)使用utf-8-sig编码写入 CSV是考虑到 Windows 系统下 Excel 打开 UTF-8 文本时容易出现中文乱码。加上 BOM 后Excel 可以直接识别。4.3 运行与验证在项目根目录执行cd src python run_spider.py预期输出类似{name: 经典法式马卡龙, author: 烘焙师小A, time: 120分钟, calorie: 480千卡, score: 9.2分, ingredients: 杏仁粉、糖粉、蛋白、细砂糖、奶油、草莓果酱} {name: 抹茶马卡龙, author: 烘焙师小B, time: 150分钟, calorie: 455千卡, score: 8.8分, ingredients: 杏仁粉、糖粉、蛋白、抹茶粉、白巧克力} ...同时data/macaron_raw.csv会生成对应的表格文件。这里需要留意虽然我的示例代码中把run_spider.py放在了src目录下但如果你选择把脚本放在项目根目录就要调整BASE_DIR的路径计算方式。最简单的办法是脚本所在文件和要读取的文件使用相同的相对关系不要依赖“当前工作目录恰好是项目根目录”这个假设。5. 数据清洗与存储5.1 数据清洗的目标原始的time、calorie、score字段都是带单位的字符串例如120分钟、480千卡、9.2分。直接分析会遇到两个问题字符串无法参与数值运算。不同的单位写法会影响统计结果。所以清洗层要做三件事把时间、热量、评分转换成数值类型。对热量做区间划分比如低热量、中等热量、高热量。对配料字段进行标准化拆分为词频统计做准备。5.2 编写清洗脚本在src/clean.py中实现清洗函数。这里使用 pandas 完成大部分操作# src/clean.py import re import sqlite3 from pathlib import Path import pandas as pd BASE_DIR Path(__file__).resolve().parent.parent RAW_CSV BASE_DIR / data / macaron_raw.csv DB_PATH BASE_DIR / data / macaron_clean.db def normalize_time(value: str) - int: 把 120分钟 转换为 120。 if pd.isna(value): return 0 match re.search(r(\d), str(value)) return int(match.group(1)) if match else 0 def normalize_calorie(value: str) - int: 把 480千卡 转换为 480。 if pd.isna(value): return 0 match re.search(r(\d), str(value)) return int(match.group(1)) if match else 0 def normalize_score(value: str) - float: 把 9.2分 转换为 9.2。 if pd.isna(value): return 0.0 match re.search(r(\d(?:\.\d)?), str(value)) return float(match.group(1)) if match else 0.0 def parse_ingredients(value: str) - list: 将配料字符串切分成配料列表。 if pd.isna(value): return [] return [item.strip() for item in str(value).split(、) if item.strip()] def clean_data(df: pd.DataFrame) - pd.DataFrame: 对原始 DataFrame 做字段清洗和派生字段生成。 df df.copy() df[time_num] df[time].apply(normalize_time) df[calorie_num] df[calorie].apply(normalize_calorie) df[score_num] df[score].apply(normalize_score) df[ingredient_list] df[ingredients].apply(parse_ingredients) # 热量区间 bins [0, 450, 500, float(inf)] labels [低热量, 中等热量, 高热量] df[calorie_level] pd.cut(df[calorie_num], binsbins, labelslabels) return df def write_to_sqlite(df: pd.DataFrame, db_path: Path) - None: 把清洗后的数据写入 SQLite 表。 conn sqlite3.connect(str(db_path)) df.to_sql(macaron, conn, if_existsreplace, indexFalse) conn.close() print(f数据已写入 {db_path}) if __name__ __main__: raw_df pd.read_csv(RAW_CSV) clean_df clean_data(raw_df) print(clean_df[[name, time_num, calorie_num, score_num, calorie_level]]) write_to_sqlite(clean_df, DB_PATH)其中pd.cut是 pandas 内置的分箱函数非常适用于热量区间这类离散化操作。这里我把热量小于等于 450 千卡划为低热量450 到 500 千卡为中等热量500 以上为高热量。这个阈值的设定取决于业务判断你可以根据自己的分析目标调整。5.3 为什么要使用 SQLiteSQLite 是 Python 内置支持的轻量级数据库不需要单独启动数据库服务。对于马卡龙食谱这种体量的数据SQLite 可以很方便地完成条件查询、排序、聚合等操作。把清洗后的数据落库也是为了后续能直接用 SQL 做进一步验证。例如查询所有评分高于 9 分的马卡龙SELECT name, score_num, calorie_num FROM macaron WHERE score_num 9 ORDER BY score_num DESC;这比直接操作 CSV 更接近真实项目的使用方式。6. 数据分析与可视化6.1 统计概览信息在src/analyze.py中先从 SQLite 读取清洗后的数据然后做统计。为了让输出更直观我会计算几个核心指标马卡龙总数。平均热量、最高热量、最低热量。平均评分、最高评分。各热量区间数量。# src/analyze.py import sqlite3 from pathlib import Path import matplotlib.pyplot as plt import pandas as pd BASE_DIR Path(__file__).resolve().parent.parent DB_PATH BASE_DIR / data / macaron_clean.db CHART_DIR BASE_DIR / output / charts def load_clean_data(db_path: Path) - pd.DataFrame: conn sqlite3.connect(str(db_path)) df pd.read_sql(SELECT * FROM macaron, conn) conn.close() return df def summary_stats(df: pd.DataFrame) - pd.DataFrame: numeric_cols [time_num, calorie_num, score_num] return df[numeric_cols].describe().T if __name__ __main__: df load_clean_data(DB_PATH) print(马卡龙数据概览) print(df[[name, calorie_level, score_num]].head()) print(\n数值统计) print(summary_stats(df)) CHART_DIR.mkdir(parentsTrue, exist_okTrue) # 中文字体设置不同环境需要调整 plt.rcParams[font.sans-serif] [SimHei, PingFang SC, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False # 评分分布直方图 plt.figure(figsize(8, 5)) plt.hist(df[score_num], bins10, edgecolorblack) plt.title(马卡龙评分分布) plt.xlabel(评分) plt.ylabel(数量) plt.savefig(CHART_DIR / score_distribution.png, dpi150) plt.close() print(f图表已保存到 {CHART_DIR})这里需要特别注意中文字体问题。matplotlib 默认字体不包含中文字符如果不设置字体图片中的中文会显示为方块。上面代码里设置了SimHei、PingFang SC和Microsoft YaHei三个候选读者需要根据自己的系统环境调整。6.2 配料词频分析马卡龙的配料虽然看起来很多但核心基础配料往往集中在杏仁粉、糖粉、蛋白这几类。我们可以把所有马卡龙的配料拼接起来统计出现次数最多的配料。from collections import Counter def ingredient_frequency(df: pd.DataFrame) - pd.DataFrame: counter Counter() for ing_list in df[ingredient_list]: if isinstance(ing_list, list): for ing in ing_list: counter[ing] 1 return pd.DataFrame( counter.most_common(), columns[ingredient, count] ) freq_df ingredient_frequency(df) print(freq_df.head(10))预期输出可能是杏仁粉、糖粉、蛋白出现次数最多。这符合马卡龙的基础配方结构。实际项目中你还可以联合百度指数或销售数据做“配料热度”分析但这就属于业务层面的扩展了。6.3 制作耗时与评分的关系我期望看到耗时更长、制作更精细的马卡龙评分更高。可以使用散点图来观察趋势plt.figure(figsize(8, 5)) plt.scatter(df[time_num], df[score_num], alpha0.7) plt.title(制作耗时与评分关系) plt.xlabel(制作耗时(分钟)) plt.ylabel(评分) plt.savefig(CHART_DIR / time_score_scatter.png, dpi150) plt.close()不过在样本量很少的情况下散点图不一定能展示出明显规律。这里更重要的不是结论而是掌握分析方法。当后续替换为更大规模的真实数据时同样的代码可以直接复用。7. 常见问题与排查思路实际运行项目时比较容易踩到下面几个坑我整理成了表格问题现象常见原因解决思路爬虫结果为空HTML 类名或选择器不对先打印页面内容确认.food-card是否存在CSV 中文乱码编码没有写成 UTF-8-SIG写入文件时使用encodingutf-8-sigpandas 读取列名不一致CSV 表头与代码字段不匹配使用pd.read_csv后打印columns检查matplotlib 中文乱码系统没有对应中文字体指定本机存在的字体名称或下载中文字体to_sql 报错表名与已有表冲突使用if_existsreplace或先删除旧表本地 HTML 不显示浏览器解析与爬虫解析不同使用 BeautifulSoup 直接读取文件不要依赖浏览器渲染时间/热量字段提取到 0正则没有匹配到数字先检查字符串原始格式再调整正则表达式如果你遇到爬虫拿到空列表最快的排查方法是把 HTML 内容输出到控制台并用soup.prettify()查看解析后的文档树确认结构是否和预期一致。很多时候不是代码写错了而是选择器没有匹配到节点。再补充一个比较隐藏的问题使用pd.read_csv读取包含中文的 CSV 时如果encoding参数写错会报UnicodeDecodeError。建议优先使用utf-8-sig不要使用单纯的utf-8否则在 Windows 平台容易被 BOM 干扰。8. 工程化建议与扩展方向8.1 适度封装工具函数当前代码把爬虫、清洗、分析分别放在不同文件中已经具备模块化的雏形。随着业务变复杂可以在utils.py中统一封装文件路径处理和数据库连接函数避免多个脚本重复写路径拼接逻辑。例如# src/utils.py from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent def get_data_path(filename: str) - Path: return BASE_DIR / data / filename def get_chart_path(filename: str) - Path: return BASE_DIR / output / charts / filename这样做的好处是当项目文件被复制或部署到其他目录时路径不会因为启动位置不同而失效。8.2 数据采集的合规性如果后续想换成真实在线数据一定要先阅读目标网站的robots.txt和用户协议。采集频率要控制在合理范围不要对目标服务器造成压力。更推荐的做法是寻找官方提供的公开 API或者在获得授权的前提下使用结构化数据源。同时采集到的数据不要用于商业用途除非你确认数据源允许这样做。本文的演示 HTML 是本地构造的不存在这些风险但还是建议你从一开始就养成合规数据采集的习惯。8.3 扩展方向这个项目可以扩展的方向很多加入更多食谱字段比如烤箱温度、制作难度、用户评论让分析维度更丰富。使用数据库查询替代 pandas当数据量变大后可以把聚合逻辑下推到 SQLite减少内存占用。接入定时任务用 cron 或 Windows 计划任务定时采集数据形成长期趋势分析。做成 Web 应用把清洗和分析逻辑封装成 FastAPI 接口前端通过图表库展示数据。增加数据质量测试使用pytest写断言比如热量字段不能为负数评分范围应该在 0-10 之间。对刚开始学习 Python 数据分析的读者来说建议按顺序完成“数据采集 → 清洗 → 存储 → 分析 → 可视化”这条链路不要只停留在某个库的语法练习上。项目完整度比单点知识点更重要这也是我坚持用“辉煌の马卡龙”这种完整案例来组织内容的原因。代码和思路已经全部贴在上面了你可以直接复制到本地跑一遍。如果卡在某个环节优先检查路径是否对应、依赖是否安装完整、编码是否正确这三个坑覆盖了绝大多数入门项目的问题。希望这份教程对你有所帮助也欢迎在实际运行后结合自己的数据做进一步调整。