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

2-4. 엑셀문서 값을 필드에 입력하기

by 회사원코딩 2022. 11. 3.

지난 포스팅에서는 특정 문자열을 한/글 문서의 필드에 입력하는 과정까지

코드로 진행해보았습니다.

그런데 업무자동화 코드를 짜다 보면,

직접 문자열을 타이핑해서 입력하기보다는

엑셀파일이나 다른 한/글 파일에서 값을 읽어

다른 한/글 보고서에 입력하는 방식이 대부분입니다.

그래서 이번 포스팅에서는 간단한 엑셀파일을 읽어와서 

한/글 문서에 삽입하는 과정을 보여드리겠습니다.

이번에는 기본기능을 소개하는 포스팅이 아니라,

광범위하게 실무에 사용 가능한 프로세스를 소개드리므로

다소 포스팅이 길게 느껴지실 수 있습니다.

찬찬히 코드를 읽어보시면서 따라해 보시길 추천드립니다.

우선 한/글 파일은 이전 포스팅에서 사용한 "누름틀필드.hwp"를 사용하겠습니다.

누름틀필드.hwp
0.02MB

그리고 엑셀 파일은 아래와 같이 "취미.xlsx"를 작성하여 저장했습니다.

취미.xlsx
0.01MB

잠시 엑셀파일을 다루는 기본적인 방법 몇 가지를 조금 자세하게 설명드리겠습니다.

별도의 포스팅으로 남길까 했는데, 흐름이 끊기는 것 같아 이 포스팅에 같이 적습니다.

 

엑셀파일에서 한/글 문서로 데이터를 옮기는 방법은 다양하지만,

가장 기초적인(가장 간단하거나 쉽다는 뜻은 아님ㅜ) 방법부터 먼저 알아보겠습니다.

엑셀파일 데이터도 한/글 파일과 마찬가지로 win32com으로 불러오겠습니다.

# 모듈 임포트
import win32com.client as win32

# 엑셀 프로그램 열기
excel = win32.gencache.EnsureDispatch("Excel.Application")

# 엑셀 프로그램 백그라운드 해제
excel.Visible = True

# 워크북(엑셀파일) 열기
wb = excel.Workbooks.Open(r"c:\users\smj02\desktop\취미.xlsx")

# 1번 워크시트 접근
ws = wb.Worksheets(1)

# A1셀 값 출력
print(ws.Cells(1, 1).Value)

첫 번째 셀의 마지막 라인인 "excel.Visible = True" 명령어는 (짐작하셨겠지만)
한/글의 "hwp.XHwpWindows.Item(0).Visible = True"와 동일하게 백그라운드상태를 해제합니다.

엑셀파일에서 한/글로 데이터를 옮길 때

어떤 분들은 데이터 전체를 파이썬의 인기있는 자료형(dict, list, pandas.DataFrame 등)으로 가져오는가 하면

또 어떤 분들은 셀 하나씩 읽어 한/글 필드에 넣는 방식을 선호하기도 하는 것 같습니다.  각각 장단점이 있겠지요.

이 포스팅에서는 두 가지 다 보여드리겠습니다. 먼저,

1. 워크시트의 셀을 하나씩 불러오기 : ws.Cells

row = 2

print(ws.Cells(row, 1).Value)
# >>> 마크

print(ws.Cells(row, 2).Value)
# >>> 남

print(ws.Cells(row, 3).Value)
# >>> 1984-05-14 00:00:00+00:00

print(ws.Cells(row, 4).Value)
# >>>VR

C2(2행3열) 값이 "1984년 5월 14일"이 아닌  "1984-05-14 00:00:00+00:00"로 출력이 됩니다.

다행히 type(ws.Cells(row, 3).Value) 명령어로 타입을 확인해보니

단순한 문자열이 아닌  pywintypes.datetime 자료형입니다.

strftime 메서드를 통해 원하는 형식의 문자열로 간편하게 변환이 됩니다.

type(ws.Cells(row, 3).Value)
# >>> pywintypes.datetime

print(ws.Cells(row, 3).Value.strftime("%Y년 %#m월 %#d일"))
# >>> 1984년 5월 14일

여기까지 테스트한 내용을 바탕으로 한 행씩 출력을 해 본 후

for문을 통해 모든 행을 출력해보겠습니다.

그 전에 지금까지 사용했던 ws.Cells 대신

여러 셀을 출력하기 위한 ws.Range메서드를 소개합니다.

2. 특정 범위를 지정해 한 번에 불러오기 : ws.Range

사용법은 아래와 같습니다.

# 문자열("A2:D2")로 직접 입력하는 방법
ws.Range("A2:D2").Value

# >>> (('마크',
# ...  '남',
# ...  pywintypes.datetime(1984, 5, 14, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  'VR'),)

# ws.Cells로 범위를 입력하는 방법
row = 2
ws.Range(ws.Cells(row,1), ws.Cells(row,4)).Value

# >>> (('마크',
# ...  '남',
# ...  pywintypes.datetime(1984, 5, 14, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  'VR'),)

입력방법이 다르지만 출력값은 동일한 것을 확인할 수 있습니다.

코딩을 간편하게 하기 위해서는 다소 코드가 복잡해 보이더라도

후자의 방법을 사용하는 것이 좋겠습니다. 변수를 사용할 수 있으니까요.

row = 2

data = list(ws.Range(ws.Cells(row,1), ws.Cells(row,4)).Value[0])
data[2] = data[2].strftime("%Y년 %#m월 %#d일")
print(data)
# >>> ['마크', '남', '1984년 5월 14일', 'VR']
위 코드의 두 번째 라인을 보면, 리턴되는 값을 리스트로 변환하는 코드가 들어 있습니다.
기본적으로 ws.Cells나 ws.Range가 리턴하는 값은 튜플인데,
이 중 한 값을 수정하기 위해 튜플을 수정 가능한 리스트 자료형으로 변환을 해 주었습니다.
for row in range(2, 8):  # 2행부터 (8-1)행까지
    data = list(
        ws.Range(ws.Cells(row,1),
                 ws.Cells(row, 4)).Value[0]
    )
    data[2] = data[2].strftime("%Y년 %#m월 %#d일")
    print(data)
# >>> ['마크', '남', '1984년 5월 14일', 'VR']
# ... ['빌', '남', '1955년 10월 28일', '기부']
# ... ['일론', '남', '1971년 6월 28일', '트위터']
# ... ['제프', '남', '1964년 1월 12일', '독서']
# ... ['리사', '여', '1969년 11월 7일', '게임']
# ... ['슬아', '여', '1983년 6월 16일', '쇼핑']

여기까지 코드를 따라해 보셨거나, 꼼꼼하게 읽어보신 분들이라면

궁금한 점이 하나 떠올랐을 것입니다.

위 코드의 첫 번째 라인, "for row in range(2, 8):"에서

첫 번째 파라미터인 2는, 제목행을 제외했으니 2일 수 있다고 치지만,

만약 몇 번째 행에서 끝나는지 모른다면? 우리가 미리 8을 입력할 수 있었을까요?

몇 번째 행에서 끝나는지 파악하기 위해 엑셀파일을 열어본 후,

다시 파이썬 코드를 수정하는 것은 굉장히 번거로운 일입니다.

그래서 몇 번째 라인에서 끝나는지 직접 확인하지 않고도

반복문 코드를 작성할 수 있는 대표적인 방법을 소개합니다.

3. 데이터가 기록된 모든 범위를 읽어오기 : ws.UsedRange

# ① len(ws.UsedRange())를 사용하는 방법
# ws.UsedRange()는 값이 입력되어 있는 모든 범위의 값들을
# 2중 튜플로 출력해줍니다.
print(ws.UsedRange())

# >>> (('이름', '성별', '생일', '취미'),
# ... ('마크',
# ...  '남',
# ...  pywintypes.datetime(1984, 5, 14, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  'VR'),
# ... ('빌',
# ...  '남',
# ...  pywintypes.datetime(1955, 10, 28, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  '기부'),
# ... ('일론',
# ...  '남',
# ...  pywintypes.datetime(1971, 6, 28, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  '트위터'),
# ... ('제프',
# ...  '남',
# ...  pywintypes.datetime(1964, 1, 12, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  '독서'),
# ... ('리사',
# ...  '여',
# ...  pywintypes.datetime(1969, 11, 7, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  '게임'),
# ... ('슬아',
# ...  '여',
# ...  pywintypes.datetime(1983, 6, 16, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True)),
# ...  '쇼핑'))

# 이를 이용해서 값을 직접 입력할 수도 있고,
# 범위를 파악할 수도 있습니다.

print(len(ws.UsedRange()))  # 7개 행 사용
# >>> 7

print(len(ws.UsedRange()[0]))  # 4개 열 사용
# >>> 4

ws.UsedRange()를 사용해서 값을 추출하고 입력하는 방법은 아래쪽에서 한 번 보여드리겠습니다.

이 포스팅의 최종 목표는 엑셀의 값을 읽어서 한/글 문서 필드에 삽입하는 방법을 소개하는 것임을 잊지 마시기 바랍니다ㅜ

for row in range(2, len(ws.UsedRange())+1):
    data = list(
        ws.Range(ws.Cells(row, 1),
                 ws.Cells(row, 4)).Value[0]
    )
    data[2] = data[2].strftime("%Y년 %#m월 %#d일")
    print(data)

# >>> ['마크', '남', '1984년 5월 14일', 'VR']
# ... ['빌', '남', '1955년 10월 28일', '기부']
# ... ['일론', '남', '1971년 6월 28일', '트위터']
# ... ['제프', '남', '1964년 1월 12일', '독서']
# ... ['리사', '여', '1969년 11월 7일', '게임']
# ... ['슬아', '여', '1983년 6월 16일', '쇼핑']

for문 대신 while문으로 작성할 수도 있습니다.

첫 열이 비어 있는지를 while문 탈출조건으로 체크하면 되겠네요.

이렇게 코드를 쓰면 ws.UsedRange를 사용할 필요도 없겠고요.

row = 2
while True:
    if not ws.Cells(row, 1).Value:
        break
    else:
        data = list(
            ws.Range(ws.Cells(row,1),
                     ws.Cells(row, 4)).Value[0]
        )
        data[2] = data[2].strftime("%Y년 %#m월 %#d일")
        print(data)
        row += 1

# >>> ['마크', '남', '1984년 5월 14일', 'VR']
# ... ['빌', '남', '1955년 10월 28일', '기부']
# ... ['일론', '남', '1971년 6월 28일', '트위터']
# ... ['제프', '남', '1964년 1월 12일', '독서']
# ... ['리사', '여', '1969년 11월 7일', '게임']
# ... ['슬아', '여', '1983년 6월 16일', '쇼핑']

이제 엑셀에서 값을 추출하는 기본적인 방법은 거의 다 소개드린 것 같습니다.

가장 간편한 방법인 pandas.DataFrame을 이용하는 방법은 다른 포스팅에서 소개드리겠습니다.
정부부처나 공공기관에서는 대부분 엑셀파일에 암호화(DRM)가 적용되어 있으므로
엑셀파일을 직접 판다스 데이터프레임으로 불러오는 데(pd.read_excel) 다소 제약이 있습니다.
다른 포스팅에서 소개드린 적이 있는데 pd.read_excel 대신 read_clipboard나, UsedRange 등을 사용하는 방식으로
우회하는 과정을 거쳐야 합니다..

이제 본격적으로 한/글 문서 필드에 삽입을 해 보겠습니다.

어떤 방식을 사용하셔도 무방하지만,

가장 마지막에 소개드린 while문을 사용한 코드에다 이어서 작성을 해 보겠습니다.

# 모듈 임포트
import win32com.client as win32

# 한/글 열기
hwp = win32.gencache.EnsureDispatch("hwpframe.hwpobject")
hwp.XHwpWindows.Item(0).Visible = True
hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule")
hwp.Open(r"C:\Users\smj02\Desktop\누름틀필드.hwp")

# 엑셀 열기
excel = win32.gencache.EnsureDispatch("Excel.Application")
excel.Visible = True
wb = excel.Workbooks.Open(r"C:\Users\smj02\Desktop\취미.xlsx")
ws = wb.Worksheets(1)

# 필드삽입 함수 정의
def 필드삽입(index, value):
    field_list = ["이름", "성별", "생일", "취미"]
    for idx, field in enumerate(field_list):
        hwp.PutFieldText(f"{field}{{{{{index}}}}}", value[idx])

# 첫 쪽 복사
hwp.Run("CopyPage")

# while문 실행
row = 2
while True:
    if not ws.Cells(row, 1).Value:
        hwp.Run("DeletePage")
        break
    else:
        data = list(
            ws.Range(ws.Cells(row,1),
                     ws.Cells(row, 4)).Value[0]
        )
        data[2] = data[2].strftime("%Y년 %#m월 %#d일")
        필드삽입(row-2, data)
        hwp.Run("PastePage")
        row += 1

위 코드를 실행해보면

실행과정을 보여드리기 위해 필드삽입 함수에 sleep(0.5)를 주었습니다.
실제로는 굉장히 빠르게 실행이 됩니다.

4. ws.UsedRange() 값을 활용하는 코드(가장 빠름)

이번 포스팅을 마치기 전에, 위에서 말씀드렸다시피 ws.UsedRange()를 사용한 코드예제를 보여드리겠습니다.

코드도 많이 짧아지고,  실행속도도 단순 for문에 비하면 열 배 정도 빨라집니다.

한/글 API 에서 제공하는 일부 명령어들이 파이썬에 비해 속도가 월등히 빠른 덕분입니다.

아래 명령어 일부에 대한 설명은 이어지는 포스팅에서 상세히 설명드릴 예정입니다.
# 셀범위 값 전부 읽어오기
xlsx_values = [list(i) for i in ws.UsedRange()]

# 날짜 문자열 수정하기
for idx, val in enumerate(xlsx_values):
    if idx:  # [0]은 제목이므로 생일 수정안함
        xlsx_values[idx][2] = xlsx_values[idx][2].strftime("%Y년 %#m월 %#d일")

print(xlsx_values)
# >>> [['이름', '성별', '생일', '취미'],
# ...  ['마크', '남', '1984년 5월 14일', 'VR'],
# ...  ['빌', '남', '1955년 10월 28일', '기부'],
# ...  ['일론', '남', '1971년 6월 28일', '트위터'],
# ...  ['제프', '남', '1964년 1월 12일', '독서'],
# ...  ['리사', '여', '1969년 11월 7일', '게임'],
# ...  ['슬아', '여', '1983년 6월 16일', '쇼핑']]

# 1페이지 복사해 둠
hwp.Run("CopyPage")

for idx, val in enumerate(xlsx_values[1:]):
    if idx:  # idx가 0인 경우는 제외
        hwp.Run("PastePage")  # 페이지 붙여넣기
    hwp.PutFieldText(f"이름{{{{{idx}}}}}\x02성별{{{{{idx}}}}}\x02생일{{{{{idx}}}}}\x02취미{{{{{idx}}}}}", 
                     "\x02".join(val))  # 4개의 값을 한 번에 배치삽입하는 방법. "\x02"로 구분함.

포스팅을 마치며

 

엑셀 값 추출부터 수정, 한/글 필드삽입까지 모든 과정을 (거의) 밑바닥부터 직접 코딩해보신 기분이 어떠신가요?

기대하셨던 것보다 어려웠거나, 다소 복잡하게 느껴지실 수도 있습니다.

다행인 것은, 이런 코드를 한 번 짜 두고 나면

비슷한 작업을 수행할 때 두고두고 재활용할 수 있다는 것이고,

밑바닥부터 코드를 짜두신 만큼

프로세스 일부를 수정하고 싶을 때, 그만큼 자유롭게 수정이나 기능추가가 수월하다는 것입니다.

그럼 이어서 다음 포스팅으로 넘어가보겠습니다.

수고하셨습니다.

반응형

댓글2