Python

PDF table-extraction (표 추출) 실험 (with pdfplumber, camelot, tabula-py)

검정비니 2024. 7. 22. 10:50
728x90
반응형

table extraction은 문서로부터 표를 양식 그대로 추출하는 기술에 대해 연구하는 분야이다.

이 기술은 주로 데이터 과학자들이나 연구자들에게 유용한데, 문헌이나 문서들로부터 표 형식의 데이터를 추출해서 가공 가능한 포맷 (예를 들면 csv나 dataframe) 등으로 변환하는 기술은 데이터로부터 다양한 인사이트를 얻는데 도움이 되기 때문이다.

 

파이썬을 사용해서 표를 추출하는 방법으로는 크게 3가지 툴이 추천되곤한다.

  • pdfplumber
  • camelot
  • tabula-py

개인적으로 이들 중에는 pdfplumber가 가장 높은 성능을 보였으며, 그 다음으로는 tabula-py가 좋았고, camelot이 가장 추출률이 좋지 않았었다.

이제 예제 코드와 함께 각 툴들에 대해서 소개를 해보고자 한다.

(참고로 해당 툴들은 모두 텍스트 추출 기반으로 작동하기 때문에 스캔된 문서와 같이 이미지로 구성된 문서들은 작동하지 않는다)

 

1. pdfplumber

설치방법:

pip install pdfplumber

 

pdfplumber는 기본적으로 Anssi Nurminen의 석사 논문을 기반으로 구현된 라이브러리이다.

참고로 해당 논문은 table extraction 분야에서 현재까지도 가장 널리 인용되고 있는 논문이니, 관심이 있으면 꼭 정독하기를 추천한다.

 

아래는 pdf 문서 내에 있는 모든 "표 (table)"라고 인식되어지는 데이터들을 출력하는 코드이다. 또한, 디버깅을 위해 각 페이지마다 텍스트를 추출해서 각 텍스트 별로 bounding box를 추가해서 이미지 파일로 저장하는 기능 역시 구현하였다.

import pdfplumber
import json


TABLE_SETTINGS = {
    "vertical_strategy": "lines",
    "horizontal_strategy": "lines",
    "explicit_vertical_lines": [],
    "explicit_horizontal_lines": [],
    "snap_tolerance": 3,
    "snap_x_tolerance": 2,
    "snap_y_tolerance": 2,
    "join_tolerance": 3,
    "join_x_tolerance": 3,
    "join_y_tolerance": 3,
    "edge_min_length": 3,
    "min_words_vertical": 3,
    "min_words_horizontal": 1,
    "intersection_tolerance": 3,
    "intersection_x_tolerance": 3,
    "intersection_y_tolerance": 3,
    "text_tolerance": 3,
    "text_x_tolerance": 3,
    "text_y_tolerance": 3,
}



def extract_data_from_pdf(pdf_path):
    with pdfplumber.open(pdf_path) as pdf:
        pages = pdf.pages
        index = 1

        for page in pages:
        	# 해당 페이지 내의 모든 텍스트를 찾아서 bounding box 추가 후 이미지로 저장
        	im = page.to_image(resolution=150)
            im.draw_rects(page.extract_words())
            im.save(f"output_{i}.png", format="PNG")
            i += 1

			# 페이지 내의 테이블들을 모두 찾기
			tables = page.find_tables(table_settings=TABLE_SETTINGS)

			# 추출된 표들을 하나씩 출력하기
            for table in tables:
            	print(table)
 
 
 if __name__ == '__main__':
 	extract_data_from_pdf('test.pdf')

 

pdfplumber는 기본적으로 높은 수준의 정확도를 보여서 다른 라이브러리들로는 추출이 안되는 표들 역시 적절히 추출하는 높은 성능을 보인다. 추출이 잘 안되는 경우가 2가지 있는데 "셀 병합이 많은 표"나 "여러 페이지에 걸쳐져 있는 표" 등에 대해서는 잘 작동하지 못하는 모습을 보인다. 그러나 이러한 항목들은 다른 라이브러리에서도 잘 해결하지 못하는 항목들이다.

 

"여러 페이지에 걸쳐져 있는 표"의 경우에는 추가적인 알고리즘을 코딩해서 문제를 해결해야 한다. 이 "알고리즘"은 기본적으로 pdf 문서마다 특성이 조금씩 다르기 때문에 일반화시키기는 조금 어려움이 있다. 보통은 추출된 표의 셀들의 좌표에 대한 메타데이터를 비교 분석하는 코드를 구현해서 문제를 해결하는 접근법이 가장 많이 사용된다고 한다.

 

2. camelot

설치방법:

pip install "camelot-py[base]"

 

camelot의 최대 장점은 "controllable"한 특성이라고 할 수 있다.

테이블 추출 시, 각 테이블의 추출 정확도를 함께 표현해줘서 accuracy threshold 기반의 필터링이 가능하며, 추출된 테이블에 대해 Lattice 방법을 통한 셀 병합 탐지 등의 기능을 제공한다.

아쉬운 점이라면, pdfplumber에 비해 추출을 못하는 경우가 다소 있다는 점일 것이다. (pdf 내의 표를 찾지 못해서 데이터를 추출하지 못하는 경우가 pdfplumber보다 많음)

 

아래는 camelot을 통한 테이블 추출 예제 코드이다.

import camelot


def camelot_extract(pdf_path):
    tables = camelot.read_pdf(pdf_path, pages="all")
    print(tables)
    for i, table in enumerate(tables):
        print(f"Table {i + 1}:")
        print(table.df)
        print("\n")


if __name__ == '__main__':
    camelot_extract('test.pdf')

 

 

3. tabula-py

설치 방법:

# tabula-py는 tabula-java를 기반으로 포팅된 라이브러리이다.
# 따라서, Java 8 이상 버전의 자바가 설치되어 있어야 한다.

# jpype는 jvm 라이브러리를 파이썬에서 호출할 수 있게 도와주는 기능을 제공. (함께 설치해야 정상 작동)
pip install "tabula-py[jpype]"

 

tabula-py는 tabula-java의 파이썬 포팅 버전으로, pdf에서 인식된 표들을 dataframe으로 변환해주는 기능을 제공한다.

tabula-java라는 JVM 기반 라이브러리를 사용하기 때문에 준수한 성능을 보장한다는 특징을 가진다.

 

개인적인 실험 결과로는 camelot보다는 높은 성능을 보이나 pdfplumber보다는 낮은 성능을 보인다는 점이 아쉬웠다.

아래는 pdf에서 표들을 추출해서 출력하는 예제 코드이다.

 

import tabula


def tabula_extract_to(pdf_path, output_path):
    """표를 추출한 뒤, 각각 csv 파일로 저장"""
    tabula.convert_into(pdf_path, output_path, output_format="csv", pages='all', lattice=True)

def tabula_extract(pdf_path):
    """표를 추출한 뒤, 각각 print"""
    dfs = tabula.read_pdf(pdf_path, pages='all', lattice=True)
    for i, df in enumerate(dfs):
        print(f"Table {i + 1}:")
        print(df)
        print("\n")

 

위의 예제에서 "lattice=True" 옵션을 사용하면 하나의 셀이 여러 줄로 이루어져 있는 경우 등을 적절히 처리할 수 있다.

 

반응형