삼성전자 종목코드는 005930이지만 OpenDART 재무제표 API에 이 값을 그대로 넣을 수는 없습니다. 공시 API는 삼성전자를 00126380이라는 8자리 기업 고유번호로 구분합니다. 주가 데이터의 종목코드와 공시 데이터의 기업 고유번호를 먼저 연결하지 않으면, 이후 재무제표 수집 코드가 회사마다 이름을 검색하는 불안정한 구조가 됩니다.
이 글에서는 OpenDART가 제공하는 기업 고유번호 파일을 받아 다음 형태의 매핑표로 저장합니다. API 키는 .env에만 두고, ZIP 응답은 디스크에 풀지 않고 메모리에서 읽습니다.
이 글의 핵심
corp_code는 공시 API가 회사를 구분하는 8자리 문자열입니다.stock_code는 비어 있을 수 있으며 선행 0과 영문을 보존해야 합니다.- 인증키는 환경변수에 두고 요청 URL이나 오류 로그에 남기지 않습니다.
- 중복과 코드 형식을 검사한 뒤 Parquet 파일을 원자적으로 교체합니다.

종목코드와 기업 고유번호는 역할이 다릅니다
종목코드는 거래소에서 증권을 구분할 때 사용하고, 기업 고유번호는 DART가 공시 제출 회사를 구분할 때 사용합니다. 한 회사의 주가와 재무제표를 결합하려면 두 값을 모두 보존해야 합니다.
| 회사명 | 종목코드 stock_code |
기업 고유번호 corp_code |
|---|---|---|
| 삼성전자 | 005930 |
00126380 |
| SK하이닉스 | 000660 |
00164779 |
| NAVER | 035420 |
00266961 |
corp_code와 stock_code를 정수로 바꾸면 안 됩니다. 005930의 앞자리 0이 사라질 수 있고, 기업 고유번호 파일에는 영문이 섞인 6자리 종목코드도 들어 있기 때문입니다. 두 열은 처음부터 문자열로 저장하는 편이 안전합니다.
OpenDART 기업 고유번호 개발가이드는 응답 ZIP 안의 CORPCODE.xml에 회사명, 영문명, 기업 고유번호, 종목코드, 최종변경일자가 들어 있다고 설명합니다. stock_code가 비어 있는 회사도 정상입니다. 공시 대상에는 상장회사만 있는 것이 아니므로, 주식 분석용 매핑표가 필요할 때만 값이 있는 행을 골라 사용합니다.
API 키는 코드가 아니라 .env에 둡니다
필요한 패키지를 먼저 설치합니다.
python -m pip install requests pandas pyarrow python-dotenv
실행 폴더에 .env 파일을 만들고 발급받은 키를 입력합니다.
DART_KEY=발급받은_40자리_인증키
.env는 Git에 올리지 않고 .gitignore에 포함해야 합니다. 공개 예제에는 DART_KEY라는 환경변수 이름만 남고 실제 값은 들어가지 않습니다. 요청 한도를 넘기기 위해 여러 키를 자동으로 돌려 쓰지 않고 한 키의 사용량 안에서 수집을 관리합니다.
API 요청을 디버깅할 때도 response.request.url을 출력하면 안 됩니다. 쿼리 문자열의 crtfc_key 값까지 로그에 남을 수 있기 때문입니다. 아래 코드는 네트워크 오류가 생겨도 키가 포함된 전체 URL 대신 상태만 보여 줍니다.
저장할 열은 다섯 개면 충분합니다
| 열 | 형식 | 의미 | 검사 기준 |
|---|---|---|---|
corp_code |
문자열 | DART 8자리 기업 고유번호 | 중복 없음 |
corp_name |
문자열 | 정식 회사명 | 빈 값 없음 |
corp_eng_name |
문자열 | 영문 정식 회사명 | 원문 보존 |
stock_code |
문자열 또는 결측 | 상장 증권의 6자리 코드 | 영문·선행 0 보존 |
modify_date |
날짜 | 기업개황정보 최종변경일 | YYYYMMDD 변환 가능 |
기업 고유번호 파일은 전체를 한 번에 내려받는 마스터 데이터입니다. 날짜별 가격처럼 매년 파일을 나누기보다 하나의 Parquet 파일을 새 응답으로 교체하는 편이 단순합니다. 다만 저장 도중 프로그램이 멈추면 기존 파일까지 깨질 수 있으므로 임시 파일을 완성한 뒤 최종 파일명으로 바꿉니다.
전체 Python 코드
아래 코드는 CORPCODE.xml을 메모리에서 읽어 열 형식을 정리하고, 기업 고유번호와 종목코드 중복을 검사한 뒤 data/dart_corp_codes.parquet에 저장합니다. 종목코드는 숫자 여섯 자리로 제한하지 않고 대문자 영문을 포함할 수 있는 6자리 문자열로 검사합니다.
#!/usr/bin/env python3
"""OpenDART 기업 고유번호를 받아 종목코드 매핑 Parquet로 저장한다."""
from __future__ import annotations
import argparse
from io import BytesIO
import os
from pathlib import Path
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
from dotenv import load_dotenv
import pandas as pd
import requests
ENDPOINT = "https://opendart.fss.or.kr/api/corpCode.xml"
COLUMNS = ("corp_code", "corp_name", "corp_eng_name", "stock_code", "modify_date")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=Path("data/dart_corp_codes.parquet"))
return parser.parse_args()
def read_error_message(content: bytes) -> str:
try:
root = ET.fromstring(content)
except ET.ParseError:
return "예상하지 못한 응답입니다."
status = root.findtext("status") or "상태 없음"
message = root.findtext("message") or "메시지 없음"
return f"OpenDART {status}: {message}"
def download_corp_codes(api_key: str) -> bytes:
try:
response = requests.get(
ENDPOINT,
params={"crtfc_key": api_key},
timeout=30,
)
except requests.RequestException:
raise RuntimeError("OpenDART 연결에 실패했습니다.") from None
if response.status_code != 200:
raise RuntimeError(f"OpenDART HTTP {response.status_code}")
return response.content
def parse_corp_codes(content: bytes) -> pd.DataFrame:
try:
with ZipFile(BytesIO(content)) as archive:
xml_content = archive.read("CORPCODE.xml")
except (BadZipFile, KeyError):
raise RuntimeError(read_error_message(content)) from None
root = ET.fromstring(xml_content)
records = []
for company in root.findall("list"):
records.append(
{
column: (company.findtext(column) or "").strip()
for column in COLUMNS
}
)
corp_codes = pd.DataFrame.from_records(records, columns=COLUMNS)
corp_codes["stock_code"] = corp_codes["stock_code"].replace("", pd.NA)
corp_codes["modify_date"] = pd.to_datetime(
corp_codes["modify_date"],
format="%Y%m%d",
errors="raise",
)
return corp_codes.sort_values("corp_code").reset_index(drop=True)
def validate_corp_codes(corp_codes: pd.DataFrame) -> dict[str, int]:
listed = corp_codes["stock_code"].notna()
invalid_stock_code = listed & ~corp_codes["stock_code"].str.fullmatch(
r"[0-9A-Z]{6}",
na=False,
)
report = {
"rows": len(corp_codes),
"listed_rows": int(listed.sum()),
"duplicate_corp_codes": int(corp_codes["corp_code"].duplicated().sum()),
"duplicate_stock_codes": int(corp_codes.loc[listed, "stock_code"].duplicated().sum()),
"missing_corp_names": int(corp_codes["corp_name"].eq("").sum()),
"invalid_stock_codes": int(invalid_stock_code.sum()),
}
error_keys = (
"duplicate_corp_codes",
"duplicate_stock_codes",
"missing_corp_names",
"invalid_stock_codes",
)
if any(report[key] for key in error_keys):
raise ValueError(f"기업 고유번호 품질 검사 실패: {report}")
return report
def save_atomically(corp_codes: pd.DataFrame, output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_suffix(".tmp.parquet")
corp_codes.to_parquet(temporary, index=False)
temporary.replace(output)
def main() -> None:
args = parse_args()
load_dotenv()
api_key = os.getenv("DART_KEY")
if not api_key:
raise SystemExit(".env에 DART_KEY를 입력해야 합니다.")
corp_codes = parse_corp_codes(download_corp_codes(api_key))
report = validate_corp_codes(corp_codes)
save_atomically(corp_codes, args.output)
print(f"전체 회사: {report['rows']:,}")
print(f"종목코드 보유 회사: {report['listed_rows']:,}")
print("중복·회사명·종목코드 검사: 정상")
print(f"저장 파일: {args.output}")
if __name__ == "__main__":
main()
현재 폴더에 Python 파일과 .env가 있다면 다음처럼 실행합니다.
python opendart_corp_codes.py
검증에 사용한 응답에서는 전체 회사 118,810개, 종목코드가 있는 회사 3,988개가 저장됐습니다. 기업 고유번호와 비어 있지 않은 종목코드의 중복은 각각 0개였고, 종목코드 가운데 58개에는 영문이 포함돼 있었습니다. 회사 등록과 변경에 따라 행 수는 달라질 수 있지만, 중복과 형식 검사를 통과해야 저장한다는 기준은 그대로 유지됩니다.
매핑할 때 회사명보다 코드를 사용합니다
회사명은 띄어쓰기, 사명 변경, 약칭 때문에 같은 회사를 안정적으로 연결하기 어렵습니다. 가격 데이터의 stock_code를 기업 매핑표와 결합해 corp_code를 얻고, 이후 공시 조회에서는 corp_code를 기본키로 사용합니다.
매핑되지 않은 종목을 임의의 비슷한 회사명에 붙이면 안 됩니다. 다음 항목을 먼저 확인합니다.
- 종목코드를 문자열로 읽어 선행 0이 보존됐는지 확인합니다.
- 기업 고유번호 파일을 최신 응답으로 다시 받습니다.
- 코드 변경, 합병, 상장폐지처럼 시점별 이력이 필요한 사건인지 확인합니다.
- 현재 매핑표만으로 과거 전체 종목군을 대신하지 않습니다.
기업 고유번호의 modify_date는 기업개황정보의 최종변경일이지 상장일이나 상장폐지일이 아닙니다. 생존자 편향이 없는 과거 백테스트를 만들려면 KRX의 시점별 종목 이력을 별도로 보존해야 합니다.
오류 메시지는 이렇게 해석합니다
OpenDART 개발가이드의 상태 코드 가운데 수집 과정에서 자주 확인할 값은 다음과 같습니다.
| 상태 | 의미 | 먼저 확인할 것 |
|---|---|---|
010 |
등록되지 않은 키 | 환경변수 이름과 발급 키 |
011 |
사용할 수 없는 키 | 키 사용 상태 |
013 |
조회 데이터 없음 | 요청 조건과 회사·기간 |
020 |
요청 제한 초과 | 같은 요청의 반복 여부와 일일 사용량 |
오류가 났다고 키 값을 터미널에 출력하지 않습니다. 환경변수가 존재하는지, 응답 상태가 무엇인지까지만 확인해도 대부분의 원인을 분리할 수 있습니다.
수집이 끝나면 원본을 바로 덮어쓰지 않습니다
기업 매핑표는 재무제표, 공시 검색, 종목 마스터를 이어 주는 기준표입니다. 잘못 저장되면 이후 모든 데이터가 다른 회사에 붙을 수 있으므로 다음 순서를 지킵니다.
- 새 응답을 메모리에서 파싱합니다.
- 기업 고유번호·종목코드 중복과 문자열 형식을 검사합니다.
- 임시 Parquet 파일을 완성합니다.
- 검사가 끝난 파일만 기존 매핑표와 교체합니다.
yfinance로 국내 주식 데이터 받기는 가격 데이터의 종목코드와 수정주가를 보존하는 방법을 다룹니다. 키움증권 REST API로 주식 DB 만들기는 반복 수집에서 기본키와 UPSERT가 필요한 이유를 설명합니다. 백테스트 결과를 믿기 전 확인할 3가지는 현재 종목 목록을 과거 전체에 적용할 때 생기는 편향을 함께 확인하기 좋습니다.
기업 고유번호 매핑표를 만들었다면 다음 재무정보 요청부터 회사명을 검색하지 않고 corp_code를 직접 사용할 수 있습니다. 이 한 단계가 가격·공시·재무제표를 안정적으로 연결하는 출발점입니다.
이 글은 OpenDART 데이터 수집 구조를 설명하기 위한 교육용 자료입니다. API 제공 범위와 이용 조건은 OpenDART의 최신 공식 안내를 우선하며, 수집한 정보만으로 특정 종목의 투자 가치를 판단하지 않습니다.