文档翻译-使用python替换word文档中的段落内容

前段时间遇到一个需求,需要将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结构非常简单。这是示例文档:

image-20210304174708352

对上面的文档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)

参考资料:

  1. https://virantha.com/2013/08/16/reading-and-writing-microsoft-word-docx-files-with-python/

你可能感兴趣的:(文档翻译-使用python替换word文档中的段落内容)