1. 项目概述当传统分类信息平台遇上AI数据提取如果你经常在法国的二手交易平台Leboncoin上“淘金”或者需要批量分析上面的商品信息那你一定体会过手动复制粘贴的痛苦。商品描述、价格、位置、联系方式……这些信息散落在成千上万个页面里想系统性地收集和分析简直是一场噩梦。今天要聊的这个项目leboncoin-chatgpt-data-extract就是专门为解决这个痛点而生的。它本质上是一个自动化工具能够模拟真实用户访问Leboncoin网站智能地抓取商品列表和详情页数据并利用类似ChatGPT的大型语言模型LLM来理解和结构化那些非标准化的文本描述。简单来说它把两个强大的能力结合在了一起一是网络爬虫的自动化抓取能力二是大语言模型的自然语言理解能力。前者负责“动手”高效地从网站上获取原始数据后者负责“动脑”从杂乱无章的文本中精准提取出我们关心的关键字段比如品牌、型号、尺寸、颜色、缺陷描述等。这个组合拳让从海量非结构化网页中获取结构化数据变得前所未有的简单和准确。无论你是数据分析师、市场研究员、二手商品倒卖者还是仅仅想监控某个品类价格趋势的普通用户这个工具都能为你节省大量时间和精力。2. 核心设计思路与技术选型2.1 为什么是“爬虫 LLM”的组合传统的网页数据提取尤其是针对像Leboncoin这样反爬措施日益严格的现代网站通常面临两大挑战。第一是技术层面的对抗网站会使用动态加载Ajax、验证码、请求频率限制、IP封禁等手段来阻止自动化访问。第二是数据层面的解析困难商品描述是用户自由填写的自然语言格式千变万化用固定的正则表达式或XPath规则去匹配要么写起来极其复杂要么覆盖率很低稍微换个说法就提取失败。leboncoin-chatgpt-data-extract项目的设计思路非常清晰用成熟的爬虫框架如Playwright或Selenium来解决第一个挑战模拟真人浏览器行为绕过基础的反爬机制用大语言模型LLM的通用语义理解能力来解决第二个挑战让AI去“读懂”描述并按要求输出结构化信息。这种架构将复杂的规则编写工作转变为了对AI模型的“提问”即设计提示词Prompt极大地提升了开发效率和提取的鲁棒性。2.2 技术栈深度解析从项目名称和常见实践推断其技术栈通常包含以下几个核心部分爬虫引擎Playwright / SeleniumPlaywright是当前的首选因为它支持多浏览器Chromium, Firefox, WebKit能更好地模拟真实环境且对现代网页技术如SPA单页应用的支持比传统的RequestsBeautifulSoup组合要好得多。它可以直接执行JavaScript等待动态元素加载处理弹窗和验证码虽然不能完全破解复杂验证码但能提供交互接口是应对动态网站的利器。大语言模型接口OpenAI API / 本地模型核心是调用LLM的API。最直接的是使用OpenAI的Chat Completion API如gpt-3.5-turbo或gpt-4它稳定、能力强。考虑到成本和对数据隐私的要求项目也可能集成开源的本地模型通过Ollama、LM Studio或直接调用Hugging Face的transformers库来运行。选择本地模型虽然初期设置复杂且对硬件有要求但长期运行成本低且数据完全不出本地。提示词工程Prompt Engineering这是项目的灵魂。如何设计一个清晰、无歧义的提示词Prompt让LLM准确理解要从一段文本中提取哪些字段并以指定的格式如JSON返回直接决定了数据提取的质量。这需要深入理解LLM的工作方式和Leboncoin商品描述的常见模式。数据管道与调度负责串联整个流程。包括管理待抓取的URL队列、控制爬虫速率以避免被封、将抓取到的原始HTML或文本发送给LLM处理、解析LLM返回的JSON、清洗数据、最后存储到数据库如SQLite、PostgreSQL或文件如CSV、JSONL中。可能还会用到任务队列如Celery来管理并发和重试。反反爬策略集成这是生产环境必须考虑的。包括使用代理IP池轮换IP、设置随机的请求延迟和鼠标移动轨迹、管理Cookies和会话状态、识别并处理验证码可能需要接入第三方打码平台等。注意直接对商业网站进行大规模爬取可能违反其服务条款ToS。在开发和使用此类工具时务必遵守robots.txt协议控制请求频率避免对目标网站服务器造成负担。本工具及讨论仅用于技术学习和个人范围内的数据收集严禁用于任何商业爬虫或恶意攻击。3. 核心模块拆解与实操要点3.1 爬虫模块稳健地获取页面数据爬虫模块的目标是可靠地拿到两个东西商品列表页和商品详情页的HTML内容。对于Leboncoin这类网站直接HTTP GET请求往往拿不到完整内容因为大量数据是通过JavaScript异步加载的。实操步骤与代码示例首先使用Playwright启动一个浏览器实例并设置一些“拟人化”参数。from playwright.sync_api import sync_playwright import time import random def fetch_page(url): with sync_playwright() as p: # 使用Chromium可配置为无头模式headlessFalse用于调试 browser p.chromium.launch(headlessTrue) context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... # 使用真实UA ) page context.new_page() try: # 访问页面等待网络空闲状态确保主要内容加载完成 page.goto(url, wait_untilnetworkidle) # 随机滚动页面模拟人类浏览行为 for _ in range(random.randint(2, 5)): page.mouse.wheel(0, random.randint(300, 800)) time.sleep(random.uniform(0.5, 2)) # 获取页面完整HTML html_content page.content() return html_content except Exception as e: print(f抓取 {url} 失败: {e}) return None finally: browser.close()关键点与避坑指南等待策略wait_untilnetworkidle比load更可靠它等待页面网络活动基本停止适合动态加载的网站。对于某些特定元素可以使用page.wait_for_selector()进行更精确的等待。行为模拟简单的滚动和随机延迟能有效降低被识别为机器人的风险。更高级的可以模拟点击、鼠标移动轨迹等。错误处理网络超时、元素未找到、验证码弹出等都是常见异常必须用try-except包裹并做好日志记录和重试机制。资源管理务必在finally块中关闭browser防止资源泄漏。在高并发场景下考虑复用浏览器上下文context而非为每个请求都启动新浏览器。3.2 数据解析模块从HTML到原始文本拿到HTML后我们需要从中提取出LLM需要处理的“核心文本”。通常包括商品标题、价格、位置、发布者信息和最重要的——商品描述。这里我们使用parselScrapy使用的选择器库语法类似或BeautifulSoup。from parsel import Selector def parse_list_page(html): 解析列表页提取商品链接和基础信息 selector Selector(texthtml) items [] # 根据Leboncoin实际HTML结构调整选择器这里为示例 for item in selector.css(div[data-qa-idaditem_container]): title item.css(p[data-qa-idaditem_title]::text).get() price item.css(span[data-qa-idaditem_price]::text).get() link item.css(a[data-qa-idaditem_container]::attr(href)).get() # 补全可能为相对链接的URL if link and not link.startswith(http): link fhttps://www.leboncoin.fr{link} if title and link: items.append({title: title.strip(), price: price, url: link}) return items def parse_detail_page(html): 解析详情页提取供LLM分析的文本块 selector Selector(texthtml) data {} # 提取各个字段 data[title] selector.css(h1[data-qa-idadview_title]::text).get().strip() data[price] selector.css(span[data-qa-idadview_price]::text).get().strip() data[location] selector.css(div[data-qa-idadview_location_informations]::text).getall() data[description] \n.join(selector.css(div[data-qa-idadview_description_container] *::text).getall()).strip() # 有时属性会放在特定的列表或表格中 data[attributes] {} for attr in selector.css(div[data-qa-idcriteria_item]): key attr.css(span[data-qa-idcriteria_title]::text).get() value attr.css(span[data-qa-idcriteria_value]::text).get() if key and value: data[attributes][key.strip()] value.strip() # 将所有文本合并成一个字符串作为LLM的输入 llm_input_text f 标题: {data[title]} 价格: {data[price]} 位置: {data.get(location, )} 描述: {data[description]} 属性: {data[attributes]} return llm_input_text, data # 返回原始数据供校验实操心得选择器稳定性优先使用>def create_extraction_prompt(raw_text): prompt f 你是一个专业的数据提取助手专门从法国二手交易平台Leboncoin的商品信息中提取结构化数据。 请仔细分析以下用包裹的商品信息并严格按照要求输出一个JSON对象。 商品信息{raw_text}提取要求 1. **brand品牌**: 从标题或描述中识别商品品牌。如“iPhone”、“Samsung”、“Dyson”。如果没有明确品牌则输出空字符串 。 2. **model型号**: 提取商品的具体型号。如“iPhone 13 Pro Max”、“Galaxy S23”、“V11 Animal”。如果没有输出空字符串。 3. **condition状态**: 判断商品状态。从以下选项中选择new全新、like_new几乎全新、good良好、fair一般、for_parts仅零件。主要依据描述中的“neuf”、“très bon état”、“bon état”、“état correct”、“pour pièces”等关键词判断。 4. **color颜色**: 提取商品颜色。如“noir”、“blanc”、“gris”。如果没有输出空字符串。 5. **dimensions尺寸**: 提取尺寸信息如“160x200cm”、“42寸”、“10 kg”。注意区分是屏幕尺寸、家具尺寸还是重量。 6. **has_defect是否有缺陷**: 根据描述判断商品是否有明确缺陷。有则输出 true否则输出 false。 7. **defect_description缺陷描述**: 如果has_defect为true简要提取缺陷描述否则输出空字符串。例如“écran fissuré”屏幕开裂、“bouton cassé”按钮损坏。 8. **extracted_price提取价格**: 从价格字符串中提取纯数字整数。例如从“150 €”中提取150。 9. **category类别**: 推断商品所属主要类别。如“electronics”电子产品、“furniture”家具、“vehicle”车辆、“clothing”服装等。 输出格式必须是严格的JSON只包含以下键brand, model, condition, color, dimensions, has_defect, defect_description, extracted_price, category。 现在开始分析并输出JSON return prompt调用LLM API以OpenAI为例import openai import json import os # 设置API密钥 openai.api_key os.getenv(OPENAI_API_KEY) def extract_with_llm(prompt): try: response openai.ChatCompletion.create( modelgpt-3.5-turbo, # 或 gpt-4 messages[ {role: system, content: 你是一个精确的数据提取助手只输出有效的JSON。}, {role: user, content: prompt} ], temperature0.1, # 低温度保证输出稳定性 max_tokens500 ) result_text response.choices[0].message.content.strip() # 尝试解析返回的JSON # LLM有时会在JSON外加一层markdown代码块标记需要处理 if result_text.startswith(json): result_text result_text[7:-3] # 去除 json 和 elif result_text.startswith(): result_text result_text[3:-3] # 去除 和 extracted_data json.loads(result_text) return extracted_data except json.JSONDecodeError as e: print(fLLM返回的不是有效JSON: {result_text}) # 可以在这里加入重试或使用正则表达式进行抢救性提取 return None except Exception as e: print(f调用LLM API失败: {e}) return None经验技巧与参数调优System Prompt设置系统角色指令约束LLM的行为比如强调“只输出JSON”能有效减少多余的解释性文字。Temperature设置为较低值如0.1-0.3使输出更确定、更可重复适合数据提取这种任务。结构化输出明确要求输出格式如“严格的JSON”并给出键名能显著提升输出的一致性。OpenAI最新的API甚至支持response_format{ type: json_object }参数来强制JSON输出。错误处理一定要对LLM的返回结果进行JSONDecodeError异常捕获。LLM偶尔会“放飞自我”在JSON前后添加说明文字。编写一个健壮的解析函数如尝试查找第一个{和最后一个}是必要的。成本与延迟gpt-3.5-turbo成本低、速度快对于大多数提取任务精度足够。gpt-4精度更高但成本和延迟也高。需要根据任务关键性和预算权衡。对于法语文本两者理解能力都足够强。3.4 流程编排与数据存储将以上模块串联起来形成一个完整的自动化管道。import csv from urllib.parse import urljoin BASE_URL https://www.leboncoin.fr/recherche SEARCH_PARAMS {category: 9, text: iphone} # 示例电子品类搜索iphone def main(): all_extracted_data [] # 1. 构建搜索URL抓取列表页 list_url f{BASE_URL}?{.join([f{k}{v} for k,v in SEARCH_PARAMS.items()])} list_html fetch_page(list_url) if not list_html: print(列表页抓取失败程序退出。) return # 2. 解析列表页获取商品链接 items parse_list_page(list_html) print(f找到 {len(items)} 个商品。) # 3. 遍历每个商品详情页 for idx, item in enumerate(items[:10]): # 限制前10个作为演示 print(f处理商品 {idx1}/{min(10, len(items))}: {item[title]}) detail_html fetch_page(item[url]) if not detail_html: continue # 4. 解析详情页获取原始文本 llm_input_text, raw_data parse_detail_page(detail_html) # 5. 创建提示词调用LLM提取结构化数据 prompt create_extraction_prompt(llm_input_text) structured_data extract_with_llm(prompt) if structured_data: # 6. 合并原始URL、标题等和LLM提取的数据 final_record { source_url: item[url], source_title: item[title], source_price: item.get(price), **structured_data # 将LLM提取的字段合并进来 } all_extracted_data.append(final_record) print(f 成功提取: {structured_data.get(brand, )} {structured_data.get(model, )}) else: print(f LLM提取失败。) # 礼貌延迟避免请求过快 time.sleep(random.uniform(2, 5)) # 7. 存储结果到CSV if all_extracted_data: keys all_extracted_data[0].keys() with open(leboncoin_extracted_data.csv, w, newline, encodingutf-8-sig) as f: dict_writer csv.DictWriter(f, fieldnameskeys) dict_writer.writeheader() dict_writer.writerows(all_extracted_data) print(f数据已保存至 leboncoin_extracted_data.csv共 {len(all_extracted_data)} 条记录。) if __name__ __main__: main()4. 高级策略与生产环境考量4.1 规模化爬取与反反爬上述示例是单线程、基础版的。要用于实际项目必须考虑以下方面并发与速率限制使用asyncio与playwright.async_api实现异步爬取或使用concurrent.futures实现线程池。必须严格遵守速率限制建议每页之间延迟3-10秒并发数控制在2-5个。可以设计一个“礼貌爬虫”类自动管理请求间隔。代理IP池这是防止IP被封的关键。需要集成一个代理IP服务并在每次请求或每隔一段时间轮换IP。代码上需要为Playwright的browser.new_context()或page.goto()设置代理服务器。请求头与指纹管理除了User-Agent还要随机化Accept-Language、Referer等头部。Playwright Context可以配置多个参数来模拟更真实的浏览器指纹。验证码处理遇到验证码时流程应暂停。可以尝试1) 使用Playwright截图并调用第三方打码API如2Captcha, Anti-Captcha进行识别然后自动填写2) 将验证码URL加入队列等待人工处理3) 最稳妥的方式是遇到验证码就停止该IP的爬取换IP或等待一段时间。会话与Cookie管理使用browser.new_context()创建独立上下文并持久化Cookies可以维持登录状态如果需要并减少每次请求的“新会话”特征。4.2 提升LLM提取的准确性与效率少样本学习Few-Shot Learning在Prompt中提供几个正确提取的示例Example能极大地引导LLM理解任务格式和难点。例如在Prompt开头加入2-3个“商品信息 - 输出JSON”的完整例子。后处理与校验LLM的输出并非100%可靠。需要编写后处理脚本进行校验例如检查extracted_price是否为数字检查condition是否在预设的枚举值中对于明显矛盾的信息如描述中说“屏幕碎裂”但has_defect为false进行标记或人工复核。缓存与成本优化相同的商品描述可能被多次抓取例如不同卖家卖同款。可以对描述文本计算MD5哈希值将哈希值与LLM提取结果存入本地缓存如SQLite。下次遇到相同描述时直接使用缓存结果避免重复调用昂贵的LLM API。使用函数调用Function Calling如果使用支持函数调用的模型如gpt-3.5-turbo-1106及以上版本可以定义严格的输出JSON Schema让模型以函数调用的形式返回数据格式更加稳定可靠。4.3 错误处理与健壮性设计一个健壮的系统必须能处理各种异常并从中恢复。重试机制对于网络请求失败、LLM API调用超时等临时性错误应实现指数退避的重试逻辑。断点续爬将待抓取的URL队列、已成功抓取和失败的URL状态持久化如用Redis或SQLite。程序重启后可以从上次中断的地方继续。监控与告警记录关键指标抓取成功率、LLM调用成功率、各字段提取的空值率、代理IP健康状态等。当错误率超过阈值时发送邮件或Slack告警。数据质量监控定期抽样检查LLM提取的数据与人工标注的结果进行对比计算准确率、召回率。如果发现某个字段如model提取质量持续下降可能需要优化Prompt或引入更复杂的后处理规则。5. 常见问题与实战排坑记录在实际开发和运行过程中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的解决方案。5.1 爬虫相关问题问题1页面加载不全抓取到的HTML里没有商品列表。排查首先检查page.goto()的wait_until参数。对于极度依赖JavaScript的页面networkidle可能还不够。打开无头模式headlessFalse肉眼观察页面加载过程。解决使用page.wait_for_selector()等待一个列表项特有的选择器出现。或者结合page.evaluate()执行一段JavaScript来触发滚动加载。问题2很快收到403错误或IP被封锁。排查请求频率是否过高请求头是否过于简单解决降低频率增加随机延迟time.sleep(random.uniform(5, 15))。完善请求头复制一个真实浏览器的完整请求头。使用代理这是必须的。购买可靠的住宅代理服务并在代码中集成轮换逻辑。模拟更真人行为在页面内随机移动鼠标、点击无关区域、在不同时间点滚动。问题3网站结构变化选择器失效。排查定期运行测试用例一旦解析失败率飙升很可能就是网站改版了。解决优先使用>