Day 28:项目 —— 网络爬虫 Agent
欢迎来到第二十八天!今天我们将综合运用前面所学的 Agent 知识,构建一个真正实用的网络爬虫 Agent。这个 Agent 能够接收用户提供的网址,自动抓取网页内容,并调用大模型生成简洁的摘要。通过这个项目,你将学会如何将外部网络请求封装为工具,如何处理网页内容的提取与清洗,以及如何让 Agent 在真实场景中自主决策。
一、今日学习目标
- 掌握使用
requests和BeautifulSoup抓取网页内容的基本方法。 - 学会将网页抓取功能封装为 LangChain 工具,供 Agent 调用。
- 处理网络请求中的常见问题:超时、编码、非 HTML 内容、反爬虫等。
- 构建一个 ReAct Agent,使其能够根据用户输入的网址,自主调用抓取工具并生成摘要。
- 理解真实项目中工具设计需考虑的鲁棒性。
二、详细实现步骤
步骤 1:安装依赖并准备环境
首先确保你的虚拟环境中已安装requests和beautifulsoup4。如果尚未安装,执行:
pipinstallrequests beautifulsoup4同时,确认之前已安装langchain、langchain-openai等库。
新建web_scraper_agent.py,导入所需模块:
importosimportrequestsfrombs4importBeautifulSoupfromdotenvimportload_dotenvfromlangchain_openaiimportChatOpenAIfromlangchain_core.toolsimporttoolfromlangchain.agentsimportcreate_react_agent,AgentExecutorfromlangchain_core.promptsimportPromptTemplatefromtypingimportAnnotated load_dotenv()# 初始化 LLMllm=ChatOpenAI(model="deepseek-chat",api_key=os.getenv("DEEPSEEK_API_KEY"),base_url="https://api.deepseek.com",temperature=0.3)步骤 2:定义网页抓取工具
我们定义一个工具函数fetch_webpage,它接收一个 URL,返回网页的纯文本内容。这个工具需要处理:
- HTTP 请求超时(设置合理超时时间)
- 状态码错误(如 404、500)
- 编码问题(尝试从响应头或内容中获取编码)
- 非 HTML 内容(如 PDF、图片,直接返回提示)
- 网页太大(截取前 N 字符,避免上下文溢出)
- 反爬虫(模拟 User-Agent)
@tooldeffetch_webpage(url:Annotated[str,"要抓取的网页完整 URL,例如 https://example.com"])->str:"""抓取指定网页的正文文本内容,用于后续摘要或信息提取。返回网页的纯文本,如果失败则返回错误信息。"""headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}try:# 发送 GET 请求,设置超时resp=requests.get(url,headers=headers,timeout=10)resp.raise_for_status()# 如果状态码不是 2xx,抛出异常exceptrequests.exceptions.Timeout:return"错误:请求超时,请检查网址是否正确或稍后重试。"exceptrequests.exceptions.ConnectionError:return"错误:无法连接到服务器,请检查网址或网络。"exceptrequests.exceptions.HTTPErrorase:returnf"错误:HTTP 错误,状态码{resp.status_code}。"exceptExceptionase:returnf"错误:请求失败,{e}"# 检查内容类型content_type=resp.headers.get("Content-Type","")if"text/html"notincontent_typeand"text/plain"notincontent_type:returnf"错误:网址返回的不是 HTML 或文本内容(Content-Type:{content_type}),无法提取。"# 处理编码ifresp.encodingisNoneorresp.encoding.lower()=='iso-8859-1':# 尝试从内容中检测编码resp.encoding=resp.apparent_encoding# 使用 BeautifulSoup 解析 HTMLsoup=BeautifulSoup(resp.text,"html.parser")# 移除脚本和样式标签forscriptinsoup(["script","style","nav","footer","header"]):script.decompose()# 提取正文文本text=soup.get_text(separator="\n",strip=True)# 截取前 8000 字符,避免内容过长max_length=8000iflen(text)>max_length:text=text[:max_length]+"\n...(内容过长,已截断)"returntext说明:
- 使用
Annotated为参数url提供描述。 - docstring 详细描述了工具功能、输入和输出,以及可能的错误情况。
- 设置了合理的请求头,模拟浏览器。
- 使用
resp.raise_for_status()快速检查 HTTP 错误。 - 处理了编码问题,使用
apparent_encoding自动检测。 - 移除了常见的非正文标签。
- 截断长文本,防止 Token 超限。
步骤 3:创建 ReAct Agent
使用与 Day 24 类似的 ReAct 提示词模板和create_react_agent。
react_template="""你是一个智能助手,可以通过调用工具来回答用户问题。你可以使用以下工具: {tools} 工具名称列表:{tool_names} 使用以下格式进行推理和行动: 问题:用户提出的问题 思考:你应该思考接下来要做什么 行动:要使用的工具名称,必须是 [{tool_names}] 中的一个 行动输入:传递给工具的输入 观察:工具返回的结果 ...(这个思考/行动/行动输入/观察可以重复 N 次) 思考:我现在知道最终答案了 最终答案:对用户问题的最终回答 开始! 问题:{input} 思考:{agent_scratchpad}"""prompt=PromptTemplate.from_template(react_template)# 创建 Agentagent=create_react_agent(llm,[fetch_webpage],prompt)# 创建执行器agent_executor=AgentExecutor(agent=agent,tools=[fetch_webpage],verbose=True,# 显示详细过程handle_parsing_errors=True,max_iterations=5)步骤 4:测试 Agent
运行以下测试用例,观察 Agent 的行为:
if__name__=="__main__":# 测试 1:正常网页摘要result=agent_executor.invoke({"input":"请抓取 https://www.example.com 的内容并总结要点"})print("最终答案:",result["output"])# 测试 2:无效网址result2=agent_executor.invoke({"input":"请抓取 https://this-is-a-fake-url-that-does-not-exist.com 并总结"})print("最终答案:",result2["output"])运行后,你会看到 Agent 先思考需要抓取网页,然后调用fetch_webpage,得到网页文本,最后根据文本生成摘要。对于无效网址,工具返回错误信息,Agent 会理解错误并告知用户无法访问。
步骤 5:优化与扩展
- 如果希望 Agent 在摘要前先确认网页是否有效,可以在工具中返回更详细的错误分类。
- 可以添加一个
summarize_text工具,但摘要由 LLM 直接完成即可。 - 如果需要抓取多个网页,Agent 可以在循环中多次调用工具。
三、常见问题与调试
Q1:网页编码乱码怎么办?
→ 在requests中设置resp.encoding = resp.apparent_encoding通常能解决大部分中文网页的编码问题。如果仍有乱码,可以尝试在BeautifulSoup构造时传入from_encoding参数。
Q2:请求被目标网站拒绝(403 Forbidden)。
→ 网站可能检测到非浏览器请求。可以尝试更换更完整的 User-Agent,或者添加其他请求头(如 Accept、Referer)。但请注意尊重网站的 robots.txt 和服务条款,不要频繁抓取。
Q3:网页内容太大,截断后可能丢失重要信息。
→ 可以只提取正文的特定部分(如文章主体),使用更智能的正文提取库(如readability-lxml)。但作为演示,截断前 8000 字符已经足够。实际项目中可根据需要调整长度。
Q4:BeautifulSoup 的get_text()提取出的文本包含大量空白和换行。
→ 我们使用了strip=True和separator="\n"来清理,但可能仍有连续换行。可以在返回前用正则替换多余空白:
importre text=re.sub(r'\n\s*\n','\n\n',text)Q5:Agent 在得到网页内容后,摘要生成质量不高。
→ 可以降低 LLM 温度,或在提示词中要求“提取关键信息,忽略广告和无关内容”。另外,截取的文本长度影响摘要质量,可以适当增加。
Q6:如何让 Agent 只抓取特定网站的链接?
→ 可以在工具描述中加入限制,例如“只抓取 example.com 域下的页面”,并在工具内部检查 URL 的域名。
四、今日总结与作业
今天你完成了:
- ✅ 使用
requests和BeautifulSoup实现了网页抓取功能。 - ✅ 将抓取功能封装为 LangChain 工具,并处理了多种网络错误。
- ✅ 构建了一个完整的 ReAct Agent,能够根据用户提供的网址自动抓取并生成摘要。
- ✅ 通过测试验证了 Agent 在正常情况和异常情况下的表现。
今日作业(必做):
- 在
fetch_webpage工具中添加对 PDF 链接的检测(如果 URL 以.pdf结尾,返回提示“不支持 PDF 文件”)。 - 修改工具,使其只提取网页中的
<article>或<main>标签内容(如果存在),否则回退到整个正文。观察提取质量是否提高。 - 尝试让 Agent 抓取一个新闻网页(例如 BBC 中文网的一篇文章),并让它用中文总结出 3 个要点。记录 Agent 的完整执行过程。
- (思考题)如果目标网页需要登录或包含动态加载的内容(JavaScript 渲染),你会如何扩展这个 Agent?请简述你的方案。
明日预告:我们将进入 RAG 的优化阶段,学习如何通过更合理的文档切分和检索策略来提升 RAG 的效果,为构建更强大的 Agent 记忆模块做准备。
有任何问题欢迎随时提问!