Подтвердить что ты не робот

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

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

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

Спасибо в Advance:)

4b9b3361

Ответ 1

Вам нужен 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"));

Ответ 2

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

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

Следует также отметить, что ниже работает, если текст, который вы вставляете, содержит \n возвращаемых символов. Мы не смогли найти способ вставки результатов, не создавая прогон для каждого раздела до возврата и отмечая пробег addCarriageReturn(). Приветствия

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

Ответ 3

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

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

Ответ 4

моя задача состояла в том, чтобы заменить тексты формата ${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;
}

Ответ 5

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

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

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

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

Ответ 6

Ответ, полученный здесь, требует еще одного обновления вместе с обновлением Justin Skiles. r.setText(текст, 0); Причина. Если не обновлять setText с переменной pos, результатом будет комбинация старой строки и замены строки.

Ответ 7

Существует replaceParagraph реализация, которая заменяет ${key} с value (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

Unit тест

Ответ 8

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

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

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

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

Ответ 9

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

Ответ 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))
    }
}

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

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