파이썬으로 한글(HWP) 자동화 — 텍스트·표 추출, PDF 변환, 보안 팝업 피하기 (한글 2024 실측)
관공서 공문, 학교 양식, 계약서까지 한국에서는 여전히 HWP 가 기본입니다. 그러다 보면 수백 개 파일에서 글자나 표를 뽑아야 하거나 한꺼번에 PDF 로 바꿔야 하는 날이 옵니다. 이 글은 그 세 가지를 파이썬으로 하는 방법을, 한컴오피스 2024(Hwp.exe 13.0.0.3380)가 깔린 윈도우 11 PC 에서 실제로 돌린 결과와 함께 정리했습니다. 어떤 호출이 됐는지, 무인 스크립트를 멈춰 세우는 보안 확인창, 공식 해결책이 이 버전에서 듣지 않았던 이유, 그리고 모든 시험에서 확인창을 없앤 우회법까지 담았습니다. 한글이 없어도 .hwpx·.hwp 를 읽는 방법도 함께 다룹니다.
세 가지 방법과 각각 할 수 있는 것
다룰 파일 형식은 두 가지입니다. .hwp 는 예전부터 쓰던 바이너리 형식으로, 옛 .doc 파일과 같은 OLE 복합 파일 구조입니다. .hwpx 는 새 형식으로, .docx 처럼 XML 파일들을 ZIP 으로 묶은 것입니다. 한글은 둘 다 저장할 수 있습니다. 파이썬으로 무엇을 할 수 있는지는 가진 파일이 어느 쪽인지, 그리고 스크립트를 돌릴 PC 에 한글이 깔려 있는지에 달려 있습니다.
| 방법 | 필요한 것 | 텍스트 | 표 | |
|---|---|---|---|---|
| 한글 + pywin32 | 윈도우, 한글, pip install pywin32 | 됨 | 됨 (행·열 그대로) | 됨 |
| .hwpx + 표준 라이브러리 | 파이썬만 | 됨 | 됨 (행·열 그대로) | 안 됨 |
| .hwp + olefile | pip install olefile | 됨 | 칸마다 한 줄 | 안 됨 |
아래 모든 시험 환경: 윈도우 11, 한컴오피스 2024(Hwp.exe 13.0.0.3380, 32비트 프로그램), 파이썬 3.11, pywin32 312, olefile 0.47. 한컴 개발자 사이트에 따르면 한글 오토메이션은 개인의 비상업적 이용에 한해 무료이고, 상업용 제품에 쓰려면 한컴에서 별도 라이선스를 받아야 합니다.
파이썬에서 한글 띄우기
한글은 HWPFrame.HwpObject 라는 COM 개체를 등록해 둡니다. pip install pywin32 를 하면 다른 COM 프로그램처럼 띄울 수 있습니다. 이 PC 에서는 뜨는 데 2.3~2.8초가 걸렸고, 스크립트가 따로 켜지 않으면 창은 숨겨진 채(Visible 이 False)였습니다.
import win32com.client as win32
hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
print(hwp.Version) # 시험 PC 에서는 13, 0, 0, 3380
print(hwp.XHwpWindows.Item(0).Visible) # False — 숨겨진 채로 돈다
hwp.Quit()아래에 나오는 메서드 이름은 한글과 함께 설치되는 형식 라이브러리(HwpAutomation.tlb)와 한컴 API 매뉴얼(HwpAutomation, 2025년 4월판)에서 그대로 확인했습니다. Open(filename, Format, arg), SaveAs(Path, Format, arg), GetTextFile(Format, option), Clear(option), Quit() 입니다.
스크립트를 멈추게 하는 확인창
스크립트가 일반 폴더의 파일을 처음 열거나 저장하자 한글이 이런 창을 띄우고 기다렸습니다.
D:\WebSite\GUI_Tools\hwp_automation_test\sample.hwp
한글을 이용하여 위 파일에 접근하려는 시도(파일의 손상 또는 유출의 위험 등)가 있습니다.
정상적인 작업 과정에만 접근을 허용하십시오.
[접근 허용(Y)] [모두 허용(N)] [허용 안 함(A)] [모두 안 함(C)]사람이 단추를 누를 때까지 아무것도 진행되지 않습니다. 수백 개 파일을 혼자 돌게 하려던 스크립트라면 거기서 끝입니다. 시험 스크립트도 6초를 기다리다 강제로 종료했습니다.
같은 작업을 사용자 임시 폴더(%TEMP%)에서 하면 확인창이 한 번도 뜨지 않았습니다. HWP 로 저장 0.33초, PDF 로 저장 0.32초, 다시 열기 0.02초, 창은 아예 없었습니다. 한컴이 함께 배포하는 예제 보안 모듈의 소스에도 임시 폴더를 허용하는 IsTempPath 규칙이 들어 있어 관찰한 동작과 맞습니다. 다만 한글 본체의 이 동작을 한컴이 문서로 밝히지는 않았습니다.
공식 해결책, 그리고 여기서 안 됐던 이유
한컴이 안내하는 해결책은 보안 모듈입니다. '보안모듈(Automation)' 압축 파일 안의 DLL 을 원하는 곳에 두고, 그 이름과 전체 경로를 HKEY_CURRENT_USER\Software\HNC\HwpAutomation\Modules 에 등록한 뒤, 코드에서 hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModuleExample") 로 켜는 방식입니다. 시험 PC 에서 그대로 따라 했더니 RegisterModule 이 False 를 돌려주었고 확인창도 그대로 떴습니다.
제 실수가 아닌지 여러 가지를 확인했습니다. DLL 은 Hwp.exe 와 같은 32비트이고, 32비트 파이썬에서는 문제없이 불러와집니다. 등록 이름을 FilePathCheckerModuleExample 과 강좌에서 흔히 쓰는 FilePathCheckerModule 두 가지로, 창을 숨긴 채와 보이게 한 채로 모두 시험했습니다. 스마트 앱 컨트롤은 꺼져 있었고 코드 무결성 차단 기록도 없었습니다. 그래도 모든 조합이 False 였습니다. 결정적인 건 Hwp.exe 가 불러온 모듈 목록이었습니다. RegisterModule 전 183개, 후에도 같은 183개로, 한글은 DLL 을 아예 불러오지 않았습니다. 이전 버전에서는 된다는 강좌가 많지만, 한글 2024 13.0.0.3380 에서는 되지 않았습니다.
등록하더라도 무엇을 하는지 알고 쓰세요. 같은 압축 파일에 든 소스 코드를 보면 내보내는 함수 IsAccessiblePath 의 첫 줄이 return TRUE; 입니다. 그 모듈 이름을 요청하는 내 계정의 모든 프로그램에 모든 경로를 허락한다는 뜻입니다. 내 스크립트만 도는 개인 PC 라면 괜찮지만, 보안 검사를 실제로 느슨하게 만드는 설정입니다.
텍스트와 표 꺼내기
아래 스크립트는 어느 폴더의 .hwp·.hwpx 든 받아서 임시 폴더로 복사한 뒤, 본문 텍스트와 모든 표를 행 목록으로 출력합니다. Open 에 형식을 빈 문자열로 주면 한글이 형식을 알아서 판단합니다. forceopen:true 는 읽기 전용으로 열어야 하는 파일에서 묻는 창을 띄우지 않게 합니다.
import shutil, sys, tempfile
from html.parser import HTMLParser
from pathlib import Path
import win32com.client as win32
class TableParser(HTMLParser):
"""GetTextFile("HTML") 결과에서 표를 [[행], [행], ...] 으로 모은다."""
def __init__(self):
super().__init__()
self.tables, self.row, self.cell = [], None, None
def handle_starttag(self, tag, attrs):
if tag == "table":
self.tables.append([])
elif tag == "tr":
self.row = []
elif tag in ("td", "th"):
self.cell = []
def handle_endtag(self, tag):
if tag in ("td", "th") and self.cell is not None:
self.row.append("".join(self.cell).strip())
self.cell = None
elif tag == "tr" and self.row is not None:
self.tables[-1].append(self.row)
self.row = None
def handle_data(self, data):
if self.cell is not None:
self.cell.append(data)
src = Path(sys.argv[1])
work = Path(tempfile.gettempdir()) / "hwp_work"
work.mkdir(exist_ok=True)
tmp = work / src.name
shutil.copy2(src, tmp) # 한글이 임시 폴더 안의 복사본만 열게 한다
hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.Open(str(tmp), "", "forceopen:true") # 형식 "" = 자동 인식 (.hwp/.hwpx 모두)
text = hwp.GetTextFile("UNICODE", "")
html = hwp.GetTextFile("HTML", "")
hwp.Clear(1)
hwp.Quit()
tmp.unlink()
print(text)
parser = TableParser()
parser.feed(html)
for i, table in enumerate(parser.tables, 1):
print(f"표 {i}:", table)제목, 3×3 점검표, 서명 한 줄로 된 시험 문서에서 일반 텍스트는 표의 칸마다 한 줄씩(장비, 상태, 점검일, 그다음 행의 칸들) 나왔습니다. HTML 쪽은 표를 구조째 돌려줬습니다. [['장비', '상태', '점검일'], ['ESP32 보드', '정상', '10/01'], ['USB 허브', '교체 필요', '10/02']]. D 드라이브의 일반 폴더에 있는 파일로 돌렸는데 확인창 없이 끝났습니다.
TEXT 말고 UNICODE 를 쓰세요. 한컴 매뉴얼에 따르면 TEXT 는 한자·옛한글·특수문자처럼 유니코드에만 있는 글자를 모두 잃습니다. 매뉴얼은 또 GetTextFile 이 문서를 메모리에서 3~4번 복사해 느리다고 적고 있습니다. 아주 큰 파일이라면 SaveAs 로 디스크에 저장하는 쪽을 권합니다.
폴더째 PDF 로 바꾸기
같은 방식을 반복문으로 돌립니다. .hwp 를 하나씩 임시 폴더로 복사해 열고, 그 자리에서 PDF 로 저장한 뒤, PDF 를 원본 옆으로 옮깁니다. versionwarning:false 는 상위 버전에서 만든 문서를 열 때 뜨는 '상위 버전에서 작성한 문서입니다' 메시지를 막습니다.
import shutil, sys, tempfile, time
from pathlib import Path
import win32com.client as win32
src_dir = Path(sys.argv[1]) # .hwp 가 있는 폴더
work = Path(tempfile.gettempdir()) / "hwp_work" # 한글은 임시 폴더 안에서만 다루게 한다
work.mkdir(exist_ok=True)
hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
t0 = time.perf_counter()
done = 0
for src in sorted(src_dir.glob("*.hwp")):
tmp_in = work / src.name
tmp_out = tmp_in.with_suffix(".pdf")
shutil.copy2(src, tmp_in)
if hwp.Open(str(tmp_in), "HWP", "forceopen:true;versionwarning:false"):
if hwp.SaveAs(str(tmp_out), "PDF", ""):
shutil.move(tmp_out, src.with_suffix(".pdf"))
done += 1
hwp.Clear(1) # 저장하지 않고 닫기
tmp_in.unlink(missing_ok=True)
hwp.Quit()
print(f"{done}개 변환, {time.perf_counter() - t0:.1f}초")D 드라이브 폴더의 한 쪽짜리 문서 10개가 2.4초에 변환됐고(한글이 뜨는 2~3초는 빼고), 결과는 모두 %PDF-1.6 으로 시작했습니다. 같은 SaveAs 에 "HWPX" 를 주면 옛 .hwp 를 .hwpx 로 바꿀 수 있습니다(둘 다 시험에서 됐습니다). 뒤에 나오는 한글 없이 읽는 방법을 쓰려면 이렇게 한 번 바꿔 두면 편합니다.
한글 없이 — .hwpx 는 표준 라이브러리로
.hwpx 파일은 ZIP 압축 파일입니다. 본문은 Contents/section0.xml(구역이 더 있으면 section1.xml …)에 있고, 글자는 hp:t 요소에, 표는 hp:tbl > hp:tr > hp:tc 구조에 들어 있습니다. 파이썬의 zipfile 과 xml.etree 만으로 충분해서 리눅스·맥에서도 돌아갑니다.
import sys, zipfile
import xml.etree.ElementTree as ET
HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
def cell_text(tc):
return "".join(t.text or "" for t in tc.iter(HP + "t"))
with zipfile.ZipFile(sys.argv[1]) as z:
for name in sorted(n for n in z.namelist() if n.startswith("Contents/section")):
root = ET.fromstring(z.read(name))
for p in root.findall(HP + "p"): # 본문 문단 (표 안 문단은 빼고)
line = "".join(t.text or "" for run in p.findall(HP + "run") for t in run.findall(HP + "t"))
if line:
print(line)
for tbl in p.iter(HP + "tbl"): # 이 문단에 붙은 표는 행·열 그대로
print("표:", [[cell_text(tc) for tc in tr.findall(HP + "tc")] for tr in tbl.findall(HP + "tr")])같은 시험 문서에서 제목, 3칸짜리 행 3개로 된 표, 서명 줄이 문서 순서대로 나왔습니다.
한글 없이 — .hwp 는 olefile 로
바이너리 형식은 손이 더 갑니다. OLE 복합 파일 안에 이름 붙은 스트림들이 있고, 본문은 BodyText/Section0(구역마다 하나)에 zlib 로 압축된 레코드의 연속으로 들어 있습니다. 레코드마다 4바이트 머리에 태그·중첩 수준·크기가 함께 들어 있고, 문단 글자는 태그 67 레코드에 있습니다.
import struct, sys, zlib
import olefile
# 문단 글자 안에서 16바이트(8글자 분량)를 차지하는 제어 문자 — 표·그림 같은 개체 자리
WIDE = {1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}
def records(data):
i = 0
while i < len(data):
head = struct.unpack_from("<I", data, i)[0]
i += 4
tag, level, size = head & 0x3FF, (head >> 10) & 0x3FF, head >> 20
if size == 0xFFF: # 길이가 크면 다음 4바이트에 들어 있다
size = struct.unpack_from("<I", data, i)[0]
i += 4
yield tag, level, data[i:i + size]
i += size
def para_text(raw):
out, i = [], 0
while i < len(raw):
c = struct.unpack_from("<H", raw, i)[0]
if c in WIDE:
i += 16
continue
if c == 9:
out.append("\t")
elif c >= 32:
out.append(chr(c))
i += 2
return "".join(out)
ole = olefile.OleFileIO(sys.argv[1])
compressed = ole.openstream("FileHeader").read()[36] & 1
for entry in sorted(e for e in ole.listdir() if e[0] == "BodyText"):
data = ole.openstream(entry).read()
if compressed:
data = zlib.decompress(data, -15)
for tag, level, body in records(data):
if tag == 67: # HWPTAG_PARA_TEXT — 문단 글자
print(para_text(body))시험 파일의 모든 문단이 나왔고, 표는 칸마다 한 줄씩 나왔습니다(칸 안 문단은 중첩 수준 3, 본문 문단은 1). 행을 다시 짜 맞추려면 표 레코드까지 해석해야 하는데, 그럴 바엔 한글로 한 번 .hwpx 로 바꿔 두는 편이 쉽습니다.
잠깐 들여다보기만 할 거라면 지름길이 있습니다. PrvText 스트림에는 압축 없는 미리보기 글자가 들어 있고 표의 행까지 <칸><칸><칸> 으로 표시됩니다. 다만 미리보기일 뿐입니다. 12,798자짜리 시험 문서에서 앞의 1,022자, 약 8% 만 담겨 있었습니다.
이 PC 에서 안 됐던 것
이 위에 무언가를 만들기 전에 알아 둘 것이 두 가지 있습니다. 하나는 위에서 다룬 보안 모듈입니다. 다른 하나는 HAction.Run 으로 부르는 동작이 시험 PC 에서 전부 False 를 돌려준 것입니다. 커서 이동(MoveRight, MoveDocBegin), SelectAll, 문단 나누기, 표 칸 이동까지 모두 그랬습니다. PowerShell 의 COM 으로 불러도 결과가 같아서 pywin32 문제는 아닙니다. 반면 HAction.Execute 와 매개변수 세트로 실행하는 동작(글자 넣기, 표 만들기)과 MovePos 는 됐습니다.
그래서 이 글은 Run 이 필요 없는 읽기와 변환까지만 다룹니다. 양식 채우기나 문서 편집이 필요하다면 그 동작들을 내 한글 버전에서 먼저 시험해 보세요. 많은 강좌가 이 동작들에 기대는데, 한글 2024 13.0.0.3380 에서는 확인하지 못했습니다.
자주 묻는 질문
Q. 맥이나 리눅스에서도 되나요?
한글 없이 읽는 두 방법은 됩니다. .hwpx 용 zipfile 과 .hwp 용 olefile 은 순수 파이썬입니다. 한글 자체를 조종하는 쪽은 COM 을 쓰므로 윈도우에서만 됩니다.
Q. pyhwpx 를 꼭 써야 하나요?
아닙니다. pyhwpx 는 같은 HWPFrame.HwpObject COM 개체를 편하게 감싼 라이브러리이고, 이 글의 코드는 pywin32 로 직접 부릅니다. 이 글을 쓰면서 pyhwpx 는 시험하지 않았습니다.
Q. 스크립트가 오류도 없이 멈춰 있습니다.
거의 확실히 파일 접근 확인창입니다. 한글이 숨겨진 채로 돌기 때문에 스크립트가 기다리는 동안 화면에 창만 떠 있을 수 있습니다. 위처럼 임시 폴더의 복사본으로 작업하세요.
Q. 회사 제품에 넣어도 되나요?
한컴 개발자 사이트는 한글 오토메이션을 개인의 비상업적 이용에 한해 무료로 두고, 상업적으로 쓰려면 한컴의 라이선스를 받도록 하고 있습니다. 한글 없이 파일 형식을 직접 읽는 방법은 그 오토메이션을 쓰지 않습니다.
Q. HWP 와 HWPX 중 어느 쪽으로 받아야 하나요?
고를 수 있다면 HWPX 입니다. XML 을 묶은 ZIP 이라 어떤 언어로든 읽을 수 있고, 표도 별도 해석 없이 구조째 나옵니다.