업무 자동화 ① 엑셀 (1) — 폴더를 훑어 표로 만든다
Article
앞 강의까지로 폴더를 훑고 파일을 읽는 것이 됐다. 여기까지만 해도 실제로 쓸 수 있는 도구를 하나 만들 수 있다.
이번 강의는 검색과 상관없다. 손으로 하면 한 시간 걸리는 일을 0.02초로 끝내는 것을 만든다.
손으로 하면
한 시간
- 엑셀을 켠다
- 파일을 하나씩 열어 이름·용량·내용을 확인한다
- 표에 옮겨 적는다
- 50번 반복한다
- 다음 달에 또 한다
코드로 하면
0.02초
- 폴더 경로를 알려주고 실행한다
- 엑셀 파일이 나온다
- 다음 달에는 다시 실행만 하면 된다
준비 — 라이브러리 하나 설치
엑셀 파일을 다루는 도구다. 하나뿐이고 2MB 다.
pip install openpyxl
파일은 계속 ai-course 폴더에 넣는다. 이번 강의는 51번부터다.
1. 실습 재료를 만든다
읽을 문서가 있어야 한다. 앞 강의에서 한 것과 같은 방식으로 코드가 만든다.
import os
os.makedirs("서류", exist_ok=True)
# "안녕 " * 3 은 "안녕 안녕 안녕 " 이 된다. 분량이 다른 문서를 빨리 만들려고 쓴 것이다.
files = {
"계약서.txt": "수업용 예제\n" + "계약 조건에 관한 내용이다. " * 12,
"견적서.txt": "수업용 예제\n" + "품목과 금액을 적은 문서다. " * 30,
"보고서.txt": "수업용 예제\n" + "이번 달 진행 상황을 정리했다. " * 45,
"회의록.txt": "수업용 예제\n" + "회의에서 오간 이야기를 적었다. " * 60,
"메모.txt": "수업용 예제\n" + "짧은 메모다. " * 3,
}
for name, content in files.items():
with open(f"서류/{name}", "w", encoding="utf-8") as f:
f.write(content)
print(sorted(os.listdir("서류")))
- ['견적서.txt', '계약서.txt', '메모.txt', '보고서.txt', '회의록.txt']
2. 폴더를 훑어 내용을 모은다
앞 강의에서 한 것 그대로다. 달라진 것은 모은 결과를 엑셀로 보낸다는 것뿐이다.
import os
import glob
rows = []
for path in sorted(glob.glob("서류/*.txt")):
with open(path, encoding="utf-8") as f:
text = f.read()
rows.append({
"파일명": os.path.basename(path), # 경로에서 이름만
"크기KB": round(os.path.getsize(path) / 1024, 1), # 바이트 -> KB
"글자수": len(text),
"첫문장": text.split("\n")[1][:20], # 첫 줄은 "수업용 예제"라 두 번째 줄
})
print(len(rows))
print(rows[0])
- 5
- {'파일명': '견적서.txt', '크기KB': 1.1, '글자수': 487, '첫문장': '품목과 금액을 적은 문서다. 품목과 '}
- os.path.getsize(path)
- 파일 크기를 바이트로
- 1024 로 나누면 KB
- round(x, 1)
- 소수점 첫째 자리에서 반올림
- 1.0546... -> 1.1
나머지는 전부 배운 것이다 — glob 으로 훑고, open 으로 읽고, 딕셔너리에 담아 append 로 모은다.
3. 엑셀로 내보낸다
모은 rows 를 그대로 밀어 넣는다. 엑셀을 만드는 데 필요한 것은 네 줄뿐이다.
import os
import glob
from openpyxl import Workbook, load_workbook
rows = []
for path in sorted(glob.glob("서류/*.txt")):
with open(path, encoding="utf-8") as f:
text = f.read()
rows.append({
"파일명": os.path.basename(path),
"크기KB": round(os.path.getsize(path) / 1024, 1),
"글자수": len(text),
"첫문장": text.split("\n")[1][:20],
})
HEADERS = ["파일명", "크기KB", "글자수", "첫문장"]
wb = Workbook() # 새 엑셀 파일 하나. 아직 메모리에만 있다
ws = wb.active # 그 안의 시트. 새 파일에는 한 장뿐이다
ws.title = "서류 현황" # 시트 탭 이름
ws.append(HEADERS) # append 는 "맨 아래에 한 줄 추가". 리스트 하나가 한 행이 된다
for r in rows:
# 딕셔너리에서 HEADERS 순서대로 꺼내 리스트로 만든다.
# r.values() 로 꺼내면 나중에 항목을 추가했을 때 열 순서가 조용히 어긋난다.
ws.append([r[h] for h in HEADERS])
wb.save("서류현황.xlsx") # 여기서 처음으로 파일이 생긴다
# 제대로 들어갔는지 다시 읽어 확인한다
chk = load_workbook("서류현황.xlsx").active
print(chk.max_row, chk.max_column)
for row in chk.iter_rows(values_only=True):
print(row)
- 6 4
- ('파일명', '크기KB', '글자수', '첫문장')
- ('견적서.txt', 1.1, 487, '품목과 금액을 적은 문서다. 품목과 ')
- ('계약서.txt', 0.5, 199, '계약 조건에 관한 내용이다. 계약 조')
- ('메모.txt', 0.1, 31, '짧은 메모다. 짧은 메모다. 짧은 메')
- ('보고서.txt', 1.9, 817, '이번 달 진행 상황을 정리했다. 이번')
- ('회의록.txt', 2.6, 1087, '회의에서 오간 이야기를 적었다. 회의')
ai-course 폴더에서 서류현황.xlsx 를 더블클릭해 열어본다. 제목 줄 하나에 문서 다섯 줄이 들어 있다.
저장한 뒤 다시 읽어 확인하는 것이 이 파일의 요점이다. wb.save 만 하고 끝내면 정말 들어갔는지 알 수 없다. load_workbook 으로 열어보면 확실하다.
- 01
Workbook() — 엑셀 파일 하나
빈 파일을 만든다. 아직 디스크에 없고 메모리에만 있다.
- 02
wb.active — 시트 하나
엑셀 하단의 「Sheet1」 탭이다. 새 파일에는 한 장뿐이라 그걸 집어 온다.
- 03
ws.append([...]) — 한 줄 추가
리스트 하나가 엑셀 한 행이 된다. 항목 개수가 그대로 열 개수다.
- 04
wb.save("이름.xlsx") — 저장
이 줄을 빼먹으면 아무 일도 안 일어난다. 파일이 안 생겼으면 여기부터 확인한다.
이번 강의에 나온 것
| 쓴 것 | 하는 일 |
|---|---|
pip install openpyxl | 엑셀을 다루는 도구를 깐다 (2MB) |
Workbook() | 새 엑셀 파일 하나 |
wb.active | 지금 시트 |
ws.title = "이름" | 시트 탭 이름 |
ws.append([...]) | 맨 아래에 한 줄 추가 |
wb.save("x.xlsx") | 이 줄이 있어야 파일이 생긴다 |
load_workbook("x.xlsx") | 있는 엑셀을 읽는다 |
ws.max_row · ws.max_column | 몇 행 몇 열이 들었나 |
ws.iter_rows(values_only=True) | 한 줄씩 값만 꺼낸다 |
os.path.getsize(path) | 파일 크기(바이트) |
[r[h] for h in HEADERS] | 제목 줄 순서대로 값을 꺼낸다 |
스스로 해보기
- 01
열을 하나 더 넣는다
ai-course/54_연습1.py. 「마지막 수정일」을 넣어본다.os.path.getmtime(path)이 숫자를 주는데, 그걸 사람이 읽는 날짜로 바꾸는 게 관건이다. - 02
확장자를 안 가리면 무슨 일이 생기나
ai-course/55_연습2.py. txt · 엑셀 · 이미지 · 하위 폴더가 섞인 폴더를 만들고*.txt대신*로 훑어본다. 읽기에서 무슨 에러가 나나.
1번 — 수정일 열 추가충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요
getmtime 은 「1970년 1월 1일부터 몇 초 지났는가」를 준다. 사람이 읽을 수 없는 숫자다.
datetime.datetime.fromtimestamp() 가 그걸 날짜로 바꾸고, strftime 으로 원하는 모양을 정한다.
import os
import datetime
ts = os.path.getmtime("서류/계약서.txt")
print(ts) # 1785930672.7417822
print(datetime.datetime.fromtimestamp(ts)) # 2026-08-05 20:51:12.741782
print(datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M"))표에 넣을 때는 rows.append 안에 "수정일": datetime.datetime.fromtimestamp(os.path.getmtime(path)).strftime("%Y-%m-%d") 를 한 줄 더한다.
그리고 HEADERS 에 "수정일" 을 더하기만 하면 표가 따라온다.
HEADERS 하나만 고치면 나머지가 따라오는 구조라 이게 쉽다. r.values() 로 짰다면 여기서 순서가 어긋난다.
2번 — 확장자를 안 가리면충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요
*.txt 를 * 로 바꾸면 이미지·엑셀·하위 폴더까지 전부 걸린다. 그리고 읽기에서 터진다.
UnicodeDecodeError 가 난다. 4강에서 본 그 에러다 — 글자가 아닌 것을 글자로 읽으려 했기 때문이다.
import os
import glob
from openpyxl import Workbook
# 확인용으로 섞인 폴더를 만든다
os.makedirs("섞인폴더/하위", exist_ok=True)
with open("섞인폴더/글.txt", "w", encoding="utf-8") as f:
f.write("수업용 예제\n글자 파일이다.")
Workbook().save("섞인폴더/표.xlsx")
open("섞인폴더/사진.jpg", "wb").write(bytes([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10]))
for path in sorted(glob.glob("섞인폴더/*")):
try:
with open(path, encoding="utf-8") as f:
f.read()
print("읽힘 ", path)
except UnicodeDecodeError:
print("못읽음", path) # jpg, xlsx ...
except PermissionError:
print("폴더 ", path) # Windows 는 폴더를 열면 이 에러다
except IsADirectoryError:
print("폴더 ", path) # macOS / Linux 는 이 에러다
# 파일만 남기려면
print([p for p in sorted(glob.glob("섞인폴더/*")) if os.path.isfile(p)])실측 — 글.txt 는 읽히고, 사진.jpg 와 표.xlsx 는 `UnicodeDecodeError`, 하위 폴더는 `PermissionError` 다.
⚠ 폴더를 열 때 나는 에러가 운영체제마다 다르다. Windows 는 PermissionError, macOS · Linux 는 IsADirectoryError 다. 그래서 위 코드에 except 를 둘 다 적어뒀다.
확장자를 먼저 제한하는 편이 낫다. 에러를 하나 더 잡는 것보다, 애초에 만나지 않는 쪽이 낫다.
정리하면
Workbook() 으로 만들고, append 로 줄을 쌓고, save 로 저장한다. 네 덩어리면 엑셀이 나온다.
폴더를 훑는 것 · 파일을 읽는 것 · 딕셔너리 · 반복문은 전부 앞 강의에서 배운 것이다. 새로 배운 건 엑셀로 내보내는 마지막 한 걸음뿐이다.
다음 강의에서 이 밋밋한 표에 서식을 입혀 사람이 열어볼 만한 문서로 만들고, 폴더 이름만 바꿔 다시 돌릴 수 있게 함수로 묶는다.