dochan 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dochan-0.1.0/LICENSE +21 -0
- dochan-0.1.0/NOTICE +8 -0
- dochan-0.1.0/PKG-INFO +263 -0
- dochan-0.1.0/README.md +229 -0
- dochan-0.1.0/dochan/__init__.py +9 -0
- dochan-0.1.0/dochan/batch.py +154 -0
- dochan-0.1.0/dochan/cli.py +118 -0
- dochan-0.1.0/dochan/constants.py +54 -0
- dochan-0.1.0/dochan/control_char.py +51 -0
- dochan-0.1.0/dochan/fallback/__init__.py +0 -0
- dochan-0.1.0/dochan/fallback/filter_server.py +93 -0
- dochan-0.1.0/dochan/hwp/__init__.py +0 -0
- dochan-0.1.0/dochan/hwp/bin_data.py +96 -0
- dochan-0.1.0/dochan/hwp/doc_info.py +212 -0
- dochan-0.1.0/dochan/hwp/header.py +78 -0
- dochan-0.1.0/dochan/hwp/records/__init__.py +0 -0
- dochan-0.1.0/dochan/hwp/records/char_shape.py +115 -0
- dochan-0.1.0/dochan/hwp/records/ctrl_header.py +67 -0
- dochan-0.1.0/dochan/hwp/records/para_char_shape.py +35 -0
- dochan-0.1.0/dochan/hwp/records/para_header.py +63 -0
- dochan-0.1.0/dochan/hwp/records/para_text.py +66 -0
- dochan-0.1.0/dochan/hwp/records/style.py +68 -0
- dochan-0.1.0/dochan/hwp/records/table.py +62 -0
- dochan-0.1.0/dochan/hwp/section.py +472 -0
- dochan-0.1.0/dochan/hwpx/__init__.py +0 -0
- dochan-0.1.0/dochan/hwpx/parser.py +461 -0
- dochan-0.1.0/dochan/model/__init__.py +0 -0
- dochan-0.1.0/dochan/model/document.py +97 -0
- dochan-0.1.0/dochan/model/equation.py +58 -0
- dochan-0.1.0/dochan/model/header_footer.py +25 -0
- dochan-0.1.0/dochan/model/image.py +25 -0
- dochan-0.1.0/dochan/model/style.py +37 -0
- dochan-0.1.0/dochan/model/table.py +37 -0
- dochan-0.1.0/dochan/output/__init__.py +0 -0
- dochan-0.1.0/dochan/output/json_out.py +84 -0
- dochan-0.1.0/dochan/output/markdown.py +159 -0
- dochan-0.1.0/dochan/output/plain_text.py +47 -0
- dochan-0.1.0/dochan/quality/__init__.py +0 -0
- dochan-0.1.0/dochan/quality/batch_validate.py +222 -0
- dochan-0.1.0/dochan/quality/checker.py +75 -0
- dochan-0.1.0/dochan/quality/comparator.py +66 -0
- dochan-0.1.0/dochan/quality/cross_validator.py +410 -0
- dochan-0.1.0/dochan/reader.py +174 -0
- dochan-0.1.0/dochan/tests/__init__.py +0 -0
- dochan-0.1.0/dochan/tests/test_char_shape.py +72 -0
- dochan-0.1.0/dochan/tests/test_control_char.py +90 -0
- dochan-0.1.0/dochan/tests/test_quality.py +82 -0
- dochan-0.1.0/dochan/utils/__init__.py +0 -0
- dochan-0.1.0/dochan/utils/error_recovery.py +55 -0
- dochan-0.1.0/dochan/utils/logger.py +38 -0
- dochan-0.1.0/dochan/utils/ocr.py +117 -0
- dochan-0.1.0/dochan/utils/safe_decompress.py +27 -0
- dochan-0.1.0/dochan.egg-info/PKG-INFO +263 -0
- dochan-0.1.0/dochan.egg-info/SOURCES.txt +58 -0
- dochan-0.1.0/dochan.egg-info/dependency_links.txt +1 -0
- dochan-0.1.0/dochan.egg-info/entry_points.txt +2 -0
- dochan-0.1.0/dochan.egg-info/requires.txt +11 -0
- dochan-0.1.0/dochan.egg-info/top_level.txt +1 -0
- dochan-0.1.0/pyproject.toml +47 -0
- dochan-0.1.0/setup.cfg +4 -0
dochan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dochan contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dochan-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
NOTICE
|
|
2
|
+
|
|
3
|
+
본 소프트웨어는 한글과컴퓨터의 HWP 문서 파일(.hwp) 공개 문서를 참고하여 개발되었습니다.
|
|
4
|
+
참조 스펙: 한글문서파일형식_5.0_revision1.3.pdf (2018.11.08)
|
|
5
|
+
|
|
6
|
+
This software was developed with reference to the publicly available HWP document
|
|
7
|
+
file format specification published by Hancom Inc.
|
|
8
|
+
Reference: HWP Document File Format 5.0 revision 1.3 (2018.11.08)
|
dochan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dochan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: dochan — HWP/HWPX 문서 파서, AI/LLM 최적 Markdown 변환
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/illuwa/dochan
|
|
7
|
+
Project-URL: Repository, https://github.com/illuwa/dochan
|
|
8
|
+
Project-URL: Issues, https://github.com/illuwa/dochan/issues
|
|
9
|
+
Keywords: hwp,hwpx,parser,korean,document,markdown,ai,llm,hancom
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
License-File: NOTICE
|
|
24
|
+
Requires-Dist: olefile>=0.47
|
|
25
|
+
Requires-Dist: lxml>=4.9
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Provides-Extra: ocr
|
|
31
|
+
Requires-Dist: pytesseract>=0.3; extra == "ocr"
|
|
32
|
+
Requires-Dist: Pillow>=9.0; extra == "ocr"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<h1 align="center">dochan (독한)</h1>
|
|
37
|
+
<p align="center">
|
|
38
|
+
<strong>독한 HWP/HWPX 파서 — AI/LLM 최적 Markdown 변환</strong>
|
|
39
|
+
</p>
|
|
40
|
+
<p align="center">
|
|
41
|
+
The toughest Korean document parser. HWP/HWPX → Markdown, JSON, Plain Text.
|
|
42
|
+
</p>
|
|
43
|
+
<p align="center">
|
|
44
|
+
<a href="https://pypi.org/project/dochan/"><img src="https://img.shields.io/pypi/v/dochan?color=blue" alt="PyPI"></a>
|
|
45
|
+
<a href="https://pypi.org/project/dochan/"><img src="https://img.shields.io/pypi/pyversions/dochan" alt="Python"></a>
|
|
46
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
47
|
+
</p>
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What is dochan?
|
|
53
|
+
|
|
54
|
+
**dochan**(독한)은 한글(HWP/HWPX) 문서를 파싱하여 AI/LLM이 바로 사용할 수 있는 Markdown으로 변환하는 Python 파서입니다.
|
|
55
|
+
|
|
56
|
+
- `doc` (문서) + `한` (韓, 한국) = **dochan** — "독한 파서"라는 더블 미닝
|
|
57
|
+
- HWP 5.0 바이너리 + HWPX(OWPML) XML 이중 지원
|
|
58
|
+
- 155개 실문서로 검증, 평균 96.5점, 공개가능한 문서로 계속 학습시켜 개선할 예정
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from dochan import Dochan
|
|
62
|
+
|
|
63
|
+
doc = Dochan("공문서.hwp")
|
|
64
|
+
print(doc.to_markdown())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
| 기능 | 설명 |
|
|
70
|
+
|------|------|
|
|
71
|
+
| **HWP + HWPX** | 바이너리(.hwp)와 XML(.hwpx) 모두 자동 감지 파싱 |
|
|
72
|
+
| **Markdown 출력** | 제목, 표, 서식(bold/italic), 수식까지 AI가 바로 쓸 수 있는 Markdown |
|
|
73
|
+
| **표 파싱** | 셀 병합, 중첩 표, 좌표 배치 지원 |
|
|
74
|
+
| **서식 보존** | CharShape 기반 bold/italic/글자크기 → TextRun 연결 |
|
|
75
|
+
| **제목 자동 감지** | Style 이름 + 글자 크기 기반 heading 레벨 판별 |
|
|
76
|
+
| **수식 LaTeX** | HWP 수식 스크립트 → LaTeX 기본 변환 |
|
|
77
|
+
| **JSON / Plain Text** | Markdown 외 구조화 JSON, 플레인 텍스트 출력 |
|
|
78
|
+
| **OCR (선택)** | Tesseract 연동, 이미지 속 텍스트 추출 |
|
|
79
|
+
| **CLI** | `dochan convert 문서.hwp` 한 줄로 변환 |
|
|
80
|
+
| **배치 처리** | 디렉토리 단위 병렬 변환 |
|
|
81
|
+
| **보안** | Zip Bomb, XXE, Path Traversal, 메모리 폭발 방어 |
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install dochan
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
OCR 기능이 필요한 경우:
|
|
90
|
+
```bash
|
|
91
|
+
pip install dochan[ocr]
|
|
92
|
+
brew install tesseract tesseract-lang # macOS
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
### Python API
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from dochan import Dochan
|
|
101
|
+
|
|
102
|
+
# HWP 또는 HWPX — 자동 감지
|
|
103
|
+
doc = Dochan("보고서.hwp")
|
|
104
|
+
|
|
105
|
+
# AI/LLM용 Markdown
|
|
106
|
+
markdown = doc.to_markdown()
|
|
107
|
+
|
|
108
|
+
# 구조화 JSON
|
|
109
|
+
json_str = doc.to_json()
|
|
110
|
+
|
|
111
|
+
# 플레인 텍스트
|
|
112
|
+
text = doc.to_plain_text()
|
|
113
|
+
|
|
114
|
+
# 요소별 접근
|
|
115
|
+
for table in doc.find_all('table'):
|
|
116
|
+
print(f"표: {table.row_count}행 x {table.col_count}열")
|
|
117
|
+
|
|
118
|
+
for eq in doc.find_all('equation'):
|
|
119
|
+
print(f"수식: {eq.latex}")
|
|
120
|
+
|
|
121
|
+
# 메타데이터
|
|
122
|
+
print(doc.metadata)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### CLI
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Markdown 변환 (stdout)
|
|
129
|
+
dochan convert 문서.hwp
|
|
130
|
+
|
|
131
|
+
# 파일로 저장
|
|
132
|
+
dochan convert 문서.hwp -o output.md
|
|
133
|
+
|
|
134
|
+
# JSON 출력
|
|
135
|
+
dochan convert 문서.hwpx --format json
|
|
136
|
+
|
|
137
|
+
# 디렉토리 일괄 변환
|
|
138
|
+
dochan batch input_dir/ output_dir/ --format markdown --workers 4
|
|
139
|
+
|
|
140
|
+
# 문서 정보
|
|
141
|
+
dochan info 문서.hwp
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### OCR (이미지 속 텍스트 추출)
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
doc = Dochan("이미지포함문서.hwpx", ocr=True)
|
|
148
|
+
print(doc.to_markdown()) # 이미지 속 텍스트도 포함
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Output Examples
|
|
152
|
+
|
|
153
|
+
### Input: 한글 공문서 (.hwp)
|
|
154
|
+
|
|
155
|
+
### Output: Markdown
|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
# **사내 규정집**
|
|
159
|
+
|
|
160
|
+
| 연번 | 내용 | 일자 |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| 1 | 제정 | 2020. 3. 1. |
|
|
163
|
+
| 2 | 개정 | 2024. 9.15. |
|
|
164
|
+
|
|
165
|
+
### **제1장 총칙**
|
|
166
|
+
|
|
167
|
+
**제1조(목적)** 이 규정은 회사 직원의 복무에 관한 사항을 정함을
|
|
168
|
+
목적으로 한다.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Supported Elements
|
|
172
|
+
|
|
173
|
+
| 요소 | HWP (바이너리) | HWPX (XML) |
|
|
174
|
+
|------|:-:|:-:|
|
|
175
|
+
| 텍스트 | ✅ | ✅ |
|
|
176
|
+
| 표 (단순) | ✅ | ✅ |
|
|
177
|
+
| 표 (셀 병합) | ✅ | ✅ |
|
|
178
|
+
| 표 (중첩) | ✅ | ✅ |
|
|
179
|
+
| 서식 (bold/italic) | ✅ | ✅ |
|
|
180
|
+
| 제목 감지 | ✅ | ✅ |
|
|
181
|
+
| 수식 | ✅ | ✅ |
|
|
182
|
+
| 이미지 참조 | ✅ | ✅ |
|
|
183
|
+
| 이미지 OCR | ✅ | ✅ |
|
|
184
|
+
| 머리글/바닥글 | ✅ | ⬜ |
|
|
185
|
+
| 각주/미주 | ✅ | ⬜ |
|
|
186
|
+
| 암호화 문서 | ⬜ | — |
|
|
187
|
+
|
|
188
|
+
✅ 지원 ⬜ 미지원 — 해당 없음
|
|
189
|
+
|
|
190
|
+
## Architecture
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
dochan/
|
|
194
|
+
├── reader.py # 통합 진입점 (Dochan 클래스)
|
|
195
|
+
├── cli.py # CLI 도구
|
|
196
|
+
├── hwp/ # HWP 5.0 바이너리 파서
|
|
197
|
+
│ ├── header.py # FileHeader (256바이트)
|
|
198
|
+
│ ├── doc_info.py # DocInfo (서식/스타일)
|
|
199
|
+
│ ├── section.py # 섹션 (레코드 트리 → 모델)
|
|
200
|
+
│ └── records/ # 개별 레코드 파서
|
|
201
|
+
├── hwpx/ # HWPX (OWPML) XML 파서
|
|
202
|
+
│ └── parser.py
|
|
203
|
+
├── model/ # Document 모델
|
|
204
|
+
│ ├── document.py # Document, Section, Paragraph
|
|
205
|
+
│ ├── table.py # Table, Cell
|
|
206
|
+
│ └── equation.py # Equation (LaTeX 변환)
|
|
207
|
+
├── output/ # 출력 포맷
|
|
208
|
+
│ ├── markdown.py # Markdown (AI/LLM 최적)
|
|
209
|
+
│ ├── json_out.py # 구조화 JSON
|
|
210
|
+
│ └── plain_text.py # 플레인 텍스트
|
|
211
|
+
└── utils/ # 유틸리티
|
|
212
|
+
├── ocr.py # Tesseract OCR
|
|
213
|
+
└── safe_decompress.py # Zip Bomb 방어
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Security
|
|
217
|
+
|
|
218
|
+
dochan은 신뢰할 수 없는 문서도 안전하게 처리합니다:
|
|
219
|
+
|
|
220
|
+
- **Zip Bomb 방어**: zlib/ZIP 해제 크기 제한 (200MB)
|
|
221
|
+
- **XXE 차단**: XML 외부 엔티티 해석 비활성화
|
|
222
|
+
- **Path Traversal 방지**: 배치 처리 시 경로 탈출 차단
|
|
223
|
+
- **메모리 제한**: 표 크기 1M셀, 재귀 깊이 100 제한
|
|
224
|
+
- **입력 검증**: FileHeader/스트림명/바이너리 바운드 체크
|
|
225
|
+
|
|
226
|
+
## Contributing
|
|
227
|
+
|
|
228
|
+
기여를 환영합니다! Issues, Pull Requests 모두 열려 있습니다.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# 개발 환경 설정
|
|
232
|
+
git clone https://github.com/illuwa/dochan.git
|
|
233
|
+
cd dochan
|
|
234
|
+
pip install -e ".[dev]"
|
|
235
|
+
python -m pytest dochan/tests/
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Acknowledgments
|
|
239
|
+
|
|
240
|
+
> [kordoc](https://github.com/chrisryugj/kordoc)를 보고 자극받아 만들었습니다.
|
|
241
|
+
|
|
242
|
+
dochan은 다음 프로젝트와 자료를 기반으로 개발되었습니다:
|
|
243
|
+
|
|
244
|
+
**스펙 참조**
|
|
245
|
+
- [한글과컴퓨터](https://www.hancom.com/) — HWP 문서 파일 형식 5.0 공개 스펙 (revision 1.3, 2018)
|
|
246
|
+
- [OWPML (KS X 6101:2011)](https://www.kssn.net/) — HWPX 국가 표준
|
|
247
|
+
|
|
248
|
+
**오픈소스**
|
|
249
|
+
- [olefile](https://github.com/decalage2/olefile) — OLE2 파일 파싱 (Philippe Lagadec, BSD)
|
|
250
|
+
- [lxml](https://lxml.de/) — XML 파싱 (BSD)
|
|
251
|
+
- [pdfplumber](https://github.com/jsvine/pdfplumber) — 품질 검증용 PDF 추출 (MIT)
|
|
252
|
+
- [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) — 이미지 텍스트 추출 (Apache 2.0)
|
|
253
|
+
|
|
254
|
+
**선행 연구**
|
|
255
|
+
- [hwplib](https://github.com/neolord0/hwplib) (Java) — HWP 레코드 구조 참조
|
|
256
|
+
- [hwp.js](https://github.com/niceeee/hwp.js) — 레코드 트리 구축 참조
|
|
257
|
+
- [pyhwp](https://github.com/mete0r/pyhwp) — Python HWP 파서 선구자
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
[MIT License](LICENSE)
|
|
262
|
+
|
|
263
|
+
본 소프트웨어는 한글과컴퓨터의 HWP 문서 파일(.hwp) 공개 문서를 참고하여 개발되었습니다. 자세한 내용은 [NOTICE](NOTICE) 파일을 참조하세요.
|
dochan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">dochan (독한)</h1>
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>독한 HWP/HWPX 파서 — AI/LLM 최적 Markdown 변환</strong>
|
|
5
|
+
</p>
|
|
6
|
+
<p align="center">
|
|
7
|
+
The toughest Korean document parser. HWP/HWPX → Markdown, JSON, Plain Text.
|
|
8
|
+
</p>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://pypi.org/project/dochan/"><img src="https://img.shields.io/pypi/v/dochan?color=blue" alt="PyPI"></a>
|
|
11
|
+
<a href="https://pypi.org/project/dochan/"><img src="https://img.shields.io/pypi/pyversions/dochan" alt="Python"></a>
|
|
12
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
13
|
+
</p>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What is dochan?
|
|
19
|
+
|
|
20
|
+
**dochan**(독한)은 한글(HWP/HWPX) 문서를 파싱하여 AI/LLM이 바로 사용할 수 있는 Markdown으로 변환하는 Python 파서입니다.
|
|
21
|
+
|
|
22
|
+
- `doc` (문서) + `한` (韓, 한국) = **dochan** — "독한 파서"라는 더블 미닝
|
|
23
|
+
- HWP 5.0 바이너리 + HWPX(OWPML) XML 이중 지원
|
|
24
|
+
- 155개 실문서로 검증, 평균 96.5점, 공개가능한 문서로 계속 학습시켜 개선할 예정
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from dochan import Dochan
|
|
28
|
+
|
|
29
|
+
doc = Dochan("공문서.hwp")
|
|
30
|
+
print(doc.to_markdown())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
| 기능 | 설명 |
|
|
36
|
+
|------|------|
|
|
37
|
+
| **HWP + HWPX** | 바이너리(.hwp)와 XML(.hwpx) 모두 자동 감지 파싱 |
|
|
38
|
+
| **Markdown 출력** | 제목, 표, 서식(bold/italic), 수식까지 AI가 바로 쓸 수 있는 Markdown |
|
|
39
|
+
| **표 파싱** | 셀 병합, 중첩 표, 좌표 배치 지원 |
|
|
40
|
+
| **서식 보존** | CharShape 기반 bold/italic/글자크기 → TextRun 연결 |
|
|
41
|
+
| **제목 자동 감지** | Style 이름 + 글자 크기 기반 heading 레벨 판별 |
|
|
42
|
+
| **수식 LaTeX** | HWP 수식 스크립트 → LaTeX 기본 변환 |
|
|
43
|
+
| **JSON / Plain Text** | Markdown 외 구조화 JSON, 플레인 텍스트 출력 |
|
|
44
|
+
| **OCR (선택)** | Tesseract 연동, 이미지 속 텍스트 추출 |
|
|
45
|
+
| **CLI** | `dochan convert 문서.hwp` 한 줄로 변환 |
|
|
46
|
+
| **배치 처리** | 디렉토리 단위 병렬 변환 |
|
|
47
|
+
| **보안** | Zip Bomb, XXE, Path Traversal, 메모리 폭발 방어 |
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install dochan
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
OCR 기능이 필요한 경우:
|
|
56
|
+
```bash
|
|
57
|
+
pip install dochan[ocr]
|
|
58
|
+
brew install tesseract tesseract-lang # macOS
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
### Python API
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from dochan import Dochan
|
|
67
|
+
|
|
68
|
+
# HWP 또는 HWPX — 자동 감지
|
|
69
|
+
doc = Dochan("보고서.hwp")
|
|
70
|
+
|
|
71
|
+
# AI/LLM용 Markdown
|
|
72
|
+
markdown = doc.to_markdown()
|
|
73
|
+
|
|
74
|
+
# 구조화 JSON
|
|
75
|
+
json_str = doc.to_json()
|
|
76
|
+
|
|
77
|
+
# 플레인 텍스트
|
|
78
|
+
text = doc.to_plain_text()
|
|
79
|
+
|
|
80
|
+
# 요소별 접근
|
|
81
|
+
for table in doc.find_all('table'):
|
|
82
|
+
print(f"표: {table.row_count}행 x {table.col_count}열")
|
|
83
|
+
|
|
84
|
+
for eq in doc.find_all('equation'):
|
|
85
|
+
print(f"수식: {eq.latex}")
|
|
86
|
+
|
|
87
|
+
# 메타데이터
|
|
88
|
+
print(doc.metadata)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### CLI
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Markdown 변환 (stdout)
|
|
95
|
+
dochan convert 문서.hwp
|
|
96
|
+
|
|
97
|
+
# 파일로 저장
|
|
98
|
+
dochan convert 문서.hwp -o output.md
|
|
99
|
+
|
|
100
|
+
# JSON 출력
|
|
101
|
+
dochan convert 문서.hwpx --format json
|
|
102
|
+
|
|
103
|
+
# 디렉토리 일괄 변환
|
|
104
|
+
dochan batch input_dir/ output_dir/ --format markdown --workers 4
|
|
105
|
+
|
|
106
|
+
# 문서 정보
|
|
107
|
+
dochan info 문서.hwp
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### OCR (이미지 속 텍스트 추출)
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
doc = Dochan("이미지포함문서.hwpx", ocr=True)
|
|
114
|
+
print(doc.to_markdown()) # 이미지 속 텍스트도 포함
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Output Examples
|
|
118
|
+
|
|
119
|
+
### Input: 한글 공문서 (.hwp)
|
|
120
|
+
|
|
121
|
+
### Output: Markdown
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+
# **사내 규정집**
|
|
125
|
+
|
|
126
|
+
| 연번 | 내용 | 일자 |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| 1 | 제정 | 2020. 3. 1. |
|
|
129
|
+
| 2 | 개정 | 2024. 9.15. |
|
|
130
|
+
|
|
131
|
+
### **제1장 총칙**
|
|
132
|
+
|
|
133
|
+
**제1조(목적)** 이 규정은 회사 직원의 복무에 관한 사항을 정함을
|
|
134
|
+
목적으로 한다.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Supported Elements
|
|
138
|
+
|
|
139
|
+
| 요소 | HWP (바이너리) | HWPX (XML) |
|
|
140
|
+
|------|:-:|:-:|
|
|
141
|
+
| 텍스트 | ✅ | ✅ |
|
|
142
|
+
| 표 (단순) | ✅ | ✅ |
|
|
143
|
+
| 표 (셀 병합) | ✅ | ✅ |
|
|
144
|
+
| 표 (중첩) | ✅ | ✅ |
|
|
145
|
+
| 서식 (bold/italic) | ✅ | ✅ |
|
|
146
|
+
| 제목 감지 | ✅ | ✅ |
|
|
147
|
+
| 수식 | ✅ | ✅ |
|
|
148
|
+
| 이미지 참조 | ✅ | ✅ |
|
|
149
|
+
| 이미지 OCR | ✅ | ✅ |
|
|
150
|
+
| 머리글/바닥글 | ✅ | ⬜ |
|
|
151
|
+
| 각주/미주 | ✅ | ⬜ |
|
|
152
|
+
| 암호화 문서 | ⬜ | — |
|
|
153
|
+
|
|
154
|
+
✅ 지원 ⬜ 미지원 — 해당 없음
|
|
155
|
+
|
|
156
|
+
## Architecture
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
dochan/
|
|
160
|
+
├── reader.py # 통합 진입점 (Dochan 클래스)
|
|
161
|
+
├── cli.py # CLI 도구
|
|
162
|
+
├── hwp/ # HWP 5.0 바이너리 파서
|
|
163
|
+
│ ├── header.py # FileHeader (256바이트)
|
|
164
|
+
│ ├── doc_info.py # DocInfo (서식/스타일)
|
|
165
|
+
│ ├── section.py # 섹션 (레코드 트리 → 모델)
|
|
166
|
+
│ └── records/ # 개별 레코드 파서
|
|
167
|
+
├── hwpx/ # HWPX (OWPML) XML 파서
|
|
168
|
+
│ └── parser.py
|
|
169
|
+
├── model/ # Document 모델
|
|
170
|
+
│ ├── document.py # Document, Section, Paragraph
|
|
171
|
+
│ ├── table.py # Table, Cell
|
|
172
|
+
│ └── equation.py # Equation (LaTeX 변환)
|
|
173
|
+
├── output/ # 출력 포맷
|
|
174
|
+
│ ├── markdown.py # Markdown (AI/LLM 최적)
|
|
175
|
+
│ ├── json_out.py # 구조화 JSON
|
|
176
|
+
│ └── plain_text.py # 플레인 텍스트
|
|
177
|
+
└── utils/ # 유틸리티
|
|
178
|
+
├── ocr.py # Tesseract OCR
|
|
179
|
+
└── safe_decompress.py # Zip Bomb 방어
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Security
|
|
183
|
+
|
|
184
|
+
dochan은 신뢰할 수 없는 문서도 안전하게 처리합니다:
|
|
185
|
+
|
|
186
|
+
- **Zip Bomb 방어**: zlib/ZIP 해제 크기 제한 (200MB)
|
|
187
|
+
- **XXE 차단**: XML 외부 엔티티 해석 비활성화
|
|
188
|
+
- **Path Traversal 방지**: 배치 처리 시 경로 탈출 차단
|
|
189
|
+
- **메모리 제한**: 표 크기 1M셀, 재귀 깊이 100 제한
|
|
190
|
+
- **입력 검증**: FileHeader/스트림명/바이너리 바운드 체크
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
기여를 환영합니다! Issues, Pull Requests 모두 열려 있습니다.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# 개발 환경 설정
|
|
198
|
+
git clone https://github.com/illuwa/dochan.git
|
|
199
|
+
cd dochan
|
|
200
|
+
pip install -e ".[dev]"
|
|
201
|
+
python -m pytest dochan/tests/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Acknowledgments
|
|
205
|
+
|
|
206
|
+
> [kordoc](https://github.com/chrisryugj/kordoc)를 보고 자극받아 만들었습니다.
|
|
207
|
+
|
|
208
|
+
dochan은 다음 프로젝트와 자료를 기반으로 개발되었습니다:
|
|
209
|
+
|
|
210
|
+
**스펙 참조**
|
|
211
|
+
- [한글과컴퓨터](https://www.hancom.com/) — HWP 문서 파일 형식 5.0 공개 스펙 (revision 1.3, 2018)
|
|
212
|
+
- [OWPML (KS X 6101:2011)](https://www.kssn.net/) — HWPX 국가 표준
|
|
213
|
+
|
|
214
|
+
**오픈소스**
|
|
215
|
+
- [olefile](https://github.com/decalage2/olefile) — OLE2 파일 파싱 (Philippe Lagadec, BSD)
|
|
216
|
+
- [lxml](https://lxml.de/) — XML 파싱 (BSD)
|
|
217
|
+
- [pdfplumber](https://github.com/jsvine/pdfplumber) — 품질 검증용 PDF 추출 (MIT)
|
|
218
|
+
- [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) — 이미지 텍스트 추출 (Apache 2.0)
|
|
219
|
+
|
|
220
|
+
**선행 연구**
|
|
221
|
+
- [hwplib](https://github.com/neolord0/hwplib) (Java) — HWP 레코드 구조 참조
|
|
222
|
+
- [hwp.js](https://github.com/niceeee/hwp.js) — 레코드 트리 구축 참조
|
|
223
|
+
- [pyhwp](https://github.com/mete0r/pyhwp) — Python HWP 파서 선구자
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
[MIT License](LICENSE)
|
|
228
|
+
|
|
229
|
+
본 소프트웨어는 한글과컴퓨터의 HWP 문서 파일(.hwp) 공개 문서를 참고하여 개발되었습니다. 자세한 내용은 [NOTICE](NOTICE) 파일을 참조하세요.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
batch.py — 배치 처리
|
|
3
|
+
다수 HWP 파일의 병렬 변환
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger('dochan')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class BatchResult:
|
|
18
|
+
"""단일 파일 처리 결과"""
|
|
19
|
+
file_path: str = ""
|
|
20
|
+
success: bool = False
|
|
21
|
+
output_path: str = ""
|
|
22
|
+
error_count: int = 0
|
|
23
|
+
errors: List[str] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class BatchSummary:
|
|
28
|
+
"""배치 처리 전체 요약"""
|
|
29
|
+
total: int = 0
|
|
30
|
+
success: int = 0
|
|
31
|
+
failed: int = 0
|
|
32
|
+
results: List[BatchResult] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def success_rate(self) -> float:
|
|
36
|
+
return self.success / max(self.total, 1) * 100
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _process_single(file_path: str, output_dir: str, output_format: str) -> BatchResult:
|
|
40
|
+
"""단일 파일 처리 (별도 프로세스에서 실행)"""
|
|
41
|
+
from .reader import Dochan
|
|
42
|
+
|
|
43
|
+
result = BatchResult(file_path=file_path)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
reader = Dochan(file_path)
|
|
47
|
+
|
|
48
|
+
# 출력 파일명 생성
|
|
49
|
+
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
50
|
+
ext_map = {'markdown': '.md', 'json': '.json', 'text': '.txt'}
|
|
51
|
+
ext = ext_map.get(output_format, '.md')
|
|
52
|
+
output_path = os.path.join(output_dir, base_name + ext)
|
|
53
|
+
|
|
54
|
+
# 변환
|
|
55
|
+
if output_format == 'json':
|
|
56
|
+
content = reader.to_json()
|
|
57
|
+
elif output_format == 'text':
|
|
58
|
+
content = reader.to_plain_text()
|
|
59
|
+
else:
|
|
60
|
+
content = reader.to_markdown()
|
|
61
|
+
|
|
62
|
+
# 저장
|
|
63
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
64
|
+
f.write(content)
|
|
65
|
+
|
|
66
|
+
result.success = True
|
|
67
|
+
result.output_path = output_path
|
|
68
|
+
result.errors = reader.errors
|
|
69
|
+
result.error_count = len(reader.errors)
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
result.success = False
|
|
73
|
+
result.errors = [str(e)]
|
|
74
|
+
result.error_count = 1
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def batch_convert(
|
|
80
|
+
input_dir: str,
|
|
81
|
+
output_dir: str,
|
|
82
|
+
output_format: str = 'markdown',
|
|
83
|
+
max_workers: int = 4,
|
|
84
|
+
extensions: tuple = ('.hwp', '.hwpx'),
|
|
85
|
+
) -> BatchSummary:
|
|
86
|
+
"""
|
|
87
|
+
디렉토리 내 HWP 파일 일괄 변환
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
input_dir: 입력 디렉토리
|
|
91
|
+
output_dir: 출력 디렉토리
|
|
92
|
+
output_format: 'markdown', 'json', 'text'
|
|
93
|
+
max_workers: 병렬 워커 수
|
|
94
|
+
extensions: 처리할 확장자
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
BatchSummary
|
|
98
|
+
"""
|
|
99
|
+
# output_dir 경로 검증
|
|
100
|
+
try:
|
|
101
|
+
Path(output_dir).resolve().relative_to(Path(os.getcwd()).resolve())
|
|
102
|
+
except ValueError:
|
|
103
|
+
pass # output_dir가 cwd 밖이어도 허용하되, 아래에서 symlink 공격 방지
|
|
104
|
+
|
|
105
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
# 파일 수집
|
|
108
|
+
files = []
|
|
109
|
+
resolved_input = Path(input_dir).resolve()
|
|
110
|
+
for root, _, filenames in os.walk(input_dir):
|
|
111
|
+
for fn in filenames:
|
|
112
|
+
if any(fn.lower().endswith(ext) for ext in extensions):
|
|
113
|
+
full_path = os.path.join(root, fn)
|
|
114
|
+
# Path traversal 방지: 실제 경로가 input_dir 내부인지 검증
|
|
115
|
+
try:
|
|
116
|
+
Path(full_path).resolve().relative_to(resolved_input)
|
|
117
|
+
except ValueError:
|
|
118
|
+
logger.warning(f"경로 이탈 감지, 건너뜀: {full_path}")
|
|
119
|
+
continue
|
|
120
|
+
files.append(full_path)
|
|
121
|
+
|
|
122
|
+
summary = BatchSummary(total=len(files))
|
|
123
|
+
logger.info(f"배치 시작: {len(files)}개 파일, {max_workers} 워커")
|
|
124
|
+
|
|
125
|
+
# 병렬 처리
|
|
126
|
+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
127
|
+
futures = {
|
|
128
|
+
executor.submit(_process_single, f, output_dir, output_format): f
|
|
129
|
+
for f in files
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for future in as_completed(futures):
|
|
133
|
+
file_path = futures[future]
|
|
134
|
+
try:
|
|
135
|
+
result = future.result()
|
|
136
|
+
summary.results.append(result)
|
|
137
|
+
if result.success:
|
|
138
|
+
summary.success += 1
|
|
139
|
+
logger.info(f"✓ {os.path.basename(file_path)}")
|
|
140
|
+
else:
|
|
141
|
+
summary.failed += 1
|
|
142
|
+
logger.error(f"✗ {os.path.basename(file_path)}: {result.errors}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
summary.failed += 1
|
|
145
|
+
summary.results.append(BatchResult(
|
|
146
|
+
file_path=file_path,
|
|
147
|
+
success=False,
|
|
148
|
+
errors=[str(e)],
|
|
149
|
+
error_count=1,
|
|
150
|
+
))
|
|
151
|
+
logger.error(f"✗ {os.path.basename(file_path)}: {e}")
|
|
152
|
+
|
|
153
|
+
logger.info(f"배치 완료: {summary.success}/{summary.total} 성공 ({summary.success_rate:.1f}%)")
|
|
154
|
+
return summary
|