본문 바로가기
업무자동화/파이썬-아래아한글 자동화 기초

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

by Martinii의 회사원코딩 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

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

댓글19

  • 양동훈 2021.08.15 12:26

    한 개의 문장에서는 아주 잘 되는데
    첫번째 문장 마지막에(두번째 문장 시작전에) 계속 반복되는 현상이 생기네요...
    Enter나 Shift-Enter 눌러서 문단을 나누는 걸 체크해야 할거 같은데... 어렵네요 ...
    답글

  • 사용하시는 버전이 2020 맞으시지요?ㅎ 줄바꿈 여러개 넣은 케이스는 테스트를 했거든요.. 다시 한 번 검토해보겠습니다. 엔터나 쉬프트엔터 고려하는 것도 간단한 편이어서요^^
    답글

    • 양동훈 2021.08.15 13:45

      한글 2020 설치했었는데.. 파이썬으로 불러오기가 잘 안되서. 2014를 다시 깔아서 지금 파이썬으로 오픈하면 한글 2014로 실행되고 있는 중입니다.
      엔터, 쉬프트 엔터를 검색을 뭘로 하는지도 잘 모르겠어서... 정말 혼자하기에는 하늘의 뜬구름 잡는 기분이라 막연하네요 ㅎㅎㅎ
      마티니님이 조언이 정말 파이썬 공부해나가는데 너무 큰 힘이 되고 있습니다. 감사합니다.

  • 양동훈 2021.08.16 23:57

    코드가 한줄로만 보여요 ..ㅎㅎㅎ

    시연동영상만 봤을때는 표까지. 다 작동이 잘되는거 같네요..
    아래한글 관련해서 코딩할때 어떤 명령, 어떤 함수를 어떻게 써야 하는지를 정말.. 감이 안와서 어렵네요..

    저도 지금 저 나름대로 코딩해보고 있는데.. 들여쓰기, 표, 이런 것들은 안되고
    그냥 한줄씩 커서 내리면서 하고 있는데도. 아직 버그가 있네요 ㅎㅎㅎ

    들여쓰기만이라도 어떻게 한번 해볼라고 하는데
    문단모양 체크해서 하면 될거 같긴한데 막연하네요..
    답글

    • 한줄로 보이는 부분 수정했어요!!!

      모바일에서 본문 일부를 수정하고 저장했는데, 코드가 한 줄로 깨져버리네요.. 모바일에서는 글수정을 하면 안되겠어요ㅜㅜㅜ

  • 양동훈 2021.08.16 23:59

    아 그리고. 웹에 코드 올릴때 무슨 방법이 있나요?
    지금 이런 댓글로는 들여쓰기 같은게 제대로 정리 안되서 올라가서 알아보기 힘든데..
    검색하다 보면 본문이나 댓글에도 코딩화면처럼 프레임으로 코드가 올라가는거 보면.. 신기해서요 ㅎㅎ
    답글

    • 저는 본문에 highlight.js나 프리즘(현재 사용중) 같은 자바스크립트 라이브러리를 사용해서 코드 하이라이팅을 합니다. 다만 댓글이나 방명록에는 적용이 되지 않아요ㅜ 제가 알기론 댓글이나 방명록에 들여쓰기까지는 적용이 돼서 코드를 어느 정도는 알아볼 수 있겠더라고요. (그래도 왠만하면 파이참에 붙여넣은 후에 리포맷팅을 해서 읽어요..)

  • 양동훈 2021.08.21 02:11

    일단 제 나름대로 만들어 보긴 했는데요 (거의 Martinii님의 코딩 배끼듯이. ㅋㅋ)
    안 배낀건 좀 무식하게 노가다로 한게 많아서.. ㅎㅎㅎㅎ
    다름이 아니라.. 자간조정은 일단 글자만 있는 문서에는 잘 돌아가는데요. 정규식 부분 관련해서
    GetText()로 검색할때

    제1조(정관) 정관은....
    제2조(목적) 목적은....
    제3조(회비) 회비는....

    이런식으로 내용이 있을때. GetText()로 검색하면
    제1조, 제2조 제3조가 한 줄 이상 띄어져 있으면 다 검색이 되는데.
    (아,,다가 아니라 1조는 검색이 안되더군요 ㅠㅠ - 테스트 하느라 1조가 첫페이지 첫번재 줄(문서 처음 시작줄)에 있으면 검색이 안되네요 -_-;)
    제1조, 제2조 제3조가 중간에 빈줄이나 다른 문단이 없이 연달아 있는 경우 한개조를 빼먹고 넘어가더라군요.. 그래서 getpos, setpos 를 이용해서 움직여 봐도 안되서요....ㅠㅠ
    이거 때문에 며칠동안 씨름하고 있는데 도저히 해결이 안되네요..(한컴 개발자 커뮤니티는 한달도 훨씬 전에 신청했는데... 아직도 대기중이네요 ㅠㅠ)

    괜찮으시면 제가 짠 코드를 검토 한번 봐주실 수 있으실까요? (GetText()로 검색하는 부분만요..)
    일부 코드만 올리기에는 다른 오류도 있을지 몰라서 전체 코드를 보내드릴까 하는데.. 이것저것 짬뽕된 코드라 1,000줄이 넘어가서....-_-;;; 메일로 보내드릴까 하는데 괜찮으신가요?
    답글

  • 갤6 2021.08.21 18:54

    아래아한글에서 개발환경에서 자주 사용되는 자동완성기능을 추가해보려고 합니다. 마치 IntelliSense 같은 거요.
    한글을 사용하는 최대한 모든 환경(웹컴포넌트나 파일열기 모두)에서 동일하게 작동하여야 하기 때문에, com+로는 힘들것 같습니다.
    찾아보니 아래와 같이 여러 추가기능이 바이너리로 배포되는 것 같은데요, 아무리 찾아봐도 api나 문서를 못찾겠네요. 조언 좀 부탁드립니다.
    https://hm.cyberseodang.or.kr/mboard.asp?Action=view&strBoardID=notice&intPage=1&intCategory=0&strSearchCategory=|s_name|s_subject|&strSearchWord=&intSeq=16103
    https://www.hancom.com/board/addinList.do
    개발 관련 인터페이스나 문서, 자료 아무거라도 있었으면 좋겠습니다.

    블로그에 좋은 내용이 많아 소소하지만 커피 보내드렸습니다~ 맛있게 드십쇼~


    답글

  • 양동훈 2021.08.22 01:56

    첫줄 안바뀌는건 해결했습니다. ^^;
    hwp.MovePos(2) # 문서의 시작으로 이동
    hwp.InitScan(0x00, 0x0007) # 본문을 대상으로 / 캐럿(커서)위치부터 / 문서 끝까지
    이거 두 줄 해주니까 첫줄 안바뀌던게 해결 됐네요.. (이제야 InitScan()에 대해서 이해를 제대로 했네요 ㅋㅋㅋㅋ)
    (코드를 보다 보니까 self.find_go 에 오타도 있고 ; 없애지 않는 것도 있고 문제가 좀 많았더라구요 ㅋㅋ)
    근데 여전히 단락과 단락이 붙어 있는 경우는 내어쓰기 설정했을때 한단락 건너뛰는건.. 이해가 안되네요 ㅠㅠ
    답글

  • 양동훈 2021.08.22 12:36

    단락 한개씩 건너뛰는거 해결했습니다. ^^;;
    def find_go 를 실행하고 나면 건너뛰어서..
    find_go 로 안찾고.. 그냥 한칸씩 오른쪽 이동해서 해결했네요 ㅎㅎㅎ

    아직 디버그 기능을 제대로 안쓰고 중간중간 print로 체크하면서 했네요.
    디버그 기능을 잘 안써봐서... 옛날에 베이직이나 파스칼 할때 잠깐 해보고 생각도 안하고 있었는데
    디버그 기능을 좀 더 공부해서 써먹어봐야겠어요

    질문드렸던 두가지가 다 해결되었네요...

    프로그램 코딩 정리하면서 구조를 조금 바꿨더니 다른 한가지가 정상으로 처리되지 않는데 그건 오늘 하루면 해결될거 같습니다. ^^;

    -----------------
    다른 한가지도 해결요 ^^;; 일단 버그는 모두 잡았네요 ㅎㅎ 표나 글상자등을 처리하는것만 남았네요
    답글

  • 양동훈 2021.08.27 13:47

    글자만 있는건 잘 되는데... 표나 그리기 글상자가 나오면 에러가 나네요.. ㅠㅠ
    혹시 현재 커서 위치가 일반 텍스트 인지 표 또는 그리기 글상자 안에 있는지 확인 하는 법이 있을까요??
    답글

  • 캐럿이 어느 컨트롤 안에 들어있는지 확인하실 필요는 없을 것 같습니다. InitScan시 option을 지정하지 않고(Range만 지정한 상태) 표나 글상자 등 컨트롤 리스트 전부를 포함하여 스캔하시면 됩니다.
    답글

  • 김용일 2021.10.01 13:12

    와우~ 엄청 좋은 자료군요~!! 저도 시도해 봐야겠습니다.
    답글