본문 바로가기
아래아한글 자동화/python+hwp 중급

[HDMI] 진짜 본격적으로 header.xml 파헤쳐보기

by 일코 2022. 12. 7.

자, 이제 모든 준비가 끝났...다고는 말할 수 없지만

본격적으로 header.xml을 분석해보겠습니다.

왜 아직 모든 준비가 끝나지 않았냐면,

한/글의 xml 파일들은 "네임스페이스"라는 일종의 축약어를 사용하는데,

예를 들면, header.xml의 시작부분은 아래와 같습니다.

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<hh:head xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"
         xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core" xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head"
         xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" version="1.4" secCnt="1">
    <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
    <hh:refList>
        <hh:fontfaces itemCnt="7">
            <hh:fontface lang="HANGUL" fontCnt="2">

뾰족괄호와 콜론(:) 사이의 "hh"가 네임스페이스인데요.

head 태그 안에 보면 xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head" 라는 문구가 있습니다.

xmlns 는 xmlNameSpace의 약자로, 이 문서 내 <hh:태그> 는 모두

실제 태그이름이 <{http://www.hancom.co.kr/hwpml/2011/head}태그> 로 치환됩니다.

iter() 메서드나 findall() 메서드 이용시 "hh:태그"로는 탐색이 되지 않기 때문에

1. 직접 저렇게 길게 입력하거나

2. 파이썬의 문자열 변수로 네임스페이스를 저장해놓고 f스트링 등으로 치환하거나

하는 여러 가지 방법이 있겠지만,

가장 우아하다고 생각되는 방법을 하나 더 소개해드리겠습니다.

3. 모든 네임스페이스를 아래 코드를 통해 ns라는 딕셔너리 변수에 저장합니다.

header = "./header.xml"

ns = dict([i for _, i in ET.iterparse(header, events=['start-ns'])])

그리고, 탐색은 아래와 같이 하면 됩니다.

아래는, 5번째 줄의 beginNum 태그의 속성들을 딕셔너리로 가져오는 코드입니다.

>>> root = ET.parse(header).getroot()
>>> root.find("hh:beginNum", ns).attrib

{'page': '1',
 'footnote': '1',
 'endnote': '1',
 'pic': '1',
 'tbl': '1',
 'equation': '1'}

 

아직 좀 막연하게 느껴지시겠지만,

빠르게 본론으로 들어가보겠습니다.

header.xml 파일의 910번 라인부터 보면 "빈 문서1.hwp"의 스타일 정보가 들어 있습니다.

총 22개의 스타일이 정의되어 있네요.

<hh:styles itemCnt="22">
    <hh:style id="0" type="PARA" name="바탕글" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0"
              langID="1042" lockForm="0"/>
    <hh:style id="1" type="PARA" name="본문" engName="Body" paraPrIDRef="1" charPrIDRef="0" nextStyleIDRef="1"
              langID="1042" lockForm="0"/>
    <hh:style id="2" type="PARA" name="개요 1" engName="Outline 1" paraPrIDRef="2" charPrIDRef="0"
              nextStyleIDRef="2" langID="1042" lockForm="0"/>
    <hh:style id="3" type="PARA" name="개요 2" engName="Outline 2" paraPrIDRef="3" charPrIDRef="0"
              nextStyleIDRef="3" langID="1042" lockForm="0"/>
    <hh:style id="4" type="PARA" name="개요 3" engName="Outline 3" paraPrIDRef="4" charPrIDRef="0"
              nextStyleIDRef="4" langID="1042" lockForm="0"/>
    <hh:style id="5" type="PARA" name="개요 4" engName="Outline 4" paraPrIDRef="5" charPrIDRef="0"
              nextStyleIDRef="5" langID="1042" lockForm="0"/>
    <hh:style id="6" type="PARA" name="개요 5" engName="Outline 5" paraPrIDRef="6" charPrIDRef="0"
              nextStyleIDRef="6" langID="1042" lockForm="0"/>
    <hh:style id="7" type="PARA" name="개요 6" engName="Outline 6" paraPrIDRef="7" charPrIDRef="0"
              nextStyleIDRef="7" langID="1042" lockForm="0"/>
    <hh:style id="8" type="PARA" name="개요 7" engName="Outline 7" paraPrIDRef="8" charPrIDRef="0"
              nextStyleIDRef="8" langID="1042" lockForm="0"/>
    <hh:style id="9" type="PARA" name="개요 8" engName="Outline 8" paraPrIDRef="18" charPrIDRef="0"
              nextStyleIDRef="9" langID="1042" lockForm="0"/>
    <hh:style id="10" type="PARA" name="개요 9" engName="Outline 9" paraPrIDRef="16" charPrIDRef="0"
              nextStyleIDRef="10" langID="1042" lockForm="0"/>
    <hh:style id="11" type="PARA" name="개요 10" engName="Outline 10" paraPrIDRef="17" charPrIDRef="0"
              nextStyleIDRef="11" langID="1042" lockForm="0"/>
    <hh:style id="12" type="CHAR" name="쪽 번호" engName="Page Number" paraPrIDRef="0" charPrIDRef="1"
              nextStyleIDRef="0" langID="1042" lockForm="0"/>
    <hh:style id="13" type="PARA" name="머리말" engName="Header" paraPrIDRef="9" charPrIDRef="2"
              nextStyleIDRef="13" langID="1042" lockForm="0"/>
    <hh:style id="14" type="PARA" name="각주" engName="Footnote" paraPrIDRef="10" charPrIDRef="3"
              nextStyleIDRef="14" langID="1042" lockForm="0"/>
    <hh:style id="15" type="PARA" name="미주" engName="Endnote" paraPrIDRef="10" charPrIDRef="3"
              nextStyleIDRef="15" langID="1042" lockForm="0"/>
    <hh:style id="16" type="PARA" name="메모" engName="Memo" paraPrIDRef="11" charPrIDRef="4" nextStyleIDRef="16"
              langID="1042" lockForm="0"/>
    <hh:style id="17" type="PARA" name="차례 제목" engName="TOC Heading" paraPrIDRef="12" charPrIDRef="5"
              nextStyleIDRef="17" langID="1042" lockForm="0"/>
    <hh:style id="18" type="PARA" name="차례 1" engName="TOC 1" paraPrIDRef="13" charPrIDRef="6"
              nextStyleIDRef="18" langID="1042" lockForm="0"/>
    <hh:style id="19" type="PARA" name="차례 2" engName="TOC 2" paraPrIDRef="14" charPrIDRef="6"
              nextStyleIDRef="19" langID="1042" lockForm="0"/>
    <hh:style id="20" type="PARA" name="차례 3" engName="TOC 3" paraPrIDRef="15" charPrIDRef="6"
              nextStyleIDRef="20" langID="1042" lockForm="0"/>
    <hh:style id="21" type="PARA" name="캡션" engName="Caption" paraPrIDRef="19" charPrIDRef="0"
              nextStyleIDRef="21" langID="1042" lockForm="0"/>
</hh:styles>

 

스타일 정보를 iter 메서드로 가져오려면 다음과 같이 입력하면 됩니다.

>>> list(root.iter(f"{{{ns['hh']}}}style"))  # iter메서드에는 네임스페이스 파라미터가 없음

[<Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EA20>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EA70>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EAC0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EB10>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EB60>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EBB0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EC00>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EC50>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5ECA0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5ECF0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5ED40>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5ED90>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EDE0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EE30>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EE80>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EED0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EF20>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EF70>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5EFC0>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5F010>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5F060>,
 <Element '{http://www.hancom.co.kr/hwpml/2011/head}style' at 0x0000029C9EC5F0B0>]

 

이 요소들의 속성을 담은 리스트를 만들려면 아래처럼 리스트컴프리헨션을 이용하면 간편합니다.

>>> styles = [i.attrib for i in root.iter(f"{{{ns['hh']}}}style")]
>>> print(styles)

[{'id': '0',
  'type': 'PARA',
  'name': '바탕글',
  'engName': 'Normal',
  'paraPrIDRef': '0',
  'charPrIDRef': '0',
  'nextStyleIDRef': '0',
  'langID': '1042',
  'lockForm': '0'},
 {'id': '1',
  'type': 'PARA',
  'name': '본문',
  'engName': 'Body',
  'paraPrIDRef': '1',
  'charPrIDRef': '0',
  'nextStyleIDRef': '1',
  'langID': '1042',
  'lockForm': '0'},
 {'id': '2',
  'type': 'PARA',
  'name': '개요 1',
  'engName': 'Outline 1',
  'paraPrIDRef': '2',
  'charPrIDRef': '0',
  'nextStyleIDRef': '2',
  'langID': '1042',
  'lockForm': '0'},
 {'id': '3',
  'type': 'PARA',
  'name': '개요 2',
  'engName': 'Outline 2',
  'paraPrIDRef': '3',
  'charPrIDRef': '0',
  'nextStyleIDRef': '3',
  'langID': '1042',
  'lockForm': '0'},
  ...]

 

우리의 최종 목적은 "스타일 리스트를 엑셀에 담기"였습니다.

직접 엑셀에 한줄한줄 대입해서 완성해도 되지만, 

데이터분석 분야에서 가장 사랑받는 파이썬의 pandas 모듈을 사용하면

임포트 포함해서 단 세 줄이면 엑셀파일이 뚝딱 만들어집니다.

import pandas as pd

df = pd.DataFrame(styles)
df.to_excel("styles.xlsx")

 

엑셀파일을 열어볼까요?

짜잔!

이제 스타일 이름을 모두 불러왔습니다.

(B열의 id칼럼이 스타일 순서입니다.)

스타일 이름을 파악했으면 이제 어떤 작업이 가능해질까요?

스타일 이름이나 순서가 동일하면 템플릿을 적용하기 쉬운데,

취합한 문서 중에 스타일 이름을 "본문" 에서 "# 본문"으로 바꾼다든지,

스타일 순서가 뒤죽박죽이 돼서 스타일 인덱스로는 문서를 교정할 수 없다든지 하는 경우에,

파이썬에 내장된 difflib의 SequenceMatcher를 이용하면

아주 간편하게 가장 유사한 문자열끼리 매칭할 수 있습니다.

비슷한 스타일 이름끼리 매칭해서 바뀔 문서의 스타일들이 템플릿 문서의 어느 스타일로 붙어야 하는지

자동으로 파악할 수 있다는 거죠.

그럼 다음 포스팅에서는 difflib의 간단한 사용법과 실전예제를 다뤄보겠습니다.

 

댓글