xwpfdocument poi library example java ms-word apache-poi xwpf

poi - word java



SustituciĆ³n de un texto en Apache POI XWPF (9)

Acabo de encontrar la biblioteca de Apache POI muy útil para editar archivos de Word usando Java. Específicamente, quiero editar un archivo DOCX utilizando las clases XWPF de Apache POI. No encontré ningún método / documentación adecuada después de lo cual podría hacer esto. ¿Alguien puede explicar en pasos cómo reemplazar un 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 :)


La primera parte del código me está dando una NullPointerException, ¿alguien sabe qué está mal?

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

Solo verifica si no es nulo antes de llamar contiene ().

Y por cierto, si desea reemplazar el texto, debe configurarlo en la posición en la que lo obtuvo, en este caso r.setText (texto, 0) ;. De lo contrario el texto será agregado no reemplazado.


A la fecha de escritura, ninguna de las respuestas reemplaza adecuadamente.

La respuesta de Gagravars no incluye los casos en que las palabras para reemplazar se dividen en carreras; La solución de Thierry Boduins a veces dejaba que las palabras se reemplazaran en blanco cuando estaban detrás de otras palabras para reemplazar, además, no verifica las tablas.

Usando la respuesta de Gagtavars como base, también he revisado la ejecución antes de la ejecución actual si el texto de ambas ejecuciones contiene la palabra para reemplazar, agregando el bloque else. Mi adición en el 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 tuve ningún problema todavía. Todo el formato está intacto.

Edición: hice una libreta de Java para reemplazar, échale un vistazo: https://github.com/deividasstr/docx-word-replacer


El método que necesita es XWPFRun.setText(String) . Simplemente revise el archivo hasta que encuentre el XWPFRun de interés, descubra cuál es el texto nuevo y sustitúyalo. (Una ejecución es una secuencia de texto con el mismo formato)

Deberías poder hacer algo como:

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


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

Sin embargo, hay un costo de hacer el reemplazo en el nivel de párrafo; se pierde el formato de las ejecuciones en ese párrafo. Por ejemplo, si en la mitad de su párrafo había escrito la palabra "bits" en negrita, y luego de analizar el archivo, reemplazó la palabra "bits" por "bytes", la palabra "bytes" ya no estaría en negrita. Porque la negrita se almacenó con una ejecución que se eliminó cuando se reemplazó todo el cuerpo de texto del párrafo. El código adjunto tiene una sección comentada que estaba trabajando 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 marcar 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<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; // }


Existe la implementación replaceParagraph que reemplaza ${key} con value (el parámetro fieldsForReport ) y guarda el formato al combinar runs contenido de las 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); } } } }

Implementación replaceParagraph

Prueba de unidad


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 antigua y la cadena de reemplazo.


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

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


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

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

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

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


mi tarea consistía en reemplazar los textos del formato $ {clave} con los valores de un mapa dentro de un documento docx de word. Las soluciones anteriores fueron un buen punto de partida, pero no tuvieron en cuenta todos los casos: $ {key} se puede distribuir no solo en varias ejecuciones, sino también en múltiples textos dentro de una ejecución. Por eso terminé con el siguiente código:

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