-
세금명세서 ocr - (1) 딥러닝의 필요성을 느끼다.projects/ocr 2026. 7. 17. 18:55
0. 결과물
2026.07.16 - [projects/ocr] - 세금명세서 ocr - 릴리즈 노트
더보기결과물은 아래와 같다. cpu(gpu 필요 없음) 및 오프라인(인터넷 off)환경에서 작동한다.
ocr 글자 인식 성능을 개선중에 있기에 완벽한 자동화에 성공했다고 말하긴 어렵지만, 기존 작업대비 작업 효율을 높였다는 것엔 의심의 여지가 없다.
지인의 사무실 컴퓨터 기준으로 12개 파일 작업하는데 90초가량 걸렸다고 한다. (수동 검토 시간 제외)
사무실 컴퓨터 사양: Intel (R) Core (TM) Ultra 5 125U 1.30 GHz / 16ram

엑셀로 내보낸 파일 예시

1. 프로젝트 동기 및 후기의 방향.
지인과 대화중에, 반복작업이 귀찮다는 얘기가 나왔다. 요지는 아래와 같다.
"여러 업체로부터 받는 세금명세서를 엑셀에 타이핑하는 작업이 귀찮다."
처음 들었을 때 자동화하는 과정이 바로 떠오를 정도로 쉬울 것 같아서, 프로그램 뚝딱 해주리라 호기롭게 말했다.
하지만, 생각보다 어렵고 긴 과정이 기다렸다.
그 과정에서 겪은 문제해결과정과 학습한 내용을 정리하는게 이 후기의 목적이다.
2. 데이터 분석
아래 샘플사진은 인터넷에서 다운받은 세금명세서 양식에 chatGPT를 활용해서 가짜 데이터를 넣은 것이다.
주소를 보면 수원시에 서초구가 있다. (없는 주소를 만들줄이야. ChatGPT의 창의력이란!)


샘플마다 각 세부 양식은 다를 수 있지만 큰 틀은 동일하다.
크게 다섯가지 영역으로 구분할 수 있다.
제목란, 인적사항 기재란, 금액란, 품목 리스트, 영수 청구란
1. 어떤 양식인지 알 수 있는 제목란

2. 공급자-공급받는자를 포함하는 인적사항 기재란

3. 작성일자를 포함하는 금액란

4. 세부 품목 리스트

5. 합계금액을 포함하는 영수·청구란

격자로 나뉜 테이블에서 중요 헤드의 내용만 뽑아내면 됐다.
여기서 잠깐 용어를 정리하자. (아래 용어는 표준용어가 아니고, 설명을 위한 용어 정의다.)
헤더셀은 각 항목의 라벨을 담고 있는 셀을 의미한다.
- "주소", "공급자", "비고" 등의 글자가 담긴 cell들의 집합.
데이터셀은 특정 헤더에 대응되는 값을 담고 있는 셀을 의미한다.
- "594-64-17309", "백두산반도체물산코리아" 등의 글자가 담긴 cell들의 집합.
셀값은 어떤 셀이 담고 있는 text를 의미한다.
- 헤더셀의 셀값으로 "주소", "공급자", "비고" 가 있고, 데이터셀의 셀값으로 "594-64-17309", "백두산반도체물산코리아" 가 있다.
중요 헤드는 아래 7개다. (노란색 하이라이트)

- 인적사항 기재란
- 공급자의 등록번호
- 공급자의 상호(법인명)
- 금액란
- 작성일자
- 품목리스트
- 품목 일자
- 품목 이름
- 공급가액
- 세 액
3. 프로토타입(v 1.0.0)의 실패
v1.0.0은 rule-based 설계로 했다. 데이터를 봤을 때, 양식은 많아야 10개도 안 되었고, 그것들 마저도 큰 틀에서 같았다. 굳이 딥러닝으로 해결할 이유가 없다고 생각했다. 지인에게 받은 양식은 모두 pdf 였고, 지인에게 세부적으로 물어볼 생각도 안 하고 내 맘대로 만들었다. 서프라이즈(?)하고 싶은 마음도 있었다. 이 프로그램을 돌리면서 놀라는 지인의 표정을 떠올렸지만, 실망스러움이 담긴 피드백이 왔다.
더보기교훈:
의뢰자는 프로그래밍을 모르고 개발자는 도메인을 모른다. 의뢰자가 모든 디테일을 설명할 수 있을거라고 생각하지 말고, 나 역시 디테일을 임의로 판단하지 말자. 가장 좋은 방법은 현장에 들어가서 문제상황을 직접 관찰하는 것인데, 그렇지 못한 상황에선 질문하는 것이 중요하다. 의뢰자는 바쁘고, 개발자의 관점을 알지 못한다. 요구사항을 정확히 끄집어 내기 위한 소통 과정이 매우 중요함을 느꼈다.
3.1 . 라이브러리 선택 - pdfplumber
쉽고 빠르게 구현가능한 파이썬을 선택했고, 'pdf 테이블 검출' 키워드로 ChatGPT에게 물어보고 pdfplumber 라이브러리를 선택했다.
더보기교훈:
바쁘다고, AI가 하는 말을 곧이 곧대로 믿고 자의적으로 해석하다가는 오히려 돌아가는 수가 있다.
이번 경우가 그렇다. 어떤 라이브러리가 좋은지 찾느라 시간 보내느니, 아무거나 하나 잡고 빠르게 개발하는게 나을거라 생각했다.
하지만 추후 알게되었지만, 내 개발목적에 맞는 더 나은 라이브러리가 있었다.
여러 라이브러리를 직접 비교 테스트하자. AI 덕분에 테스트 과정이 쉬워졌다. 테스트를 위해 라이브러리 API 사용법을 익히는데 드는 노력조차 없어졌다.
3.2. pdfplumber - extract_table()으로 테이블 데이터 추출.
import pdfplumber # PDF 파일 경로 설정 pdf_path = "data/샘플.pdf" # pdfplumber를 사용하여 PDF 파일 열기 with pdfplumber.open(pdf_path) as pdf: # PDF의 모든 페이지를 순회 for i, page in enumerate(pdf.pages): # 페이지에서 표 추출 # extract_table()은 해당 페이지의 가장 큰 테이블을 추출 table = page.extract_table() # table은 라인(row)별로 리스트에 담아 저장한 2차원 리스트다. for i, line in enumerate(table): cleaned_line = [x for x in line if x is not None] print(f"{i}번째 라인: {len(cleaned_line)} / {len(line)}") print(cleaned_line)pdfplumber 에서 모든 페이지를 순회하고, extract_table()로, 해당 페이지의 가장 큰 테이블을 추출한다.
0번째 라인: 1 / 19 ['전자세금계산서 승인번호 20250316-66156126-65695411'] 1번째 라인: 10 / 19 ['공', '등록\n번호', '913-97-90682', '종사업장\n번호', '0003', '공', '등록\n번호', '630-01-63410', '종사업장\n번호', ''] 2번째 라인: 8 / 19 ['상호\n(법인명)', '(주)우성오토모티브글로\n벌코리아', '성명', '신채원', '상호\n(법인명)', '㈜솔뫼인더스트리물산\n코리아', '성명', '전광수'] 3번째 라인: 6 / 19 ['급', '사업장\n주소', '경상남도 창원시 유성구 판교역로 123', '급\n받', '사업장\n주소', '경상남도 창원시 서초구 산업단지1로 288, 롯데테크노 20층\n1083호'] 4번째 라인: 10 / 19 ['자', '업태', '건설업', '종목', '실내건축', '는\n자', '업태', '도매 및 소', '매업종목', '산업용기계'] 5번째 라인: 6 / 19 ...셀의 line(row) 수와 열(column) 개수는 테이블을 이루는 병합전의 최소 기준선의 개수다.
이해를 돕기 위해 보조선을 그려봤다. 열의 개수가 19개 임을 알 수 있다.


- extract_table() 의 테스트 결과.
1. "공급자"와 같이 세로 쓰기로 된 헤더셀값들은 "공", "급", "자"가 쪼개져서 서로 다른 라인으로 분리되는 경우가 있었다.
2. 리스트의 순서는 셀의 x 좌표의 오름차순이었다.
자세한 extract_table()의 작동원리는 다음에 포스팅해서 정리하겠다.
3.3. brudeforce 알고리듬으로 구현.
결국 추출하고자 하는 데이터값들은 아래와 같이 노란색 하이라이트된 셀값들이다.

추출해야하는 데이터값들을 살펴보자.
- 등록번호는 "000-00-00000" 형식으로 고정되어있고, 작성일자 역시 날짜 형식으로 고정되어 있다. 정규표현식으로 검출하기 쉽다.
- 문제는 품목과 상호, 그리고 공급가액과 세액이다. 이것들만 검출할 수 있는 함수를 만들면, 모든 데이터값을 검사하기만 하면 된다.
바로 꼼수가 생각났다.
1. 품목, 공급가액, 세액에 경우 몇 번째 요소인지 인덱스로 찾을 수 있다.
3번째가 품목이고, 7번째가 공급가액, 8번째가 세액이다.2. 상호는 첫번째 '상호' 요소 다음 요소가 찾고자 하는 공급자의 상호다.
3.4. 사소한 문제 해결과 검증 절차
구현중에 사소한 문제들이 있었다.
너무 많아서 다 나열하면 끝이 없고, 그 중 가장 간단한(최약체) 문제 하나를 소개하겠다.
날짜 형식이 연도월일의 8개의 숫자로 이뤄진 경우도 있고, 각각이 다른 셀로 나뉜 경우도 있었다. -> 날짜 형식은 경우의 수가 적기 때문에 모든 형식에 대해 정규표현식으로 검사. -> 셀이 나뉜 경우 역시 앞서 사용한 인덱스 아이디어를 적용.마지막으로 검증 과정을 추가했다.
예를 들면 아래와 같다.
1. 미리 회사 상호와 등록번호를 입력 받아서 같은지 검증. -> 최초 1회만 입력하면 됨. 2. 공급가액과 세액의 총합계가 테이블 최하단의 "합계금액"과 일치하는지 확인.4. 프로토타입의 실패와 고찰
지인이 준 피드백은 짧았다.
"안 되는 게 너무 많은데?"
몇차례 대화가 오가고 문제를 파악했다.
1. 세금명세서 문서가 이미지 형식으로 저장된 pdf 파일이 대다수다.
-> 글자 검출이 가능한 pdf라는 전제로 짰기 때문에 애초에 검출이 될리가 없다. 시작부터 틀려먹었다.
2. 글자 검출이 가능한 pdf 에서도 문제가 있었다.
-> 분석한 양식과 다른 양식이 존재했다. 기출변형도 있었고, 전혀 다른 새로운 양식도 존재했다.
-> 생각 이상으로 다양한 양식으로 인해, 룰 기반의 코딩은 너무나 취약했다.새로운 양식이 생길때마다 그에 맞게 코드를 업데이트해줘야하는 문제와 이미지로부터 글자를 추론해야하는 문제를 해결해야했다.
이미지로부터 글자 추론 문제와 다양한 양식을 커버해야하는 문제를 해결하기 위해서 딥러닝기반으로 전환할 필요성을 느꼈다.
'projects > ocr' 카테고리의 다른 글
세금명세서 ocr - 릴리즈 노트 (0) 2026.07.16