aiaiteam Claude Sonnet 4.6 commited on
Commit
02ba824
·
1 Parent(s): f0ac9a7

fix: HWP 읽기 — LibreOffice 제거, olefile 순수 Python 파서로 교체

Browse files

- requirements.txt: olefile==0.47 추가
- Dockerfile: libreoffice-writer 제거 (이미지 크기 대폭 감소)
- app.py: _ext_hwp()를 OLE2 직접 파싱 방식으로 재구현
(HWPTAG_PARA_TEXT 레코드에서 UTF-16LE 텍스트 추출)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. Dockerfile +0 -5
  2. app.py +52 -14
  3. requirements.txt +1 -0
Dockerfile CHANGED
@@ -2,11 +2,6 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /code
4
 
5
- # LibreOffice: .hwp 파일을 텍스트로 변환
6
- RUN apt-get update && apt-get install -y --no-install-recommends \
7
- libreoffice-writer \
8
- && rm -rf /var/lib/apt/lists/*
9
-
10
  COPY requirements.txt .
11
  RUN pip install --no-cache-dir -r requirements.txt
12
 
 
2
 
3
  WORKDIR /code
4
 
 
 
 
 
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
 
app.py CHANGED
@@ -286,20 +286,58 @@ def _ext_hwpx(path: Path) -> str:
286
 
287
 
288
  def _ext_hwp(path: Path) -> str:
289
- import subprocess
290
- import tempfile
291
- with tempfile.TemporaryDirectory() as tmpdir:
292
- r = subprocess.run(
293
- ["libreoffice", "--headless", "--norestore",
294
- "--convert-to", "txt:Text",
295
- "--outdir", tmpdir, str(path)],
296
- capture_output=True, text=True, timeout=60,
297
- env={**os.environ, "HOME": "/tmp"},
298
- )
299
- txt = Path(tmpdir) / (path.stem + ".txt")
300
- if not txt.exists():
301
- raise RuntimeError(f"HWP 텍스트 변환 실패\n{r.stderr[:300]}")
302
- return _ext_txt(txt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
 
305
  def _ext_docx(path: Path) -> str:
 
286
 
287
 
288
  def _ext_hwp(path: Path) -> str:
289
+ import struct, zlib
290
+ try:
291
+ import olefile
292
+ except ImportError:
293
+ raise RuntimeError("olefile 라이브러리 없음 (pip install olefile)")
294
+
295
+ ole = olefile.OleFileIO(str(path))
296
+
297
+ compressed = True
298
+ if ole.exists("FileHeader"):
299
+ hdr = ole.openstream("FileHeader").read()
300
+ if len(hdr) >= 40:
301
+ compressed = bool(struct.unpack_from("<I", hdr, 36)[0] & 0x1)
302
+
303
+ lines: list[str] = []
304
+ idx = 1
305
+ while True:
306
+ sname = f"BodyText/Section{idx:04d}"
307
+ if not ole.exists(sname):
308
+ break
309
+ raw = ole.openstream(sname).read()
310
+ data = zlib.decompress(raw, -15) if compressed else raw
311
+
312
+ pos = 0
313
+ while pos + 4 <= len(data):
314
+ hval = struct.unpack_from("<I", data, pos)[0]
315
+ tag_id = hval & 0x3FF
316
+ size = (hval >> 20) & 0xFFF
317
+ pos += 4
318
+ if size == 0xFFF:
319
+ if pos + 4 > len(data): break
320
+ size = struct.unpack_from("<I", data, pos)[0]
321
+ pos += 4
322
+
323
+ if tag_id == 67 and size >= 2: # HWPTAG_PARA_TEXT
324
+ chunk = data[pos:pos + size]
325
+ t, chars = 0, []
326
+ while t + 2 <= len(chunk):
327
+ cp = struct.unpack_from("<H", chunk, t)[0]
328
+ t += 2
329
+ if cp == 0x0D:
330
+ lines.append("".join(chars)); chars = []
331
+ elif 0x01 <= cp <= 0x0C:
332
+ t += 8 # inline object: skip 8-byte attribute block
333
+ elif cp >= 0x20:
334
+ chars.append(chr(cp))
335
+ if chars:
336
+ lines.append("".join(chars))
337
+ pos += size
338
+ idx += 1
339
+
340
+ return re.sub(r"\n{3,}", "\n\n", "\n".join(l for l in lines if l)).strip()
341
 
342
 
343
  def _ext_docx(path: Path) -> str:
requirements.txt CHANGED
@@ -10,3 +10,4 @@ aiofiles==23.2.1
10
  python-multipart==0.0.12
11
  python-docx==1.1.2
12
  pypdf==5.1.0
 
 
10
  python-multipart==0.0.12
11
  python-docx==1.1.2
12
  pypdf==5.1.0
13
+ olefile==0.47