ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python爬虫实战:从薄荷健康网构建结构化食物营养数据库

Python爬虫实战:从薄荷健康网构建结构化食物营养数据库 1. 项目概述为什么我们要爬取薄荷健康网作为一个经常和数据打交道的开发者我最近在做一个与健康饮食相关的数据分析项目需要一份结构化的食物营养数据库。市面上的商业数据库要么价格昂贵要么数据不够全面。这时我自然想到了“薄荷健康”这个在国内健康管理领域知名度极高的平台。它积累了海量的食物营养数据并且分类清晰信息相对权威。这个项目的核心目标就是通过Python爬虫技术自动化地从薄荷健康网获取其食物库数据。具体来说我们需要拿到四个维度的信息食物分类比如“谷薯芋、杂豆、主食”、食物名称比如“米饭蒸”、食物热量单位通常是千卡/100克以及最关键的食物详情页链接。有了链接未来就可以进一步扩展爬取蛋白质、脂肪、碳水化合物等更详细的营养信息。这听起来像是一个典型的静态网页爬取任务但实际操作起来你会发现它融合了多种常见爬虫技术点如何应对网站的反爬策略如请求头校验、如何解析复杂的页面结构特别是涉及JavaScript动态加载的情况、如何设计稳健的数据存储方案以及如何处理爬取过程中的各种异常。对于刚入门爬虫的朋友来说这是一个绝佳的综合性练手项目对于有经验的开发者也能在其中找到优化策略和架构设计的挑战。接下来我就把自己从分析到实现的完整过程以及踩过的坑和总结的技巧毫无保留地分享出来。2. 核心思路与网站结构分析在动手写代码之前花时间仔细分析目标网站的结构和行为往往能事半功倍避免后期大量返工。我的分析流程通常是手动浏览 - 开发者工具探查 - 请求分析。2.1 目标数据定位与页面逻辑梳理首先我手动访问了薄荷健康网的食物库页面。通常这类数据会有一个“食物百科”或“热量查询”的入口。通过浏览发现数据呈现是典型的分页列表形式左侧有食物分类的树形导航栏右侧是具体的食物列表列表项包含食物名称、热量和指向详情页的链接。这里第一个关键点出现了数据是静态加载还是动态加载我打开浏览器的开发者工具F12切换到“网络(Network)”选项卡然后刷新页面并特别关注“XHR”或“Fetch”类型的请求。很快我发现了一个规律当点击不同食物分类或者翻页时页面URL并没有变化依然是同一个地址但右侧的食物列表内容更新了。同时网络请求中出现了新的、携带了特定参数如分类ID、页码的请求其响应是JSON格式的数据。这明确告诉我薄荷健康网的食物列表数据是通过Ajax动态加载的。这是一个非常重要的结论。它意味着我们不能简单地用requests库请求列表页的HTML然后用BeautifulSoup解析因为初始HTML里不包含食物数据。我们必须找到那个真正的数据接口API模拟浏览器发送Ajax请求直接从接口获取结构化的JSON数据这通常更高效、更稳定。2.2 关键接口与参数逆向工程接下来我的目标就是找到并理解这个数据接口。在开发者工具的“网络”选项卡中我筛选出“XHR”请求然后逐个点击左侧分类观察哪个请求的响应里包含了食物列表数据。很快我锁定了一个形如https://www.boohee.com/food/search?keywordpage1food_type1的请求。对接口URL和参数进行拆解分析基础URL:https://www.boohee.com/food/search。这是数据查询的接口地址。关键参数:keyword: 搜索关键词留空表示获取该分类下所有食物。page: 页码用于分页加载。food_type:这是核心参数代表食物分类的ID。当我点击“谷薯芋”分类时food_type1点击“蔬菜类”时food_type2。这个映射关系是我们爬取所有分类数据的关键。那么如何获取所有food_type的列表呢我回到最初的页面查看左侧分类导航栏的HTML源码。发现每个分类a标签的href属性或onclick事件里通常就包含了food_type的信息。例如a hrefjavascript:; onclicksearchFood(1)谷薯芋、杂豆、主食/a。这样我就可以通过解析这个导航栏的HTML提取出所有分类的名称和对应的food_typeID构建一个分类字典。注意有些网站的分类ID可能隐藏在更深的JavaScript逻辑或初始化的全局变量里需要仔细查找。薄荷健康网的这个方式算是比较直观的。2.3 反爬策略初步评估与应对计划在分析请求时我也留意了网站可能存在的反爬措施请求头校验这是最基本也是最常见的。查看上述数据接口的请求头会发现它包含了完整的User-Agent、Referer有时还会有Cookie。我们的爬虫必须模拟这些头部信息否则服务器可能直接返回错误或空数据。频率限制虽然没有明确证据但作为一个公开网站对同一IP的频繁请求进行限制是常规操作。我们需要在代码中引入随机延时避免请求过快。参数签名或Token更复杂的网站可能会对请求参数进行加密或者要求携带一个有时效性的Token。我检查了薄荷健康网的请求参数目前看来是明文的没有复杂的签名机制。但这并不意味着未来不会增加爬虫代码需要有一定的容错和适应性。基于以上分析我的爬虫技术路线图已经清晰首先请求食物库主页解析出所有食物分类及ID然后遍历每个分类ID构造API请求URL循环请求每一页数据最后从JSON响应中解析出我们需要的数据字段并存储起来。3. 技术选型与环境准备工欲善其事必先利其器。选择一套合适、高效的工具链能让开发过程更顺畅。3.1 核心工具库选择与理由请求库Requests为什么选它Requests是Python生态中事实标准的HTTP库其API设计极其人性化功能强大且稳定。对于处理我们遇到的这种携带简单头部、参数明确的GET请求它是绝佳选择。相比于原生urllib它的代码更简洁直观。替代方案考量aiohttp适用于高性能异步爬虫但本项目数据量几十个分类每分类最多几十页和复杂度尚未达到需要异步并发的程度引入异步反而增加复杂度。Selenium或Playwright用于处理复杂JavaScript渲染但我们已经找到了数据接口无需动用这些“重型武器”。解析库BeautifulSoup4 和 lxml为什么选它们我们需要解析两个部分一是初始页面的HTML用于提取分类导航二是API返回的JSON数据。对于HTML解析BeautifulSoup配合lxml解析器是黄金组合。BeautifulSoup提供了灵活的find、select等方法像写CSS选择器一样定位元素学习成本低。lxml解析器速度非常快远优于Python内置的html.parser。JSON解析Python内置的json库就足够了直接将响应文本response.json()即可转为Python字典操作方便。数据存储CSV 文件为什么选CSV本项目目标数据是结构化的表格型数据分类、名称、热量、链接CSV格式简单、通用可以用Excel直接打开查看也便于后续导入数据库如MySQL, PostgreSQL或用于Pandas进行数据分析。作为第一阶段的数据落地CSV是最轻量、最便捷的选择。进阶考量如果数据量极大或需要频繁更新可以考虑直接存入SQLite或MySQL。如果需要存储原始HTML或JSON以备复查可以搭配使用pickle或直接保存为文本文件。3.2 开发环境搭建步骤创建虚拟环境这是Python项目的最佳实践可以隔离项目依赖避免版本冲突。# 在项目目录下 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate安装依赖库使用pip安装选定的库。pip install requests beautifulsoup4 lxml这里没有专门安装lxml因为beautifulsoup4安装时会自动处理。但为了确保使用最快的解析器可以显式安装pip install lxml。准备项目结构一个清晰的项目结构有助于管理代码和数据。boohee_food_crawler/ ├── crawler.py # 主爬虫脚本 ├── config.py # 配置文件如请求头、基础URL ├── utils.py # 工具函数如请求函数、解析函数 ├── data/ # 数据存储目录 │ └── foods.csv # 最终输出的CSV文件 └── logs/ # 日志目录可选但强烈推荐我习惯将配置如URL、请求头单独放在一个文件里方便修改。工具函数也独立出来保持主逻辑清晰。3.3 模拟浏览器请求头配置这是绕过基础反爬的第一步。我们需要从浏览器中复制一个真实的请求头。打开开发者工具找到我们之前定位到的那个数据接口请求在“Headers”选项卡下找到“Request Headers”部分。我们需要的关键头部通常包括User-Agent: 告诉服务器我们是什么浏览器。这是必须的。Referer: 表示请求是从哪个页面发起的。对于API请求通常需要设置为食物库主页的URL。Accept/Accept-Language: 告诉服务器客户端接受的内容类型和语言。Cookie: 可能包含会话信息。有些网站不校验有些则必须。可以先不带Cookie试试如果被拒再考虑处理登录或会话维持。在config.py中我们可以这样配置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, Referer: https://www.boohee.com/food/, Accept: application/json, text/javascript, */*; q0.01, Accept-Language: zh-CN,zh;q0.9,en;q0.8, # Cookie: 你的Cookie字符串如果需要的话, # 初期可注释掉 }实操心得User-Agent最好准备一个列表每次请求随机选取一个可以进一步降低被识别为爬虫的风险。Referer对于来自API的请求非常重要很多服务器会校验这个字段。4. 爬虫核心代码实现与解析环境准备好后我们就可以开始编写核心爬虫逻辑了。我将整个过程分解为几个关键函数确保代码模块化易于理解和维护。4.1 第一步获取并解析食物分类首先我们需要从食物库首页提取所有分类信息。编写一个函数fetch_food_categories。import requests from bs4 import BeautifulSoup from config import HEADERS, BASE_URL import time import random def fetch_food_categories(): 获取薄荷健康网食物分类信息。 返回一个列表每个元素是 (分类名称, food_type_id) 的元组。 url f{BASE_URL}/food/ # 假设 BASE_URL https://www.boohee.com try: response requests.get(url, headersHEADERS, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 response.encoding response.apparent_encoding or utf-8 # 智能判断编码 except requests.RequestException as e: print(f请求分类页面失败: {e}) return [] soup BeautifulSoup(response.text, lxml) categories [] # 分析页面找到分类导航栏所在的HTML元素。 # 这里需要根据实际页面结构调整选择器。 # 假设分类在 class 为 ‘food-list’ 的 div 下的 a 标签里 # 更精确的方式是使用浏览器的“检查”功能右键点击分类元素 - “复制” - “复制 selector” category_elements soup.select(div.food-list a[onclick^searchFood]) # 示例选择器需调整 for elem in category_elements: # 获取分类名称通常在标签的文本里 category_name elem.get_text(stripTrue) if not category_name: continue # 提取 food_type_id。从 onclick 属性中提取例如 searchFood(1) onclick_attr elem.get(onclick, ) # 使用简单字符串查找或正则表达式提取数字 import re match re.search(rsearchFood\((\d)\), onclick_attr) if match: food_type_id int(match.group(1)) categories.append((category_name, food_type_id)) print(f发现分类: {category_name} - ID: {food_type_id}) print(f共找到 {len(categories)} 个食物分类。) return categories代码解析与注意事项异常处理网络请求必须用try...except包裹并设置合理的超时(timeout)避免程序因单个请求失败而卡死。编码处理response.encoding显式设置为apparent_encoding或utf-8可以解决大部分中文乱码问题。选择器精度soup.select中的CSS选择器是关键。这里用的‘div.food-list a[onclick^“searchFood”]’是一个示例意为“选择div.food-list下所有onclick属性以searchFood开头的a标签”。你必须使用浏览器的开发者工具检查实际页面的HTML结构来调整这个选择器这是爬虫开发中最需要耐心的一步。数据提取这里演示了从onclick属性中用正则表达式提取数字ID。如果ID在href或其他属性中方法类似。4.2 第二步根据分类ID爬取食物列表有了分类ID我们就可以遍历每个分类请求其API接口。编写函数fetch_foods_by_category。def fetch_foods_by_category(food_type_id, category_name, max_pages50): 根据分类ID爬取该分类下的所有食物。 :param food_type_id: 分类ID :param category_name: 分类名称用于记录 :param max_pages: 最大爬取页数防止意外死循环 :return: 该分类下的食物信息列表 foods [] base_api_url f{BASE_URL}/food/search for page in range(1, max_pages 1): # 构造请求参数 params { keyword: , # 搜索关键词留空 page: page, food_type: food_type_id } # 添加随机延时模拟人类操作避免请求过快 time.sleep(random.uniform(1, 3)) # 随机等待1-3秒 try: response requests.get(base_api_url, headersHEADERS, paramsparams, timeout15) # 检查状态码和响应内容 if response.status_code ! 200: print(f请求分类 {category_name}(ID:{food_type_id}) 第 {page} 页失败状态码: {response.status_code}) break # 如果状态码异常可能已无更多数据或触发反爬跳出循环 data response.json() # 检查API返回的数据结构。通常有一个字段表示列表数据比如 ‘foods’ 或 ‘items’ items data.get(foods, []) # 根据实际JSON结构调整键名 if not items: # 如果当前页没有数据说明已到最后一页 print(f分类 {category_name} 第 {page} 页无数据爬取结束。) break print(f正在处理分类 [{category_name}] 第 {page} 页获取到 {len(items)} 条食物。) for item in items: # 解析每条食物数据字段名需根据实际JSON结构调整 food_name item.get(name, ).strip() food_calory item.get(calory, ) # 可能是字符串如 “116 大卡/100克” food_link item.get(link, ) # 可能是相对路径 # 如果链接是相对路径需要补全为绝对URL if food_link and not food_link.startswith(http): food_link f{BASE_URL}{food_link} if food_name: # 确保食物名称不为空 foods.append({ category: category_name, name: food_name, calory: food_calory, link: food_link }) except requests.RequestException as e: print(f请求分类 {category_name} 第 {page} 页时发生网络错误: {e}) break # 网络错误跳出当前分类的爬取 except ValueError as e: print(f解析JSON响应失败: {e}响应文本: {response.text[:200]}) # 打印前200字符辅助调试 break except Exception as e: print(f处理分类 {category_name} 第 {page} 页时发生未知错误: {e}) break print(f分类 [{category_name}] 爬取完成共获得 {len(foods)} 条记录。) return foods代码解析与核心技巧分页逻辑使用for循环和max_pages参数控制最大页数这是一个安全措施。更优雅的方式是根据API返回的“总页数”或“是否有下一页”标志来循环。你需要检查JSON响应里是否有total_pages或has_next这样的字段。延时策略time.sleep(random.uniform(1, 3))是简单有效的反反爬措施。更复杂的策略可以模拟人类阅读时间的随机分布。健壮的JSON解析使用.get(‘key’, default)方法获取字典值可以避免因键不存在而抛出KeyError异常。错误处理与调试在except ValueError块中打印响应文本的前一部分对于调试API返回非JSON格式如HTML错误页面的情况至关重要。链接补全注意处理详情页链接。API返回的link很可能是相对路径如/food/view/123需要拼接上网站的基础域名才能形成完整的URL。4.3 第三步数据存储与主流程控制我们将爬取到的数据存储到CSV文件中并编写主函数来串联整个流程。import csv import os def save_to_csv(foods_list, filenamedata/foods.csv): 将食物列表保存到CSV文件。 :param foods_list: 食物字典的列表 :param filename: 输出文件名 # 确保输出目录存在 os.makedirs(os.path.dirname(filename), exist_okTrue) # 定义CSV文件的列头 fieldnames [category, name, calory, link] file_exists os.path.isfile(filename) try: with open(filename, a, newline, encodingutf-8-sig) as csvfile: # ‘utf-8-sig’解决Excel打开乱码 writer csv.DictWriter(csvfile, fieldnamesfieldnames) if not file_exists: writer.writeheader() # 如果文件不存在写入列头 writer.writerows(foods_list) print(f数据已成功追加保存至 {filename}) except IOError as e: print(f写入CSV文件失败: {e}) def main(): 主函数控制整个爬取流程。 all_foods [] # 用于累积所有食物数据 print(开始爬取薄荷健康网食物数据...) # 1. 获取所有分类 categories fetch_food_categories() if not categories: print(未能获取到食物分类程序退出。) return total_categories len(categories) for idx, (category_name, food_type_id) in enumerate(categories, 1): print(f\n[{idx}/{total_categories}] 开始爬取分类: {category_name} (ID: {food_type_id})) # 2. 爬取单个分类下的所有食物 foods fetch_foods_by_category(food_type_id, category_name) # 3. 将当前分类的数据添加到总列表 all_foods.extend(foods) # 可选每爬完一个分类就保存一次防止中途出错数据丢失 save_to_csv(foods, fdata/foods_part_{idx}.csv) # 分类间增加较长延时减轻服务器压力 time.sleep(random.uniform(3, 6)) # 4. 最终保存所有数据到一个文件 if all_foods: save_to_csv(all_foods, data/foods_all.csv) print(f\n全部爬取完成共获取 {len(all_foods)} 条食物数据。) else: print(\n爬取完成但未获取到任何数据。) if __name__ __main__: main()流程设计与优化点增量保存主流程中每爬完一个分类就立即保存到一个小文件foods_part_{idx}.csv。这是一个非常重要的容错设计。如果程序在爬取第10个分类时意外中断网络波动、被禁、程序崩溃前9个分类的数据已经保存不会丢失。最后再合并所有数据。分类间延时在爬取不同分类之间增加了比页间延时更长的等待时间3-6秒进一步模拟人类浏览不同板块的行为降低被封IP的风险。编码问题CSV文件使用‘utf-8-sig’编码写入。‘utf-8-sig’会在文件开头写入一个BOM字节顺序标记这样用Windows系统下的Excel打开时可以正确识别UTF-8编码避免中文乱码。如果只用‘utf-8’Excel可能会显示乱码。程序入口if __name__ ‘__main__’:是Python的标准写法确保当该脚本被直接运行时才执行main()函数而被其他模块导入时不会自动执行。5. 高级策略与反爬虫对抗实战基础的爬虫写完后在长时间或大规模运行时你几乎一定会遇到反爬虫机制的挑战。下面分享我遇到的一些问题及应对策略。5.1 请求头深度伪装与会话维持最初的简单HEADERS可能很快会失效。我们需要更逼真地模拟浏览器。动态User-Agent准备一个常见的浏览器UA列表每次请求随机选择。USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... Chrome/120.0.0.0, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ... Version/16.0 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ... Chrome/119.0.0.0, ] headers[User-Agent] random.choice(USER_AGENTS)使用Session对象requests.Session()可以自动管理Cookie在多次请求间保持会话状态使请求行为更接近真实浏览器。session requests.Session() # 可以预先用Session访问一次首页获取并保存初始Cookie session.get(BASE_URL /food/, headersheaders) # 后续请求都使用这个session response session.get(api_url, paramsparams, headersheaders)完善其他头部参考浏览器发送的真实请求补全Accept-Encoding,Connection,Upgrade-Insecure-Requests等字段。但注意requests库默认会处理Accept-Encoding如果手动设置错误可能导致解压问题。5.2 代理IP池的引入与使用如果网站对单一IP的请求频率限制非常严格即使加了延时也可能被封这时就需要使用代理IP。免费与付费代理网上有大量免费代理IP列表但质量极不稳定响应慢、存活时间短。对于严肃的项目建议使用付费的代理IP服务它们提供高匿、稳定、高速的IP池。集成代理在requests请求中设置代理非常简单。proxies { http: http://your_proxy_ip:port, https: http://your_proxy_ip:port, # 注意很多代理服务器的https协议也用http端口 } response requests.get(url, headersheaders, proxiesproxies, timeout10)代理池管理你需要一个列表来管理多个代理IP并在请求失败时自动切换。这涉及到更复杂的错误重试和IP淘汰机制。5.3 异步爬虫加速方案探讨当分类和页面数量很多时同步爬虫一个请求完成后再发下一个会非常慢。此时可以考虑异步IO来并发请求极大提升效率。库的选择aiohttpasyncio是Python异步爬虫的经典组合。核心改造将网络请求函数改为async函数使用aiohttp.ClientSession。然后通过asyncio.gather()或asyncio.as_completed()来并发执行多个请求。并发控制必须使用信号量asyncio.Semaphore限制并发数否则瞬间发出成百上千个请求会拖垮目标网站或导致IP被永久封禁。注意事项异步编程模型比同步复杂错误处理、重试逻辑都需要重新设计。对于新手建议先完成稳定的同步版本再考虑异步优化。我的经验对于薄荷健康网这个规模的数据同步爬虫加上合理的延时通常在半小时到一小时内也能完成。除非需要极高频次地更新数据否则引入异步的复杂度可能得不偿失。稳定性优先于速度是爬虫项目的一个重要原则。6. 数据清洗、校验与后续扩展爬取到的原始数据往往存在一些问题需要进行清洗和校验才能投入使用。6.1 常见数据问题与清洗方法热量字段格式化爬取到的热量可能是“116 大卡/100克”、“116千卡”或“--”。我们需要统一格式并提取出数值部分。def clean_calory(calory_str): if not calory_str or -- in calory_str: return None # 使用正则表达式提取数字 import re match re.search(r(\d(\.\d)?), calory_str) return float(match.group(1)) if match else None食物名称去重与规范化同一食物可能有不同名称如“米饭蒸”、“白米饭”或者含有多余空格、特殊字符。需要进行基本的清洗str.strip()更复杂的去重和归一化可能需要基于规则或机器学习。链接有效性校验可以编写一个简单的函数对爬取到的链接发起HEAD请求只请求头部不下载内容检查状态码是否为200以过滤掉无效链接。缺失值处理对于清洗后仍为None的热量或空白的名称可以记录日志并在最终数据集中标记或剔除。6.2 数据存储方案升级当数据量增大或需要复杂查询时CSV就显得力不从心了。SQLite数据库Python内置支持无需安装额外服务。适合中小型项目可以将数据存入foods表并建立分类、名称等索引方便查询。import sqlite3 conn sqlite3.connect(food_data.db) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS foods (id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT, name TEXT, calory REAL, link TEXT)) # 插入数据 c.execute(INSERT INTO foods (category, name, calory, link) VALUES (?,?,?,?), (food[category], food[name], food[calory], food[link])) conn.commit()MySQL/PostgreSQL适用于团队协作或需要更高性能、更复杂查询的场景。可以使用pymysql或psycopg2库进行连接和操作。MongoDB如果食物详情页的结构非常复杂或不统一非结构化或半结构化数据NoSQL数据库如MongoDB可能更灵活。6.3 项目扩展从列表到详情本项目只爬取了列表页的基础信息。食物详情页food_link指向的页面通常包含更丰富的营养数据如蛋白质、脂肪、碳水化合物、维生素等。扩展思路解析详情页编写新的函数parse_food_detail(link)接收食物链接请求并解析详情页HTML。数据提取使用BeautifulSoup定位详情页中的营养元素表格提取各项数值。关联数据将详情数据与列表数据通过食物名称或ID关联起来更新到数据库的同一张表或关联表中。增量爬取维护一个已爬取详情页的链接集合避免重复爬取。只对新增的食物爬取详情。这会将项目从一个简单的列表爬虫升级为一个完整的、立体的营养数据库构建工具。7. 常见问题排查与实战心得在开发和运行爬虫的过程中我遇到了不少“坑”。这里总结一份速查表希望能帮你快速定位问题。问题现象可能原因排查步骤与解决方案返回状态码403请求被服务器拒绝反爬生效。1. 检查User-Agent是否设置且有效。2. 检查Referer是否正确。3. 检查是否需要携带有效的Cookie可能需要模拟登录。4. 检查IP是否被封锁尝试使用代理。返回状态码404接口URL或参数错误。1. 确认API地址是否正确用浏览器开发者工具核对。2. 确认food_type等参数的值是否在有效范围内。返回状态码200但内容是验证页面或空数据触发了动态反爬如验证码。1. 降低请求频率大幅增加延时。2. 检查请求头是否完整模拟了浏览器。3. 考虑使用更高级的模拟工具如Selenium绕过前端验证但这会极大增加复杂度。response.json()抛出JSONDecodeError服务器返回的不是JSON可能是HTML错误页。1. 打印response.text[:500]查看返回内容。2. 检查请求头特别是Accept字段是否包含application/json。3. 检查网络环境是否被重定向到登录页或错误页。爬取速度非常慢同步请求延时导致。1. 评估是否真的需要这么快稳定性更重要。2. 如需加速考虑改造为异步爬虫aiohttp。3. 优化代码减少不必要的I/O操作。数据缺失或字段错位页面结构或API响应格式发生变化。1.这是爬虫的常态。定期运行爬虫并监控日志。2. 编写健壮的解析代码多用.get()方法并提供默认值。3. 将解析逻辑与数据模型分离便于适配变化。程序运行中途崩溃数据丢失网络异常、程序bug导致。1. 实施增量保存策略每爬取一部分就保存一次。2. 加强异常捕获记录详细的错误日志。3. 考虑使用任务队列将爬取任务拆分成可重试的单元。最后几点个人体会尊重robots.txt在爬取前访问https://www.boohee.com/robots.txt查看网站是否允许爬虫访问/food/路径。虽然技术上可以绕过但遵守规则是良好的网络公民行为。控制爬取力度务必添加足够的延时避免对目标网站服务器造成压力。我们的目标是获取数据不是攻击网站。数据用途爬取的数据应用于个人学习、研究或符合网站条款的合法用途。切勿用于商业倒卖或任何违法活动。代码即文档为关键函数和复杂逻辑编写清晰的注释。几个月后回头看或者别人接手你的代码时会非常感谢你这么做。这个项目从分析到实现涵盖了爬虫大部分核心环节。希望这份超详细的总结能帮你不仅成功爬取到薄荷健康网的数据更能深入理解爬虫技术背后的原理和工程化思考。遇到具体问题时多利用浏览器的开发者工具进行观察和调试那才是爬虫开发者最强大的“眼睛”。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进