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

파이썬으로 "제?조(조항명)"을 "제?조[조항명]"으로 수정하기

by 일코 2020. 12. 8.
...저는 한 열흘 전 우연히 파이썬 이라는 것을 처음 알고.

유투브에서 어느 분이 가르쳐 주신대로 파이썬을 깔아놓았고. print("hello")만 딱 한 번 해 보았을 뿐입니다.

참고로, 저는 젊어서 Dos시절 포트란, 파스칼, C 등등 당신의 프로그램 언어로 코딩을 개인적으로 많이 하기도 하였다가, 이제는 나이가 ㅇㅇ살인데 뭘 더 하겠습니까만. 그래도 하는 일이 비쥬얼베이직으로 엑셀내에서 조금씩 기억을 더듬어 가며 공부 아닌 공부를 하고 있는 사람입니다.

부탁 드렸던 방법은 제가 회사의 ㅇㅇ을 많이 손 보아야 하는 직업이라.

수년전 한글내에서 매크로로 겨우 조항 제목 문자 굵게 만들고, 제1조(상호명)  () 괄호를 []대괄호로 제1조[상호명] 바꾸는 것을 시도 해 본 이후, 아마 7년전 일 같습니다. 당시 번호 바꾸는 것은 일일이 손으로 하였습니다. ..ㅠㅠ

지금은 다 잊었지만.....글쎄 되려나 모르겠지만...
최근에 제가  이 일을 다시 시작하게 되면서 선생님께서 작성해 주신 방법이 파이썬에서 구현이 되는가 며칠 동안 잠을 자며 꿈꿔 보던 것이 었습니다.
이렇게 만들어 주셔서 너무 고맙습니다.

제가 한 번 도전 해 보겠지만.

행여 선생님께서 시간이 되시는 날. 제가 시도 해 보려던 저 위에 방식을 구현 해 주신다면 더 없이 감사 함 드리겠지만, 부담 갖지 마시었으면 합니다.

이 정도 만으로도 너무 너무 감사 드리고 있습니다.

(중략)

뜻하지 않게 감사 인사 드리다가 그만 부탁 아닌 글도 써 지고 말았습니다.
부담 갖지 않으시기 바라오며
하시는 일 언제나 항상 축복 받으시고 복된 가정 되시기 기도 드리겠습니다.
다시 한 번 대단히. 진심으로 감사. 감사. 감사 드립니다.

연세가 정말 상당하심에도, 배움의 의지를 놓지 않고 계신 부분에 저도 큰 자극이 되었습니다. 메일 본문에는 훨씬 길게 양해 부탁의 말씀과 감사의 메시지를 전해 주셨고, 그 부분은 생략하였습니다.

하여튼 골자는 아래 두 개입니다.

1. 조항 제목 문자를 굵게 만드는 작업(바로 아래 포스팅에서 자세히 다뤘어요.)

2. "제1조(상호명)"을 "제1조[상호명]"으로 일괄 변경하는 작업.

이번에는 2번 작업을 자동화하는 스크립트를 짜보겠습니다.

샘플파일을 아래 업로드하였으니, 연습하실 경우에 다운받아서 활용하시기 바랍니다.

[예시]건축물관리법 시행령(대통령령)(제31194호)(20201204).hwp
0.13MB

(hwp파일 출처 : 법제처 law.go.kr)

위 문서는 조항번호가 정렬되어 있으며, 조항명에 "진하게"가 적용되어 있습니다.


이제 제목 부분의 ()괄호만 []괄호로 바꿔봅시다.

우선, 코딩을 사용하지 않고 직접 바꾸는 방법에 대해 먼저 알아봅시다.

1. 기본적인 "찾아바꾸기" : 조항 제목 뿐 아니라 본문의 괄호도 모두 대괄호로 바뀌기 때문에 사용불가.

2. "조(""조["로 바꾸고, ") ""] " 로 바꾸기 : 찾아보니 본문에도 해당되는 부분이 참 많아서 사용불가.

3. 하나하나 찾아가서 직접 바꾸거나 찾아바꾸기 실행 : 오래 걸리고 무식한 방법이지만, 가능합니다.

 

저는 자동화를 구상할 때 이런 방식으로 개념을 먼저 잡아봅니다. 그러면 코딩하기도 참 쉬워집니다.

왜냐고요?

무식하게 하나하나 손으로 작업하는 "그 작업"만 컴퓨터한테 맡기면 되거든요.

쪼개고 쪼개서 단순화한 후에 코드로 옮기려면, 무식해 보이는 방법이 코딩하기 가장 쉽고 편합니다.

그래서 이번에도 3번 방식으로 코드를 짜 볼 예정입니다.

 

작업과정을 좀 더 자세히 적으면 아래와 같습니다.

1. 모든 문서에서 "제{숫자}조({글자}) "로 시작하는 부분으로 찾아가기를 실행합니다.

2. "(""["로 찾아바꾸기를 한 번만 실행합니다.

3. ")""]"로 찾아바꾸기를 또 한 번만 실행합니다.

4. 1번 과정을 반복합니다. 언제까지? "제{숫자}조({글자}) "가 나타나지 않을 때까지.

 


1번 과정 : "제{숫자}조({글자})" 로 구성된 문자열 찾기

1번 과정을 진행하기 앞서 긴 고민 끝에 여러분께 "조건식(정규표현식)"에 대해 소개해 드리기로 마음먹었습니다.

조건식은 조금만 배워도 아주 유용하게 써먹을 수 있는 파워풀한 검색문법입니다. 

아래아한글도 오래전부터 조건식 검색 기능을 제공하고 있었고, voidtools의 everything이나, 노트패드++ 같은 응용프로그램 다수에서도 다소 문법의 차이는 있지만, 검색에 조건식을 적용할 수 있습니다.

(근데 왜 긴 고민을 거쳤냐면, 조건식의 첫인상이 굉장히 부담스럽거든요. 복잡해 보이기도 하고...)

아래아한글에서도 버전별로 아~주약간 다른 부분이 있지만, 하여튼 "찾기"에 조건식을 활용할 수 있습니다.

여기서, 조건식을 쉽게 표현하면  "이러이런 문자열을 찾아줘" 라고 컴퓨터에게 요청하는 일종의 검색문법입니다.

우선 아래 영상을 한 번 보여드리겠습니다. 조건식으로 특정 문자열을 검색하는 과정입니다.

조건식을 아주 조금만 익혀 두셔도, 이런 고급 검색작업을 손쉽게 하실 수 있습니다.

조건식을 통한 특정 문자열 검색 화면. "조건식 사용"에 체크한 후 사용 가능.

재밌고 신기하지 않나요? (저도 업무에 조건식을 정말정말 많이 활용해 왔습니다.)

위 검색영상에 사용된 조건식 문자열은 아래와 같습니다. 

^제\d+조\(.+?\)

이 조건식으로 검색하면 문단 시작부분의 "제1조(목적)" 이나 "제40조(과태료의 부과기준)" 등의 문자열만 선택해줍니다.

조금씩 잘라서 설명드리겠습니다.

^제\d+조\(.+?\)

^"이렇게 시작하는 문자열을 찾아줘" 라는 의미입니다.

(반대로 이렇게 끝나는 걸 찾아줘 할 때는 "....$" 를 사용하시면 됩니다.)

문단 중간에 나오는 "제\d+조\(.+?\)" 해당 문자열은 전부 무시하죠.

^제\d+조\(.+?\)

^제가 나왔으니, "제"로 시작하는 문자열을 찾아줘 라는 의미가 되겠습니다.

^제\d+조\(.+?\)

\d는 한 자리 숫자를 뜻합니다.

^제\d+조\(.+?\)

근데 뒤에 +가 붙었네요? \d+는 한 자리 이상의 숫자를 뜻합니다. 1이든 99든 1000이든 말이죠.

참고로 \는 특수한 문자를 뜻하는 일종의 약속입니다. 유식하게 "이스케이프 문자열"이라고도 부릅니다.

(실수로 ^제d+ 라는 조건식을 썼으면 한/글 검색엔진이 "제d"나 "제dddddd"같은 문자열을 찾으려고 시도했을 겁니다.)

^제\d+\(.+?\)

\(나 \)는 순수한 "("")"를 가리킵니다. \가 붙지 않은 "(",  ")"는 조건식 문법에서 "그룹화"에 사용되는 글자거든요.

^제\d+조\(.+?\)

온점"."은 그냥 아무 글자 하나를 가리켜요. 조건식에서 가장 기본이 되는 문법 중 하나입니다.

^제\d+조\(.+?\)

온점.플러스+가 붙어있고, 괄호로 닫혀 있으니, "어떤 글자들 이후에 괄호가 닫히는 부분까지의 문자열을 찾아달라"는 요청이 되겠습니다.

참, 하나 빠진 게 있죠? 바로

^제\d+조\(.+?\)

플러스+ 뒤에 나오는 ?입니다. 또 유식한 말로 "탐욕알고리즘 해제"라고 부르기도 하는데,

닫는괄호도 조건식 문법 . 에 해당할 수 있기 때문에 원하지 않게 아래와 같은 문자열을 검색할 수도 있게 됩니다.

39(고유식별정보의 처리) 국토교통부장관(법 제11조제7항 및 제50조에 따라 국토교통부장관의 권한 및 업무를 위임ㆍ위탁받은 자를 포함한다)

위 문자열도 어쨌든 괄호로 끝났으니까요.

(조건식은 기본적으로 최대한 많은 문자열을 포함하는(탐욕적인greedy) 방식으로 검색하도록 설계되어 있습니다.)

조건식의 탐욕을 해제해 주는 게 바로 "?"입니다.

(그 외 다른 곳에서 쓰이는 "?"는 . 과 비슷하지만 약간 다르게 "어떤 글자가 있거나 없거나"라는 문법입니다.)

자, 그럼 본 예제에 사용된 모든 조건식의 설명을 마쳤습니다.

조건식을 사용해서 내가 원하는 문자열을 검색할 수 있는지 테스트해 보시면서 연습하시다 보면 정말 강력한 도구로 활용하실 수 있는 날이 분명 옵니다. 저는 요즘 데이터 전처리나 정제작업, CSV나 엑셀 파일 서식 정리, 파일명 검색이든 뭐든 안 쓰는 날이 거의 없습니다. 그럼 여기서 1번 과정의 설명을 마치겠습니다.

위 영상의 검색과정을 스크립트매크로로 녹화하면 아래와 같습니다.

// 스크립트 매크로 녹화한 코드(자바스크립트)

function OnScriptMacro_script5()
{
	HAction.GetDefault("RepeatFind", HParameterSet.HFindReplace.HSet);
	with (HParameterSet.HFindReplace)
	{
		ReplaceString = "제40조(";
		FindString = "^제\\d+조\\(.+?\\)";
		IgnoreReplaceString = 0;
		IgnoreFindString = 0;
		Direction = FindDir("Forward");
		WholeWordOnly = 0;
		UseWildCards = 0;
		SeveralWords = 0;
		AllWordForms = 0;
		MatchCase = 0;
		ReplaceMode = 0;
		ReplaceStyle = "";
		FindStyle = "";
		FindRegExp = 1;
		FindJaso = 0;
		HanjaFromHangul = 0;
		IgnoreMessage = 1;
		FindType = 1;
	}
	HAction.Execute("RepeatFind", HParameterSet.HFindReplace.HSet);
}

그리고 위 코드를 파이썬으로 옮겨서 함수로 만들면 아래와 같습니다. 큰 의미 없는 라인은 삭제하고, 한/글의 인스턴스명인 hwp를 모든 변수 앞에 붙여주는 간단한 작업을 거쳤습니다. 위와 아래의 코드를 비교해보시면 이해하시기 쉬울 겁니다.

# 파이썬으로 옮긴 코드

def find_word(target_word):
    hwp.HAction.GetDefault("RepeatFind", hwp.HParameterSet.HFindReplace.HSet)
    hwp.HParameterSet.HFindReplace.FindString = target_word # "^제\\d+조\\(.+?\\)"
    hwp.HParameterSet.HFindReplace.Direction = hwp.FindDir("Forward")
    hwp.HParameterSet.HFindReplace.FindRegExp = 1
    hwp.HParameterSet.HFindReplace.IgnoreMessage = 1
    hwp.HParameterSet.HFindReplace.FindType = 1
    hwp.HAction.Execute("RepeatFind", hwp.HParameterSet.HFindReplace.HSet)
    

그럼 다음 과정으로 넘어가보겠습니다.

 


 

사실 "찾아바꾸기"는 이전 여러 포스팅에서 자주 다룬 주제이지만, 그만큼 많이 활용되기도 하는 중요한 주제라고 생각됩니다. 간단히 다시 설명드리겠습니다.

스크립트 매크로 실행

1. Shift-Alt-H를 눌러서 스크립트매크로 슬롯을 선택하고 스크립트 녹화를 시작합니다.

"("를 "["로 찾아바꾸기 1번 실행

2. Ctrl-H를 눌러서 찾아바꾸기 창을 열고 "("를 "["로 바꾸기를 한 번 실행한 후, 매크로녹화를 종료합니다.(Shift-Alt-X)

* 이 때 찾는 문자열이 하나도 없다는 메시지가 자꾸 뜨면, (를 \(로 변경해서 검색하시거나, Ctrl-F를 눌러 조건식을 체크해제하고 한 번 검색을 실행하신 후 찾아바꾸기를 다시 실행하시면 문제가 해결됩니다.)

3. Shift-Alt-L을 열어서 해당 스크립트를 여러분의 에디터로 복사합니다. 제 경우는 아래와 같네요.

// 스크립트매크로 녹화 코드(자바스크립트)

function OnScriptMacro_script5()
{
	HAction.GetDefault("ExecReplace", HParameterSet.HFindReplace.HSet);
	with (HParameterSet.HFindReplace)
	{
		MatchCase = 0;
		AllWordForms = 0;
		SeveralWords = 0;
		UseWildCards = 0;
		WholeWordOnly = 0;
		AutoSpell = 1;
		Direction = FindDir("Forward");
		IgnoreFindString = 0;
		IgnoreReplaceString = 0;
		FindString = "(";
		ReplaceString = "[";
		ReplaceMode = 1;
		IgnoreMessage = 1;
		HanjaFromHangul = 0;
		FindJaso = 0;
		FindRegExp = 0;
		FindStyle = "";
		ReplaceStyle = "";
		FindType = 1;
	}
	HAction.Execute("ExecReplace", HParameterSet.HFindReplace.HSet);
	HAction.GetDefault("ExecReplace", HParameterSet.HFindReplace.HSet);
	with (HParameterSet.HFindReplace)
	{
		MatchCase = 0;
		AllWordForms = 0;
		SeveralWords = 0;
		UseWildCards = 0;
		WholeWordOnly = 0;
		AutoSpell = 1;
		Direction = FindDir("Forward");
		IgnoreFindString = 0;
		IgnoreReplaceString = 0;
		FindString = "(";
		ReplaceString = "[";
		ReplaceMode = 1;
		IgnoreMessage = 1;
		HanjaFromHangul = 0;
		FindJaso = 0;
		FindRegExp = 0;
		FindStyle = "";
		ReplaceStyle = "";
		FindType = 1;
	}
	HAction.Execute("ExecReplace", HParameterSet.HFindReplace.HSet);
}

실행해보시면 아시겠지만 클릭을 한 번 하면 (로 찾아가고, 한 번 더 눌러야 "["로 바뀌었죠. 그래서 자동화할 때에도 두 번 실행합니다. 위의 자바스크립트 코드를 파이썬 문법으로 고치고,  함수로 사용할 수 있게 조금 수정하면 아래와 같습니다. 속성값이 0이거나 ""인 것들은 대부분 뺐습니다.

# 파이썬 코드

def hwp_find_replace(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.HParameterSet.HFindReplace.FindType = 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.HParameterSet.HFindReplace.FindType = 1
    hwp.HAction.Execute("ExecReplace", hwp.HParameterSet.HFindReplace.HSet)
    hwp.Run("Cancel")

이제 파이썬으로 한/글을 열어놓고, hwp_find_replace("(", "[") 라고 실행하면 찾아바꾸기가 한 번 실행되는 거죠.

그럼 괄호를 찾아바꾸는 2번, 3번 과정도 설명을 마친 것 같네요.


4번 과정 : 반복하는 코드

몇 번 반복해야 할지 알아내는 코드는 복잡하다면 복잡하고, 단순하다면 또 단순합니다.

1. 그냥 넉넉하게 for문 돌린다. 어차피 검색해서 안나오면 바뀌는 것도 없으니까.

    for i in range(10000): 

2. hwp.GetTextFile("TEXT", "")와 str.count(세고 싶은 문자열) 메서드를 사용해서 for문을 돌린다.

    for i in range(hwp.GetTextFile("TEXT", "").count("\r\n제")):

3. hwp.GetText와 조건식을 활용해서 몇개조항인지 알아낸 다음 for문을 돌린다..... 흠;

더 많이 있을 것 같은데 막상 생각이 잘 안 나네요. 저는 무난하게 2번을 선택해서 진행해보겠습니다.

참고로 hwp.GetTextFile("TEXT", "")는 현재 열린 문서의 전체 텍스트를 한 번에 리턴해주는 유용한 메서드입니다.

그리고 "\r\n"은 줄바꿈(엔터)를 가리킵니다.

하여튼 반복하는 코드는 아래처럼 만들었습니다.

  for i in range(hwp.GetTextFile("TEXT", "").count("\r\n제")): 

코드 종합하기

위의 파이썬 코드를 모아다가 붙여넣고 돌려보았습니다.

몇 가지 오류가 있어 다소간 수정을 했는데, 궁금하신 분은 위의 코드와 최종코드를 비교해보시기 바랍니다.

소스코드 보여드리기 전에 코드 돌리는 영상 먼저 보여드립니다. 오류없이 실행되니 기분이 좋습니다.

 

파이썬으로 조항번호(조항명)을 조항번호[조항명]으로 일괄 서식변경하는 모습

파이썬으로 조항번호(조항명)을 조항번호[조항명]으로 일괄 서식변경하는 움짤

소스코드 전체는 아래에 공개해 두었습니다. 필요하다고 생각되시면 저장해 두셨다가 활용하시기 바랍니다.

from tkinter import Tk
from tkinter.filedialog import askopenfilename
import win32com.client as win32


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 find_word(target_word, direction="Forward"):
    hwp.HAction.GetDefault("RepeatFind", hwp.HParameterSet.HFindReplace.HSet)
    hwp.HParameterSet.HFindReplace.FindString = target_word  # "^제\\d+조\\(.+?\\)"
    hwp.HParameterSet.HFindReplace.Direction = hwp.FindDir(direction)  # "Forward" or "Backward"
    hwp.HParameterSet.HFindReplace.FindRegExp = 1
    hwp.HParameterSet.HFindReplace.IgnoreMessage = 1
    hwp.HParameterSet.HFindReplace.FindType = 1
    hwp.HAction.Execute("RepeatFind", hwp.HParameterSet.HFindReplace.HSet)
    hwp.HAction.Run("MoveLineBegin")


def hwp_find_replace(find_string, replace_string):
    hwp.HAction.Run("MoveLineBegin")
    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.HParameterSet.HFindReplace.FindType = 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.HParameterSet.HFindReplace.FindType = 1
    hwp.HAction.Execute("ExecReplace", hwp.HParameterSet.HFindReplace.HSet)
    hwp.Run("Cancel")
    hwp.HAction.Run("MoveLineBegin")


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

    hwp = hwp_init(filename=filename)
    for i in range(hwp.GetTextFile("TEXT", "").count("\r\n제")):
        find_word("^제\\d+조\\(.+?\\)", "Forward")
        current_position = hwp.GetPos()  # 현위치 저장(간혹 다음 검색위치로 튀는문제 조치)
        hwp_find_replace("(", "[")
        hwp.SetPos(*current_position)  # 방금위치 복원
        hwp_find_replace(")", "]")

* 아래에서 두 번째 줄 "*"은 리스트나 튜플 등의 배열자료를 언패킹하는 파이썬 문법입니다. 변수 current_position은 커서의 위치를 나타내는 세 개의 정수로 이뤄진 튜플이고, hwp.SetPos 메서드는 (튜플이 아닌) 세 개의 정수 파라미터를 인자로 받거든요.

재미있게 보셨거나 도움이 되셨으면 좋겠습니다. 나름대로 여러가지 노하우가 녹아 있으니, 아래아한글 자동화에 관심이 많으신 분이라면 한 번쯤은 따라해 보셔도 좋을 튜토리얼이라고 생각됩니다.

이번 포스팅은 여기서 마칩니다.

최근들어, 문의주시는 내용을 바탕으로 포스팅을 제작하고 있습니다. 제 기준으로 콘텐츠를 만드니까 의욕도 잘 생기지 않고, 가끔은 너무 매니악(?)해지기도 하는 경향이 있더라고요. 여러분 궁금하시거나 필요하신 자동화 테마 관련해서 자유롭게 의견이나 문의 주시면, 이렇게 포스팅으로 회신 드리겠습니다.

감사합니다.

행복한 하루 되세요^^

댓글