Post List

2019년 2월 18일 월요일

R에서 전자공시시스템(DART) API를 이용한 크롤링



금융감독원의 전자공시 시스템(http://dart.fss.or.kr/)은
투자자라면 누구나 한번쯤 들어가본 사이트 입니다.

각종 공시가 가장 먼저 올라오는 곳이기도 하며,
과거 재무제표가 가공되지 않은채 존재하기도 하니까요


또한 dart는 API를 제공해주어
실시간으로 공시정보 수집 및 과거 데이터를 받을 수 있지만,

인터넷 대부분에 파이썬을 이용한 방법만 있으며
R을 이용한 dart API 이용법은 찾을 수 없습니다.

그래서 R에서도 동일하게 해당 api를 이용하여
전자공시 수집 및 DB 다운로드 방법을 살펴보도록 하겠습니다.




먼저 상단의 '오픈 API' 선택 후
좌측의 인증키 신청/관리를 통해
API KEY를 발급받으셔야 합니다.

하루 동안 요청할 수 있는 횟수가 10,000 번으로 매우 넉넉합니다.



오픈API 개발가이드를 보시면
각 쿼리가 의미하는 바를 알 수 있으며,
한번에 최대 10개까지의 데이터를 받을 수 있습니다.





아무런 쿼리도 입력하지 않으면
단순히 최신 공시 10건을 출력합니다.

최근에는 이를 이용하여 공시정보가 자동으로
텔레그램이나 문자로 오는 서비스도 많이 있습니다.

이 중 우리는 회사의 사업보고서 10건을 크롤링 하는
예제를 살펴보도록 하겠습니다.

예제에 따르면 주소는

http://dart.fss.or.kr/api/search.xml?auth=APIKEY&crp_cd=005930&start_dt=19990101&bsn_tp=A001

이 될 것입니다.
(A002는 반기, A003은 분기를 의미하므로)

이중 auth 는 여러분이 발급받은 API key를 입력하셔야 하며,
crd_cd는 회사의 티커 6자리를 입력하시면 됩니다.


또한 데이터를 xml이 아닌 json 형태로 받을수도 있으며,
json 형태로 받는것이 향후 데이터 처리에 훨씬 수월합니다.

이는 .../api/search. 뒤의 xml을 json으로만 바꾸어주면 됩니다.
즉 주소가 아래와 같이 바뀌면 됩니다.


http://dart.fss.or.kr/api/search.json?auth=APIKEY&crp_cd=005930&start_dt=19990101&bsn_tp=A001


이제 본격적으로 R에서 작업을 해보도록 하겠습니다.



library(httr)
library(rvest)
library(jsonlite)

api.key = 'Your API KEY'
start.date = '19990101'
ticker = '005930'
url = paste0("http://dart.fss.or.kr/api/search.json?auth=",
             api.key,"&crp_cd=",ticker,"&start_dt=",start.date,
             "&bsn_tp=A001")

먼저 크롤링에 필요한 패키지들을 불러오도록 합니다.
그 후 여러분이 부여받은 API Key,
시작시점 일자, 예제로 쓰일 삼성전자의 6자리 티커를 입력합니다.

그 후 위에서 봤던 양식에 맞춰 url을 생성해주도록 합니다.

url
[1] "http://dart.fss.or.kr/api/search.json?auth=api.key&crp_cd=005930&start_dt=19990101&bsn_tp=A001"


이렇게 생성된 url을 웹페이지 창에 검색해도
우리가 원하는 공시정보를 응답 받았음이 확인됩니다.





data = fromJSON(url)
data.df = data$list
data.df.rcp = data.df$rcp_no


그 후 fromJSON() 함수를 사용하면 json 형태의 데이터를 읽어오게 됩니다.
우리가 원하는 데이터는 list에 있으므로 
해당 정보만 뽑아 data.df에 저장하도록 합니다.


data.df %>% head()
  crp_cls   crp_nm crp_cd               rpt_nm         rcp_no   flr_nm   rcp_dt rmk
1       Y 삼성전자 005930 사업보고서 (2017.12) 20180402005019 삼성전자 20180402  
2       Y 삼성전자 005930 사업보고서 (2016.12) 20170331004518 삼성전자 20170331  
3       Y 삼성전자 005930 사업보고서 (2015.12) 20160330003536 삼성전자 20160330  
4       Y 삼성전자 005930 사업보고서 (2014.12) 20150331002915 삼성전자 20150331  
5       Y 삼성전자 005930 사업보고서 (2013.12) 20140331002427 삼성전자 20140331  
6       Y 삼성전자 005930 사업보고서 (2012.12) 20130401003031 삼성전자 20130401  


이 중 우리가 가장 필요한 값은 공시번호에 해당하는
rcp_no 값 입니다.

따라서 해당 열만 data.df.rcp 변수에 저장해주도록 합니다.


이제 다시 dart 홈페이지로 돌아가 각종 공시들의 url이
어떻게 생겼는지 파악을 할 필요가 있습니다.





삼성전자의 정기공시를 검색해보면
17년 사업보고서가 나오게 됩니다.




이를 클릭해보면 url 주소가

http://dart.fss.or.kr/dsaf001/main.do?rcpNo=20180402005019

로 나타나게 됩니다.

즉, 가장 사업보고서의 url은

앞부분의 내용들, 그리고 마지막 쿼리에
위에서 구한 rcp_no를 입력하면 됩니다.


이번엔 첨부되어 있는 재무제표 파일들을 다운로드 받기 위한
각 공시별 dcm을 뽑아보도록 하겠습니다.
(dcm의 역할은 뒤에서 설명합니다.)






먼저 dcm을 뽑아내기 위해 
다운로드 그림에 해당하는 부분의 xpath를
개발자도구를 이용하여 추출합니다.

//*[@id="north"]/div[2]/ul/li[1]/a


해당 부분의 값을 R에서 추출하는 법은 다음과 같습니다.


url.business.report = 'http://dart.fss.or.kr/dsaf001/main.do?rcpNo=20180402005019'
  
req = GET(url.business.report) 
req = read_html(req) %>% html_node(xpath = '//*[@id="north"]/div[2]/ul/li[1]/a')


먼저 위 페이지 url 정보를 GET() 함수를 통해 가져오며,
read_html()을 통해 html 정보를 읽을 후,
xpath 값으로 해당 노드의 정보를 읽어오도록 합니다.


req
{xml_node}
"#download"
onclick="openPdfDownload('20180402005019', '6060273'); return false;"> [1] "/images/common/viewer_down.gif" style="cursor:pointer;" alt="다운로드" title="다운로드">

req 변수를 확인해보면 onclick 뒤의 openPdfDownload 부분이 있으며
앞의 20180402005019 부분은 위에서 나온 rcp_no,
위의 6060273 부분이 해당 파일의 dcm에 해당합니다.

즉, dcm은 각 공시에 해당하는 문서번호로 이해하시면 됩니다.


req = req %>% html_attr('onclick')
dcm = stringr::str_split(req, ' ')[[1]][2] %>% readr::parse_number()


6060273 부분만 뽑아내기 위해 약간의 노가다가 필요합니다.
먼저 html_attr() 함수를 이용해 'onclick' 부분의 데이터만 뽑아내며,
stringr 패키지의 str_split() 함수를 통해 캐릭터 값을 나눠준 후
두번째 값만을 뽑아내기,

마지막으로 readr 패키지의 parse_number() 함수를 통해
해당 값에서 숫자 값만을 뽑아주도록 합니다.

물론 정규표현식을 아신다면 해당작업이 
훨씬 쉽게 이루어지기도 합니다.



이번에는 다시 홈페이지로 돌아가
첨부된 엑셀 파일이 다운로드 받는 과정을 살펴보도록 합니다.




먼저 상단의 다운로드 부분을 클릭한 후
팝업창이 뜨면, 다시 개발자 도구를 열어줍니다.

그 후, 재무제표 항목을 클릭하면 파일이 다운로드 되면서
네트워크 부분에 다운로드 과정이 보여줍니다.

http://dart.fss.or.kr/pdf/download/excel.do

위의 url에 데이터를 요청하며
쿼리에 해당하는 부분은

rcp_no는 위에서 구한 값(공시번호),
dcm_no 역시 위에서 찾아낸 문서번호,
lang은 한국어인 ko

가 있습니다.


이를 R에서 post 형식으로 나타내면 다음과 같습니다.


query.base = list(
    rcp_no = '20180402005019',
    dcm_no = dcm
  )
  
down.excel = POST('http://dart.fss.or.kr/pdf/download/excel.do',
                    query = query.base)

down.excel
Response [http://dart.fss.or.kr/pdf/download/excel.do?rcp_no=20180402005019&dcm_no=5026126]
  Date: 2019-02-18 14:08
  Status: 200
  Content-Type: application/vnd.ms-excel
  Size: 74.8 kB


down.excel 변수를 확인해보면 엑셀 파일이 연결되어 있음이 확인됩니다.


writeBin(content(down.excel, "raw"), 
           paste0(ticker, "_", data.df.rcp[i], '.xls'))

writeBin() 함수를 통해 해당 파일을 다운로드 받으며
저장 이름은 티커_rcp.xls 로 하도록 합니다.




모든 재무제표 항목이 포함된 엑셀파일이
잘 다운로드 됨이 확인됩니다.


df = readxl::read_excel( paste0(ticker, "_", data.df.rcp[i], '.xls'), sheet = 2)
df
# A tibble: 62 x 4
   `연결 재무상태표`        ..2       ..3       ..4      
   <chr>                    <chr>     <chr>     <chr>    
 1  49  2017.12.31 현재 NA        NA        NA       
 2  48  2016.12.31 현재 NA        NA        NA       
 3  47  2015.12.31 현재 NA        NA        NA       
 4 (단위 : 백만원)          NA        NA        NA       
 5 NA                        49    48    47  
 6 자산                     NA        NA        NA       
 7 유동자산                 146982464 141429704 124814725
 8 현금및현금성자산         30545130  32111442  22636744 
 9 단기금융상품             49447696  52432411  44228800 
10 단기매도가능금융자산     3191375   3638460   4627530  
# ... with 52 more rows


read_excel() 함수를 통해 다운로드 받은 파일을
읽어올 수도 있습니다.


처음 API를 통한 json 형태로 얻은 10개의 rcp를 통해
최근 10년치 엑셀 파일을 모두 다운로드 받는 방법은
for loop를 통해 해결할 수 있습니다.


for (i in 1 : length(data.df.rcp)) {

  url.business.report = paste0('http://dart.fss.or.kr/dsaf001/main.do?rcpNo=',data.df.rcp[i])
  
  req = GET(url.business.report) 
  req = read_html(req) %>% html_node(xpath = '//*[@id="north"]/div[2]/ul/li[1]/a')
  req = req %>% html_attr('onclick')
  dcm = stringr::str_split(req, ' ')[[1]][2] %>% readr::parse_number()
  
  query.base = list(
    rcp_no = '20180402005019',
    dcm_no = dcm,
    lang = 'ko'
  )
  
  down.excel = POST('http://dart.fss.or.kr/pdf/download/excel.do',
                    query = query.base)
  
  writeBin(content(down.excel, "raw"), 
           paste0(ticker, "_", data.df.rcp[i], '.xls'))
  
  Sys.sleep(2)
  
  print(i)
  
}



10년치 재무제표 전 항목 엑셀파일이
잘 저장되어 있습니다.




* 해당 엑셀을 읽어온 후 클랜징 작업을 거쳐
나만의 DB를 만들 수도 있습니다.

* 시작시점과 끝시점을 바꾸면 2000년 이후
모든 데이터 다운로드도 가능합니다.

*  API의 티커만 바꾸면 전종목 데이터 다운로드도 가능합니다.

* API 검색 쿼리만 바꾸면 다른 항목 크롤링도 가능합니다.

댓글 11개:

  1. dcm 뽑는 부분 정규표현식으로는

    dcm = stringr::str_extract_all(req, "[0-9]+", simplify = TRUE)[2]

    답글삭제
  2. 안녕하세요. 순서대로 진행하다가 data.df %>% head()에서 list()만 나오고 값이 안나오는데 어떤 오류가 있었을지 문의 드립니다. 감사합니다.

    답글삭제
    답글
    1. fromJSON으로 데이터 제대로 긁어 와졌나요?

      삭제
    2. 제대로 안 긁어 진거 같습니다. 긁었다는 메시지가 따로 있는지요? 그냥 다음 으로 넘어가 버리네요

      삭제
  3. 다음과 같습니다.
    > library(httr)
    > library(rvest)
    > library(jsonlite)
    > api.key = '저의 인증키'
    > start.date = '19990101'
    > ticker = '005930'
    > url = paste0("http://dart.fss.or.kr/api/search.json?auth=api.key&crp_cd=005930&start_dt=19990101&bsn_tp=A001")
    > data = fromJSON(url)
    > data.df = data$list
    > data.df.rcp = data.df$rdcp_no
    > data.df %>% head()
    list()

    감사합니다.

    답글삭제
    답글
    1. 컬럼 찾는 코드에 오타 입력하셨네요

      data.df.rcp = data.df$rdcp_no 가 아니라
      data.df.rcp = data.df$rcp_no 해야합니다.

      'd'가 중간에 들어갔습니다.

      삭제
  4. 좋은자료 감사합니다!!

    for loop 내에서, query.base 설정 하실때 오타가 있는듯 합니다!!

    현재 rcp_no가 고정되있어서 한년도만 계속해서 저장이 되고 있네요!
    rcp_no = data.df.rcp[i] 로만 바꾸시면 될 것 같아요 !!!

    답글삭제
  5. R 공부 중에 좋은 자료 감사합니다. 윗 댓글분이 남겨주시긴 했는데 여전히 수정되지 않은거 같아 댓글남깁니다.
    10년치 다운받는 과정에서 rcp_no가 2018년꺼로 고정되어 있습니다. 이 코드대로 다운 받으면, 모든 재무제표 파일이 2018년꺼로 생성됩니다.

    답글삭제
  6. BS와 IS 자료를 API로 읽어오는 작이 필요했는데 매우 유익하네요. 설명하신 대로 한번 시도해보겠습니다.

    답글삭제
  7. 혹시 dart에서 주소가 바뀌었는지
    library(httr)
    library(rvest)
    library(jsonlite)

    api.key = '본인 API'
    start.date = '19990101'
    ticker = '005930'
    url = paste0("http://opendart.fss.or.kr/api/search.json?auth=",
    api.key,"&crp_cd=",ticker,"&start_dt=",start.date,
    "&bsn_tp=A001")

    data = fromJSON(url)
    data.df = data$list
    data.df.rcp = data.df$rcp_no
    data.df %>% head()
    list()

    입력하고 실행해도 진행되지 않습니다.. 혹시 방법이 있을까요?

    답글삭제