반응형

전자공시(Open DART) 재무제표 크롤링 03:: 단일회사 전체 재무제표

 

  앞서 전자공시 오픈API를 통해 기업고유번호를 받아와 DataFrame으로 만드는 것을 해보았습니다. 이제는 단일회사 전체 재무제표를 받아오는 것을 하겠습니다. 아직 여러 기업의 재무제표에 대해 테스트를 해보지는 않았지만 현재 코드를 남겨봅니다.

 

  전자공시 오픈API의 "상장기업 재무정보" > "단일회사 전체 재무제표 개발가이드"를 살펴보면 기본적으로 요청url이 다음처럼 2개가 있습니다.

  • https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json
  • https://opendart.fss.or.kr/api/fnlttSinglAcntAll.xml

출력포멧을 JSON 또는 XML으로 하는지에 따라 요청하는 URL이 달라집니다. 앞서 기업고유번호를 받을 때에는 xml로 된 파일만 제공이 되었었습니다. 근데 저는 xml이든 json이든 모르기는 똑같아서 Julia 패키지 설명이 더 직관적이라는 이유로 JSON을 택했습니다.

 

  우선 간략하게 필요한 인자를 살펴보면 아래 표와 같으며, 5가지의 값을 필수로 넘겨줘야할 값들입니다.

명칭 타입 값설명
crtfc_key API 인증키 STRING(40) 발급받은 인증키(40자리)
corp_code 고유번호 STRING(8) 공시대상회사의 고유번호(8자리)
※ 개발가이드 > 공시정보 > 고유번호 API조회 가능
bsns_year 사업연도 STRING(4) 사업연도(4자리)
※ 2015년 이후 부터 정보제공
reprt_code 보고서 코드 STRING(5) 1분기보고서 : 11013
반기보고서 : 11012
3분기보고서 : 11014
사업보고서 : 11011
fs_div 개별/연결구분 STRING(3) CFS:연결재무제표, OFS:재무제표

API인증키와 고유번호는 앞서 해결이 되었고, 나머지 3개의 값은 본인이 원하는 값을 넣어주면 됩니다.

 

요청할 때의 URL은 이런 모습이 됩니다.

"https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json?crtfc_key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&corp_code=00126380&bsns_year=2018&reprt_code=11011&fs_div=OFS"

 

요청해서 반환받은 JSON의 구조는 아래 그림과 같습니다.

status나 message는 정상적인 값이 반환되는지, 에러가 있으면 어떤 에러인지를 담고 있습니다. 재무제표는 list에 담겨 있습니다. list 안에는 0, 1, 2, 3, ..., n 이렇게 담겨 있는데, 이는 재무제표를 출력하게 되었을 때, 한 줄씩 해당됩니다. 그리고 최종적으로 각 줄은 아래 표와 같은 정보를 갖게 됩니다.

 

명칭 출력설명
rcept_no 접수번호 접수번호(14자리)
※ 공시뷰어 연결에 이용예시
- PC용 : http://dart.fss.or.kr/dsaf001/main.do?rcpNo=접수번호
- 모바일용 : http://m.dart.fss.or.kr/html_mdart/MD1007.html?rcpNo=접수번호
reprt_code 보고서 코드 1분기보고서 : 11013
반기보고서 : 11012
3분기보고서 : 11014
사업보고서 : 11011
bsns_year 사업 연도 2018
corp_code 고유번호 공시대상회사의 고유번호(8자리)
sj_div 재무제표구분 BS : 재무상태표
IS : 손익계산서
CIS : 포괄손익계산서
CF : 현금흐름표
SCE : 자본변동표
sj_nm 재무제표명 ex) 재무상태표 또는 손익계산서 출력
account_id 계정ID XBRL 표준계정ID
※ 표준계정ID가 아닐경우 "-표준계정코드 미사용-" 표시
account_nm 계정명 계정명칭
ex) 자본총계
account_detail 계정상세 ※ 자본변동표에만 출력
ex) 계정 상세명칭 예시
- 자본 [member]|지배기업 소유주지분
- 자본 [member]|지배기업 소유주지분|기타포괄손익누계액 [member]
thstrm_nm 당기명 ex) 제 13 기
thstrm_amount 당기금액 9,999,999,999
※ 분/반기 보고서이면서 (포괄)손익계산서 일 경우 [3개월] 금액
thstrm_add_amount 당기누적금액 9,999,999,999
frmtrm_nm 전기명 ex) 제 12 기말
frmtrm_amount 전기금액 9,999,999,999
frmtrm_q_nm 전기명(분/반기) ex) 제 18 기 반기
frmtrm_q_amount 전기금액(분/반기) 9,999,999,999
※ 분/반기 보고서이면서 (포괄)손익계산서 일 경우 [3개월] 금액
frmtrm_add_amount 전기누적금액 9,999,999,999
bfefrmtrm_nm 전전기명 ex) 제 11 기말(※ 사업보고서의 경우에만 출력)
bfefrmtrm_amount 전전기금액 9,999,999,999(※ 사업보고서의 경우에만 출력)
ord 계정과목 정렬순서 계정과목 정렬순서

 

위의 표를 살펴보면 총 20개의 키와 값이 있습니다. 실제로 데이터를 받아보면 20개의 모든 값이 반환되는 것은 아닙니다. 보고서의 종류에 따라 일부 키와 값이 없을 수 있습니다.

 

여기서 보고서의 종류에 따라서 키의 개수가 바뀔 수 있으니 그 개수에 맞는 컬럼을 가지는 DataFrame을 만드는게 나을지, 아니면 무시하고 위의 20개의 키를 컬럼명으로 가지는 DataFrame을 만드는게 나을지 한참을 고민하였습니다. 이 고민을 하는 것은 이번 글에는 담지는 않겠지만 최종적으로 SQL DB에 Table 형태로 넣었을때, 어느게 관리하는게 유용한지 DB쪽도 초보인 저로썬 판단이 잘 서질 않았습니다. 최종적으로는 위의 20개 키를 컬럼으로 하는 DataFrame을 만드는 걸로 결정했습니다. 그게 우선 편하거니와, 일단 보고서 종류에 상관없이 하나의 table에 다 때려넣고 뽑아쓸 때 잘 필터링하자라는 생각입니다.

 

잡담이 길었고, 작성한 코드는 2개의 함수입니다. 2개로 나눈 이유는 본 글의 "단일회사 전체 재무제표" 외에도 "단일회사 주요계정"과 "다중회사 주요계정"을 크롤링하는 코드를 만들다보니 겹치는 부분이 있어 공통된 부분은 별도의 함수로 빼내었습니다.

 

우선 이용될 패키지는 다음과 같습니다.

using HTTP, JSON, DataFrames

 

앞으로 "단일회사 전체 재무제표" 외에도 "단일회사 주요계정"과 "다중회사 주요계정"을 요청할 때 동일하게 사용할 함수는 다음과 같습니다.

function convertFnltt(url::String,items::Array{String},item_names::Array{String})
  res = HTTP.get(url) 
  json_dict = JSON.parse(String(res.body)) # json을 dictionary로 파싱
  if json_dict["status"] == "000" # DART에서 받아온 게 "정상"일때
    tmp_arr = [] # 임시 1차원 배열
    for key in keys(json_dict["list"]) # list 내부의 0,1,2,3,..,n
      ks = keys(json_dict["list"][key]) # n 내부의 키 값들
      for i in 1:length(items) # 키와 값이 있든 없든 items 순서대로 1차원 배열에 넣기
        if items[i] in ks
          push!(tmp_arr,json_dict["list"][key][items[i]])
        else
          push!(tmp_arr,"")
        end
      end
    end
    df = convert(DataFrame,permutedims(reshape(tmp_arr,length(items),trunc(Int,length(tmp_arr)/length(items)))))
    # 1차원 배열을 item개수의 컬럼을 가진 2차원 형태로 변환 후 DataFrame으로 변환
    rename!(df,item_names)  # DataFrame의 컬럼 이름 넣기
    return df
  end
end

 

위의 함수가 하는 일은 단순합니다. Open DART에 요청할 URL과 데이터의 키를 담은 배열(items), 각 키에 해당하는 컬럼명(item_names)을 넘겨주면, DART에서 받아온 데이터를 items 갯수에 맞는 컬럼을 가지는 테이터프레임을 반환합니다. 

 

이제 위의 함수를 이용하여 "단일회사 전체 재무제표"를 가져오는 get_fnlttSinglAcntAll()을 만들어보면 아래와 같습니다.

 

function get_fnlttSinglAcntAll(crtfc_key::String, corp_code::String,
        bsns_year::String, reprt_code::String, fs_div::String = "CFS")

  items = ["rcept_no","reprt_code","bsns_year","corp_code","sj_div","sj_nm",
          "account_id","account_nm","account_detail","thstrm_nm",
          "thstrm_amount","thstrm_add_amount","frmtrm_nm","frmtrm_amount",
          "frmtrm_q_nm","frmtrm_q_amount","frmtrm_add_amount","bfefrmtrm_nm",
          "bfefrmtrm_amount","ord"]
          # 반환 키 값
          
  item_names = ["접수번호","보고서코드","사업연도","고유번호","재무제표구분","재무제표명",
            "계정ID","계정명","계정상세","당기명","당기금액","당기누적금액","전기명","전기금액",
            "전기명(분/반기)","전기금액(분/반기)","전기누적금액","전전기명",
            "전전기금액","계정과목정렬순서"]
           # DataFrame의 컬럼 이름
  
  url = "https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json?"*
        "crtfc_key=$crtfc_key"*"&corp_code=$(corp_code)"*
        "&bsns_year=$(bsns_year)"*"&reprt_code=$(reprt_code)"*"&fs_div=$(fs_div)"
  		# 요청 URL
        
  return convertFnltt(url,items,item_names)
end

 

반환된 DataFrame을 출력해보면 확인이 되는데, 컬럼이 20개라 보기가 어렵습니다. 제대로 작동하는지 확인하기 위해 위의 함수 예제로 엊그제 갑자기 상승한 "로보티즈" 업체의 2019년 재무제표를 전자공시 API로 받아와서 excel로 저장해 보겠습니다.

 

julia> using XLSX

julia> df = get_fnlttSinglAcnt("인증키40자리","00946030","2019","11011","CFS")
julia> XLSX.writetable("report.xlsx", SHEET1=(collect(DataFrames.eachcol(df)), DataFrames.names(df)))

 

줄리아의 데이터프레임을 엑셀로 변환하는 것은 나중에 다른 글에서 설명하겠습니다. 아무튼 요청한 재무제표를 저장한 엑셀 파일을 열어보면 아래 그림과 같습니다.

 

 

빈 셀이 많은 것은 요청해서 받은 데이터에 키와 값이 있던 없던 빈 칸으로 만들었기 때문입니다. 앞으로의 계획은 하나의 테이블에 모든 년도의 각종 보고서의 재무제표를 하나의 테이블에 다 때려넣고 뽑아쓸 때 잘 걸러내서 쓸 생각입니다.

 

반응형

+ Recent posts