【问题标题】:Python docx Replace string in paragraph while keeping stylePython docx在保持样式的同时替换段落中的字符串
【发布时间】:2016-01-14 00:39:18
【问题描述】:

我需要帮助替换 word 文档中的字符串,同时保持整个文档的格式。

我正在使用 python-docx,在阅读文档后,它适用于整个段落,所以我松散了格式,比如粗体或斜体字。 包括要替换的文本以粗体显示,我想保持这种方式。 我正在使用此代码:

from docx import Document
def replace_string2(filename):
    doc = Document(filename)
    for p in doc.paragraphs:
        if 'Text to find and replace' in p.text:
            print 'SEARCH FOUND!!'
            text = p.text.replace('Text to find and replace', 'new text')
            style = p.style
            p.text = text
            p.style = style
    # doc.save(filename)
    doc.save('test.docx')
    return 1

所以如果我实现它并想要类似的东西(包含要替换的字符串的段落会丢失其格式):

这是第1段,这是粗体的文字。

这是第2段,我将替换旧文本

目前的结果是:

这是第1段,这是粗体的文字。

这是第2段,我将替换新文本

【问题讨论】:

  • 您可以尝试使用索引。例如for p in range(len(doc.paragraphs)): . . .,然后将段落设置为doc.paragraphs[p] = text,假设 doc.paragraphs 返回文档所述的列表。
  • 我认为这给出了我目前得到的相同输出。该文件保留其格式,但包含要替换的字符串的段落除外。如果我错了,请纠正我。
  • Text formatting is not saved when using assignment. "保留段落级别的格式,例如样式。所有运行级别的格式,例如粗体或斜体,都被删除。"

标签: python python-2.7 python-docx


【解决方案1】:

我发布了这个问题(尽管我在这里看到了一些相同的问题),因为这些(据我所知)都没有解决这个问题。有一个使用 oodocx 库,我试过了,但没有用。所以我找到了解决方法。

代码非常相似,但逻辑是:当我找到包含我要替换的字符串的段落时,使用 runs 添加另一个循环。 (这仅在我希望替换的字符串具有相同格式时才有效)。

def replace_string(filename):
    doc = Document(filename)
    for p in doc.paragraphs:
        if 'old text' in p.text:
            inline = p.runs
            # Loop added to work with runs (strings with same style)
            for i in range(len(inline)):
                if 'old text' in inline[i].text:
                    text = inline[i].text.replace('old text', 'new text')
                    inline[i].text = text
            print p.text

    doc.save('dest1.docx')
    return 1

【讨论】:

  • 我不认为这可能适用于所有情况,内联列表是指段落的内部项目,因此段落文本是它们的复合物,您不会找到完整的替换文本在一项中
  • 问题是,如果您在一行中找到要替换的单词,您仍然会通过这样做替换整行,而不是只替换您要替换的一个单词。不过,这仍然比替换整个段落要好。
  • 运行创建是非常不可预测和不可控的。我正在文档中创建“标签”来替换它们,并且运行正在分解我的标签。例如:标签 -> ${cma-tag-one},运行 -> [${, cma-tag, one, }]。标记 -> cmatagthree,运行 -> [cma, tag, thr, ee]。标签 -> mytag,运行 -> [mytag]。标记 -> ImTryingToFindYou,运行 -> [Im, Trying, To, Find, You]。一些使用运行可能不是最好的方法,因为您可能无法找到您的标签
  • 如果搜索字符串中混有字符和符号,则字符串会分成几部分,无法找到和替换文本。我在这个link 作品中找到了代码。
【解决方案2】:

这就是我在替换文本时保留文本样式的方法。

基于Alo 的回答以及搜索文本可以在多次运行中拆分的事实,这就是我在模板 docx 文件中替换占位符文本的方法。它检查占位符的所有文档段落和任何表格单元格内容。

一旦在段落中找到搜索文本,它就会循环遍历它的运行,以确定哪个运行包含搜索文本的部分文本,然后在第一次运行中插入替换文本,然后将剩余的搜索文本字符清空剩余运行次数。

我希望这对某人有所帮助。这是gist,如果有人想改进它

编辑: 我随后发现了python-docx-template,它允许在 docx 模板中使用 jinja2 样式模板。这是documentation的链接

def docx_replace(doc, data):
    paragraphs = list(doc.paragraphs)
    for t in doc.tables:
        for row in t.rows:
            for cell in row.cells:
                for paragraph in cell.paragraphs:
                    paragraphs.append(paragraph)
    for p in paragraphs:
        for key, val in data.items():
            key_name = '${{{}}}'.format(key) # I'm using placeholders in the form ${PlaceholderName}
            if key_name in p.text:
                inline = p.runs
                # Replace strings and retain the same style.
                # The text to be replaced can be split over several runs so
                # search through, identify which runs need to have text replaced
                # then replace the text in those identified
                started = False
                key_index = 0
                # found_runs is a list of (inline index, index of match, length of match)
                found_runs = list()
                found_all = False
                replace_done = False
                for i in range(len(inline)):

                    # case 1: found in single run so short circuit the replace
                    if key_name in inline[i].text and not started:
                        found_runs.append((i, inline[i].text.find(key_name), len(key_name)))
                        text = inline[i].text.replace(key_name, str(val))
                        inline[i].text = text
                        replace_done = True
                        found_all = True
                        break

                    if key_name[key_index] not in inline[i].text and not started:
                        # keep looking ...
                        continue

                    # case 2: search for partial text, find first run
                    if key_name[key_index] in inline[i].text and inline[i].text[-1] in key_name and not started:
                        # check sequence
                        start_index = inline[i].text.find(key_name[key_index])
                        check_length = len(inline[i].text)
                        for text_index in range(start_index, check_length):
                            if inline[i].text[text_index] != key_name[key_index]:
                                # no match so must be false positive
                                break
                        if key_index == 0:
                            started = True
                        chars_found = check_length - start_index
                        key_index += chars_found
                        found_runs.append((i, start_index, chars_found))
                        if key_index != len(key_name):
                            continue
                        else:
                            # found all chars in key_name
                            found_all = True
                            break

                    # case 2: search for partial text, find subsequent run
                    if key_name[key_index] in inline[i].text and started and not found_all:
                        # check sequence
                        chars_found = 0
                        check_length = len(inline[i].text)
                        for text_index in range(0, check_length):
                            if inline[i].text[text_index] == key_name[key_index]:
                                key_index += 1
                                chars_found += 1
                            else:
                                break
                        # no match so must be end
                        found_runs.append((i, 0, chars_found))
                        if key_index == len(key_name):
                            found_all = True
                            break

                if found_all and not replace_done:
                    for i, item in enumerate(found_runs):
                        index, start, length = [t for t in item]
                        if i == 0:
                            text = inline[index].text.replace(inline[index].text[start:start + length], str(val))
                            inline[index].text = text
                        else:
                            text = inline[index].text.replace(inline[index].text[start:start + length], '')
                            inline[index].text = text
                # print(p.text)

# usage

doc = docx.Document('path/to/template.docx')
docx_replace(doc, dict(ItemOne='replacement text', ItemTwo="Some replacement text\nand some more")
doc.save('path/to/destination.docx')

【讨论】:

    【解决方案3】:
    from docx import Document
    
    document = Document('old.docx')
    
    dic = {
        '{{FULLNAME}}':'First Last',
        '{{FIRST}}':'First',
        '{{LAST}}' : 'Last',
    }
    for p in document.paragraphs:
        inline = p.runs
        for i in range(len(inline)):
            text = inline[i].text
            for key in dic.keys():
                if key in text:
                     text=text.replace(key,dic[key])
                     inline[i].text = text
    
    
    document.save('new.docx')
    

    【讨论】:

    • 欢迎赞恩。您能否在您的答案中添加一些上下文,解释为什么您发布的代码解决了原始问题的问题?
    • 运行创建是非常不可预测和不可控的。我正在文档中创建“标签”来替换它们,并且运行正在分解我的标签。例如:标签 -> ${cma-tag-one},运行 -> [${, cma-tag, one, }]。标记 -> cmatagthree,运行 -> [cma, tag, thr, ee]。标签 -> mytag,运行 -> [mytag]。标记 -> ImTryingToFindYou,运行 -> [Im, Trying, To, Find, You]。一些使用运行可能不是最好的方法,因为您可能无法找到您的标签
    【解决方案4】:

    根据DOCX文档的架构:

    1. 文本:doc>段落>运行
    2. 文本表格:doc>Form>row>col>cell>Paragraph>run
    3. 标题:doc>sections>header>Paragraph>run
    4. 标题表:doc>sections>header>Form>row>col>cell>Paragraph>run

    footer和header一样,我们可以直接遍历段落查找替换我们的关键词,但是这样会导致文本格式重置,所以只能遍历run中的单词替换掉。但是,由于我们的关键字可能超出运行的长度范围,我们无法成功替换它们。

    因此,我在这里提供一个思路:首先,以段落为单位,通过列表标记段落中每个字符的位置;然后,标记每个字符在遍历列表中的位置;在段落中查找关键字,按对应关系删除并以字符为单位替换。

    '''
    -*- coding: utf-8 -*-
    @Time    : 2021/4/19 13:13
    @Author  : ZCG
    @Site    : 
    @File    : Batch DOCX document keyword replacement.py
    @Software: PyCharm
    '''
    
    from docx import Document
    import os
    import tqdm
    
    def get_docx_list(dir_path):
        '''
        :param dir_path:
        :return: List of docx files in the current directory
        '''
        file_list = []
        for path,dir,files in os.walk(dir_path):
            for file in files:
                if file.endswith("docx") == True and str(file[0]) != "~":  #Locate the docx document and exclude temporary files
                    file_root = path+"\\"+file
                    file_list.append(file_root)
        print("The directory found a total of {0} related files!".format(len(file_list)))
        return file_list
    
    class ParagraphsKeyWordsReplace:
        '''
            self:paragraph
        '''
        def paragraph_keywords_replace(self,x,key,value):
            '''
            :param x:  paragraph index
            :param key: Key words to be replaced
            :param value: Replace the key words
            :return:
            '''
            keywords_list = [s for s in range(len(self.text)) if self.text.find(key, s) == s] # Retrieve the number of occurrences of the Key in this paragraph and record the starting position in the List
            # there if use: while self.text.find(key) >= 0,When {"ab":" ABC "} is encountered, it will enter an infinite loop
            while len(keywords_list)>0:             #If this paragraph contains more than one key, you need to iterate
                index_list = [] #Gets the index value for all characters in this paragraph
                for y, run in enumerate(self.runs):  # Read the index of run
                    for z, char in enumerate(list(run.text)):  # Read the index of the chars in the run
                        position = {"run": y, "char": z}  # Give each character a dictionary index
                        index_list.append(position)
                # print(index_list)
                start_i = keywords_list.pop()   # Fetch the starting position containing the key from the back to the front of the list
                end_i = start_i + len(key)      # Determine where the key word ends in the paragraph
                keywords_index_list = index_list[start_i:end_i]  # Intercept the section of a list that contains keywords in a paragraph
                # print(keywords_index_list)
                # return keywords_index_list    #Returns a list of coordinates for the chars associated with keywords
                ParagraphsKeyWordsReplace.character_replace(self, keywords_index_list, value)
                # print(f"Successful replacement:{key}===>{value}")
    
        def character_replace(self,keywords_index_list,value):
            '''
            :param keywords_index_list: A list of indexed dictionaries containing keywords
            :param value: The new word after the replacement
            : return:
            Receive parameters and delete the characters in keywords_index_list back-to-back, reserving the first character to replace with value
            Note: Do not delete the list in reverse order, otherwise the list length change will cause a string index out of range error
            '''
            while len(keywords_index_list) > 0:
                dict = keywords_index_list.pop()    #Deletes the last element and returns its value
                y = dict["run"]
                z = dict["char"]
                run = self.runs[y]
                char = self.runs[y].text[z]
                if len(keywords_index_list) > 0:
                    run.text = run.text.replace(char, "")       #Delete the [1:] character
                elif len(keywords_index_list) == 0:
                    run.text = run.text.replace(char, value)    #Replace the 0th character
    
    class DocxKeyWordsReplace:
        '''
            self:docx
        '''
        def content(self,replace_dict):
            print("Please wait for a moment, the body content is processed...")
            for key, value in tqdm.tqdm(replace_dict.items()):
                for x,paragraph in enumerate(self.paragraphs):
                    ParagraphsKeyWordsReplace.paragraph_keywords_replace(paragraph,x,key,value)
    
        def tables(self,replace_dict):
            print("Please wait for a moment, the body tables is processed...")
            for key,value in tqdm.tqdm(replace_dict.items()):
                for i,table in enumerate(self.tables):
                    for j,row in enumerate(table.rows):
                        for cell in row.cells:
                            for x,paragraph in enumerate(cell.paragraphs):
                                ParagraphsKeyWordsReplace.paragraph_keywords_replace(paragraph,x,key,value)
    
        def header_content(self,replace_dict):
            print("Please wait for a moment, the header body content is processed...")
            for key,value in tqdm.tqdm(replace_dict.items()):
                for i,sections in enumerate(self.sections):
                    for x,paragraph in enumerate(self.sections[i].header.paragraphs):
                        ParagraphsKeyWordsReplace.paragraph_keywords_replace(paragraph, x, key, value)
    
        def header_tables(self,replace_dict):
            print("Please wait for a moment, the header body tables is processed...")
            for key,value in tqdm.tqdm(replace_dict.items()):
                for i,sections in enumerate(self.sections):
                    for j,tables in enumerate(self.sections[i].header.tables):
                        for k,row in enumerate(tables[j].rows):
                            for l,cell in row.cells:
                                for x, paragraph in enumerate(cell.paragraphs):
                                    ParagraphsKeyWordsReplace.paragraph_keywords_replace(paragraph, x, key, value)
    
        def footer_content(self, replace_dict):
            print("Please wait for a moment, the footer body content is processed...")
            for key,value in tqdm.tqdm(replace_dict.items()):
                for i, sections in enumerate(self.sections):
                    for x, paragraph in enumerate(self.sections[i].footer.paragraphs):
                        ParagraphsKeyWordsReplace.paragraph_keywords_replace(paragraph, x, key, value)
    
    
        def footer_tables(self, replace_dict):
            print("Please wait for a moment, the footer body tables is processed...")
            for key,value in tqdm.tqdm(replace_dict.items()):
                for i, sections in enumerate(self.sections):
                    for j, tables in enumerate(self.sections[i].footer.tables):
                        for k, row in enumerate(tables[j].rows):
                            for l, cell in row.cells:
                                for x, paragraph in enumerate(cell.paragraphs):
                                    ParagraphsKeyWordsReplace.paragraph_keywords_replace(paragraph, x, key, value)
    
    def main():
        '''
        How to use it: Modify the values in replace_dict and file_dir
        Replace_dict: The following dictionary corresponds to the format, with key as the content to be replaced and value as the new content
        File_dir: The directory where the docx file resides. Supports subdirectories
        '''
        # Input part
        replace_dict = {
            "MG life technology (shenzhen) co., LTD":"Shenzhen YW medical technology co., LTD",
            "MG-":"YW-",
            "2017-":"2020-",
            "Z18":"Z20",
    
            }
        file_dir = r"D:\Working Files\SVN\"
        # Call processing part
        for i,file in enumerate(get_docx_list(file_dir),start=1):
            print(f"{i}、Files in progress:{file}")
            docx = Document(file)
            DocxKeyWordsReplace.content(docx, replace_dict=replace_dict)
            DocxKeyWordsReplace.tables(docx, replace_dict=replace_dict)
            DocxKeyWordsReplace.header_content(docx, replace_dict=replace_dict)
            DocxKeyWordsReplace.header_tables(docx, replace_dict=replace_dict)
            DocxKeyWordsReplace.footer_content(docx, replace_dict=replace_dict)
            DocxKeyWordsReplace.footer_tables(docx, replace_dict=replace_dict)
            docx.save(file)
            print("This document has been processed!\n")
    
    if __name__ == "__main__":
        main()
        print("All complete processing!")
    

    【讨论】:

    • char = self.runs[y].text[z] IndexError: string index out of range,你知道原因吗?
    【解决方案5】:

    https://gist.github.com/heimoshuiyu/671a4dfbd13f7c279e85224a5b6726c0

    这使用“穿梭”,因此它可以找到跨越多个运行的密钥。这类似于 MS Word 中的“全部替换”行为

    def shuttle_text(shuttle):
        t = ''
        for i in shuttle:
            t += i.text
        return t
    
    def docx_replace(doc, data):
        for key in data:
    
            for table in doc.tables:
                for row in table.rows:
                    for cell in row.cells:
                        if key in cell.text:
                            cell.text = cell.text.replace(key, data[key])
    
            for p in doc.paragraphs:
    
                begin = 0
                for end in range(len(p.runs)):
    
                    shuttle = p.runs[begin:end+1]
    
                    full_text = shuttle_text(shuttle)
                    if key in full_text:
                        # print('Replace:', key, '->', data[key])
                        # print([i.text for i in shuttle])
    
                        # find the begin
                        index = full_text.index(key)
                        # print('full_text length', len(full_text), 'index:', index)
                        while index >= len(p.runs[begin].text):
                            index -= len(p.runs[begin].text)
                            begin += 1
    
                        shuttle = p.runs[begin:end+1]
    
                        # do replace
                        # print('before replace', [i.text for i in shuttle])
                        if key in shuttle[0].text:
                            shuttle[0].text = shuttle[0].text.replace(key, data[key])
                        else:
                            replace_begin_index = shuttle_text(shuttle).index(key)
                            replace_end_index = replace_begin_index + len(key)
                            replace_end_index_in_last_run = replace_end_index - len(shuttle_text(shuttle[:-1]))
                            shuttle[0].text = shuttle[0].text[:replace_begin_index] + data[key]
    
                            # clear middle runs
                            for i in shuttle[1:-1]:
                                i.text = ''
    
                            # keep last run
                            shuttle[-1].text = shuttle[-1].text[replace_end_index_in_last_run:]
    
                        # print('after replace', [i.text for i in shuttle])
    
                        # set begin to next
                        begin = end
    
    # usage
    
    doc = docx.Document('path/to/template.docx')
    docx_replace(doc, dict(ItemOne='replacement text', ItemTwo="Some replacement text\nand some more")
    doc.save('path/to/destination.docx')
    

    【讨论】:

      猜你喜欢
      • 2021-06-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多