docx4j найти и заменить

У меня есть документ 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));
            }
        }
    }
}

Но это работает редко, потому что обычно заполнители разделяются на несколько текстовых прогонов.

Я пробовал UnmarshallFromTemplate, но он тоже работает редко.

Как решить эту проблему?

3 ответа

Ты можешь использовать VariableReplace достичь того, чего, возможно, не было во время других ответов. Это не делает поиск / замену как таковой, но работает на заполнителях, например $(myField)

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

Обратите внимание, что вы не проходите $(myField) как имя поля; скорее передайте имя неэкранированного поля myField - Это довольно негибко в том смысле, что в настоящее время ваши заполнители должны иметь формат $(xyz) тогда как если бы вы могли передать что-нибудь, то вы могли бы использовать это для любого поиска / замены. Возможность использовать это также существует для людей C# в docx4j.NET

Смотрите здесь для получения дополнительной информации о VariableReplace или здесь для VariablePrepare

Я создал библиотеку для публикации своего решения, потому что это довольно много кода: https://github.com/phip1611/docx4j-search-and-replace-util

Рабочий процесс следующий:

Первый шаг:

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

Таким образом, мы получаем весь фактический текстовый контент в правильном порядке, но без промежуточной разметки стилей. Мы можем редактировать текстовые объекты (по setValue) и сохранять стили.

Результирующая проблема: поисковый текст / заполнители могут быть разделены на несколько экземпляров Text (поскольку в исходном документе может быть невидимая между ними разметка стиля), например${FOOBAR}, ${ + FOOBAR}, или $ + {FOOB + AR}

Второй шаг:

Объединить все Text-объекты в полную строку / "полную строку"

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

Третий шаг:

Создать класс TextMetaItem. Каждый TextMetaItem знает для своего Text-объекта, где его содержимое начинается и заканчивается полной строкой. Например, если текстовые объекты для "foo" и "bar" приводят к полной строке "foobar", чем индексы0-2 принадлежит "foo"-Text-object а также 3-5 к "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;
}

Четвертый шаг:

Построить 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;
}

промежуточный результат:

Теперь у вас есть достаточно метаданных, чтобы делегировать каждое действие, которое вы хотите выполнить с полной строкой, соответствующему объекту Text! (Чтобы изменить содержимое Text-объектов, вам просто нужно вызвать (#setValue()). Это все, что нужно в Docx4J для редактирования текста. Вся информация о стилях и т. Д. Будет сохранена!

последний шаг: поиск и замена

  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;
    } 
    

    используя это, я создаю новый список ReplaceCommandс. АReplaceCommand является простым классом и хранит FoundResultи новое значение.

  2. далее вы должны упорядочить этот список от последнего элемента к первому (по позиции в полной строке)

  3. теперь вы можете написать алгоритм замены всех, потому что вы знаете, какое действие нужно выполнить над каким Text-объектом. Мы сделали (2), чтобы операции замены не делали недействительными индексы другихFoundResultс.

    3.1.) Найти текстовые объекты, которые необходимо изменить 3.2.) Вызвать getValue() для них 3.3.) Изменить строку на новое значение 3.4.) Вызвать setValue() для текстовых объектов

Это код, который творит всю магию. Он выполняет одну команду 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);
        }
    }

Добрый день, я сделал пример, как быстро заменить текст на что-то, что вам нужно, с помощью регулярного выражения. Я нахожу ${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;
  }

Это может быть проблемой. Я расскажу, как смягчить разбитые текстовые прогоны в этом ответе здесь: /questions/26166271/shablon-docx-docx4j-zamenyaet-tekst-v-java/26166287#26166287

... но вы могли бы вместо этого рассмотреть элементы управления контентом. На исходном сайте docx4j представлены различные примеры контроля контента:

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

Другие вопросы по тегам