학습 자료

업무 자동화 ① 엑셀 (1) — 폴더를 훑어 표로 만든다


Article

앞 강의까지로 폴더를 훑고 파일을 읽는 것이 됐다. 여기까지만 해도 실제로 쓸 수 있는 도구를 하나 만들 수 있다.

이번 강의는 검색과 상관없다. 손으로 하면 한 시간 걸리는 일을 0.02초로 끝내는 것을 만든다.

폴더에 문서 50개가 있다

손으로 하면

한 시간

  • 엑셀을 켠다
  • 파일을 하나씩 열어 이름·용량·내용을 확인한다
  • 표에 옮겨 적는다
  • 50번 반복한다
  • 다음 달에 또 한다

코드로 하면

0.02초

  • 폴더 경로를 알려주고 실행한다
  • 엑셀 파일이 나온다
  • 다음 달에는 다시 실행만 하면 된다

준비 — 라이브러리 하나 설치

엑셀 파일을 다루는 도구다. 하나뿐이고 2MB 다.

pip install openpyxl

파일은 계속 ai-course 폴더에 넣는다. 이번 강의는 51번부터다.

1. 실습 재료를 만든다

읽을 문서가 있어야 한다. 앞 강의에서 한 것과 같은 방식으로 코드가 만든다.

ai-course/51_서류만들기.py
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. 폴더를 훑어 내용을 모은다

앞 강의에서 한 것 그대로다. 달라진 것은 모은 결과를 엑셀로 보낸다는 것뿐이다.

ai-course/52_모으기.py
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 를 그대로 밀어 넣는다. 엑셀을 만드는 데 필요한 것은 네 줄뿐이다.

ai-course/53_엑셀로.py
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 으로 열어보면 확실하다.

네 덩어리만 알면 된다
  1. 01

    Workbook() — 엑셀 파일 하나

    빈 파일을 만든다. 아직 디스크에 없고 메모리에만 있다.

  2. 02

    wb.active — 시트 하나

    엑셀 하단의 「Sheet1」 탭이다. 새 파일에는 한 장뿐이라 그걸 집어 온다.

  3. 03

    ws.append([...]) — 한 줄 추가

    리스트 하나가 엑셀 한 행이 된다. 항목 개수가 그대로 열 개수다.

  4. 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]제목 줄 순서대로 값을 꺼낸다

스스로 해보기

확인 문제 둘
  1. 01

    열을 하나 더 넣는다

    ai-course/54_연습1.py. 「마지막 수정일」을 넣어본다. os.path.getmtime(path) 이 숫자를 주는데, 그걸 사람이 읽는 날짜로 바꾸는 게 관건이다.

  2. 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 로 저장한다. 네 덩어리면 엑셀이 나온다.

폴더를 훑는 것 · 파일을 읽는 것 · 딕셔너리 · 반복문은 전부 앞 강의에서 배운 것이다. 새로 배운 건 엑셀로 내보내는 마지막 한 걸음뿐이다.

다음 강의에서 이 밋밋한 표에 서식을 입혀 사람이 열어볼 만한 문서로 만들고, 폴더 이름만 바꿔 다시 돌릴 수 있게 함수로 묶는다.