Java docx4j 查找和替换

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19676282/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-12 19:19:00  来源:igfitidea点击:

docx4j find and replace

javadocxdocx4j

提问by luckyi

I have docx document with some placeholders. Now I should replace them with other content and save new docx document. I started with docx4jand found this method:

我有一些带有占位符的 docx 文档。现在我应该用其他内容替换它们并保存新的 docx 文档。我从docx4j开始,发现了这个方法:

public static List<Object> getAllElementFromObject(Object obj, Class<?> toSearch) {
    List<Object> result = new ArrayList<Object>();
    if (obj instanceof JAXBElement) obj = ((JAXBElement<?>) obj).getValue();

    if (obj.getClass().equals(toSearch))
        result.add(obj);
    else if (obj instanceof ContentAccessor) {
        List<?> children = ((ContentAccessor) obj).getContent();
        for (Object child : children) {
            result.addAll(getAllElementFromObject(child, toSearch));
        }
    }
    return result;
}

public static void findAndReplace(WordprocessingMLPackage doc, String toFind, String replacer){
    List<Object> paragraphs = getAllElementFromObject(doc.getMainDocumentPart(), P.class);
    for(Object par : paragraphs){
        P p = (P) par;
        List<Object> texts = getAllElementFromObject(p, Text.class);
        for(Object text : texts){
            Text t = (Text)text;
            if(t.getValue().contains(toFind)){
                t.setValue(t.getValue().replace(toFind, replacer));
            }
        }
    }
}

But that only work rarely because usually the placeholders splits across multiple texts runs.

但这很少起作用,因为占位符通常会拆分到多个文本运行中。

I tried UnmarshallFromTemplatebut it work rarely too.

我试过UnmarshallFromTemplate但它也很少工作。

How this problem could be solved?

如何解决这个问题?

回答by Ben

This can be a problem. I cover how to mitigate broken-up text runs in this answer here: https://stackoverflow.com/a/17066582/125750

这可能是一个问题。我在此处介绍了如何减轻此答案中的破碎文本运行:https: //stackoverflow.com/a/17066582/125750

... but you might want to consider content controls instead. The docx4j source site has various content control samples here:

...但您可能需要考虑内容控制。docx4j 源站点在这里有各种内容控制示例:

https://github.com/plutext/docx4j/tree/master/src/samples/docx4j/org/docx4j/samples

https://github.com/plutext/docx4j/tree/master/src/samples/docx4j/org/docx4j/samples

回答by lunicon

Good day, I made an example how to quickly replace text to something you need by regexp. I find ${param.sumname} and replace it in document. Note, you have to insert text as 'text only'! Have fun!

美好的一天,我举了一个例子,如何通过正则表达式快速将文本替换为您需要的内容。我找到 ${param.sumname} 并在文档中替换它。请注意,您必须将文本插入为“仅文本”!玩得开心!

  WordprocessingMLPackage mlp = WordprocessingMLPackage.load(new File("filepath"));
  replaceText(mlp.getMainDocumentPart());

  static void replaceText(ContentAccessor c)
    throws Exception
  {
    for (Object p: c.getContent())
    {
      if (p instanceof ContentAccessor)
        replaceText((ContentAccessor) p);

      else if (p instanceof JAXBElement)
      {
        Object v = ((JAXBElement) p).getValue();

        if (v instanceof ContentAccessor)
          replaceText((ContentAccessor) v);

        else if (v instanceof org.docx4j.wml.Text)
        {
          org.docx4j.wml.Text t = (org.docx4j.wml.Text) v;
          String text = t.getValue();

          if (text != null)
          {
            t.setSpace("preserve"); // needed?
            t.setValue(replaceParams(text));
          }
        }
      }
    }
  }

  static Pattern paramPatern = Pattern.compile("(?i)(\$\{([\w\.]+)\})");

  static String replaceParams(String text)
  {
    Matcher m = paramPatern.matcher(text);

    if (!m.find())
      return text;

    StringBuffer sb = new StringBuffer();
    String param, replacement;

    do
    {
      param = m.group(2);

      if (param != null)
      {
        replacement = getParamValue(param);
        m.appendReplacement(sb, replacement);
      }
      else
        m.appendReplacement(sb, "");
    }
    while (m.find());

    m.appendTail(sb);
    return sb.toString();
  }

  static String getParamValue(String name)
  {
    // replace from map or something else
    return name;
  }

回答by wal

You can use VariableReplaceto acheive this which may not have existed at the time of the other answers. This does not do a find/replace per se but works on placeholders eg $(myField)

您可以使用VariableReplace来实现在其他答案时可能不存在的这一点。这本身不会进行查找/替换,而是适用于占位符,例如$(myField)

java.util.HashMap mappings = new java.util.HashMap();
VariablePrepare.prepare(wordMLPackage);//see notes
mappings.put("myField", "foo");
wordMLPackage.getMainDocumentPart().variableReplace(mappings);

Note that you do not pass $(myField)as the field name; rather pass the unescaped field name myField- This is rather inflexible in that as it currently stands your placeholders must be of the format $(xyz)whereas if you could pass in anything then you could use it for any find/replace. The ability to use this also exists for C# people in docx4j.NET

请注意,您不$(myField)作为字段名称传递;而是传递未转义的字段名称myField- 这是相当不灵活的,因为它目前代表您的占位符必须具有格式,$(xyz)而如果您可以传递任何内容,那么您可以将其用于任何查找/替换。docx4j.NET 中的 C# 人员也可以使用此功能

See herefor more info on VariableReplaceor herefor VariablePrepare

这里获取更多信息的VariableReplace在此进行VariablePrepare

回答by phip1611

I created a library to publish my solution because it's quite a lot of code: https://github.com/phip1611/docx4j-search-and-replace-util

我创建了一个库来发布我的解决方案,因为它有很多代码:https: //github.com/phip1611/docx4j-search-and-replace-util

The workflow is the following:

工作流程如下:

First step:

第一步:

// (this method was part of your question)  
List<Text> texts = getAllElementFromObject(docxDocument.getMainDocumentPart(), Text.class);

This way we get all actual Text-content in the correct order but without style markup in-between. We can edit the Text-objects (by setValue) and keep styles.

通过这种方式,我们以正确的顺序获得所有实际的文本内容,但中间没有样式标记。我们可以编辑文本对象(通过 setValue)并保持样式。

Resulting problem:Search-text/placeholders can be split accoss multiple Text-instances (because there can be style markup that is invisble in-between in original document), e.g. ${FOOBAR}, ${+ FOOBAR}, or $+ {FOOB+ AR}

随之而来的问题:搜索文本/占位符可以分割accoss多个文本实例(因为可以有风格的标记是invisble中的中间原件),例如${FOOBAR}${+ FOOBAR}$+ {FOOB+AR}

Second step:

第二步:

Concat all Text-objects to a full string / "complete string"

将所有文本对象连接到一个完整的字符串/“完整的字符串”

Optional<String> completeStringOpt = texts.stream().map(Text::getValue).reduce(String::concat);

Third step:

第三步:

Create a class TextMetaItem. Each TextMetaItem knows for it's Text-object where it's content begins and ends in the complete string. E.g. If the Text-objects for "foo" and "bar" results in the complete string "foobar" than indices 0-2belongs to "foo"-Text-objectand 3-5to "bar"-Text-object. Build a List<TextMetaItem>

创建一个类TextMetaItem。每个 TextMetaItem 都知道它的文本对象,它的内容以完整字符串开始和结束。例如,如果“foo”和“bar”的文本对象产生完整的字符串“foobar”,则索引0-2属于"foo"-Text-object3-5to "bar"-Text-object。建个List<TextMetaItem>

static List<TextMetaItem> buildMetaItemList(List<Text> texts) {
    final int[] index = {0};
    final int[] iteration = {0};
    List<TextMetaItem> list = new ArrayList<>();
    texts.forEach(text -> {
        int length = text.getValue().length();
        list.add(new TextMetaItem(index[0], index[0] + length - 1, text, iteration[0]));
        index[0] += length;
        iteration[0]++;
    });
    return list;
}

Fourth step:

第四步:

Build a Map<Integer, TextMetaItem>where the key is the index/char in the complete string. This means the map's length equals completeString.length()

构建一个Map<Integer, TextMetaItem>,其中键是完整字符串中的索引/字符。这意味着地图的长度等于completeString.length()

static Map<Integer, TextMetaItem> buildStringIndicesToTextMetaItemMap(List<Text> texts) {
    List<TextMetaItem> metaItemList = buildMetaItemList(texts);
    Map<Integer, TextMetaItem> map = new TreeMap<>();
    int currentStringIndicesToTextIndex = 0;
    // + 1 important here! 
    int max = metaItemList.get(metaItemList.size() - 1).getEnd() + 1;
    for (int i = 0; i < max; i++) {
        TextMetaItem currentTextMetaItem = metaItemList.get(currentStringIndicesToTextIndex);
        map.put(i, currentTextMetaItem);
        if (i >= currentTextMetaItem.getEnd()) {
            currentStringIndicesToTextIndex++;
        }
    }
    return map;
}

interim result:

中期结果:

Now you have enough metadata to delegate every action you want to do on the complete string to the corresponding Text object! (To change the content of Text-objects you just need to call (#setValue()) That's all what's needed in Docx4J to edit text. All style info etc will be preserved!

现在您有足够的元数据将您想要对完整字符串执行的每个操作委托给相应的 Text 对象!(要更改文本对象的内容,您只需要调用 (#setValue())这就是 Docx4J 中编辑文本所需的全部内容。所有样式信息等都将被保留!

last step: search and replace

最后一步:搜索和替换

  1. build a method that finds all occurrences of your possible placeholders. You should create a class like FoundResult(int start, int end)that stores begin and end indices of a found value (placeholder) in the complete string

    public static List<FoundResult> findAllOccurrencesInString(String data, String search) {
        List<FoundResult> list = new ArrayList<>();
        String remaining = data;
        int totalIndex = 0;
        while (true) {
            int index = remaining.indexOf(search);
            if (index == -1) {
                break;
            }
    
            int throwAwayCharCount = index + search.length();
            remaining = remaining.substring(throwAwayCharCount);
    
            list.add(new FoundResult(totalIndex + index, search));
    
            totalIndex += throwAwayCharCount;
        }
        return list;
    } 
    

    using this I build a new list of ReplaceCommands. A ReplaceCommandis a simple class and stores a FoundResultand the new value.

  2. next you mustorder this list from the last item to the first (order by position in complete string)

  3. now you can write a replace all algorithm because you know what action needs to be done on which Text-object. We did (2) so that replace operations won't invalidate indices of other FoundResults.

    3.1.) find Text-object(s) that needs to be changed 3.2.) call getValue() on them 3.3.) edit the string to the new value 3.4.) call setValue() on the Text-objects

  1. 构建一个方法来查找所有可能出现的占位符。您应该创建一个类似的类FoundResult(int start, int end),在完整字符串中存储找到的值(占位符)的开始和结束索引

    public static List<FoundResult> findAllOccurrencesInString(String data, String search) {
        List<FoundResult> list = new ArrayList<>();
        String remaining = data;
        int totalIndex = 0;
        while (true) {
            int index = remaining.indexOf(search);
            if (index == -1) {
                break;
            }
    
            int throwAwayCharCount = index + search.length();
            remaining = remaining.substring(throwAwayCharCount);
    
            list.add(new FoundResult(totalIndex + index, search));
    
            totalIndex += throwAwayCharCount;
        }
        return list;
    } 
    

    使用这个我建立了一个新的ReplaceCommands列表。AReplaceCommand是一个简单的类并存储 aFoundResult新值

  2. 接下来,您必须从最后一项到第一项对此列表进行排序(按完整字符串中的位置排序)

  3. 现在您可以编写替换所有算法,因为您知道需要对哪个文本对象执行什么操作。我们做了 (2) 以便替换操作不会使其他FoundResults 的索引无效。

    3.1.) 找到需要更改的文本对象 3.2.) 对它们调用 getValue() 3.3.) 将字符串编辑为新值 3.4.) 在文本对象上调用 setValue()

This is the code that does all the magic. It executes a single ReplaceCommand.

这是实现所有魔法的代码。它执行单个 ReplaceCommand。

   /**
     * @param texts All Text-objects
     * @param replaceCommand Command
     * @param map Lookup-Map from index in complete string to TextMetaItem
     */
    public static void executeReplaceCommand(List<Text> texts, ReplaceCommand replaceCommand, Map<Integer, TextMetaItem> map) {
        TextMetaItem tmi1 = map.get(replaceCommand.getFoundResult().getStart());
        TextMetaItem tmi2 = map.get(replaceCommand.getFoundResult().getEnd());
        if (tmi2.getPosition() - tmi1.getPosition() > 0) {
            // it can happen that text objects are in-between
            // we can remove them (set to null)
            int upperBorder = tmi2.getPosition();
            int lowerBorder = tmi1.getPosition() + 1;
            for (int i = lowerBorder; i < upperBorder; i++) {
                texts.get(i).setValue(null);
            }
        }

       if (tmi1.getPosition() == tmi2.getPosition()) {
            // do replacement inside a single Text-object

            String t1 = tmi1.getText().getValue();
            int beginIndex = tmi1.getPositionInsideTextObject(replaceCommand.getFoundResult().getStart());
            int endIndex = tmi2.getPositionInsideTextObject(replaceCommand.getFoundResult().getEnd());

            String keepBefore = t1.substring(0, beginIndex);
            String keepAfter = t1.substring(endIndex + 1);

            tmi1.getText().setValue(keepBefore + replaceCommand.getNewValue() + keepAfter);
        } else {
            // do replacement across two Text-objects

            // check where to start and replace 
            // the Text-objects value inside both Text-objects
            String t1 = tmi1.getText().getValue();
            String t2 = tmi2.getText().getValue();

            int beginIndex = tmi1.getPositionInsideTextObject(replaceCommand.getFoundResult().getStart());
            int endIndex = tmi2.getPositionInsideTextObject(replaceCommand.getFoundResult().getEnd());

            t1 = t1.substring(0, beginIndex);
            t1 = t1.concat(replaceCommand.getNewValue());
            t2 = t2.substring(endIndex + 1);

            tmi1.getText().setValue(t1);
            tmi2.getText().setValue(t2);
        }
    }