CSV 행별 금액이 수량×단가와 맞는지 Python으로 검산하기

설명용 합성 자료의 도해이며 실제 거래·앱 화면이 아닙니다. 소계 200은 통과한 한 행만 포함합니다.

CSV에 금액 열이 이미 있어도 그 숫자를 그대로 합치기 전에 같은 행의 수량과 단가로 다시 계산해 보세요. 이 실습은 세 행을 통과 1·금액불일치 1·입력오류 1로 나누고, 불일치 행의 예상금액 150과 차이 −1을 보고서에 남깁니다. 통과한 행의 재계산 소계는 200입니다.

입력은 모두 직접 만든 합성 자료입니다. 실제 고객·매출 자료나 거래 규칙이 아닙니다. 로컬 CSV만 읽으며 API 호출과 외부 서버 연결은 없습니다. Python 기초와 터미널 명령 실행을 아는 독자를 위한 예제입니다.

1 먼저 금액의 뜻과 허용오차를 정합니다

이번 금액은 수량×단가만으로 만든 금액입니다. 부가세·할인·배송비·환율 변환과 반올림은 적용하지 않습니다. 실제 파일의 금액이 그 항목을 포함한다면 이 검사식을 바로 적용하지 말고 계산 계약부터 확인하세요.

비교는 다음 세 단계입니다.

  1. expected_amount = qty × unit_price로 예상금액을 독립 계산합니다
  2. difference = amount − expected_amount로 방향이 있는 차이를 남깁니다
  3. abs(difference) <= tolerance이면 통과합니다

--tolerance의 기본값은 0입니다. 표기가 0.30과 0.3이어도 숫자가 같으면 정확 일치로 통과합니다. 허용오차는 동일한 금액 단위의 절대 차이이며 비율이나 반올림 자릿수가 아닙니다. --tolerance 0.01을 지정하면 차이의 절댓값이 정확히 0.01인 행까지 통과합니다.

절대 허용오차0.01의 닫힌 구간에서 차이−0.01과+0.01은 통과하고−0.0101과+0.0101은 불일치인 설명용 확대 도해
합성 입력의 절대 허용오차 설명용 도해입니다. 양끝 0.01은 포함합니다. 이해를 위한 비례 축이 아닌 확대 도식이며 반올림과 통화 규칙을 뜻하지 않습니다.

허용오차는 데이터에서 적당히 추측하는 값이 아닙니다. 결과를 보기 전에 승인된 기준으로 정하고 보고서의 tolerance와 함께 보관하세요. 차이의 원인을 확인하지 않고 허용오차를 늘리면 오류 행을 통과시킬 수 있습니다.

꺼진 계산기와 익명 자료를 나란히 놓고 계산 기준을 준비한 AI 연출 사진
AI로 만든 연출 이미지. 수량·단가·금액의 계산 기준을 검토하는 업무 준비 장면이며, 실제 거래 자료나 Python 실행·검산 결과가 아닙니다.

2 같은 세 행과 입력 계약을 확인합니다

실습 ZIP 받기: 행별 금액 검산 파일을 새 빈 로컬 폴더에 풀고 fixtures/main.csv를 텍스트 편집기로 엽니다.

id,qty,unit_price,amount
A01,2,100,200
A02,3,50,149
A03,2.5,10,25

직접 계산한 기준은 다음과 같습니다.

ID수량×단가기재금액차이기본 허용오차 0의 판정
A012×100=2002000pass
A023×50=150149−1amount_mismatch
A03계산하지 않음25계산하지 않음invalid, qty_format

A02는 숫자로 읽을 수 있지만 열 사이 관계가 맞지 않습니다. A03은 낱개 정수 수량 표기에 맞지 않으므로 2로 잘라 계산하거나, 25가 맞는 것처럼 승인하지 않습니다.

이 예제가 받는 범위는 아래와 같습니다.

  • 파일: UTF-8 또는 UTF-8 BOM, 쉼표 CSV, 1,000,000바이트 이하, 데이터 1~10,000행
  • 구조: 머리글이 정확히 id,qty,unit_price,amount 순서이고 매 레코드는 네 열
  • ID: ASCII 영문으로 시작해 영문·숫자·밑줄·하이픈만 사용, 1~32자, 대소문자 구분, 중복 불허
  • 수량: 0 또는 0이 아닌 숫자로 시작하는 ASCII 정수, 범위 0~999999
  • 단가: 부호 없는 ASCII 정수 또는 일반 소수, 불필요한 앞자리 0 없이 소수 1~4자리, 범위 0~999999.9999
  • 기재금액과 허용오차: 단가와 같은 표기, 범위 0~999999999999.9999

2.0, +2, 01, 빈 수량은 받지 않습니다. 금액의 NaN, Infinity, 1e2, 1_000, 앞뒤 공백, 소수 5자리도 받지 않습니다. "1,000"은 CSV에서는 한 필드이지만 허용한 금액 표기가 아닙니다. 자동 공백 제거나 기호 제거도 하지 않습니다. CSV 필드 하나의 파서 한도는 131072자이며 이 한도를 넘으면 파일 전체를 중단합니다.

모든 금액에 같은 하나의 단위를 쓴다는 전제입니다. 통화 열이 없으므로 원·달러가 섞인 파일을 찾아내거나 환산할 수 없습니다. 범위 숫자는 연습용 입력 계약이며 회계·세금 규정이 아닙니다.

Python csv.reader 문서는 기본 필드를 문자열로 돌려준다고 설명합니다. 이 코드는 자동 float 변환 옵션을 사용하지 않고 원문을 먼저 검사합니다. strict=True의 CSV 구문 검사 외에 헤더·열 수·ID 조건도 별도로 확인합니다.

3 전체 코드를 저장합니다

아래는 ZIP의 code/row_amount_checks.py와 같은 전체 코드입니다. Python 3.12 표준 라이브러리만 사용하며 추가 패키지나 API 키가 필요하지 않습니다.

"""Check synthetic CSV cross-column amounts without trusting stated totals."""
import argparse
import csv
import hashlib
import io
import json
import os
from pathlib import Path
import re
import stat
import sys
import tempfile
from decimal import Context, Decimal, DecimalException, Inexact, localcontext

HEADER = ['id', 'qty', 'unit_price', 'amount']
ID = re.compile(r'[A-Za-z][A-Za-z0-9_-]{0,31}\Z')
QTY = re.compile(r'(?:0|[1-9][0-9]{0,5})\Z')
PRICE = re.compile(r'(?:0|[1-9][0-9]{0,6})(?:\.[0-9]{1,4})?\Z')
AMOUNT = re.compile(r'(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,4})?\Z')
MAX_PRICE = Decimal('999999.9999')
MAX_AMOUNT = Decimal('999999999999.9999')
MAX_BYTES, MAX_ROWS, MAX_FIELD = 1_000_000, 10_000, 131_072


def safe_path(value):
    path = Path(value)
    if '..' in path.parts:
        raise ValueError('.. 경로 대신 직접 경로를 지정하세요')
    path = path.absolute()
    for part in [path, *path.parents]:
        if part.is_symlink():
            raise ValueError('심볼릭 링크와 링크 폴더는 사용하지 않습니다')
    return path


def read_source(source):
    source = safe_path(source)
    info = source.stat()
    if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
        raise ValueError('입력은 하드 링크가 아닌 일반 파일이어야 합니다')
    flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_NONBLOCK', 0)
    with os.fdopen(os.open(source, flags), 'rb') as handle:
        opened = os.fstat(handle.fileno())
        if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
            raise ValueError('읽기 전 입력 파일이 바뀌었습니다')
        if not stat.S_ISREG(opened.st_mode) or opened.st_nlink != 1:
            raise ValueError('입력 파일 유형 또는 링크 수를 확인하세요')
        raw = handle.read(MAX_BYTES + 1)
        after = os.fstat(handle.fileno())
    if (opened.st_size, opened.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
        raise ValueError('읽는 중 입력 파일이 바뀌었습니다')
    if len(raw) > MAX_BYTES:
        raise ValueError('입력 한도 1,000,000바이트 초과')
    return raw


def number(raw, pattern, maximum, field):
    if not pattern.fullmatch(raw):
        return None, field + '_format'
    value = Decimal(raw)
    if not value.is_finite() or not Decimal('0') <= value <= maximum:
        return None, field + '_range'
    return value, None


def plain(value):
    text = format(value, 'f')
    if '.' in text:
        text = text.rstrip('0').rstrip('.')
    return '0' if value == 0 else text


def inspect(source, tolerance='0'):
    tol, issue = number(tolerance, AMOUNT, MAX_AMOUNT, 'tolerance')
    if issue:
        raise ValueError('허용오차는 부호 없는 유한 십진수, 소수 최대 4자리입니다')
    raw = read_source(source)
    try:
        text = raw.decode('utf-8-sig', errors='strict')
    except UnicodeDecodeError as error:
        raise ValueError('UTF-8 해석 실패: 인코딩을 자동 추정하지 않습니다') from error
    rows, seen = [], set()
    total = Decimal('0')
    old_limit = csv.field_size_limit(MAX_FIELD)
    try:
        reader = csv.reader(io.StringIO(text, newline=''), strict=True)
        if next(reader, None) != HEADER:
            raise ValueError('머리글은 id,qty,unit_price,amount 순서여야 합니다')
        # At most 12 integer + 4 fraction digits per product, 10,000 rows.
        # A sum needs at most 16 integer + 4 fraction digits; precision 28 suffices.
        with localcontext(Context(prec=28)) as context:
            context.traps[Inexact] = True
            for record, fields in enumerate(reader, 2):
                if record - 1 > MAX_ROWS:
                    raise ValueError('행 한도 10,000행 초과')
                if len(fields) != 4:
                    raise ValueError(f'레코드 {record}: 열 수는 4개여야 합니다')
                ident, q, p, a = fields
                if not ID.fullmatch(ident) or ident in seen:
                    raise ValueError(f'레코드 {record}: ID 형식 또는 중복을 확인하세요')
                seen.add(ident)
                qty, qi = number(q, QTY, Decimal('999999'), 'qty')
                price, pi = number(p, PRICE, MAX_PRICE, 'unit_price')
                amount, ai = number(a, AMOUNT, MAX_AMOUNT, 'amount')
                issues = [x for x in (qi, pi, ai) if x]
                expected = difference = None
                status = 'invalid'
                if not issues:
                    expected = qty * price
                    difference = amount - expected
                    status = 'pass' if difference.copy_abs() <= tol else 'amount_mismatch'
                    if status == 'pass':
                        total += expected
                rows.append({
                    'source_record': record, 'id': ident, 'raw_qty': q,
                    'raw_unit_price': p, 'raw_amount': a, 'status': status,
                    'issues': issues,
                    'expected_amount': None if expected is None else plain(expected),
                    'difference': None if difference is None else plain(difference),
                })
    except csv.Error as error:
        raise ValueError('CSV 구문 또는 필드 길이 오류: ' + str(error)) from error
    except DecimalException as error:
        raise ValueError('Decimal 계산 실패: 범위·정밀도 계약을 확인하세요') from error
    finally:
        csv.field_size_limit(old_limit)
    if not rows:
        raise ValueError('데이터 행이 1개 이상 필요합니다')
    counts = {s: sum(r['status'] == s for r in rows) for s in ('pass', 'amount_mismatch', 'invalid')}
    return {
        'input_rows': len(rows), 'pass_count': counts['pass'],
        'mismatch_count': counts['amount_mismatch'], 'invalid_count': counts['invalid'],
        'validated_amount_total': plain(total),
        'all_rows_pass': counts['pass'] == len(rows), 'tolerance': plain(tol),
        'difference_definition': 'stated_amount_minus_qty_times_unit_price',
        'comparison': 'abs(difference) <= tolerance',
        'total_scope': 'recomputed expected amounts of pass rows only',
        'rows': rows, 'source_sha256': hashlib.sha256(raw).hexdigest(),
    }


def write_report(source, target, tolerance='0'):
    source, target = safe_path(source), safe_path(target)
    if source == target:
        raise ValueError('입력과 보고서는 서로 다른 경로여야 합니다')
    if target.suffix.lower() != '.json':
        raise ValueError('보고서는 새 .json 파일로 지정하세요')
    if os.path.lexists(target):
        raise ValueError('기존 보고서 보존: 새 --report 이름을 지정하세요')
    report = inspect(source, tolerance)
    rendered = json.dumps(report, ensure_ascii=False, indent=2) + '\n'
    target.parent.mkdir(parents=True, exist_ok=True)
    safe_path(target)
    temporary, committed = None, False
    try:
        with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', newline='\n',
                delete=False, dir=target.parent, prefix='.row-amount-', suffix='.tmp') as handle:
            temporary = Path(handle.name)
            handle.write(rendered)
            handle.flush()
            os.fsync(handle.fileno())
        # Same directory: publish only a fully written new file, never overwrite.
        os.link(temporary, target)
        committed = True
    finally:
        if temporary is not None:
            try:
                temporary.unlink(missing_ok=True)
            except OSError as error:
                if not committed:
                    raise
                print('경고: 완성 보고서는 저장됐지만 임시 파일 정리에 실패했습니다: '
                      + str(error), file=sys.stderr)
    return report


def main():
    parser = argparse.ArgumentParser(description='CSV 행별 수량×단가와 기재금액 검산')
    parser.add_argument('--input', required=True)
    parser.add_argument('--report', required=True)
    parser.add_argument('--tolerance', default='0', help='절대 허용오차, 기본값 0')
    args = parser.parse_args()
    try:
        report = write_report(args.input, args.report, args.tolerance)
    except (ValueError, OSError) as error:
        print('오류: ' + str(error), file=sys.stderr)
        return 2
    print(f"입력 {report['input_rows']}행 / 통과 {report['pass_count']} / "
          f"금액불일치 {report['mismatch_count']} / 입력오류 {report['invalid_count']} / "
          f"통과 행 재계산 소계 {report['validated_amount_total']} / 허용오차 {report['tolerance']}")
    return 0 if report['all_rows_pass'] else 1


if __name__ == '__main__':
    raise SystemExit(main())

숫자 열에 부적합한 값이 있으면 행을 invalid로 기록하고 원문과 issues를 보존합니다. 다른 정상 행은 검산하지만 invalid 행에는 예상금액과 차이를 null로 남깁니다. 같은 행에 형식 문제가 여러 개면 각각의 이유를 남깁니다.

계산에는 문자열에서 바로 만든 Decimal을 씁니다. Decimal 생성자는 특수값과 다양한 표기도 받을 수 있으므로 생성 성공만으로 업무 숫자를 승인하지 않습니다. 정규식과 유한값·범위 검사부터 통과시킵니다.

Decimal도 정밀도가 부족하면 연산 결과가 반올림될 수 있습니다. 이번 최대 곱은 999999×999999.9999=999998999900.0001이며 정수부 12자리·소수부 4자리입니다. 이를 최대 10,000행 더해도 정수부 16자리·소수부 4자리 안에 들어가므로 28자리 정밀도로 계산합니다. localcontext 문서의 계산 범위를 쓰고 Inexact 예외를 켜서 뜻하지 않은 값 손실을 중단합니다. 입력 한도를 늘리면 이 근거와 시험도 다시 정해야 합니다.

보고서 숫자는 JSON 문자열입니다. plain()은 0.30을 0.3으로 표시하고 끝의 0만 줄일 뿐 값을 반올림하지 않습니다. 원래 표기는 raw_qty, raw_unit_price, raw_amount에 남습니다. source_record는 헤더를 1로 센 논리 레코드 번호이며 따옴표 안 줄바꿈이 있으면 물리 줄 번호와 다를 수 있습니다.

4 통과 1과 소계 200을 함께 읽습니다

ZIP을 푼 폴더에서 명령을 하나씩 실행합니다. 검수 환경은 Linux의 CPython 3.12.14입니다.

python --version
python code/row_amount_checks.py --input fixtures/main.csv --report out/main.json

화면의 결과는 다음과 같습니다.

입력 3행 / 통과 1 / 금액불일치 1 / 입력오류 1 / 통과 행 재계산 소계 200 / 허용오차 0

이 실행의 종료코드는 1입니다. 오류 때문에 보고서가 없다는 뜻이 아니라, 완성된 보고서에 확인할 행이 남았다는 뜻입니다. out/main.json에서 다음 값을 대조합니다.

  • pass_count=1, mismatch_count=1, invalid_count=1, 세 수의 합이 입력 3행과 같음
  • A02: status=amount_mismatch, expected_amount="150", difference="-1"
  • A03: status=invalid, issues=["qty_format"], 예상금액과 차이는 null
  • validated_amount_total="200", all_rows_pass=false, tolerance="0"

validated_amount_total은 통과한 행의 수량×단가만 더한 소계입니다. 149나 25를 더해 세 행 전체의 확정 금액처럼 표시하지 않습니다. 허용오차가 0보다 큰 경우에도 소계는 재계산한 예상금액을 더하며, 입력의 기재금액을 더한 값과 다를 수 있습니다.

종료코드의 의미는 다음과 같습니다.

종료코드보고서와 판정다음 행동
0새 보고서 생성, 모든 행 통과이 검사 범위 안의 결과를 대조
1새 보고서 생성, 불일치 또는 부적합 행 존재행 ID·원문·차이·이유 검토
2옵션·입력·구조·경로·저장 오류새 최종 보고서 없음, 원인 복구

모든 행 통과도 실제 판매 수량이나 단가의 진위를 보증하지 않습니다. 잘못된 수량·단가·금액이 서로 맞아떨어질 수 있으므로 원본 거래와의 대조는 별도입니다. 첫 실행처럼 종료코드 1을 의도한 명령을 &&로 이어 쓰면 다음 명령이 실행되지 않을 수 있습니다.

5 허용오차의 경계와 바로 바깥을 시험합니다

fixtures/tolerance.csv는 다음 네 행입니다. 예상금액은 각 행 모두 1입니다.

id,qty,unit_price,amount
T01,1,1,1.01
T02,1,1,1.0101
T03,1,1,0.99
T04,1,1,0.9899
python code/row_amount_checks.py --input fixtures/tolerance.csv --report out/tolerance_001.json --tolerance 0.01
python code/row_amount_checks.py --input fixtures/tolerance.csv --report out/tolerance_exact.json
ID기재금액−예상금액허용오차 0.01기본 0
T010.01pass, 정확 경계amount_mismatch
T020.0101amount_mismatchamount_mismatch
T03−0.01pass, 정확 경계amount_mismatch
T04−0.0101amount_mismatchamount_mismatch

허용오차 0.01에서는 통과 2·불일치 2·입력오류 0, 재계산 소계 2입니다. 기본 0에서는 통과 0·불일치 4·소계 0입니다. 두 실행 모두 종료코드 1입니다. 손계산 기대값은 expected/tolerance_001.json과 expected/tolerance_exact.json에 각각 있습니다.

--tolerance -0.01, --tolerance NaN, --tolerance Infinity는 종료코드 2로 거부합니다. 0.01이 특정 통화나 청구에 적절하다는 뜻은 아닙니다. 단위를 확인한 업무 기준으로 따로 정하세요.

6 숫자 오류와 파일 오류를 나누어 복구합니다

다음 명령은 17행의 숫자 반례를 검사해 새 보고서를 만듭니다.

python code/row_amount_checks.py --input fixtures/bad_numbers.csv --report out/bad_numbers.json

이 파일에는 단가·금액의 NaN와 Infinity, True 수량, 지수·천 단위 구분·빈칸·공백·소수 5자리·앞자리 0·음수·+ 부호·범위 초과·전각 숫자·밑줄 표기가 있습니다. 통과 0·불일치 0·입력오류 17·소계 0, 종료코드 1이 기대값입니다. 숫자 의미가 확인되지 않은 값을 금액불일치로 단정하지 않으며 모두 계산 전 invalid로 남깁니다.

반면 다음 세 명령은 파일 검사 단계에서 종료코드 2로 멈추고 지정한 새 최종 보고서를 만들지 않습니다.

python code/row_amount_checks.py --input fixtures/failures/wrong_header.csv --report out/rejected_header.json
python code/row_amount_checks.py --input fixtures/failures/invalid_utf8.csv --report out/rejected_encoding.json
python code/row_amount_checks.py --input fixtures/not_here.csv --report out/rejected_missing.json
관찰한 상태먼저 확인할 것복구 후 대조
amount_mismatch같은 행의 수량·단가·기재금액 및 세금·할인 계약승인된 사본으로 새 보고서, 차이와 적용 규칙 확인
invalid원문 표기·범위·낱개 수량 단위임의 절삭·0 대체 없이 근거 있는 수정
머리글·열 수·중복 ID 오류공급 파일 구조와 식별 기준모든 행 다시 검사, 부분 결과 사용 안 함
UTF-8 해석 실패실제 인코딩과 내보내기 설정원본 보존 후 UTF-8 사본의 내용 대조
입력 없음·저장 오류파일 위치·폴더 권한·하드 링크 지원새 로컬 실습 폴더와 새 결과 이름

ZIP의 fixtures/failures에는 위 사례 외에 추가 열, 중복 ID, 잘못된 ID, 닫히지 않은 따옴표, 헤더만 있는 파일, 빈 레코드가 있습니다. 이 8개는 각각 전체 중단 사례입니다. 구조가 틀린 파일의 앞쪽 정상 행만 보고서로 내보내지 않습니다.

A02의 149를 곧바로 150으로 고치지 마세요. 단가가 틀렸는지, 계약에 할인액이 있는지 먼저 원본에서 확인해야 합니다. 계약이 확인되지 않으면 불일치 상태로 보류합니다.

7 기존 결과를 보존하고 같은 입력을 재검산합니다

첫 명령을 같은 out/main.json으로 다시 실행하면 종료코드 2로 기존 결과를 보존합니다. 새 결과가 필요하면 이름을 바꿉니다.

python code/row_amount_checks.py --input fixtures/main.csv --report out/main_again.json
python -m unittest discover -s tests -p "test_*.py" -v
python code/verify_examples.py

단위시험 20개가 통과했고, 예제 재검산기는 CLI 19건의 기대 종료코드·고정 JSON·기존 출력 보존·실패 출력 부재를 확인했습니다. 제공한 CSV 12개의 실행 전후 SHA-256도 같았습니다. 최대 곱과 최대 10,000행의 합, 10,000자리 수량 거부, 허용오차의 양·음 경계, fsync·하드 링크 실패를 주입한 경우의 최종 보고서 부재도 시험에 포함했습니다. unittest의 discover 안내에서 위 명령 옵션을 확인할 수 있습니다.

코드는 입력과 같은 출력 경로, 기존 파일, 심볼릭 링크 입력·출력·부모 폴더, 하드 링크 입력, ..를 포함한 경로를 거부합니다. 새 보고서는 출력 폴더의 임시 파일에 모두 쓰고 동기화한 다음 os.link로 최종 이름을 만듭니다. 기존 이름을 덮어쓰는 방식은 없습니다. 임시 파일에는 NamedTemporaryFile을 사용합니다.

처리한 쓰기 오류에서는 최종 보고서가 생기지 않도록 했습니다. 완료 뒤 임시 파일 정리만 실패하면 완성 보고서를 보존하고 경고를 냅니다. .row-amount-로 시작하는 임시 파일은 완료 보고서로 읽지 마세요. 강제 종료·정전·악의적인 동시 경로 변경까지 완전히 보장하는 구조는 아니며, SHA-256도 입력을 잠그는 기능은 아닙니다. 다른 프로그램이 자료를 바꾸지 않는 별도 로컬 실습 폴더를 사용하세요.

첨부 ZIP에는 전체 코드, 같은 합성 CSV와 실패 반례, 독립 기대 JSON 5개·손계산표, README, 시험 코드와 SHA-256 목록만 들어 있습니다. 검수용 로그와 실제 출력은 독자 파일에 섞지 않았습니다. 고정 기대값을 입력과 함께 바꿔 결과를 맞추면 이 실습의 독립 대조가 사라집니다.

자주 묻는 질문

허용오차를 0.01로 주면 소수 둘째 자리로 반올림하나요?

아닙니다. 원래 예상금액과 기재금액의 차이를 반올림 없이 계산하고 절댓값이 0.01 이하인지 비교합니다.

통과 행만 소계를 내면 전체 금액도 알 수 있나요?

확인할 행이 남아 있으면 전체 금액은 아직 확정하지 않습니다. all_rows_pass=false와 불일치·입력오류 목록을 함께 보세요. 허용오차를 사용한 통과 소계도 기재금액 합이 아니라 재계산 예상금액 합입니다.

부가세나 할인이 있는 CSV에도 그대로 쓸 수 있나요?

이번 식은 그 항목을 포함하지 않습니다. 수량×단가와 기재금액의 관계를 승인된 규칙으로 먼저 정하고, 필요한 열과 새 기대값·반례를 추가한 별도 검사로 만들어야 합니다.

확인 범위와 이어서 볼 내용

검산 뒤에는 저장할지, 첨부 준비를 확인할지 고릅니다. 중복 ID·수량 제약을 확인하며 새 로컬 DB에 넣는 방법은 CSV의 SQLite 적재에서, 목록의 파일이 실제로 있는지 확인하는 방법은 첨부 목록과 상대 경로 대조에서 연습하세요. 금액이 맞아도 첨부가 준비됐다는 뜻은 아닙니다.

이 보고서는 검산 결과 JSON이며 두 실습의 CSV 입력과 호환되는 변환 파일이 아닙니다. 적재 예제는 id·qty, 첨부 예제는 id·attachment_path라는 별도 입력 계약을 사용합니다. 통과 행만 조용히 추출해 전체 완료로 표시하지 말고 불일치·입력오류 행과 원본 ID를 함께 관리하세요.

기술 자료 확인일은 2026-10-06입니다. 공식 링크는 Python 3.12 계열이며 확인 당시 3.12.15 문서로 표시됐습니다. 실제 실행은 Linux CPython 3.12.14였고 Windows·macOS·Excel 앱, 실제 거래 데이터와 대용량 성능은 시험하지 않았습니다.

수량 열 자체의 허용 표기는 CSV 수량 열의 소수·음수·빈칸 분리에서, 반올림 단계의 차이는 Decimal CSV 반올림 기준에서 이어서 볼 수 있습니다. 이번 실습의 완료 기준은 세 숫자 열이 읽혔다는 것에 그치지 않고 같은 행의 계산 관계·허용오차·차이·보류 사유가 남는 것입니다.

작성자 소개