공고문에서 “기술평가 배점이 얼마냐”를 물으면 검색 증강 생성이 엉뚱한 숫자를 내는 일이 있습니다. 문서를 못 찾은 게 아닙니다. 문서는 찾았는데, 그 안의 표가 본문으로 풀리면서 “기술평가” 라는 라벨과 “90” 이라는 숫자가 서로 다른 줄로 흩어진 것입니다. 모델은 가까이 있는 다른 숫자를 집습니다. 로그에는 아무 오류도 남지 않습니다.
한국 공공 문서는 핵심을 표에 둡니다. 평가 배점, 제출서류 목록, 가격 내역, 과업 일정. 그래서 추출 단계에서 표가 어떻게 되는지가 검색 품질 전체를 좌우합니다. 말로만 하면 누구나 하는 얘기라, 숫자로 재 봤습니다.
무엇을 쟀나
- 문서: 나라장터 공고 첨부 216파일(공고 62건, 2026년 5월 이후 용역·경쟁입찰에서 무작위) 과 공정위 표준계약서·제안요청서 35파일. 채점한 것은 214문서 — HWP 108, HWPX 59, PDF 47.
- 정답: 추출기와 독립된 경로로 표를 읽었습니다. HWPX 는 zip 안의 XML 에서 표 셀을 직접, HWP 는 제3자 구조 파서(pyhwp)의 HTML 변환에서
<table>을. PDF 는 같은 공고에 같은 이름의 HWP 원본이 있을 때만 채점하고, 그 원본의 표를 정답으로 썼습니다. 즉 “같은 문서를 PDF 로 받으면 얼마나 잃는가”를 잰 것입니다. - 지표 셋, 전부 자동 판정: 셀 보존율(정답 셀의 글자가 출력에 남았나), 행 무결성(한 행의 셀들이 출력의 같은 줄에 있나), 라벨↔숫자 짝(평가항목과 배점처럼 한글 라벨과 숫자가 같은 줄에 있나). 세 번째가 위의 증상에 직접 닿는 지표입니다.
- 비교한 추출기: HWP 는 pyhwp 의 hwp5txt 와 GitHub 에 흔한 나이브 추출(olefile 로 본문 스트림을 열어 문단만 긁는 방식), HWPX 는 XML 태그 제거, PDF 는 pdftotext(기본·-layout)·PyMuPDF·pdfplumber(텍스트·표 인식). 그리고 미리보아가 쓰는 추출 체인.
실패는 두 종류다
| 형식 | 추출기 | 셀 보존 | 행 무결성 | 라벨↔숫자 짝 |
|---|---|---|---|---|
| HWP (108) | hwp5txt | 0.152 | 0.017 | 0.057 |
| HWP (108) | 나이브 추출 | 0.988 | 0.038 | 0.104 |
| HWP (108) | 미리보아 체인 | 1.000 | 1.000 | 1.000 |
| HWPX (59) | XML 태그 제거 | 0.994 | 0.038 | 0.070 |
| HWPX (59) | 미리보아 체인 | 1.000 | 0.991 | 0.997 |
첫 번째 실패는 글자가 사라지는 실패입니다. hwp5txt 는 표를 <표> 한 토큰으로 바꿉니다. 셀의 85% 가 없어집니다. 이건 눈에 보입니다. 출력이 짧고, 배점표가 통째로 없습니다.
두 번째 실패는 구조가 사라지는 실패입니다. 나이브 추출과 XML 태그 제거는 글자를 99% 남깁니다. 텍스트 양이 정상이라 아무도 의심하지 않습니다. 그런데 행 무결성이 4%, 라벨↔숫자 짝이 7~10% 입니다. “기술평가” 와 “90” 이 둘 다 문서 안에 있지만 서로 다른 줄에 있습니다. 검색은 이 문서를 찾아 오고, 모델은 그 안에서 가까운 숫자를 고릅니다. 두 번째 실패가 첫 번째보다 위험한 이유입니다.
표를 표로 옮기면 어떻게 되는지가 세 번째 줄입니다. 형식을 제대로 읽으면 HWP 원본에서 셀·행·짝이 전부 살아남습니다. 한 행을 한 줄로, 셀을 구분자로 직렬화하는 것뿐이지 특별한 기술이 아닙니다. 다만 그렇게 하지 않는 범용 경로가 기본값이라는 게 문제입니다.
같은 문서를 PDF 로 받으면
공고는 흔히 같은 문서를 HWP 와 PDF 로 둘 다 붙입니다. 47쌍에서 원본과 PDF 를 비교했습니다(문서 평균).
| 셀 보존 | 행 무결성 | 라벨↔숫자 짝 | |
|---|---|---|---|
| HWP·HWPX 원본 | 1.000 | 0.998 | 0.995 |
| PDF, 문서마다 가장 잘한 도구 | 0.872 | – | – |
| PDF, 미리보아 체인 | 0.819 | 0.657 | 0.558 |
| PDF, pdfplumber 표 인식 | 0.725 | 0.672 | 0.572 |
| PDF, pdftotext -layout | 0.729 | 0.552 | 0.605 |
PDF 에서는 문서마다 가장 잘한 도구를 골라 써도 셀의 13% 가 없고, 행 구조는 최대 67%, 라벨↔숫자 짝은 최대 61% 입니다. 어느 도구를 쓰느냐보다 어느 형식을 읽느냐가 더 큰 차이를 냅니다. 공고가 둘 다 붙였으면 HWP 를 읽어야 합니다.
측정이 우리 결함을 둘 찾았다
미리보아 체인의 첫 결과는 HWP 셀 보존 0.952, PDF 0.498 이었습니다. 100% 와 0.742 가 아니라. 그 차이가 어디서 왔는지 파 보니 둘 다 우리 코드의 결함이었습니다.
첫째, 확장자를 믿었습니다. 나라장터 첨부 216파일 중 5건은 이름이 .hwpx 인데 내용은 HWP 바이너리였고, 3건은 이름이 .hwp 인데 내용은 XML 이었습니다. 우리 체인은 .hwpx 이름을 보고 zip 을 열려다 5건 전부 실패했습니다. 첫 바이트를 보고 형식을 정하도록 고쳤습니다.
둘째, PDF 6건에서 셀의 80% 를 놓쳤습니다. 처음엔 pdfplumber 가 poppler 보다 못 읽는다고 봤습니다. 원인을 파 보니 한글 2024 가 내보낸 PDF 는 낱말 사이 공백을 NUL 문자로 쓰고, pdfplumber 는 그것을 그대로 넘기고 있었습니다. poppler 는 NUL 을 공백으로 읽어서 멀쩡해 보였을 뿐입니다. 처방은 도구 교체가 아니라 NUL 을 공백으로 바꾸는 한 줄이었습니다. 원인을 보지 않고 도구를 바꿨다면 다른 문서에서 다른 것을 잃었을 겁니다.
고친 뒤 같은 214문서를 다시 쟀습니다. HWP 는 셀·행·짝 모두 1.000, PDF 는 셀 0.498 에서 0.742. 움직인 문서는 결함이 지목한 그 11건뿐이고 나머지는 소수 셋째 자리까지 그대로였습니다. 측정 없이는 몰랐을 결함이고, 골든셋을 동결해 두었기에 고친 게 다른 것을 건드리지 않았다는 것도 확인됩니다.
처방 다섯 줄
- 원본 형식을 읽습니다. HWP 와 PDF 가 둘 다 있으면 HWP.
- 형식은 확장자가 아니라 첫 바이트로 정합니다. 8건이 거짓말을 했습니다.
- 표는 한 행을 한 줄로 직렬화합니다. 라벨과 숫자가 같은 줄에 있어야 모델이 짝을 맞춥니다.
- 텍스트 레이어를 의심합니다. 글자 수가 정상이어도 NUL 같은 것이 섞여 있을 수 있습니다.
- 골든셋을 동결하고 재측정합니다. 고쳤다는 말은 전후 숫자가 있을 때만 성립합니다.
한계
- 정답이 자동 판정입니다. 1자짜리 셀과 병합 셀은 놓치고, 본문에 우연히 같은 문자열이 있으면 맞은 것으로 셉니다. 모든 추출기에 같은 잣대라 차이는 믿을 만하고 절대값은 낮춰 봐야 합니다.
- PDF 정답이 원본의 표라, PDF 로 렌더링되면서 표가 재배치된 경우 잘하는 도구도 손해를 봅니다. 그래서 “가장 잘한 도구” 열을 따로 두었습니다.
- HWP 에서 미리보아 체인과 정답 파서가 같은 라이브러리(pyhwp)를 씁니다. HWP 의 100% 는 “구조를 잃지 않고 옮겼다”는 뜻이지 “pyhwp 보다 잘 읽는다”는 뜻이 아닙니다. HWPX 는 정답 파서가 독립이라 그 제약이 없습니다.
- 코퍼스가 2026년 5월 이후 용역·경쟁입찰 첨부에 한정됩니다. 공사·물품 첨부는 표 밀도가 다를 수 있습니다.
정리
한국 공공 문서의 검색 품질은 토크나이저 이전에 추출에서 갈립니다. 글자를 잃는 실패는 눈에 보이지만, 구조를 잃는 실패는 텍스트 양이 정상이라 로그에 남지 않습니다. 원본 형식을 읽고, 형식을 바이트로 판정하고, 표를 행 단위로 옮기고, 같은 골든셋으로 다시 재는 것. 이 코퍼스에서는 그 순서로 라벨↔숫자 짝이 10% 에서 100% 가 됐습니다.
데이터 피드는 이 추출 체인을 고객이 지정한 출처에 붙여 매달 정제된 데이터를 납품하는 서비스입니다.
실험 파일
스크립트·채점 결과·문서 목록을 그대로 올립니다. 나라장터 첨부 216파일(108MB)은 공개 데이터이지만 여기 올리지 않고, 어느 공고의 어떤 파일인지 목록(manifest.json, 공고번호·파일명·크기·해시)을 올립니다. fetch_g2b.py 로 같은 공고번호의 첨부를 다시 받을 수 있습니다(입찰공고정보서비스 API 키 필요).
- truth.py — 정답 표 파서(HWPX 는 XML 직접, HWP 는 pyhwp HTML). 형식은 매직 바이트로
- extractors.py — 비교 추출기 8종
- score.py — 셀 보존·행 무결성·라벨↔숫자 짝
- report.py · paired_stats.py · compare_before_after.py — 집계·47쌍 비교·수선 전후
- fetch_g2b.py · merge_corpora.py · prepare_corpus.py — 수집·병합·채점 목록
- results.json · results_after.json — 수선 전·후 214문서 채점
- docs.txt · pairs.json · manifest.json — 채점 목록·PDF 짝·공고별 첨부 목록
- requirements.txt — pymupdf·pyhwp·pdfplumber·olefile