행위

주식 재무제표 크롤링

DB CAFE

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

  1. 금융감독원의 전자공시 시스템(http://dart.fss.or.kr/)
  2. 오픈 API 인증키 신청 필요 , API KEY 발급
  3. 하루 동안 요청할 수 있는 횟수 10,000 번
  4. 한번에 최대 10개까지의 데이터 받을수 있음.
  5. 아무런 쿼리도 입력하지 않으면 단순히 최신 공시 10건을 출력


1.1 자기주식 취득공시 크롤링

  1. 해당 주소 http://dart.fss.or.kr/api/search.xml?auth=APIKEY&crp_cd=005930&start_dt=19990101&bsn_tp=A001
  2.  :(A002은 반기, A003은 분기)
  3. auth 는 발급받은 API key 입력
  4. corp_code 는 회사코드 다운로드(https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key=API키)
  5. 데이터를 xml,json 형태( .../api/search. 뒤의 xml을 json으로만 바꾸어주면 됩니다.)
library(httr)
library(rvest)
library(jsonlite)
library(XML)
library(methods)
library(RMySQL)

corpCode <- xmlToDataFrame("D:/dev/R/corpCode.xml")
data.df = corpCode$list


api.key = "OPEN API 키값"
#start.date = "19990101"
list_bsns_year <- c('2015', '2016', '2017', '2018', '2019','2020')  # 
list_reprt_code <- c('11011', '11012', '11013', '11014') # 분기 리포트

# # 
# # if (mysqlHasDefault()) {
#   # DB Connection 
con <- dbConnect(MySQL(),
                 host = '주소',
                 dbname = 'db명',
                 user = '유저명',
                 password='비번')

dbSendQuery(con, "SET NAMES utf8;")
dbSendQuery(con, "SET CHARACTER SET utf8;")
dbSendQuery(con, "SET character_set_connection=utf8;")
  
#   
#   # connection info & table schma info
#   dbListTables(con)
#   dbListFields(con, "tb_self_meme_company")
# # }
# 
meme_comp.table <- dbGetQuery(con, "select '한글테스트',a.* from tb_self_meme_company2 a") # Query Test 2
head(meme_comp.table)
# class(df.table)


# Company List 
corpCode <- xmlToDataFrame("D:/dev/R/corpCode.xml")
compList.df = corpCode$list
#start.date = "19990101"
corp_code = "00385363"

for(i in list_bsns_year){
  for(j in list_reprt_code){
    url = paste0("https://opendart.fss.or.kr/api/tesstkAcqsDspsSttus.json?crtfc_key=",api.key,"&corp_code=",corp_code,"&bsns_year=",i,"&reprt_code=",j)
    # print(url)
    data = fromJSON(url)
    # data.df = data$list
    alldata <- rbind(alldata,data$list)    
    # data.df.rcept_no = data.df$rcept_no
    # data.df %>% head()
  }
}

alldata %>% head()
alldata %>% tail()

1.2 공시번호로 사업보고서 확인

  1. 필요한 값은 공시번호에 해당하는 rcp_no 값 (data.df$rcept_no) 만 data.df.rcp 변수에 저장
  2. dart 홈페이지 공시들의 url 파악 필요
  3. 정기공시를 검색해보면 00 년 사업보고서 예시
    url 주소
    http://dart.fss.or.kr/dsaf001/main.do?rcpNo=20160330000776

1.3 재무제표 파일 다운로드

  1. 첨부되어 있는 재무제표 파일들을 다운로드 받기 위한 각 공시별 dcm 구하기
  2. dcm을 뽑아내기 위해 크롬 > 개발자도구 > xpath를 이용하여 추출
    //*[@id="north"]/div[2]/ul/li[1]/a
  3. 해당 부분의 값을 R에서 추출하는 법
# 위 페이지 url 정보를 GET() 함수를 통해 가져오며,read_html()을 통해 html 정보를 읽을 후,xpath 값으로 해당 노드의 정보를 읽어오도록 합니다.
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')

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()
  1. html_attr() 함수를 이용해 'onclick' 부분의 데이터만 뽑아
  2. stringr 패키지의 str_split() 함수를 통해 캐릭터 값을 나눠준 후 두번째 값만을 뽑아내기
  3. 마지막으로 readr 패키지의 parse_number() 함수를 통해 해당 값에서 숫자 값만을 뽑아주도록 합니다.
  4. 정규표현식을 아신다면 해당작업이 훨씬 쉽게 가능
1.3.1 홈페이지 첨부된 엑셀 파일이 다운로드
  1. 상단의 다운로드 부분을 클릭한 후 팝업창이 뜨면, 다시 개발자 도구를 열어줍니다.
  2. 재무제표 항목을 클릭하면 파일이 다운로드

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를 통해 해결할 수 있습니다.

<source lang=r>
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)  
}



2 네이버 증권 재무제표

https://engkimbs.tistory.com/625

  • 파이썬 코드
import requests
from bs4 import BeautifulSoup

URL = "https://finance.naver.com/item/main.nhn?code=005930"

samsung_electronic = requests.get(URL)
html = samsung_electronic.text

soup = BeautifulSoup(html, 'html.parser')

finance_html = soup.select('div.section.cop_analysis div.sub_section')[0]

th_data = [item.get_text().strip() for item in finance_html.select('thead th')]
annual_date = th_data[3:7]
quarter_date = th_data[7:13]

finance_index = [item.get_text().strip() for item in finance_html.select('th.h_th2')][3:]

finance_data = [item.get_text().strip() for item in finance_html.select('td')]

import numpy as np

finance_data = np.array(finance_data)
finance_data.resize(len(finance_index), 10)

finance_date = annual_date + quarter_date

import pandas as pd
finance = pd.DataFrame(data=finance_data[0:,0:], index=finance_index, columns=finance_date)

annual_finance = finance.iloc[:, :4]
quarter_finance = finance.iloc[:, 4:]

3 네이버 증권 재무제표 WITH 판다스

import requests
from bs4 import BeautifulSoup
import os
import datetime as dt
import pandas as pd
# user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36'
session = requests.Session()
# session.headers.update( {'User-agent': user_agent, 'referer': None})

# url_test = 'https://comp.fnguide.com/SVO2/asp/SVD_Finance.asp?pGB=1&gicode=A005380&cID=&MenuYn=Y&ReportGB=D&NewMenuID=103&stkGb=701'
url_test = 'https://m.stock.naver.com/api/html/item/financialInfo.nhn?type=annual&code=284740'
r = session.get(url_test)
# r.encoding='utf-8'
# r.text
# print(r.text)
data = pd.read_html(r.text)
# print(data)
# print(type(data))
# print(len(data))
print(data[0])
KRX = pd.read_excel('D:/DEV/R/all_company.xlsx')

KRX = KRX[['종목코드', '종목명']]
# KRX['종목코드'] = KRX['종목코드'].apply(lambda x: '{0:0>6}'.format(x))
# print(KRX)

code = KRX['종목코드']
code_list = []
for cd in code.values:
    cd = str(cd)
    cd = ('0'*(6-len(cd)))+cd
    code_list.append(cd)

# print(code_list)
for z, code in enumerate(code_list[:int(len(code_list) * 0.3)]):
    try:
        url_test = 'https://m.stock.naver.com/api/html/item/financialInfo.nhn?type=annual&code=%s' % code
        r = session.get(url_test)
        # r.encoding = 'utf-8'

        data = pd.read_html(r.text)

        IS_temp = data[0]
        # print(IS_temp)
        # IS_temp.index = IS_temp['기간'].values
        # IS_temp.drop(['IFRS(연결)', '전년동기', '전년동기(%)'], inplace=True, axis=1)

        # for i, name in enumerate(IS_temp.index):
        #     if '참여한' in name:
        #         name = name.strip()
        #         name = name.replace('계산에 참여한 계정 펼치기', '')
        #         name = name.replace(' ', '')
        #         IS_temp.rename(index={str(IS_temp.index[i]): str(name)}, inplace=True)

        # IS_temp = IS_temp.T
        # IS_temp = IS_temp.reset_index()
        IS_temp.rename(columns={'index': 'date'}, inplace=True)
        IS_temp.insert(0, '종목코드', code)
        print(IS_temp[3])
        if z == 0:
            IS_data = IS_temp
        else:
            IS_data = pd.concat([IS_data, IS_temp])
    except KeyError as e:
        print(e, code)
    except ValueError as e:
        print(e, code)

# print(IS_data)
# IS_data.to_excel('D:/DEV/R/export_sample.xlsx', sheet_name='new_name')
# IS_temp = data[0]4
# IS_temp.index =IS_temp['IFRS(연결)'].values
# IS_temp.drop(['IFRS(연결)', '전년동기', '전년동기(%)'], inplace=True, axis=1)

# for i, name in enumerate(IS_temp.index):
#     # 저 글씨가 껴있는 애들만 걸리도록 설정을 해서
#     if '참여한' in name:
#         name = name.strip().replace('계산에 참여한 계정 펼치기', '')
#         # 공백이 있는지 모르곘는데, 그냥 지우고, 우리가 지우고자 하는 글씨도 지워버린다음에
#         name = name.replace(' ', '')
#         # 빈ㅌ칸이 있을 수 있으니, 빈칸들도 날려버립시다.
#         IS_temp.rename(index = {str(IS_temp.index[i]): str(name)}, inplace = True)
#         # rename으로 index를 다시 설정해주면 됩니다.
# code = 'A005380'
# IS_temp.insert(0, '종목코드', code)
# print(IS_temp)