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

[파이썬-한/글] 자동 자간조정으로 문서 깔끔하게 만들기

by 일코 2021. 8. 14.

질문주신 내용은,

양동훈 님께서 질문 주신 내용

저도 고민해 본 적이 많은 주제입니다. 미흡하게나마 설명과 코드를 남깁니다.
먼저 예문을 만들어봅시다. 한글Lorem Ipsum 생성기에 들어가서 아래와 같이 두 개의 문단을 만들었습니다.

한글 Lorem Ipsum

국가원로자문회의의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 모든 국민의 재산권은 보장된다. 그 내용과 한계는 법률로 정한다. 대통령은 국민의 보통·평등·직접·비밀선거에 의하여 선출한다. 국민의 모든 자유와 권리는 국가안전보장·질서유지 또는 공공복리를 위하여 필요한 경우에 한하여 법률로써 제한할 수 있으며, 제한하는 경우에도 자유와 권리의 본질적인 내용을 침해할 수 없다. 모든 국민은 주거의 자유를 침해받지 아니한다. 주거에 대한 압수나 수색을 할 때에는 검사의 신청에 의하여 법관이 발부한 영장을 제시하여야 한다. 비상계엄이 선포된 때에는 법률이 정하는 바에 의하여 영장제도, 언론·출판·집회·결사의 자유, 정부나 법원의 권한에 관하여 특별한 조치를 할 수 있다.
피고인의 자백이 고문·폭행·협박·구속의 부당한 장기화 또는 기망 기타의 방법에 의하여 자의로 진술된 것이 아니라고 인정될 때 또는 정식재판에 있어서 피고인의 자백이 그에게 불리한 유일한 증거일 때에는 이를 유죄의 증거로 삼거나 이를 이유로 처벌할 수 없다. 모든 국민은 직업선택의 자유를 가진다. 국정의 중요한 사항에 관한 대통령의 자문에 응하기 위하여 국가원로로 구성되는 국가원로자문회의를 둘 수 있다. 국가는 청원에 대하여 심사할 의무를 진다. 국민경제의 발전을 위한 중요정책의 수립에 관하여 대통령의 자문에 응하기 위하여 국민경제자문회의를 둘 수 있다. 선거운동은 각급 선거관리위원회의 관리하에 법률이 정하는 범위안에서 하되, 균등한 기회가 보장되어야 한다.

(별로 중요하지 않지만) 단어가 잘려 있으면 뭔가 불편하다?

저는 이런 문서를 보면 아래와 같이 수정하고 싶은 욕구가 치솟습니다. 이건 거의 뭐... "파블로프의 회사원" 수준;

마음이 편안해진다...

파이썬으로 이런 작업을 간단하게 자동화해볼 수 있을까요?

예. 간단합니다. 우선 알고리즘은 이렇습니다.


1. 라인전체를 선택해서, 텍스트 가져오기

라인 전체 선택(MoveLineBegin&MoveSelLineEnd)


2. 가져온 텍스트의 마지막이 공백인지 확인

"모든 국민의 재산권은 보장된다. 그 내용과 한계는 법률로 정한다. 대통"


3-1. 만일 가져온 텍스트 마지막이 공백이면 : 다음줄로 넘어감.
3-2. 공백이 아닌 경우 : 잘린 단어의 긴 쪽이 현재 줄인지, 다음 줄인지 체크해서 자간을 넓히거나 줄임
잘린 단어의 위아래 글자수가 같아도 자간을 줄이는 방향으로 함(온점, 괄호 등 좁은글자도 있으므로)

위쪽이 길거나 같으면 라인 전체의 자간을 1%씩 줄임. 


3-3. 라인의 끝이 스페이스가 될 때까지 3-2를 반복한 후 다음줄로 넘어가서 1번과정 반복. 끝.

이걸 파이썬 코드로 한 번 짜보겠습니다.
미리 몇 가지 헬퍼함수를 짜 두었습니다. 메인함수를 읽기 좋게 하려는 목적입니다.
함수 이름을 한글로 적어두었으니 참고하여 주시기 바랍니다.
(전문가들은 한글로 짜지 말라는데... 하여튼 읽기는 좋습니다.)

시연화면을 먼저 보여드리겠습니다.

아래는 전체코드입니다.

위의 코드설명에서 몇 가지를 보완수정하였는데, 본문과 캡쳐화면을 고치기 어려워서
코드에 주석을 꼼꼼히 달았고, 가급적 함수명을 한글로 적어보았습니다.
가급적 실행방식이 단순한 Run 메서드가 위주이므로
분량에 부담갖지 마시고 알고리즘만 살펴보시기 바랍니다.

import win32com.client as win32


def 한글시작(visible=False, open_file=None):
    한글 = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
    if visible:
        한글.XHwpWindows.Item(0).Visible = True
    한글.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
    if open_file:
        한글.Open(open_file)
    else:
        pass
    return 한글


def 문서시작점으로():
    한글.HAction.Run("MoveDocBegin")  # 문서 시작으로 이동


def 마지막_라인():
    """
    while문으로 아래로 이동할 예정이므로
    마지막 라인인지 체크하는 함수가 필요함.
    마지막 라인이면 True를 리턴.
    """
    line_number = 한글.KeyIndicator()[5]  # 현재 라인넘버 저장
    한글.HAction.Run("MoveDown")  # 한 줄 아래로 커서이동
    if 한글.KeyIndicator()[5] == line_number:  # 라인넘버가 안바뀌었으면
        return True  # 마지막 라인이 맞음
    else:  # 바뀌었으면
        한글.HAction.Run("MoveUp")  # 다시 원래 위치로 돌아감
        return False


def 선택구간_텍스트(field_name):
    """
    필드를 임시로 생성하여 텍스트를 가져옴. 가져온 후에는 필드 삭제.
    선택구간의 텍스트를 리턴
    """
    한글.CreateField(Direction="", name=field_name)  # 필드생성
    selected_text = 한글.GetFieldText(field_name)  # 텍스트 가져오기
    한글.HAction.Run("DeleteField")  # 필드제거(커서는 맨 앞에 있음)
    return selected_text


def 걸친단어_길이():
    """
    라인끝에 걸쳐진 단어가 앞쪽이 긴지 뒷쪽이 긴지 알려주는 함수
    튜플로 잘린 글자의 길이를 각각 알려줌. 예: (2,3)
    추가로, 리턴으로 끝나는 문장 처리.
    """
    한글.HAction.Run("MoveLineBegin")
    한글.HAction.Run("MoveSelLineEnd")
    한글.HAction.Run("MoveSelRight")
    한글.HAction.Run("MoveSelRight")
    마지막단어_앞부분 = 선택구간_텍스트("마지막단어_앞부분")
    if 마지막단어_앞부분.endswith("\r\n"):
        return (len(마지막단어_앞부분[:-2].rsplit(" ", maxsplit=1)[-1]), 0)
    else:
        마지막단어_앞부분 = 마지막단어_앞부분[:-2]
        if 마지막단어_앞부분.endswith(" ") or 마지막단어_앞부분 == "\r":
            return (len(마지막단어_앞부분.strip().rsplit(" ", maxsplit=1)[-1]), 0)
        else:
            마지막단어_앞부분_길이 = len(마지막단어_앞부분.strip().rsplit(" ", maxsplit=1)[-1])
            한글.HAction.Run("MoveLineEnd")
            한글.HAction.Run("MoveSelNextWord")
            마지막단어_뒷부분 = 선택구간_텍스트("마지막단어_뒷부분")
            마지막단어_뒷부분_길이 = len(마지막단어_뒷부분.strip())
            한글.HAction.Run("Cancel")
            한글.HAction.Run("MoveUp")
            한글.HAction.Run("MoveLineBegin")
            return (마지막단어_앞부분_길이, 마지막단어_뒷부분_길이)


def 자간자동조정(걸친단어_길이튜플):
    if 걸친단어_길이튜플[0] >= 걸친단어_길이튜플[1]:
        한글.HAction.Run("MoveSelLineEnd")
        한글.Run("CharShapeSpacingDecrease")  # 자간 1%씩 줄임
    elif 걸친단어_길이튜플[0] < 걸친단어_길이튜플[1]:
        한글.HAction.Run("MoveSelLineEnd")
        한글.Run("CharShapeSpacingIncrease")  # 자간 1%씩 줄임
    else:
        pass


if __name__ == '__main__':
    한글 = 한글시작(visible=True, open_file=r"C:\Users\smj02\PycharmProjects\hwptest\20210814_자간자동조정\자간줄이기예문.hwp")
    문서시작점으로()
    while not 마지막_라인():
        while True:
            걸친단어_길이튜플 = 걸친단어_길이()
            if 걸친단어_길이튜플[1] == 0:
                한글.HAction.Run("MoveDown")
                break
            else:
                자간자동조정(걸친단어_길이튜플)


스페이스 없이 한 글자가 두 줄을 넘어간다든지 하는 극단적인 경우 빼고는 대체로 잘 돌아가는 것 같네요.

마치며

위의 프로그램은 선택영역을 파이썬으로 가져올 때 CreateField 메서드를, 이동할 때는 MovePos 메서드와 Run("MoveSel~") 메서드를 주로 사용했습니다. 이 방법은 시간이 다소 걸리기도 하고, 여러 가지 예외사항을 예측하고 처리하느라 코드도 상당히 길어졌습니다.

이를 보완하기 위한 방법으로 Field 대신 GetText를, MovePos 대신 SetPos 메서드를 사용하면 위의 문제점을 다소간 보완할 수 있습니다. 이 방법은 이후의 포스팅에서 한 번 더 다루겠습니다.
참고로, Run 메서드에 대한 설명은 블로그에 현재 작성중이므로,
이와 비슷한 방식으로 코드를 짜시는 경우에 참고하시기 바랍니다.

 

hwp의 Run메서드 전체목록 및 시연화면

아래아한글 자동화는 동일한 업무를 가지고도 여러 가지 방식의 코드로 작성이 가능합니다. 이 페이지에서는 가장 간단한 Run("액션") 방식의 커맨드 목록과, 이해를 돕기 위한 커맨드별 시연화

martinii.fun

양동훈님, 버그 발견해 주셔서 감사합니다.
코드와 시연화면 수정해놓았습니다.
남들이 이해하기 쉽게 코드를 짜고 싶은데 쉽지 않네요ㅜ

댓글