# install.packages("tabulapdf")
library(tabulapdf)
library(data.table)들어가며
보통 데이터는 csv, xlsx, 혹은 sas나 rtf 형식으로 받습니다. 이런 형식들은 읽어오기도 편하고, 데이터의 무결성도 유지하는 확실한 방법들입니다.
하지만 간혹 pdf 파일에 있는 데이터를 가져와야 하는 경우가 있습니다. 이럴 때는 어떻게 해야 할까요?
저 역시 그런 상황을 겪었는데, 복사-붙여넣기도 제대로 되지 않아 많이 당황했습니다. 수많은 데이터가 담긴 pdf를 일일이 타이핑하는 것은 부정확할 뿐만 아니라 시간도 너무 오래 걸리는 일입니다.
바로 이때 나타난 해결사, pdf에 있는 테이블을 추출해주는 tabulapdf 패키지를 소개합니다.
패키지 설치 및 로드
tabulapdf는 Java를 기반으로 만들어진 패키지입니다. 다행히 별도의 복잡한 Java 설정 없이 패키지 설치만으로 대부분 작동합니다.
기본 사용법
가장 기본적이면서 강력한 함수는 extract_tables입니다. 이 함수는 pdf 파일의 모든 페이지를 스캔하며 테이블을 인식하고, 이를 R에서 다루기 쉬운 tibble 등의 리스트 형태로 반환해 줍니다. 한 페이지에 여러 테이블이 있더라도 일일이 다 스캔하여 반환해줍니다.
다음은 패키지에 내장된 예제 mtcars.pdf를 활용한 코드입니다.
# 예시 데이터
f <- system.file("examples", "mtcars.pdf", package = "tabulapdf")
# 전체 페이지 추출
extract_tables(f)[[1]]
# A tibble: 5 × 12
model mpg cyl disp hp drat wt qsec vs am gear carb
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Mazda RX4 21 6 160 110 3.9 2.62 16.5 0 1 4 4
2 Mazda RX4 W… 21 6 160 110 3.9 2.88 17.0 0 1 4 4
3 Datsun 710 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1
4 Hornet 4 Dr… 21.4 6 258 110 3.08 3.21 19.4 1 0 3 1
5 Hornet Spor… 18.7 8 360 175 3.15 3.44 17.0 0 0 3 2
[[2]]
# A tibble: 1 × 5
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<chr> <chr> <chr> <chr> <chr>
1 "5.10\r4.90\r4.70\r4.60\r5.00" "3.50\r3.00\r… "1.40\r1.40… "0.20\r0.2… "setos…
[[3]]
# A tibble: 5 × 3
len supp dose
<dbl> <chr> <dbl>
1 4.2 VC 0.5
2 11.5 VC 0.5
3 7.3 VC 0.5
4 5.8 VC 0.5
5 6.4 VC 0.5
이런 기본 기능만으로도 훌륭하지만 모든 페이지를 탐색할 필요 없이 특정 페이지만 보고 싶다면 pages 옵션을 사용하면 됩니다. 처리 시간도 단축되고, 추출된 결과물을 확인하기도 훨씬 수월해집니다.
# 특정 페이지 추출
extract_tables(f, pages = 1)[[1]]
# A tibble: 5 × 12
model mpg cyl disp hp drat wt qsec vs am gear carb
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Mazda RX4 21 6 160 110 3.9 2.62 16.5 0 1 4 4
2 Mazda RX4 W… 21 6 160 110 3.9 2.88 17.0 0 1 4 4
3 Datsun 710 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1
4 Hornet 4 Dr… 21.4 6 258 110 3.08 3.21 19.4 1 0 3 1
5 Hornet Spor… 18.7 8 360 175 3.15 3.44 17.0 0 0 3 2
여러 페이지를 원한다면 벡터로 입력하면 됩니다.
# 여러 페이지 추출
extract_tables(f, pages = c(1, 3))[[1]]
# A tibble: 5 × 12
model mpg cyl disp hp drat wt qsec vs am gear carb
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Mazda RX4 21 6 160 110 3.9 2.62 16.5 0 1 4 4
2 Mazda RX4 W… 21 6 160 110 3.9 2.88 17.0 0 1 4 4
3 Datsun 710 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1
4 Hornet 4 Dr… 21.4 6 258 110 3.08 3.21 19.4 1 0 3 1
5 Hornet Spor… 18.7 8 360 175 3.15 3.44 17.0 0 0 3 2
[[2]]
# A tibble: 5 × 3
len supp dose
<dbl> <chr> <dbl>
1 4.2 VC 0.5
2 11.5 VC 0.5
3 7.3 VC 0.5
4 5.8 VC 0.5
5 6.4 VC 0.5
테이블 형태에 따른 설정
extract_tables 를 이용하여 테이블을 가져올 때 크게 두 가지 방식이 있습니다.
-
lattice(기본값): 테이블에 테두리나 선이 명확하게 있는 경우 사용합니다. -
stream: 테이블에 테두리나 선이 없는 경우 사용합니다.
일반적인 테이블은 method = "lattice" 로 인식이 잘 됩니다. 여기서 일반적인 테이블이란 테두리와 경계선이 모두 그려진 규칙적인 테이블을 의미합니다.
하지만 분명히 행렬이거나 테이블은 맞는데, 선이 없는 디자인의 테이블이라면 알고리즘이 표를 인식하기 힘들어합니다. 그럴 때 method = "stream"을 씁니다. 이 방법은 문자열 사이의 공백을 기준으로 컬럼을 구분하기 때문에, 선이 없거나 한 페이지에 열 개수가 다른 여러 테이블이 섞여 있을 때 훨씬 정확한 결과를 얻을 수 있습니다.
밑에 예시를 보시면 기본 메소드로는 테이블을 못 읽고, method = "stream" 인 경우에 제대로 인식합니다.
# 선이 없는 테이블 추출 시 (lattice) 사용 - 부정확
extract_tables(f, pages = 2, method = "lattice")[[1]]# A tibble: 1 × 5
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<chr> <chr> <chr> <chr> <chr>
1 "5.10\r4.90\r4.70\r4.60\r5.00" "3.50\r3.00\r… "1.40\r1.40… "0.20\r0.2… "setos…
# 선이 없는 테이블 추출 시 (stream) 사용 - 정확
extract_tables(f, pages = 2, method = "stream")[[1]]# A tibble: 5 × 5
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<dbl> <dbl> <dbl> <dbl> <chr>
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5 3.6 1.4 0.2 setosa
마우스로 드래그해서 가져오기
만약 페이지 전체가 아니라 특정 구역의 테이블만 콕 집어서 가져오고 싶다면 어떻게 해야 할까요?
이 패키지의 가장 혁신적인 기능인 locate_areas와 extract_areas를 사용하면 됩니다.
이 함수들을 실행하면 RStudio의 Viewer 패널에 pdf가 한 페이지씩 렌더링됩니다. 실제로 해보시면 느끼시겠지만, Shiny 인터페이스와 연동되어 작동하는 방식이 굉장히 직관적입니다.
사용자는 마우스로 원하는 구역을 드래그하고 오른쪽 위의 ’완료’를 누르기만 하면 됩니다. 함수에 여러 페이지를 넣었다면, 모든 페이지에서 드래그 후 완료를 눌러야 테이블 추출 작업이 끝나고 반환해줍니다. 드래그할 내용이 없는 페이지는 바로 완료 누르고 넘어가면 됩니다.

마우스로 드래그해서 가져오는 두 함수의 기능은 각각 다음과 같습니다.
locate_areas: 드래그한 구역의 좌표를 반환합니다.extract_areas: 드래그한 구역의 데이터(테이블)를 즉시 반환합니다.
좌표 확인하기
locate_areas는 구역을 고르면 그 구역의 각 꼭짓점의 좌표를 반환해줍니다. 한 페이지에 하나의 구역 설정을 할 수 있습니다.
# 실행 후 영역 드래그 -> 좌표 반환
locate_areas(f)
테이블 바로 추출하기
좌표 없이 바로 드래그를 하고 싶으시다면 extract_areas로 구역을 정하면 바로 그 테이블을 반환합니다. 마찬가지로 한 페이지에 하나의 구역 설정을 할 수 있습니다.
# 실행 후 영역 드래그 -> 테이블 반환
extract_areas(f)복잡한 헤더 구조 해결하기
여러 종류의 pdf를 다루다 보면 테이블 구조가 복잡해서 인식이 잘 안될 때가 있습니다. 특히 데이터나 헤더가 여러 열에 걸쳐 병합되어 있는 경우, extract_tables나 extract_areas를 쓰더라도 열이 뭉개지거나 하나로 합쳐져서 반환되는 문제가 발생합니다.
이럴 때는 문제가 되는 헤더 부분을 제외하고, 규칙적인 데이터 영역만 스캔하는 것이 좋습니다. 이렇게 데이터만 먼저 긁어오고, 헤더는 R에서 후작업으로 추가하는 방식이 더 정확합니다.
다음은 패키지에 내장된 예제 covid.pdf를 활용한 코드입니다. 제일 상단 행이 병합되어 있어 그대로 긁어오면 열이 깨집니다. 따라서 데이터 영역만 좌표로 지정하고 col_names = FALSE 옵션을 주어 깔끔하게 가져오는 방법을 씁니다.
# 예시 데이터
f <- system.file("examples", "covid.pdf", package = "tabulapdf")
# locate_areas(f)
# 헤더를 제외한 데이터 영역만 추출 (col_names = FALSE)
covid_list <- extract_tables(f, pages = 1,
guess = FALSE, # area를 좌표로 입력할 때 꼭 쓰기
col_names = FALSE, # 헤더 자동 인식 끄기
area = list(c(120.6259, 92.2500, 373.5551, 633.3750 )) # 좌표
)
covid_dt <- as.data.table(covid_list[[1]])
# 필요한 경우 여기서 setnames() 등을 통해 컬럼명 지정
# setnames(covid_dt, c("Col1", "Col2", ...))
covid_dt X1 X2 X3 X4 X5
<char> <char> <char> <char> <char>
1: Regione Remdesivir Inc% Remdesivir Molnupiravir
2: Abruzzo 2.343 3,0% 27 23
3: Basilicata 927 1,2% - -
4: Calabria 1.797 2,3% - -
5: Campania 3.289 4,1% 20 12
6: Emilia Romagna 7.945 10,0% 244 17
7: Friuli Venezia Giulia 1.063 1,3% 11 59
8: Lazio 11.206 14,1% 63 219
9: Liguria 5.332 6,7% 87 207
10: Lombardia 12.089 15,3% 239 114
11: Marche 3.739 4,7% 10 85
12: Molise 42 0,1% - 5
13: Piemonte 5.593 7,1% 50 203
14: Prov. Auton. Bolzano 188 0,2% - 11
15: Prov. Auton. Trento 211 0,3% - 3
16: Puglia 4.439 5,6% 13 66
17: Sardegna 757 1,0% - 29
18: Sicilia 4.383 5,5% 2 30
19: Toscana 5.916 7,5% 106 161
20: Umbria 1.583 2,0% 9 42
21: Valle D'aosta 351 0,4% - 20
22: Veneto 6.072 7,7% 20 186
23: Italia 79.265 100,0% 901 1.492
X1 X2 X3 X4 X5
X6 X7 X8
<char> <char> <char>
1: Totale per regione Remdesivir % Molnupiravir %
2: 50 3,0% 1,5%
3: - 0,0% 0,0%
4: - 0,0% 0,0%
5: 32 2,2% 0,8%
6: 261 27,1% 1,1%
7: 70 1,2% 4,0%
8: 282 7,0% 14,7%
9: 294 9,7% 13,9%
10: 353 26,5% 7,6%
11: 95 1,1% 5,7%
12: 5 0,0% 0,3%
13: 253 5,5% 13,6%
14: 11 0,0% 0,7%
15: 3 0,0% 0,2%
16: 79 1,4% 4,4%
17: 29 0,0% 1,9%
18: 32 0,2% 2,0%
19: 267 11,8% 10,8%
20: 51 1,0% 2,8%
21: 20 0,0% 1,3%
22: 206 2,2% 12,5%
23: 2.393 37,7% 62,3%
X6 X7 X8
일관성 있는 테이블 추출을 위한 팁
두 가지 접근 방식이 있습니다.
일회성 작업: 단순히 엑셀이나 CSV로 한 번 다운로드하는 것이 목적이라면
extract_areas로 바로 데이터를 뽑는 것이 편합니다.반복 작업: 만약 코드를 저장해두고 계속 반복해서 실행해야 한다면,
locate_areas를 한 번 사용해 좌표를 먼저 얻는 것을 추천합니다. 얻어낸 좌표를extract_tables(..., area = ...)에 입력하여 사용하면, 매번 마우스로 드래그할 필요 없이 일관성 있는 결과를 얻을 수 있습니다.
마무리
tabulapdf 패키지 덕분에 PDF에서 데이터를 추출하는 과정이 획기적으로 간단해졌습니다. 단순히 좌표를 입력하는 것을 넘어 GUI 환경에서 직접 드래그하여 구역을 설정할 수 있다는 점이 매우 편리했습니다. 테이블이 복잡할수록 읽는 데에 어려움이 있지만, 규칙적으로 읽어주기 때문에 가져올 수 있는 부분이라도 가져온 후에 전처리 하는 것이 중요하다고 생각합니다. 앞으로 다른 패키지들도 이러한 기능을 도입하여 분석가들의 작업 효율을 높여주길 기대해 봅니다.
참고 자료
이 글에서 소개한 tabulapdf 패키지에 대한 더 자세한 정보나 소스 코드는 아래 링크에서 확인하실 수 있습니다.
- tabulapdf GitHub Repository: 패키지 개발자의 깃허브입니다.
- CRAN Package Page: CRAN 공식 페이지입니다.
Reuse
Citation
@online{hahn2025,
author = {Hahn, Wonbin},
title = {마우스 {드래그로} {끝내는} {PDF} {테이블} {추출:} Tabulapdf},
date = {2025-12-17},
url = {https://blog.zarathu.com/posts/2025-12-17-tabulapdf/},
langid = {en}
}