mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
fix(tool/extract): route xlsx/pptx extraction directly through Tika
This commit is contained in:
parent
bf2ffbd62b
commit
a65aaa7aa4
@ -19,13 +19,15 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档文本提取工具
|
* Document text extraction tool.
|
||||||
* 支持 PDF、DOCX、XLSX、PPTX 等 Office 文档的文本提取
|
* Supports PDF, DOCX, XLSX, PPTX with format-specific fallback chains.
|
||||||
* 实现 fallback 链:系统命令 -> Java 实现 -> 结构化错误
|
|
||||||
*
|
*
|
||||||
* 实现策略:
|
* Strategy by format:
|
||||||
* - PDF: pdftotext -> pypdf/pdfplumber (Java 实现)
|
* - PDF: pdftotext -> pdfplumber/pypdf -> pdfbox -> OCR (scanned) -> Tika
|
||||||
* - DOCX: textutil/pandoc -> ZIP XML 解析
|
* - DOCX: textutil / pandoc / libreoffice -> ZIP+XML -> Tika
|
||||||
|
* - XLSX/PPTX: Tika directly (POI-based; correctly resolves the shared-strings
|
||||||
|
* indirection table and walks SmartArt / chart / grouped-shape
|
||||||
|
* text that a naive ZIP+XML scan misses).
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -45,12 +47,11 @@ public class DocumentExtractTool {
|
|||||||
- Excel (.xlsx, .xls) - 提取为文本表格
|
- Excel (.xlsx, .xls) - 提取为文本表格
|
||||||
- PowerPoint (.pptx, .ppt)
|
- PowerPoint (.pptx, .ppt)
|
||||||
|
|
||||||
提取策略(默认自动选择最优方式):
|
提取策略(按格式分链):
|
||||||
1. 优先使用系统命令(pdftotext, textutil, pandoc 等)
|
- PDF: pdftotext → pdfplumber/pypdf → pdfbox → OCR(扫描版) → Tika
|
||||||
2. 系统命令不可用时使用纯 Java 实现
|
- DOCX: textutil / pandoc / libreoffice → ZIP-XML → Tika
|
||||||
3. PDF 扫描版进入 OCR
|
- XLSX/PPTX: 直接走 Tika(基于 POI,正确解析 sharedStrings 表与 SmartArt / 图表文本)
|
||||||
4. 全部失败前用 Apache Tika 兜底(覆盖 SmartArt、共享字符串表等盲区)
|
- 返回详细的提取过程和元数据
|
||||||
5. 返回详细的提取过程和元数据
|
|
||||||
|
|
||||||
参数 options 可包含:
|
参数 options 可包含:
|
||||||
- pages: 指定页码范围(如 "1-5" 或 "1,3,5")
|
- pages: 指定页码范围(如 "1-5" 或 "1,3,5")
|
||||||
@ -743,90 +744,51 @@ public class DocumentExtractTool {
|
|||||||
// ==================== XLSX 提取 ====================
|
// ==================== XLSX 提取 ====================
|
||||||
|
|
||||||
private ExtractedContent extractXlsx(Path path, String options, List<String> attempts) throws Exception {
|
private ExtractedContent extractXlsx(Path path, String options, List<String> attempts) throws Exception {
|
||||||
StringBuilder text = new StringBuilder();
|
long t = System.currentTimeMillis();
|
||||||
|
String text = TikaExtractor.extract(path);
|
||||||
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(path))) {
|
long elapsed = System.currentTimeMillis() - t;
|
||||||
ZipEntry entry;
|
if (text != null && !text.isBlank()) {
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
attempts.add("tika: 成功 (" + elapsed + "ms)");
|
||||||
if (entry.getName().startsWith("xl/worksheets/sheet") && entry.getName().endsWith(".xml")) {
|
return new ExtractedContent(text, "tika", 0);
|
||||||
String xml = new String(zis.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
|
||||||
text.append("--- ").append(entry.getName()).append(" ---\n");
|
|
||||||
text.append(extractTextFromXlsxXml(xml)).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
attempts.add("tika: 失败或不可用 (" + elapsed + "ms)");
|
||||||
// Our ZIP-XML extractor only reads <v> tags and skips the shared-strings table,
|
throw new Exception("XLSX 提取失败:Tika 无法解析(文件可能损坏、加密或非标准格式)");
|
||||||
// so cells full of text labels look "empty". When that happens, fall through to
|
|
||||||
// Tika which knows how to resolve the shared-strings indirection.
|
|
||||||
if (text.toString().replaceAll("---.*?---", "").strip().isEmpty()) {
|
|
||||||
String fallback = TikaExtractor.extract(path);
|
|
||||||
if (fallback != null && !fallback.isBlank()) {
|
|
||||||
attempts.add("tika: 成功(ZIP-XML 仅有数字 / 共享字符串未解析)");
|
|
||||||
return new ExtractedContent(fallback, "tika", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts.add("java_zip_xml: 成功");
|
|
||||||
return new ExtractedContent(text.toString(), "java_zip_xml", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractTextFromXlsxXml(String xml) {
|
|
||||||
StringBuilder text = new StringBuilder();
|
|
||||||
int start = 0;
|
|
||||||
while ((start = xml.indexOf("<v>", start)) != -1) {
|
|
||||||
int end = xml.indexOf("</v>", start);
|
|
||||||
if (end == -1) break;
|
|
||||||
String value = xml.substring(start + 3, end);
|
|
||||||
text.append(value).append("\t");
|
|
||||||
start = end + 4;
|
|
||||||
}
|
|
||||||
return text.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== PPTX 提取 ====================
|
// ==================== PPTX 提取 ====================
|
||||||
|
|
||||||
private ExtractedContent extractPptx(Path path, String options, List<String> attempts) throws Exception {
|
private ExtractedContent extractPptx(Path path, String options, List<String> attempts) throws Exception {
|
||||||
StringBuilder text = new StringBuilder();
|
long t = System.currentTimeMillis();
|
||||||
int slideNum = 1;
|
String text = TikaExtractor.extract(path);
|
||||||
|
long elapsed = System.currentTimeMillis() - t;
|
||||||
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(path))) {
|
if (text != null && !text.isBlank()) {
|
||||||
ZipEntry entry;
|
attempts.add("tika: 成功 (" + elapsed + "ms)");
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
int slides = countPptxSlides(path);
|
||||||
if (entry.getName().startsWith("ppt/slides/slide") && entry.getName().endsWith(".xml")) {
|
return new ExtractedContent(text, "tika", slides);
|
||||||
String xml = new String(zis.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
|
||||||
text.append("--- Slide ").append(slideNum++).append(" ---\n");
|
|
||||||
text.append(extractTextFromPptxXml(xml)).append("\n\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
attempts.add("tika: 失败或不可用 (" + elapsed + "ms)");
|
||||||
// Slide layouts with text inside SmartArt / charts / grouped shapes don't surface
|
throw new Exception("PPTX 提取失败:Tika 无法解析(文件可能损坏、加密或非标准格式)");
|
||||||
// through the simple <a:t> grep — Tika walks the full DrawingML graph and pulls
|
|
||||||
// them out. Only invoke when our walker produced nothing useful.
|
|
||||||
if (text.toString().replaceAll("---.*?---", "").strip().isEmpty()) {
|
|
||||||
String fallback = TikaExtractor.extract(path);
|
|
||||||
if (fallback != null && !fallback.isBlank()) {
|
|
||||||
attempts.add("tika: 成功(ZIP-XML 未抓到正文,可能是 SmartArt / 图表)");
|
|
||||||
return new ExtractedContent(fallback, "tika", Math.max(0, slideNum - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts.add("java_zip_xml: 成功");
|
|
||||||
return new ExtractedContent(text.toString(), "java_zip_xml", Math.max(0, slideNum - 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractTextFromPptxXml(String xml) {
|
/**
|
||||||
StringBuilder text = new StringBuilder();
|
* Cheap slide count for the result metadata. Counts {@code ppt/slides/slideN.xml}
|
||||||
int start = 0;
|
* entries in the OOXML zip without parsing the slide content. Returns 0 if the
|
||||||
while ((start = xml.indexOf("<a:t>", start)) != -1) {
|
* file isn't a readable zip.
|
||||||
int end = xml.indexOf("</a:t>", start);
|
*/
|
||||||
if (end == -1) break;
|
private int countPptxSlides(Path path) {
|
||||||
String txt = xml.substring(start + 5, end);
|
int count = 0;
|
||||||
text.append(txt).append(" ");
|
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(path))) {
|
||||||
start = end + 6;
|
ZipEntry e;
|
||||||
|
while ((e = zis.getNextEntry()) != null) {
|
||||||
|
String name = e.getName();
|
||||||
|
if (name.startsWith("ppt/slides/slide") && name.endsWith(".xml")) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// Slide count is best-effort metadata; never fail the extract on this.
|
||||||
}
|
}
|
||||||
return text.toString().trim();
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 工具方法 ====================
|
// ==================== 工具方法 ====================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user