Reemplazar un texto en Apache POI XWPF

Acabo de encontrar que la biblioteca de POI de Apache es muy útil para editar archivos de Word usando Java. Específicamente, quiero editar un archivo DOCX utilizando las clases XWPF de POI de Apache. No encontré ningún método / documentación adecuada después de lo cual pude hacer esto. ¿Puede alguien explicar por pasos cómo reemplazar texto en un archivo DOCX?

** El texto puede estar en una línea / párrafo o en una fila / columna de la tabla

Gracias por adelantado 🙂

El método que necesita es XWPFRun.setText (String) . Simplemente avance por el archivo hasta encontrar el XWPFRun de interés, determine qué quiere que sea el nuevo texto y reemplácelo. (Una ejecución es una secuencia de texto con el mismo formato)

Debería poder hacer algo como:

XWPFDocument doc = new XWPFDocument(OPCPackage.open("input.docx")); for (XWPFParagraph p : doc.getParagraphs()) { List 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")); 

Esto es lo que hicimos para el reemplazo de texto usando Apache POI. Descubrimos que no valía la pena ni la molestia de reemplazar el texto de un XWPFParagraph completo en lugar de una ejecución. Una ejecución se puede dividir aleatoriamente en el medio de una palabra ya que Microsoft Word está a cargo de donde se crean las ejecuciones dentro del párrafo de un documento. Por lo tanto, el texto que podría estar buscando podría ser la mitad en una ejecución y la mitad en otra. Usar el texto completo de un párrafo, eliminar las ejecuciones existentes y agregar una nueva ejecución con el texto ajustado parece resolver el problema del reemplazo de texto.

Sin embargo, hay un costo de hacer el reemplazo en el nivel de párrafo; pierde el formateo de las ejecuciones en ese párrafo. Por ejemplo, si en el medio de su párrafo había marcado en negritas la palabra “bits”, y luego al analizar el archivo reemplazó la palabra “bits” por “bytes”, la palabra “bytes” ya no estará en negrita. Debido a que la negrita se almacenó con una ejecución que se eliminó cuando se reemplazó todo el cuerpo del texto del párrafo. El código adjunto tiene una sección comentada que funcionaba para reemplazar el texto en el nivel de ejecución si lo necesita.

También debe tenerse en cuenta que lo siguiente funciona si el texto que está insertando contiene \ n caracteres de retorno. No pudimos encontrar una forma de insertar devoluciones sin crear una ejecución para cada sección antes de la devolución y marcando la ejecución addCarriageReturn (). Aclamaciones

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

Si alguien necesita también mantener el formato del texto, este código funciona mejor.

 private static Map getPosToRuns(XWPFParagraph paragraph) { int pos = 0; Map map = new HashMap(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  void replace(XWPFDocument document, Map map) { List paragraphs = document.getParagraphs(); for (XWPFParagraph paragraph : paragraphs) { replace(paragraph, map); } } public static  void replace(XWPFDocument document, String searchText, V replacement) { List paragraphs = document.getParagraphs(); for (XWPFParagraph paragraph : paragraphs) { replace(paragraph, searchText, replacement); } } private static  void replace(XWPFParagraph paragraph, Map map) { for (Map.Entry entry : map.entrySet()) { replace(paragraph, entry.getKey(), entry.getValue()); } } public static  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 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); } } } } 

mi tarea era reemplazar textos del formato $ {key} con valores de un mapa dentro de un documento word docx. Las soluciones anteriores fueron un buen punto de partida, pero no tuvieron en cuenta todos los casos: $ {key} puede extenderse no solo en varias ejecuciones, sino también en varios textos dentro de una ejecución. Por lo tanto, terminé con el siguiente código:

  private void replace(String inFile, Map 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 data) { String pText = p.getText(); // complete paragraph as string if (pText.contains("${")) { // if paragraph does not include our pattern, ignore TreeMap 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 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 getPosToRuns(XWPFParagraph paragraph) { int pos = 0; TreeMap map = new TreeMap(); 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; } 

El primer trozo de código me está dando una NullPointerException, ¿Alguien sabe lo que está mal?

run.getText (int position) – de la documentación: Devuelve: el texto de este texto se ejecuta o nulo si no se establece

Simplemente verifique si no es nulo antes de llamar a contains () en él

Y, por cierto, si quiere reemplazar el texto, debe configurarlo en su lugar desde donde lo obtiene, en este caso r.setText (texto, 0) ;. De lo contrario, se agregará texto no reemplazado

La respuesta aceptada aquí necesita una actualización más junto con la actualización de Justin Skiles. r.setText (texto, 0); Motivo: si no se actualiza setText con la variable pos, la salida será la combinación de la cadena anterior y la cadena reemplazada.

Existe la implementación replaceParagraph que reemplaza ${key} con value (el parámetro fieldsForReport ) y guarda el formato combinando runs contenido de ejecución ${key} .

 private void replaceParagraph(XWPFParagraph paragraph, Map fieldsForReport) throws POIXMLException { String find, text, runsText; List 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); } } } } 

Implementación replaceParagraph

Prueba de unidad

Sugiero mi solución para reemplazar texto entre #, por ejemplo: este # marcador # debe ser reemplazado. Es reemplazar en:

  • párrafos;
  • mesas;
  • pies de página.

Además, toma en cuenta situaciones, cuando el símbolo # y el marcador están en las ejecuciones separadas ( reemplaza la variable entre diferentes ejecuciones ).

Aquí enlace al código: https://gist.github.com/aerobium/bf02e443c079c5caec7568e167849dda

A la fecha de redacción, ninguna de las respuestas se reemplaza correctamente.

La respuesta de Gagravars no incluye los casos en que las palabras para reemplazar se dividen en ejecuciones; La solución Thierry Boduins a veces dejaba palabras para reemplazar el espacio en blanco cuando estaban después de otras palabras para reemplazar, y tampoco revisa las tablas.

Usando la respuesta de Gagtavars como base, también he comprobado la ejecución antes de la ejecución actual si el texto de ambas ejecuciones contiene la palabra para reemplazar, agregando else block. Mi adición en kotlin:

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

es la lista de ejecuciones en un párrafo. Lo mismo con el bloque de búsqueda en la tabla. Con esta solución no tenía ningún problema todavía. Todo el formato está intacto.

Editar: hice una lib de java para reemplazar, compruébalo: https://github.com/deividasstr/docx-word-replacer