Application/Python

[Python] 11. 파이썬으로 PDF 텍스트 추출하고 엑셀로 정리하기

devsalix 2026. 7. 3. 16:55
728x90

파이썬으로 PDF 텍스트 추출하고 엑셀로 정리하기

지난 글에서 PDF를 합치고 나눠봤잖아요. 근데 사실 진짜 필요한 건 그게 아니었죠. PDF 안에 든 숫자를 꺼내 쓰는 것, 그게 진짜 일이거든요.

카드 명세서 열어놓고, 옆에 엑셀 켜놓고, 날짜 하나 치고 금액 하나 치고... 이거 해보신 분들 많으실 거예요. 거래내역 수십 줄을 셀에 옮겨 적다 보면 어느새 퇴근 시간이 지나 있고요.

그 노동을 오늘 파이썬한테 넘길 거예요. PDF 표를 통째로 뽑아서 엑셀에 그대로 꽂는 거죠.

그리고 지난 글 마지막에 "스캔본은 글자가 안 뽑힌다, 이유는 다음에"라고 떡밥 하나 던져놨었죠. 그것도 오늘 회수합니다.


내 PDF는 왜 글자가 안 뽑히죠? (텍스트 PDF vs 스캔본)

시작하기 전에 이거부터 짚고 갈게요. 안 짚으면 나중에 "내 코드가 틀렸나?" 하고 한참 헤매시거든요.

PDF는 겉보기엔 다 똑같아 보여도 속은 두 종류예요.

텍스트 PDF는 워드나 한글, 엑셀에서 "PDF로 저장"한 거예요. 안에 진짜 '글자 데이터'가 들어 있어요. 그래서 마우스로 드래그하면 글자가 파랗게 선택되죠. 이런 건 파이썬이 글자를 술술 뽑아냅니다.

스캔본은 종이를 스캐너로 스캔했거나 사진 찍어서 만든 PDF예요. 겉보기엔 글자처럼 보이는데, 실은 글자 모양의 사진 한 장이에요.

이 차이가 핵심이에요. 사진 속 간판 글씨를 복사할 수 없잖아요? 스캔본 PDF의 글자도 똑같아요. 컴퓨터 눈엔 그냥 픽셀 덩어리라 뽑을 글자가 아예 없어요.

그럼 내 PDF가 어느 쪽인지 어떻게 아냐고요? 셀프 진단법이 있어요.

PDF 뷰어로 열고, 본문 글자를 마우스로 드래그해보세요.
글자가 파랗게 선택되면 → 텍스트 PDF (추출 됨)
사진처럼 네모만 잡히면 → 스캔본 (추출 안 됨)

 

스캔본에서 글자를 뽑으려면 OCR(사진 속 글자를 알아보는 기술)이라는 게 따로 필요한데, 이건 별도 프로그램 설치도 해야 하고 얘기가 길어져서 나중에 따로 다룰게요. 오늘은 텍스트 PDF 기준으로 갑니다.


pdfplumber 설치하고 페이지 텍스트 추출하기

PDF 텍스트 추출에는 pdfplumber라는 도구를 쓸 거예요. 설치는 늘 하던 그대로예요.

pip install pdfplumber

 

pip install openpyxl, pip install pypdf 해보셨으면 똑같은 흐름이에요. 한글이나 오피스 같은 별도 프로그램 없이 순수 파이썬이라, 이거 한 줄이면 준비 끝이거든요.

먼저 페이지 하나에서 글자를 뽑아볼게요.

import pdfplumber

with pdfplumber.open("보고서.pdf") as pdf:   # PDF 열기 (다 쓰면 자동으로 닫힘)
    page = pdf.pages[0]                       # 1페이지 (인덱스 0 — 지난 글 그거!)
    text = page.extract_text()                # 이 페이지 글자를 통째로 뽑기
    print(text)

 

여기서 페이지 번호가 또 나오네요. pages[0]이 1페이지예요. 지난 글에서 "페이지는 0부터"라고 각인시켰던 그거, 여기서도 똑같이 적용돼요.

with ... as pdf: 이 방식은 다 쓰고 나면 파일을 알아서 닫아줘요. 파일이 잠기는 걸 막아주는 안전한 습관이라고만 알아두시면 돼요.

참고로 지난 글에서 깐 pypdf에도 텍스트 뽑는 기능이 있긴 해요.

from pypdf import PdfReader          # 지난 글에서 이미 깔아둔 도구

reader = PdfReader("보고서.pdf")
print(reader.pages[0].extract_text())   # pypdf로도 텍스트가 나오긴 해요

 

줄글만 대충 뽑을 거면 pypdf도 괜찮아요. 근데 우리 목적은 를 칸 구조 그대로 뽑아서 엑셀에 넣는 거잖아요. 그건 pdfplumber가 훨씬 잘해요. 그래서 오늘 주인공은 pdfplumber예요.


표를 통째로 뽑아 엑셀로 (extract_tables)

여기가 오늘의 하이라이트예요. 표 추출은 extract_tables()를 써요.

import pdfplumber

with pdfplumber.open("명세서.pdf") as pdf:
    page = pdf.pages[0]
    tables = page.extract_tables()       # 이 페이지의 '모든 표'를 찾아냄
    print(f"표 {len(tables)}개 찾음")

    for table in tables:                 # 표 하나씩
        for row in table:                # 표의 한 줄(행)씩
            print(row)                   # 예: ['2025-10-01', '사무용품', '15000']

 

여기서 결과가 어떤 모양으로 나오는지가 진짜 중요해요. 이걸 이해하면 뒷부분이 술술 풀리거든요.

  • extract_tables()가 주는 건 표들의 목록이에요. (한 페이지에 표가 여러 개일 수 있으니까요)
  • 표 하나는 행들의 목록이에요.
  • 행 하나는 칸들의 리스트예요. 예를 들면 ['2025-10-01', '사무용품', '15000'] 이런 식이죠.

실행 결과가 이렇게 나와요.

표 1개 찾음
['날짜', '내역', '금액']
['2025-10-01', '사무용품', '15000']
['2025-10-02', '점심식대', '9000']

 

보이시죠? row['날짜', '내역', '금액'] 같은 리스트로 나와요. 이게 왜 중요하냐면요.

이 "리스트 안의 리스트" 구조가 엑셀이랑 똑같은 모양이거든요.


추출한 표를 엑셀에 저장하기 (여기부턴 아는 거예요)

자, 이제 안심하셔도 돼요. 여기서부터는 auto-05에서 이미 배운 코드예요.

extract_tables가 준 게 "행들의 리스트"인데, 그때 ws.append([...])로 엑셀에 한 줄씩 넣던 거랑 완벽하게 맞아떨어져요.

import pdfplumber
from openpyxl import Workbook           # auto-05에서 쓴 그 도구

wb = Workbook()                         # 빈 엑셀 한 권
ws = wb.active                          # 첫 시트

with pdfplumber.open("명세서.pdf") as pdf:
    page = pdf.pages[0]
    table = page.extract_table()        # 첫 번째 표 하나만 (단수형!)

    for row in table:                   # 표의 각 행을
        ws.append(row)                  # 엑셀에 한 줄씩 추가 (그 append 맞아요)

wb.save("명세서_정리.xlsx")             # 저장 (save 안 하면 파일 안 바뀌는 거, 기억나시죠)
print("엑셀로 정리 완료: 명세서_정리.xlsx")

# 실행 결과:
# 엑셀로 정리 완료: 명세서_정리.xlsx

 

row가 이미 ['날짜', '내역', '금액'] 같은 리스트라서 ws.append(row)가 그냥 돼요. auto-05에서 ws.append(["김영희", "개발팀"]) 하던 거랑 똑같은 모양이잖아요.

정리하면, PDF에서 뽑은 행 → ws.append(row) → 저장. 이게 전부예요. PDF에서 꺼내는 앞부분만 오늘 새로 배운 거고, 엑셀에 담는 뒷부분은 이미 아시던 거예요.

참고로 표가 여러 개면 복수형 extract_tables(), 하나만 확실하면 단수형 extract_table()을 써요. 위 예제는 표가 하나라 단수형을 썼어요.

명세서가 여러 장이면 페이지를 다 돌면 돼요.

import pdfplumber
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

with pdfplumber.open("거래내역.pdf") as pdf:
    for page in pdf.pages:                     # 모든 페이지를 하나씩
        for table in page.extract_tables():    # 그 페이지의 모든 표
            for row in table:                  # 표의 각 행
                ws.append(row)                 # 엑셀에 계속 이어붙이기

wb.save("거래내역_전체.xlsx")
print("모든 페이지 정리 끝!")

 

반복문이 세 겹이라 좀 부담스러워 보이죠? 근데 지난 글에서 "폴더 안 PDF 전부 순회" 해봤잖아요. 거기서 한 겹만 더 들어간 거예요.


자주 만나는 에러: 글자가 안 뽑혀요

처음에 이 부분에서 제일 많이 당황하시더라고요. 코드는 멀쩡한데 결과가 텅 비어 있는 거예요.

extract_text()가 아무것도 안 뽑고 None이나 빈 문자열을 주는 경우가 있어요. 표는 빈 리스트 []가 나오고요. 에러가 뜨는 게 아니라 그냥 결과가 없어요. 그래서 더 헷갈리죠.

십중팔구 스캔본이에요. 아까 얘기한 그 사진 PDF요. 글자가 그림이라 뽑을 게 없는 거예요. 이럴 땐 코드가 먼저 알려주게 하면 좋아요.

import pdfplumber

with pdfplumber.open("의심스러운.pdf") as pdf:
    page = pdf.pages[0]
    text = page.extract_text()              # 일단 뽑아보고

    if not text:                            # None이거나 빈 문자열이면
        print("글자가 안 뽑혀요. 스캔본(이미지) PDF일 수 있어요.")
        print("→ 뷰어에서 글자를 드래그해보세요. 선택 안 되면 스캔본!")
    else:
        print("텍스트 PDF 맞아요. 추출 진행할게요.")
        print(text[:200])                   # 앞부분 살짝 미리보기

 

또 자주 보는 에러가 이거예요.

AttributeError: 'NoneType' object has no attribute 'split'

 

이건 스캔본이라 extract_text()None을 줬는데, 거기에 바로 .split() 같은 걸 붙여서 나는 에러예요. text = page.extract_text() or "" 이렇게 써두면 없을 때 빈 문자열로 받아서 안전해요.

그리고 표 추출은 100%가 아니에요. 선(테두리) 없이 공백으로만 정렬된 표는 pdfplumber가 표로 못 알아볼 때가 있어요. 그러니 뽑은 결과는 꼭 눈으로 한 번 확인하는 습관을 들이세요. 완벽할 거라 기대하면 실망하거든요.


직접 해보기

  • 엑셀이나 워드에서 간단한 표(날짜·항목·금액 3열, 몇 줄)를 만들어서 "PDF로 저장"하세요. 이렇게 만든 건 텍스트 PDF라 잘 뽑혀요. 이걸로 위 예제를 돌려서 엑셀로 뽑아보세요. 결과 엑셀을 열어 칸이 제대로 나뉘었는지 확인해보시고요.
  • (대비 실습) 스마트폰으로 종이 하나 찍어서 PDF로 만든 뒤 스캔본 감지 코드를 돌려보세요. "글자가 안 뽑히는" 걸 직접 겪어보면 "아 이래서 OCR이 필요하구나" 확 와닿아요.

힌트: 표가 안 잡히면 print(page.extract_tables())로 결과가 빈 리스트인지부터 확인하세요. 비었으면 스캔본이거나 선 없는 표예요.


이제 읽는 쪽은 웬만큼 하시는 거예요

여기까지 왔으면 PDF에서 데이터를 꺼내 엑셀로 정리하는 건 이제 손에 익으신 거예요.

그런데 자동화의 마지막 한 조각은 보통 반대 방향이에요. 뽑은 데이터로 계약서나 보고서, 안내문을 만들어내는 것. 그거 매번 손으로 워드에 옮겨 치고 계시죠? 다음 글에서는 파이썬으로 Word 문서를 자동으로 생성하는 법을 다뤄볼게요.

아, 그리고 스캔본에서 글자를 뽑는 OCR은 준비되면 따로 한 편 제대로 다룰게요. 그거 궁금하셨던 분들 조금만 기다려주세요.


 


제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀

 

 
728x90