|
| 1 | +"""HWP → Document IR (Pydantic HwpDocument) 변환 + 블록 순회 + JSON 직렬화 시연. |
| 2 | +
|
| 3 | +Document IR 은 구역/단락/표/셀을 계층 구조로 보존하며, 표는 cells 배열 + HTML |
| 4 | ++ 평문 3중 표현을 병기한다. 병합된 빈 셀은 ``role="layout"`` 으로 자동 태깅된다. |
| 5 | +
|
| 6 | +사용법: |
| 7 | + python examples/04_document_ir.py path/to/your/file.hwp |
| 8 | + python examples/04_document_ir.py path/to/your/file.hwp --limit 20 |
| 9 | + python examples/04_document_ir.py path/to/your/file.hwp --out ir.json |
| 10 | +
|
| 11 | +설치: |
| 12 | + pip install "rhwp-python[examples]" |
| 13 | +""" |
| 14 | + |
| 15 | +from pathlib import Path as PathLibPath |
| 16 | + |
| 17 | +import rhwp |
| 18 | +import typer |
| 19 | +from rhwp.ir.nodes import ParagraphBlock, TableBlock, UnknownBlock |
| 20 | + |
| 21 | + |
| 22 | +def main( |
| 23 | + path: PathLibPath = typer.Argument( |
| 24 | + PathLibPath("external/rhwp/samples/table-vpos-01.hwpx"), |
| 25 | + exists=False, |
| 26 | + help="HWP 또는 HWPX 파일 경로 (기본값은 submodule 샘플 — 표가 9개 포함됨)", |
| 27 | + ), |
| 28 | + limit: int = typer.Option(15, "--limit", "-n", help="미리보기할 블록 최대 개수"), |
| 29 | + out: PathLibPath = typer.Option( |
| 30 | + None, |
| 31 | + "--out", |
| 32 | + "-o", |
| 33 | + help="IR 전체를 JSON 파일로 저장 (예: ir.json)", |
| 34 | + ), |
| 35 | +) -> None: |
| 36 | + """Document IR 구조를 탐색하고 선택적으로 JSON 으로 덤프한다.""" |
| 37 | + if not path.exists(): |
| 38 | + typer.echo(f"파일이 없습니다: {path}", err=True) |
| 39 | + raise typer.Exit(code=1) |
| 40 | + |
| 41 | + doc = rhwp.parse(str(path)) |
| 42 | + ir = doc.to_ir() |
| 43 | + |
| 44 | + # * 문서 요약 |
| 45 | + typer.echo("=" * 60) |
| 46 | + typer.echo("[문서 메타]") |
| 47 | + typer.echo("=" * 60) |
| 48 | + typer.echo(f" schema: {ir.schema_name} v{ir.schema_version}") |
| 49 | + typer.echo(f" sections: {len(ir.sections)}") |
| 50 | + typer.echo(f" body: {len(ir.body)} 블록 (top-level)") |
| 51 | + |
| 52 | + # * 블록 타입 분포 (recurse=True 로 중첩 표 내부까지) |
| 53 | + type_counts: dict[str, int] = {} |
| 54 | + layout_cells = 0 |
| 55 | + for block in ir.iter_blocks(scope="body", recurse=True): |
| 56 | + type_counts[type(block).__name__] = type_counts.get(type(block).__name__, 0) + 1 |
| 57 | + if isinstance(block, TableBlock): |
| 58 | + layout_cells += sum(1 for c in block.cells if c.role == "layout") |
| 59 | + |
| 60 | + typer.echo("\n[블록 분포 — recurse=True]") |
| 61 | + for name, cnt in sorted(type_counts.items()): |
| 62 | + typer.echo(f" {name:20s} {cnt}") |
| 63 | + if layout_cells: |
| 64 | + typer.echo(f" {'layout cells':20s} {layout_cells} (병합된 비의미 셀)") |
| 65 | + |
| 66 | + # * 블록 리스트 미리보기 |
| 67 | + typer.echo("\n" + "=" * 60) |
| 68 | + typer.echo(f"[body 미리보기 — 최대 {limit} 블록]") |
| 69 | + typer.echo("=" * 60) |
| 70 | + for i, block in enumerate(ir.iter_blocks(scope="body")): |
| 71 | + if i >= limit: |
| 72 | + typer.echo(f" ... ({len(ir.body) - limit} more)") |
| 73 | + break |
| 74 | + prov = f"s={block.prov.section_idx} p={block.prov.para_idx}" |
| 75 | + if isinstance(block, ParagraphBlock): |
| 76 | + text = block.text[:60].replace("\n", "⏎") |
| 77 | + typer.echo(f" [P {prov}] {text!r}") |
| 78 | + elif isinstance(block, TableBlock): |
| 79 | + cap = f" caption={block.caption!r}" if block.caption else "" |
| 80 | + typer.echo(f" [T {prov}] {block.rows}x{block.cols} cells={len(block.cells)}{cap}") |
| 81 | + # ^ 첫 행만 간단 덤프 |
| 82 | + first_row = [c for c in block.cells if c.row == 0] |
| 83 | + for c in first_row[:4]: |
| 84 | + snippet = block.text.split("\n")[0][:50] |
| 85 | + typer.echo(f" cell(0,{c.col}) role={c.role} row1={snippet!r}") |
| 86 | + break # first row 한 줄만 |
| 87 | + elif isinstance(block, UnknownBlock): |
| 88 | + typer.echo(f" [? {prov}] kind={block.kind!r} (forward-compat)") |
| 89 | + |
| 90 | + # * 첫 TableBlock 의 HTML 미리보기 |
| 91 | + first_table = next((b for b in ir.iter_blocks(scope="body") if isinstance(b, TableBlock)), None) |
| 92 | + if first_table: |
| 93 | + typer.echo("\n" + "=" * 60) |
| 94 | + typer.echo(f"[첫 표의 HTML — HtmlRAG 호환, {first_table.rows}x{first_table.cols}]") |
| 95 | + typer.echo("=" * 60) |
| 96 | + typer.echo(first_table.html) |
| 97 | + |
| 98 | + # * 선택: JSON 파일로 덤프 |
| 99 | + if out is not None: |
| 100 | + json_str = doc.to_ir_json(indent=2) |
| 101 | + out.parent.mkdir(parents=True, exist_ok=True) |
| 102 | + out.write_text(json_str, encoding="utf-8") |
| 103 | + typer.echo(f"\n전체 IR 을 {out} 에 저장 ({len(json_str):,} 바이트).") |
| 104 | + |
| 105 | + typer.echo( |
| 106 | + "\n다음 단계: `rhwp.ir.schema.export_schema()` 또는 `load_schema()` 로 JSON Schema 확보." |
| 107 | + ) |
| 108 | + typer.echo(" 또는 `examples/05_langchain_ir_blocks.py` 로 LangChain ir-blocks 모드 시연.") |
| 109 | + |
| 110 | + |
| 111 | +if __name__ == "__main__": |
| 112 | + typer.run(main) |
0 commit comments