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

이미지에서 표 데이터를 추출하여 CSV로 변환하기

표, 주문서, 배송 전표 사진을 깔끔한 CSV 파일로 변환하세요. space-ocr이 명세행을 어떻게 읽고, 행 단위 검증이 확인이 필요한 값을 어떻게 드러내는지 살펴봅니다.

4 분 분량· 2026-08-31

스캔한 표의 데이터를 스프레드시트로 옮겨 적는 일은 번거롭기 짝이 없습니다. 배송 전표나 구매 주문서처럼 여러 항목이 나열된 선명한 이미지가 있지만, 결국은 픽셀 덩어리에 불과하죠. 보통은 각 항목, 수량, 가격을 하나씩 보면서 새로운 행에 일일이 입력하는 지루한 수작업을 거쳐야 합니다. 이 과정은 시간이 오래 걸릴 뿐만 아니라, 오타 하나만으로도 전체 데이터가 엉망이 될 수 있습니다.

표 형식의 주문/배송 문서
반복되는 항목이 많은 표 — 일관된 형식으로 데이터를 추출합니다.

더 나은 방법은 표의 구조를 스키마로 정의하는 것입니다. 텍스트 덩어리를 통째로 가져오는 대신 필요한 열을 선언합니다. 반복되는 명세행 구간은 array 타입 필드로 두고 그 하위 요소(children)에 열을 나열하며, 항상 숫자가 들어오는 열에는 타입을 선언합니다.

1
2
3
4
5
6
7
8
9
10
{
  "name": "items",
  "type": "array",
  "children": [
    { "name": "name",   "type": "string" },
    { "name": "qty",    "type": "integer" },
    { "name": "price",  "type": "number" },
    { "name": "amount", "type": "number" }
  ]
}

행 수는 지정하지 않습니다. 몇 행이 돌아올지는 페이지가 정합니다. 각 행은 첨자 경로(values.items[0], values.items[1] …)로 돌아오고, 같은 경로가 값별 좌표·검증 맵의 키 cells["items[0].price"] 가 됩니다. 타입을 선언해도 추출되는 값은 달라지지 않습니다. 타입이 모델에 전달되지 않기 때문입니다. 선언한 타입이 더하는 것은 values 옆에 놓이는 결정론적 두 번째 층 data.normalized 입니다.

반복 항목에 대한 배열 필드를 정의한 후, 이미지를 업로드하여 표를 구조화된 그리드로 추출합니다.

이 방식은 동일한 값이 반복되는 빽빽한 표에서도 효과적입니다. 시스템은 대규모 언어 모델(LLM)을 사용해 추출할 텍스트를 먼저 제안하지만, 거기서 멈추지 않습니다. 품명 "刻みたくあん" 이나 단가 "580" 처럼 각 값에 대해 교차 검증을 수행합니다. 엔진은 언어 모델의 읽기를 문서의 열 구조와 대조하고, 페이지에서 최초로 인식된 OCR 기호와 문자 하나하나를 맞춰 봅니다. 값이 옆 행으로 밀리면 그 좌표의 문자는 대개 일치하지 않게 되고, 해당 필드는 그대로 통과하는 대신 검토 대상으로 기록됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"data": {
  "values": {
    "items": [
      { "name": "刻みたくあん", "qty": "3",
        "price": "580", "amount": "1,740" }
    ]
  },
  "cells": {
    "items[0]": {
      "box": { "xmin": 263, "ymin": 460,
               "xmax": 738, "ymax": 523 },
      "quad": [ { "x": 263, "y": 460 }, { "x": 738, "y": 460 },
                { "x": 738, "y": 523 }, { "x": 263, "y": 523 } ],
      "verified": null, "review": null
    },
    "items[0].name": {
      "box": {…}, "quad": […],
      "verified": true, "review": null,
      "evidence": { "text_match": true, "match_ratio": 1.0 }
    },
    "items[0].qty": { "box": {…}, "quad": […],
                      "verified": true, "review": null },
    "items[0].price": {
      "box": { "xmin": 693, "ymin": 460,
               "xmax": 738, "ymax": 488 },
      "quad": […],
      "verified": false,
      "review": { "reasons": ["text_mismatch"] },
      "evidence": { "text_match": false, "match_ratio": 0.62 }
    }
  },
  "review": {
    "unit": "field",
    "flagged": [
      { "path": "items[0].price", "reasons": ["text_mismatch"] }
    ]
  },
  "normalized": {
    "items": [ { "qty": 3, "price": 580, "amount": 1740 } ]
  }
}

이 검토 목록이 곧 작업 대기열입니다. data.review.flagged 가 정확한 경로와 사유를 지목하고, cells["items[0].price"] 에는 대조에 사용한 좌표가 들어 있습니다. 다만 전수 포착 장치는 아닙니다. 옆 행에 같은 숫자가 인쇄돼 있으면 박스가 어느 쪽에 붙어도 문자는 일치하고, 두 엔진이 같은 오독에 합의하면 맞대어 볼 상대가 없습니다. 그래서 문자 대조로는 보이지 않는 행·열 착오는 선언 가능한 규칙으로 넘겨 두는 편이 낫습니다. 코드 열에는 pattern, 마스터가 이미 보유한 값에는 enum, 타당한 범위에는 min 과 max 를 겁니다. 위반은 모두 같은 review.flagged 에 실립니다.

마지막 층은 직접 하는 검산이고, 이는 하류의 몫입니다. qty 를 integer 로, 금액 열을 number 로 선언해 두었으므로 data.normalized 에는 해석된 수치가 놓입니다("1,740" 은 1740). 행 검산은 곱셈 하나면 충분합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { values, normalized, review } = data;

const rows = values.items.map((item, i) => {
  const n = normalized.items[i];
  const flagged = review.flagged.some((f) =>
    f.path.startsWith(`items[${i}]`)
  );
  return {
    name: item.name,
    qty: n.qty,
    price: n.price,
    amount: n.amount,
    // 사람이 볼 행: 검토 플래그가 섰거나 검산이 안 맞는 경우
    check:
      flagged || n.qty == null || n.qty * n.price !== n.amount,
  };
});

해석되지 않은 리프는 normalized 에서 null 이 되고, 실패 종류는 cells[path].normalized.error 에, 검토 목록에는 type_mismatch 로 실립니다. 계산에 쓰는 값은 normalized 에서 가져오고, 표시할 문자는 values 를 그대로 두십시오. 좌표와 검증이 붙어 있는 쪽은 values 입니다.

데이터 추출이 완료되면 클릭 한 번으로 전체 시트를 깔끔한 CSV 파일로 내보낼 수 있습니다.
✓ Verified

각 값은 그 값이 실려 있던 페이지와 맞대어 봅니다. 모델의 읽기를 그 자리에서 실제로 감지된 OCR 기호와 문자 단위로 대조하고, 얼마나 일치했는지가 evidence.match_ratio 에 남습니다. 0.85 이상이면 신뢰할 수 있는 일치입니다. 판정 자체는 verified 이고 이것은 review 의 거울입니다. 무엇이든 플래그가 서면 false, 대조가 돌고 아무것도 서지 않으면 true, 대조할 대상이 없으면(행 union 박스 등) null 입니다. 좌표는 일치한 기호에서 box 와 quad 로 도출되고 data.image 를 기준으로 0–1000 범위로 정규화됩니다. 이는 값이 어디서 왔는지에 대한 증거이지 원하는 값이라는 증명은 아니며, 확인되지 않은 값은 review.flagged 에 목록으로 남습니다.

이용 요금은 처리된 이미지당 100원입니다. 계정에는 매월 100건의 무료 스캔이 제공됩니다. 어떤 이유로든 추출에 실패하면 비용이 청구되지 않습니다.

  1. 시트 스키마 정의하기
    새 시트를 만들고 필요한 열을 정의합니다. 반복되는 항목의 경우 '배열(array)' 타입을 사용하고 상품명, 수량, 가격 등에 대한 하위 열을 추가하세요.
  2. 이미지 업로드하기
    표 이미지를 시트로 드래그 앤 드롭하거나 API를 통해 업로드합니다.
  3. 추출된 데이터 검토하기
    이미지는 정의한 스키마에 따라 처리됩니다. 표의 각 항목은 시트에서 구조화된 행으로 나타납니다.
  4. 필요시 수정하기
    특정 셀을 클릭하면 이미지의 해당 영역을 바로 확인할 수 있습니다. 그리드에서 직접 값을 수정하세요.
  5. CSV로 내보내기
    '내보내기' 버튼을 클릭하고 CSV를 선택하세요. 모든 항목을 포함한 표 데이터가 깔끔한 구조의 파일로 다운로드됩니다.
셀 병합이나 복잡한 레이아웃이 있는 표는 어떻게 하나요?
이 시스템은 표준적인 행과 열 구조의 표에 최적화되어 있습니다. 매우 복잡한 레이아웃의 경우, 여러 스키마를 정의하거나 초기 추출 후 시트에서 직접 데이터를 수동으로 조정할 수 있습니다.
CSV로 내보내기 시 표의 각 항목은 어떻게 처리되나요?
'items'라는 이름의 배열 열에 'name'과 'price'라는 하위 열이 있다면, CSV 헤더는 'items.name'과 'items.price'가 됩니다. 이미지의 각 품목은 CSV 파일에서 별도의 행으로 생성됩니다.
PDF 파일에 있는 표도 처리할 수 있나요?
네, 웹 앱에서 가능합니다. PDF 파일을 드래그 앤 드롭하면 각 페이지가 이미지로 자동 변환되어 처리됩니다. API 자체는 JPEG나 PNG와 같은 래스터 이미지 형식만 지원합니다.
각 셀의 좌표는 어떻게 결정되나요?
추출된 각 값에 대해, 시스템은 해당 값의 문자를 페이지에서 인식된 OCR 기호와 대조합니다. 일치한 기호에서 그 값의 `box` 와 `quad` 가 정해지고, 실제로 읽은 페이지(`data.image`)를 기준으로 0–1000 범위로 정규화됩니다. 이 좌표는 값이 어디서 왔는지를 가리키는 증거이므로, 이미지 위에 다시 그려서 직접 확인하실 수 있습니다.
표의 행 개수에 제한이 있나요?
행 개수를 설정하는 항목은 없습니다. 몇 행이 돌아올지는 페이지가 정합니다. 상한이 있는 쪽은 동기 호출입니다. 처리는 180초에서 끊기고, 넘으면 504 `ocr_engine_timeout` 이 돌아오며 이 경우 과금되지 않습니다. 원인은 픽셀 수보다 기재 밀도인 경우가 많으므로, 아주 조밀한 표는 1페이지를 1이미지로 나누거나 비동기 `POST /upload` 를 쓰시는 편이 좋습니다.

이미지 속 표를 데이터로 바꾸세요

매월 100건의 무료 스캔을 이용해 보세요. 시작하는 데 신용카드는 필요 없습니다.

관련 글