Замена текста в Apache POI XWPF

Я только что нашел библиотеку Apache POI очень полезной для редактирования файлов Word с помощью Java. В частности, я хочу отредактировать DOCX файл с использованием классов XWPF POI Apache. Я не нашел подходящего метода / документации, после которого я мог бы это сделать. Может кто-нибудь объяснить шагами, Как заменить некоторый текст в файле DOCX.

** текст может быть в строке / абзаце или в строке таблицы/колонки

спасибо заранее :)

9 ответов


вам нужен метод XWPFRun.setText (String). Просто проработайте свой путь через файл, пока не найдете xwpfrun интереса, выяснить, что вы хотите новый текст, чтобы быть, и заменить его. (Серия-это последовательность текста с одинаковым форматированием)

вы должны быть в состоянии сделать что-то вроде:

XWPFDocument doc = new XWPFDocument(OPCPackage.open("input.docx"));
for (XWPFParagraph p : doc.getParagraphs()) {
    List<XWPFRun> runs = p.getRuns();
    if (runs != null) {
        for (XWPFRun r : runs) {
            String text = r.getText(0);
            if (text != null && text.contains("needle")) {
                text = text.replace("needle", "haystack");
                r.setText(text, 0);
            }
        }
    }
}
for (XWPFTable tbl : doc.getTables()) {
   for (XWPFTableRow row : tbl.getRows()) {
      for (XWPFTableCell cell : row.getTableCells()) {
         for (XWPFParagraph p : cell.getParagraphs()) {
            for (XWPFRun r : p.getRuns()) {
              String text = r.getText(0);
              if (text != null && text.contains("needle")) {
                text = text.replace("needle", "haystack");
                r.setText(text,0);
              }
            }
         }
      }
   }
}
doc.write(new FileOutputStream("output.docx"));

вот что мы сделали для замены текста с помощью Apache POI. Мы обнаружили, что это не стоило хлопот и проще заменить текст всего XWPFParagraph вместо запуска. Запуск может быть случайным образом разделен в середине слова, поскольку Microsoft Word отвечает за то, где запуски создаются в абзаце документа. Поэтому текст, который вы можете искать, может быть наполовину в одном запуске и наполовину в другом. Используя полный текст абзаца, удаляя его существующие прогоны, и добавление нового прогона с скорректированным текстом, похоже, решает проблему замены текста.

однако существует стоимость выполнения замены на уровне абзаца; вы теряете форматирование запусков в этом абзаце. Например, если в середине абзаца вы выделили слово "bits" жирным шрифтом, а затем при анализе файла вы заменили слово "bits" на "bytes", слово "bytes" больше не будет выделено жирным шрифтом. Потому что полужирный шрифт был сохранен с запуском, который был удален, когда весь текст этого пункта был заменен. В прилагаемом коде есть раздел комментариев, который работает для замены текста на уровне выполнения, если вам это нужно.

следует также отметить, что ниже работает, если текст, который вы вставляете, содержит \ N возвращаемых символов. Мы не могли найти способ вставить возвраты без создания запуска для каждого раздела до возврата и маркировки запуска addCarriageReturn(). Ура!--2-->

    package com.healthpartners.hcss.client.external.word.replacement;

import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;

public class TextReplacer {
    private String searchValue;
    private String replacement;

    public TextReplacer(String searchValue, String replacement) {
        this.searchValue = searchValue;
        this.replacement = replacement;
    }

    public void replace(XWPFDocument document) {
        List<XWPFParagraph> paragraphs = document.getParagraphs();

    for (XWPFParagraph xwpfParagraph : paragraphs) {
        replace(xwpfParagraph);
    }
}

private void replace(XWPFParagraph paragraph) {
    if (hasReplaceableItem(paragraph.getText())) {
        String replacedText = StringUtils.replace(paragraph.getText(), searchValue, replacement);

        removeAllRuns(paragraph);

        insertReplacementRuns(paragraph, replacedText);
    }
}

private void insertReplacementRuns(XWPFParagraph paragraph, String replacedText) {
    String[] replacementTextSplitOnCarriageReturn = StringUtils.split(replacedText, "\n");

    for (int j = 0; j < replacementTextSplitOnCarriageReturn.length; j++) {
        String part = replacementTextSplitOnCarriageReturn[j];

        XWPFRun newRun = paragraph.insertNewRun(j);
        newRun.setText(part);

        if (j+1 < replacementTextSplitOnCarriageReturn.length) {
            newRun.addCarriageReturn();
        }
    }       
}

private void removeAllRuns(XWPFParagraph paragraph) {
    int size = paragraph.getRuns().size();
    for (int i = 0; i < size; i++) {
        paragraph.removeRun(0);
    }
}

private boolean hasReplaceableItem(String runText) {
    return StringUtils.contains(runText, searchValue);
}

//REVISIT The below can be removed if Michele tests and approved the above less versatile replacement version

//  private void replace(XWPFParagraph paragraph) {
//      for (int i = 0; i < paragraph.getRuns().size()  ; i++) {
//          i = replace(paragraph, i);
//      }
//  }

//  private int replace(XWPFParagraph paragraph, int i) {
//      XWPFRun run = paragraph.getRuns().get(i);
//      
//      String runText = run.getText(0);
//      
//      if (hasReplaceableItem(runText)) {
//          return replace(paragraph, i, run);
//      }
//      
//      return i;
//  }

//  private int replace(XWPFParagraph paragraph, int i, XWPFRun run) {
//      String runText = run.getCTR().getTArray(0).getStringValue();
//      
//      String beforeSuperLong = StringUtils.substring(runText, 0, runText.indexOf(searchValue));
//      
//      String[] replacementTextSplitOnCarriageReturn = StringUtils.split(replacement, "\n");
//      
//      String afterSuperLong = StringUtils.substring(runText, runText.indexOf(searchValue) + searchValue.length());
//      
//      Counter counter = new Counter(i);
//      
//      insertNewRun(paragraph, run, counter, beforeSuperLong);
//      
//      for (int j = 0; j < replacementTextSplitOnCarriageReturn.length; j++) {
//          String part = replacementTextSplitOnCarriageReturn[j];
//
//          XWPFRun newRun = insertNewRun(paragraph, run, counter, part);
//          
//          if (j+1 < replacementTextSplitOnCarriageReturn.length) {
//              newRun.addCarriageReturn();
//          }
//      }
//      
//      insertNewRun(paragraph, run, counter, afterSuperLong);
//      
//      paragraph.removeRun(counter.getCount());
//      
//      return counter.getCount();
//  }

//  private class Counter {
//      private int i;
//      
//      public Counter(int i) {
//          this.i = i;
//      }
//      
//      public void increment() {
//          i++;
//      }
//      
//      public int getCount() {
//          return i;
//      }
//  }

//  private XWPFRun insertNewRun(XWPFParagraph xwpfParagraph, XWPFRun run, Counter counter, String newText) {
//      XWPFRun newRun = xwpfParagraph.insertNewRun(counter.i);
//      newRun.getCTR().set(run.getCTR());
//      newRun.getCTR().getTArray(0).setStringValue(newText);
//      
//      counter.increment();
//      
//      return newRun;
//  }

Если кому-то также нужно сохранить форматирование текста, этот код работает лучше.

private static Map<Integer, XWPFRun> getPosToRuns(XWPFParagraph paragraph) {
    int pos = 0;
    Map<Integer, XWPFRun> map = new HashMap<Integer, XWPFRun>(10);
    for (XWPFRun run : paragraph.getRuns()) {
        String runText = run.text();
        if (runText != null) {
            for (int i = 0; i < runText.length(); i++) {
                map.put(pos + i, run);
            }
            pos += runText.length();
        }
    }
    return (map);
}

public static <V> void replace(XWPFDocument document, Map<String, V> map) {
    List<XWPFParagraph> paragraphs = document.getParagraphs();
    for (XWPFParagraph paragraph : paragraphs) {
        replace(paragraph, map);
    }
}

public static <V> void replace(XWPFDocument document, String searchText, V replacement) {
    List<XWPFParagraph> paragraphs = document.getParagraphs();
    for (XWPFParagraph paragraph : paragraphs) {
        replace(paragraph, searchText, replacement);
    }
}

private static <V> void replace(XWPFParagraph paragraph, Map<String, V> map) {
    for (Map.Entry<String, V> entry : map.entrySet()) {
        replace(paragraph, entry.getKey(), entry.getValue());
    }
}

public static <V> void replace(XWPFParagraph paragraph, String searchText, V replacement) {
    boolean found = true;
    while (found) {
        found = false;
        int pos = paragraph.getText().indexOf(searchText);
        if (pos >= 0) {
            found = true;
            Map<Integer, XWPFRun> posToRuns = getPosToRuns(paragraph);
            XWPFRun run = posToRuns.get(pos);
            XWPFRun lastRun = posToRuns.get(pos + searchText.length() - 1);
            int runNum = paragraph.getRuns().indexOf(run);
            int lastRunNum = paragraph.getRuns().indexOf(lastRun);
            String texts[] = replacement.toString().split("\n");
            run.setText(texts[0], 0);
            XWPFRun newRun = run;
            for (int i = 1; i < texts.length; i++) {
                newRun.addCarriageReturn();
                newRun = paragraph.insertNewRun(runNum + i);
                /*
                    We should copy all style attributes
                    to the newRun from run
                    also from background color, ...
                    Here we duplicate only the simple attributes...
                 */
                newRun.setText(texts[i]);
                newRun.setBold(run.isBold());
                newRun.setCapitalized(run.isCapitalized());
                // newRun.setCharacterSpacing(run.getCharacterSpacing());
                newRun.setColor(run.getColor());
                newRun.setDoubleStrikethrough(run.isDoubleStrikeThrough());
                newRun.setEmbossed(run.isEmbossed());
                newRun.setFontFamily(run.getFontFamily());
                newRun.setFontSize(run.getFontSize());
                newRun.setImprinted(run.isImprinted());
                newRun.setItalic(run.isItalic());
                newRun.setKerning(run.getKerning());
                newRun.setShadow(run.isShadowed());
                newRun.setSmallCaps(run.isSmallCaps());
                newRun.setStrikeThrough(run.isStrikeThrough());
                newRun.setSubscript(run.getSubscript());
                newRun.setUnderline(run.getUnderline());
            }
            for (int i = lastRunNum + texts.length - 1; i > runNum + texts.length - 1; i--) {
                paragraph.removeRun(i);
            }
        }
    }
}

моей задачей было заменить тексты формата ${key} значениями карты в документе word docx. Вышеприведенные решения были хорошей отправной точкой, но не учитывали все случаи: ${key} можно распространять не только на несколько запусков, но и на несколько текстов в рамках запуска. Поэтому я получил следующий код:

    private void replace(String inFile, Map<String, String> data, OutputStream out) throws Exception, IOException {
    XWPFDocument doc = new XWPFDocument(OPCPackage.open(inFile));
    for (XWPFParagraph p : doc.getParagraphs()) {
        replace2(p, data);
    }
    for (XWPFTable tbl : doc.getTables()) {
        for (XWPFTableRow row : tbl.getRows()) {
            for (XWPFTableCell cell : row.getTableCells()) {
                for (XWPFParagraph p : cell.getParagraphs()) {
                    replace2(p, data);
                }
            }
        }
    }
    doc.write(out);
}

private void replace2(XWPFParagraph p, Map<String, String> data) {
    String pText = p.getText(); // complete paragraph as string
    if (pText.contains("${")) { // if paragraph does not include our pattern, ignore
        TreeMap<Integer, XWPFRun> posRuns = getPosToRuns(p);
        Pattern pat = Pattern.compile("\$\{(.+?)\}");
        Matcher m = pat.matcher(pText);
        while (m.find()) { // for all patterns in the paragraph
            String g = m.group(1);  // extract key start and end pos
            int s = m.start(1);
            int e = m.end(1);
            String key = g;
            String x = data.get(key);
            if (x == null)
                x = "";
            SortedMap<Integer, XWPFRun> range = posRuns.subMap(s - 2, true, e + 1, true); // get runs which contain the pattern
            boolean found1 = false; // found $
            boolean found2 = false; // found {
            boolean found3 = false; // found }
            XWPFRun prevRun = null; // previous run handled in the loop
            XWPFRun found2Run = null; // run in which { was found
            int found2Pos = -1; // pos of { within above run
            for (XWPFRun r : range.values())
            {
                if (r == prevRun)
                    continue; // this run has already been handled
                if (found3)
                    break; // done working on current key pattern
                prevRun = r;
                for (int k = 0;; k++) { // iterate over texts of run r
                    if (found3)
                        break;
                    String txt = null;
                    try {
                        txt = r.getText(k); // note: should return null, but throws exception if the text does not exist
                    } catch (Exception ex) {

                    }
                    if (txt == null)
                        break; // no more texts in the run, exit loop
                    if (txt.contains("$") && !found1) {  // found $, replace it with value from data map
                        txt = txt.replaceFirst("\$", x);
                        found1 = true;
                    }
                    if (txt.contains("{") && !found2 && found1) {
                        found2Run = r; // found { replace it with empty string and remember location
                        found2Pos = txt.indexOf('{');
                        txt = txt.replaceFirst("\{", "");
                        found2 = true;
                    }
                    if (found1 && found2 && !found3) { // find } and set all chars between { and } to blank
                        if (txt.contains("}"))
                        {
                            if (r == found2Run)
                            { // complete pattern was within a single run
                                txt = txt.substring(0, found2Pos)+txt.substring(txt.indexOf('}'));
                            }
                            else // pattern spread across multiple runs
                                txt = txt.substring(txt.indexOf('}'));
                        }
                        else if (r == found2Run) // same run as { but no }, remove all text starting at {
                            txt = txt.substring(0,  found2Pos);
                        else
                            txt = ""; // run between { and }, set text to blank
                    }
                    if (txt.contains("}") && !found3) {
                        txt = txt.replaceFirst("\}", "");
                        found3 = true;
                    }
                    r.setText(txt, k);
                }
            }
        }
        System.out.println(p.getText());

    }

}

private TreeMap<Integer, XWPFRun> getPosToRuns(XWPFParagraph paragraph) {
    int pos = 0;
    TreeMap<Integer, XWPFRun> map = new TreeMap<Integer, XWPFRun>();
    for (XWPFRun run : paragraph.getRuns()) {
        String runText = run.text();
        if (runText != null && runText.length() > 0) {
            for (int i = 0; i < runText.length(); i++) {
                map.put(pos + i, run);
            }
            pos += runText.length();
        }

    }
    return map;
}

первый кусок кода-это исключение NullPointerException, кто-нибудь знает, что не так?

run.getText (int position) - из документации: Возвращается: текст этого текста run или null, если не установлен

просто проверьте, не является ли он null перед вызовом contains () на нем

и кстати, если вы хотите заменить текст, вам нужно установить его в положение, из которого вы его получите, в этом случае r.setText (text, 0);. В противном случае текст будет добавлен не заменено


ответ, принятый здесь, нуждается в еще одном обновлении вместе с обновлением Justin Skiles. Р.помощью setText(текст, 0); Причина: если не обновлять setText с переменной pos, выход будет комбинацией старой строки и строки замены.


есть replaceParagraph реализация, которая заменяет ${key} С value (the fieldsForReport параметр) и сохраняет формат путем слияния runs содержание ${key}.

private void replaceParagraph(XWPFParagraph paragraph, Map<String, String> fieldsForReport) throws POIXMLException {
    String find, text, runsText;
    List<XWPFRun> runs;
    XWPFRun run, nextRun;
    for (String key : fieldsForReport.keySet()) {
        text = paragraph.getText();
        if (!text.contains("${"))
            return;
        find = "${" + key + "}";
        if (!text.contains(find))
            continue;
        runs = paragraph.getRuns();
        for (int i = 0; i < runs.size(); i++) {
            run = runs.get(i);
            runsText = run.getText(0);
            if (runsText.contains("${") || (runsText.contains("$") && runs.get(i + 1).getText(0).substring(0, 1).equals("{"))) {
                while (!runsText.contains("}")) {
                    nextRun = runs.get(i + 1);
                    runsText = runsText + nextRun.getText(0);
                    paragraph.removeRun(i + 1);
                }
                run.setText(runsText.contains(find) ?
                        runsText.replace(find, fieldsForReport.get(key)) :
                        runsText, 0);
            }
        }
    }
}

реализация replaceParagraph

единица теста


Я предлагаю свое решение для замены текста между#, например:эта # закладка# должна быть заменена. Это заменить на:

  • абзацы;
  • таблицы;
  • колонтитулы.

кроме того, он учитывает ситуации, когда символ # и закладка находятся в разделенных запусках ( заменить переменную между различными прогонами).

здесь ссылка на код: https://gist.github.com/aerobium/bf02e443c079c5caec7568e167849dda


на дату написания ни один из ответов не заменяется должным образом.

ответ Gagravars не включает случаи, когда слова для замены разбиваются на прогоны; решение Thierry Boduins иногда оставляло слова для замены пустыми, когда они были после других слов для замены, также оно не проверяет таблицы.

используя ответ Gagtavars в качестве базы, я также проверил запуск перед текущим запуском, если текст обоих запусков содержит слово для замены, добавив блок else. Мое добавление в Котлин:

if (text != null) {
        if (text.contains(findText)) {
            text = text.replace(findText, replaceText)
            r.setText(text, 0)
        } else if (i > 0 && p.runs[i - 1].getText(0).plus(text).contains(findText)) {
            val pos = p.runs[i - 1].getText(0).indexOf('$')
            text = textOfNotFullSecondRun(text, findText)
            r.setText(text, 0)
            val findTextLengthInFirstRun = findTextPartInFirstRun(p.runs[i - 1].getText(0), findText)
            val prevRunText = p.runs[i - 1].getText(0).replaceRange(pos, findTextLengthInFirstRun, replaceText)
            p.runs[i - 1].setText(prevRunText, 0)
        }
    }

private fun textOfNotFullSecondRun(text: String, findText: String): String {
    return if (!text.contains(findText)) {
        textOfNotFullSecondRun(text, findText.drop(1))
    } else {
        text.replace(findText, "")
    }
}

private fun findTextPartInFirstRun(text: String, findText: String): Int {
    return if (text.contains(findText)) {
        findText.length
    } else {
        findTextPartInFirstRun(text, findText.dropLast(1))
    }
}

Это список запусков в абзаце. То же самое с блоком поиска в таблице. С этим решением у меня пока не было никаких проблем. Все форматирование сохранилось.

Edit: я сделал Java lib для замены, проверьте его:https://github.com/deividasstr/docx-word-replacer