MakeAI 딥러닝

HWPX는 HWP랑 뭐가 다를까 — ZIP 안의 XML, 그리고 하나의 중간 표현

HWPX는 ZIP으로 묶인 XML(OWPML)이라 읽기는 훨씬 편해요. 그런데 HWP와 같은 문서인데 구조가 미묘하게 달라서, 저희는 두 형식을 하나의 중간 표현으로 합쳐서 다뤄요.

4분 읽기

지난주에 HWP 5.0 바이너리 구조를 뜯어봤는데요, 오늘은 그 "새 버전"인 HWPX 이야기예요. 요즘 공공기관에서 개방형 문서라고 HWPX로 배포하는 경우가 늘고 있어서, 파서가 둘 다 읽어야 해요.

HWPX는 사실상 ZIP이에요

HWPX 파일 확장자를 .zip으로 바꿔서 풀어 보시면 돼요. 안에는 이런 게 들어 있어요.

mimetype
version.xml
META-INF/container.xml
Contents/header.xml      ← 글꼴, 스타일, 문단 모양 (HWP의 DocInfo)
Contents/section0.xml    ← 본문 (HWP의 BodyText/Section0)
Contents/content.hpf
BinData/image1.png
Preview/PrvText.txt

구조가 ODF(오픈 문서 형식)나 OOXML(docx)과 비슷하죠. XML 스키마는 OWPML(Open Word-Processor Markup Language)이라고 KS 표준으로 등록돼 있어요. 바이너리 레코드를 비트 단위로 뜯던 지난주에 비하면 훨씬 사람 친화적이에요.

그런데 "같은 문서"가 아니에요

처음엔 HWP 파서 따로, HWPX 파서 따로 만들고 결과만 같은 형식으로 내면 되겠지 싶었어요. 그런데 실제로 같은 문서를 두 형식으로 저장해서 비교해 보니 미묘하게 다른 데가 꽤 있었어요.

  • 문자 모양이 붙는 방식. HWP는 문단 텍스트와 "위치 → 문자 모양 ID" 배열이 따로 있어요. HWPX는 <hp:run charPrIDRef="..."> 처럼 텍스트가 run 단위로 쪼개져 있고요. 결과는 같아야 하는데, run 경계와 문자 모양 전환 위치가 1글자씩 어긋나는 파일이 있었어요.
  • 표 병합. HWP는 셀 속성에 colspan/rowspan이 있고, HWPX는 <hp:cellSpan colSpan="2" rowSpan="1"/> 로 들어 있어요. 이건 1:1로 맞아요. 다만 HWPX에서는 <hp:cellAddr>가 따로 있어서 셀 주소를 두 군데서 읽을 수 있고, 둘이 다른 파일도 봤어요(저희는 cellAddr을 믿어요).
  • 각주. HWP는 컨트롤 문자 17 뒤에 각주 본문이 자식 레코드로 와요. HWPX는 <hp:footNote> 요소가 run 안에 인라인으로 들어 있어요. 위치는 같지만 트리 모양이 달라서, "각주 참조가 붙은 글자 위치"를 뽑는 코드가 따로 필요했어요.
  • 빈 요소. HWPX는 빈 <hp:t/> 가 자주 나와요. 빈 문단으로 처리하면 줄이 늘어나요.

그래서 중간 표현을 하나로 만들었어요

파서를 두 개 만들되, 출력은 하나의 중간 표현(IR) 으로 맞췄어요. 대략 이런 모양이에요.

{
  "type": "table",
  "rows": 3, "cols": 4,
  "cells": [
    { "r": 0, "c": 0, "rowspan": 1, "colspan": 2,
      "blocks": [ { "type": "paragraph", "runs": [ { "text": "구분", "bold": true } ] } ],
      "src": { "section": 0, "para": 12 } }
  ]
}

핵심은 두 가지예요.

  1. 구조는 HWP/HWPX 어느 쪽에도 기울지 않게. 둘 다 표현할 수 있는 최소 공통 집합으로 잡았어요. 특정 형식에만 있는 서식 정보는 ext 필드에 따로 넣어 두고, 변환기는 그걸 무시해도 되게 했어요.
  2. 모든 블록에 src가 있어요. 어느 구역의 몇 번째 문단에서 왔는지요. 나중에 추출 결과를 원본에서 찾아갈 때 이게 없으면 아무것도 못 해요. 지난 1월 6일 글에서 말씀드린 "확인 가능한 추출"의 시작점이에요.

이렇게 해 두니까 Markdown 변환기, JSON 변환기, RAG 청크 분할기가 전부 IR 하나만 보면 돼요. 새 형식(예를 들어 DOCX)이 들어와도 파서 하나만 추가하면 되고요.

테스트는 어떻게 했냐면요

같은 원본 문서를 한/글에서 HWP와 HWPX로 각각 저장한 뒤, 두 파서의 IR 출력을 diff 하는 방식으로 테스트했어요. 서식 세부 정보(ext)를 뺀 구조 diff가 0이어야 통과예요. 저희가 모은 공개 문서(정부 보도자료, 입찰 공고, 학술 논문 서식 등) 수백 건에서 시작했고, 처음에는 절반 넘게 diff가 나왔어요. 지금은 거의 다 통과하는데, 아직 안 되는 건 주로 글상자 안에 표가 들어간 경우와 세로쓰기예요. 이건 솔직히 아직 못 풀었어요.

정리

  • HWPX는 읽기 쉬워요. 하지만 "쉽다"와 "HWP랑 같다"는 다른 얘기예요.
  • 두 형식을 각자 파싱하되, 출력은 하나의 IR로 모으세요. 그 IR에 원본 위치를 꼭 남기시고요.
  • 테스트는 같은 문서를 두 형식으로 저장해서 diff 하는 게 가장 확실했어요.

다음 주에는 이 IR을 Markdown으로 바꿀 때 뭘 잃고 뭘 지켜야 하는지 이야기해 볼게요. 병합 셀 문제가 드디어 등장해요.