阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。
DOCX 本质上是一组 ZIP 条目,EPUB 也采用 ZIP 容器。收到一个体积很小的压缩文件,不代表解开以后仍然小。入口处应限制压缩体积、条目数量、单项大小与总展开量,而不是等内存用完才判断文件不正常。
先检查目录表,但别把它当最终保证
ZipInfo
提供声明大小与压缩大小,适合做预筛。实际解压时仍要累计已经读出的字节,并设置时间与内存预算。目录表可以帮助提前拒绝,却不能代替运行时限制。
一个不解压的预筛示例
from io import BytesIO
from pathlib import PurePosixPath
from zipfile import ZipFile, ZIP_DEFLATED
def inspect(raw, max_entries=10, max_total=4096):
if len(raw) > 4096:
raise ValueError("compressed input budget")
with ZipFile(BytesIO(raw)) as z:
items = z.infolist()
if len(items) > max_entries:
raise ValueError("entry count budget")
total, seen = 0, set()
for item in items:
name = item.filename
path = PurePosixPath(name)
if (path.is_absolute() or ".." in path.parts or
"\\" in name or ":" in name or name in seen):
raise ValueError("unsafe or duplicate name")
seen.add(name)
total += item.file_size
if total > max_total:
raise ValueError("expanded budget")
return len(items), total
def sample(name, data):
out = BytesIO()
with ZipFile(out, "w", ZIP_DEFLATED) as z:
z.writestr(name, data)
return out.getvalue()
assert inspect(sample("word/document.xml", b"<doc/>")) == (1, 6)
for raw in [sample("../escape", b"x"), sample("large", b"x" * 5000)]:
try:
inspect(raw)
except ValueError:
pass
else:
raise AssertionError("unsafe archive accepted")
这些预算是为了演示拒绝路径而选的很小数值,不能直接复制为生产限制。示例不调用
extractall
,也不实施 ZIP 条目写入,因此没有声称完成安全解压器。加密条目、符号链接、嵌套归档和解析器自身资源消耗都需要单独策略。
拒绝时给用户可行动的信息
触发条件
对用户说明
内部记录
总展开量超预算
文件展开后过大
限制名与观测值
重复条目名
容器结构不明确
条目索引,不记全文
越界路径
文件结构不受支持
安全事件关联 ID
容器损坏
请重新导出文件
解析异常类型
不要为了兼容关闭所有限制
合理的大型文档也可能超过预算,应提供独立的大文件队列或人工处理入口。提高一个租户的业务配额,不等于允许所有任务无限解压。限制需要与执行隔离配合,而不是只写在上传按钮旁边。
翻译失败要分清在哪一层
准备向
OmniDoc(产品入口)
提交文档时,先把容器损坏与翻译失败分开。一个连 XML 都读不出的 DOCX,问题可能发生在输入准备阶段。清楚的预检报告能让用户知道应该重新导出、压缩图片,还是联系翻译服务排查。
参考:
Python zipfile
。
- 1 -
更新于 2026-10-08