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

[교육업무자동화5/7] 청구서자동화5(개선할 부분 찾아보기) #중급

by 일코 2021. 1. 25.

지난 포스팅은...

 

[교육업무자동화4/10] 청구서자동화4(청구서 마무리하기)

지난 포스팅은... [교육업무자동화3/10] 청구서자동화3(파이썬으로 엑셀 열어서 값 얻기) 지난 포스팅은... [교육업무자동화2/10] 청구서자동화2(필드에 텍스트 자동입력) 지난 포스팅은... [교육업

www.martinii.fun

 


 

이번 포스팅에서는 다른 사용자들과 공유할 수 있게 코드를 pyinstaller로 컴파일하기 전에 
보다 일반적인 상황에서도 코드가 실행될 수 있게, 예외처리 및 보완을 해볼 예정이다.
각종 오류를 방지하기 위해 파이썬 문법이 다소 가미되며,
회사원 수준에서는 "어렵다"고 느낄 수 있다.
아래의 코딩 과정을 이해하는 것이 필수는 아니며,
완성된 코드가 이런 식으로 동작하는구나 하는 맥락만 짚고 넘어가도 된다.

업무용 컴퓨터에 파이썬과 코딩전용 에디터, 혹은 IDE가 설치되어 있고, py 파일을 익숙하게 다루는 독자라면
소스만 받아서 필요없는 부분을 지우고 실행하거나, 특정 기능을 입맛대로 수정하는 데 큰 어려움이 없을 것이다.

하지만 대부분의 "일반" 회사원들은 파이썬코드를 쉽게 이해하지 못하며, 
앞서 우리가 작성한 (가칭)엑셀-한/글 필드작성 자동화 파일(?) 또한 사용할 줄 모르는 것이 당연하다.

이럴 땐 지난 포스팅까지 열심히 작성했던 스크립트에서 문제가 발생한다.
그래서 지금까지 작성한 코드를 exe파일로 (컴파일해서) 배포하기 전에,
(어느 정도 코드를 이해할 수 있는) 우리들의 손으로
여러가지 특수한 경우를 어우를 수 있는 "일반화" 작업을 상당부분 해 줘야 한다.
그렇지 않으면 배포를 하더라도 여러가지 문제가 발생할 수 있다.
(당연히 내 컴퓨터에서 나만 사용하겠다면 이런 작업을 하지 않아도 되며, 사실 모든 예외처리를 해줄 수는 없는 노릇)

예를 들면,

1. 남의 컴퓨터인 만큼 사용자 이름이 바뀌어 있어서, 경로 오류 때문에 프로그램이 실행되지 않는다.
2. 데이터소스가 되는 엑셀파일의 이름을 바꾸고 싶은데, 바꾸면 오류가 난다.
3.
복사할 페이지 수가 (100장이 아니고,) 200장이든 35장이든 엑셀의 행 수만큼만 생성하고 싶다면?
4. 필드명이나 필드갯수가 바뀐다면? 엑셀파일의 열순서가 바뀐다면?

남의 컴퓨터에서, 혹은 여러 가지 조건에서도 오류가 나지 않고 실행되게 하려면?
필자 기준으로는 아래 세 가지(+1) 조건은 꼭 배려해줘야 한다고 생각한다.

1. 경로문제

일반적으로 동료들과 메일을 통해 파일을 주고받는다고 가정해보자.
저장하고 압축을 푸는 폴더는 아마 "다운로드" 혹은 "바탕화면"일 것이다.
혹은 바탕화면에 파일을 많이 두기 싫어하는 사용자들은 특정 폴더를 생성해서 그 안에 집어넣을텐데
현재 우리의 코드는 경로가 모두 "c:\users\사용자명\desktop\특정경로" 형식이기 때문에 오류가 발생할 것이다.

그러면 이 문제를 어떻게 해결하면 좋을까? 여러 가지 방법이 있겠지만,

필자의 경우는 절대경로가 들어가는 모든 부분을 os.getcwd()나 "%userprofile%" 등으로 대체하겠다.

2. 파일명 문제

파일명을 입맛대로 바꾸고 싶어하는 사람도 있을 것이다. (현재 코딩으로는 파일명을 바꾸면 무조건 오류가 뜬다.)

이건 어떻게 하면 좋을까?

필자의 경우는 tkinter 모듈을 통해서 GUI를 붙여놓는 것을 추천한다.
다만, 실행할 때마다 파일선택창이 뜨고, 매번 동일한 한/글파일이나 엑셀파일을 선택하는 것은 귀찮으므로,
최근 실행된 한/글과 엑셀파일명을 저장해 두는 일종의 캐시를 생성해두고,
파일명이 바뀌었을 때에만 오류메시지 대신 GUI를 띄워
파일을 재선택할 수 있게 try~except문이나 조건문을 사용하면 좋을 것이다.

3. 엑셀 행의 갯수가 바뀌는 문제

앞서 작성한 코드에서는 100명을 가정하고 예제코드를 작성했다.
만약 청구서를 받을 사람이 20명 밖에 아니라면? 혹은 2,000명이라면?
한/글 문서 페이지의 갯수를 엑셀 행 갯수에 따라 바뀌게 하는 방법을 아래에 설명하고자 한다.

4. 누름틀 이름이나 엑셀 칼럼순서가 바뀌는 경우

(이 부분은 다소 심화내용이므로 자세히 설명하지 않겠다. 이해되지 않으면 넘어가도 된다.)
다행히 우리가 작성한 코드는 누름틀의 이름이 바뀌더라도 문제가 발생하지 않는다.
왜냐면 hwp.GetFieldList()를 통해서 문서 내 필드의 이름을 매번 가져오기 때문이다.
한/글 문서의 필드명과 순서에 맞게 엑셀파일이 만들어져 있기만 하면,
다른 어떤 문서라도 이 프로그램에 집어넣고 돌릴 수 있다는 뜻이다.

그렇다고 하더라도, 한/글의 필드순서에 맞게 엑셀시트를 수정는 것은 번거로울 수 있다.
왜냐면, 일반적인 업무순서상, 데이터인 엑셀이 먼저 만들어지고 나서, 출력물인 한/글 보고서가 만들어지니까.

그래서 엑셀 칼럼이 어떤 순서로 짜여 있더라도, 한/글의 필드명과 1:1로 매치되기만 하면
오류없이 한/글 보고서 뭉치가 만들어질 수 있게 코드를 수정해 보겠다.

위의 네 가지 내용을 한 포스팅에 담는 것이 다소 부담스럽지만,
네 개로 쪼개서 읽는 것보다 한큐에 가는 것이 직관적이라고 생각되어 끊지 않고 갈 생각이다.

중간중간에 계속 전체코드를 넣어둘 예정이니,
집중력이 흐트러질 때 잠시 멈추고 전체 코드를 한 번 리뷰해보는 것을 추천한다.

 


 

수정할 코드는 직전에 작성한 "한 장의 한/글 누름틀 템플릿을 사용하여 100페이지 한/글문서 만들기"로 하겠다.
앞의 포스팅을 보면서 따라서 입력해 왔겠지만, 그렇지 않더라도 지금 아래에 입력된 코드를 통해
이번 포스팅부터 시작해도 무관하다. 또한, "PDF개별저장"이나, "HWP개별저장" 코드도 이와 유사하므로
다른 버전의 스크립트 수정은 독자들이 각자 도전해 보기로 하자.

기존의 코드

import win32com.client as win32


hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "SecurityModule")  # 추가
hwp.Open("C:\\Users\\smj02\\Desktop\\청구서+누름틀.hwp")
field_list = hwp.GetFieldList().split("\x02")

hwp.Run("CopyPage")       # 추가
for i in range(99):       # 추가
    hwp.Run("PastePage")  # 추가
    print(i+2)            # 추가

excel = win32.gencache.EnsureDispatch("Excel.Application")
wb = excel.Workbooks.Open(r"C:\Users\smj02\Desktop\교육생리스트#마티니한잔.xlsx")
ws = wb.Worksheets(1)

for 행번호 in range(2, 102):
    text_list = ws.Range(ws.Cells(행번호, 1),
                         ws.Cells(행번호, 6)).Value[0]
    for 인덱스, 필드명 in enumerate(field_list):
        hwp.PutFieldText(필드명+f"{{{{{행번호-2}}}}}", text_list[인덱스])  # 수정
        if 필드명 == "이름":
            hwp.MoveToField(필드명+f"{{{{{행번호-2}}}}}")

hwp.XHwpWindows.Item(0).Visible = True
excel.Visible = True

 


1. 경로문제 수정(불러올 파일경로) 해결방법

이번 포스팅 전반에 걸쳐 수정하는 부분은 한/글 API보다는 파이썬의 기본문법과 연관된 부분이 많을 것으로 생각된다.
그 점을 감안하고, 이해되지 않는다면 무리하지 말고 그냥 넘어가도 괜찮다.
"그런 게 있나보구나" 정도만 짚고 가도 된다.

이 챕터에서 수정할 부분은 6, 15번 라인이다.

6번 라인 : hwp.Open("C:\\Users\\smj02\\Desktop\\청구서+누름틀.hwp")
15번 라인 : wb = excel.Workbooks.Open(r"C:\Users\smj02\Desktop\교육생리스트#마티니한잔.xlsx")

일반적으로 바탕화면 이하 폴더의 경로는 중간에 사용자이름이 들어가기 때문에,
하드코딩(전체경로를 코드에 그대로 써놓는 방식)은 오류가 발생하기 마련이다.
(파일이름이 바뀌는 문제는 챕터2에서 다루겠다.)

위에서도 이야기했지만, 해결방법은 의외로 간단하며 또 여러 가지 해법이 있다.

 


 

1-1. os.getcwd()를 사용하는 방법

실행파일과, 한/글파일(청구서), 엑셀파일(소스)을 동일한 폴더 안에 넣어두기만 하면,
"바탕화면"이든 "내 문서"든 어느 폴더에 저장하든지 실행되는 방식이다.

참고로 os.getcwd()는 "현재 디렉토리"를 리턴하며,
기본적으로 현재 콘솔이 실행되고 있는 폴더를 가리킨다.
언제든 변경 가능하며, 파일탐색기에서 폴더 옮겨다니듯이,
os.chdir(경로)를 통해 변경한 후 다시 os.getcwd()를 하면 현재 콘솔이 위치한
폴더경로를 리턴한다.

커맨드프롬프트창에서 파이썬 실행 후 os 내장모듈을 이용한 폴더 탐색

os.getcwd()가, 실행파일이 위치한 폴더를 리턴한다는 점을 이용해서 위의 코드를 다음과 같이 바꿔볼 수 있다.

import os  # 추가
import win32com.client as win32


hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "SecurityModule")
hwp.Open(os.path.join(os.getcwd(), "청구서+누름틀.hwp"))  # 변경
field_list = hwp.GetFieldList().split("\x02")

hwp.Run("CopyPage")
for i in range(99):
    hwp.Run("PastePage")
    print(i+2)

excel = win32.gencache.EnsureDispatch("Excel.Application")
wb = excel.Workbooks.Open(os.path.join(os.getcwd(), "교육생리스트#마티니한잔.xlsx"))  # 변경
ws = wb.Worksheets(1)

for 행번호 in range(2, 102):
    text_list = ws.Range(ws.Cells(행번호, 1),
                         ws.Cells(행번호, 6)).Value[0]
    for 인덱스, 필드명 in enumerate(field_list):
        hwp.PutFieldText(필드명+f"{{{{{행번호-2}}}}}", text_list[인덱스])
        if 필드명 == "이름":
            hwp.MoveToField(필드명+f"{{{{{행번호-2}}}}}")

hwp.XHwpWindows.Item(0).Visible = True
excel.Visible = True

1번, 7번, 그리고 16번 라인이 변경되었다.

참고로 위에서 사용한 os.path.join 메서드는 경로와 경로를 붙이거나, 경로와 파일을 붙이는 데 사용된다.
'그냥 "+" 연산자를 사용해서 붙이면 더 편하지 않을까?' 생각이 들 수도 있다.
필자 경험상, "\\"를 붙이거나 "/"를 붙이는 걸 쉽게 깜빡할 수 있었고, OS종속성에도 매이지 않기 때문에,
os 모듈이나 (본 튜토리얼엔 소개하지 않았지만) pathlib 모듈을 사용하는 것을 추천한다.

깔끔하게 폴더이름과 파일명을 붙일 수 있다.

 


 

1-2. (참고) 시스템 환경변수를 사용하는 방법

이 경우는 다소 제한적이긴 하지만, 코딩을 하다 보면 분명 언젠가는 쓰게 될 날이 올 것이다.
윈도우의 사용자 이름을 모른 채로, "바탕화면" 또는 "다운로드" 등의 폴더에 접근하는 방법이 있을까?

당연히 있다. 그것도 아주 간단하다.

시스템의 환경변수 중 "userprofile"을 이용하는 방법인데,
사용자 이름을 모르더라도 아래처럼 사용하면 된다.

시스템 환경변수 "userprofile"을 이용한 바탕화면 접근

참고로 커맨드프롬프트에서도 다음과 같이 좌우로 "%"를 붙여서 환경변수를 사용할 수 있다.

커맨드프롬프트에서 환경변수 사용방법

본 튜토리얼에서는 환경변수를 사용하지는 않겠지만;;
환경변수는 컴퓨터를 다루다 보면 꼭 유용하게 쓰일 수 있고, 모르면 문제가 되는 경우도 많아 소개해 보았다.

 


 

2. 파일명이 바뀌는 문제 해결방법

폴더는 어찌어찌 os모듈을 통해서 오류를 줄일 수 있다지만,
이 프로그램을 공유한 사용자가 파일명을 임의로 바꿔버리면 어떻게 될까?
현재 코드 안에는 파일명이 버젓이 적혀 있어서, 파일명을 한 글자만 바꿔도 오류가 튀어나온다.
이것도 몇 가지 방법론이 있지만, 필자는 경험상 아래 두 가지 방법을 선호하게 되었다.
(둘 중에서도 선호하는 건 1번이다.)

1. 실행파일이 들어있는 폴더 안에 한/글 파일과 엑셀파일이 하나씩 들어있으면
파일명이 어떻든 각각 보고서파일과 데이터파일인 것으로 간주하는 방법
(파일이 없거나 그렇지 않은 경우 파일선택 GUI 표시)

2. 최초 실행시 GUI를 띄워서 hwp, xlsx파일을 선택하고, 별도로 파일경로를 저장해놓는 방법
(파일이 없어진 경우 파일선택 GUI 표시)

1번과 2번 다 코드는 비슷할 것이므로 필자는 1번을 예제로 보여드리겠다.
GUI는 의외로 코드가 길어지거나 배포시 용량이 커질 수 있어,
코드를 최소화하고 내장모듈인 tkinter를 사용할 예정이다.

아래처럼 코드를 수정했다.

import os
from tkinter import Tk  # 추가
from tkinter.filedialog import askopenfilename  # 추가
import win32com.client as win32

BASE_DIR = os.getcwd()  # 추가
엑셀파일_리스트 = [i for i in os.listdir() if any((i.endswith(".xlsx"), i.endswith(".xls")))]  # 추가
if len(엑셀파일_리스트) == 1:  # 추가
    엑셀파일 = 엑셀파일_리스트[0]  # 추가
else:  # 추가
    root = Tk()  # 추가
    엑셀파일 = os.path.basename(
        askopenfilename(title="엑셀 또는 CSV 파일을 선택해 주세요.", initialdir=BASE_DIR,
                        filetypes=[("스프레드시트", "*.xlsx *.xls *.csv")]))  # 추가
    root.destroy()  # 추가
    
한글파일_리스트 = [i for i in os.listdir() if i.endswith(".hwp")]  # 추가
if len(한글파일_리스트) == 1:  # 추가
    한글파일 = 한글파일_리스트[0]  # 추가
else:  # 추가
    root = Tk()  # 추가
    한글파일 = os.path.basename(
        askopenfilename(title="HWP 파일을 선택해 주세요.", initialdir=BASE_DIR,
                        filetypes=[("아래아한글", "*.hwp")]))  # 추가
    root.destroy()  # 추가


hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "SecurityModule")
hwp.Open(os.path.join(os.getcwd(), 한글파일))  # 변경
field_list = hwp.GetFieldList().split("\x02")

hwp.Run("CopyPage")
for i in range(99):
    hwp.Run("PastePage")
    print(i+2)

excel = win32.gencache.EnsureDispatch("Excel.Application")
wb = excel.Workbooks.Open(os.path.join(os.getcwd(), 엑셀파일))  # 변경
ws = wb.Worksheets(1)

for 행번호 in range(2, 102):
    text_list = ws.Range(ws.Cells(행번호, 1),
                         ws.Cells(행번호, 6)).Value[0]
    for 인덱스, 필드명 in enumerate(field_list):
        hwp.PutFieldText(필드명+f"{{{{{행번호-2}}}}}", text_list[인덱스])
        if 인덱스 == field_list[-1]:
            hwp.MoveToField(필드명+f"{{{{{행번호-2}}}}}")  # 변경

hwp.XHwpWindows.Item(0).Visible = True
excel.Visible = True

2~3번 라인에 임포트문, 6~25번 라인에 tkinter 코드를 추가했으며,
30, 39번 라인에 파일이름을 하드코딩에서 변수로 바꿨다.
48번 라인의 필드명도 하드코딩에서 "페이지 마지막 필드"를 가리키는 변수로 바꿨다.

폴더 안에 한/글파일, 엑셀파일을 집어넣고, 이름도 다 바꿨는데,
오류 없이 정상적으로 파일명을 가져왔다.

한/글과 엑셀파일이 두 개 이상 있는 경우에는 어떻게 될까?

캡쳐화면과 같이 각각의 파일을 하나씩 복사해놓았다.
파일이 두 개 이상 있는 경우에는 파일선택창이 뜬다.

코드 안에 절대경로나 파일명을 집어넣는 코딩을 "하드코딩"이라고 하며,
이는 프로그램 설계시 지양해야 할 점으로 꼽힌다.
왜냐면, 파일명 또는 경로 등이 변경된 경우 하드코딩된 라인을 모두 찾아가 직접 변경해줘야 하기 때문이다.

이렇게 경로와 파일명 관련 하드코딩된 부분을 해결했다.
이제 프로그램 경로를 어디 두든지, 또 파일명을 어떻게 바꾸든지 오류가 나지는 않게 되었다.

 


 

3. 엑셀 행의 갯수가 바뀌는 경우

여기 해당하는 라인은 2번챕터 내용까지 수정한 최근 코드 기준으로, 34, 42번 라인이다.

34번라인 : for i in range(99):
42번라인 : for 행번호 in range(2, 102):

둘 다 for문을 사용한 반복문이다.

엑셀파일에 입력된 행의 갯수를 구할 수 있다면, 문제를 해결할 수 있을 것이다.
그리고, 행의 갯수는 간단히 아래의 코드로 구할 수 있다.

len(ws.UsedRange())

현재 엑셀파일의 UsedRange의 행 갯수는 101이다.

데이터행의 갯수는 위의 101에서 제목행 1을 뺀 100임을 알 수 있다.

data_len = len(ws.UsedRange()) - 1

그런데, 이런 경우를 생각해보자.
데이터가 든 스프레드시트 아랫쪽으로 주석을 달았거나,
아래처럼 하단에 불필요한 텍스트가 있는 경우 오류가 날 수 있다.

이럴 가능성은 정말 희박하지만;;;
이런 경우엔 101이 아닌 109를 리턴한다.

이렇게 공백 뒤로 텍스트가 있어 UsedRange와 실제 데이터의 길이가 다르다면 어떻게 해야 할까?
여러 방법이 있겠지만, 간단히 while문을 통해 해결할 수 있다.

row_len = 0
while True:
    if ws.Cells(row_len+2, 1).Value:  # 1열이 비어있지 않다면
        row_len += 1
    else:
        break

실행해보면,

약간의 트윅이 섞여 있다. (row_len을 1이 아닌 0으로 놓고, if문 안에 2를 더해놓았다.)

row_len이 109가 아닌 100을 리턴한다.

일반적인 경우 ws.UsedRange를 쓰면 되고,
만의하나 위와 같은 오류(표 아래 주석)가 발생한 경우 주석을 지우면 된다.
부득이하게 주석을 지워선 안 되는 경우에는 while문으로 짠 row_len을 사용해보자.
본 튜토리얼에서는 ws.UsedRange를 사용한다.

이를 적용한 코드는 아래와 같다.

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

BASE_DIR = os.getcwd()  
엑셀파일_리스트 = [i for i in os.listdir() if any((i.endswith(".xlsx"), i.endswith(".xls")))]  
if len(엑셀파일_리스트) == 1:  
    엑셀파일 = 엑셀파일_리스트[0]  
else:  
    root = Tk()  
    엑셀파일 = os.path.basename(
        askopenfilename(title="엑셀 또는 CSV 파일을 선택해 주세요.", initialdir=BASE_DIR,
                        filetypes=[("스프레드시트", "*.xlsx *.xls *.csv")]))  
    root.destroy()  

한글파일_리스트 = [i for i in os.listdir() if i.endswith(".hwp")]  
if len(한글파일_리스트) == 1:  
    한글파일 = 한글파일_리스트[0]  
else:  
    root = Tk()  
    한글파일 = os.path.basename(
        askopenfilename(title="HWP 파일을 선택해 주세요.", initialdir=BASE_DIR,
                        filetypes=[("아래아한글", "*.hwp")]))  
    root.destroy()  

if not all([엑셀파일, 한글파일]):
    raise FileExistsError("파일이 존재하지 않습니다.")

hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "SecurityModule")
hwp.Open(os.path.join(BASE_DIR, 한글파일))  
field_list = hwp.GetFieldList().split("\x02")

excel = win32.gencache.EnsureDispatch("Excel.Application")
wb = excel.Workbooks.Open(os.path.join(BASE_DIR, 엑셀파일))  
ws = wb.Worksheets(1)

row_len = len(ws.UsedRange())-1

hwp.Run("CopyPage")
for i in range(row_len-1):  # 수정
    hwp.Run("PastePage")
    print(i)

for 행번호 in range(2, row_len + 1):  # 수정
    text_list = ws.Range(ws.Cells(행번호, 1),
                         ws.Cells(행번호, 6)).Value[0]
    for 인덱스, 필드명 in enumerate(field_list):
        hwp.PutFieldText(필드명+f"{{{{{행번호-2}}}}}", text_list[인덱스])
        if 인덱스 == field_list[-1]:
            hwp.MoveToField(필드명+f"{{{{{행번호-2}}}}}")  

excel.Visible = True
hwp.XHwpWindows.Item(0).Visible = True

이런 방법을 통해서 여러가지 오류가능성을 줄이는 것처럼 보이기도 하지만;;;
이런 생각 안 드는가?

"이런 거 뚝딱거릴 시간에 차근차근 타이핑했으면 작업도 다 끝냈겠다..."

맞다. 필자도 항상 이 정도 타이밍이면 이런 고민을 항상, 정말 항상 한다.
그래서, 정말 오래오래 활용할 스크립트가 아니라면, 다소 불안정해도 그냥 불안정한 대로 돌려버린다.
(사실 지금 필자도 뭐하는 짓거린가 싶다...)

그저, 오류가 발생할 떄 어떻게 대처해야 하는지,
혹은 어떤 이유로 오류가 발생하는지만이라도 알아둔다면,
마주치지 못한 오류를 해결하는데 수많은 시간을 검색하고 디버깅하는 시간을 줄일 수 있을 것이다.
그런 의미에서, 조금만 더 힘내자.
3번 챕터까지 마쳤고 이제 마지막 4번 챕터, 필드 의존성 문제를 해결하러 가보자.

 


 

4. 필드명, 필드갯수나 엑셀 칼럼순서가 바뀌는 경우 해결방법

불행중 다행으로 현재 우리의 코드에 필드명 의존성은 없는 상태다. (필드명은 하드코딩되어 있지 않다.)
그러나 칼럼 개수가 하드코딩되어 있는데, 어느 라인이냐면, 52~53번 라인이다.

text_list = ws.Range(ws.Cells(행번호, 1),
                     ws.Cells(행번호, 6)).Value[0]

그리고 한 가지 더 걸고 넘어지자면,
현재 코드는 순전히 엑셀파일의 칼럼 순서에 의존한다.
2번 열과 3번 열이 바뀐다면?
그러면 한/글 문서도 바로 깨진다.
이건 어떻게 해결하면 좋을까?
가장 트렌디한 방법은 역시 엑셀파일을 판다스 데이터프레임으로 불러와서 df의 메서드를 사용하는 방법이지만
배포시 용량도 커지고, 닭 잡는 데 소 잡는 칼을 쓰는 격이다.
필자가 가장 애용하는 방법은 각 행을 "사전"자료형으로 바꾼 리스트를 생성하는 것이다.
아래 코드를 보자.

used_range = ws.UsedRange()
title_list = used_range[0]
content_list = used_range[1:]

dict_list = []
for i in content_list:
    dict_list.append(dict(zip(title_list, i)))

실행결과는 아래와 같다.

행 갯수 길이의 리스트가 만들어졌으며, 각각의 원소는 "사전"타입이다.

hwp보고서 1쪽에 들어갈 데이터를 호출하려면 아래와 같은 방식으로 처리하면 될 것이다.

for n in dict_list:
	for i in field_list:
	    hwp.PutFieldText(i, n[i])

이런 식으로 코드를 변경한 최종코드는 아래와 같다.

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

BASE_DIR = os.getcwd()  
엑셀파일_리스트 = [i for i in os.listdir() if any((i.endswith(".xlsx"), i.endswith(".xls")))]  
if len(엑셀파일_리스트) == 1:  
    엑셀파일 = 엑셀파일_리스트[0]  
else:  
    root = Tk()  
    엑셀파일 = os.path.basename(
        askopenfilename(title="엑셀 또는 CSV 파일을 선택해 주세요.", initialdir=BASE_DIR,
                        filetypes=[("스프레드시트", "*.xlsx *.xls *.csv")]))  
    root.destroy()  

한글파일_리스트 = [i for i in os.listdir() if i.endswith(".hwp")]  
if len(한글파일_리스트) == 1:  
    한글파일 = 한글파일_리스트[0]  
else:  
    root = Tk()  
    한글파일 = os.path.basename(
        askopenfilename(title="HWP 파일을 선택해 주세요.", initialdir=BASE_DIR,
                        filetypes=[("아래아한글", "*.hwp")]))  
    root.destroy()  

if not all([엑셀파일, 한글파일]):
    raise FileExistsError("파일이 존재하지 않습니다.")

hwp = win32.gencache.EnsureDispatch("HWPFrame.HwpObject")
hwp.RegisterModule("FilePathCheckDLL", "SecurityModule")
hwp.Open(os.path.join(BASE_DIR, 한글파일))  
field_list = hwp.GetFieldList().split("\x02")

excel = win32.gencache.EnsureDispatch("Excel.Application")
wb = excel.Workbooks.Open(os.path.join(BASE_DIR, 엑셀파일))  
ws = wb.Worksheets(1)

hwp.XHwpWindows.Item(0).Visible = True
excel.Visible = True

used_range = ws.UsedRange()
title_list = used_range[0]
content_list = used_range[1:]

dict_list = []
for i in content_list:
    dict_list.append(dict(zip(title_list, i)))

hwp.Run("CopyPage")
for i in range(len(dict_list) - 1):
    hwp.Run("PastePage")
    print(i+2)  # 1페이지는 이미 있고, 2페이지부터 만들기 시작하는데, 인덱스는 0부터 시작하니까 "+2"

if not sorted(field_list) == sorted(list(title_list)):
    raise KeyError("불러온 한/글 파일과 엑셀파일의 필드목록이 다릅니다.")

for 행번호 in range(len(dict_list)):
    for 인덱스, 필드명 in enumerate(field_list):
        hwp.PutFieldText(필드명+f"{{{{{행번호}}}}}", dict_list[행번호][필드명])
        if 필드명 == field_list[-1]:
            hwp.MoveToField(필드명+f"{{{{{행번호}}}}}")

실행화면은 아래와 같다.

최종 코드 실행화면. 엑셀의 칼럼순서를 바꿔도 오류가 나지 않는다.

 


정리

기나긴 과정을 통해 보완한 내용을 네 줄로 정리해보자.

1. 어느 폴더에 넣어도, 오류없이 실행될 수 있게 하드코딩한 부분을 배제했다.

2. 한/글 및 엑셀파일의 이름이 바뀌어도 실행되며, 특정할 수 없는 경우 GUI가 뜬다.

3. 엑셀 행의 갯수에 따라 한/글파일 쪽 복사를 실행하도록 변경했다.

4. 엑셀 칼럼명과 한/글 필드명이 동일하다면 칼럼순서가 달라도 문서가 잘 완성된다.

 


 

다음 포스팅에는 드디어 pyinstaller를 통한 컴파일 및 배포방법에 대해 알아보겠다.
이번 포스팅은 다소 길었으나, 문제별 해결방법이나, 코딩 컨벤션을 알아두면
앞으로도 자주 쓸모가 있을 것이다.

긴 글 읽어주셔서 감사드리며
행복한 하루 되시길~

 


 

다음 포스팅은...

 

[교육업무자동화6/10] 청구서자동화6(pyinstaller로 배포하기)

지난 포스팅은... [교육업무자동화5/10] 청구서자동화5(개선할 부분 찾아보기) #중급 지난 포스팅은... [교육업무자동화4/10] 청구서자동화4(청구서 마무리하기) 지난 포스팅은... [교육업무자동화3/1

www.martinii.fun

 

댓글