1. 项目缘起与目标为什么选择薄荷健康网作为数据源最近在做一个关于饮食健康管理的个人项目需要一份结构化的食物热量数据库。市面上的商业API要么收费不菲要么数据不够全面。在对比了几个主流健康类网站后我最终把目标锁定在了薄荷健康网。原因很简单它的食物库非常庞大分类细致从主食、蔬菜到零食、调味品一应俱全而且每项食物的热量、蛋白质、脂肪、碳水化合物等营养数据标注得相对规范、清晰。对于需要本地化、定制化数据的研究或应用来说如果能把这些数据爬取下来无疑是一个高质量的起点。我的核心目标很明确完整地获取薄荷健康网食物数据库中的食物分类、食物名称、食物热量千卡以及指向该食物详情页的链接。这四类数据构成了一个食物热量查询系统最基础的骨架。有了分类和名称可以方便检索和归类有了热量数据可以直接用于计算有了详情页链接则为后续可能需要的、更详细的营养信息如蛋白质、脂肪含量爬取预留了入口。这个任务看似是典型的静态网页爬虫但实际操作中会遇到一些有趣的挑战比如网站的反爬机制、动态加载内容的处理以及如何高效、稳定地组织爬取逻辑。接下来我就把这次爬取薄荷健康网数据的完整过程、踩过的坑和总结的经验毫无保留地分享出来。2. 环境准备与工具选型为什么是Requests BeautifulSoup工欲善其事必先利其器。对于网页爬虫Python生态提供了丰富的库选择哪一套组合往往决定了后续开发的效率和难度。2.1 核心库Requests 与 BeautifulSoup4我选择了最经典、也最受初学者欢迎的“Requests BeautifulSoup”组合。很多人可能会问现在不是有很多更“高级”的框架吗比如Scrapy、Selenium、Playwright为什么不用它们这里就涉及到技术选型的核心逻辑用最简单的工具解决核心问题。首先我对薄荷健康网的初步分析通过浏览器查看网页源代码发现它的食物列表页和分类页面的主要内容都是直接渲染在HTML中的没有通过复杂的JavaScript动态加载。这意味着我只需要发送HTTP请求获取到原始的HTML文档然后解析其中固定的标签结构即可。Requests库正是发送HTTP请求的利器它简单、直观、高效。而BeautifulSoup则是解析HTML/XML的“瑞士军刀”它支持多种解析器能让我们用类似查找字典的方式通过标签名、CSS类名等属性轻松定位到所需数据。如果页面数据是动态加载的比如滚动到底部才加载更多那我可能会考虑Selenium或Playwright来模拟浏览器行为。但在这个项目里引入它们无异于“大炮打蚊子”不仅增加了环境配置的复杂性需要安装浏览器驱动执行速度也会慢很多。因此对于静态或半静态页面Requests BeautifulSoup是性价比最高的选择。2.2 辅助库lxml, pandas, timelxml 这是BeautifulSoup可以选用的一个解析器。相比Python内置的html.parserlxml的解析速度更快容错能力也更强。在需要处理大量页面时这个优势会很明显。pandas 并非爬虫必需但它是数据处理和保存的神器。我们可以把爬取到的数据临时存放在列表或字典里最后用pandas的DataFrame整理并一键导出为结构清晰的CSV或Excel文件方便后续分析。time 用于在请求之间添加延时。这是应对网站反爬机制最基本、也是最有效的手段之一。毫无节制地高速请求很容易触发服务器的保护机制返回429 Too Many Requests错误。2.3 环境搭建步骤安装这些库非常简单一行命令搞定建议在虚拟环境中进行pip install requests beautifulsoup4 lxml pandas如果安装lxml遇到问题特别是在Windows上可能需要先安装一些C库依赖或者直接使用预编译的wheel文件。不过大多数情况下上述命令都能顺利执行。3. 网站结构分析与爬取策略设计动手写代码之前花时间分析网站结构是至关重要的一步这能帮你理清爬取路径避免写到一半发现逻辑走不通。3.1 入口页面与分类导航打开薄荷健康网的食物库页面可以看到左侧有非常清晰的分类导航树比如“主食谷物”、“肉蛋奶”、“蔬菜菌藻”等。每个大类下又有子类。我们的第一个任务就是获取这个完整的分类树以及每个最末级分类对应的列表页链接。通过浏览器的开发者工具F12查看网络请求我发现这些分类数据很可能是在页面加载时通过一个API接口获取的JSON数据然后前端再渲染成树形菜单。直接去解析渲染后的HTML菜单结构会比较复杂因为涉及多层嵌套的ul和li标签。更高效的方法是直接找到那个提供分类数据的API接口。在开发者工具的“Network”标签页中筛选XHR或Fetch请求刷新页面很容易就能找到一个返回JSON数据的请求其响应内容正是完整的食物分类信息包括分类ID、名称、层级关系等。记下这个请求的URL、请求方法通常是GET和必要的请求头Headers。3.2 食物列表页分析当我们点击某个具体分类例如“苹果”会跳转到一个食物列表页。这个页面通常以分页形式展示该分类下的所有食物。同样我们需要分析它的URL规律。是像/food/list?category_id123page1这样的形式还是其他模式通过多翻几页观察地址栏的变化就能总结出规律。更重要的是分析列表页的HTML结构。我们需要找到每个食物条目对应的HTML代码块。通常每个食物会用一个div或li包裹。在这个代码块里我们需要提取食物名称 一般在a标签的文本或title属性里。食物详情链接 就是上述a标签的href属性。注意可能是相对路径需要拼接上网站域名。食物热量 这个有点 tricky。热量信息有时直接显示在列表页有时只在详情页才有。需要仔细查看。如果在列表页有它通常在一个特定的span或em标签里可以通过其class名或附近的文本来定位。3.3 详情页数据补全可选如果列表页没有热量信息或者我们想获取更全面的营养数据蛋白质、脂肪等就需要进入每个食物的详情页。详情页的URL就是我们上一步爬取到的链接。在详情页里数据通常以更规整的表格形式呈现解析起来反而更简单。但代价是请求量会成倍增加必须更加注意爬取速度和礼貌性。3.4 整体爬取流程设计基于以上分析我设计了如下爬取流程这是一个经典的“广度优先”策略获取分类 模拟请求分类API解析JSON得到所有未级分类的ID和名称。遍历分类 对于每一个未级分类构造其食物列表页的URL。遍历列表页 对于该分类循环请求每一页列表页直到没有更多页面为止。解析列表项 在每一页中解析出所有食物条目的名称、链接和热量如果存在。补全详情如需 如果列表页没有热量则根据食物链接发起请求进入详情页解析热量数据。存储数据 将每一条记录分类、名称、热量、链接保存到列表或字典中。礼貌等待 在每一个请求之后随机休眠一段时间例如1-3秒模拟人类操作避免被封IP。4. 核心代码实现与关键细节剖析理论分析完毕现在进入实战编码环节。我会分模块讲解代码并解释每个关键步骤背后的考量。4.1 获取食物分类信息首先我们解决分类问题。假设我们通过分析找到了分类API的URL为https://www.boohee.com/food/group。import requests import json def get_food_categories(): 获取薄荷健康网的食物分类信息。 返回一个列表每个元素是一个字典包含分类id、名称、及其对应的列表页基础URL。 url https://www.boohee.com/food/group headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 检查请求是否成功 # 假设接口返回的是JSON data response.json() categories [] # 这里需要根据实际返回的JSON结构来解析 # 假设数据结构是 data[groups] 包含所有大组每个大组下有 categories 列表 for group in data.get(groups, []): for cat in group.get(categories, []): # 提取我们需要的信息例如分类ID和名称 cat_id cat.get(id) cat_name cat.get(name) if cat_id and cat_name: # 构造该分类第一页的列表URL需要根据网站实际规则来 list_url fhttps://www.boohee.com/food/group/{cat_id}?page1 categories.append({ id: cat_id, name: cat_name, list_url_template: list_url # 存储模板页码后续替换 }) print(f成功获取 {len(categories)} 个食物分类。) return categories except requests.exceptions.RequestException as e: print(f获取分类失败: {e}) return [] except json.JSONDecodeError as e: print(f解析分类JSON失败: {e}) return [] # 注意以上URL和JSON结构是假设的实际需要根据网站分析结果调整。关键点解析User-Agent 设置一个常见的浏览器UA是最基础的伪装避免被服务器直接拒绝。异常处理 网络请求充满不确定性必须用try...except包裹并对请求状态response.raise_for_status()和JSON解析可能出现的异常进行处理。URL构造 爬虫代码的很大一部分工作就是在拼接正确的URL。这里我们根据分类ID构造了列表页的URL模板页码留作参数。4.2 解析单个食物列表页接下来我们编写解析列表页的函数。这里就需要用到BeautifulSoup了。from bs4 import BeautifulSoup import time import random def parse_food_list_page(html_content, category_name): 解析一个食物列表页的HTML内容提取食物信息。 返回一个列表每个元素是一个食物字典。 soup BeautifulSoup(html_content, lxml) food_items [] # 关键步骤找到包裹每个食物项的HTML元素。 # 需要手动分析网页来确定。假设每个食物项在一个 classfood-item 的 div 里。 item_elements soup.find_all(div, class_food-item) if not item_elements: # 如果没找到可能是class名不对或者页面结构变了打印提示以便调试 print(f警告在分类 {category_name} 的页面中未找到食物项可能选择器需要更新。) # 可以尝试打印一部分HTML看看结构 # print(soup.prettify()[:1000]) for item in item_elements: food_info {} food_info[category] category_name # 提取食物名称和链接 name_link_elem item.find(a, class_food-name) # 假设食物名称在classfood-name的a标签里 if name_link_elem: food_info[name] name_link_elem.get_text(stripTrue) relative_link name_link_elem.get(href) # 将相对链接补全为绝对链接 if relative_link and not relative_link.startswith(http): food_info[link] https://www.boohee.com relative_link else: food_info[link] relative_link else: food_info[name] 未知 food_info[link] # 提取热量 - 假设热量在一个 classheat 的 span 里 heat_elem item.find(span, class_heat) if heat_elem: heat_text heat_elem.get_text(stripTrue) # 通常格式是“热量xxx千卡”需要提取数字 # 这里用一个简单的方法查找字符串中的数字 import re numbers re.findall(r\d, heat_text) food_info[heat_kcal] int(numbers[0]) if numbers else None else: food_info[heat_kcal] None # 标记为None可能需要后续去详情页获取 # 只有当成功提取到名称时才加入列表 if food_info.get(name) and food_info[name] ! 未知: food_items.append(food_info) return food_items关键点解析选择器的确定find_all(div, class_food-item)这行代码是核心。food-item这个类名必须通过查看网页源代码来确定。右键点击网页上的一个食物项选择“检查”在开发者工具里找到对应的HTML元素观察它最外层的class是什么。这个选择器如果不准就抓不到数据。文本清理get_text(stripTrue)可以获取标签内的文本并去除首尾空白字符。链接补全 爬取的href属性很可能是相对路径如/food/view/123需要手动拼接上网站根域名才能构成有效的完整URL。数据清洗 热量信息往往是“热量53千卡”这样的文本我们需要用正则表达式re.findall来提取其中的数字部分。这里处理得比较简单实际情况可能更复杂比如含有范围值“53-60千卡”需要根据业务逻辑决定如何存储。容错处理 加了if not item_elements的判断如果找不到食物项会打印警告。在实际爬虫运行中这能帮你快速定位是网站结构变了还是你的选择器写错了。4.3 整合爬取流程与应对反爬现在我们把分类获取、列表页遍历、详情页补全如果需要和存储整合起来。import pandas as pd def crawl_boohee_food(): all_foods [] # 1. 获取所有分类 categories get_food_categories() if not categories: print(无法获取分类爬虫终止。) return # 2. 遍历每个分类 for idx, cat in enumerate(categories): print(f正在爬取分类 [{idx1}/{len(categories)}]: {cat[name]}) page_num 1 has_next_page True # 3. 遍历该分类下的所有列表页 while has_next_page: # 构造当前页的URL list_url cat[list_url_template].replace(page1, fpage{page_num}) # 发送请求带上请求头 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Referer: https://www.boohee.com/ # 有时需要Referer } try: response requests.get(list_url, headersheaders, timeout15) # 重点检查状态码特别是429 if response.status_code 429: print(f触发429限制等待更长时间... 当前URL: {list_url}) time.sleep(30) # 等待30秒再试 continue # 重新尝试当前页 response.raise_for_status() # 4. 解析当前列表页 foods_in_page parse_food_list_page(response.text, cat[name]) all_foods.extend(foods_in_page) print(f 第{page_num}页获取到{len(foods_in_page)}条食物。) # 5. 判断是否有下一页 # 方法解析页面中“下一页”按钮是否被禁用。例如查找 classdisabled 的下一页链接 soup BeautifulSoup(response.text, lxml) next_button soup.find(a, text下一页) # 根据实际文本找 if not next_button or disabled in next_button.get(class, []): has_next_page False else: page_num 1 has_next_page True except requests.exceptions.RequestException as e: print(f请求列表页失败: {list_url}, 错误: {e}) has_next_page False # 发生错误跳出当前分类的循环 break # 6. 礼貌延时避免请求过快 sleep_time random.uniform(1.5, 3.5) # 随机等待1.5到3.5秒 time.sleep(sleep_time) # 完成一个分类后可以稍作休息 time.sleep(random.uniform(2, 5)) # 7. 数据存储 if all_foods: df pd.DataFrame(all_foods) # 可以选择需要的列 df df[[category, name, heat_kcal, link]] output_file boohee_foods.csv df.to_csv(output_file, indexFalse, encodingutf-8-sig) # utf-8-sig 避免Excel打开乱码 print(f爬取完成共获取 {len(all_foods)} 条食物数据已保存至 {output_file}) else: print(未爬取到任何数据。) if __name__ __main__: crawl_boohee_food()关键点解析反爬应对延时与随机性time.sleep(random.uniform(1.5, 3.5))是核心策略。固定的延时如time.sleep(2)容易被识别为机器人。加入随机性后更贴近人类不规律的点击行为。在完成一个分类后增加一次较长休息也是为了降低请求频率。反爬应对状态码429429 Too Many Requests是服务器明确告诉你“请求太快了”。代码中专门检查了这个状态码一旦遇到就等待更长时间30秒再重试当前请求。这是必须处理的异常否则爬虫会卡死或丢失数据。翻页逻辑 判断“是否有下一页”是爬虫的经典问题。这里展示了两种常见思路一是查找“下一页”按钮并检查其是否带有disabled类表示不可点击二是更通用的方法如果当前页解析出的食物数量为0或者请求下一页返回404/空页面也可以认为没有下一页了。需要根据网站的具体实现来调整。数据存储 使用pandas的DataFrame非常方便。to_csv方法可以轻松导出为CSV。encodingutf-8-sig这个参数很重要它会在文件开头添加BOM标记让Windows系统的Excel正确识别UTF-8编码避免中文乱码。5. 进阶问题与优化方案上面的代码是一个基础但可用的框架。在实际运行中你几乎肯定会遇到下面这些问题。这里分享我的应对经验。5.1 处理动态加载内容如果食物列表是滚动加载的AJAX我们通过Requests获取的初始HTML里就不会包含全部食物项。这时需要分析滚动时浏览器发送了哪些额外的网络请求。通常是一个返回JSON数据的API里面包含了后续的食物列表。解决方法是在开发者工具的Network中找到这个API请求。模仿这个请求构造URL和参数可能包括分类ID、页码、或一个last_id之类的游标。修改爬虫直接请求这个API接口解析返回的JSON数据而不是解析HTML。这通常比用Selenium模拟滚动要高效和稳定得多。5.2 应对IP封锁与验证码如果请求过于频繁即使加了延时也可能遭遇IP暂时封锁或弹出验证码。代理IP池 这是最有效的解决方案之一。可以使用一些免费的或付费的代理IP服务在代码中随机切换IP地址。Requests库使用代理很简单proxies {http: http://your-proxy:port, https: https://your-proxy:port}然后在requests.get()中传入proxies参数即可。请求头精细化 除了User-Agent还可以设置Referer来源页、Accept-Language等让请求看起来更像来自真实浏览器。Session保持 使用requests.Session()对象它可以自动管理cookies在某些需要登录或保持会话的网站上很有用。验证码处理 如果遇到简单的图形验证码可以考虑接入打码平台。如果是复杂的滑块、点选验证码通常意味着网站防护等级很高可能需要重新评估爬取的可行性和必要性。5.3 异步爬取提升效率对于大量页面同步请求一个接一个会非常慢。可以使用aiohttpasyncio库进行异步爬取并发地发送数十上百个请求极大提升速度。但异步编程复杂度较高且对目标网站的压力巨大更容易触发反爬需要非常小心地控制并发数和添加延时。5.4 健壮性与错误恢复一个要长时间运行的爬虫必须考虑健壮性。断点续爬 可以将已成功爬取的分类ID或食物ID记录到一个文件中如JSON或数据库。每次启动时先加载这个记录跳过已经处理过的部分。这样即使程序中途崩溃或手动停止下次也能从中断处继续。更细致的异常处理 对每一个网络请求、每一步数据解析都进行try...except包裹记录错误日志而不是让整个程序崩溃。数据去重 在存储前检查食物名称或链接是否已经存在避免重复数据。6. 数据清洗与后续应用建议爬取下来的原始数据往往比较“脏”直接使用可能有问题。6.1 常见的数据清洗工作缺失值处理 对于heat_kcal为None的数据如果数量不多可以手动去详情页补抓如果数量大可以考虑用同类食物的平均值填充或者直接标记为缺失。单位统一 确保所有热量的单位都是“千卡”。有些网站可能用“大卡”或“Kcal”本质上是一样的但文本需要统一。去除重复项 同一个食物可能出现在多个分类下例如“土豆”既在“蔬菜”类也在“主食”类。根据你的应用场景决定是保留所有记录还是根据食物名称或ID去重。字符串清理 食物名称前后可能有空格、换行符等用str.strip()清理。有些名称可能包含特殊字符或表情需要根据情况处理或过滤。6.2 数据存储与使用CSV/Excel 适合中小规模数据几万条以内方便用Excel或文本编辑器查看。SQLite/MySQL 适合大规模数据或需要复杂查询的场景。你可以建立categories和foods两张表通过外键关联数据结构更清晰。应用方向 有了这份数据你可以构建一个本地的命令行或桌面端食物热量查询工具。结合用户输入的食谱计算一餐的总热量。做数据分析比如找出热量最高的零食类别或者分析不同类别食物的平均热量分布。作为机器学习项目的训练数据比如预测食物的营养类别。爬虫项目最迷人的地方在于它连接了网络世界的海量数据和本地计算能力。从分析目标、设计策略到编写代码、应对反爬最后清洗数据、付诸应用整个过程就像一次数字世界的“采矿与冶炼”。薄荷健康网的食物数据爬取只是一个起点掌握了这套方法你就能举一反三去获取其他对你有价值的公开信息。记住爬虫的伦理和法律边界同样重要务必遵守网站的robots.txt协议控制爬取速度不要对目标网站造成负担。