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

[4/4] 파이썬으로 정관.HWP 서식잡기 종합

by 일코 2020. 12. 16.

지난 포스팅은...

 

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

3번 질문============== 정관 제 1 장 총칙 제 1조 제 2조.. 제 3조... 제 2 장 주식과 주권 제 4조.... 제 5조.. 제 3 장 임원 제 6조 등과 같을 때. 장의 위치를 페이지 가운데로 위치하고 싶고, 또한 각 장

www.martinii.fun


중앙정렬과 각 조항 위아래 빈 줄을 추가하는 것을 끝으로 정관이나 법령 서식 교정하기 콘텐츠는 마무리를 지으려 한다. 이번 포스팅에서는 추가 정보를 제공하는 것보다는, 1. 입맛대로 개별의 코드를 하나의 파일로 모아서, 한 번에 실행할 수 있다는 걸 보여주고 싶고, 2. 아래에서 보여줄 예제처럼, 모든 함수를 한 파일에 우겨넣으면 파일이 굉장히 비대해지고, 어느 부분에서 잘못되어 가는지 짚어내기도 쉽지 않다. 특정 함수들을 묶어서 모듈화하면 코드가 짧아진다는 것도 추후에 보여드리고 싶다.

이야기한 바와 같이 이번 포스팅은, 0~3번까지의 포스팅을 종합한 코드와 영상을 보여드릴 예정. 영상은 아래와 같다.

파이썬으로 한/글문서 서식 조정하는 화면
코드를 하나하나 펼쳐서 살펴보는 화면

코드와 예제파일은 여기 붙여놓았다. 파이썬과 pywin32모듈, 그리고 한/글 프로그램이 설치되어 있다면 아래의 예제를 실행해볼 수 있다.

종합파일(0~4).py
0.01MB
[예시]건축물관리법 시행령(대통령령)(제31194호)(20201204).hwp
0.26MB


마지막으로 아래 자료는 법령집의 예시자료가 아닌, 실제 주식회사 정관 샘플과 교정용 코드다.
공부에 참고하시길 바람. 녹화 영상 먼저.

[00:11~00:17]번호정렬, [~00:27]번호앞빈칸, [~00:38]제목진하게, [~00:51]위아래빈줄삽입, [00:55~끝]코드공개

 

정관샘플.hwp
0.11MB
실전.py
0.01MB

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

# 우선 한글을 불러오겠습니다. GUI를 열어서 해당 HWP파일을 선택합니다.
root = Tk()
filename = askopenfilename()
root.destroy()

# 이제 아래아한글을 엽니다.
hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
hwp.Open(filename)
hwp.XHwpWindows.Item(0).Visible = True
hwp.HAction.Run("FrameFullScreen")


# %% 코드반복이 많아질 것을 대비해서, 여기 몇 개만 미리 정의해놓겠습니다.


def 찾아바꾸기(find_string, replace_string):
    """찾아바꾸기 단축함수"""
    hwp.Run("MoveSelNextWord")
    hwp.HAction.GetDefault("ExecReplace", hwp.HParameterSet.HFindReplace.HSet)  # 한/글 특성상 부득이하게 두두번번 실실행행
    hwp.HParameterSet.HFindReplace.Direction = hwp.FindDir("Forward")
    hwp.HParameterSet.HFindReplace.FindString = find_string
    hwp.HParameterSet.HFindReplace.ReplaceString = replace_string
    hwp.HParameterSet.HFindReplace.ReplaceMode = 1
    hwp.HParameterSet.HFindReplace.IgnoreMessage = 1
    hwp.HAction.Execute("ExecReplace", hwp.HParameterSet.HFindReplace.HSet)
    hwp.HAction.GetDefault("ExecReplace", hwp.HParameterSet.HFindReplace.HSet)
    hwp.HParameterSet.HFindReplace.Direction = hwp.FindDir("Forward")
    hwp.HParameterSet.HFindReplace.FindString = find_string
    hwp.HParameterSet.HFindReplace.ReplaceString = replace_string
    hwp.HParameterSet.HFindReplace.ReplaceMode = 1
    hwp.HParameterSet.HFindReplace.IgnoreMessage = 1
    hwp.HAction.Execute("ExecReplace", hwp.HParameterSet.HFindReplace.HSet)
    return


# 문자입력 함수
def 입력(문자열):
    """문자입력 함수"""
    hwp.HAction.GetDefault("InsertText", hwp.HParameterSet.HInsertText.HSet)
    hwp.HParameterSet.HInsertText.Text = 문자열
    hwp.HAction.Execute("InsertText", hwp.HParameterSet.HInsertText.HSet)
    return


def 조항번호_테스트():
    """조항번호 순서 및 갯수 맞는지 테스트
    이 부분은 조금 복잡할 수 있는데, "리스트 컴프리헨션"이라는 파이썬 문법입니다.
    문서 모든 텍스트를 파이썬으로 불러와서[hwp.GetTextFile]
    전체문자열을 엔터[\r\n]로 자른 리스트 중에 "제"로 시작하면서 "("를 가지고 있는 원소만 가져다가
    좌우 스페이스 있으면 떼고[strip], "("로 자른 리스트 왼쪽에 있는 값만 리스트로 리턴합니다.
    최종적으로는 ["제1조", "제2조", .. "제46조"] 식의 리스트가 됩니다.
    len(각조항시작)을 실행해보시면 조항이 50개인 걸 확인하실 수 있습니다. 마지막 조항번호는 (현재) 46이니까, 문제가 있는 게 확실하네요."""
    각조항시작 = [int(i[1:].strip().split("조")[0]) for i in hwp.GetTextFile("TEXT", "").split("\r\n") if
             i.startswith("제") and "(" in i]
    print("검사완료:합격" if 각조항시작 == sorted(각조항시작) and max(각조항시작) == len(
        각조항시작) else "검사완료:불합격")  # True를 리턴하면 조항 중복이 없고 갯수가 맞다는 뜻입니다.


################################################ 1. 조항번호 정렬작업


# %%
# 이제 번호를 정렬하는 작업을 해보겠습니다. 저는 어떻게 하고 싶냐면,
# hwp.GetText()를 사용하되, 기존의 방법대로 말고 변경할 부분의 위치(튜플)만 사전으로 뽑아다 따로 만들어놓고,
# 다 찾은 다음에 해당 리스트를 순회하면서 숫자를 바꾸겠습니다. 위의 사전은 "위치"를 키, (기존숫자, 바꿀숫자) 튜플을 값으로 만들어 둡니다.
#
# 아래 코드부터 가만히 읽어보시기 바랍니다.

hwp.InitScan()
조항번호 = 1
while True:
    문단 = hwp.GetText()
    if 문단[0] == 1:
        break
    else:
        if re.match(r"^제[ ]?\d+[ ]?조[ ]?\(?", 문단[1]):  # 무식한 방법임. "제?조"로 시작하는 문단으로 가서
            hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
            hwp.MovePos(27)  # moveCurrentCaret : 현재 캐럿이 위치한 곳으로 이동한다.
            hwp.Run("MoveSelLineEnd")  # 한 줄 선택해서
            hwp.Run("Copy")  # 복사해놓고
            바꿀문자열 = re.sub(pattern="제[ ]?\d+[ ]?조[ ]?\(", repl=f"제{조항번호}조(", string=cb.paste())  # 숫자를 옳게 바꾼다. (옳아도 실행)
            입력(바꿀문자열)
            조항번호 += 1
hwp.ReleaseScan()
hwp.MovePos(2)

조항번호_테스트()
# 조항번호 정렬작업 끝

################################################ 2-1. 조항제목 괄호 뒤에 빈칸 없으면 추가하기
#%%


def hwp_조항제목_괄호뒤에_공백_없으면_넣기(hwp):
    hwp.InitScan()
    while True:
        text = hwp.GetText()
        if text[0] == 1:  # 끝에 닿은 경우
            break
        elif re.match("제[ ]*\d+조[ ]*\(", text[1].replace(" ", "")) and text[1].split(")", maxsplit=1)[1][0] != " ":
            hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
            hwp.Run("MoveSelLineEnd")
            hwp.Run("Copy")
            입력(cb.paste().replace(")", ") "))
            hwp.InitScan()
            continue
        else:
            pass
    hwp.ReleaseScan()
    hwp.MovePos(2)


# 위 함수는 그대로 사용합니다.
hwp_조항제목_괄호뒤에_공백_없으면_넣기(hwp)

################################################ 2-2. 조항번호 뒤에 공백 붙이기


def hwp_번호앞에_공백넣기(hwp):  # 주의사항 : 번호정렬이 되어 있어야 한다.
    hwp.InitScan()
    조항번호 = 1
    while True:
        text = hwp.GetText()
        if text[0] == 1:  # 끝에 닿은 경우
            break
        elif text[0] == 0:  # 탐색 중 문자열이 수정된 경우
            hwp.ReleaseScan()
            hwp.InitScan()
            continue
        elif text[1].replace(" ", "").startswith(f"제{조항번호}조(") and not text[1][1:].startswith(" "):
            hwp.MovePos(201)  # moveScanPos : GetText() 실행 후 위치로 이동한다.
            hwp.HAction.Run("MoveLineBegin")
            target_text = re.match(r"^제?\d+조\(?", text[1]).group(0)  # '제1조('를 찾음
            찾아바꾸기(target_text,
                             f"제{조항번호: 3}조(")  # ": 3"의 의미는, "문자열을 포함하여 세자리가 되어야 하고 남은 칸은 공백(3 앞의 스페이스)으로. 003으로 변경하려면 :03으로 바꾸면 됨."
            조항번호 += 1
            continue
        else:
            pass
    hwp.ReleaseScan()
    hwp.MovePos(2)


# 위 함수는 그대로 사용합니다.
hwp_번호앞에_공백넣기(hwp)

################################################ 3. 장제목, 조항제목 굵게
#%% 결국 정규식으로 다 가게 되네요. 오타도 많고, 예외도 많아서 코드로 처리하면 너무 길어집니다. 정규식으로는 간단해요.


def hwp_진하게(hwp, type):
    if type == "장":
        hwp.HAction.Run("MoveSelLineEnd")
        if hwp.CharShape.Item("Bold") != 1:
            hwp.HAction.Run("CharShapeBold")
    elif type == "조":
        hwp.HAction.GetDefault("RepeatFind", hwp.HParameterSet.HFindReplace.HSet)
        hwp.HParameterSet.HFindReplace.FindString = ")"
        hwp.HParameterSet.HFindReplace.Direction = hwp.FindDir("Forward")
        hwp.HParameterSet.HFindReplace.IgnoreMessage = 1
        hwp.HParameterSet.HFindReplace.FindType = 1
        hwp.HAction.Execute("RepeatFind", hwp.HParameterSet.HFindReplace.HSet)
        hwp.HAction.Run("MoveRight")
        hwp.HAction.Run("MoveLeft")
        hwp.HAction.Run("MoveSelLineBegin")
        if hwp.CharShape.Item("Bold") != 1:
            hwp.HAction.Run("CharShapeBold")
        hwp.HAction.Run("MoveLineBegin")


def hwp_장과조_찾아가서_진하게(hwp):
    # max_number = hwp_get_max_number(hwp)
    hwp.InitScan()
    장번호 = 1
    조항번호 = 1
    while True:
        text = hwp.GetText()
        if text[0] == 1:
            break
        else:
            if re.match(rf"^제{조항번호}조\(?", text[1].replace(" ", "")):
                hwp.MovePos(201)
                hwp.MovePos(27)
                hwp_진하게(hwp, "조")
                조항번호 += 1
                hwp.InitScan()
            if re.match(rf"^제{장번호}장", text[1].replace(" ", "")):
                hwp.MovePos(201)
                hwp.MovePos(27)
                hwp_진하게(hwp, "장")
                장번호 += 1
                hwp.InitScan()
            else:
                pass
    hwp.ReleaseScan()
    hwp.MovePos(2)

hwp_장과조_찾아가서_진하게(hwp)


################################################ 4. 장은 센터정렬, 모든 장조 위아래 빈 줄 하나씩

#%%
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() 실행 후 위치로 이동한다.
                hwp.MovePos(27)
                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() 실행 후 위치로 이동한다.
                hwp.MovePos(27)
                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)

hwp_find_and_go(hwp)

개인적으로 가장 피곤한 자동화작업을 고르라면, 이런 종류의 서식교정 작업이다.
한 달에도 몇 번씩 반복하는 작업이 아니라면, 이 정도 시간을 자동화 구축에 들이는 것이 손해라는 느낌이 든다.

오늘 포스팅은 여기에서 마친다.

행복한 하루 되시길!

댓글