Post List

2019년 1월 25일 금요일

R을 이용한 데이터로 투자하기 - (1) 거래소 데이터 크롤링



헤들리 위컴에 따르면 데이터과학은 다음과 같은 프로세스를 거칩니다.


data science hadley에 대한 이미지 검색결과


Import: 데이터를 불러오는 과정
Tidy: 데이터를 정리하는 과정
Transform: 데이터를 변형하는 과정
Visualize: 데이터를 시각화 하는 과정
Model: 데이터를 통해 모델을 짜는 과정
Communicate: 결과물을 출력 및 해석과정


이를 투자에 적용하면
Import는 금융데이터를 수집하여 불러오는 과정입니다.

물론 데이터가이드 혹은 블룸버그처럼
유료 데이터벤더를 사용할 경우 데이터를 얻는 것이 문제가 아니지만,
그렇지 않을 경우 우리는
'크롤링을'통해 데이터를 구해야 합니다.

HenryQuant 패키지의 get_KOR_price() 및 get_KOR_fs()
함수를 통해 데이터를 구할 수 있는 방법을 제공하고 있으며,

이번 글에서는 기본적인 크롤링 예제로서
거래소의 데이터를 가져오도록 하겠습니다.

사용할 데이터는 산업분류(20004)와 개별종목 지표(30009)입니다.

산업분류: http://marketdata.krx.co.kr/mdi#document=03030102
개별지표: http://marketdata.krx.co.kr/mdi#document=13020401




물론 크롤링이 아니라 Excel 버튼을 눌러 엑셀을 받은 후,
R에서 해당 파일을 불러올 수도 있습니다.


library(readxl)
data_sector = read_excel('data.xls')
data_value = read_excel('data (1).xls')

head(data_sector)
head(data_value)

> head(data_sector)
# A tibble: 6 x 6
  시장구분 산업분류 종목수 종목비중 시가총액            시가총액비중
  <chr>    <chr>    <chr>  <chr>    <chr>               <chr>       
1 코스피   어업     4      0.44     1,318,318,152,850   0.09        
2 코스피   광업     1      0.11     145,864,936,080     0.01        
3 코스피   음식료품 48     5.33     27,173,363,143,267  1.92        
4 코스피   섬유의복 27     3.00     5,552,708,739,310   0.39        
5 코스피   종이목재 22     2.44     3,257,749,729,490   0.23        
6 코스피   화학     112    12.43    134,206,773,522,390 9.48        
> head(data_value)
# A tibble: 6 x 11
  일자       종목코드 종목명   관리여부 종가   EPS   PER   BPS   PBR   주당배당금 배당수익률
  <chr>      <chr>    <chr>    <chr>    <chr>  <chr> <chr> <chr> <chr> <chr>      <chr>     
1 2019/01/25 000250   삼천당제약~ -        37,300 704   52.98 6,620 5.63  50         0.13      
2 2019/01/25 000440   중앙에너비스~ -        7,700  344   22.38 7,258 1.06  176        2.29      
3 2019/01/25 001000   신라섬유 -        2,400  17    141.~ 580   4.14  0          0         
4 2019/01/25 001540   안국약품 -        10,500 712   14.75 10,5~ 1     220        2.1       
5 2019/01/25 001810   무림SP   -        2,670  472   5.66  8,738 0.31  35         1.31      
6 2019/01/25 001840   이화공영 -        8,460  83    101.~ 2,146 3.94  40         0.47  


그러나 매번 엑셀을 다운받고 불러오는 작업은 상당히 비효율적이므로,
크롤링을 통해 해당 데이터를 불러오는 작업을 알아보도록 하겠습니다.

먼저 산업분류 데이터를 받아오는 법을 알아보도록 하며,
기준일은 2018년 12월 28일 데이터로 하도록 합니다.




해당화면에 접속, 조회일자를 20181228로 수정하여 조회를 누른 후,
F12를 눌러 개발자도구 화면을 연 상태에서 Excel을 누릅니다.

거래소에서 Excel 데이터를 받는 과정은 다음과 같습니다.

http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx 사이트에
원하는 항목을 쿼리로 발송하면 
해당 쿼리에 해당하는 OTP를 받게 됩니다.

부여받은 OTP를 http://file.krx.co.kr/download.jspx 에 제출하면
이에 해당하는 데이터를 다운로드 받게 됩니다.

첫번째 과정이 GenerateOTP.jspx?.... 항목에,
두번째 과정이 download.jspx 항목에 나와있습니다.

이러한 크롤링 과정을 R에서 실행하는 방법은 다음과 같습니다.


library(httr)
library(rvest)
library(readr)
library(readxl)

date = '20181228'

# 산업별 현황 다운로드
gen_otp_url = "http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx"
gen_otp_data = list(name = "fileDown", filetype = "csv",
                    url = "MKD/03/0303/03030103/mkd03030103", tp_cd = 'ALL',
                    date = date, lang = 'ko',
                    pagePath = "/contents/MKD/03/0303/03030103/MKD03030103.jsp")
otp = POST(gen_otp_url, query = gen_otp_data) %>%
  read_html() %>% html_text()


먼저 필요한 패키지들을 연 후, date20181228로 고정해 줍니다.
gen_otp_url 변수에는 otp를 요청할 주소를
gen_otp_data에는 쿼리 내용을 입력하며,
이는 GenerateOTP.jspx?의 Query String Parameter 부분을
입력해주면 됩니다.

단, filetype 부분은 엑셀 즉 xls이 아닌
csv로 입력해 줍니다.

이는 엑셀 형식으로 파일 다운로드 및 불러오기보다
csv 형식을 이용하는 것이 훨씬 간편하기 때문입니다.


그 후 POST 방식으로 url에 쿼리를 전송하면
이에 해당하는 데이터를 받게 되며,
read_html() 및 html_text()를 이용하여 텍스트 부분만 추출합니다.


이제 생성된 OTP를 쿼리 형식으로
http://file.krx.co.kr/download.jspx에 제출하면 됩니다.

down = POST(down_url, query = down_data,
            add_headers(referer = gen_otp_url)) %>%
  read_html() %>% html_text() %>% read_csv()

POST형식으로 해당 url에 OTP를 쏘면 됩니다.

과거에는 해당 방식으로도 데이터가 다운로드 되었지만,
거래소의 데이터 시스템이 바뀌어
add_headers() 구문을 통해 referer를 추가해 주어야 합니다.

리퍼러란 하이퍼링크를 통해서 각각의 사이트로 방문시 남는 흔적입니다.

위의 과정은 http://marketdata.krx.co.kr에서 OTP를 받고
이를 다시 http://file.krx.co.kr/download.jspx에 제출하는 플로우였습니다.

그런데 이러한 과정의 흔적이 없이
OTP를 바로 http://file.krx.co.kr/download.jspx에 제출하면
서버는 이를 로봇으로 인식하여 데이터를 반환하지 않습니다.

따라서 add_headers()를 통해 우리가 거쳐온 과정을 흔적으로 남겨야
사람이 데이터를 다운로드 받는 과정과 동일하게 인식하여
데이터를 반환하게 됩니다.

그 후 read_html()html_text() 함수를 통해 데이터를 추출하고
read_csv() 함수를 통해 csv 형식으로 파일을 읽어오도록 합니다.


data_sector = down[, c('시장구분', '종목코드', '종목명',
                              '산업분류','시가총액(원)')]
write.csv(data_sector, 'data_sector.csv')

이 중 필요한 부분인
시장구분, 종목코드, 종목명, 산업분류, 시가총액(원) 부분을 선택하여 
data_sector 변수 및 csv 파일로 저장합니다.


> data_sector
# A tibble: 2,227 x 5
   시장구분 종목코드 종목명         산업분류 `시가총액(원)`
   <chr>    <chr>    <chr>          <chr>             
 1 코스피   030720   동원수산       어업        41884245000
 2 코스피   007160   사조산업       어업       249250000000
 3 코스피   006040   동원산업       어업       682729600000
 4 코스피   004970   신라교역       어업       206400000000
 5 코스피   003580   넥스트사이언스 광업       137311128100
 6 코스피   280360   롯데제과       음식료품   635574402000
 7 코스피   271560   오리온         음식료품  4744075320000
 8 코스피   006090   사조오양       음식료품    91683250470
 9 코스피   26490K   크라운제과우   음식료품     5879056000
10 코스피   264900   크라운제과     음식료품   116471805000
# ... with 2,217 more rows


data_sector를 확인해보면 사이트의 내용과 동일함이 확인됩니다.
위의 과정에서 date부분만 원하는 날짜로 수정하면,
해당일의 데이터가 다운로드 되기도 합니다.






이번에는 개별종목의 가치지표를 다운로드 받도록 하며,
방법은 위의 산업분류와 거의 동일합니다.




단 isu_cdnm, isu_cd, isu_nm, isu_srt_cd,
schdate, sfromdate, todate 부분은
개별 종목의 가치지표에 해당하는 부분이므로
쿼리에 입력하지 않아야 합니다.



gen_otp_data = list(name = "fileDown", filetype = "csv",
                    url = "MKD/13/1302/13020401/mkd13020401", market_gubun = 'ALL',
                    gubun = '1', schdate = date,
                    pagePath = "/contents/MKD/13/1302/13020401/MKD13020401.jsp")
otp = POST(gen_otp_url, query = gen_otp_data) %>%
  read_html() %>% html_text()
down_data = list(code = otp)
down = POST(down_url, query = down_data,
            add_headers(referer = gen_otp_url)) %>%
  read_html() %>% html_text() %>% read_csv()

data_value = down[, c('종목코드', 'PER', 'PBR', '배당수익률')]

write.csv(data_value, 'data_value.csv')


이를 코드로 나타내면 위와 같습니다.
쿼리 부분만 주의하면 기존의 코드와 거의 동일하며,

다운받은 데이터 중 종목코드, PER, PBR, 배당수익률 부분만을 선택하여
down_value 변수 및 csv 파일에 저장하도록 합니다.

> data_value
# A tibble: 2,187 x 4
   종목코드 PER    PBR   배당수익률
   <chr>    <chr>  <chr>      
 1 016670   34.29  1.01        0   
 2 043090   319.29 6.48        0   
 3 044180   -      0.97        0   
 4 051160   -      6.52        0   
 5 068240   55.99  2.23        0.31
 6 114120   -      0.41        0   
 7 121800   2.33   1.21        0   
 8 206650   -      8.09        0   
 9 222420   33.11  2.03        0   
10 226350   -      1.09        0   
# ... with 2,177 more rows


한국거래소에서 산업분류 및 개별종목의 밸류지표를 
크롤링하는 방법에 대해 알아보았습니다.

다음에는 다운로드받은 데이터를 바탕으로
tidy(정리하기) 및 transform(변형하기) 하는 단계에 대해
알아보도록 하겠습니다.

댓글 7개:

  1. 덕분에 난생 첫 크롤링 무사히 성공했습니다! 감사합니다 ㅎㅎ

    답글삭제
  2. 안녕하세요..^^.. R을 통한 웹크롤링은 처음인데요. 위 작성된 코드를 따라서 해보는중 아래와 같은 에러메세지를 만났습니다.
    Error in handle_url(handle, url, ...) : object 'down_url' not found

    아래내용을 실행하다가 "down_url"에 대한 정의가 없어서 발생한 에러라고 생각이되는데요...정확히 어떤것이 문제일까요?
    down = POST(down_url, query = down_data,
    add_headers(referer = gen_otp_url)) %>%
    read_html() %>% html_text() %>% read_csv()

    답글삭제
    답글
    1. 아..해결했습니다. down_url 대신에.."http://file.krx.co.kr/download.jspx" 이것을 대신해서 넣었더니 해결되었네요..휴..

      삭제
  3. 위 코드를 활용해서 개별종목 시세를 가져오려고 시도를 해봤는데요. 아래와 같이 에러가 발생하네요
    어떤점이 잘못됐는지 혹시 짚어줄수 있으신지요? ^^;;

    Error: '년/월/일,종가,대비,거래량(주),거래대금(원),시가,고가,저가,시가총액(백만),상장주식수(주)' does not exist in current working directory

    변경한 코드는 아래와 같습니다.

    #개별종목 시세추이
    gen_otp_url = "http://marketdata.krx.co.kr/contents/COM/GenerateOTP.jspx"
    gen_otp_data = list(name = "fileDown", filetype = "csv",
    url = "MKD/13/1302/13020103/mkd13020103_02",
    isu_cdnm = 'A005930/삼성전자',
    isu_cd = 'KR700593003',
    isu_nm = '삼성전자',
    isu_srt_cd = 'A005930',
    fromdate = '20160215',
    todate = '20190222',
    pagePath = "/contents/MKD/13/1302/13020103/MKD13020103.jsp")
    otp = POST(gen_otp_url, query = gen_otp_data) %>%
    read_html() %>% html_text()
    down_data = list(code = otp)
    down = POST("http://file.krx.co.kr/download.jspx", query = down_data, ## <-- 이 줄에서 에러가 뜹니다.
    add_headers(referer = gen_otp_url)) %>% read_html() %>% html_text() %>% read_csv()

    data_price = down[, c('년/월/일', '종가')]
    write.csv(data_price, 'data_price.csv')

    답글삭제
    답글
    1. gen_otp_data = list(name = "fileDown", filetype = "csv",
      url = "MKD/04/0402/04020100/mkd04020100t3_02", #
      isu_cdnm = 'A005930/삼성전자',
      isu_cd = 'KR7005930003',
      isu_nm = '삼성전자',
      isu_srt_cd = 'A005930',
      fromdate = '20160215',
      todate = '20190222',
      pagePath = "/contents/MKD/04/0402/04020100/MKD04020100T3T2.jsp") #


      OTP에서 샵 쳐놓은 url, isu_cdnm, pagePath를 잘못 입력하셔서
      잘못된 OTP를 생성받는 바람에 데이터를 못받은 것 같습니다.

      쿼리값들은 개발자화면에서 더블클릭으로 복사한후 붙여넣기해야
      타이핑 에러가 없습니다.

      삭제
  4. 안녕하세요 산업분류 데이터를 받아올때 위에 사진에서는 보내는 쿼리내용중 url이 "MKD/03/0303/03030102/mkd03030102" 로 나와있는데 코드에 왜 "MKD/03/0303/03030103/mkd03030103"를 쓰는지 잘 모르겠습니다. "MKD/03/0303/03030102/mkd03030102"로 작성을 하면 Error: '시장구분,산업분류,종목수,종목비중,시가총액,시가총액비중' does not exist in current working directory ('C:/Users/user/Documents'). 라는 에러가 뜨네요...

    답글삭제