scripts/docx_read.py
scripts/docx_read.pyBrowse 12 files
1,260 tokens
5,405 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32# MIT License. Part of the Hermes docx skill.3"""Read a .docx: text, structure outline, styles, images, revision detection.4 5Usage:6 docx_read.py file.docx --text # full text incl. tables + headers/footers7 docx_read.py file.docx --structure # JSON outline (headings, tables, counts)8 docx_read.py file.docx --styles # JSON list of styles actually used9 docx_read.py file.docx --images DIR # extract embedded images into DIR10 docx_read.py file.docx --revisions # JSON: tracked changes / comments present?11 12Text output is JSON: {"body": [...], "tables": [[...rows]], "headers": [...],13"footers": [...]}. Body text is the accepted/as-is text (python-docx ignores14deleted-in-revision text and shows inserted text).15"""16from __future__ import annotations17 18import argparse19import json20import os21import sys22import zipfile23 24from docx import Document25 26 27def table_to_rows(table) -> list:28 return [[cell.text for cell in row.cells] for row in table.rows]29 30 31def extract_text(doc) -> dict:32 out = {"body": [p.text for p in doc.paragraphs],33 "tables": [table_to_rows(t) for t in doc.tables],34 "headers": [], "footers": []}35 for section in doc.sections:36 out["headers"].extend(p.text for p in section.header.paragraphs)37 out["footers"].extend(p.text for p in section.footer.paragraphs)38 for t in section.header.tables:39 out["headers"].append(json.dumps(table_to_rows(t), ensure_ascii=False))40 for t in section.footer.tables:41 out["footers"].append(json.dumps(table_to_rows(t), ensure_ascii=False))42 return out43 44 45def extract_structure(doc) -> dict:46 outline = []47 for i, para in enumerate(doc.paragraphs):48 style = para.style.name if para.style else ""49 if style.startswith("Heading"):50 try:51 level = int(style.split()[-1])52 except ValueError:53 level = 154 outline.append({"index": i, "level": level, "text": para.text})55 return {56 "outline": outline,57 "paragraph_count": len(doc.paragraphs),58 "table_count": len(doc.tables),59 "tables": [{"rows": len(t.rows), "cols": len(t.columns)}60 for t in doc.tables],61 "section_count": len(doc.sections),62 }63 64 65def styles_used(doc) -> list:66 used = set()67 for para in doc.paragraphs:68 if para.style:69 used.add(para.style.name)70 for run in para.runs:71 if run.style:72 used.add(run.style.name)73 for table in doc.tables:74 if table.style:75 used.add(table.style.name)76 for row in table.rows:77 for cell in row.cells:78 for para in cell.paragraphs:79 if para.style:80 used.add(para.style.name)81 return sorted(used)82 83 84def extract_images(path: str, outdir: str) -> list:85 os.makedirs(outdir, exist_ok=True)86 written = []87 with zipfile.ZipFile(path) as zf:88 for name in zf.namelist():89 if name.startswith("word/media/"):90 target = os.path.join(outdir, os.path.basename(name))91 with open(target, "wb") as f:92 f.write(zf.read(name))93 written.append(target)94 return written95 96 97def detect_revisions(path: str) -> dict:98 """Detect tracked changes and comments by scanning the raw XML parts."""99 markers = {"insertions": b"<w:ins ", "deletions": b"<w:del ",100 "format_changes": b"<w:rPrChange"}101 result = {k: False for k in markers}102 result["comments"] = False103 with zipfile.ZipFile(path) as zf:104 names = zf.namelist()105 result["comments"] = any(n.startswith("word/comments") for n in names)106 for name in names:107 if name.startswith("word/") and name.endswith(".xml"):108 data = zf.read(name)109 for key, marker in markers.items():110 if marker in data:111 result[key] = True112 result["has_tracked_changes"] = any(113 result[k] for k in ("insertions", "deletions", "format_changes"))114 return result115 116 117def main() -> int:118 ap = argparse.ArgumentParser(description="Read/inspect a .docx file.")119 ap.add_argument("path", help=".docx file to read")120 g = ap.add_mutually_exclusive_group(required=True)121 g.add_argument("--text", action="store_true", help="extract all text as JSON")122 g.add_argument("--structure", action="store_true", help="outline JSON")123 g.add_argument("--styles", action="store_true", help="styles used, JSON")124 g.add_argument("--images", metavar="DIR", help="extract images to DIR")125 g.add_argument("--revisions", action="store_true",126 help="detect tracked changes / comments")127 args = ap.parse_args()128 129 if args.images:130 print(json.dumps({"images": extract_images(args.path, args.images)},131 ensure_ascii=False))132 return 0133 if args.revisions:134 print(json.dumps(detect_revisions(args.path), ensure_ascii=False))135 return 0136 137 doc = Document(args.path)138 if args.text:139 out = extract_text(doc)140 elif args.structure:141 out = extract_structure(doc)142 else:143 out = {"styles": styles_used(doc)}144 print(json.dumps(out, ensure_ascii=False, indent=2))145 return 0146 147 148if __name__ == "__main__":149 sys.exit(main())150 Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 51.SKILL.mdView in source ↗51python scripts/docx_create.py spec.json out.docx52python scripts/docx_read.py out.docx --text53python scripts/docx_edit.py replace out.docx --find old --replace new
Source excerpt starting at line 99.99 format is documented at the top of `scripts/docx_create.py`.1002. **Read.** Use `scripts/docx_read.py` with exactly one mode flag.101 `--text` returns body paragraphs, all table cell text, and