ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于大模型与Playwright的网页正文智能提取方案

基于大模型与Playwright的网页正文智能提取方案 1. 项目缘起从信息焦虑到自动化提取不知道你有没有过这样的经历在网上看到一篇技术文章或者一篇深度报道内容写得特别好你想把它保存下来方便以后查阅或者整理到自己的知识库里。但当你复制粘贴时问题就来了——页面上那些导航栏、侧边栏、广告、评论区、推荐阅读甚至页脚的各种版权声明全都一股脑地被复制了下来。你得到的是一份夹杂着大量无用信息的“脏”文本后期还得花时间去手动清理非常影响效率。这就是网页正文提取要解决的问题。它的目标很明确从一个结构复杂的HTML页面中精准地“抠”出我们真正需要的那部分核心内容也就是文章的标题和正文同时过滤掉所有无关的页面元素。对于经常需要做信息收集、内容分析或者知识管理的人来说一个可靠的提取工具就是生产力倍增器。市面上当然有现成的工具比如Readability的算法、Newspaper3k这样的Python库甚至一些在线API。但它们要么规则固定面对千变万化的网页结构时提取效果不稳定要么是闭源服务有调用次数和费用限制。今天我想分享一个我自己在用的方案利用字节跳动的Doubao-Seed-Evolving大模型结合Python和Playwright零成本搭建一个高准确率、可定制的网页正文提取工具。这个方案的核心思路是让大模型来理解网页的语义和结构而不仅仅是依赖预设的规则或标签从而获得更强的泛化能力和更高的准确度。整个工具链完全是免费的代码也足够简洁非常适合作为Python自动化实践的一个有趣项目。下面我就带你一步步实现它。2. 核心工具选型与原理拆解在动手写代码之前我们先搞清楚要用到哪些“兵器”以及它们各自扮演什么角色。整个流程可以概括为一个浏览器自动化工具负责“抓取”原始网页一个大模型负责“理解”并“提取”核心内容。2.1 Playwright新一代的浏览器自动化利器我们的第一步是获取目标网页完整的HTML内容。你可能会想到requests库但它只能获取初始的HTML对于大量依赖JavaScript动态渲染内容的现代网站如React、Vue构建的单页应用就无能为力了。页面上的文章内容可能是通过JS异步加载的直接用requests拿到的是一个几乎空白的骨架。因此我们需要一个能模拟真实用户操作、执行页面JavaScript并获取最终渲染结果的工具。这就是Playwright的用武之地。它由微软开发支持Chromium、Firefox和WebKit三大浏览器引擎。相比于老牌的SeleniumPlaywright在速度、稳定性和API设计上都有显著优势特别是它对异步操作的原生支持让代码写起来非常流畅。注意Playwright需要安装浏览器二进制文件。虽然第一次运行时会自动下载但在某些网络环境下可能会比较慢。你可以通过设置环境变量PLAYWRIGHT_DOWNLOAD_HOST来使用国内镜像加速例如设置为https://npmmirror.com/mirrors/playwright/。它的工作原理是启动一个无头浏览器没有图形界面加载目标网页等待页面完全加载包括网络请求和JS执行然后将此时的DOM状态完整地提供给我们。这样我们拿到的HTML就是用户最终看到的样子包含了所有动态生成的内容。2.2 Doubao-Seed-Evolving免费的“智能大脑”拿到完整的HTML后最关键的一步是如何从中识别出正文。传统方法有基于规则/启发式算法比如Readability通过计算标签的密度、链接数量、类名等特征来猜测正文区域。优点是快缺点是对不同网站模板的适应性差规则需要不断维护。基于机器学习/深度学习训练模型来识别正文区域。效果通常更好但需要标注数据和训练成本。而我们这次采用的是一种更“取巧”且强大的方法利用大语言模型LLM的语义理解能力。Doubao-Seed-Evolving豆包-Seed-Evolving是字节跳动提供的免费大模型API服务。我们不需要关心复杂的模型训练只需要将网页的HTML内容或经过简化的DOM文本连同我们的指令“请提取文章的标题和正文”一起发送给它的API。大模型会像一个人一样去“阅读”这段HTML理解其中哪些部分是导航哪些是广告哪些才是真正的文章内容然后按照我们要求的格式比如Markdown将提取出的内容返回。这种方法的核心优势在于泛化能力极强。无论网页结构多么奇特只要模型能理解语义就能大概率正确提取省去了我们编写和维护复杂规则的成本。2.3 为什么是Markdown格式在摘要描述和相关热词中Markdown被多次提及。我们选择将提取的正文输出为Markdown格式是因为MD是一种轻量级标记语言它结构清晰支持标题、列表、代码块、链接、图片等能很好地保留原文的格式信息。通用性强几乎所有的笔记软件如Obsidian、Notion、博客平台和编辑器都支持Markdown。便于后续处理Markdown是纯文本可以轻松地转换为HTML、PDF、Word等其他格式也可以直接放入支持MD的系统中进行管理。让大模型直接输出Markdown相当于一步完成了“提取”和“格式化”两件事。3. 环境搭建与依赖安装工欲善其事必先利其器。我们先来把开发环境配置好。我假设你已经在电脑上安装了Python3.7或以上版本和一个顺手的代码编辑器如VSCode。首先打开你的终端命令行创建一个新的项目目录并进入该目录。mkdir web-content-extractor cd web-content-extractor然后创建一个Python虚拟环境来隔离项目依赖。这是最佳实践可以避免不同项目之间的包版本冲突。# 使用 venv 创建虚拟环境 python -m venv venv # 激活虚拟环境 # 在 Windows 上 venv\Scripts\activate # 在 macOS/Linux 上 source venv/bin/activate激活后你的命令行提示符前应该会出现(venv)字样。接下来安装我们所需的Python包。我们将主要用到playwright、用于调用豆包API的openai库因为豆包API兼容OpenAI格式以及用于处理HTTP请求的httpx或requests。# 安装 Playwright 核心库 pip install playwright # 安装 Playwright 的浏览器Chromium、Firefox、WebKit。这一步耗时较长。 playwright install chromium # 通常只安装 Chromium 就足够了 # 安装 openai 库用于调用兼容OpenAI API格式的服务 pip install openai # 安装 httpx 作为HTTP客户端可选但性能更好 pip install httpx这里有一个关键的细节豆包Seed-Evolving模型的API端点endpoint和认证方式与OpenAI官方略有不同。我们需要知道它的API基础地址和如何获取API Key。获取API Key你需要访问豆包开放平台通常可以在字节跳动的相关开发者站点找到注册账号并创建一个应用从而获得一个API Key。这个过程是免费的但可能需要实名认证。请妥善保管你的API Key不要泄露。API基础地址豆包提供的API地址通常不是https://api.openai.com/v1而是一个特定的URL例如https://ark.cn-beijing.volces.com/api/v3具体地址请以豆包平台最新文档为准。4. 实战代码分步构建提取工具环境准备好了钥匙API Key也拿到了现在开始写代码。我会将整个过程拆解成几个函数让逻辑更清晰。4.1 第一步用Playwright抓取动态渲染的HTML我们先写一个函数负责用Playwright打开网页并获取渲染后的HTML内容。这里我们会用到异步编程async/await因为Playwright的很多操作都是异步的这样能提升效率。import asyncio from playwright.async_api import async_playwright async def fetch_html_with_playwright(url: str, timeout: int 30000) - str: 使用 Playwright 获取完全渲染后的页面 HTML。 参数: url: 目标网页地址 timeout: 页面加载超时时间毫秒 返回: 页面的完整 HTML 字符串 async with async_playwright() as p: # 启动一个 Chromium 浏览器实例使用无头模式不显示界面 browser await p.chromium.launch(headlessTrue) # 创建一个新的浏览器上下文类似于一个独立的会话 context await browser.new_context() # 在新上下文中打开一个页面 page await context.new_page() try: # 导航到目标URL并等待网络状态达到‘load’即HTML文档加载完成 await page.goto(url, wait_untilnetworkidle, timeouttimeout) # 可选可以在这里执行一些滚动操作确保懒加载的内容被触发 # await page.evaluate(window.scrollTo(0, document.body.scrollHeight)) # await page.wait_for_timeout(2000) # 等待2秒让内容加载 # 获取页面主体部分的HTML也可以获取整个document # 这里获取整个文档的 outerHTML html_content await page.content() return html_content except Exception as e: print(f抓取页面时发生错误: {e}) return finally: # 无论如何最后都要关闭浏览器释放资源 await browser.close() # 同步函数包装方便在非异步环境中调用 def fetch_html(url: str) - str: 同步接口内部调用异步函数 return asyncio.run(fetch_html_with_playwright(url))代码解读与注意事项wait_untilnetworkidle这个参数非常有用。它告诉Playwright在至少500毫秒内没有超过2个网络连接时才认为页面加载完成。这比单纯的load事件更能确保动态内容加载完毕。错误处理网络不稳定、页面本身错误、超时等都可能导致抓取失败。我们用try...except包裹核心代码并在失败时返回空字符串避免程序崩溃。资源释放finally块中的await browser.close()至关重要。确保即使发生异常浏览器进程也会被关闭防止内存泄漏。反爬策略一些网站会检测自动化工具。Playwright虽然比Selenium更隐蔽但并非完全无法检测。对于这类网站可能需要更复杂的策略如设置更真实的user-agent、使用代理、添加随机延迟等。本项目以功能演示为主暂不深入讨论反反爬。4.2 第二步设计给大模型的“指令”Prompt直接向大模型扔过去一个几万行的HTML字符串不仅效率低还可能因为超出模型上下文长度而失败。我们需要对HTML进行一些预处理并设计一个清晰的指令Prompt。预处理的目标是精简HTML保留可能包含正文的主要区域。一个简单有效的方法是利用Playwright已经获取的DOM通过CSS选择器移除一些明显无关的标签如script,style,nav,footer,aside等。但更通用的做法是直接将body标签内的文本内容提取出来这已经能过滤掉所有标签属性大大减少了噪音。from html import unescape import re def extract_main_text_from_html(html: str) - str: 从HTML中提取主要的文本内容用于发送给大模型。 这是一个简化的预处理目的是减少无关噪音。 if not html: return # 使用正则简单提取 body 标签内的内容不包含标签本身 # 这是一个非常粗糙的方法仅用于演示。在实际应用中你可能需要使用更健壮的HTML解析器如lxml或bs4。 body_match re.search(rbody[^]*(.*?)/body, html, re.DOTALL | re.IGNORECASE) if not body_match: # 如果没有找到body标签返回整个HTML的文本部分去除标签 text re.sub(r[^], , html) else: body_content body_match.group(1) # 移除 script 和 style 标签及其内容 body_content re.sub(r(script|style)[^]*.*?/\1, , body_content, flagsre.DOTALL | re.IGNORECASE) # 移除所有HTML标签只保留文本 text re.sub(r[^], , body_content) # 合并多个空白字符为一个空格并取消HTML实体转义如 nbsp; - 空格 text re.sub(r\s, , text).strip() text unescape(text) # 截断文本避免超出模型上下文限制例如限制在12000字符内 max_length 12000 if len(text) max_length: print(f警告提取的文本过长{len(text)}字符已截断至前{max_length}字符。) text text[:max_length] ...[内容已截断] return text接下来是核心的Prompt设计。我们需要明确地告诉大模型我们的任务、输入格式和输出格式。def build_extraction_prompt(web_text: str) - str: 构建用于正文提取的提示词Prompt。 prompt f 你是一个专业的网页内容提取助手。你的任务是从一段从网页中提取的文本中识别出文章的**主标题**和**核心正文内容**并忽略所有无关信息如导航栏、侧边栏、广告、页脚、评论、推荐阅读、版权声明等。 请遵循以下要求 1. **输出格式**必须且只能输出一个标准的 Markdown 文档。 2. **文档结构** - 第一行是文章的主标题使用一级标题格式 (# 标题)。 - 从第二行开始是文章的完整正文内容。 3. **内容处理** - 只输出最终的文章内容不要输出任何额外的解释、分析或提示词本身。 - 保留原文中的段落结构、列表、加粗、斜体等语义格式并用对应的Markdown语法表示。 - 如果原文中有图片请以Markdown图片语法 ![描述](图片URL) 的形式保留并确保URL是完整的。 - 如果无法从给定文本中清晰识别出文章标题和正文请输出 # 提取失败并在第二行简要说明原因。 以下是从网页中获取的文本内容{web_text}现在请开始提取并输出Markdown格式的文章内容 return prompt这个Prompt有几个关键点角色定义让模型进入“专业助手”的角色。任务明确清晰说明了要“识别标题和正文忽略无关信息”。格式严格强制要求输出纯Markdown并规定了标题的格式。这有利于我们后续程序化地处理结果。容错处理给出了提取失败时的输出指令让程序有判断依据。4.3 第三步调用Doubao-Seed-Evolving API现在我们将预处理后的文本和精心设计的Prompt发送给大模型。这里我们使用openai这个库因为它提供了通用的接口只要配置好不同的base_url和api_key就可以对接多种兼容OpenAI API格式的服务。from openai import OpenAI import os def extract_content_with_llm(cleaned_text: str, api_key: str, base_url: str, model: str doubao-seed-evolving) - str: 调用 Doubao-Seed-Evolving API 提取正文内容。 参数: cleaned_text: 预处理后的网页文本 api_key: 豆包平台的API Key base_url: API端点地址 model: 模型名称默认为 doubao-seed-evolving 返回: 大模型返回的文本内容应为Markdown格式 # 构建完整的Prompt prompt build_extraction_prompt(cleaned_text) # 初始化OpenAI客户端指向豆包的API地址 client OpenAI( api_keyapi_key, base_urlbase_url, # 例如 https://ark.cn-beijing.volces.com/api/v3 ) try: response client.chat.completions.create( modelmodel, messages[ {role: user, content: prompt} ], temperature0.1, # 温度设低使输出更确定、更稳定 max_tokens4000, # 根据模型上下文和预期输出长度调整 ) # 提取模型返回的消息内容 extracted_content response.choices[0].message.content.strip() return extracted_content except Exception as e: print(f调用大模型API时发生错误: {e}) return f# API调用失败\n错误信息: {e}关键参数解析temperature控制输出的随机性。范围0到2。值越低如0.1输出越确定、可重复值越高输出越有创造性、不可预测。对于提取任务我们需要高确定性所以设为较低值。max_tokens限制模型生成的最大令牌数。一个中文汉字大约1-2个token。需要根据模型的最大上下文窗口和你输入文本的长度来设置确保留出足够空间给输出。豆包Seed-Evolving模型的具体限制需查阅其官方文档。4.4 第四步整合与主流程最后我们将上述所有步骤串联起来形成一个完整的工具函数。同时添加一些简单的文件保存功能。import json from pathlib import Path def extract_article_to_markdown(url: str, api_key: str, base_url: str, output_dir: str ./output) - dict: 主函数从URL提取文章并保存为Markdown文件。 返回一个字典包含状态和文件路径等信息。 result {url: url, success: False, output_path: None, error: None} print(f开始处理: {url}) # 1. 抓取HTML print(步骤1: 使用Playwright抓取页面...) html fetch_html(url) if not html: result[error] 无法获取页面HTML print(错误页面抓取失败。) return result # 2. 预处理提取主要文本 print(步骤2: 预处理HTML提取主要文本...) cleaned_text extract_main_text_from_html(html) if not cleaned_text: result[error] 从HTML中提取文本失败 print(错误文本提取失败。) return result print(f预处理后文本长度: {len(cleaned_text)} 字符) # 3. 调用大模型提取结构化内容 print(步骤3: 调用大模型进行智能提取...) markdown_content extract_content_with_llm(cleaned_text, api_key, base_url) # 4. 检查输出并保存 if markdown_content.startswith(# 提取失败) or markdown_content.startswith(# API调用失败): result[error] markdown_content print(f提取失败: {markdown_content[:100]}...) else: # 创建输出目录 Path(output_dir).mkdir(parentsTrue, exist_okTrue) # 从Markdown内容中提取标题作为文件名取第一行#后的内容 first_line markdown_content.split(\n)[0] if first_line.startswith(# ): title_for_filename first_line[2:].strip().replace(/, _).replace(\\, _)[:50] # 简单清理 else: title_for_filename extracted_article # 生成安全的文件名 from datetime import datetime timestamp datetime.now().strftime(%Y%m%d_%H%M%S) safe_filename f{timestamp}_{title_for_filename}.md output_path Path(output_dir) / safe_filename # 保存Markdown文件 with open(output_path, w, encodingutf-8) as f: f.write(markdown_content) result[success] True result[output_path] str(output_path) print(f成功文章已保存至: {output_path}) return result # 示例用法 if __name__ __main__: # 请替换为你自己的API Key和Base URL YOUR_API_KEY sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 你的豆包API Key YOUR_API_BASE https://ark.cn-beijing.volces.com/api/v3 # 豆包API地址请以官方文档为准 TARGET_URL https://example.com/some-article # 你想提取的文章URL # 执行提取 res extract_article_to_markdown(TARGET_URL, YOUR_API_KEY, YOUR_API_BASE) # 打印结果摘要 print(json.dumps(res, indent2, ensure_asciiFalse))5. 效果测试、优化与踩坑实录代码写完了是骡子是马得拉出来遛遛。我用自己的API Key测试了几个不同类型的网站。5.1 测试案例与效果分析我选取了三个有代表性的网站进行测试技术博客静态渲染一个用Hexo搭建的个人技术博客。Playwright顺利抓取大模型完美提取了标题和正文代码块也以Markdown格式保留得很好。新闻门户动态加载一个主流新闻网站的某篇报道。页面有大量侧边栏、相关推荐和评论。我们的工具成功过滤了这些干扰提取出了干净的新闻正文。但偶尔会把文章末尾的“编辑XXX”也保留下来不过这无伤大雅。论坛帖子复杂交互一个技术论坛的帖子页面包含楼主内容、多层回复、用户签名档。这是挑战最大的。大模型有时会把最热门的一条回复误判为正文的一部分。这说明对于极度非线性的页面结构当前简单的Prompt可能不够。总体效果对于典型的博客、新闻文章页面提取准确率非常高估计在90%以上远超传统的基于规则的方法。输出格式规整直接可用于归档。5.2 常见问题与优化策略在实际运行中你可能会遇到以下问题这里是我的解决方案问题一大模型返回内容不符合Markdown格式要求。现象返回的内容开头或结尾多了一些解释性文字如“好的我将为您提取...”。原因Prompt的指令不够强硬或者模型没有严格遵守。解决强化Prompt中的指令。可以使用“必须且只能输出一个标准的Markdown文档”、“不要输出任何额外的解释”等措辞。也可以在代码后处理中用正则匹配第一个#开头到文件尾的内容。问题二提取某些长文章时返回内容不完整或中途截断。现象文章后半部分丢失了。原因可能是max_tokens参数设置得太小模型输出被截断也可能是输入给模型的cleaned_text本身就被截断了我们的预处理函数有长度限制。解决检查并调大max_tokens值。优化预处理对于超长文章可以尝试只提取article标签、或通过计算文本密度等启发式方法先定位正文区域再送给模型而不是发送全文。这需要更复杂的HTML解析如使用BeautifulSoup。问题三API调用速度慢或网络超时。现象程序卡在extract_content_with_llm函数很久然后报错。原因网络延迟或模型服务端响应慢。解决为httpx或openai客户端设置合理的超时参数。考虑加入重试机制如tenacity库对瞬时的网络波动进行容错。如果批量处理大量URL需要加入速率限制避免对API服务造成冲击。问题四面对特定网站提取效果差。现象某个网站总是提取失败或提取错误。原因该网站结构特殊或者我们的预处理去掉了关键信息。解决这是“大模型规则”混合策略发挥优势的地方。你可以为这个特定网站编写一个小的预处理函数在调用通用流程前先用这个特定函数处理。例如如果知道这个网站的正文都在div classpost-content里就可以先用BeautifulSoup提取这个div的内容再送给大模型。这样就实现了“规则保底AI增强”的灵活策略。5.3 进阶优化思路如果你想把这个工具变得更强大、更实用可以考虑以下方向并行处理使用asyncio或concurrent.futures同时处理多个URL大幅提升批量提取效率。注意要合理控制并发数避免被封IP或超过API速率限制。缓存机制对已经成功提取的URL进行缓存将结果保存到本地数据库或文件下次再处理同一URL时直接读取缓存节省API调用次数和时间。内容后处理对模型提取出的Markdown进行二次清洗比如统一标题级别、修复错误的链接、去除多余的空行等。图形界面GUI使用tkinter、PyQt或streamlit为工具制作一个简单的图形界面方便非技术用户使用。集成到工作流将工具作为命令行脚本结合cronLinux或任务计划程序Windows定时抓取特定网站的最新文章并自动保存到笔记软件如Obsidian的指定目录。这个项目最让我满意的地方在于它用一种相对简单的方式结合了浏览器自动化的“抓取力”和大语言模型的“理解力”解决了一个实际且普遍的需求。虽然它依赖一个外部的API但鉴于目前豆包提供的免费额度对于个人和小规模使用来说是完全可行的。整个过程也是对Python异步编程、API调用和Prompt工程的一次很好的实践。
返回列表