PDF를 파싱하고 싶으신가요?
(eliot-jones.com)- PDF 파싱은 명확한 순서와 구조를 바탕으로 작동해야 하나, 실제 파일은 이 규격을 자주 따르지 않음
- cross-reference(xref) 포인터와 오프셋 찾기에서 다양한 오류와 불일치가 발생함
- 실제로는 PDF 헤더 앞의 불필요한 데이터나 포인터, 오프셋의 잘못된 위치로 인해 많은 문제가 생김
- PDF xref 테이블 자체가 명확하지 않거나 잘못 형식화되어 있는 사례도 많음
- 그래서 주요 뷰어들은 비표준 PDF 파일까지 지원하는 로직을 추가 구현하고 있음
PDF 파싱에 대한 이상적 접근
-
PDF 파싱은 이론적으로 일정한 단계로 진행됨
- 파일 시작 부분에서 버전 헤더 주석을 찾음
- cross-reference(xref) 포인터를 찾음
- 모든 객체 오프셋을 수집함
- trailer 딕셔너리를 찾아서 전체 카탈로그 구조에 접근함
PDF 객체 소개
- PDF 객체는 넘버, 문자열, 딕셔너리 등 여러 PDF 요소를 감싸서 저장하는 단위임
- 각 객체는 "
obj
/endobj
" 마커 사이에 존재함 - 객체들은 간접 참조(indirect reference, 예: "16 0 R") 방식으로 서로 연결됨
- 파일 내 객체 분할 방식은 자유롭지만 일부 객체 유형은 반드시 간접참조여야 함
cross-reference 오프셋 찾기
- PDF에는 구조상 cross-reference(xref) 테이블이 있는데, 이는 객체 위치의 인덱스 역할을 함
- 파일 끝에 "startxref" 구문으로 특정 바이트 위치가 포인터로 명시됨
- 이 포인터가 xref 위치를 지정하지만, 스펙과 실제 파일에서 차이가 있음. 예를 들어 "%EOF" 마커가 본래 마지막 줄이어야 하지만, 현실 PDF에서는 1,024바이트 이내 어디든 있을 수 있음
- 실제 파일에서 포인터의 형식 오류(startref 등), 줄바꿈 누락 등 다양한 변형이 발견됨
객체 오프셋 찾기
- xref 테이블은 "xref", 객체 시작 번호, 객체 수가 차례로 이어지고, 각 객체의 오프셋/생성번호/상태(n 또는 f)가 한 줄에 기록됨
- xref 테이블이 여러 개거나, /Prev엔트리를 통해 서로 연결될 수도 있음
trailer 딕셔너리 위치 탐색
- startxref 마커 윗부분에 trailer 딕셔너리가 존재하며, 루트 객체를 찾을 수 있는 필수 메타데이터가 포함됨
- 루트 객체를 기준으로 전체 구조 해석 착수 가능
실제 환경: 예상치 못한 문제점들
-
PDF 스펙을 준수하지 않는 파일이 많아 일반적인 파서로는 처리가 어려움
-
cross-reference 포인터 탐색에서 흔히 실패하는 경우
- 포인터가 파일 끝 또는 마지막 1,024바이트에 없음
- 오탈자(startref 등)
- 예외적 형식
-
3,977개 실제 PDF 샘플 조사에서 약 0.5%가 xref 선언 오류를 가짐
PDF 콘텐츠가 0이 아닌 오프셋에서 시작
- 헤더 앞에 쓸모없는 데이터(junk) 가 있으면 모든 바이트 오프셋이 밀려 startxref 위치가 어긋남
- 헤더 위치를 기준으로 오프셋을 재계산해야 하며, 두 위치 모두 확인해야 함
- 전체 오류의 약 50%를 차지
xref 포인터가 xref 테이블 중간을 가리킴
- 지정된 오프셋이 xref 테이블 내용 한가운데로 이동할 수도 있음
- 3,977개 샘플 중 약 5건에서 발견
포인터가 xref 근처에 있음
- 종종 포인터가 정확하지 않지만, xref 바로 앞이나 다음의 공백, 개행문자 오차만큼 어긋난 경우가 많음
포인터는 맞으나 xref 오프셋이 잘못됨
- xref 표에 기록된 오프셋 자체가 잘못될 수도 있음
- 일부 객체만 올바르고 나머지는 offset 오류를 가질 수도 있음
첫 포인터는 정상인데 이전 오프셋(/Prev)이 이상함
- PDF를 수정하며 생성되는 /Prev 포인터에 잘못된 값(예: 0)이 저장된 사례 다수
xref 테이블 형식이 비정상임
- 줄바꿈 없이
"xref"
와 숫자가 붙거나, 선언된 객체보다 더 많은 항목이 있거나, 표 중간에 쓰레기 데이터가 포함된 경우 다양하게 나타남 - 이러한 사례는 PdfPig 등에서 issue로 다수 보고됨
결론
- 명세에 따르면 PDF 파싱은 정형화된 순서로 처리되어야 하지만, 실제 파일 다수는 그렇지 않아서 파싱에 다양한 문제가 발생함
- 실사용 PDF 뷰어들은 비규격 PDF 지원 확대 기능을 기본적으로 포함함
- 이번 요약 내용은 PDF 명세(총 1300페이지 중 22페이지)에 해당하는 일부분 파싱만을 다루었음
이번 요약 내용은 PDF 명세(총 1300페이지 중 22페이지)에 해당하는 일부분 파싱만을 다루었음 <-... 1300페이지 어마무시하네요...
Hacker News 의견
-
답은 명확함
- PDF는 원하는 모든 포맷의 메타데이터 첨부를 지원함
- 모든 PDF 생성 소프트웨어가 동일한 정보를 기계가 읽기 쉬운 방식으로 첨부해야 함
- 그러면 PDF를 파싱하려는 사람은 메타데이터만 바라보면 됨
현실적으로, 내 이름이 Geoff인데, 이력서 파서 중 절반이 내 이름을 "Geo"와 "ff"로 따로 인식함
이는 텍스트가 PDF에 들어가는 방식 때문이며 여러 소스 앱에서 계속 발생하는 문제임
-
PDF 파싱과 PDF 콘텐츠 파싱은 완전히 다름
PDF 파일을 파싱하는 것도 골치 아프지만, PDF 자체가 "지정 위치에 뭔가 찍기" 기반이라, 경계 박스 내의 잘 정의된 텍스트와는 달라서 단어 추출하려면 어떤 글자가 함께 있는지 추측해야 하는 상황임
이력서 파서를 돕고 싶으면, 접근성 트리(Accessibility tree)에 주목해볼 만함
모든 PDF 렌더러가 접근성 PDF를 내보내는 건 아니지만, 접근성 PDF가 그나마 이름 같은 걸 제대로 읽어오게 도와줄 수 있음
"ff" 문제는 아마도 비ASCII 문자(예: ff 리가처)를 이력서 분석기가 처리 못하는 경우임
PDF 렌더러가 리가처를 생성하지 않도록 설정 가능하지만, 이러면 텍스트가 보기 흉해질 수 있음 -
"해야한다(should)"라는 단어에 많은 걸 기대하는 느낌임
PDF 사용이 실제로는 꽤 적대적이면 사람들이 그 정도로 생각하지 않는 것 같음
이력서를 PDF로 내는 것부터 중간 유통자가 못 고치게 하려는 목적이 있고, "편집"도 이미지 위에 박스 그어서 가리기, 표를 CSV 대신 PDF로 만들어 분석 어렵게 만드는 등 다양한 이유가 있음 -
실제로 이 방법이 잘 통하는 경우도 있음, 일부 앱에서 이 방식을 사용하고 있음
다만, 두 가지 표현(본문/메타데이터)이 실제로 일치하지 않는 문제가 남아있음 -
손글씨 스캔이나 다른 스캔 문서는 스캐너 및 일반 가정용 컴퓨터가 완벽한 OCR 지원을 하지 않으면 어떻게 하냐는 의문이 있음
-
아마도 ff가 리가처로 렌더링돼서 생기는 문제 같음
-
Tensorlake 창업자임
개발자를 위한 문서 파싱 API를 만들었음
PDF 파싱에서 Computer Vision 방식이 실제 현장에서 잘 동작하는 이유임
파일 내 메타데이터에만 의존하는 것은 다양한 PDF 소스에서는 확장성이 없음
그래서 PDF를 이미지로 변환 후, 레이아웃 인식 모델을 먼저 적용, 이어서 텍스트 및 표 인식 등 특화 모델 돌린 후 조각을 합쳐서, 정확성이 필수인 분야에서도 쓸 만한 결과를 얻는 방식임-
이런 방식이 언뜻 보면 우습지만, 실은 가장 현실적인 해결책 같음
PDF는 본질적으로 사람이 읽을 레이아웃을 표현하기 위해 고안된 포맷이라 컴퓨터가 읽도록 설계된 게 아니라, 보기 좋은 디스플레이에 초점을 맞춘 포맷임
그렇기에 인간이 읽는 방식을 흉내 내는 접근법이 이치에 맞게 느껴짐
다만 30년 넘는 시간 동안 PDF가 기계 판독성을 더하지 못한 점은 아쉬움
어떤 인센티브가 부족했길래 이걸 가능하게 하지 못했는지 궁금함
혹시 이에 대해 통찰이 있는 사람이 있으면 듣고 싶음 -
약간 우스운 점임
PDF를 인쇄해서 스캔 후 이메일 보내는 건 비웃음거리 감인데, PDF 파싱에선 사실상 똑같은 짓을 하는 셈임
그런 접근법이 필요하다는 게 답답한 현실임
세상은 HTML을 그렇게 파싱하진 않음 -
Nutrient.io 공동 창업자임, 10년 넘게 PDF 다루고 있음
웹브라우저처럼 PDF 뷰어들은 엄청나게 다양한 PDF를 받아야 함
PDF가 워낙 오래돼서, 파일 생성자들이 본인이 쓰는 뷰어에서만 잘 보이면 되도록 임의로 수정하기 때문임
그래서 우리 회사는 AI 문서 처리 SDK(REST API, PDF를 입력하면 JSON으로 구조화된 데이터 반환)를 만들었음
시각적 방법뿐 아니라 구조적 전처리/후처리 경험으로, 순수 비전 기반 대비 성능/비용 모두 더 나은 결과 제공함
직접 PDF 처리를 고민하고 싶지 않고 본연의 일에 집중하고 싶다면 도움이 될 수 있음
https://www.nutrient.io/sdk/ai-document-processing -
PDF 내부 구조 전문가가 있는 김에 질문이 있음
왜 mupdf-gl이(기본 데스크톱 리눅스 기준) 다른 모든 프로그램보다 훨씬 빠른지 궁금함
대용량 PDF 검색 속도가 확연히 월등한데, 왜 다른 뷰어들은 이렇게 빠를 수 없는지 항상 궁금했음
관련 통찰이 있다면 듣고 싶음 -
결국 PDF를 이미지로 렌더링할 때 사용하는 소프트웨어에 파싱 작업을 외주 준 셈임
-
-
오래전부터 레이아웃 위주의 문서 커뮤니케이션에서 벗어나야 한다고 생각함
즉, 전문적으로 꾸민 레이아웃 자체가 사실 옛날 관습에 더 가깝고, 실제 컨텐츠 이해와 거의 연관이 없다고 봄
예를 들어, 각종 규제기관 제출 서류는 엄청나게 두꺼운 문서들이고, 레이아웃 규칙을 맞추려면 Microsoft Word에서 오랜 시간 작업하게 됨
이런 레이아웃 보장을 위해 DOCX나 PDF 형식으로 제출하는데, 이 포맷들은 프로그램이 자동으로 내용 추출하거나 가공하기 매우 부적합함
LLM도 이 파일을 읽을 순 있지만, 단순한 기계 친화 파일(텍스트, markdown, XML, JSON 등)에 비해 계산 비용이 크게 듦대안으로 아예 '기계 우선', '내용 우선'의 단순한 포맷(JSON, XML, HTML 기반 등)을 표준화하는 접근 가능성 생각해봄
최소한의 구조 및 이미지 임베딩 정보만 있고, 인간이 읽을 땐 뷰어 앱으로 보기 좋게 재구성하면 됨
기계 처리는 훨씬 쉬움
이미 HTML/브라우저, EPUB 등 유사 포맷이 존재함에도, 고전 방식 대체가 필요한 시점이라 생각함
LLM 혁명이 이런 방향으로 이끌길 기대하고, 앞으로 비싼 PDF 파싱이 전통 파이프로 남기만을 바람- PDF 문제엔 동의하지만, DOCX가 실제로 그렇게까지 나쁜 거냐고 반문함
아직 DOCX 파서를 만들어본 적은 없지만, DOCX는 XML 기반이고, 명시적으로 레이아웃을 지정하지 않는 한 모든 게 절대 좌표화되지 않으니, JPEG이 0점, PDF가 15점, markdown이 100점이면, DOCX는 대략 80점쯤 되는 쉬운 난이도 아닐까 하는 추측임
- PDF 문제엔 동의하지만, DOCX가 실제로 그렇게까지 나쁜 거냐고 반문함
-
훌륭한 정리였다고 생각하며, 흥미롭게 느낀 추가 포인트가 있음
Incremental-save 체인: 첫 startxref 오프셋은 괜찮지만, Acrobat이 여러 번 수정할 때마다 반복 추가하는 /Prev 링크가 다음 xref로부터 몇 바이트 짧게 가리키는 경우가 많음
대부분의 뷰어(PDF.js, MuPDF, Adobe Reader까지)에서는 obj 토큰을 파일 전체에서 무식하게 찾아 새로운 테이블을 재구성하고, 명세 친화적 파서는 폭발하는 상황 발생
실제 필드에서 여러 애플리케이션이 반복 수정한 문서를 다루고 싶으면 이런 복구 경로(salvage path)는 필수적임- 맞는 지적임, 이건 샘플 집합에서 자주 보던 실패 케이스임
이전 레퍼런스 또는 체인 내 하나가 파일 바깥 오프셋, 0 오프셋, 잘못된 값 등으로 가리키는 경우가 많음
이 글을 쓰게 된 계기는 내 프로젝트 PdfPig의 초기 파싱 로직 개편 때문임
처음엔 Java PDFBox 코드를 이식했지만, 좀 더 빠르고 단순하게 바꾸고 싶었음
새 로직은 xref 테이블/스트림을 하나라도 놓치면 파일 전체를 스캔하며 복구 경로에선 해당 오프셋만 신뢰함
하지만 이전보다 확실히 느려졌고, 변화가 실제로 괜찮은지 확신이 어려움
1만 개 파일 테스트셋으로 각종 특이 케이스(엣지 케이스)를 탐색 중임
https://github.com/UglyToad/PdfPig/pull/1102
- 맞는 지적임, 이건 샘플 집합에서 자주 보던 실패 케이스임
-
잘 동작하는 가정과 적절한 PDF 오브젝트 파서만 있다면 쉬울 것 같지만 현실은 절대 그렇지 않다고 생각함
이 상황은 PDF 지옥과도 같음
PDF는 명세가 아니라 사회적 합의, '분위기' 수준임
버둥거릴수록 더 깊이 빠져들게 되고, 이제 우리 모두 신의 시야에서 멀어진 수렁에 살게 된 느낌임
이 얘기에 웃음이 났음- 이 글은 James Mickens가 쓴 것 같다는 농담임
-
"PDF를 파싱하고 싶은가"라는 질문에 대해, 절대 아니다라고 단언할 수 있음
이유는 원글에서 잘 설명됨-
제 은행이 좀 더 읽기 쉬운 포맷으로 자료를 제공해주면 좋겠지만, 그 전까진 어쩔 수 없음
-
이미 그 실수를 해봤고, 다시는 그러지 않으려 함
-
-
PDF 파서 작성 경험자로서, PDF는 정말 이상한 형식이라고 느낌
이건 이진(binary)과 텍스트의 혼합이라는 태생적 설계가 이런 괴상한 점을 만든 것 같음
조금 어설프게 부정확한 xref 오프셋 문제도 LF/CR 줄바꿈 변환 처리 중 버그에서 비롯된 경우라 추정함
글에서 언급 안 된 것 중 하나는, 최신 PDF(v1.5+)는 일반 텍스트 xref 테이블 없이 "xref stream"으로 담긴 경우가 많음
v1.6 이상에선 오브젝트 자체를 object stream에 담을 수도 있음- 나도 단순 xref 테이블 수준을 넘어서 스트림과 압축 이야기까지는 다루지 않은 게 의외였음
별문제 없어 보이다가도, 원하는 오브젝트가 스트림 안에 들어있고, 그 스트림 자체가 PNG 압축을 변형해서 쓰거나, 오프셋이 flate 압축된 xref stream 안에 있으면 골치 아파짐
게다가 여러 문서 버전이 뒤섞여 있어서 어디부터 어디까지가 최신인지 따지는 것도 복잡함
PDF 1.7 문서까지는 구하기 쉽지만, 불과 2년 전까지 PDF 2.0 명세 자료는 유료 벽에 막혀 있었음
- 나도 단순 xref 테이블 수준을 넘어서 스트림과 압축 이야기까지는 다루지 않은 게 의외였음
-
PDF는 스트리밍을 고려하지 않은 포맷임
끝에 위치한 trailer dictionary 때문에 파일 전체가 다 로드될 때까지 파싱하기 어렵게 만듦
다만, "스트리밍 가능한 PDF"도 존재해서, 처음 부분에 필요한 정보가 있으면 첫 페이지는 바로 렌더링 가능함(나머진 아닐 수 있음)
최근에는 PDF 쪽과 거리가 좀 있으니 감안해야 함-
풋터가 있더라도 웹사이트가 Range Request를 지원하고 Content-Length 헤더만 잘 써주면 PDF도 스트리밍이 가능함
스트리밍 리더는 HEAD 요청, 파일 맨 끝 수백 바이트 요청해 포인터와 테이블 구한 뒤 나머지 부분 이어 받으면 됨
실시간 생성 PDF엔 부적합해도, 꽤 오래된 웹 서버라면 1~2회 왕복시간만 추가되면 충분함
아쉽게도 파일별 Range 기반 파서를 신경 쓰는 사례가 드물지만, 기술적으로 불가능한 건 아니라고 생각함 -
맞음, Linearized PDF라는 형식이 있는데, 첫 페이지를 전체 파일 다운로드 없이도 빠르게 보여줄 수 있게 고안됨
요약에선 해당 방식은 부속 설명이 많아 생략했던 점 알려줌
-
-
파이썬 배우고 처음 도전한 프로젝트 중 하나가 PDF 파서였음
DnD 캠페인용 지도 자동 추출을 노렸는데, 결과는 실패였음(웃음) -
TIFF 리더를 작성해 본 적 있음
TIFF도 쓰기는 쉬운데 읽기는 어렵기로 악명이 높음
PDF도 똑같은 부류에 들어가는 것 같음