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

[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)

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

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

행복한 하루 되시길!

반응형

댓글1

  • 이이잇 2022.08.19 17:51 신고

    이건 신세계로군요!!!
    과제 보고서를 제출 마감기간이 되면
    성과 취합해서 양식 맞춰 보고서 제출해야하는데..
    적어도 300장.. 많으면 1000장이 되는 경우도 있습니다.
    성과 취합하려 해도 담당자가 늦게 주면
    정말 울며 겨자먹기로 반나절만에 서식 맞추다 그냥 제출하고
    서식 이모양이냐고 욕먹고.. 참 힘든데..
    가뭄의 단비같은 포스팅이군요 ㅠㅠ
    감사합니다. 위 내용 공부 열심히해서
    저도 다음 보고서부터는 자동화로 서식 잡도록 하겠습니다. 정말 감사합니다.
    답글