YOGYUI

금융감독원::OPENDART 전자공시 Open API 사용하기 본문

Data Analysis/Data Engineering

금융감독원::OPENDART 전자공시 Open API 사용하기

요겨 2021. 9. 18. 11:24
반응형

 

대한민국 금융감독원의 기업정보전자공시시스템(Data Analysis, Retrieval and Transfer system, DART)는 개인, 기업, 기관 누구든지 공시되어 있는 공시보고서 원문을 쉽게 열람할 수 있는 OpenAPI 서비스인 OPENDART를 제공하고 있다

연혁을 보면 OPENDART는 2020년 4월부터 개시된 서비스이니 상당히 따끈따끈한 신종 Open API라 할만하다

공시자료 읽지 않는 자, 주식투자하지 말라

 

심지어 파이썬 패키지로도 개발되어 있다

https://dart-fss.readthedocs.io/en/latest/

 

DART-FSS — dart-fss documentation v0.3.10 documentation

© Copyright 2021, Sungwoo Jo Revision dc5f617d.

dart-fss.readthedocs.io

(이 외에도 안드로이드 SDK 등 조금만 구글링해보면 개발자들이 열심히 개발해둔 라이브러리가 꽤 많다 ㅎㅎ 주식투자 열풍을 뒷받침하는게 아닐까 싶다)

 

남이 만들어 둔 걸 그대로 쓰는건 재미가 없으니, 직접 코드를 짜면서 OpenAPI 사용법을 알아보도록 하자

 

1. 회원가입

OPENDART 홈페이지로 들어가자 (https://opendart.fss.or.kr/)

메뉴 - '인증키 신청/관리' - '인증키 신청' 클릭

이용약관과 개인정보 수집동의 후, 이메일과 비밀번호 및 API 사용환경 (나는 웹과 앱 모두 사용) 및 사용용도를 기입한 후 등록하면 된다

 

등록 후, 기입한 메일주소로 바로 신청 확인 메일이 날아온다

 

인증링크를 클릭하면 인증이 즉시 완료된다

 

2. API Key 확인하기

가입한 이메일주소로 로그인 후 메뉴 - '인증키 신청/관리' - '인증키 관리' 항목을 보자

 

별다른 사유가 없으면 상태는 즉시 '승인'으로 신청된 상태

'오픈API 이용현황' 메뉴를 열어보면 제일 중요한 API Key를 확인할 수 있다

이 Key를 활용해서 앞으로 모든 작업을 수행하게 되니 메모장같은데 복사해두자 (길이 40의 문자열)

 

일일이용현황을 보면 하루에 허용되는 API 호출 건수는 10,000회다

개발할 때는 하루에 만번 이상 호출할 일이 없을테지만, 웹이나 앱으로 deploy할 때는 일일허용건수 제한을 고려해야 한다

(개인이 아니라면 다를지도? 자세히 알아보진 않았다)

 

3. OpenAPI 매뉴얼 열람하기

공공데이터포털 수준의 상세한 OpenAPI 사용 매뉴얼을 제공한다 (메뉴 - '개발가이드')

일례로 '상장기업 재무정보' - 단일회사 주요계정 개발가이드를 보자

API 호출 시 메서드(GET/POST) 및 인자, 결과값에 대한 해석 방법, 요청 응답 코드에 대한 설명 등을 자세하게 기재해두었으니, 개발자는 그냥 매뉴얼대로만 동작 코드 및 예외 처리를 구현해주면 된다 (쉽다!)

 

4. 코드 구현

이제 실제로 코드를 구현해보자 (모든 코드는 Python 3.8 기반으로 작성했다)

모든 코드의 핵심 패키지는 HTTP 라이브러리 중 가장 대중적인 requests를 활용했다

https://pypi.org/project/requests/

4.1. 기업 고유번호 리스트 가져오기

제일 처음으로 구현해 볼 것은 기업의 '고유번호' 리스트를 가져오는 것이다

(유가증권시장에서 사용되는 6자리 약식코드랑은 별개의 것)

검색할 때 회사의 '고유번호'를 제대로 알아야 조회가 편리하기 때문~ (그냥 회사명으로 검색해도 상관은 없지만...)

해당 API를 호출하면 Zip 압축파일의 바이트스트림을 보내주기 때문에, zipfile 라이브러리도 사용해주자

import requests
import zipfile

url = "https://opendart.fss.or.kr/api/corpCode.xml"
api_key = "Your API Key"
params = {
    'crtfc_key': api_key
}

response = requests.get(url, params=params)

# save zip file in local
with open('./id.zip', 'wb') as fp:
    fp.write(response.content)

# extract file
zf = zipfile.ZipFile('./id.zip')
zf.extractall()

xml_path = os.path.abspath('./CORPCODE.xml')

압축을 풀면 'CORPCODE.xml' 파일 1개가 생성된다 

xml 파일을 열어보면

<?xml version="1.0" encoding="UTF-8"?>
<result>
    <list>
        <corp_code>00434003</corp_code>
        <corp_name>다코</corp_name>
        <stock_code> </stock_code>
        <modify_date>20170630</modify_date>
    </list>
    <list>
        <corp_code>00434456</corp_code>
        <corp_name>일산약품</corp_name>
        <stock_code> </stock_code>
        <modify_date>20170630</modify_date>
    </list>

각 기업별로 <list> 태그안에 고유번호, 정식명칭, 종목코드, 최종변경일자 태그들이 담겨있다

bs4 패키지의 BeautifulSoup를 활용해서 xml 파일을 읽어보자

import cchardet
from bs4 import BeautifulSoup

with open(xml_path, 'r', encoding='utf-8') as fp:
    corpcode = BeautifulSoup(fp.read(), 'lxml')

<list> 태그의 개수를 얻어보면

In [1]: len(corpcode.find_all('list'))
Out[1]: 88127

주식시장에 상장되지 않았더라도, 일반적으로 기업은 '공시'를 해야되기 때문에 대상 기업수가 9월 18일 기준으로 88,127개로 굉장히 많은 것을 알 수 있다

 

BeautifulSoup로 xml 파싱하는게 너무 오래 걸려서, built-in 패키지인 xml.etree로 바꿨다 ㅠ

정확한 원인 판명은 귀찮아서 안했는데, 아무래도 character detection 관련 이슈인거 같긴 한데 아무리 손써봐도 성능 개선이 잘 안되어서 그냥 xml.etree로 바꾸니 훨씬 빨라졌다 ㅎㅎ (체감상 10배 이상?)

import xml.etree.ElementTree as ET

tree = ET.parse(xml_path)
root = tree.getroot()
tags_list = root.findall('list')

pandas 패키지로 xml 파일 내용물을 DataFrame으로 만들어보자

import pandas as pd

def convert(tag: ET.Element) -> dict:
    conv = {}
    for child in list(tag):
        conv[child.tag] = child.text
    return conv

tags_list_dict = [convert(x) for x in tags_list]
df = pd.DataFrame(tags_list_dict)
In [2]: df.head(5)
Out[2]:
  corp_code          corp_name stock_code modify_date
0  00434003                 다코               20170630
1  00434456               일산약품               20170630
2  00430964              굿앤엘에스               20170630
3  00432403               한라판지               20170630
4  00388953  크레디피아제이십오차유동화전문회사         20170630

In [3]: df.tail(5)
Out[3]: 
      corp_code   corp_name stock_code modify_date
88122  01560457        삼원개발               20210506
88123  01556533   에이더블유파트너스             20210506
88124  01546299      동영와이케이              20210506
88125  00694605        미디어윌               20210506
88126  01368761  엔브이에이치원방테크             20210506

head와 tail을 보니 비상장회사라 그런지 처음보는 기업들이 막 나온다 ㅋㅋ

In [4]: df[df['corp_name'] == '카카오뱅크']
Out[4]:
      corp_code corp_name stock_code modify_date
83001  01133217     카카오뱅크     323410    20210809

올해 상장한 종목인 카카오뱅크를 검색해보면 약식코드 323410이 제대로 기재되어 있는 것을 알 수 있다

 

4.2. 공시자료 검색

'공시검색' API로 공시보고서 검색 결과를 가져와보자

자세한 API 명세는 공시검색 개발가이드 참고

앞서 알아본 카카오뱅크는 8월 6일 유가증권에 상장되었으니 7월 중에 증권신고서가 공시되었을 것 같으니 기업고유코드 01133217을 기입해서 검색해보자 (JSON 포맷으로 요청)

- 검색조건 중 날짜를 2021년 7월 1일 ~ 2021년 7월 31일로 제한

import requests

url_json = "https://opendart.fss.or.kr/api/list.json"
url_xml = "https://opendart.fss.or.kr/api/list.xml"

api_key = "Your API Key"
corp_code = '01133217'  # 카카오뱅크

params = {
    'crtfc_key': api_key,
    'corp_code': corp_code,
    'bgn_de': '20210701',
    'end_de': '20210731'
}

response = requests.get(url_json, params=params)
data = response.json()
In [5]: data
Out[5]:
{'status': '000', 'message': '정상', 'page_no': 1, 'page_count': 10, 'total_count': 9, 'total_page': 1, 'list': [{'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '증권발행실적보고서', 'rcept_no': '20210729000467', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210729', 'rm': ''}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '[기재정정]주요사항보고서(유상증자결정)', 'rcept_no': '20210722000394', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210722', 'rm': ''}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '[기재정정]투자설명서', 'rcept_no': '20210722000374', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210722', 'rm': ''}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '[발행조건확정]증권신고서(지분증권)', 'rcept_no': '20210722000371', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210722', 'rm': ''}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '투자설명서', 'rcept_no': '20210720000303', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210720', 'rm': '정'}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '[첨부정정]증권신고서(지분증권)', 'rcept_no': '20210719000305', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210719', 'rm': '정'}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '[기재정정]증권신고서(지분증권)', 'rcept_no': '20210719000237', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210719', 'rm': '정'}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '계열금융회사의약관에의한금융거래-[유가증권-채권]', 'rcept_no': '20210709000305', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210709', 'rm': '공'}, {'corp_code': '01133217', 'corp_name': '카카오뱅크', 'stock_code': '323410', 'corp_cls': 'Y', 'report_nm': '계열금융회사의약관에의한금융거래-[유가증권-수익증권]', 'rcept_no': '20210709000293', 'flr_nm': '카카오뱅크', 'rcept_dt': '20210709', 'rm': '공'}]}

status = 00 으로 정상 응답이 들어왔으며 총 9개의 검색 결과 레코드가 반환되었다

json의 'list' 항목은 일정한 메타데이터로 구성되어 있으므로 손쉽게 pandas 데이터프레임으로 변환가능하다

import pandas as pd

data_list = data.get('list')
df_list = pd.DataFrame(data_list)
In [6]: df_list
Out[6]:
  corp_code corp_name stock_code corp_cls  ...        rcept_no flr_nm  rcept_dt rm
0  01133217     카카오뱅크     323410        Y  ...  20210729000467  카카오뱅크  20210729   
1  01133217     카카오뱅크     323410        Y  ...  20210722000394  카카오뱅크  20210722   
2  01133217     카카오뱅크     323410        Y  ...  20210722000374  카카오뱅크  20210722   
3  01133217     카카오뱅크     323410        Y  ...  20210722000371  카카오뱅크  20210722   
4  01133217     카카오뱅크     323410        Y  ...  20210720000303  카카오뱅크  20210720  정
5  01133217     카카오뱅크     323410        Y  ...  20210719000305  카카오뱅크  20210719  정
6  01133217     카카오뱅크     323410        Y  ...  20210719000237  카카오뱅크  20210719  정
7  01133217     카카오뱅크     323410        Y  ...  20210709000305  카카오뱅크  20210709  공
8  01133217     카카오뱅크     323410        Y  ...  20210709000293  카카오뱅크  20210709  공
[9 rows x 9 columns]

여기서 중요한 것은 문서의 제목(보고서명, report_nm)과 접수번호(rcept_no)이다

(접수번호를 정확히 알아야 문서 원본파일을 가져올 수 있다)

In [7]: df_list[['report_nm', 'rcept_no']]
Out[7]:
                      report_nm              rcept_no
0                     증권발행실적보고서        20210729000467
1         [기재정정]주요사항보고서(유상증자결정)     20210722000394
2                   [기재정정]투자설명서         20210722000374
3           [발행조건확정]증권신고서(지분증권)      20210722000371
4                         투자설명서          20210720000303
5             [첨부정정]증권신고서(지분증권)       20210719000305
6             [기재정정]증권신고서(지분증권)       20210719000237
7    계열금융회사의약관에의한금융거래-[유가증권-채권]   20210709000305
8  계열금융회사의약관에의한금융거래-[유가증권-수익증권]  20210709000293

발행조건이 확정된 증권신고서의 접수번호는 20210722000371 이다

이 번호를 토대로 원본문서를 가져와보자

4.3. 공시자료 문서 가져오기

'공시서류원본파일' API로 공시보고서 원본 파일을 가져와보자

자세한 API 명세는 공시서류원본파일 개발가이드 참고

앞서 검색한 카카오뱅크(DART 고유아이디: 01133227, 증권코드: 323410)의 발행조건확정 증권신고서(문서 접수번호 20210722000371) 문서를 가져와보자

반환 결과는 앞서 살펴본 고유번호 리스트와 동일하게 Zip File 포맷의 바이너리 파일이기 때문에 zipfile로 압축해제하는 과정이 필요하다

import requests
import zipfile

url = "https://opendart.fss.or.kr/api/document.xml"
api_key = "Your API Key"
rcept_no = "20210722000371"  # 카카오뱅크 증권신고서

params = {
    'crtfc_key': api_key,
    'rcept_no': rcept_no
}

doc_zip_path = os.path.abspath('./document.zip')

if not os.path.isfile(doc_zip_path):
    response = requests.get(url, params=params)
    with open(doc_zip_path, 'wb') as fp:
        fp.write(response.content)

zf = zipfile.ZipFile(doc_zip_path)
zf.extractall()

압축 해제 후 경로에 문서번호.xml 형식의 파일이 생성된 것을 볼 수 있다

그런데, 압축해제된 xml 파일을 문서 리더기로 읽어보면 인코딩이 깨져있다

Sublime Text로 문서 읽은 결과

이리저리 삽질해본 결과, API에는 분명 UTF-8이라고 인코딩이 명시되어 있음에도 불구하고, 희한하게도 xml 파일의 인코딩이 EUC-KR로 설정되어 있어서 발생하는 문제같았다 (macOS, windows 모두 동일...)

그래서~~ 압축 해제된 파일을 euc-kr 인코딩으로 읽은 뒤, 다시 utf-8 인코딩으로 저장하는 코드를 추가했다

zf = zipfile.ZipFile(doc_zip_path)
zipinfo = zf.infolist()
filenames = [x.filename for x in zipinfo]
filename = filenames[0]
zf.extract(filename)
zf.close()

# encoding 깨짐 문제 해결
with open(f'./{filename}', 'r', encoding='euc-kr') as fp:
    lines = fp.readlines()
with open('./temp.xml', 'w', encoding='utf-8') as fp:
    fp.writelines(lines)

새롭게 저장된 temp.xml 파일을 열어보면 

문자열이 제대로 표현된다!

 

그런데...

크롬 브라우저에서 xml 파일을 불러오니 또다른 문제가 발생했다

xml 파일 내용 중 일부분에 &cr; 라는 항목들이 끼어있어서 웹 브라우저에서는 오류가 발생한다...

아무래도 carriage return (CR)을 의도한 것 같으니 해당 문자열을 모두 &#13; 으로 교체해보도록 하자

# encoding 깨짐 문제 해결
with open(f'./{filename}', 'r', encoding='euc-kr') as fp:
    lines = fp.readlines()
    
# &cr; 문자열 대체
lines = [x.replace('&cr;', '&#13;') for x in lines]

with open('./temp.xml', 'w', encoding='utf-8') as fp:
    fp.writelines(lines)

그리고 다시 브라우저에서 열어보면...

tag mismatch 오류가 발생한다...

본문 중에 주석을 가리키는 <주1> 과 같은 문자열들이 태그로 인식되기 때문에 나타나는 문제로 판명되었다

이 외에도 'M&A', 'R&D', 'S&P' 같은 문자열도 파싱이 안되는 등 여러가지 문제가 있으니, 예외처리 코드는 사용자가 입맛대로 추가해주면 된다

# &cr; 문자열 대체
lines = [x.replace('&cr;', '&#13;') for x in lines]
# <주 문자열 대체
lines = [x.replace('<주', '&lt;주') for x in lines]
# & 문자열 대체
lines = [x.replace('M&A', 'M&amp;A') for x in lines]
lines = [x.replace('R&D', 'R&amp;D') for x in lines]

이제 브라우저에서 열어보면

파싱이 제대로 된 것을 알 수 있다

앞서 기업 고유번호 xml 파일과 동일하게 공시문서도 xml.etree 패키지로 읽어보자

import xml.etree.ElementTree as ET
tree = ET.parse('./temp.xml')
root = tree.getroot()
body = root.find('BODY')
insertion = body.find('INSERTION')
In [8]: insertion.attrib
Out[8]: {'ABASISNUMBER': 'E1', 'AFREQUENCY': '1', 'ADUPLICATION': 'N'}

이제 파이썬으로 특정 공시문서를 xml 포맷으로 불러온 뒤 분석할 준비는 끝났다

(html로 문서를 예쁘게 불러오는 방법을 고민하다가 일단은 때려치웠다 ㅋㅋ)

5. 마무리

OPENDART가 제공하는 API 종류는 굉장히 많으니, 여기에 링크를 모두 걸어두기로 한다

(왠만한건 다 무슨 소린지도 모르겠다 ㅋㅋㅋ 회계 전문가가 아닌 이상에야 전부 다 볼 필요가 있을까싶긴 하다)

대부분 API는 정기보고서(사업, 분기, 반기) 내에서 특정 정보들만 추출한 것이므로, 필요에 따라 요긴하게 쓸 수 있을 것 같다

무려 81종류의 API !!

마지막 예시로 '교환사채권 발행 결정' API를 한번 호출해보자

https://www.bloter.net/newsView/blt202109160013

 

[넘버스]LS전선 교환사채 발행, ‘유동성 경고’ 신호일까

넘쳐나는 데이터와 숫자, 누구에게나 공개돼 있고 누구나 볼 수 있지만 해석하기가 쉽지 않습니다. 숫자 뒤에 숨은 진실을 보는 눈, 데이터를 해석해

www.bloter.net

LS전선 (DART ID: 00683283)이 9월 15일 교환사채를 발행했다는 뉴스를 18일에 접했다

과연 OPENDART에서 교환사채 발행 관련 API 호출이 제대로 될까?

import requests

url_json = "https://opendart.fss.or.kr/api/exbdIsDecsn.json"
api_key = "Your API Key"
corp_code = "00683283"  # LS전선

params = {
    'crtfc_key': api_key,
    'corp_code': corp_code,
    'bgn_de': "20210101",
    'end_de': "20210918"
}

response = requests.get(url_json, params=params)
data = response.json()
elem = data.get('list')[0]
for key, value in elem.items():
    print(key + '\t' + value)
rcept_no	20210914000374
corp_cls	E
corp_code	00683283
corp_name	LS전선
bddd	2021년 09월 14일
od_a_at_t	0
od_a_at_b	0
adt_a_atn	불참
fdpp_fclt	-
fdpp_bsninh	-
fdpp_op	-
fdpp_dtrp	30,000,000,000
fdpp_ocsa	-
fdpp_etc	-
ftc_stt_atn	미해당
bd_tm	25
bd_knd	무기명식 이권부 무보증 사모 교환사채
bd_fta	30,000,000,000
ovis_fta	-
ovis_fta_crn	-
ovis_ster	-
ovis_isar	-
ovis_mktnm	-
bd_intr_ex	0.00
bd_intr_sf	1.00
bd_mtd	2022년 12월 17일
bdis_mthn	사모
sbd	2021년 09월 16일
pymd	2021년 09월 17일
rpmcmp	-
grint	-
rs_sm_atn	아니오
ex_sm_r	
-  면제사유 : 사모발행으로 인한 증권신고서 제출면제
   (증권의 발행 및 공시 등에 관한 규정 제2-2조제2항      제2호 조치에 따라 거래단위를 50단위 미만으로 발행   하고 발행일로부터 1년간 최초 발행시의 거래단위 이    상으로 분할금지)
ovis_ltdtl	해당사항 없음
ex_rt	100
ex_prc	9,201
ex_prc_dmth	- "본 사채”최초 교환가격은 1주당 금 9,201원으로 
    결정하며, 최초 교환가격은 하기와 같은 기준으로 
    결정한다.   
※ 최초 교환가격 산정 기준
- "본 사채“의 최초 교환가격은 “본 사채” 발행을         위한 이사회결의일 전일을 기산일로 소급하여 산정      한 다음의 가격 중(하기 1,2,3참조) 가장 높은 가액       대비 5% 할증한 금액을 최초 교환가격으로 하되, 
    원단위 미만은 절상한다.
1)  LS전선아시아 보통주의 1개월 가중산술평균주가
    (그 기간 동안 한국거래소에서 거래된 해당 종목의 
    총 거래금액을 총 거래량으로 나눈 가격), 
    1주일 가중산술평균주가 및 최근일 
    가중 산술평균주가를 산술평균한 가액
2) LS전선아시아 보통주의 최근일 가중산술평균주가
3) LS전선아시아 보통주의 교환사채 청약일 전
   (청약일이 없는 경우 납입일) 제3거래일 
   가중산술평균주가
extg	 LS전선아시아㈜의 기명식 보통주식
extg_stkcnt	3,260,515
extg_tisstk_vs	-
exrqpd_bgd	2021년 10월 17일
exrqpd_edd	2022년 11월 17일

사채의 종류(bd_knd)는 '무기명식 이권부 무보증 사모 교환사채 (뭔소리여...)'이며, 자금조달의 목적(fdpp_dtrp)은 채무상환을 위한 300억원, 교환대상 주식(extg_stkcnt)수는 3,260,515주, 교환에 관한 사항(ex_prc)에서 주당 9,201원 등 사채 발생과 관련된 거의 모든 정보를 쉽게 얻을 수 있다!

 

이왕 재미로 시작한 거, 나만의 OPENDART 프로젝트를 하나 만들어서 꾸준히 진행해나가볼 예정

81개 API를 모두 호출할 수 있도록 기능을 한개씩 한개씩 점차적으로 추가해보자!

시작할 때 GitHub repo도 한개 만들고...

 

최종적으로는 NLP로 공시보고서 자동 분석 시스템을 구현하는 것이 목표!

(사실 공시보고서는 구조화가 잘 된 문서라서 굳이 NLP까지 써야할 지는 의문이지만)

 

반응형