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

[3/4] 정관문서 서식잡기(장이름 중앙정렬, 위아래 빈라인 삽입)

by 일코 2020. 12. 12.

지난 포스팅은...

 

[2/4] 정관문서 서식잡기(장이름, 조제목만 굵게)

부탁 드렸던 방법은 제가 회사의 정관을 많이 손 보아야 하는 직업이라. 수년전 한글내에서 매크로로 겨우 조항 제목 문자 굵게 만들고, 2번 질문====================== 제 1조(상호) 제 2조(목적)을 다

www.martinii.fun


3번 질문==============
정관
제 1 장 총칙
제 1조
제 2조..
제 3조...
제 2 장 주식과 주권
제 4조....
제 5조..
제 3 장 임원
제 6조
등과 같을 때.

장의 위치를 페이지 가운데로 위치하고 싶고, 또한
각 장 줄의 위와 아래에 빈 줄을 삽입해 넣고 싶습니다. 즉,

빈줄
정관
빈줄
제 1 장 총칙
빈줄
제 1조
제 2조..
제 3조...
빈줄
제 2 장 주식과 주권
빈줄
제 4조....
제 5조..
빈줄
제 3 장 임원
빈줄
제 6조

등과 같이 만들고 싶습니다.

4번 질문==============
제 1조...
제 2조....
제 3조...
라고 했을 때
각 조 사이 마다 빈 줄을 하나씩 삽입하고 싶습니다 .즉
제 1조...
빈줄
제 2조....
빈줄
제 3조...
빈줄
이와 같이 만들고 싶습니다.

(후략)

한 구독자 분으로부터 문의메일을 받고 자문을 드렸던 내용을 포스팅으로 올리는 중입니다. 벌써 마지막 시간이네요.

일명 "정관서식 자동교정(?)" 시리즈입니다. 총 5개의 포스팅으로 연재 예정입니다.

0. [준비] 조항번호 재정렬
1. 조항번호 앞 공백 추가(제1조 -> 제__1조, 제10조 -> 제_10조)
2. 장제목 및 조항제목이 굵지 않다면 굵게
3. 장제목은 센터정렬, 장제목 위아래 빈줄, 조제목 위에 빈줄
4. 종합

 

정관의 초기 서식은 아래와 같습니다.

정관의 초기 서식

지난 포스팅에서는 장제목과 조항제목 부분만 선택해서 "굵게" 서식을 적용해보았습니다. 간단한 스크립트였지만, 액션아이디와 아이템셋을 사용해서 현재 선택한 문자열이 굵은지 아닌지를 먼저 체크하고, 필요한 때에만 "굵게"를 적용하는 코드가 (제 생각엔) 하이라이트였습니다.

2020/12/11 - [파이썬-아래아한글 자동화 응용] - [2/4] 정관문서 서식잡기(장이름, 조제목만 굵게)

 

이번 시리즈를 마칠 때, 최종적으로 제가 원하는 서식은 아래와 같습니다.

정관의 최종 서식


이번 포스팅에서는 장제목은 가운데정렬 및 위아래 빈 라인 한 줄씩 두고, 조항제목 부분마다 상단에 빈 라인 한 줄을 추가해보겠습니다.

이 자동화작업도 말은 간단하지만 간과해선 안 될 과정이 하나 있는데, 단순하게 위아래 엔터만 입력하는 코드만 넣어버리면!? 코드를 실행할 때마다 빈 라인이 늘어나서 문서가 괴상해지겠지요? 그래서 현재 선택된 구간 위나 위아래로 빈 라인이 있는지 확인하는 함수를 만들어줘야 합니다. 뭐, 그 외엔 "위에 빈줄 추가하는 함수", "아래에 빈줄 추가하는 함수", "제?장이나 제?조 문자열로 찾아가는 함수" 정도만 만들어주면 될 것 같아요.


실제 구현화면은 아래와 같습니다.

 

파이썬으로 빈 라인 삽입하면서 특정라인 중앙정렬하는 작업

파이썬으로 빈 라인 삽입하면서 특정라인 중앙정렬하는 작업(움짤)

아래는 작업전 HWP파일, 작업후 HWP파일과 소스코드입니다. 위 작업을 따라해 보시거나, 다른 작업으로 응용해보실 때 활용해 주시기 바랍니다.

작업 전 파일

2번완료.hwp
0.13MB

작업 후 파일

3~4번완료(최종).hwp
0.13MB

 

"""
3번 질문==============
정관
제 1 장 총칙
제 1조
제 2조..
제 3조...
제 2 장 주식과 주권
제 4조....
제 5조..
제 3 장 임원
제 6조
등과 같을 때.

장의 위치를 페이지 가운데로 위치하고 싶고, 또한
각 장 줄의 위와 아래에 빈 줄을 삽입해 넣고 싶습니다. 즉,

                        빈줄
                        정관
                        빈줄
                    제 1 장 총칙
                        빈줄
제 1조
제 2조..
제 3조...
                        빈줄
                제 2 장 주식과 주권
                        빈줄
제 4조....
제 5조..
                        빈줄
                    제 3 장 임원
                        빈줄
제 6조

등과 같이 만들고 싶습니다.
4번 질문==============
제 1조...
제 2조....
제 3조...
라고 했을 때
각 조 사이 마다 빈 줄을 하나씩 삽입하고 싶습니다 .즉
제 1조...
빈줄
제 2조....
빈줄
제 3조...
빈줄
이와 같이 만들고 싶습니다.
이것을 이미 만들어 주신 조항번호 파이썬 소스에 제가 부탁드린 부분만을 수정하여서
4가지로 파이썬 파일로 각각 만들어 주신다면 그것을 바탕으로 눈이 빠져라 열심히 공부를 하겠음을 약속 드리겠습니다.
좀 빠르게 배우고 싶은 마음에.
만들어진 소스를 바탕으로 역으로 공부해 가는 방향을 선택하고 싶습니다.
정말 어처구니 없는 부탁 같아서. 염치가 없습니다.
그래도 제 마음속에 염원을 말씀 드려 보았습니다.
죄송하고 부끄럽습니다....


 => "제1장"이나 "제 1 장"을 모두 찾아내는 경우는 정규식을 쓰면 참 간단하지만, (X)
    정규식 없이 str.replace(" ","") 방식으로 문자열에서 스페이스를 제거하고 검색하는 방법도 있다. (ㅇ)
    "제"와 "장"으로 쪼갠 0번 인덱스 원소가 정수면 된다.

    빈 줄을 삽입하는 코드는 자칫 반복 실행하면 두 줄, 세 줄로 늘어나버릴 수도 있으므로,
    부담스럽지만 빈 줄을 모두 제거한 후에(제목 위아래, 장 위아래 말고는 빈 줄이 없을 것이므로(?))  # "^n^n" -> "^n"
    빈줄삽입 코드를 실행한다. (X)
    혹은, 찾은 문자열 위가 빈줄인지, 아래도 빈줄인지 검색만 하는 코드로도 대체가 가능하다. (ㅇ)

    마지막으로, 위는 빈 줄이 있는데 아래에만 빈 줄이 없는 경우가 있으므로 둘을 따로 점검한다.

 => 4번 질문은 간단하다. 조제목 위에 빈줄이 있는지 확인하고 없으면 엔터. 아래는 확인할 필요가 없다.
"""

from tkinter import Tk
from tkinter.filedialog import askopenfilename
import re
import win32com.client as win32
import pyperclip as cb


def hwp_init(filename):
    hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
    hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
    hwp.Open(filename)
    hwp.XHwpWindows.Item(0).Visible = True
    hwp.HAction.Run("FrameFullScreen")
    return hwp


def hwp_center_align_and_insert_blank_line(hwp, dir, target):
    if target == "장":
        hwp.HAction.Run("ParagraphShapeAlignCenter")
    else:
        pass

    if dir == "above":
        hwp.HAction.Run("MoveLineBegin")
        hwp.HAction.Run("BreakPara")
    elif dir == "below":
        hwp.HAction.Run("MoveLineEnd")
        hwp.HAction.Run("BreakPara")
    else:
        raise ValueError


def hwp_check_if_blank_exists_above(hwp):
    current_position = hwp.GetPos()  # 현위치 저장(간혹 다음 검색위치로 튀는문제 조치)
    hwp.HAction.Run("MoveLineBegin")
    hwp.HAction.Run("MoveSelLeft")
    hwp.HAction.Run("MoveSelLeft")
    hwp.HAction.Run("Copy")
    hwp.SetPos(*current_position)  # 방금위치 복원
    if cb.paste() == "\r\n\r\n":
        return True
    else:
        return False


def hwp_check_if_blank_exists_below(hwp):
    current_position = hwp.GetPos()  # 현위치 저장(간혹 다음 검색위치로 튀는문제 조치)
    hwp.HAction.Run("MoveLineEnd")
    hwp.HAction.Run("MoveSelRight")
    hwp.HAction.Run("MoveSelRight")
    hwp.HAction.Run("Copy")
    hwp.SetPos(*current_position)  # 방금위치 복원
    if cb.paste() == "\r\n\r\n":
        return True
    else:
        return False


def hwp_find_and_go(hwp):
    hwp.InitScan()
    장번호 = 1
    조번호 = 1
    while True:
        text = hwp.GetText()
        if text[0] == 1:
            break
        else:
            if re.match(rf"^제{장번호}장.+", text[1].strip().replace(" ", "")):
                장번호 += 1
                hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
                if not hwp_check_if_blank_exists_above(hwp):
                    hwp_center_align_and_insert_blank_line(hwp, "above", "장")
                if not hwp_check_if_blank_exists_below(hwp):
                    hwp_center_align_and_insert_blank_line(hwp, "below", "장")
                hwp.InitScan()

            if re.match(rf"^제{조번호}조.+", text[1].strip().replace(" ", "")):
                조번호 += 1
                hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
                if not hwp_check_if_blank_exists_above(hwp):
                    hwp_center_align_and_insert_blank_line(hwp, "above", "조")
                    hwp.InitScan()

            else:
                pass
    hwp.ReleaseScan()
    hwp.MovePos(2)


if __name__ == '__main__':
    root = Tk()
    filename = askopenfilename()
    root.destroy()
    hwp = hwp_init(filename=filename)
    hwp_find_and_go(hwp)

 

위는 전체 소스코드입니다. 아래는 함수 정의 하나하나 설명이 필요할 것 같아 덧붙입니다.

from tkinter import Tk
from tkinter.filedialog import askopenfilename
import re
import win32com.client as win32
import pyperclip as cb
- tkinter는 파이썬에 내장되어 있는 GUI 모듈입니다. PyQt5와 더불어 가장 많이 쓰이는 GUI 툴이에요. 콘솔이나 IDE에 익숙하지 않은 다른 사용자를 위해서라면 간단한 GUI 붙이는 코드는 배워두시면 훨씬 협업이 쉬울 것 같아요.

- askopenfilename은 tkinter의 기본위젯 중 하나인 파일선택 다이얼로그입니다. 이걸로 해당 HWP파일을 선택하시면 실행돼요. 타이틀이나 아이콘, 선택가능한 확장자를 설정할 수도 있고, 심지어 툴팁도 추가할 수 있습니다. 하지만, 그런 게 있다는 것만 알아둡시다.

- re는 정규표현식(Regular Expression)을 사용할 수 있는 모듈입니다. 저도 개인적으로 습관처럼 임포트하는 모듈이 두 개가 있는데 하나가 pyperclip이고, 다른 하나가 re입니다. 정규식과 클립보드제어(pyperclip as cb)는 지금도 가장 많이 활용하는 기능입니다. 자동화가 엄청 편해져요.

- 마지막으로 win32com.client 모듈은 윈도우에서 실행되는 프로그램의 오브젝트를 원격으로 실행하고 조작할 수 있는 아주 강력한 모듈입니다. 수많은 엑셀관련 모듈이 출시되었지만, 우리 회사에서 돌아가지 않는 이유는 바로 엑셀파일에 걸려있는 DRM 때문인데요. 저처럼 IT부서원도 아니고 보안부서원도 아니고 일개 코딩연습생 나부랭이가 파이썬 프로그램 제작을 위해 DRM을 해제해달라고 노래를 불렀다간, "이를 정보화사업으로 간주하여 보안성평가를 어쩌고," 하면서 귀찮게 할 것이 뻔하기 때문입니다. 근데 win32com으로 엑셀을 열고 거기서 데이터를 다루면 이건 Fasoo고 뭐고 누구도 막을 수 없죠. 셀레늄으로 열어놓은 브라우저와 같은 느낌입니다. (꿀팁스러운) TMI를 하나 드리면, DRM이 걸린 xlsx파일을 pd.read_excel로 불러오시려면, win32com으로 엑셀을 열고 해당파일과 워크시트를 열어놓은 후에, "SelectAll" & "Copy" 하시고 pd.read_clipboard() 하면 깔끔하게 데이터프레임 생성이 됩니다. win32com 정말 써먹을 데 많아요.
def hwp_init(filename):
    hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
    hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
    hwp.Open(filename)
    hwp.XHwpWindows.Item(0).Visible = True
    hwp.HAction.Run("FrameFullScreen")
    return hwp
- 한/글 오브젝트를 생성하는 코드입니다. 항상 기본적으로 실행해야 하는 라인이 대여섯줄 정도 되니까, 다른 파일에 함수로 저장해놓고 임포트하시거나 하는 방식으로 사용하시면 훨씬 편할 수 있습니다.

- gencache.EnsureDispatch는, 생소한 분들도 계실 것 같아요. win32.Dispatch는 기본적으로 Lazy-Binding, EnsureDispatch는 Early-Binding 개념입니다. 간단히 설명드리면, MakePy라는 프로세스를 통해 파이썬 바인딩 파일을 생성하고 클래스 메서드나 프로퍼티 등을 모두 저장해놓는 개념입니다. 아래아한글은 특히 이 부분에 차이가 큰데, 그냥 Dispatch로 한/글 오브젝트를 생성하면 모든 메서드 파라미터 기본값이 불러와지지 않습니다. EnsureDispatch로 한/글 오브젝트를 만들었으면 hwp파일을 불러올 때 hwp.Open(파일명)만 해도 되는데, Dispatch로 한/글을 생성하면, hwp.Open(path=파일명, format="HWP", arg="None") 이렇게 세 개의 파라미터를 모두 직접 지정해줘야 Open메서드가 정상적으로 실행됩니다. 아, 물론 EnsureDispatch를 한 번 실행한 후에는 Dispatch로 한/글을 열어도 EnsureDispatch로 연 것과 동일하게 됩니다. (저는 튜토리얼 차원에서 계속 Ensure~를 붙이고 있습니다.)

- 아래는 차례로 보안모듈(자세한 설명은 이 글 마지막 링크 참고), 파일 열기, 숨김해제, 전체화면 설정입니다.
def hwp_center_align_and_insert_blank_line(hwp, dir, target):
    if target == "장":
        hwp.HAction.Run("ParagraphShapeAlignCenter")
    else:
        pass

    if dir == "above":
        hwp.HAction.Run("MoveLineBegin")
        hwp.HAction.Run("BreakPara")
    elif dir == "below":
        hwp.HAction.Run("MoveLineEnd")
        hwp.HAction.Run("BreakPara")
    else:
        raise ValueError
- 이게 잘 짜여진 함수 같지는 않습니다. 프로그램 구조나 디자인 패턴에 익숙한 분들이 손 좀 봐주시면 좋겠습니다. 하여튼, 위 함수는 이런 기능을 합니다.
- 세 개의 인자값을 가졌다. 첫 번째는 hwp객체, dir은 "above"나 "below"를 받고, target은 "장"이나 "조"를 받는다. target이 "장"인 경우에만 "중앙정렬"을 실행하고, 그 다음은 dir이 "above"인 경우에 라인시작으로 가서 엔터, "below"인 경우에 라인끝으로 가서 엔터.
- 다만 이 함수는 혼자서 작동할 수 없는데 "찾아가는 함수"와 "위아래 빈라인이 있는지를 체크하는 함수" 두 개가 더 필요합니다. 그건 아래에~
def hwp_check_if_blank_exists_above(hwp):
    current_position = hwp.GetPos()  # 현위치 저장(간혹 다음 검색위치로 튀는문제 조치)
    hwp.HAction.Run("MoveLineBegin")
    hwp.HAction.Run("MoveSelLeft")
    hwp.HAction.Run("MoveSelLeft")
    hwp.HAction.Run("Copy")
    hwp.SetPos(*current_position)  # 방금위치 복원
    if cb.paste() == "\r\n\r\n":
        return True
    else:
        return False

 

-함수이름처럼,  상단에 빈라인이 있는지 체크합니다. 현재 캐럿의 위치를 GetPos()로 저장하고, 줄시작으로 가서 "SHIFT-좌좌"를 누르고 복사를 누릅니다. 상단이 빈 줄이면 클립보드에 "\r\n\r\n"이 찍힐 거고, 빈 줄이 아니면 어떤 문자열이 섞여 있을 것입니다. 그리고 SetPos(기존위치)를 실행해서 원래 캐럿위치로 돌아옵니다.
- TMI : pyperclip의 컨벤션으로 cb를 쓰는 것을 어색해 하는 분들이 있는데, 저도 처음엔 clipboard 모듈을 사용하고 있었어요. import clipboard as cb. 근데 cb모듈의 소스를 직접 열어보고는 경악을 금치 못했습니다. 그 이후로 pyperclip만 사용하게 되었습니다. (왜 경악했는지는 직접 열어보시면 좋을 것.) 그래도 컨벤션은 기존의 cb가 편해서 쓰고 있는 중입니다.
def hwp_check_if_blank_exists_below(hwp):
    current_position = hwp.GetPos()  # 현위치 저장(간혹 다음 검색위치로 튀는문제 조치)
    hwp.HAction.Run("MoveLineEnd")
    hwp.HAction.Run("MoveSelRight")
    hwp.HAction.Run("MoveSelRight")
    hwp.HAction.Run("Copy")
    hwp.SetPos(*current_position)  # 방금위치 복원
    if cb.paste() == "\r\n\r\n":
        return True
    else:
        return False
- 위의 exists_above 함수와 거의 같습니다. 다만 아래쪽의 빈 라인을 체크하는 것만 차이가 있어요.
def hwp_find_and_go(hwp):
    hwp.InitScan()
    장번호 = 1
    조번호 = 1
    while True:
        text = hwp.GetText()
        if text[0] == 1:
            break
        else:
            if re.match(rf"^제{장번호}장.+", text[1].strip().replace(" ", "")):
                장번호 += 1
                hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
                if not hwp_check_if_blank_exists_above(hwp):
                    hwp_center_align_and_insert_blank_line(hwp, "above", "장")
                if not hwp_check_if_blank_exists_below(hwp):
                    hwp_center_align_and_insert_blank_line(hwp, "below", "장")
                hwp.InitScan()

            if re.match(rf"^제{조번호}조.+", text[1].strip().replace(" ", "")):
                조번호 += 1
                hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
                if not hwp_check_if_blank_exists_above(hwp):
                    hwp_center_align_and_insert_blank_line(hwp, "above", "조")
                    hwp.InitScan()

            else:
                pass
    hwp.ReleaseScan()
    hwp.MovePos(2)
- 아래아한글 문자열 탐색의 가장 기본이 되는 GetText와 re.match, 그리고 str.replace(" ", "") 등 유용한 함수와 메서드가 많이 들어 있는 파트입니다. 문자열 탐색 메서드인 GetText도 내부적으로 최적화가 잘 되어 있어 아무리 긴 문서라도 정말 순식간이에요. 가급적 클립보드나 찾기Ctrl-F를 이용해서 우회하거나 파이썬으로 더 느려지게 발목잡지 말고, GetText만 사용해서 탐색하고 문자열을 검토하면 "정말 빠르다"를 느낄 수 있을 것입니다.
- GetText는 InitScan() 메서드를 먼저 실행해서 "탐색초기화"란 걸 해 줘야 합니다.
- GetText는 한 번씩 실행할 때마다 엔터를 기준으로 한 문단씩 문자열을 리턴해줍니다. 정확히는 2개 원소를 지닌 튜플을 리턴하는데, 인덱스[0]에는 상태코드, [1]에는 해당 문단의 문자열 전체를 담고 있고, 그래서 while문을 돌리면서 text[0]이 1(종료)이 될 때까지 원하는 탐색이나 연산을 계속하면 됩니다.
- GetText로 용무를 다 본 후에는 항상 ReleaseScan을 통해 메모리초기화를 해줘야 한다고 합니다.
- 마지막으로 MovePos는 정수를 인자로 받아 캐럿을 특정위치로 옮기는 메서드인데, 유용한 파라미터로는, 2:문서시작, 3:문서끝, 6:현재문단시작, 7:현재문단끝, 10:다음문단시작, 27:현재 캐럿이 위치한 곳으로 뷰 이동, 201:현재 GetText로 탐색중인 위치로 이동 등 (총 38개 정도 됩니다.)
- 정규식을 좀 더 자세히 설명드리고 싶은데, 앞 포스팅에서 열심히 다뤘으므로 참고해 주시기 바랍니다.
if __name__ == '__main__':
    root = Tk()
    filename = askopenfilename()
    root.destroy()
    hwp = hwp_init(filename=filename)
    hwp_find_and_go(hwp)
- 위 함수들을 모아다가 프로그램 진입점 같은 걸 만들었습니다. (엄밀히는 진입점이란 단어도 틀린 것 같아요. 파이썬은 기본적으로 스크립트 상단에서부터 실행합니다.)
- if __name__ == "__main__": 의 의미는 다음과 같습니다.
"이 py파일을 직접 파이썬으로 실행한 경우라면 아래의 코드도 실행한다.
하지만 이 파일을 다른 py파일에서 import를 한 경우라면 아래의 코드는 실행하지 않는다."
 
- root = Tk()는 다소 생소할 수 있지만 Tk의 인스턴스를 생성하는 문법입니다. tkinter은 기본적으로 클래스 방식으로 동작합니다. 이를 종료할 때에는 해당 인스턴스를 제거(destory)해 줘야 해요.

 

여기까지 정관 서식을 수정할 때 필요한 정말 거의 대부분의 자동화 코드를 살펴보았습니다.

앞으로 연재할 튜토리얼 대부분도 이와 유사하거나, 훨씬 더 단순할 거라고 생각해요.

이번 "정관 서식조정 자동화" 튜토리얼은 여기서 마치겠습니다.

긴 글 읽어주셔서 감사합니다. 행복한 하루 되세요!

후기

마지막으로, 한/글 워드프로세서를 사랑하건 사랑하지 않건, 업무에 많이 쓰시는 분들이라면 한 번은 한/글 자동화 코드를 작성해 보시는 것을 추천한다. 필자도 이를 업무에 활용하면서 정말 여러가지 좋은 기회, 멋진 경험을 접하고 있다. WTO 사무총장 제네바 선거캠프에 기여하기도 하고, 파이콘 연사로 발표하기도 하고, 강남 유수의 협회에 컨설팅을 해주기도 했고, 대학생 강연, 집필의뢰, 모 질병과 관련한 보고서 자동화 강의 의뢰, I사, F사의 온라인강의 제안, 그리고 유튜브 채널도 하나 소소하게 운영하게 되었고, 블로그도 이렇게 하나 가지게 되었다.
그리고 무엇보다, 필요에 의해 배우는 것들은 잘 배워진다고 한다. 코딩과 IT에 관심이 있었지만, 이렇게까지 업무에 적용할 수 있는 줄 몰랐다면 두어달 있다가 그만뒀을텐데, 운좋게 해외파견을 나갔을 때 현지인들의 엑셀업무 프로세스를 정리해주고 파이썬으로 일부 자동화코드를 (공부해서) 짜 준 게 내 딴에는 터닝포인트였다. 너무너무 편리했다.
혹시 이 글을 읽는 여러분들 중에, 정말 물리적인 시간이 부족해서 야근을 하는 분이 있다면, 뭔가 잘못되었다. 대부분은 일이 줄어들어야 하는 케이스다. 일을 줄이기 힘들다면, 그 땐 일하는 방법을 바꿔야 한다.
그 때 이런 업무자동화 소스들이 조금이나마 도움이 되기를 바라는 마음이다.

댓글