У меня есть шаблон .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 несколько случайно (и несколько на основе истории) решит разрезать текст на столько прогонов, сколько ему захочется, и вы не можете это контролировать. Вы просто должны справиться с этим!
Текущий apache poi 4.1.2
предоставляет TextSegment для решения этих Word
проблем с запуском текста. XWPFParagraph.searchText ищет строку в абзаце и возвращает TextSegment
. Это обеспечивает доступ к началу и концу этого текста в этом абзаце (BeginRun
и EndRun
). Он также обеспечивает доступ к положению начального символа в начале цикла и позиции конечного символа в конце цикла (BeginChar
и EndChar
).
Дополнительно предоставляет доступ к индексу текстового элемента в текстовом прогоне (BeginText
и EndText
). Это всегда должно быть 0
, потому что текст по умолчанию имеет только один текстовый элемент.
Имея это, мы можем сделать следующее:
Замените найденную частичную строку в начале выполнения заменой. Для этого возьмите текстовую часть, которая была перед искомой строкой, и соедините с ней замену. После этого стартовый тираж полностью содержит замену.
Удалите все текстовые прогоны между начальным прогоном и конечным прогоном, так как они содержат части искомой строки, которые больше не нужны.
Пусть останется только текстовая часть после искомой строки в конце прогона.
Таким образом, мы можем заменить текст, который находится в нескольких текстовых прогонах.
Следующий пример показывает это.
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)) && (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) {
...
большое спасибо @Axel ... это сработало для меня. Я столкнулся с этой проблемой и искал решение.
Точно так же, как кто-то прокомментировал ваш вопрос, вы не можете контролировать, где или когда 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, чтобы написать этот ответ.
Учитывая, что вы не можете контролировать, когда Word решит разделить данные на разные прогоны, даже если они имеют одинаковое форматирование, почему бы не обновить свою логику, чтобы справиться с прогонами, охватывающими текст?