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

[QnA] 교정부호 시작/끝 위치를 파악해서 엑셀로 저장하는 프로그램

by 일코 2022. 11. 8.
안녕하세요.

저는 한글 문서 분석을 공부하고 있는데 다음과 같은 기능이 필요합니다.
* 설정 영역의 시작과 끝의 위치 정보(교정 부호나 메모 등)
예를 들어 교정 부호 사용시 시작 부분은 찾을 수 있으나 끝 부분은 찾을 수가 없네요.
확인 부탁합니다.

이상입니다.

질문 주신 분이 이런 기능을 어떻게 활용하실지는 잘 모르겠습니다.

그래도, 어떤 문제를 겪고 계신지는 공감이 됩니다.

왜냐면 한/글 문서 내에서 교정부호를 비롯한 다수의 컨트롤이 

시작위치는 ctrl.GetAnchorPos(1)로 파악이 가능한 반면

끝위치는 도무지 찾을 방법이 없기 때문입니다.

하지만, (다소 조잡하긴 하지만) 몇 가지 메서드를 조합하면

이런 기능도 직접 구현이 가능한데요.

아래 문서를 예제로 사용하겠습니다.

튜토리얼을 따라해 보고 싶은 분은 다운받아서 활용해 주시기 바랍니다.

예제파일

교정부호.hwp
0.04MB

한글 Lorem Ipsum (간세네) (guny.kr)에서 두 문단만 생성한 후

임의로 교정부호를 붙여보았습니다.

위 문서를 조판보기(Ctrl-GC)로 변환해보면 아래와 같습니다.

일부러 극단적인 케이스를 몇 개 집어넣었는데,

교정부호 안에 다른 교정부호가 있는 경우나, 교정부호 범위 일부가 겹친다든지 하는

까다로운 경우도 포함해서 예제를 만들어보았습니다.

아래 프로그램을 실행하면 다음과 같은 엑셀파일이 생성됩니다.

 

완성된 코드를 먼저 보여드리겠습니다.

from pathlib import Path  # 경로 입력시 간편하게 하기 위한 모듈

import win32com.client as win32  # 한/글 및 엑셀을 열기 위한 모듈
import pandas as pd  # 데이터프레임 클래스를 사용하기 위한 판다스 모듈


def start_hwp(visible=True):
    """한/글 시작하는 함수"""
    hwp = win32.gencache.EnsureDispatch("hwpframe.hwpobject")
    hwp.XHwpWindows.Item(0).Visible = visible
    hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
    hwp.Run("FileOpen")  # 파일선택 팝업창을 열어서 추출할 한/글파일을 선택합니다.
    return hwp


def start_excel(filename):
    """엑셀 시작하는 함수"""
    excel = win32.gencache.EnsureDispatch("Excel.Application")
    excel.Visible = True
    wb = excel.Workbooks.Open(filename)
    ws = wb.Worksheets(1)


def get_rev_pos():
    """{인덱스: (컨트롤명, 시작위치, 끝위치)} 딕셔너리를 생성하는 함수"""
    init_pos = hwp.GetPos()  # 최초위치 저장
    rev_dict = {}  # 리턴할 딕셔너리
    ctrl = hwp.HeadCtrl  # 최초컨트롤부터 순회하면서
    idx = 0  # idx를 딕셔너리 키로 사용할 예정
    while ctrl:  # ctrl이 None을 반환하기 전까지 쭈욱
        desc = ctrl.UserDesc  # 컨트롤명
        if desc not in ["구역 정의", "단 정의"]:
            pos_set = ctrl.GetAnchorPos(0)
            hwp.SetPosBySet(pos_set)  # 해당 컨트롤 위치로 이동
            rev_dict[idx] = [desc, hwp.GetPos()]  # rev_dict에 컨트롤 이름과 위치 저장
            idx += 1
        ctrl = ctrl.Next  # 다음 컨트롤로 이동
    hwp.SetPos(*init_pos)  # 순회 마치면 다시 최초위치로 이동
    return rev_dict


def get_rev_end():
    """컨트롤의 끝위치를 조회하는 함수. 
    어느 컨트롤의 끝위치인지는 아직 알 수 없으므로
    별도의 탐색 프로세스를 통해 매칭을 해야 함"""
    init_pos = hwp.GetPos()  # 시작위치 저장
    field_ends = []  # 리턴할 리스트
    hwp.Run("MoveDocEnd")  # 문서 끝으로 이동한 후
    while hwp.GetPos() != (0, 0, 16):  # 문서 시작점에 도착할 때까지
        if hwp.KeyIndicator()[-1] == "필드":  # 교정부호 끝 컨트롤이 나오면
            field_ends.append([hwp.GetPos(), hwp.KeyIndicator()[3:7]])  # 위치정보 저장
        hwp.Run("MovePrevChar")  # 앞 글자로 이동
    hwp.SetPos(*init_pos)  # 순회를 마치면 시작위치로 재이동
    return list(reversed(field_ends))


if __name__ == '__main__':
    hwp = start_hwp()  # 한/글 시작
    hwp.SetMessageBoxMode(0x10)  # 컨트롤 삭제시 [지움/취소] 팝업에서 [지움] 자동클릭
    rev_pos = get_rev_pos()  # 엑셀파일로 기록할 딕셔너리
    rev_end_list = [i[1] for i in get_rev_end()]  # 화면 하단 KeyIndicator() 상의 위치정보
    rev_end_pos = [i[0] for i in get_rev_end()]  # (List, Para, Pos) 위치정보

    for i, j in rev_pos.items():
        hwp.SetPos(*j[1])  # 해당 교정부호로 이동한 후
        hwp.Run("Delete")  # 교정부호를 삭제해보고
        rev_end_modified = set([i[1] for i in get_rev_end()])  # 차집합 계산을 위해 set로 변환
        rev_end = list(set(rev_end_list) - rev_end_modified)[0]  # 끝리스트 중 방금 삭제된 위치 찾아서
        idx = rev_end_list.index(rev_end)  # 삭제된 끝위치 컨트롤의 인덱스 저장
        rev_pos[i].append(rev_end_pos[idx])  # 인덱스에 해당하는 컨트롤 끝위치를 rev_pos에 추가
        hwp.Run("Undo")  # 삭제했던 컨트롤은 되살려놓기

    hwp.SetMessageBoxMode(0xf0)  # 대화상자 자동클릭 해제
    df = pd.DataFrame(rev_pos).transpose()  # 데이터프레임 생성
    df.columns = ["부호명", "시작위치", "끝위치"]  # 칼럼명 추가
    xlsx_path = Path(hwp.XHwpDocuments.Item(0).Path).absolute() / "rev_loc.xlsx"
    df.to_excel(xlsx_path)  # 엑셀파일로 저장
    start_excel(xlsx_path)  # 엑셀에서 해당파일 열기
코드 설명은 나름 꼼꼼하게 주석으로 달아보았습니다.
한 가지 추가로 설명드리면,
get_rev_end 함수 안에서 두 가지 위치정보를 사용합니다.
첫 번째는 pos 좌표로, hwp.GetPos()로 얻는 (List, Para, pos) 튜플이고
두 번째는 KeyIndicator 좌표로, 한/글 프로그램 하단에 표시되는 [Page, Line, Offset] 리스트입니다.
pos 좌표는 조판부호(컨트롤) 길이가 다양한 길이(8~)로 반영되기 때문에
컨트롤을 추가/삭제하는 경우 영향을 받는 범위의 좌표가 흐트러지는 반면
KeyIndicator 좌표는 조판부호와 무관하게 문자열 길이만 고려하는 방식이므로 
컨트롤이 추가/삭제되더라도 해당 문단의 좌표가 바뀌지 않는다는 장점이 있습니다.

핵심원리를 간략히 설명드리면 

먼저 모든 교정부호의 시작위치와 끝위치를 따로 저장한 후에,

(어차피 출현 순서로는 제대로 매칭이 되지 않으니까요. 교정부호 안에 교정부호가 있을 수도 있고...)

컨트롤을 하나씩 삭제해보면서, 함께 삭제되는 "끝위치" 조판부호 위치를 일일이 찾아서 1:1 매칭을 합니다.

그리고 완성된 dict는 판다스 데이터프레임을 통해 최종적으로 엑셀파일로 저장하는 프로그램입니다.

한 번 실행해볼까요?ㅎ

잘 실행되네요.

끝위치 컨트롤을 탐색할 때, 한 글자씩 일일이 훑어보느라 다소 시간이 걸리기는 합니다.

완성된 코드를 pyinstaller로 컴파일해보았는데 용량이 제법 크네요. 120메가 정도...

파이썬이 설치되지 않은 PC에서도 다운받아서 테스트해보실 수 있습니다.

교정부호시작끝위치.zip
19.14MB
교정부호시작끝위치.z01
19.53MB
교정부호시작끝위치.z02
19.53MB
교정부호시작끝위치.z03
19.53MB
교정부호시작끝위치.z04
19.53MB
교정부호시작끝위치.z05
19.53MB

 

코드 중 이해가 잘 되지 않는 부분이 있으면 자유롭게 댓글로 문의 주시기 바랍니다.

행복한 하루 되세요!

앞으로도 도움이 되는 콘텐츠를 만들어 가겠습니다.

 

댓글