Apache Tika无法读取嵌套内容控件内容的问题求助
带有嵌套占位符的Word文档内容提取问题
问题背景
尝试提取带有嵌套结构化文档标签(SDT,即Word占位符)的Word文档内容,其document.xml核心结构如下:
<w:document..> <w:body> <!-- Modified by docx4j 8.3.1 (Apache licensed) using REFERENCE JAXB in Eclipse Adoptium Java 17.0.7 on Windows Server 2022 --> <w:p w:rsidR="005C4CF0" w:rsidP="005C4CF0" w14:paraId="78C54BDB" w14:textId="77777777"> <w:r> <w:t>Im</w:t> </w:r> <w:r> <w:t xml:space="preserve"> going to add some basic information that comes from the clause:</w:t> </w:r> </w:p> <w:p w:rsidR="005C4CF0" w:rsidP="005C4CF0" w14:paraId="4241C94B" w14:textId="77777777"/> <w:sdt> <w:sdtPr> <w:alias w:val="Standard : LC Simple Clause"/> <w:tag w:val="CLS_171644_182392_1329803"/> <w:id w:val="849765303"/> <w:richText/> </w:sdtPr> <w:sdtContent> <w:p w:rsidR="005C4CF0" w14:paraId="6751289D" w14:textId="22A85AC8"> <w:r> <w:t xml:space="preserve">This is a simple clause, for contract:</w:t> </w:r> <w:sdt> <w:sdtPr> <w:alias w:val="Contract Name"/> <w:tag w:val="ContractMgmt_ContractName"/> <w:id w:val="-1113981428"/> <w:dataBinding w:prefixMappings="xmlns:sqph='http://schemas.sciquest.com/tcm/office/placeholders/v1'" w:xpath="/sqph:contractplaceholders[1]/sqph:ContractMgmt_ContractName[1]" w:storeItemID="{F2499371-A9EB-4C98-8057-F42491D7ED37}"/> <w:text/> </w:sdtPr> <w:sdtContent> <w:r> <w:t>LC Chat 04</w:t> </w:r> </w:sdtContent> </w:sdt> <!-- 原XML此处缺失<w:p>闭合标签 --> </w:sdtContent> </w:sdt> <w:r> <w:t xml:space="preserve"> with contract number</w:t> </w:r> <w:sdt> <w:sdtPr> <w:alias w:val="Work Group"/> <w:tag w:val="ContractConfigSection_ContractProject"/> <w:id w:val="-261304195"/> <w:dataBinding w:prefixMappings="xmlns:sqph='http://schemas.sciquest.com/tcm/office/placeholders/v1'" w:xpath="/sqph:contractplaceholders[1]/sqph:ContractConfigSection_ContractProject[1]" w:storeItemID="{F2499371-A9EB-4C98-8057-F42491D7ED37}"/> <w:text/> </w:sdtPr> <w:sdtContent> <w:r> <w:t>TCMTEST</w:t> </w:r> </w:sdtContent> </w:sdt> </w:p> </w:body> </w:document>
使用Apache Tika提取后得到结果:
Im going to add some basic information that comes from the clause<br /> This is a simple clause, for contract: with contract number TCMTEST
可见顶层SDT外的内容和最后一个非嵌套SDT的内容被正确提取,但嵌套在另一个SDT内的"Contract Name"占位符内容LC Chat 04未被读取。
原因分析
- Tika默认解析逻辑限制:Apache Tika的DOCX解析器对嵌套SDT的递归处理不完整,默认只遍历顶层SDT节点,未深入解析嵌套在
<w:sdtContent>内部的SDT节点,导致其文本内容被遗漏。 - XML结构错误:提供的
document.xml存在标签闭合问题,外层SDT内的<w:p>标签未正确闭合,后续的<w:r>和<w:sdt>节点处于非法结构位置,干扰了Tika的解析器对节点层次的识别。
解决办法
方法1:修复XML结构异常
先修正document.xml的标签闭合问题,确保所有节点层次正确:
<!-- 修正后的核心结构片段 --> <w:sdt> <w:sdtPr> <w:alias w:val="Standard : LC Simple Clause"/> <w:tag w:val="CLS_171644_182392_1329803"/> <w:id w:val="849765303"/> <w:richText/> </w:sdtPr> <w:sdtContent> <w:p w:rsidR="005C4CF0" w14:paraId="6751289D" w14:textId="22A85AC8"> <w:r> <w:t xml:space="preserve">This is a simple clause, for contract:</w:t> </w:r> <w:sdt> <w:sdtPr> <w:alias w:val="Contract Name"/> <w:tag w:val="ContractMgmt_ContractName"/> <w:id w:val="-1113981428"/> <w:dataBinding w:prefixMappings="xmlns:sqph='http://schemas.sciquest.com/tcm/office/placeholders/v1'" w:xpath="/sqph:contractplaceholders[1]/sqph:ContractMgmt_ContractName[1]" w:storeItemID="{F2499371-A9EB-4C98-8057-F42491D7ED37}"/> <w:text/> </w:sdtPr> <w:sdtContent> <w:r> <w:t>LC Chat 04</w:t> </w:r> </w:sdtContent> </w:sdt> </w:p> <!-- 补充缺失的段落闭合标签 --> </w:sdtContent> </w:sdt> <!-- 将后续内容放入合法的段落节点 --> <w:p w:rsidR="005C4CF0" w:rsidP="005C4CF0" w14:paraId="XXXXXX" w14:textId="XXXXXX"> <w:r> <w:t xml:space="preserve"> with contract number</w:t> </w:r> <w:sdt> <w:sdtPr> <w:alias w:val="Work Group"/> <w:tag w:val="ContractConfigSection_ContractProject"/> <w:id w:val="-261304195"/> <w:dataBinding w:prefixMappings="xmlns:sqph='http://schemas.sciquest.com/tcm/office/placeholders/v1'" w:xpath="/sqph:contractplaceholders[1]/sqph:ContractConfigSection_ContractProject[1]" w:storeItemID="{F2499371-A9EB-4C98-8057-F42491D7ED37}"/> <w:text/> </w:sdtPr> <w:sdtContent> <w:r> <w:t>TCMTEST</w:t> </w:r> </w:sdtContent> </w:sdt> </w:p>
修正后重新用Tika提取,大概率能正常识别嵌套SDT的内容。
方法2:自定义Tika解析器扩展嵌套SDT处理
扩展Tika的DocxParser类,重写SDT节点处理逻辑,增加递归遍历嵌套SDT的逻辑:
import org.apache.tika.parser.microsoft.ooxml.DocxParser; import org.apache.tika.sax.XHTMLContentHandler; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamException; import java.io.IOException; public class NestedSDTDocxParser extends DocxParser { @Override protected void processSDT(XMLStreamReader reader, XHTMLContentHandler xhtml) throws XMLStreamException, IOException { // 先处理当前SDT的内容 super.processSDT(reader, xhtml); // 遍历当前节点的子节点,检查是否有嵌套的SDT while (reader.hasNext()) { int event = reader.next(); if (event == XMLStreamReader.START_ELEMENT && "w:sdt".equals(reader.getLocalName())) { // 递归处理嵌套的SDT processSDT(reader, xhtml); } else if (event == XMLStreamReader.END_ELEMENT && "w:sdtContent".equals(reader.getLocalName())) { // 退出当前SDT内容节点 break; } } } }
使用自定义解析器替换默认的DocxParser,即可提取嵌套SDT的内容。
方法3:直接用Docx4j解析XML
利用Docx4j库直接解析document.xml,递归遍历所有SDT节点提取内容,避免Tika的限制:
import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.SdtBlock; import org.docx4j.wml.SdtRun; import org.docx4j.wml.P; import java.util.List; import java.util.stream.Collectors; public class SDTContentExtractor { public static String extractAllSDTContent(String docxPath) throws Exception { WordprocessingMLPackage pkg = WordprocessingMLPackage.load(new java.io.File(docxPath)); StringBuilder content = new StringBuilder(); // 提取顶层SDT List<SdtBlock> topLevelSDTs = pkg.getMainDocumentPart().getContent().stream() .filter(obj -> obj instanceof SdtBlock) .map(obj -> (SdtBlock) obj) .collect(Collectors.toList()); for (SdtBlock sdt : topLevelSDTs) { // 提取当前SDT的文本内容 content.append(org.docx4j.Docx4J.extractText(sdt.getSdtContent())); // 递归处理SDT内容内的嵌套SDT processNestedSDT(sdt.getSdtContent().getContent(), content); } // 提取非SDT的普通段落内容 List<P> paragraphs = pkg.getMainDocumentPart().getContent().stream() .filter(obj -> obj instanceof P) .map(obj -> (P) obj) .collect(Collectors.toList()); for (P p : paragraphs) { content.append(org.docx4j.Docx4J.extractText(p)); } return content.toString(); } private static void processNestedSDT(List<Object> contentList, StringBuilder result) { for (Object obj : contentList) { if (obj instanceof SdtRun) { // 处理行内嵌套SDT result.append(org.docx4j.Docx4J.extractText(((SdtRun) obj).getSdtContent())); } else if (obj instanceof P) { // 处理段落内的嵌套内容 processNestedSDT(((P) obj).getContent(), result); } else if (obj instanceof SdtBlock) { // 处理块级嵌套SDT result.append(org.docx4j.Docx4J.extractText(((SdtBlock) obj).getSdtContent())); processNestedSDT(((SdtBlock) obj).getSdtContent().getContent(), result); } } } }
内容的提问来源于stack exchange,提问作者Luis Alfredo Conde Vela
相关产品推荐
相关产品推荐

