#java #ms-word #apache-poi #xwpf
#java #ms-word #apache-poi #xwpf
Вопрос:
У меня есть шаблон .docx с заполнителями, которые нужно заполнить, например, ${programming_language}
, и т.д. ${education}
Ключевые слова-заполнители должны быть легко отличимы от других простых слов, поэтому они заключены в ${ }
.
for (XWPFTable table : doc.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph paragraph : cell.getParagraphs()) {
for (XWPFRun run : paragraph.getRuns()) {
System.out.println("run text: " run.text());
/** replace text here, etc. */
}
}
}
}
}
Я хочу извлечь заполнители вместе с заключающими ${ }
символами. Проблема в том, что кажется, что заключающие символы обрабатываются как разные прогоны…
run text: ${
run text: programming_language
run text: }
run text: Some plain text here
run text: ${
run text: education
run text: }
Вместо этого я хотел бы добиться следующего эффекта:
run text: ${programming_language}
run text: Some plain text here
run text: ${education}
Я пытался использовать другие заключающие символы, такие как: { }
, < >
, и т.д. # #
Я не хочу делать какие-то странные конкатенации и т.д. runs
Я хочу, чтобы это было в одном XWPFRun.
Если я не смогу найти правильное решение, я просто сделаю это так: VAR_PROGRAMMING_LANGUGE
, VAR_EDUCATION
, я думаю.
Комментарии:
1. Учитывая, что вы не можете контролировать, когда Word решит разделить вещи на разные прогоны, даже если они имеют одинаковое форматирование, почему бы не обновить вашу логику, чтобы справиться с прогонами, охватывающими текст?
2. Что вы подразумеваете под прогонами, охватывающими текст?
3. Нужный вам текст будет содержать более 1 запусков, возможно, это не единственное, что есть в любом из этих запусков. Word несколько случайным образом (и в некоторой степени на основе истории) решит разделить текст на столько прогонов, сколько захочет, и вы не можете это контролировать. Вам просто нужно с этим справиться!
Ответ №1:
Current apache poi 4.1.2
предоставляет TextSegment для решения этих Word
проблем с текстовым запуском. XWPFParagraph.searchText выполняет поиск строки в абзаце и возвращает TextSegment
. Это обеспечивает доступ к началу запуска и конечному запуску этого текста в этом абзаце ( BeginRun
и EndRun
). Он также предоставляет доступ к начальной позиции символа в begin run и конечной позиции символа в end run ( BeginChar
и EndChar
). Он дополнительно предоставляет доступ к индексу текстового элемента в текстовом запуске ( BeginText
и EndText
). Так должно быть всегда 0
, потому что текстовые прогоны по умолчанию содержат только один текстовый элемент.
Имея это, мы можем сделать следующее:
Замените найденную частичную строку в begin run заменой. Для этого возьмите текстовую часть, которая была перед искомой строкой, и объедините с ней замену. После этого запуск begin полностью содержит замену.
Удалите все текстовые прогоны между началом и завершением, поскольку они содержат части искомой строки, которые больше не нужны.
Пусть останется только текстовая часть после искомой строки в конечном запуске.
Таким образом, мы можем заменить текст, который находится в нескольких текстовых прогонах.
Следующий пример показывает это.
import java.io.*;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
public class WordReplaceTextSegment {
static public void replaceTextSegment(XWPFParagraph paragraph, String textToFind, String replacement) {
TextSegment foundTextSegment = null;
PositionInParagraph startPos = new PositionInParagraph(0, 0, 0);
while((foundTextSegment = paragraph.searchText(textToFind, startPos)) != null) { // search all text segments having text to find
System.out.println(foundTextSegment.getBeginRun() ":" foundTextSegment.getBeginText() ":" foundTextSegment.getBeginChar());
System.out.println(foundTextSegment.getEndRun() ":" foundTextSegment.getEndText() ":" foundTextSegment.getEndChar());
// maybe there is text before textToFind in begin run
XWPFRun beginRun = paragraph.getRuns().get(foundTextSegment.getBeginRun());
String textInBeginRun = beginRun.getText(foundTextSegment.getBeginText());
String textBefore = textInBeginRun.substring(0, foundTextSegment.getBeginChar()); // we only need the text before
// maybe there is text after textToFind in end run
XWPFRun endRun = paragraph.getRuns().get(foundTextSegment.getEndRun());
String textInEndRun = endRun.getText(foundTextSegment.getEndText());
String textAfter = textInEndRun.substring(foundTextSegment.getEndChar() 1); // we only need the text after
if (foundTextSegment.getEndRun() == foundTextSegment.getBeginRun()) {
textInBeginRun = textBefore replacement textAfter; // if we have only one run, we need the text before, then the replacement, then the text after in that run
} else {
textInBeginRun = textBefore replacement; // else we need the text before followed by the replacement in begin run
endRun.setText(textAfter, foundTextSegment.getEndText()); // and the text after in end run
}
beginRun.setText(textInBeginRun, foundTextSegment.getBeginText());
// runs between begin run and end run needs to be removed
for (int runBetween = foundTextSegment.getEndRun() - 1; runBetween > foundTextSegment.getBeginRun(); runBetween--) {
paragraph.removeRun(runBetween); // remove not needed runs
}
}
}
public static void main(String[] args) throws Exception {
XWPFDocument doc = new XWPFDocument(new FileInputStream("source.docx"));
String textToFind = "${This is the text to find}"; // might be in different runs
String replacement = "Replacement text";
for (XWPFParagraph paragraph : doc.getParagraphs()) { //go through all paragraphs
if (paragraph.getText().contains(textToFind)) { // paragraph contains text to find
replaceTextSegment(paragraph, textToFind, replacement);
}
}
FileOutputStream out = new FileOutputStream("result.docx");
doc.write(out);
out.close();
doc.close();
}
}
Приведенный выше код работает не во всех случаях, потому XWPFParagraph.searchText
что имеет ошибки. Поэтому я предложу лучший searchText
метод:
/**
* this methods parse the paragraph and search for the string searched.
* If it finds the string, it will return true and the position of the String
* will be saved in the parameter startPos.
*
* @param searched
* @param startPos
*/
static TextSegment searchText(XWPFParagraph paragraph, String searched, PositionInParagraph startPos) {
int startRun = startPos.getRun(),
startText = startPos.getText(),
startChar = startPos.getChar();
int beginRunPos = 0, candCharPos = 0;
boolean newList = false;
//CTR[] rArray = paragraph.getRArray(); //This does not contain all runs. It lacks hyperlink runs for ex.
java.util.List<XWPFRun> runs = paragraph.getRuns();
int beginTextPos = 0, beginCharPos = 0; //must be outside the for loop
//for (int runPos = startRun; runPos < rArray.length; runPos ) {
for (int runPos = startRun; runPos < runs.size(); runPos ) {
//int beginTextPos = 0, beginCharPos = 0, textPos = 0, charPos; //int beginTextPos = 0, beginCharPos = 0 must be outside the for loop
int textPos = 0, charPos;
//CTR ctRun = rArray[runPos];
CTR ctRun = runs.get(runPos).getCTR();
XmlCursor c = ctRun.newCursor();
c.selectPath("./*");
try {
while (c.toNextSelection()) {
XmlObject o = c.getObject();
if (o instanceof CTText) {
if (textPos >= startText) {
String candidate = ((CTText) o).getStringValue();
if (runPos == startRun) {
charPos = startChar;
} else {
charPos = 0;
}
for (; charPos < candidate.length(); charPos ) {
if ((candidate.charAt(charPos) == searched.charAt(0)) amp;amp; (candCharPos == 0)) {
beginTextPos = textPos;
beginCharPos = charPos;
beginRunPos = runPos;
newList = true;
}
if (candidate.charAt(charPos) == searched.charAt(candCharPos)) {
if (candCharPos 1 < searched.length()) {
candCharPos ;
} else if (newList) {
TextSegment segment = new TextSegment();
segment.setBeginRun(beginRunPos);
segment.setBeginText(beginTextPos);
segment.setBeginChar(beginCharPos);
segment.setEndRun(runPos);
segment.setEndText(textPos);
segment.setEndChar(charPos);
return segment;
}
} else {
candCharPos = 0;
}
}
}
textPos ;
} else if (o instanceof CTProofErr) {
c.removeXml();
} else if (o instanceof CTRPr) {
//do nothing
} else {
candCharPos = 0;
}
}
} finally {
c.dispose();
}
}
return null;
}
Это будет вызываться как:
...
while((foundTextSegment = searchText(paragraph, textToFind, startPos)) != null) {
...
Ответ №2:
Точно так же, как кто-то прокомментировал ваш вопрос, вы не можете контролировать, где или когда Word разделит абзац в некоторых прогонах. Если другой ответ вам все еще не помог, тогда у меня есть способ обойти это:
Прежде всего, у этого «решения» есть большая проблема, но все же я приведу ее здесь по той причине, что кто-то может ее решить.
public void mainMethod(XWPFParagraph paragraph) {
if (paragraph.getRuns().size() > 1) {
String myRun = unifyRuns(paragraph.getRuns());
// make the verification of placeholders ${...}
paragraph.getRuns().get(0).setText(myRun);
while(paragraph.getRuns().size() > 1) {
paragraph.removeRun(1);
}
}
}
private String unifyRuns(List<XWPFRun> runElements) {
StringBuilder unifiedRun = new StringBuilder();
for (XWPFRun run : runElements) {
unifiedRun.append(run);
}
return unifiedRun.toString();
}
Код может содержать некоторую ошибку, поскольку я делаю это так, как помню.
Проблема здесь в том, что когда Word разделяет абзацы на прогоны, он делает это не зря, потому что, когда есть тексты с разными шрифтами (например, font-family или font-size), он разделяет тексты в разных прогонах.
В тексте «Вот мой жирный текст» Word разделит текст, чтобы отделить жирный и обычный текст. Тогда приведенный выше код является плохим решением, если вы используете POI для создания больших документов с разными типами шрифтов. В этом случае вам нужно будет сначала проверить, действительно ли прогон выделен жирным шрифтом, затем вы будете обрабатывать заполнители.
Опять же, это «решение», которое я нашел, и оно еще не завершено. Извините за ошибки в английском, я использую Google Translate для написания этого ответа.