인보이스 명세 항목 자동으로 추출하기
인보이스와 영수증의 명세 항목을 정형화된 행으로 자동 추출합니다. 배열 필드 하나만 선언하면 항목마다 검증 가능한 행이 하나씩(각 행에 바운딩 박스까지) 나오고, 그대로 CSV로 내보낼 수 있습니다.
인보이스와 영수증은 사람들이 가장 디지털화하고 싶어 하는 문서지만, 정작 까다로운 건 헤더가 아닙니다. 거래처명, 날짜, 인보이스 번호 같은 단일 값은 OCR 모델이 한 번에 잡아냅니다. 골치 아픈 건 가운데에 있는 표입니다. 개수가 들쭉날쭉한 명세 항목마다 품목명·수량·단가가 붙어 있고, 이걸 합계를 내고 대사하고 장부에 올릴 수 있는 깔끔한 행으로 뽑아내야 하니까요.
이 글에서는 space-ocr로 인보이스 명세 항목을 자동으로 추출하는 방법을 소개합니다. 텍스트로 뭉개 버리는 게 아니라, 한 줄 한 줄이 각자의 행이 되고 셀 하나하나가 페이지에서 읽힌 정확한 위치를 가리키는 정형화된 배열로 뽑아내는 방식입니다. 표만이 아니라 문서 전체를 추출하려는 분이라면 더 폭넓게 다룬 인보이스·영수증 OCR 가이드부터 시작하세요.
핵심 비결: 명세 항목을 array 필드로 선언하기
대부분의 OCR API는 표를 통째로 문자열 하나로 추출하게 해 놓고, 파싱은 알아서 하라고 떠넘깁니다. space-ocr는 명세 항목 표를 스키마의 일부로 기술할 수 있습니다. type: "array" 와 children 목록을 가진 FieldSpec 하나가 엔진에게 이렇게 일러 주는 셈이죠. 이 영역은 반복되고, 반복될 때마다 이런 하위 필드들을 갖는다.
아래는 어떤 영수증의 스키마 예시입니다. 商品("항목") 필드는 배열이고, 그 children은 商品名(품목명), 数量(수량), 単価(단가)입니다:
{
"fields": [
{ "name": "店舗名", "type": "string", "description": "store name" },
{ "name": "日付", "type": "string", "description": "date" },
{ "name": "合計", "type": "string", "description": "total" },
{
"name": "商品",
"type": "array",
"description": "one row per line item",
"children": [
{ "name": "商品名", "type": "string", "description": "item name" },
{ "name": "数量", "type": "string", "description": "quantity" },
{ "name": "単価", "type": "string", "description": "unit price" }
]
}
]
}이걸 이미지와 함께 POST /ocr/fields 로 보내면 배열 필드가 리스트로 돌아옵니다. 이 영수증에서는 명세 항목 10개가 나옵니다. ポッカレモン100 은 359, シール割引 은 -34(할인 줄이라 부호가 그대로 보존됩니다), エキストラBオリー 는 698, 이런 식이죠. 행 파서도, 컬럼 분리 로직도, 정규식도 직접 짤 필요가 없었습니다. 구조를 한 번 선언했을 뿐입니다.
curl -s https://api.space-ocr.com/ocr/fields \
-H "Authorization: Bearer $SPACE_OCR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/receipt.jpg",
"imageType": "url",
"fields": [
{ "name": "total", "type": "string" },
{ "name": "items", "type": "array",
"children": [
{ "name": "description", "type": "string" },
{ "name": "qty", "type": "string" },
{ "name": "unit_price", "type": "string" }
] }
]
}'명세 항목 하나하나를 따로 검증할 수 있다
명세 항목 추출은 보통 여기서 어긋납니다. 모델이 겉보기엔 깔끔한 표를 내놓지만 미묘하게 어긋나 있는 거죠. 가격이 한 줄 위로 밀려 있거나, 품목명이 아래 줄과 뒤섞여 있거나. space-ocr에서는 배열 항목마다 자기만의 bbox, vertices, match_ratio, 그리고 children에 대한 field_bboxes 맵을 가집니다. 영수증의 한 줄은 이렇게 생겼습니다:
{
"商品名": "ポッカレモン100",
"単価": "359",
"match_ratio": 1.0,
"bbox_source": "vision_symbol_match",
"field_bboxes": {
"単価": {
"bbox": { "xmin": 450, "ymin": 356, "xmax": 484, "ymax": 378 },
"vertices": [
{ "x": 450, "y": 360 }, { "x": 483, "y": 356 },
{ "x": 485, "y": 374 }, { "x": 452, "y": 378 }
],
"match_ratio": 1.0
}
}
}그러니까 가격은 그냥 359 가 아닙니다. 0–1000 normalized 그리드 위의 특정 사각형(xmin/ymin/xmax/ymax, 좌상단 원점)에 자리 잡은 359 이고, 문서가 기울어진 각도를 따라가는 방향성 있는 vertices 네 개, 그리고 그 텍스트가 실제로 페이지에서 얼마나 발견됐는지를 알려 주는 match_ratio 가 함께 붙어 있죠. match_ratio 가 1.0 이면 모든 글자가 발견됐다는 뜻이고, 엔진은 ≥ 0.85 면 확신할 만한 매칭으로 봅니다. 추출한 행들을 match ratio로 정렬해서 가장 약한 것들만 눈으로 확인하면 됩니다. 전체 작동 방식은 바운딩 박스로 OCR 검증하기를 참고하세요.
모델이 그 좌표를 지어내는 게 아닙니다. 언어 모델이 반환하는 건 각 명세 항목의 텍스트, 그리고 어떤 단어 토큰을 썼는지에 대한 힌트뿐입니다. 박스는 절대 반환하지 않습니다. 그다음 엔진이 그 텍스트를 비전 OCR이 페이지에서 실제로 검출한 심볼들과 글자 단위로 맞춰 보면서, 각 값이 얼마나 발견됐는지를 match_ratio 로 보고합니다. 모델이 주는 토큰 힌트는 비슷비슷한 행이 반복될 때 부정확해지기 쉬워서, 그대로 믿는 대신 컬럼·행 일관성 검사로 검증합니다. 줄들이 서로 닮은 30행짜리 표에서 특히 중요한 대목이죠. 바로 이 점 덕분에 표가 그저 그럴듯한 수준에 그치지 않고 확인 가능한 것이 됩니다. 모든 줄이 페이지와 얼마나 잘 맞았는지를 점수로 달고 있으니까요.
한 줄을 클릭하면 바로 그 픽셀로
명세 항목마다 자기가 어디 있는지 알고 있으니, 표를 점검하는 일이 클릭 한 번으로 끝납니다. 앱에서 품목명이든 수량이든 단가든 아무 셀이나 클릭하면, 원본 이미지에서 그 값이 나온 정확한 영역이 확대 크롭과 함께 하이라이트됩니다. 30줄짜리 인보이스라도 페이지 전체를 훑을 필요 없이, 어딘가 이상해 보이는 그 한 줄로 시선이 바로 갑니다.
명세 항목에서 회계 도구가 읽는 CSV로
명세 항목을 일단 시트에 저장하고 나면, 내보내기 단계에서 배열 구조가 다시 한번 빛을 발합니다. space-ocr는 내보낼 때 배열 필드를 펼칩니다. 헤더는 # 와 스칼라 컬럼들에 더해, 배열 child마다 colName.childName 이름의 컬럼이 하나씩(즉 商品.商品名, 商品.数量, 商品.単価) 붙습니다. 명세 항목 하나하나가 각자의 하위 행이 되고요. 항목 10개짜리 영수증은 10개의 행이 되며, 모두 같은 매장명과 날짜를 달고 있습니다. 스프레드시트와 장부 임포터가 기대하는, 바로 그 길고 납작한 형식이죠.
이 영수증의 내보내기 결과를 간추리면 이렇게 보입니다:
| # | 店舗名 | 日付 | 商品.商品名 | 商品.単価 |
|---|---|---|---|---|
| 1 | KINSHO | 2019年08月17日 | ポッカレモン100 | 359 |
| 2 | KINSHO | 2019年08月17日 | エキストラBオリー | 698 |
| 3 | KINSHO | 2019年08月17日 | シール割引 | -34 |
파일은 BOM이 붙은 UTF-8이라 일본어·한국어·중국어 품목명도 Excel(엑셀)에서 깨지지 않고 깔끔하게 열립니다. 직접 수정한 값이 있으면 내보낼 때 그 값이 OCR 값을 덮어쓰지만, 원본은 기록에 그대로 남습니다. 이미지 폴더에서 스프레드시트까지 한 번에 이어지는 흐름은 스캔 문서를 CSV로를 참고하세요.
몇 단계로 끝내기
- 명세 항목용 배열 필드 정의하기fields[] 스키마에 type "array" 와 children 목록을 가진 필드를 추가하세요 — 예: description, qty, unit_price. 이렇게 하면 엔진에게 명세 항목 영역이 그 하위 필드들과 함께 반복된다고 알려 줍니다.
- 인보이스를 /ocr/fields 로 보내기이미지(URL 또는 base64)를 imageType, fields[] 와 함께 https://api.space-ocr.com/ocr/fields 로 POST 하세요. 배열 필드가 명세 항목당 객체 하나씩, 리스트로 돌아옵니다.
- 각 줄 검증하기모든 배열 항목은 자기만의 bbox, vertices, match_ratio를 가집니다. match_ratio로 정렬하거나 앱에서 셀을 클릭해 이미지의 정확한 영역으로 점프해 값을 확인하세요.
- CSV로 내보내기시트를 내보내면 — 배열 children이 colName.childName 컬럼으로 펼쳐지고 명세 항목 하나하나가 각자의 행이 되며, 문서 단위 필드가 반복돼 회계 도구에 바로 넣을 수 있는 형태가 됩니다.