我有一些占位符的docx文档。现在我应该用其他内容替换它们并保存新的 docx 文档。我从docx4j开始,发现了这种方法:
public static List<Object> getAllElementFromObject(Object obj, Class<?> toSearch) {
List<Object> result = new ArrayList<Object>();
if (obj instanceof JAXBElement) obj = ((JAXBElement<?>) obj).getValue();
if (obj.getClass().equals(toSearch))
result.add(obj);
else if (obj instanceof ContentAccessor) {
List<?> children = ((ContentAccessor) obj).getContent();
for (Object child : children) {
result.addAll(getAllElementFromObject(child, toSearch));
}
}
return result;
}
public static void findAndReplace(WordprocessingMLPackage doc, String toFind, String replacer){
List<Object> paragraphs = getAllElementFromObject(doc.getMainDocumentPart(), P.class);
for(Object par : paragraphs){
P p = (P) par;
List<Object> texts = getAllElementFromObject(p, Text.class);
for(Object text : texts){
Text t = (Text)text;
if(t.getValue().contains(toFind)){
t.setValue(t.getValue().replace(toFind, replacer));
}
}
}
}
但这很少有效,因为通常占位符会拆分到多个文本运行中。
我尝试了UnmarshallFromTemplate,但它也很少起作用。
如何解决这个问题?
VariableReplace
来实现这一点,这在其他答案时可能不存在。这本身不做查找/替换,但适用于占位符,例如${myField}
java.util.HashMap mappings = new java.util.HashMap();
VariablePrepare.prepare(wordMLPackage);//see notes
mappings.put("myField", "foo");
wordMLPackage.getMainDocumentPart().variableReplace(mappings);
请注意,您不会传递${myField}
作为字段名称;而是将未转义的字段名称传递myField
- 这是相当不灵活的,因为就目前而言,您的占位符必须是${xyz}
格式,而如果您可以传入任何内容,那么您可以将其用于任何查找/替换。C# 人员也可以使用此功能 docx4j.NET
有关VariableReplace
的更多信息,请参阅此处或在此处获取VariablePrepare
美好的一天,我做了一个例子,如何快速将文本替换为您需要的内容通过正则表达式。我找到 ${param.sumname} 并在文档中替换它。请注意,您必须将文本作为"仅文本"插入!玩得愉快!
WordprocessingMLPackage mlp = WordprocessingMLPackage.load(new File("filepath"));
replaceText(mlp.getMainDocumentPart());
static void replaceText(ContentAccessor c)
throws Exception
{
for (Object p: c.getContent())
{
if (p instanceof ContentAccessor)
replaceText((ContentAccessor) p);
else if (p instanceof JAXBElement)
{
Object v = ((JAXBElement) p).getValue();
if (v instanceof ContentAccessor)
replaceText((ContentAccessor) v);
else if (v instanceof org.docx4j.wml.Text)
{
org.docx4j.wml.Text t = (org.docx4j.wml.Text) v;
String text = t.getValue();
if (text != null)
{
t.setSpace("preserve"); // needed?
t.setValue(replaceParams(text));
}
}
}
}
}
static Pattern paramPatern = Pattern.compile("(?i)(\$\{([\w\.]+)\})");
static String replaceParams(String text)
{
Matcher m = paramPatern.matcher(text);
if (!m.find())
return text;
StringBuffer sb = new StringBuffer();
String param, replacement;
do
{
param = m.group(2);
if (param != null)
{
replacement = getParamValue(param);
m.appendReplacement(sb, replacement);
}
else
m.appendReplacement(sb, "");
}
while (m.find());
m.appendTail(sb);
return sb.toString();
}
static String getParamValue(String name)
{
// replace from map or something else
return name;
}
我创建了一个库来发布我的解决方案,因为它有很多代码:https://github.com/phip1611/docx4j-search-and-replace-util
工作流如下:
第一步:
// (this method was part of your question)
List<Text> texts = getAllElementFromObject(docxDocument.getMainDocumentPart(), Text.class);
通过这种方式,我们以正确的顺序获得所有实际的文本内容,但中间没有样式标记。我们可以编辑文本对象(通过 setValue)并保留样式。
由此产生的问题:搜索文本/占位符可以拆分为多个文本实例(因为在原始文档中可能存在不可见的样式标记),例如 ${FOOBAR}
, ${
+ FOOBAR}
或 $
+ {FOOB
+ AR}
第二步:
将所有文本对象连接到一个完整的字符串/"完整的字符串"
Optional<String> completeStringOpt = texts.stream().map(Text::getValue).reduce(String::concat);
第三步:
创建类TextMetaItem
。每个 TextMetaItem 都知道它的内容在完整字符串中开始和结束的 Text-对象。例如,如果 "foo" 和 "bar" 的文本对象导致完整的字符串 "foobar",则索引0-2
属于 "foo"-Text-object
并3-5
属于 "bar"-Text-object
。构建List<TextMetaItem>
static List<TextMetaItem> buildMetaItemList(List<Text> texts) {
final int[] index = {0};
final int[] iteration = {0};
List<TextMetaItem> list = new ArrayList<>();
texts.forEach(text -> {
int length = text.getValue().length();
list.add(new TextMetaItem(index[0], index[0] + length - 1, text, iteration[0]));
index[0] += length;
iteration[0]++;
});
return list;
}
第四步:
构建一个Map<Integer, TextMetaItem>
,其中键是完整字符串中的索引/字符。这意味着地图的长度等于completeString.length()
static Map<Integer, TextMetaItem> buildStringIndicesToTextMetaItemMap(List<Text> texts) {
List<TextMetaItem> metaItemList = buildMetaItemList(texts);
Map<Integer, TextMetaItem> map = new TreeMap<>();
int currentStringIndicesToTextIndex = 0;
// + 1 important here!
int max = metaItemList.get(metaItemList.size() - 1).getEnd() + 1;
for (int i = 0; i < max; i++) {
TextMetaItem currentTextMetaItem = metaItemList.get(currentStringIndicesToTextIndex);
map.put(i, currentTextMetaItem);
if (i >= currentTextMetaItem.getEnd()) {
currentStringIndicesToTextIndex++;
}
}
return map;
}
中期业绩:
现在,您有足够的元数据来将要对完整字符串执行的每个操作委托给相应的 Text 对象!(要更改文本对象的内容,您只需调用 (#setValue()) 这就是 Docx4J 中编辑文本所需的全部内容。所有样式信息等都将保留!
最后一步:搜索和替换
生成一个查找可能占位符的所有匹配项的方法。您应该创建一个类似
FoundResult(int start, int end)
的类,该类将找到的值(占位符)的开始和结束索引存储在完整字符串中public static List<FoundResult> findAllOccurrencesInString(String data, String search) { List<FoundResult> list = new ArrayList<>(); String remaining = data; int totalIndex = 0; while (true) { int index = remaining.indexOf(search); if (index == -1) { break; } int throwAwayCharCount = index + search.length(); remaining = remaining.substring(throwAwayCharCount); list.add(new FoundResult(totalIndex + index, search)); totalIndex += throwAwayCharCount; } return list; }
使用它,我构建了一个新的
ReplaceCommand
列表。ReplaceCommand
是一个简单的类,用于存储FoundResult
和新值。接下来,您必须从最后一项到第一项对此列表进行排序(在完整字符串中按位置排序)
现在你可以编写一个替换所有算法,因为你知道需要对哪个文本对象执行什么操作。我们这样做 (2) 以便替换操作不会使其他
FoundResult
的索引无效。3.1.) 查找需要更改的文本对象3.2.) 在他们身上调用 getValue()3.3.) 将字符串编辑为新值3.4.) 在文本对象上调用 setValue()
这是执行所有魔术的代码。它执行单个替换命令。
/**
* @param texts All Text-objects
* @param replaceCommand Command
* @param map Lookup-Map from index in complete string to TextMetaItem
*/
public static void executeReplaceCommand(List<Text> texts, ReplaceCommand replaceCommand, Map<Integer, TextMetaItem> map) {
TextMetaItem tmi1 = map.get(replaceCommand.getFoundResult().getStart());
TextMetaItem tmi2 = map.get(replaceCommand.getFoundResult().getEnd());
if (tmi2.getPosition() - tmi1.getPosition() > 0) {
// it can happen that text objects are in-between
// we can remove them (set to null)
int upperBorder = tmi2.getPosition();
int lowerBorder = tmi1.getPosition() + 1;
for (int i = lowerBorder; i < upperBorder; i++) {
texts.get(i).setValue(null);
}
}
if (tmi1.getPosition() == tmi2.getPosition()) {
// do replacement inside a single Text-object
String t1 = tmi1.getText().getValue();
int beginIndex = tmi1.getPositionInsideTextObject(replaceCommand.getFoundResult().getStart());
int endIndex = tmi2.getPositionInsideTextObject(replaceCommand.getFoundResult().getEnd());
String keepBefore = t1.substring(0, beginIndex);
String keepAfter = t1.substring(endIndex + 1);
tmi1.getText().setValue(keepBefore + replaceCommand.getNewValue() + keepAfter);
} else {
// do replacement across two Text-objects
// check where to start and replace
// the Text-objects value inside both Text-objects
String t1 = tmi1.getText().getValue();
String t2 = tmi2.getText().getValue();
int beginIndex = tmi1.getPositionInsideTextObject(replaceCommand.getFoundResult().getStart());
int endIndex = tmi2.getPositionInsideTextObject(replaceCommand.getFoundResult().getEnd());
t1 = t1.substring(0, beginIndex);
t1 = t1.concat(replaceCommand.getNewValue());
t2 = t2.substring(endIndex + 1);
tmi1.getText().setValue(t1);
tmi2.getText().setValue(t2);
}
}
一个问题。我在这里的答案中介绍了如何减少破碎的文本运行:https://stackoverflow.com/a/17066582/125750
。但您可能需要改为考虑内容控件。docx4j 源站点在此处包含各种内容控件示例:
https://github.com/plutext/docx4j/tree/master/src/samples/docx4j/org/docx4j/samples