从字符串中提取 Document 的 page_content:手动拆还是用工具?
背景
继上一题的手拆字符串之后,又来了一个升级版:
字符串内容如下(为了阅读方便,我做了换行处理):
“[Document(metadata={‘pk’: 12, ‘page’: 2}, page_content=‘这里是第一段正文内容…’), Document(metadata={‘pk’: 27, ‘page’: 2}, page_content=‘这里是第二段正文内容…’), Document(metadata={‘pk’: 14, ‘page’: 3}, page_content=‘这里是第三段正文内容…’)]”
需求是:从这个字符串中提取每个 Document 的 page_content(正文内容),然后拼接成一个完整的字符串。
这道题的核心考点是:给定一个包含对象的字符串,如何安全地提取其中的特定字段(page_content)。
思路一:纯手动拆分(正则表达式)
既然字符串有固定格式:Document(metadata={‘pk’: 数字, ‘page’: 数字}, page_content=‘这里是正文…’)
我们可以直接用正则表达式提取 page_content 后面的内容:
import re raw_str = "[Document(metadata={'pk': 12, 'page': 2}, page_content='这里是第一段正文内容...'), Document(metadata={'pk': 27, 'page': 2}, page_content='这里是第二段正文内容...'), Document(metadata={'pk': 14, 'page': 3}, page_content='这里是第三段正文内容...')]" # 提取 page_content 后面的内容 contents = re.findall(r"page_content='(.*?)'", raw_str) # 拼接成完整字符串 result = "".join(contents) print(result) # 输出: 这里是第一段正文内容...这里是第二段正文内容...这里是第三段正文内容...优点
- 代码短,思路直接
- 不需要额外库(re 是标准库)
缺点
- 极其脆弱:如果正文里包含单引号,正则就会失效
- 不通用:换一种对象结构就得重写正则
- 如果 page_content 的值包含换行或特殊字符,代码立刻报错
思路二:用 ast.literal_eval 解析(更安全)
如果我们能把字符串转成 Python 对象,然后直接访问 page_content 属性,那就安全得多。
但问题来了:ast.literal_eval 不认识 Document 这个类名,所以不能直接解析。
解决思路:把 Document(…) 替换成字典 {…}
import re import ast raw_str = "[Document(metadata={'pk': 12, 'page': 2}, page_content='这里是第一段正文内容...'), Document(metadata={'pk': 27, 'page': 2}, page_content='这里是第二段正文内容...'), Document(metadata={'pk': 14, 'page': 3}, page_content='这里是第三段正文内容...')]" # 提取 metadata 和 page_content pattern = r"Document\(metadata=({.*?}), page_content='(.*?)'\)" matches = re.findall(pattern, raw_str) contents = [] for metadata_str, content in matches: contents.append(content) # content 就是 page_content 的值 result = "".join(contents) print(result) # 输出: 这里是第一段正文内容...这里是第二段正文内容...这里是第三段正文内容...优点
- 解析的是 Python 字面量,安全可靠
- 能处理稍微复杂的格式变化(只要 metadata 部分合法)
缺点
- 代码比纯正则稍复杂
- 如果 page_content 里有单引号,仍然会出问题
思路三:针对 page_content 包含特殊字符的终极方案
如果 page_content 里面可能包含单引号、换行等特殊字符,上面的正则就失效了。
这时候需要更精确的匹配策略:
import re import ast raw_str = "[Document(metadata={'pk': 12, 'page': 2}, page_content='第一段正文...'), Document(metadata={'pk': 27, 'page': 2}, page_content='第二段正文...')]" # 用正则提取 metadata 部分,再单独提取 page_content pattern = r"Document\(metadata=({.*?}), page_content='(.*?)'\)" matches = re.findall(pattern, raw_str) contents = [] for metadata_str, content in matches: # 如果 content 里有转义字符,用 ast.literal_eval 安全还原 try: # 把 content 当作 Python 字符串字面量解析 safe_content = ast.literal_eval(f"'{content}'") contents.append(safe_content) except: contents.append(content) result = "".join(contents)思路四:如果题目给的是对象列表(最理想情况)
如果题目不是给你一个字符串,而是给你一个已经存在的 documents 列表,那就直接操作对象即可:
contents = [doc.page_content for doc in documents] result = "".join(contents)这是最简单、最直接、最安全的方式。
这道题在考试中的正确答法
- 如果题目明确说"这是一个字符串":用 ast.literal_eval 配合 re.findall 提取 page_content 字段,然后拼接。
- 如果题目说"有一个 documents 列表":直接用列表推导式 [doc.page_content for doc in documents]。
- 如果 page_content 可能包含特殊字符(如引号、换行),用 ast.literal_eval 做二次安全解析。
总结一句话
手动拆分是"能做到",但正确做法是"安全解析"。考试看的是你对工具的理解,不是你折腾字符串的能力。
三种方式对比
| 方式 | 代码量 | 安全性 | 通用性 | 推荐度 |
|---|---|---|---|---|
| 纯正则手拆 | 短 | 极低 | 极低 | 不推荐 |
| ast.literal_eval + 正则 | 中 | 高 | 中 | 推荐 |
| 直接操作对象列表 | 短 | 极高 | 极高 | 最推荐 |
彩蛋:如果 page_content 包含换行或特殊字符怎么办?
如果正文内容可能包含换行或特殊字符,上面的方法仍然可以处理,因为 ast.literal_eval 能安全解析 Python 字符串字面量。
但如果正文内容非常长,或者包含各种特殊符号,最稳妥的方式是在字符串生成时就做好序列化,而不是在解析时临时处理。