PDF 문서에서 데이터를 추출할 때 테이블 구조가 무너지거나 텍스트 단락이 뒤섞이는 문제를 자주 겪게 됩니다. PyPDF2나 pdfminer와 같은 기존 라이브러리는 복잡한 레이아웃, 특히 테이블 처리에 한계가 있어 데이터 분석이나 자동화 작업 시 효율이 떨어집니다. 이를 보완하기 위해 pdfplumber가 등장했습니다. 이 라이브러리는 PDF의 시각적 레이아웃을 정밀하게 복원하는 데 특화되어 있으며, 텍스트와 테이블은 물론 글자의 좌표와 폰트 정보까지 세밀하게 추출할 수 있습니다.
pdfplumber의 핵심 강점
- 정확한 테이블 구조 인식: 셀 병합과 테두리를 자동으로 감지하여 엑셀과 유사한 구조로 데이터를 반환합니다.
- 레이아웃 기반 텍스트 복원: 사람의 눈으로 읽는 순서와 유사하게 텍스트를 추출하며, 단락이 잘리는 현상을 방지합니다.
- 직관적인 API: PDF의 복잡한 내부 구조를 이해하지 않고도 몇 줄의 코드로 원하는 데이터를 파싱할 수 있습니다.
- 세밀한 영역 제어: 페이지 내의 특정 좌표 영역(Bounding Box)을 지정하여 불필요한 정보를 필터링할 수 있습니다.
환경 구축 및 설치
pdfplumber는 순수 Python으로 작성되었으며, pip를 통해 쉽게 설치할 수 있습니다.
pip install pdfplumber
설치 후 버전 출력을 통해 정상적으로 로드되는지 확인합니다.
import pdfplumber
print(f"현재 설치된 버전: {pdfplumber.__version__}")
기본 사용법: 문서 로드 및 데이터 추출
1. 문서 로드 및 페이지 메타데이터 확인
pdfplumber.open()을 사용하여 문서를 열고, pages 속성을 통해 각 페이지의 정보에 접근합니다. 리소스 관리를 위해 with 구문을 사용하는 것이 권장됩니다.
import pdfplumber
file_path = "sample_document.pdf"
with pdfplumber.open(file_path) as doc:
total_pages = len(doc.pages)
print(f"문서 전체 페이지 수: {total_pages}")
# 첫 번째 페이지 객체 가져오기
target_page = doc.pages[0]
# 페이지 메타데이터 출력
print(f"페이지 규격: {target_page.width} x {target_page.height}")
print(f"회전 각도: {target_page.rotation}")
print(f"이미지 포함 여부: {bool(target_page.images)}")
2. 텍스트 및 단어 메타데이터 추출
단순 텍스트 추출을 넘어, 각 단어의 위치와 스타일 정보를 활용하면 제목과 본문을 구분하거나 특정 조건의 텍스트만 필터링할 수 있습니다.
with pdfplumber.open("sample_document.pdf") as doc:
page_content = doc.pages[1]
# 일반 텍스트 추출
raw_text = page_content.extract_text()
if raw_text:
print(raw_text[:200])
# 단어별 상세 정보 추출
token_list = page_content.extract_words()
for token in token_list[:3]:
print(f"단어: {token['text']}")
print(f"좌표: X={token['x0']:.1f}, Y={token['top']:.1f}")
print(f"폰트: {token.get('fontname', 'Unknown')}, 크기: {token.get('size', 'Unknown')}\n")
3. 테이블 데이터 구조화
pdfplumber의 가장 강력한 기능은 테이블 추출입니다. 추출된 2차원 리스트는 pandas를 통해 즉시 데이터프레임으로 변환할 수 있습니다.
import pdfplumber
import pandas as pd
with pdfplumber.open("financial_report.pdf") as doc:
sheet = doc.pages[2]
grid_data = sheet.extract_table()
if grid_data:
header = grid_data[0]
rows = grid_data[1:]
df = pd.DataFrame(rows, columns=header)
print(df.head())
# CSV로 내보내기
df.to_csv("extracted_financial_data.csv", index=False, encoding="utf-8-sig")
한 페이지에 여러 테이블이 존재하는 경우 extract_tables()를 사용하여 모든 테이블을 리스트 형태로 가져올 수 있습니다.
with pdfplumber.open("complex_report.pdf") as doc:
sheet = doc.pages[0]
all_grids = sheet.extract_tables()
for idx, grid in enumerate(all_grids):
df = pd.DataFrame(grid[1:], columns=grid[0])
df.to_csv(f"output_table_{idx}.csv", index=False)
4. 사용자 정의 테이블 설정 및 영역 추출
테두리가 없는 테이블이나 특정 영역의 데이터만 필요할 때는 추출 전략을 수정하거나 페이지를 크롭(Crop)해야 합니다.
with pdfplumber.open("receipt.pdf") as doc:
sheet = doc.pages[0]
# 테두리 대신 텍스트 간격을 기준으로 테이블 인식
custom_settings = {
"vertical_strategy": "text",
"horizontal_strategy": "text"
}
grid = sheet.extract_table(table_settings=custom_settings)
# 특정 좌표 영역(Bounding Box)만 크롭하여 텍스트 추출
bbox = (40, 40, 350, 120) # (x0, top, x1, bottom)
cropped_section = sheet.crop(bbox)
print(cropped_section.extract_text())
실전 시나리오: 실제 업무 적용
1. 다중 페이지 텍스트 일괄 추출
전체 문서의 텍스트를 페이지 구분자와 함께 단일 텍스트 파일로 저장하는 파이프라인입니다.
def save_pdf_as_text(input_pdf, output_txt):
with pdfplumber.open(input_pdf) as doc, open(output_txt, "w", encoding="utf-8") as out_file:
for num, pg in enumerate(doc.pages, 1):
content = pg.extract_text()
if content:
out_file.write(f"--- Page {num} ---\n")
out_file.write(f"{content}\n\n")
print(f"텍스트 추출 완료: {output_txt}")
save_pdf_as_text("manual.pdf", "manual_text.txt")
2. 전체 테이블을 엑셀로 통합
문서 내의 모든 테이블을 추출하여 하나의 엑셀 파일 내 여러 시트로 분리하여 저장합니다.
def export_tables_to_xlsx(input_pdf, output_xlsx):
with pdfplumber.open(input_pdf) as doc:
with pd.ExcelWriter(output_xlsx, engine="openpyxl") as writer:
sheet_idx = 0
for pg_num, pg in enumerate(doc.pages, 1):
grids = pg.extract_tables()
for grid in grids:
sheet_idx += 1
try:
df = pd.DataFrame(grid[1:], columns=grid[0])
except ValueError:
df = pd.DataFrame(grid) # 헤더가 없는 경우 대비
# 엑셀 시트명 길이 제한(31자) 처리
sheet_name = f"P{pg_num}_T{sheet_idx}"
df.to_excel(writer, sheet_name=sheet_name[:31], index=False)
export_tables_to_xlsx("quarterly_reports.pdf", "consolidated_tables.xlsx")
3. 영수증 및 청구서 주요 정보 파싱
고정된 양식의 문서에서 특정 필드(결제일, 총액 등)의 좌표를 미리 지정하여 데이터를 추출합니다.
def get_invoice_details(pdf_file):
with pdfplumber.open(pdf_file) as doc:
pg = doc.pages[0]
zones = {
"InvoiceNo": (300, 100, 450, 120),
"Date": (300, 130, 450, 150),
"TotalAmount": (300, 280, 450, 300)
}
result = {}
for field, coords in zones.items():
txt = pg.crop(coords).extract_text()
result[field] = txt.replace("\n", " ").strip() if txt else "N/A"
return result
metadata = get_invoice_details("invoice_2023.pdf")
for k, v in metadata.items():
print(f"{k}: {v}")
4. 스캔본 PDF 처리 (OCR 결합)
pdfplumber는 텍스트 기반 PDF만 처리할 수 있습니다. 이미지가 포함된 스캔본의 경우 pytesseract와 결합하여 광학 문자 인식(OCR)을 수행해야 합니다.
import pytesseract
def process_scanned_pdf(pdf_file, out_txt):
with pdfplumber.open(pdf_file) as doc, open(out_txt, "w", encoding="utf-8") as f:
for num, pg in enumerate(doc.pages, 1):
# 페이지를 이미지 객체로 변환
img_obj = pg.to_image().original
# Tesseract OCR 엔진을 통한 텍스트 인식
ocr_text = pytesseract.image_to_string(img_obj, lang="kor+eng")
f.write(f"--- Page {num} ---\n{ocr_text}\n\n")
# 사전에 Tesseract OCR 엔진 설치가 필요합니다.
# process_scanned_pdf("scanned_contract.pdf", "contract_ocr.txt")
문제 해결 및 주의사항 (Troubleshooting)
- 텍스트 인코딩 및 누락 문제: PDF 내부 폰트 인코딩이 비표준인 경우 글자가 깨질 수 있습니다. 이때는
extract_text(x_tolerance=1, y_tolerance=1)과 같이 허용 오차(Tolerance) 파라미터를 조절해 보거나, 스캔본으로 간주하고 OCR 방식을 적용해야 합니다. - 테이블 구조 틀어짐 현상: 셀 병합이 많거나 테두리가 불명확한 경우 행과 열이 어긋날 수 있습니다.
table_settings딕셔너리를 통해text또는lines전략을 명시적으로 지정하고, 필요시crop()으로 노이즈 영역을 제거한 뒤 추출하십시오. - 대용량 파일 처리 시 메모리 과부하: 수백 페이지에 달하는 문서를 한 번에 로드하면 메모리 누수가 발생할 수 있습니다. 반드시 페이지 단위로 반복문을 돌리고, 사용이 끝난 페이지 객체는 가비지 컬렉션이 될 수 있도록 변수를 재할당하거나
with블록 내에서 즉시 처리하십시오. - 보안 설정된 PDF 처리: 비밀번호로 잠겨 있거나 편집이 제한된 PDF는 pdfplumber로 열 수 없습니다.
qpdf또는pikepdf와 같은 외부 도구를 사용하여 사전에 보안 설정을 해제(Decrypt)한 후 작업해야 합니다.