LangChain 实战:URL 加载与网页内容爬虫封装
Web 检索是 AI 大模型应用的一个热门方向。其涉及的主要步骤如下:
- 用户提问,联网检索
- 通过 URLs 记载网页 HTML 数据
- 加载到的数据通过转换,获取关注的内容,形成文本
- 对文本进行分块、向量化、存储
- 调用大模型进行总结、答案生成
这其实就是 RAG(Retrieval-Augmented Generation)的基本流程,只不过知识库不再局限在你自己的知识库,而是利用在线检索,搜罗互联网上的数据作为相关知识。
搜罗数据的过程,可以有两种方法:一种是调用检索的 API(例如 GoogleSearch API),直接获取检索结果;另一种方法是靠爬虫,将网页数据抓取下来,存入向量数据库使用。
本文重点探讨基于 LangChain 框架的爬虫相关使用方法。
0. LangChain 接口架构
LangChain 中,将爬虫功能分成了两个核心模块:Loading 和 Transforming。
- Loading 模块:负责将 URL 加载转换成 HTML 内容。封装的类包括
AsyncHtmlLoader 类、AsyncChromiumLoader 类等。
- Transforming 模块:负责将 HTML 内容转换成需要的纯文本。封装的类包括
HTML2Text 类、BeautifulSoup 类等。
0.1 Loading 模块简介
- AsyncHtmlLoader:使用 aiohttp 库生成异步 HTTP 请求,适用于更简单、轻量级的抓取场景。它适合静态页面,响应速度快,资源消耗低。
- AsyncChromiumLoader:使用 Playwright 启动 Chromium 实例,该实例可以处理 JavaScript 渲染和更复杂的 Web 交互。Chromium 是 Playwright 支持的浏览器之一,Playwright 是一个用于控制浏览器自动化的库。此方式适合动态加载内容的网站。
0.2 Transforming 模块简介
- HTML2Text:将 HTML 内容直接转换为纯文本,而无需任何特定的标记操作。它最适合于目标是提取人类可读文本而不需要操作特定 HTML 元素的场景。
- Beautiful Soup:对 HTML 内容提供了更细粒度的控制,支持特定的标记提取、删除和内容清理。它适用于需要提取特定信息并根据需要清理 HTML 内容的情况。
1. 快速上手 - Quick Start
1.1 环境准备
在开始之前,请确保已安装必要的依赖库:
pip install langchain langchain-community beautifulsoup4 playwright
playwright install chromium
1.2 Demo 代码
以下示例演示了如何使用 AsyncChromiumLoader 加载 URL,并使用 BeautifulSoupTransformer 提取特定标签内容。
urls = ["https://mp.weixin.qq.com/s/Zklc3p5uosXZ7XMHD1k2QA"]
from langchain_community.document_loaders import AsyncChromiumLoader
from langchain_community.document_transformers import BeautifulSoupTransformer
loader = AsyncChromiumLoader(urls)
html_docs = loader.load()
print("============= html =====================")
for doc in html_docs:
print(doc.page_content[:500])
bs_transformer = BeautifulSoupTransformer()
docs_transformed = bs_transformer.transform_documents(html_docs, tags_to_extract=["span"])
print("================= doc_transformed ===============")
for doc in docs_transformed:
print(doc.page_content)
1.3 代码解释
- 加载器初始化:程序使用了
AsyncChromiumLoader 类来加载 URL 为 HTML 内容。
- 注意:
AsyncChromiumLoader 接收的参数是一个 URL 数组,这意味着它可以同时加载多个 URL,提高并发效率。
- 转换器应用:使用了
BeautifulSoupTransformer 类作为 transform 来将 HTML 内容转换成文本内容。
- 注意:
transform_documents 函数中的 tags_to_extract 参数,指定了将 HTML 中的什么 tag 内的内容提取成文本。默认情况下可能只提取部分可见文本。
1.4 效果分析与改善
原始提取结果
URL 转 HTML 内容后,抓出来的其实是 HTML 脚本语言及结构代码,并不是我们想要的纯净文本信息。所以后面必须有个 Transform 步骤。
经过 Transform 步骤后,出现了我们需要的文本信息。但原网页内容对比发现,提取的文本丢失了很多内容。主要原因是 tags_to_extract 参数设置过于单一。
HTML 常用标签说明
HTML 脚本语言的常用文本标签大体有:
<h1> 到 <h6>:标题标签,用于定义标题的级别,<h1> 是最高级别的标题,依次递减。
<p>:段落标签,用于定义段落。
<a>:链接标签,用于创建超链接,通过 href 属性指定链接目标。
<span>:内联容器标签,用于包裹一小段文本或行内元素。
<div>:块级容器标签,用于组合和布局其他元素。
<strong>:强调文本标签,使文本加粗显示。
<em>:强调文本标签,使文本以斜体显示。
<br>:换行标签,用于插入一个换行符。
<code>:代码块标签,常用于展示编程代码。
优化提取策略
要改善上面的提取结果,使其能提取出更多的文本,我们可以修改提取的 tags 参数,如下,提取出 span, code, p 的内容:
docs_transformed = bs_transformer.transform_documents(
html_docs,
tags_to_extract=["span", "code", "p", "div", "article"]
)
修改后运行效果通常会将里面的文字和代码全部提取出来(虽然还有些特殊符号,不过没关系,后面可以再过滤一层去掉)。
那上面我是怎么确认要提取 "span", "code", "p" 这三个 tag 内的文本的呢?具体操作步骤如下:
- 打开你要爬取的网页,按 F12 打开网页调试工具。
- 找到'元素'选项卡,然后点击左上角的图标(选择元素模式)。
- 将鼠标悬浮在你想提取的文字上面,它就会自动展示当前文字所在的标签 tag 是什么。
- 将这些 tag 全部填到参数里,就 OK 了。
2. 高级方法 - 使用大模型的 Function Calling 提取所需文本
该方法是在以上方法的基础上,在得到文本后,再利用大模型,从文本中二次提取出所关注的文本内容。
这种方法的好处在于,对于网页内容和结构变化时,我们不需要再去频繁的调整提取 tag 等参数,而是最后利用大模型统一提取关心内容即可。这对于非结构化程度高或结构多变的网页非常有效。
2.1 Demo 代码
def scraping_with_extraction():
from langchain_openai import ChatOpenAI
from langchain.chains import create_extraction_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import AsyncChromiumLoader
from langchain_community.document_transformers import BeautifulSoupTransformer
import pprint
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0613")
schema = {
"properties": {
"article_title": {"type": "string"},
"article_content": {"type": "string"},
"article_example_python_code": {"type": "string"},
},
"required": ["article_title", "article_content", "article_example_python_code"],
}
def extract(content: str, schema: dict):
return create_extraction_chain(schema=schema, llm=llm).run(content)
def scrape_with_playwright(urls, schema):
loader = AsyncChromiumLoader(urls)
docs = loader.load()
bs_transformer = BeautifulSoupTransformer()
docs_transformed = bs_transformer.transform_documents(
docs, tags_to_extract=["span", "code", ]
)
()
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=, chunk_overlap=
)
splits = splitter.split_documents(docs_transformed)
extracted_content = extract(schema=schema, content=splits[].page_content)
pprint.pprint(extracted_content)
extracted_content
urls = []
extracted_content = scrape_with_playwright(urls, schema=schema)
scraping_with_extraction()
2.2 代码详解
- Schema 定义:定义一个模式来指定要提取的数据类型。在这里,key 的名称很重要,因为这是告诉 LLM 我们想要什么样的信息。所以,尽可能详细。
- 文本分块:代码中还做了 split,将全部文本分块了,可能是为了避免超出 LLM 的 Token 长度限制。
- 提取链:最重要、最灵魂的几句,将文本内容,和模式传入
create_extraction_chain 来获取输出。
2.3 优化 Schema 描述
create_extraction_chain 源码解析显示,其做的事儿比较简单,就是通过 _get_extraction_function 函数将上面我们定义的 schema 转换成了 function calling 中的 function 的结构。然后创建了一个 LLMChain 链。
它的内置 Prompt 让大模型提取出在 information_extraction 函数中定义的 properties 相关信息。很明显,这个 Prompt 比较简单,要想大模型提取的结果好,information_extraction 函数中定义的 properties 必须要尽可能详细。可以在参数下面加一个描述来详细描述该参数的含义。
仿照这个方法,我们可以优化 schema:
schema = {
"properties": {
"文章标题": {"type": "string", "description": "文章题目,通常为一级标题 h1 内容"},
"文章正文全部内容": {"type": "string", "description": "文章的正文内容,不要包含 Python 代码,只输出文字,去除无关广告"},
"文章中的示例 Python 代码": {"type": "string", "description": "文章中的 Python 代码,只输出代码,用 markdown 格式输出,可能存在多段代码,多段代码之间分开"},
},
"required": ["文章标题", "文章正文全部内容", "文章中的示例 Python 代码"],
}
还有一种方法,create_extraction_chain 函数的参数接收一个额外的 Prompt,我们也可以通过此参数来调优提取结果。然而最终结果并没有多少改善… 待继续研究怎么优化。
该方法有点过于依赖大模型的能力,并且会大量消耗 Token,目前还没看到有实际的落地效果,处于探索阶段。
3. 最佳实践与注意事项
在实际生产环境中,直接使用上述代码可能会遇到各种网络问题或反爬机制。以下是建议的最佳实践:
3.1 请求头设置
为了模拟真实浏览器行为,避免被服务器封禁,建议在 Loader 中设置 User-Agent 和其他必要 Header。
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept-Language": "zh-CN,zh;q=0.9"
}
3.2 错误处理与重试
网络请求具有不稳定性,应增加重试机制和异常捕获。
import time
from requests.exceptions import RequestException
try:
loader = AsyncChromiumLoader(urls)
html_docs = loader.load()
except Exception as e:
print(f"Loading failed: {e}")
time.sleep(5)
3.3 性能优化
- 并发控制:不要一次性加载过多 URL,建议分批处理,避免触发 IP 封锁。
- 缓存机制:对于相同 URL 的请求,考虑本地缓存 HTML 内容,减少重复请求。
- Token 管理:在使用 LLM 提取时,严格控制输入 token 数量,优先使用切片策略。
4. 方案对比与总结
| 特性 | 传统标签提取 (BS/HTML2Text) | LLM Function Calling 提取 |
|---|
| 准确性 | 依赖标签结构,结构变动易失效 | 语义理解强,适应结构变化 |
| 成本 | 极低,本地计算 | 高,消耗 Token 和 API 费用 |
| 速度 | 快 | 慢,受限于 LLM 响应时间 |
| 维护性 | 需定期调整 tags | 仅需更新 Schema 描述 |
| 适用场景 | 结构稳定的新闻站、博客 | 复杂、动态、非结构化网页 |
结论
本文详细介绍了基于 LangChain 实现 URL 网页内容抓取与处理的技术方案。涵盖了 RAG 流程中的检索步骤,重点讲解了 Loading 模块与 Transforming 模块的配合使用。通过代码示例演示了如何将 HTML 转换为纯文本,并利用浏览器自动化工具处理动态渲染页面。此外,还探讨了结合大模型 Function Calling 进行二次提取的高级方法,对比了传统标签提取与 AI 提取的优劣。
对于大多数常规场景,推荐使用 AsyncChromiumLoader 配合 BeautifulSoupTransformer 进行基础清洗,再根据需求决定是否引入 LLM 进行深度结构化提取。开发者应根据实际项目的成本预算和对准确性的要求,选择合适的技术路径。