본문 바로가기
inpilot.dev

OpenDART로 재무제표 긁기 — 회사 이름으로는 못 부른다

2026-08-05 · 2 min read

$ curl opendart.fss.or.kr/api/list.json

status "013" — 조회 결과 없음

첫 벽: 회사 이름 파라미터가 없다

금융감독원 전자공시(DART)는 OpenAPI를 무료로 연다. 키만 발급받으면 된다. 그런데 문서를 보고 corp_name=삼성전자로 부르면 아무것도 안 나온다. 공식 파라미터 표에 corp_name이 없기 때문이다.

모든 조회는 corp_code라는 8자리 고유번호로만 된다. 그리고 이 매핑표는 API가 아니라 zip 파일로 통째로 받아야 한다.

corp_code 확보

curl -fsS -o corp.zip \
  "https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key=$API_K_DART"
unzip -o corp.zip -d ./dart_corp

압축 풀면 CORPCODE.xml 하나가 나온다. 약 30MB, 상장·비상장 전체 법인이 들어있다. 구조는 단순하다.

<list>
  <corp_code>00126380</corp_code>
  <corp_name>삼성전자</corp_name>
  <stock_code>005930</stock_code>
  <modify_date>20250701</modify_date>
</list>

stock_code가 빈 항목이 비상장이다. 상장사만 필요하면 여기서 거른다.

import xml.etree.ElementTree as ET
 
tree = ET.parse("dart_corp/CORPCODE.xml")
listed = {
    e.findtext("stock_code"): e.findtext("corp_code")
    for e in tree.iter("list")
    if (e.findtext("stock_code") or "").strip()
}
assert listed["005930"] == "00126380"

매번 다시 받지 않는다. 로컬에 캐시하고, 신규 상장 반영이 필요할 때만 갱신한다. 30MB를 스크립트 돌릴 때마다 받으면 호출 한도만 먹는다.

재무제표 호출

curl -fsS --get 'https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json' \
  --data-urlencode "crtfc_key=$API_K_DART" \
  --data-urlencode 'corp_code=00126380' \
  --data-urlencode 'bsns_year=2025' \
  --data-urlencode 'reprt_code=11011' \
  --data-urlencode 'fs_div=CFS'

헷갈리는 두 파라미터를 표로 박아둔다.

reprt_code보고서
110131분기
11012반기
110143분기
11011사업보고서(연간)
fs_div재무제표
CFS연결
OFS개별(별도)

종목 비교 지표를 만들 거면 CFS로 통일한다. 섞으면 자회사가 큰 회사에서 숫자가 완전히 달라진다. 다만 연결재무제표를 안 만드는 회사는 CFS가 비니까 CFS 실패 시 OFS로 폴백하는 분기를 넣어둔다.

HTTP 200인데 실패다

이게 제일 짜증났던 부분이다. DART는 에러를 HTTP 상태코드가 아니라 본문의 status 필드로 준다. requestsraise_for_status()는 통과한다.

r = requests.get(url, params=params, timeout=10)
r.raise_for_status()          # 여기선 안 걸림
data = r.json()
if data["status"] != "000":   # 이 체크가 진짜 검증
    raise DartError(data["status"], data["message"])

자주 보는 코드:

status의미
000정상
013조회 결과 없음 (에러 아님. 그 해 보고서가 없을 뿐)
020요청 제한 초과
100필수 파라미터 누락

013을 예외로 던지면 전 종목 루프가 중간에 죽는다. 없는 건 정상으로 취급하고 None을 반환한 뒤 다음 종목으로 넘어가게 짰다.

호출 한도

공식 가이드는 일반적으로 일 20,000건 초과 시 020이 난다고만 적어놨다. 키별로 다른 한도가 걸려 있으면 다른 지점에서도 뜬다. 즉 정확한 임계치를 가정하고 짜면 안 된다. 020이 오면 그날은 멈추고 다음 날 이어받는 구조로 만든다.

상장사 약 2,500개 × 4개 분기면 1만 건이다. 한 번에 다 긁으려 하지 말고 수집 상태를 테이블에 남겨서 이어받게 한다.

CREATE TABLE fetch_log (
  corp_code CHAR(8), bsns_year INT, reprt_code CHAR(5),
  status TEXT, fetched_at TIMESTAMPTZ DEFAULT now(),
  PRIMARY KEY (corp_code, bsns_year, reprt_code)
);

재실행하면 이미 성공한 조합은 건너뛴다. 이거 없이 돌렸다가 한도 걸리고 처음부터 다시 하느라 하루 날렸다.

한 줄 요약

corp_code zip은 캐시, status 필드로 검증, 수집 진행상황은 DB에 남겨 이어받기.

새 글이 올라오면 받아보기

스팸 없이, 새 글이 올라올 때만 보내드려요.

댓글

댓글은 giscus 설정 후 표시됩니다. (docs/SETUP-features.md 참고)