1. 项目概述一个为土耳其美食爱好者打造的AI食谱助手如果你和我一样既是个烹饪爱好者又对土耳其美食的丰富香料和独特风味着迷但面对海量食谱和冰箱里零散的食材时常感到无从下手那么这个项目或许能成为你的“厨房军师”。CoPaw一个由开发者EminKolac开源的AI食谱助手它巧妙地解决了几个核心痛点如何高效地从付费食谱平台Cookidoo整理自己的食谱库如何根据手头现有的食材快速找到能做的菜以及如何在一个清爽的界面上浏览和管理这些食谱。这个项目本质上是一个全栈Web应用后端用Python和FastAPI搭建负责数据抓取、AI对话和API服务前端用Next.js和React构建提供直观的用户界面核心的“智能”则交给了Google的Gemini模型。最吸引人的是它的“零配置”理念使用文件型SQLite数据库开箱即用并且贴心地提供了12道经典的土耳其食谱作为演示数据让你无需任何付费账号也能立即体验全部浏览和搜索功能。接下来我将带你从零开始深入这个项目的每一个技术细节并分享我在部署和扩展它时踩过的坑和总结的经验。2. 技术栈选型与架构解析2.1 为什么是FastAPI Next.js的组合选择FastAPI作为后端框架在我看来是看重了它的两大优势极高的异步处理性能和自动生成的交互式API文档。食谱抓取Scraping和AI聊天Chat都是典型的I/O密集型操作需要等待网络响应FastAPI原生支持async/await能轻松处理这类并发请求避免阻塞这对于提升用户体验至关重要。而自动生成的/docs页面对于前后端分离的开发模式来说简直是联调神器后端开发者几乎无需额外编写接口文档。前端选用Next.js 16项目创建时的最新稳定版和React 19则瞄准了现代Web开发的核心需求服务端渲染SSR和极简的路由。食谱列表、详情页这类内容非常适合用SSR来提升首屏加载速度和SEO。Next.js基于文件系统的路由app/recipes/[id]/page.tsx让页面组织变得异常直观大大降低了路由配置的复杂度。Tailwind CSS 4用于样式其实用优先Utility-First的理念能让我们快速构建出响应式且一致的UI而无需在CSS文件和组件间反复横跳。这个前后端分离的架构清晰地将职责划分开后端是纯粹的数据和逻辑中心前端是专注的展示和交互层。两者通过RESTful API通信部署时可以分开非常灵活。2.2 数据库SQLite的轻量之道项目使用了aiosqlite这个异步SQLite驱动而非更常见的PostgreSQL或MySQL。这是一个非常务实且巧妙的选择。对于个人或小范围使用的食谱助手来说数据量不会爆炸式增长SQLite完全能够胜任。它的最大优点就是“零配置”——无需安装和运行独立的数据库服务一个.db文件搞定一切这极大地简化了部署和迁移成本。aiosqlite则让这个轻量级数据库也能完美融入FastAPI的异步生态避免在数据库操作上出现性能瓶颈。注意虽然SQLite轻便但在高并发写的场景下比如多人同时触发食谱抓取它可能会成为瓶颈。不过对于CoPaw预设的个人或家庭使用场景这完全不是问题。如果你的规划是做成一个多用户公共平台那么在架构早期就需要考虑更换为PostgreSQL等更强大的关系型数据库。2.3 AI引擎为何选择Gemini 2.0 FlashAI聊天功能是CoPaw的“灵魂”。它选择了Google的Gemini 2.0 Flash模型。Gemini Flash是Gemini系列中的“轻量快跑”型号相比更强大的Gemini Pro它在保持足够理解能力的同时响应速度更快成本也更低。这对于需要实时交互的“我有这些食材能做什么菜”这类场景来说速度和性价比是关键。通过简单的API调用我们就能将用户输入的、可能杂乱无章的食材描述转化为结构化的食谱建议。3. 核心模块深度剖析与实操3.1 食谱抓取器与Cookidoo的“对话”scraper.py是这个项目中最具技巧性的模块之一。它需要模拟用户登录Cookidoo网站遍历食谱列表页并进入每个详情页提取结构化数据。这通常涉及到处理会话Session、Cookie、解析动态加载的内容可能用到类似Playwright的无头浏览器以及应对网站的反爬机制。虽然源码中使用了cookidoo-api这个库但我们可以深入理解其一般原理。一个健壮的食谱抓取器通常包含以下步骤会话建立与登录使用requests或httpx库创建会话向登录接口发送携带邮箱和密码的POST请求并妥善保存返回的认证Cookie。列表页遍历分析Cookidoo食谱列表页的URL规律和分页逻辑循环请求每一页使用BeautifulSoup或lxml解析HTML提取出每个食谱的标题、链接、可能的主图等基本信息。详情页解析对于每个食谱链接发起请求获取详情页HTML。这里是解析的重灾区需要仔细分析DOM结构定位到食材清单ingredients、步骤说明instructions、烹饪时间、难度等字段所在的HTML标签并编写稳定的选择器Selector进行提取。网站改版是抓取器的天敌因此选择器要尽可能健壮或者考虑备用方案。数据存储与去重将提取的结构化数据JSON格式通过后端API或直接写入数据库。必须要有去重机制例如根据食谱ID或唯一URL避免多次运行抓取器产生重复数据。礼貌爬取与错误处理在请求间添加随机延时如time.sleep(random.uniform(1, 3))避免对目标网站造成压力。同时必须用try...except包裹每个请求和解析步骤记录错误日志确保单个页面解析失败不会导致整个抓取任务崩溃。# 一个简化的抓取逻辑示意非项目原码 async def scrape_recipe_detail(session, recipe_url): try: async with session.get(recipe_url) as response: html await response.text() soup BeautifulSoup(html, html.parser) # 假设的解析逻辑实际需要根据Cookidoo网站结构调整 title soup.select_one(h1.recipe-title).text.strip() ingredients [li.text for li in soup.select(ul.ingredients-list li)] instructions [step.text for step in soup.select(div.instructions ol li)] return { title: title, ingredients: ingredients, instructions: instructions, source_url: recipe_url } except Exception as e: logging.error(fFailed to scrape {recipe_url}: {e}) return None3.2 AI聊天引擎从食材到食谱的魔法chat.py模块是实现智能推荐的核心。它接收用户输入的一段自然语言描述例如“我有鸡肉、番茄、洋葱和一点酸奶”然后构造一个精心设计的提示词Prompt发送给Gemini API请求其生成食谱建议。一个有效的Prompt工程是成败的关键。你不能简单地问“用这些食材能做什么”而应该给AI设定明确的角色、输出格式和约束条件。例如你是一位精通土耳其料理的厨师。用户提供了一些他们现有的食材。请根据这些食材推荐1-3道可行的土耳其菜肴并遵循以下格式 1. **推荐菜名** - **主要食材**[列出用户已有且用到的食材] - **可能需要的额外食材**[列出1-2种常见、易得的补充食材] - **简要做法**[用2-3句话描述核心步骤] - **风味特点**[如浓郁、清爽、辛辣等]这样的Prompt能引导AI生成结构化、有用的回答而不是天马行空的散文。后端在收到AI的回复后可以进一步解析这段文本或者直接将其格式化后返回给前端展示。# chat.py 核心函数示意 import google.generativeai as genai genai.configure(api_keyos.getenv(GEMINI_API_KEY)) model genai.GenerativeModel(gemini-2.0-flash) async def get_recipe_suggestion(user_input: str) - str: prompt f你是一位土耳其菜专家。用户说“{user_input}” 请根据用户拥有的食材推荐合适的土耳其菜。回答请简洁直接给出菜名和核心思路。 try: response await model.generate_content_async(prompt) return response.text except Exception as e: return fAI服务暂时不可用{e}3.3 数据库模型设计在database.py中我们可以看到核心的数据表结构。一个设计良好的食谱数据库通常至少包含以下两张表recipes食谱表id(主键)title(菜名)description(描述)ingredients(JSON或TEXT存储食材列表)instructions(JSON或TEXT存储步骤列表)prep_time(准备时间)cook_time(烹饪时间)category_id(外键关联分类)image_url(封面图链接)source_url(原始链接)categories分类表id(主键)name(分类名如“汤类”、“主菜”、“甜品”)使用JSON字段存储ingredients和instructions非常灵活便于前端直接解析渲染成列表。关系型数据库如SQLite的JSON支持使得这种半结构化数据存储和查询例如查询包含“番茄”的食谱变得可行。4. 从零开始的完整部署与配置指南4.1 环境准备与项目初始化首先确保你的开发环境满足要求。我推荐使用pyenv管理Python版本用nvm管理Node.js版本这样可以轻松切换。# 1. 克隆代码库 git clone https://github.com/EminKolac/copaw.git cd copaw # 2. 设置Python虚拟环境强烈推荐避免包冲突 cd backend python -m venv venv # 创建虚拟环境 # 激活虚拟环境 # 在Linux/macOS上 source venv/bin/activate # 在Windows上 # venv\Scripts\activate # 3. 安装Python依赖 pip install -r requirements.txt4.2 关键配置详解接下来是配置环节backend/.env文件是整个项目的钥匙。cp .env.example .env # 使用你喜欢的编辑器如vim, code打开 .env 文件.env文件内容及解读# Cookidoo凭证用于抓取真实食谱 COOKIDOO_EMAILyour_real_emaildomain.com COOKIDOO_PASSWORDyour_secure_password # 重要请使用强密码并确保此.env文件不被提交到公开仓库。 # Gemini API密钥用于AI聊天功能 GEMINI_API_KEYyour_actual_gemini_api_key_hereCookidoo凭证只有在你拥有Cookidoo土耳其站cookidoo.com.tr订阅账号时才需要填写。抓取器会使用这些信息登录并获取你的食谱。如果没有留空即可项目依然可以运行在演示模式。Gemini API密钥这是可选的但强烈建议申请一个。前往 Google AI Studio 需要Google账号可以免费创建API密钥有一定的免费额度足够个人体验。有了它才能解锁“根据食材推荐菜”的AI功能。实操心得.env文件务必添加到.gitignore中永远不要将包含真实密码和API密钥的配置文件提交到Git。一个常见的做法是提交.env.example文件其中只包含空的键或示例值作为配置模板。4.3 启动后端服务配置好后我们可以初始化数据库并启动后端API。# 确保在backend目录下且虚拟环境已激活 # 导入演示数据即使没有Cookidoo账号也会创建12道土耳其食谱 python seed_data.py # 看到“Database seeded successfully!”或类似提示即成功。 # 启动FastAPI开发服务器 python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload--host 0.0.0.0允许从本机以外的设备访问比如同一局域网内的手机方便测试。--port 8000指定端口。--reload开启热重载修改代码后服务器会自动重启非常适合开发。启动成功后你可以在浏览器打开http://localhost:8000/docs看到FastAPI自动生成的交互式API文档这里可以测试所有后端接口。4.4 启动前端开发服务器打开一个新的终端窗口或标签页进入前端目录。cd ../frontend npm install # 或使用 yarn/pnpm npm run devNext.js开发服务器默认运行在http://localhost:3000。npm run dev同样启用了热模块替换HMR前端代码的改动会即时反映在浏览器中。4.5 验证与访问现在打开浏览器访问http://localhost:3000。你应该能看到CoPaw的主页。如果一切顺利你可以点击“浏览食谱”查看由seed_data.py导入的12道演示食谱。使用搜索框尝试搜索“köfte”土耳其肉丸或“soup”。点击任意食谱卡片查看详细的食材和步骤。如果配置了GEMINI_API_KEY可以尝试点击AI聊天功能输入“chicken, rice, yogurt”看看它会推荐什么土耳其菜。5. 功能扩展与个性化定制思路原项目已经搭建了一个非常坚实的框架但总有地方可以按照自己的需求进行打磨。以下是我在把玩这个项目后想到的几个扩展方向5.1 支持更多食谱数据源Cookidoo很棒但食谱世界很大。我们可以让抓取器支持更多网站比如国内的下厨房、美食天下或者国际化的AllRecipes、BBC Good Food。思路是抽象出一个“抓取器接口”Scraper Interface每个数据源实现这个接口。然后在配置或UI中让用户选择数据源。# 伪代码示例 class BaseScraper: async def login(self, credentials): pass async def fetch_recipes(self, max_num): pass class CookidooScraper(BaseScraper): # 实现Cookidoo特定的抓取逻辑 pass class XiaChuFangScraper(BaseScraper): # 实现下厨房的抓取逻辑可能需要处理不同的登录和页面结构 pass # 在配置或API请求中指定数据源 scraper get_scraper(source_namexiachufang) await scraper.login({username: ..., password: ...}) recipes await scraper.fetch_recipes(50)5.2 增强AI提示词与输出结构化目前的AI聊天可能返回自由文本。我们可以通过更精细的Prompt工程让Gemini直接返回结构化的JSON数据。这样前端就可以用更漂亮的组件来展示AI推荐的食谱甚至能一键将推荐的食谱保存到本地数据库。例如让AI返回如下格式{ suggestions: [ { name: 酸奶鸡肉饭, confidence: 高, missing_ingredients: [米饭, 黄油], description: 一道 creamy 的主菜... } ] }后端收到后直接解析JSON并返回给前端前端渲染成卡片列表并高亮缺失的食材。5.3 添加用户系统与收藏功能当前项目是单用户/无状态模式。如果想做成多用户服务需要引入用户认证如JWT、独立的用户表以及关联表来存储“用户-食谱”的收藏关系、评分、笔记等。这会将项目复杂度提升一个层级但可玩性也大大增加。FastAPI有完善的依赖注入系统可以方便地集成像fastapi-users这样的库来快速搭建用户体系。5.4 容器化部署为了让部署更简单可以编写Dockerfile和docker-compose.yml文件将前后端以及可能的Nginx代理都容器化。这样无论是在自己的云服务器上还是在Railway、Fly.io这样的PaaS平台都能一键部署。# backend/Dockerfile 示例 FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]6. 常见问题与故障排除实录在实际搭建和运行过程中你可能会遇到以下问题。这里记录了我遇到的情况和解决方法。6.1 后端服务启动失败问题运行uvicorn main:app时提示模块导入错误例如ModuleNotFoundError: No module named fastapi。原因Python依赖没有正确安装或者没有在正确的虚拟环境中操作。解决确认终端当前路径在backend/目录下。确认虚拟环境已激活命令行提示符前通常有(venv)字样。重新安装依赖pip install -r requirements.txt。如果还不行尝试手动安装核心包pip install fastapi uvicorn sqlalchemy aiosqlite。6.2 前端无法连接到后端API问题前端页面能打开但食谱列表为空浏览器开发者工具控制台Console显示网络错误如Failed to fetch或Connection refused。原因前端配置的API代理地址不正确或者后端服务没有运行。解决首先确认后端服务正在运行并且能通过http://localhost:8000/docs访问。检查frontend/next.config.ts或frontend/next.config.js文件中的rewrites或proxy配置。原项目通常配置了将/api/*请求代理到http://localhost:8000/api/*。确保端口一致。如果配置正确可能是CORS问题。需要在FastAPI后端添加CORS中间件。检查backend/main.py中是否有类似以下代码from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[http://localhost:3000], # 你的前端地址 allow_credentialsTrue, allow_methods[*], allow_headers[*], )6.3 Cookidoo抓取失败问题运行python scraper.py或通过API触发抓取后日志显示登录失败或抓取不到数据。原因凭证错误.env文件中的邮箱或密码有误。网站改版Cookidoo的网页结构发生变化导致解析器Selector失效。反爬机制网站检测到自动化脚本返回验证码或封锁IP。解决仔细核对.env文件中的COOKIDOO_EMAIL和COOKIDOO_PASSWORD确保是有效的土耳其站订阅账号。手动用浏览器登录cookidoo.com.tr确认账号状态正常。如果网站改版需要分析新的HTML结构更新scraper.py中的CSS选择器或XPath。这是维护抓取器最常见的工作。对于反爬可以尝试增加请求间隔时间、使用轮换的User-Agent、或者考虑使用更模拟浏览器的工具如Playwright。但请注意遵守网站的服务条款。6.4 AI聊天无响应或报错问题在聊天界面输入内容后长时间无反应或返回“AI服务不可用”错误。原因API密钥未配置或无效.env文件中GEMINI_API_KEY为空或错误。额度用尽或账单问题Google AI Studio的免费额度用完或账户有异常。网络问题无法访问Google API。解决检查.env文件确保密钥已正确粘贴没有多余的空格或换行。前往 Google AI Studio 查看API使用情况和配额。在后端日志中查找更详细的错误信息。可以在chat.py的请求部分添加更详细的异常日志打印。如果是网络问题可能需要检查代理设置注意此项目不涉及任何网络访问工具仅指常规的网络连通性。6.5 数据库文件权限问题Linux/macOS问题运行python seed_data.py或应用尝试写数据库时出现sqlite3.OperationalError: unable to open database file。原因运行进程的用户没有在项目目录下创建或写入copaw.db文件的权限。解决# 确保在项目根目录copaw/ touch copaw.db # 尝试创建文件 # 如果提示权限被拒可以更改目录权限谨慎操作确保目录安全 chmod 755 . # 赋予当前目录读写执行权限 # 或者更安全地只更改数据库文件的权限 sudo chown $USER:$USER copaw.db # 将文件所有者改为当前用户这个项目就像一个精心设计的乐高套装提供了所有核心部件和清晰的说明书。通过亲手搭建它你不仅能获得一个实用的个人食谱助手更能深入理解一个现代全栈应用是如何从数据抓取、AI集成到前后端交互一步步构建起来的。无论是作为学习样板还是作为满足自己特定需求的一个起点CoPaw都提供了极高的价值和灵活性。我最享受的部分就是在它原有的骨架上按照自己的烹饪习惯和口味一点点添砖加瓦让它真正变成我的专属厨房智能伙伴。