![# 如何将包含 Document 对象的字符串转换为 List[Document]?](http://pic.xiahunao.cn/yaotu/# 如何将包含 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 docDocument(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...)]# 直接 evaldocseval(doc_string)print(type(docs))# class listprint(type(docs[0]))# class langchain_core.documents.base.Documentprint(docs[0].metadata)# {pk: 12, page: 2}优点一行代码搞定简单粗暴完整还原对象类型缺点eval()会执行字符串中的任意 Python 代码存在安全风险仅适用于完全可信的数据来源安全提示如果字符串来自用户输入、网络请求等不可信来源绝对不要使用 eval()。方案二正则 ast.literal_eval —— 安全且通用ast.literal_eval是 Python 标准库提供的安全解析函数它只解析 Python 字面量字符串、数字、字典、列表等不会执行任何代码。但问题是ast.literal_eval无法直接解析Document(...)这种自定义类的构造调用。所以我们需要分两步用正则提取每个Document(...)块中的metadata和page_content用ast.literal_eval安全解析提取出的字面量importreimportastfromlangchain_core.documentsimportDocumentdefparse_documents_from_string(s:str)-list[Document]: 将包含 Document(...) 的字符串解析为 list[Document] 使用正则提取 ast.literal_eval 安全解析 results[]idx0whileTrue:starts.find(Document(,idx)ifstart-1:break# 通过括号深度匹配找到完整的 Document(...)depth1istartlen(Document()whileilen(s)anddepth0:ifs[i](:depth1elifs[i]):depth-1i1doc_strs[start:i]# 提取 metadata字典格式meta_matchre.search(rmetadata(\{.*?\}),doc_str,re.DOTALL)# 提取 page_content引号包裹的字符串到块结尾content_matchre.search(rpage_content(.*?)(?:\)\s*$),doc_str,re.DOTALL)ifmeta_matchandcontent_match:metadataast.literal_eval(meta_match.group(1))page_content_strcontent_match.group(1).strip().rstrip())page_contentast.literal_eval(page_content_str)results.append(Document(metadatametadata,page_contentpage_content))idxireturnresults核心原理解析为什么要用括号深度匹配page_content的值本身可能包含括号比如page_content费用报销凭发票。如果用简单的正则Document\(([^)]*)\)遇到第一个)就会提前截断。括号深度匹配的原理是遍历字符遇到(加一遇到)减一当深度归零时就找到了完整的闭合位置Document(metadata{pk: 12}, page_content(USD)) ^ ^ start end (depth0)为什么用 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奖惩制度...)]docsparse_documents_from_string(doc_string)fordocindocs:print(fpk{doc.metadata[pk]}, page{doc.metadata[page]})print(fcontent:{doc.page_content[:50]}...)print()输出pk12, page2 content: 费用报销内容... pk27, page2 content: 奖惩制度...方案三处理无依赖场景自定义 Document 类如果你的环境中没有安装 LangChain但你需要解析这种字符串并重建类似结构可以自定义一个等价的 Document 类importreimportastfromdataclassesimportdataclassdataclassclassDocument:模拟 langchain Document 的简化版本metadata:dictpage_content:strdef__repr__(self):returnfDocument(metadata{self.metadata}, page_content{repr(self.page_content)})defparse_documents_from_string(s:str,doc_classDocument)-list:解析字符串为 list[Document]支持自定义 Document 类results[]idx0whileTrue:starts.find(Document(,idx)ifstart-1:breakdepth1istartlen(Document()whileilen(s)anddepth0:ifs[i](:depth1elifs[i]):depth-1i1doc_strs[start:i]meta_matchre.search(rmetadata(\{.*?\}),doc_str,re.DOTALL)content_matchre.search(rpage_content(.*?)(?:\)\s*$),doc_str,re.DOTALL)ifmeta_matchandcontent_match:metadataast.literal_eval(meta_match.group(1))page_content_strcontent_match.group(1).strip().rstrip())page_contentast.literal_eval(page_content_str)results.append(doc_class(metadatametadata,page_contentpage_content))idxireturnresults这个版本通过doc_class参数支持传入任意 Document 类无论你用的是 LangChain 的 Document 还是自定义的。方案对比方案安全性健壮性复杂度适用场景eval()低高极低数据完全可信且 Document 类已在作用域中正则 ast.literal_eval高高中任意数据来源生产环境推荐自定义类 正则高高中无 LangChain 环境或需要自定义结构实际测试验证用用户提供的真实数据测试方案二raw_stringr[Document(metadata{pk: 12, page: 2}, page_content2. 费用报销...), Document(metadata{pk: 27, page: 2}, page_content2. 费用报销...), Document(metadata{pk: 14, page: 3}, page_content3. 最后警告...)]docsparse_documents_from_string(raw_string)print(len(docs))# 3测试结果3 个 Document 全部正确解析metadata 中pk和page字段完整保留page_content 中包含\n换行符、中文、特殊标点均正确还原含括号(连续三天或月累计五天)的内容也被正确处理常见陷阱1. page_content 中包含括号page_content金额($100)错误做法用简单正则Document\(([^)]*)\)会在$100后的)处提前截断。正确做法使用括号深度匹配。2. page_content 中包含引号page_contentHe said helloast.literal_eval能正确处理嵌套引号单引号包裹的字符串中包含双引号无需特殊处理。3. 转义字符 \n字符串中的\n在ast.literal_eval解析后会被正确转换为换行符不需要手动替换。完整代码importreimportastfromlangchain_core.documentsimportDocumentdefparse_documents_from_string(s:str)-list[Document]: 将包含 Document(...) 的字符串安全地解析为 list[Document]。 Args: s: 包含 Document(...) 表示的字符串 Returns: list[Document]: 解析后的 Document 对象列表 results[]idx0whileTrue:starts.find(Document(,idx)ifstart-1:break# 括号深度匹配处理 page_content 中含括号的情况depth1istartlen(Document()whileilen(s)anddepth0:ifs[i](:depth1elifs[i]):depth-1i1doc_strs[start:i]meta_matchre.search(rmetadata(\{.*?\}),doc_str,re.DOTALL)content_matchre.search(rpage_content(.*?)(?:\)\s*$),doc_str,re.DOTALL)ifmeta_matchandcontent_match:metadataast.literal_eval(meta_match.group(1))page_content_strcontent_match.group(1).strip().rstrip())page_contentast.literal_eval(page_content_str)results.append(Document(metadatametadata,page_contentpage_content))idxireturnresults# 使用示例 if__name____main__:doc_string[Document(metadata{pk: 12, page: 2}, page_content示例内容)]docsparse_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安全第一。