Python伪列表字符串解析:json.loads与正则提取
在 Python 开发中,经常遇到看起来像 list、实际类型是 str 的数据。例如接口或日志里出现:data = '["qwen-turbo","qwen-plus","deepseek"]'
print(type(data))# <class 'str'>
另一种是包含自定义对象的 repr 字符串,例如 LangChain 的 Document:
data = ""
这两类字符串的解析方式不同:标准 JSON 数组可以用 json.loads(),而 Python 对象 repr 字符串需要正则提取字段。下面按两种场景说明。
一、标准 JSON 格式字符串转列表
题目:
raw_str = '["qwen-turbo","qwen-plus","deepseek"]'
# 将其转换为 list
这是标准 JSON 格式字符串,内容是字符串数组。
方法一:json.loads() 推荐
import json
raw_str = '["qwen-turbo","qwen-plus","deepseek"]'
result = json.loads(raw_str)
print(result) # ['qwen-turbo', 'qwen-plus', 'deepseek']
print(type(result)) # <class 'list'>
优点:速度快,标准库支持,JSON 格式通用性强。
方法二:ast.literal_eval()
import ast
raw_str = '["qwen-turbo","qwen-plus","deepseek"]'
result = ast.literal_eval(raw_str)
print(result) # ['qwen-turbo', 'qwen-plus', 'deepseek']
print(type(result)) # <class 'list'>
优点:安全性高,只解析字面量,不会执行恶意代码。
方法三:eval() 不推荐
raw_str = '["qwen-turbo","qwen-plus","deepseek"]'
result = eval(raw_str)
print(result) # ['qwen-turbo', 'qwen-plus', 'deepseek']
缺点:会执行任意 Python 代码,有安全风险,生产环境慎用。
三种方法对比:json.loads() 速度最快、安全性较好,适合 JSON 格式字符串;ast.literal_eval() 速度一般、最安全,适合 Python 字面量格式;eval() 速度一般、危险,不推荐使用。
二、对象格式字符串提取字段
题目:
raw_str = """[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. 最后警告:情节严重,留司察看...')]"""
问题是:如何获取里面的 page 属性,并按页码组织 page_content?
这个字符串包含 LangChain 的 Document 对象。它不是标准 JSON,而是 Python 的 repr() 输出格式。
为什么 json.loads() 不行?因为字符串中使用单引号,JSON 要求双引号;Document(...) 这种自定义对象也不是 JSON 支持的数据类型。调用 json.loads(raw_str) 会报 json.decoder.JSONDecodeError。
为什么 ast.literal_eval() 也不行?因为它只能解析 Python 字面量,例如字符串、数字、列表、字典等,不能解析函数调用 Document(...)。调用 ast.literal_eval(raw_str) 会报 ValueError: malformed node。
解决方案是使用正则表达式。
方法 1:只提取 page
import re
raw_str = ""
pages = re.findall(r"'page':\s*(\d+)", raw_str)
pages =
print(pages)#
正则含义:'page': 匹配字面量;\s* 匹配 0 个或多个空白字符;(\d+) 捕获 1 个或多个数字。
方法 2:提取 page 和 page_content,并按页分组
import re
from collections import defaultdict
raw_str = ""
docs = re.findall(
r"Document\(metadata=\{.*?'page':\s*(\d+).*?page_content='(.*?)'\)",
raw_str,
re.DOTALL
)
page_contents = defaultdict(list)
for page, content in docs:
page_contents.append(content)
for page in sorted(page_contents.keys()):
combined = "\n".join(page_contents)
print(f"=== 第 {page} 页 ===")
print(combined)
print()
正则 r"Document\(metadata=\{.*?'page':\s*(\d+).*?page_content='(.*?)'\)" 解析:
Document\( 匹配 Document(;
metadata=\{ 匹配 metadata={;
.*? 非贪婪匹配任意字符;
'page':\s*(\d+) 捕获 page 的值;
page_content='(.*?)' 捕获 page_content 的内容;
\) 匹配 )。
re.DOTALL 让 . 可以匹配换行符。
三、完整代码
第一题完整代码:
import json
import ast
def parse_json_list(raw_str: str) -> list:
"""解析 JSON 格式的字符串列表"""
return json.loads(raw_str)
def parse_python_list(raw_str: str) -> list:
"""解析 Python 字面量格式的字符串列表"""
return ast.literal_eval(raw_str)
raw_str = '["qwen-turbo","qwen-plus","deepseek"]'
result1 = parse_json_list(raw_str)
result2 = parse_python_list(raw_str)
print(f"json.loads: {result1}")
print(f"ast.literal_eval: {result2}")
第二题完整代码:
import re
from collections import defaultdict
def extract_pages(raw_str: str) -> list:
"""从 Document 字符串中提取所有 page"""
return
def extract_docs_by_page(raw_str: str) -> dict:
"""从 Document 字符串中提取 page 和 page_content,按页分组"""
docs = re.findall(
r"Document\(metadata=\{.*?'page':\s*(\d+).*?page_content='(.*?)'\)",
raw_str,
re.DOTALL
)
page_contents = defaultdict(list)
for page, content in docs:
page_contents.append(content)
return dict(page_contents)
def combine_by_page(raw_str: str) -> str:
"""按页码拼接所有内容"""
page_contents = extract_docs_by_page(raw_str)
result = []
for page in sorted(page_contents.keys()):
combined = "\n".join(page_contents)
result.append(f"=== 第 {page} 页 ===\n{combined}")
return "\n\n".join(result)
raw_str = """[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. 最后警告...')]"""
print("提取的页码:", extract_pages(raw_str))
print("\n按页拼接结果:")
print(combine_by_page(raw_str))
四、对比与选型
纯数据、标准 JSON 字符串:json.loads() 能用,ast.literal_eval() 能用,eval() 也能用但不安全,推荐 json.loads()。
对象格式、Python repr 字符串:json.loads() 不能用,ast.literal_eval() 不能用,eval() 能用但需要上下文中的 Document 且存在安全风险,推荐 re.findall()。
简单选型流程:
遇到字符串形式的“列表”时,先判断格式:
是标准 JSON 格式,用 json.loads();
是 Python 基本字面量,用 ast.literal_eval();
包含自定义对象,用正则表达式提取。
五、常见问题
Q1:为什么不推荐 eval()?
eval() 会执行任意 Python 代码。比如恶意输入:
malicious = "__import__('os').system('rm -rf /')"
eval(malicious)
这类代码会带来严重安全风险。
Q2:正则表达式太难了,有更简单的方法吗?
如果字符串格式固定,也可以用字符串分割:
pages = []
for part in raw_str.split("'page': "):
if part and part.isdigit():
pages.append(int(part.split(",")))
但正则更通用、更可靠。
Q3:LangChain 的 Document 对象为什么不是标准 JSON?
因为 Document 是 Python 类,它的 repr() 输出是 Python 语法,不是 JSON。这是 Python 对象序列化的常见问题。
总结:标准格式用标准工具,JSON 字符串用 json.loads();复杂格式用正则,包含自定义对象时正则表达式是最佳选择;安全第一,避免使用 eval(),除非能完全信任输入源。掌握这两类解析技巧后,再遇到“看起来像 list 的字符串”就能按格式选择方法。
Re: Python伪列表字符串解析:json.loads与正则提取
楼主把两类“伪列表字符串”分开处理,思路很清楚。标准 JSON 数组优先用 json.loads 是合适的,ast.literal_eval 适合 Python 字面量而且更安全,eval 确实应该尽量避开。Document 这种 repr 字符串不是 JSON,也不是纯字面量,json.loads 和 literal_eval 都会失败,用正则提取字段是比较务实的做法。 只取 page 时直接 findall 最省事;要同时取 page 和 page_content,用 DOTALL 加非贪婪匹配在格式稳定时够用,但要注意 page_content 里如果出现单引号、右括号或换行,可能会提前截断。如果担心健壮性,可以先按 Document( 切分,再逐段提取,会更可控。最后用 defaultdict 按页码聚合,再 sorted 输出,顺序和结构都很自然。 另外如果原始字符串里真的带有 " 这类 HTML 实体,记得先 html.unescape 再 json.loads,否则也会解析失败。整体上这个分类和方案对实际开发挺有参考价值。Re: Python伪列表字符串解析:json.loads与正则提取
这篇整理得很实用,json.loads、ast.literal_eval 和 eval 的取舍讲得很清楚,尤其强调 eval 的安全风险这点很有必要。Document 那种 repr 字符串确实不能当 JSON 处理,用正则先抓 page 和 page_content 再按页分组,思路很直接。一个小建议:如果 page_content 里可能包含单引号、换行或类似 page_content='...' 的片段,正则里的 .*? 可能会提前截断,实际用的时候最好先确认日志格式是否稳定,或者考虑把 Document 的 repr 输出改成结构化日志。最后按页拼接那部分好像没贴完,期待补全。Re: Python伪列表字符串解析:json.loads与正则提取
楼主这个整理挺实用的,json.loads、ast.literal_eval 和 eval 的取舍讲得很清楚,尤其是生产环境尽量避开 eval 这点很重要。Document 那种 repr 字符串确实不能走 JSON 和 literal_eval,用正则提取字段是很常见的应急方案,但实际用的时候要留意 page_content 里如果出现单引号、右括号或换行,非贪婪匹配可能会提前截断,最好根据日志格式把边界写得更确定一些,或者先按 Document(...) 切段再分别解析。最后按页分组那段好像没贴完,期待补上完整的 defaultdict 合并和排序输出示例。
页:
[1]