当前位置: 首页 > news >正文

# 如何将包含 Document 对象的字符串转换为 List[Document]?

如何将包含 Document 对象的字符串转换为 List[Document]?

问题背景

在 LangChain 开发中,我们经常需要处理Document对象。但有时候你会拿到一个"看起来像列表、实际上是字符串"的东西——比如从数据库读取、从 API 返回、或从日志中提取的内容:

# 你拿到的可能是这样一个字符串,而不是真正的 listdoc_string="[Document(metadata={'pk': 12, 'page': 2}, page_content='费用报销内容...'), Document(metadata={'pk': 27, 'page': 2}, page_content='...')]"

你的目标是把它变成真正的list[Document]。本文介绍3 种方案,从最简单到最健壮,覆盖不同场景。


先认识 Document 对象

在 LangChain 中,Document的结构非常简单:

fromlangchain_core.documentsimportDocument doc=Document(metadata={"pk":12,"page":2},page_content="这是文档的文本内容")

它只有两个核心字段:

  • metadata:字典,存储元数据(主键、页码等)
  • page_content:字符串,存储实际文本内容

当它被str()repr()转成字符串时,输出格式是Document(metadata={...}, page_content='...')


方案一:eval() —— 最直接(需注意安全风险)

如果字符串中引用的是真实的Document类,且你的环境中已经导入了该类,最简单的方法就是直接eval()

fromlangchain_core.documentsimportDocument doc_string="[Document(metadata={'pk': 12, 'page': 2}, page_content='...')]"# 直接 evaldocs=eval(doc_string)print(type(docs))# <class 'list'>print(type(docs[0]))# <class 'langchain_core.documents.base.Document'>print(docs[0].metadata)# {'pk': 12, 'page': 2}

优点

  • 一行代码搞定,简单粗暴
  • 完整还原对象类型

缺点

  • eval()会执行字符串中的任意 Python 代码,存在安全风险
  • 仅适用于完全可信的数据来源

安全提示:如果字符串来自用户输入、网络请求等不可信来源,绝对不要使用 eval()


方案二:正则 + ast.literal_eval —— 安全且通用

ast.literal_eval是 Python 标准库提供的安全解析函数,它只解析 Python 字面量(字符串、数字、字典、列表等),不会执行任何代码。

但问题是:ast.literal_eval无法直接解析Document(...)这种自定义类的构造调用。所以我们需要分两步

  1. 用正则提取每个Document(...)块中的metadatapage_content
  2. ast.literal_eval安全解析提取出的字面量
importreimportastfromlangchain_core.documentsimportDocumentdefparse_documents_from_string(s:str)->list[Document]:""" 将包含 Document(...) 的字符串解析为 list[Document] 使用正则提取 + ast.literal_eval 安全解析 """results=[]idx=0whileTrue:start=s.find("Document(",idx)ifstart==-1:break# 通过括号深度匹配,找到完整的 Document(...)depth=1i=start+len("Document(")whilei<len(s)anddepth>0:ifs[i]=='(':depth+=1elifs[i]==')':depth-=1i+=1doc_str=s[start:i]# 提取 metadata(字典格式)meta_match=re.search(r"metadata=(\{.*?\})",doc_str,re.DOTALL)# 提取 page_content(引号包裹的字符串,到块结尾)content_match=re.search(r"page_content=(.*?)(?:\)\s*$)",doc_str,re.DOTALL)ifmeta_matchandcontent_match:metadata=ast.literal_eval(meta_match.group(1))page_content_str=content_match.group(1).strip().rstrip(')')page_content=ast.literal_eval(page_content_str)results.append(Document(metadata=metadata,page_content=page_content))idx=ireturnresults

核心原理解析

为什么要用"括号深度匹配"?

page_content的值本身可能包含括号,比如page_content='费用报销(凭发票)'。如果用简单的正则Document\(([^)]*)\),遇到第一个)就会提前截断。

括号深度匹配的原理是:遍历字符,遇到(加一,遇到)减一,当深度归零时就找到了完整的闭合位置:

Document(metadata={'pk': 12}, page_content='(USD)') ^ ^ start end (depth=0)

为什么用 ast.literal_eval 而不是 eval?

特性eval()ast.literal_eval()
执行代码
解析字面量
安全性低(可执行任意代码)高(仅解析字面量)
适用场景可信数据任意数据

使用示例

doc_string="""[Document(metadata={'pk': 12, 'page': 2}, page_content='费用报销内容...'), Document(metadata={'pk': 27, 'page': 2}, page_content='奖惩制度...')]"""docs=parse_documents_from_string(doc_string)fordocindocs:print(f"pk={doc.metadata['pk']}, page={doc.metadata['page']}")print(f"content:{doc.page_content[:50]}...")print()

输出:

pk=12, page=2 content: 费用报销内容... pk=27, page=2 content: 奖惩制度...

方案三:处理无依赖场景(自定义 Document 类)

如果你的环境中没有安装 LangChain,但你需要解析这种字符串并重建类似结构,可以自定义一个等价的 Document 类:

importreimportastfromdataclassesimportdataclass@dataclassclassDocument:"""模拟 langchain Document 的简化版本"""metadata:dictpage_content:strdef__repr__(self):returnf"Document(metadata={self.metadata}, page_content={repr(self.page_content)})"defparse_documents_from_string(s:str,doc_class=Document)->list:"""解析字符串为 list[Document],支持自定义 Document 类"""results=[]idx=0whileTrue:start=s.find("Document(",idx)ifstart==-1:breakdepth=1i=start+len("Document(")whilei<len(s)anddepth>0:ifs[i]=='(':depth+=1elifs[i]==')':depth-=1i+=1doc_str=s[start:i]meta_match=re.search(r"metadata=(\{.*?\})",doc_str,re.DOTALL)content_match=re.search(r"page_content=(.*?)(?:\)\s*$)",doc_str,re.DOTALL)ifmeta_matchandcontent_match:metadata=ast.literal_eval(meta_match.group(1))page_content_str=content_match.group(1).strip().rstrip(')')page_content=ast.literal_eval(page_content_str)results.append(doc_class(metadata=metadata,page_content=page_content))idx=ireturnresults

这个版本通过doc_class参数支持传入任意 Document 类,无论你用的是 LangChain 的 Document 还是自定义的。


方案对比

方案安全性健壮性复杂度适用场景
eval()极低数据完全可信,且 Document 类已在作用域中
正则 + ast.literal_eval任意数据来源,生产环境推荐
自定义类 + 正则无 LangChain 环境,或需要自定义结构

实际测试验证

用用户提供的真实数据测试方案二:

raw_string=r"""[Document(metadata={'pk': 12, 'page': 2}, page_content='2. 费用报销:...'), Document(metadata={'pk': 27, 'page': 2}, page_content='2. 费用报销:...'), Document(metadata={'pk': 14, 'page': 3}, page_content='3. 最后警告:...')]"""docs=parse_documents_from_string(raw_string)print(len(docs))# 3

测试结果:

  • 3 个 Document 全部正确解析
  • metadata 中pkpage字段完整保留
  • page_content 中包含\n换行符、中文、特殊标点均正确还原
  • 含括号(连续三天或月累计五天)的内容也被正确处理

常见陷阱

1. page_content 中包含括号

page_content='金额($100)'

错误做法:用简单正则Document\(([^)]*)\)会在$100后的)处提前截断。

正确做法:使用括号深度匹配。

2. page_content 中包含引号

page_content='He said "hello"'

ast.literal_eval能正确处理嵌套引号(单引号包裹的字符串中包含双引号),无需特殊处理。

3. 转义字符 \n

字符串中的\nast.literal_eval解析后会被正确转换为换行符,不需要手动替换。


完整代码

importreimportastfromlangchain_core.documentsimportDocumentdefparse_documents_from_string(s:str)->list[Document]:""" 将包含 Document(...) 的字符串安全地解析为 list[Document]。 Args: s: 包含 Document(...) 表示的字符串 Returns: list[Document]: 解析后的 Document 对象列表 """results=[]idx=0whileTrue:start=s.find("Document(",idx)ifstart==-1:break# 括号深度匹配,处理 page_content 中含括号的情况depth=1i=start+len("Document(")whilei<len(s)anddepth>0:ifs[i]=='(':depth+=1elifs[i]==')':depth-=1i+=1doc_str=s[start:i]meta_match=re.search(r"metadata=(\{.*?\})",doc_str,re.DOTALL)content_match=re.search(r"page_content=(.*?)(?:\)\s*$)",doc_str,re.DOTALL)ifmeta_matchandcontent_match:metadata=ast.literal_eval(meta_match.group(1))page_content_str=content_match.group(1).strip().rstrip(')')page_content=ast.literal_eval(page_content_str)results.append(Document(metadata=metadata,page_content=page_content))idx=ireturnresults# ===== 使用示例 =====if__name__=="__main__":doc_string="[Document(metadata={'pk': 12, 'page': 2}, page_content='示例内容')]"docs=parse_documents_from_string(doc_string)print(f"解析到{len(docs)}个文档")print(docs[0].metadata)# {'pk': 12, 'page': 2}print(docs[0].page_content)# 示例内容

总结

需求推荐方案
快速验证、数据完全可信eval()
生产环境、数据来源不可控正则 +ast.literal_eval
无 LangChain 依赖自定义 Document 类 + 正则

核心原则:能用ast.literal_eval就不用eval,安全第一。

http://www.cnnetsun.cn/news/3958765.html

相关文章:

  • 每一步都合理,但结果是错的——企业AI落地的真实困境
  • 知网维普AIGC检测新标准怎么过?2026论文降AI合规改写指南
  • 初识机器学习(SVM)
  • 从Visual Studio迁移到VSCode:配置指南与避坑经验
  • 智能充电桩选购指南:核心指标与避坑策略
  • HEIF Utility:Windows上处理iPhone照片的终极免费解决方案
  • AI总听不懂我的话!提示词要怎样写?
  • 从零构建IM聊天模块:消息模型、文件处理与实时通信实战
  • 微软MAI-Thinking-1训练解析:RL爬山与GRPO算法如何突破推理瓶颈
  • Unity WebGL项目部署实战:服务器配置与优化全解析
  • C 裸机编程与硬件驱动深度调试:卡顿时先查哪里
  • Linux防火墙实战:firewalld区域管理与端口安全配置详解
  • 比克发布“毫秒级”超能芯:12C狂暴放电,让AI算力彻底告别0延时!
  • Git入门到精通:核心概念、工作流与团队协作实战指南
  • Java LangChain4j 实战搭建私有 RAG 知识库
  • Java转大模型:别急着学Prompt,你的工程经验才是真正壁垒
  • 大模型接入调查岗位匹配度
  • 魔兽争霸3终极优化指南:3步免费解锁完整功能体验
  • 图像融合技术全解析:从传统算法到深度学习实战指南
  • AI Agent中间件:从工具管理到系统架构的核心设计
  • Matlab电力储能调频模型开发与优化实践
  • Hadoop+Spark构建股票大数据分析系统实战
  • JavaScript 字符串工具库设计思路
  • OpenRGB:一站式RGB灯光控制平台,终结多软件混乱时代
  • 数字记忆的守护者:让聊天记录成为永恒的生命印记
  • 从Claude Fable 5系统提示词看AI产品工程化:安全、可控与人格塑造
  • 如何快速为Mac双系统安装Boot Camp驱动:Brigadier终极指南
  • SQL注入文件读写实战:从数据库查询到系统入侵的攻防解析
  • 意图共鸣科技《AI协作记忆系统 · 认知架构白皮书》: AI记住更多,是错的
  • State、Session 与 Checkpoint:Agent 如何保存任务现场?