前段时间遇到一个需求,需要将word文档中的内容进行替换,并且需要保证格式不变。在找了一圈资料后,发现没有现成的api供使用;由于本人做过一段时间文档解析,因此打算从word文档的xml入手,通过python解析xml来完成word文本替换。本文参考:https://virantha.com/2013/08/16/reading-and-writing-microsoft-word-docx-files-with-python/
该技术可以用到文档翻译、文档纠错、文档脱敏等领域, 目前个人已经将其用到了文档翻译中,兼容了【doc、docx、ppt、pptx、xls、xlsx】已经交付了多个项目,效果良好,欢迎有兴趣的同学来了解,互相交流。
1. 获取文档内容
本质上,docx
文件只是一个zip
文件(尝试对其运行unzip
!),其中包含一堆定义良好的XML
和附带文件。主要的文本内容和结构在以下XML文件中定义:
word/document.xml
因此,第一步是读取此zip
中的内容并获取xml
.
import zipfile
def get_word_xml(docx_filename):
with open(docx_filename) as f:
zip = zipfile.ZipFile(f)
xml_content = zip.read('word/document.xml')
return xml_content
第二步,我们需要将字符串的xml解析成一颗xml树. 本文使用lxml
包来操作[pip install lxml]
.
from lxml import etree
def get_xml_tree(xml_string):
return etree.fromstring(xml_string)
现在我们已经有了xml
树,下一步分析这颗树的结构
2. word的xml树解析
对于由文本段落组成并应用了某些样式/格式的基本文档,XML结构非常简单。这是示例文档:
对上面的文档unzip,读取document.xml, 我们会得到下面的内容
测试
这是一段测试文字,测试文档的结构树。
可以观察到:
1. 最上面的标签是,然后是标签,然后将正文分为几段,用划界。
2. 每个段落可能包含段落样式,在每个段落中,会包含中,每个都包括, 并用标签将文本括起来
3. 除此以外,xml中同一段落中的文本,有时候会被切分到多个run中,每个run中包含一部分文本。
3. Word文档节点内容合并
下面介绍一个例子,关于修改XML树以更改Word内容的快速示例。
在我的需求中,我需要用其他一些文本替换(文档翻译)。所以我首先会做一些节点合并,将同一个段落下的内容合并到第一个节点【注意:这样会更改到其它节点的文字内容样式,但是我默认允许这样的更改】,将其它节点的内容合并到当前节点。
def word_join_tags(self, my_etree):
""" 合并tags
my_etree : 文档树
"""
# merge 最底层下的所有
parent_nodes = my_etree.xpath("//w:p",
namespaces={'w': self.type_schema})
for parent_node in parent_nodes:
chars = []
childs = parent_node.xpath("./w:r/w:t",
namespaces={'w': self.type_schema})
for i, child_node in enumerate(childs):
chars.append(str(child_node.text) if child_node.text else "")
if i != 0:
try:
t_patent_node = child_node.getparent()
if t_patent_node.getparent() is not None:
t_patent_node.getparent().remove(t_patent_node)
except Exception as e:
logger.error(e)
logger.warn("remove null word node error")
child_node.text = ""
if childs:
childs[0].text = "".join(chars)
return my_etree
4. 段落内容替换-翻译
我以翻译为例子, 思路是对XML节点的文本进行替换,这块可以采用一些开源的工具来进行翻译【百度、google和微软都有翻译接口】。 示例代码如下:
def translate_node(self, my_etree):
'''
description: 翻译节点内容
param {type}
my_etree : 待翻译的文档树
return {type}
my_etree : 翻译好的文档树
author: zhangzhen20
'''
# translate document
text_list = []
node_list = []
max_len = MAX_INPUT_BYTE_LEN # 每次翻译的最长字节数
max_count = MAX_NODE_COUNT # 每次最多翻译的节点数
buff_len = 0 # 当前翻译的字节长度
count = 0 # 当前翻译的节点数
need_trans_node_list = []
need_trans_text_list = []
for node, text in self._itertext(my_etree, "t"):
text = str(text).replace("\n", "")
if len(text) == 0 or not is_need_translate(text):
continue
text_len = len(text.encode('utf-8'))
if buff_len + text_len < max_len and count < max_count:
text_list.append(text)
node_list.append(node)
buff_len += text_len
count += 1
continue
need_trans_node_list.append(node_list[:])
need_trans_text_list.append(text_list[:])
# translation_text_list = request(text_list, self.source,
# self.target, self.term_list,
# self.mem_list)
# for cur_node, cur_text in zip(node_list, translation_text_list):
# cur_node.text = cur_text
buff_len = len(text)
text_list = [text]
node_list = [node]
count = 1
if text_list:
need_trans_node_list.append(node_list[:])
need_trans_text_list.append(text_list[:])
translation_text_list = multiprocess_request_translation(need_trans_text_list, self.source,
self.target, self.term_list,
self.mem_list)
for node_list, trans_list in zip(need_trans_node_list, translation_text_list):
for node, text in zip(node_list, trans_list):
node.text = text
return my_etree
5. 将文本替换好的xml转换为文档
word文档都是xml文件,work可以解压成xml,xml也可以压缩为word, 这块有两点需要:1. 压缩的时候需要将所有的xml文件进行压缩。 2. 压缩的时候需要注意模式,否则会导致压缩文件较大。
def re_write_xml(self, xml_file_dict, output_filename):
""" Create a temp directory, expand the original pptx zip.
Write the modified xml to word/document.xml
Zip it up as the new pptx
"""
tmp_dir = tempfile.mkdtemp()
self.zipfile.extractall(tmp_dir)
for filename, xml_content in xml_file_dict.items():
with open(os.path.join(tmp_dir, filename), 'wb') as f:
xmlstr = etree.tostring(xml_content, pretty_print=True)
f.write(xmlstr)
# Get a list of all the files in the original pptx zipfile
filenames = self.zipfile.namelist()
# Now, create the new zip file and add all the filex into the archive
zip_copy_filename = output_filename
with ZipFile(zip_copy_filename, "w", compression=ZIP_DEFLATED) as pptx:
for filename in filenames:
pptx.write(os.path.join(tmp_dir, filename), filename)
# Clean up the temp dir
shutil.rmtree(tmp_dir)
参考资料:
- https://virantha.com/2013/08/16/reading-and-writing-microsoft-word-docx-files-with-python/