POI Apache: ${my_placeholder} рассматривается как три разных запуска

У меня есть шаблон .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, я думаю.

Учитывая, что вы не можете контролировать, когда Word решит разделить данные на разные прогоны, даже если они имеют одинаковое форматирование, почему бы не обновить свою логику, чтобы справиться с прогонами, охватывающими текст?

Gagravarr 13.12.2020 16:12

Что вы подразумеваете под текстовым охватом?

weno 13.12.2020 20:04

Текст, который вы хотите, будет в 1+ прогонах, возможно, не единственный в любом из этих прогонов. Word несколько случайно (и несколько на основе истории) решит разрезать текст на столько прогонов, сколько ему захочется, и вы не можете это контролировать. Вы просто должны справиться с этим!

Gagravarr 13.12.2020 23:22
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
3
1 554
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Текущий 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 ... это сработало для меня. Я столкнулся с этой проблемой и искал решение.

Jayanth 19.04.2023 15:54

Точно так же, как кто-то прокомментировал ваш вопрос, вы не можете контролировать, где или когда 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, чтобы написать этот ответ.

Другие вопросы по теме