space ocr
가이드아티클요금문서
receipts

인보이스 명세 항목 자동으로 추출하기

인보이스와 영수증의 명세 항목을 정형화된 행으로 자동 추출합니다. 배열 필드 하나만 선언하면 항목마다 검증 가능한 행이 하나씩(각 행에 바운딩 박스까지) 나오고, 그대로 CSV로 내보낼 수 있습니다.

7 분 분량· 2026-06-25

인보이스와 영수증은 사람들이 가장 디지털화하고 싶어 하는 문서지만, 정작 까다로운 건 헤더가 아닙니다. 거래처명, 날짜, 인보이스 번호 같은 단일 값은 OCR 모델이 한 번에 잡아냅니다. 골치 아픈 건 가운데에 있는 표입니다. 개수가 들쭉날쭉한 명세 항목마다 품목명·수량·단가가 붙어 있고, 이걸 합계를 내고 대사하고 장부에 올릴 수 있는 깔끔한 행으로 뽑아내야 하니까요.

이 글에서는 space-ocr로 인보이스 명세 항목을 자동으로 추출하는 방법을 소개합니다. 텍스트로 뭉개 버리는 게 아니라, 한 줄 한 줄이 각자의 행이 되고 셀 하나하나가 페이지에서 읽힌 정확한 위치를 가리키는 정형화된 배열로 뽑아내는 방식입니다. 표만이 아니라 문서 전체를 추출하려는 분이라면 더 폭넓게 다룬 인보이스·영수증 OCR 가이드부터 시작하세요.

핵심 비결: 명세 항목을 array 필드로 선언하기

대부분의 OCR API는 표를 통째로 문자열 하나로 추출하게 해 놓고, 파싱은 알아서 하라고 떠넘깁니다. space-ocr는 명세 항목 표를 스키마의 일부로 기술할 수 있습니다. type: "array"children 목록을 가진 FieldSpec 하나가 엔진에게 이렇게 일러 주는 셈이죠. 이 영역은 반복되고, 반복될 때마다 이런 하위 필드들을 갖는다.

아래는 어떤 영수증의 스키마 예시입니다. 商品("항목") 필드는 배열이고, 그 children은 商品名(품목명), 数量(수량), 単価(단가)입니다:

fields[] — 명세 항목을 배열로
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "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개가 나옵니다. ポッカレモン100359, シール割引-34(할인 줄이라 부호가 그대로 보존됩니다), エキストラBオリー698, 이런 식이죠. 행 파서도, 컬럼 분리 로직도, 정규식도 직접 짤 필요가 없었습니다. 구조를 한 번 선언했을 뿐입니다.

인보이스 하나에서 명세 항목 추출하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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을 가집니다. 영수증의 한 줄은 이렇게 생겼습니다:

商品 배열의 한 항목 (요약)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "商品名": "ポッカレモン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_ratio1.0 이면 모든 글자가 발견됐다는 뜻이고, 엔진은 ≥ 0.85 면 확신할 만한 매칭으로 봅니다. 추출한 행들을 match ratio로 정렬해서 가장 약한 것들만 눈으로 확인하면 됩니다. 전체 작동 방식은 바운딩 박스로 OCR 검증하기를 참고하세요.

✓ Verified

모델이 그 좌표를 지어내는 게 아닙니다. 언어 모델이 반환하는 건 각 명세 항목의 텍스트, 그리고 어떤 단어 토큰을 썼는지에 대한 힌트뿐입니다. 박스는 절대 반환하지 않습니다. 그다음 엔진이 그 텍스트를 비전 OCR이 페이지에서 실제로 검출한 심볼들과 글자 단위로 맞춰 보면서, 각 값이 얼마나 발견됐는지를 match_ratio 로 보고합니다. 모델이 주는 토큰 힌트는 비슷비슷한 행이 반복될 때 부정확해지기 쉬워서, 그대로 믿는 대신 컬럼·행 일관성 검사로 검증합니다. 줄들이 서로 닮은 30행짜리 표에서 특히 중요한 대목이죠. 바로 이 점 덕분에 표가 그저 그럴듯한 수준에 그치지 않고 확인 가능한 것이 됩니다. 모든 줄이 페이지와 얼마나 잘 맞았는지를 점수로 달고 있으니까요.

한 줄을 클릭하면 바로 그 픽셀로

명세 항목마다 자기가 어디 있는지 알고 있으니, 표를 점검하는 일이 클릭 한 번으로 끝납니다. 앱에서 품목명이든 수량이든 단가든 아무 셀이나 클릭하면, 원본 이미지에서 그 값이 나온 정확한 영역이 확대 크롭과 함께 하이라이트됩니다. 30줄짜리 인보이스라도 페이지 전체를 훑을 필요 없이, 어딘가 이상해 보이는 그 한 줄로 시선이 바로 갑니다.

명세 항목 셀을 클릭하면 → 원본 인보이스에서 해당 영역이 켜집니다.

명세 항목에서 회계 도구가 읽는 CSV로

명세 항목을 일단 시트에 저장하고 나면, 내보내기 단계에서 배열 구조가 다시 한번 빛을 발합니다. space-ocr는 내보낼 때 배열 필드를 펼칩니다. 헤더는 # 와 스칼라 컬럼들에 더해, 배열 child마다 colName.childName 이름의 컬럼이 하나씩(즉 商品.商品名, 商品.数量, 商品.単価) 붙습니다. 명세 항목 하나하나가 각자의 하위 행이 되고요. 항목 10개짜리 영수증은 10개의 행이 되며, 모두 같은 매장명과 날짜를 달고 있습니다. 스프레드시트와 장부 임포터가 기대하는, 바로 그 길고 납작한 형식이죠.

시트를 내보내면 — 배열 명세 항목이 항목당 한 행으로 펼쳐지고 colName.childName 컬럼이 붙습니다.

이 영수증의 내보내기 결과를 간추리면 이렇게 보입니다:

#店舗名日付商品.商品名商品.単価
1KINSHO2019年08月17日ポッカレモン100359
2KINSHO2019年08月17日エキストラBオリー698
3KINSHO2019年08月17日シール割引-34

파일은 BOM이 붙은 UTF-8이라 일본어·한국어·중국어 품목명도 Excel(엑셀)에서 깨지지 않고 깔끔하게 열립니다. 직접 수정한 값이 있으면 내보낼 때 그 값이 OCR 값을 덮어쓰지만, 원본은 기록에 그대로 남습니다. 이미지 폴더에서 스프레드시트까지 한 번에 이어지는 흐름은 스캔 문서를 CSV로를 참고하세요.

몇 단계로 끝내기

  1. 명세 항목용 배열 필드 정의하기
    fields[] 스키마에 type "array" 와 children 목록을 가진 필드를 추가하세요 — 예: description, qty, unit_price. 이렇게 하면 엔진에게 명세 항목 영역이 그 하위 필드들과 함께 반복된다고 알려 줍니다.
  2. 인보이스를 /ocr/fields 로 보내기
    이미지(URL 또는 base64)를 imageType, fields[] 와 함께 https://api.space-ocr.com/ocr/fields 로 POST 하세요. 배열 필드가 명세 항목당 객체 하나씩, 리스트로 돌아옵니다.
  3. 각 줄 검증하기
    모든 배열 항목은 자기만의 bbox, vertices, match_ratio를 가집니다. match_ratio로 정렬하거나 앱에서 셀을 클릭해 이미지의 정확한 영역으로 점프해 값을 확인하세요.
  4. CSV로 내보내기
    시트를 내보내면 — 배열 children이 colName.childName 컬럼으로 펼쳐지고 명세 항목 하나하나가 각자의 행이 되며, 문서 단위 필드가 반복돼 회계 도구에 바로 넣을 수 있는 형태가 됩니다.
인보이스에서 명세 항목을 자동으로 추출하려면 어떻게 하나요?
명세 항목 표를 type "array" 와 children 목록(예: description, qty, unit_price)을 가진 필드로 선언한 다음, 이미지를 /ocr/fields 로 POST 하세요. 엔진이 배열을 행 리스트로, 즉 명세 항목당 객체 하나씩 반환하므로 표 파싱 코드를 직접 짤 필요가 없습니다. 각 항목은 자기만의 바운딩 박스, vertices, match ratio도 함께 들고 옵니다.
인보이스마다 명세 항목 개수가 다른데, OCR이 처리할 수 있나요?
네. 배열 필드는 행 개수가 고정되어 있다고 가정하지 않습니다. 데모의 영수증은 10개 항목을 내놓지만, 다른 인보이스는 30개를 내놓을 수도 있죠. 엔진은 검출된 텍스트 레이아웃에서 반복되는 행들을 그룹으로 묶기 때문에, 페이지에 줄이 있는 만큼 명세 항목 객체가 나오고, 각각이 이미지 위에 독립적으로 위치합니다.
CSV 내보내기에서 명세 항목은 어떻게 나타나나요?
배열 필드는 내보낼 때 펼쳐집니다. 헤더는 '#' 와 스칼라 컬럼들에 더해, 배열 child마다 colName.childName 이름의 컬럼이 하나씩(예: items.description, items.qty, items.unit_price) 붙습니다. 명세 항목 하나하나가 각자의 하위 행이 되고, 거래처·날짜 같은 문서 단위 필드는 반복됩니다. 장부와 스프레드시트 임포터가 기대하는 납작한 형식이죠. 파일은 BOM이 붙은 UTF-8이라 Excel에서 CJK가 깨지지 않습니다.
명세 항목이 제대로 읽혔는지 어떻게 알 수 있나요?
모든 배열 항목은 match_ratio(글자 중 페이지에서 찾아낸 비율)와 바운딩 박스를 함께 가지고 나옵니다. match_ratio가 1.0이면 모든 글자가 발견됐다는 뜻이고, 엔진은 0.85 이상을 확신할 만한 매칭으로 간주합니다. 행들을 match ratio로 정렬해 가장 약한 것들만 검토하거나, 앱에서 셀을 클릭해 그 값이 나온 정확한 영역을 하이라이트할 수 있습니다.
영어가 아닌 인보이스에서도 작동하나요?
네. 언어 감지는 자동입니다. 일본어, 한국어, 중국어, 영어가 전각 문자와 세로쓰기 CJK 텍스트까지 포함해 하나의 엔진에서 처리됩니다. 데모에서는 일본어 영수증의 商品(항목) 배열을 商品名, 数量, 単価 children과 함께 추출합니다. 따로 설정할 언어 플래그 같은 건 없습니다.
관련 글