doocus first ver
This commit is contained in:
114
doocus/extractors/office.py
Normal file
114
doocus/extractors/office.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Office-family extractor: docx, pptx, xlsx.
|
||||
|
||||
Each format is handled with its own lazy import so a missing dependency degrades
|
||||
only that format. These formats carry core properties (author, created/modified
|
||||
dates, title) which go straight into metadata.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from .base import ExtractionResult
|
||||
|
||||
MAX_SHEET_ROWS = 200
|
||||
|
||||
|
||||
def _core_props(props) -> dict:
|
||||
"""Common OOXML core properties → metadata (only non-empty ones).
|
||||
|
||||
python-docx/pptx expose `.author`/`.last_modified_by`; openpyxl uses
|
||||
`.creator`/`.lastModifiedBy`. Each target checks both spellings.
|
||||
"""
|
||||
# dst -> candidate source attribute names (first non-empty wins)
|
||||
fields = {
|
||||
"title": ("title",),
|
||||
"author": ("author", "creator"),
|
||||
"created": ("created",),
|
||||
"modified_doc": ("modified",),
|
||||
"last_modified_by": ("last_modified_by", "lastModifiedBy"),
|
||||
}
|
||||
out = {}
|
||||
for dst, sources in fields.items():
|
||||
for src in sources:
|
||||
val = getattr(props, src, None)
|
||||
if val:
|
||||
out[dst] = str(val)
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _extract_docx(source: Path) -> ExtractionResult:
|
||||
import docx # python-docx
|
||||
doc = docx.Document(str(source))
|
||||
paras = [p.text for p in doc.paragraphs]
|
||||
content = "\n\n".join(p for p in paras if p.strip()) + "\n"
|
||||
metadata = _core_props(doc.core_properties)
|
||||
metadata["paragraphs"] = len([p for p in paras if p.strip()])
|
||||
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-docx@1")
|
||||
|
||||
|
||||
def _extract_pptx(source: Path) -> ExtractionResult:
|
||||
from pptx import Presentation
|
||||
prs = Presentation(str(source))
|
||||
blocks = []
|
||||
for i, slide in enumerate(prs.slides, 1):
|
||||
texts = []
|
||||
for shape in slide.shapes:
|
||||
if shape.has_text_frame:
|
||||
for para in shape.text_frame.paragraphs:
|
||||
line = "".join(run.text for run in para.runs)
|
||||
if line.strip():
|
||||
texts.append(line)
|
||||
blocks.append(f"## Slide {i}\n\n" + "\n".join(texts))
|
||||
content = "\n\n".join(blocks) + "\n"
|
||||
metadata = _core_props(prs.core_properties)
|
||||
metadata["slides"] = len(prs.slides)
|
||||
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-pptx@1")
|
||||
|
||||
|
||||
def _extract_xlsx(source: Path) -> ExtractionResult:
|
||||
from openpyxl import load_workbook
|
||||
wb = load_workbook(str(source), read_only=True, data_only=True)
|
||||
warnings = []
|
||||
blocks = []
|
||||
for ws in wb.worksheets:
|
||||
rows = []
|
||||
for r, row in enumerate(ws.iter_rows(values_only=True)):
|
||||
if r > MAX_SHEET_ROWS:
|
||||
warnings.append(f"sheet '{ws.title}' truncated to {MAX_SHEET_ROWS} rows")
|
||||
break
|
||||
rows.append(["" if c is None else str(c) for c in row])
|
||||
blocks.append(f"## {ws.title}\n\n" + _rows_to_md(rows))
|
||||
wb.close()
|
||||
content = "\n\n".join(blocks) + "\n"
|
||||
metadata = _core_props(wb.properties)
|
||||
metadata["sheets"] = [ws.title for ws in wb.worksheets]
|
||||
return ExtractionResult(content=content, metadata=metadata,
|
||||
warnings=warnings, extractor="office/openpyxl@1")
|
||||
|
||||
|
||||
def _rows_to_md(rows: list) -> str:
|
||||
if not rows:
|
||||
return "_(empty)_\n"
|
||||
width = max(len(r) for r in rows)
|
||||
rows = [r + [""] * (width - len(r)) for r in rows]
|
||||
|
||||
def esc(c):
|
||||
return str(c).replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
lines = ["| " + " | ".join(esc(c) for c in rows[0]) + " |",
|
||||
"| " + " | ".join(["---"] * width) + " |"]
|
||||
for r in rows[1:]:
|
||||
lines.append("| " + " | ".join(esc(c) for c in r) + " |")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def extract(source: Path, options: dict) -> ExtractionResult:
|
||||
ext = source.suffix.lower().lstrip(".")
|
||||
if ext == "docx":
|
||||
return _extract_docx(source)
|
||||
if ext == "pptx":
|
||||
return _extract_pptx(source)
|
||||
if ext == "xlsx":
|
||||
return _extract_xlsx(source)
|
||||
return ExtractionResult(extractor="office/unhandled",
|
||||
warnings=[f"office extractor got unexpected '.{ext}'"])
|
||||
Reference in New Issue
Block a user