报错
Traceback (most recent call last):
File "read_docx.py", line 27, in <module>
document = docx.Document(docx_file)
File "docx/api.py", line 30, in Document
document_part = cast("DocumentPart", Package.open(docx).main_document_part)
File "docx/opc/package.py", line 126, in open
pkg_reader = PackageReader.from_file(pkg_file)
File "docx/opc/pkgreader.py", line 96, in _walk_phys_parts
blob = phys_reader.blob_for(partname)
File "zipfile.py", line 1484, in getinfo
raise KeyError(
KeyError: "There is no item named 'NULL' in the archive"
DOCX 是 ZIP 格式的 Open XML 包。异常文件的某个 *.rels 中存在无效关系:
<Relationship
Id="rId8"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
Target="../NULL"/>
python-docx 解析关系后尝试从压缩包读取 NULL,最终由 zipfile 抛出 KeyError。
关系文件位置可以直接列出:
unzip -Z1 broken.docx | rg '\.rels$'
搜索异常 Target:
for rels in $(unzip -Z1 broken.docx | rg '\.rels$'); do
unzip -p broken.docx "$rels" | rg -n 'Target="(\.\./)?NULL"'
done
清理关系
不修改原文件,清理后的文档写入新路径:
from pathlib import Path
from xml.etree import ElementTree
from zipfile import ZipFile
INVALID_TARGETS = {"NULL", "../NULL"}
def remove_null_relationships(xml_data: bytes):
root = ElementTree.fromstring(xml_data)
removed = []
for relationship in list(root):
target = relationship.get("Target", "")
if target in INVALID_TARGETS:
removed.append((relationship.get("Id", ""), target))
root.remove(relationship)
if not removed:
return xml_data, removed
repaired = ElementTree.tostring(
root,
encoding="utf-8",
xml_declaration=True,
)
return repaired, removed
def repair_docx(source: Path, destination: Path):
removed = []
with ZipFile(source) as source_zip, ZipFile(destination, "w") as dest_zip:
for item in source_zip.infolist():
data = source_zip.read(item.filename)
if item.filename.endswith(".rels"):
data, current = remove_null_relationships(data)
removed.extend(
(item.filename, relation_id, target)
for relation_id, target in current
)
dest_zip.writestr(item, data)
return removed
source = Path("broken.docx")
destination = Path("fixed.docx")
for filename, relation_id, target in repair_docx(source, destination):
print(filename, relation_id, target)
结果
使用包含 Target="../NULL" 的测试文档复现:
before: "There is no item named 'NULL' in the archive"
removed: word/_rels/document.xml.rels rIdNull ../NULL
after: test
清理后 python-docx 1.2.0 可以正常读取 fixed.docx。原始 broken.docx 保持不变。
参考
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。