Post List

2018년 11월 8일 목요일

get_KOR_ticker() 함수 리뉴얼 (네이버 금융에서 전종목 티커 크롤링하기)






네이버 금융에서 국내증시 → 시가총액을 들어가면
코스피와 코스닥의 시가총액별 정보가 나타나 있습니다.

또한 종목명에 마우스를 올려보면 아래에 나타나는 링크를 통해
끝 6자리가 각 종목의 거래소 티커임도 확인이 됩니다.



티커 정리를 위해 우리가 HTML에서 확인해야 할 부분은 총 2가지 입니다.

먼저, 링크에 해당하는 주소 중 끝 6자리 입니다.





크롬 개발자도구화면을 열어 해당 페이지의 구조를 확인해보면 해당 링크는
tbody  td  a 태그에서 href 속성에 위치하고 있음을 알수 있습니다.
rvest 패키지에서 태그는 html_nodes(), 속성은 html_attr() 함수를 통해
각각 발라낼 수 있겠죠


두번째는 하단의 페이지 네비게이션을 통해
코스피와 코스닥 시가총액에 해당하는 페이지가 각각
몇번째 페이지까지 존재하는지를 알아야 합니다.




이 중 맨뒤에 해당하는 페이지가 가장 마지막 페이지가 되겠죠



개발자 도구 화면을 살펴보면 '맨뒤' 에 해당하는 링크는
pgRR 클래스의 a태그 중 href 속성에 위치하며,
page= 뒤에 위치하는 31번째 페이지로 링크가 걸려있습니다.

즉, 코스피 종목은 총 31번째 페이지까지 시가총액 화면이 존재합니다.

동일한 방법으로 코스닥을 확인해보면
코스닥은 총 26번째 페이지까지 화면이 존재합니다.


해당 정보들을 이용하여 전체 내용을 크롤링 하는 코드를 짜보도록 합니다.



data = list()
 
i = 0
 
mkt = ifelse(i == 0, "KS", "KQ")
ticker = list()
url = paste0("https://finance.naver.com/sise/sise_market_sum.nhn?sosok=",i,"&page=1")
down_table = GET(url)

먼저 데이터가 들어간 빈리스트인 data 변수를 만들어 줍니다.
그 후 sosok= 뒤 코스피는 0, 코스닥은 1 이므로 
for loop를 통해 양쪽 시장의 데이터를 다운로드 받을 수 있습니다.

일단은 코스피에 해당하는 i=0을 예시로 살펴봅니다.

ifelse() 함수를 통해 i=0, 즉 코스피면 "KS" 아니면 코스닥인 "KQ"를 반환하여
시장 구분에 해당하는 mkt 변수에 저장해 줍니다.

또한 테이블이 들어간 빈 리스트인 ticker 변수를 만들어 줍니다.

그 후, paste0() 함수를 이용하여 코스피 시가총액 페이지의 url을 만들고
("https://finance.naver.com/sise/sise_market_sum.nhn?sosok=0&page=1")
GET() 함수를 통해 해당 페이지 내용을 받아, down_table 변수에 저장합니다.



down_table 변수를 확인해 봅니다.

Status가 200이니 데이터가 잘 들어와 있군요
인코딩은 EUC-KR 타입으로 되어 있습니다.
(네이버 크롤링 시 유의 사항이기도 합니다....)

가장 먼저 해야할 작업은 가장 마지막 페이지가
몇번째 페이지인지 찾아내는 작업입니다.

앞서 해당 데이터는
pgRR 클래스의 a태그 중 href 속성에 위치함을 확인했습니다.

해당 정보를 이용하여 데이터를 발라내보도록 하겠습니다.


navi.final = read_html(down_table, encoding = "EUC-KR") %>%
      html_nodes(., ".pgRR") %>%
      html_nodes(., "a") %>%
      html_attr(.,"href") %>%
      strsplit(., "=") %>%
      unlist() %>%
      tail(., 1) %>%
      as.numeric()

위 코드를 입력하면 마지막 페이지에 해당하는 '31'이라는 숫자가
navi.final 이라는 변수에 저장됩니다.

이에 대한 자세한 설명은 다음과 같습니다.



read_html(down_table, encoding = "EUC-KR") %>%
      html_nodes(., ".pgRR") %>%
      html_nodes(., "a") %>%
      html_attr(.,"href")

먼저 read_html() 함수를 이용하여 html 내용을 읽어오며,
인코딩은 "EUC-KR"로 셋팅해주도록 합니다.

그 후 html_nodes() 함수를 이용하여 "pgRR" 클래스 정보만을 불러오도록 하며,
클래스 태그의 경우 이름 앞에 .을 붙여 읽어올 수 있습니다.

 다시 html_nodes() 함수를 통해 a 태그 정보만을 불러오도록 하며,
마지막으로 html_attr() 함수를 통해 href 속성을 불러오도록 합니다.

위 코드를 입력하면 다음과 같은 내용이 나옵니다.


[1] "/sise/sise_market_sum.nhn?sosok=0&page=31"

우리는 이중 가장 마지막에 위치하는 '31' 이라는 숫자만을 때오고자 합니다.
현재 상황에는 strsplit() 함수가 가장 적합합니다.

strsplit(., "=")

strsplit() 함수는 전체 문장을 특정 글자 기준으로 나누는 것입니다.
page 뒷부분만을 때오기 위해 '='를 기준으로 문장을 나눠주도록 하며,
결과는 다음과 같습니다.

[[1]]
[1] "/sise/sise_market_sum.nhn?sosok" "0&page"                          "31"

총 3개의 그룹으로 분리가 되며, 우리는 이중 마지막 그룹을 원합니다.
또한 "31" 이라는 문자열의 형태를 숫자의 형태로 바꾸어야 합니다.

unlist() %>%
      tail(., 1) %>%
      as.numeric()
먼저 unlist() 함수를 통해 리스트의 형태를 풀고,
tail() 함수를 통해 마지막 첫번째 데이터만 선택합니다.
그 후 as.numeric() 함수를 통해 숫자 형태로 바꾸도록 합니다.

이제 남은 작업은 1페이지 부터 navi.final 즉 마지막 페이지까지
for loop 구문을 통해 모든 페이지의 내용을 긁어오는 일입니다.
이를 코드로 나타내면 다음과 같습니다.


for (j in 1:navi.final) {
 
      url = paste0("https://finance.naver.com/sise/sise_market_sum.nhn?sosok=",i,"&page=",j)
      down_table = GET(url)
 
      Sys.setlocale("LC_ALL", "English")
 
      table = read_html(down_table, encoding = "EUC-KR") %>% html_table(fill = TRUE)
      table = table[[2]]
 
      Sys.setlocale("LC_ALL", "Korean")
 
      table[, ncol(table)] = NULL
      table = na.omit(table)
 
      symbol = read_html(down_table, encoding = "EUC-KR") %>%
        html_nodes(., "tbody") %>%
        html_nodes(., "td") %>%
        html_nodes(., "a") %>%
        html_attr(., "href")
 
      symbol = sapply(symbol, function(x) {
        substr(x, nchar(x) - 5, nchar(x)) 
      }) %>% unique()
 
      table$N = symbol
      colnames(table)[1] = "종목코드"
 
      rownames(table) = NULL
      ticker[[j]] = table
 
      Sys.sleep(0.5)
 }

먼저 첫번째 페이지인 j=1의 예시를 통해 살펴보도록 하죠
i=0, j=1이므로 이를 paste0() 함수로 붙이면
다음과 같은 url이 생성됩니다.

[1] "https://finance.naver.com/sise/sise_market_sum.nhn?sosok=0&page=1"

먼저 GET() 함수를 통해 페이지 정보를 호출합니다.

한글로 되어있는 페이지가 뻑나는 현상을 막기 위해
Sys.setlocale() 함수를 통해 로케일 언어를 영어로 설정 해줍니다.

그 후 read_html() 함수를 통해 html 정보를 읽어오며,
html_table() 함수를 통해 테이블 정보만을 읽어옵니다.
셀 간 병합이 된 데이터가 존재하므로 fill=TRUE 를 추가해주기도 합니다.

이를 통해 table 변수에는 총 3가지 테이블이 불러와 집니다.




이 중 우리가 원하는 테이블은 2번째 리스트에 해당하는 내역입니다.
따라서 table = table[[2]] 코드를 통해
2번째 리스트 만을 table 변수에 저장하도록 합니다.

그 후 한글을 읽기 위해 Sys.setlocale() 함수를 통해
로케일 언어를 다시 Korean으로 변경해 줍니다.

저장된 table 내용을 확인하면 다음과 같습니다.




이 중 마지막 열인 '토론실'은 필요가 없으므로,
table[, ncol(table)] = NULL 코드를 통해 날려버리도록 합니다.

또한 홈페이지에서 가로로 긴 줄




에 해당하는 부분은 3개 행에 걸쳐 NA로 표시되게 됩니다.
na.omit() 함수를 통해 해당 행도 날려버릴 수 있습니다.

이렇게 정리된 테이블을 살펴보면 다음과 같습니다.




대충 깔끔하게 정리는 되었습니다.
그러나 우리에게 가장 필요한 6자리 티커가 아직까지 없습니다.

해당 데이터는 개발자 도구 화면을 통해
tbody  td  a 태그에서 href 속성에 위치하고 있음을 알았습니다.

이를 발라내는 코드가 다음과 같습니다.


symbol = read_html(down_table, encoding = "EUC-KR") %>%
        html_nodes(., "tbody") %>%
        html_nodes(., "td") %>%
        html_nodes(., "a") %>%
        html_attr(., "href")

먼저 read_html() 함수를 통해 html 정보를 읽어온 후,
html_nodes() 함수를 통해 'tbody' 태그 정보를,
그 후 다시 html_nodes() 함수를 통해 'td'와 'a' 태그를 발라냅니다.

마지막으로 html_attr() 함수를 이용하여 'href' 속성을 발라냅니다.

이를 통해 symbol에는 href 속성에 해당하는
링크 주소들이 저장되게 됩니다.




이 중 마지막 6자리 글자만 때오는 함수는 다음과 같습니다.

sapply(symbol, function(x) {
        substr(x, nchar(x) - 5, nchar(x)) 
      })

sapply() 함수를 통해 symbol 변수의 내용들에 function() 내용들을 적용하며,
substr() 함수 내에 nchar() 함수를 적용하여
마지막 6자리 글자만을 추출하도록 합니다.




결과를 살펴보면 티커에 해당하는 마지막 6글자만 추출된 것이 확인됩니다.
그러나 동일한 내용이 두번 추출되어 지며,

/item/main.nhn?code=005930 내용은 종목명 열에 해당하는 코드,
/item/board.nhn?code=005930 내용은 토론실에 해당하는 코드입니다.

unique() 함수를 통해 중복되는 내용을 제가하면 다음과 같습니다.





우리가 원하는 티커 부분만 깔끔하게 정리가 되었군요.

위의 symbol 내용을 table$N을 통해 'N' 열에 넣어주며,
열이름을 "종목코드"로 변경해 줍니다.

또한 앞서 na.omit() 함수를 통해 NA로 이루어진 행을 삭제하였으므로,
행이름이 "2"  "3"  "4"  "5"  "6"  "10" "11" "12" 와 같이
순서대로 이루어져 있지 않습니다.

rownames(table) = NULL 코드를 통해 행이름을 1,2,3,,,,, 순서로 초기화 해주며
최종적으로 정리된 데이터를 ticker 리스트의 j번째 열에 입력해 줍니다.

또한 Sys.sleep 코드를 통해 페이지마다 0.5초 정도의 슬립을 주도록 합니다.

해당 for loop구문이 모두 돌게 되면,
ticker 리스트에는 각각의 페이지에 해당하는 내용이
j번째 리스트에 들어가 있게 됩니다.




ticker = do.call(rbind, ticker)
ticker$market = mkt
 
data[[i + 1]] = ticker

do.call() 함수를 통해 해당 리스트를 하나의 데이터 프레임으로 만든 후,
market 열에 거래소에 해당하는 "KS" 혹은 "KQ" 문자를 입력해 줍니다.

그 결과는 다음과 같습니다.




i=0, 즉 코스피에 해당하는 정보들이 깔끔하게 정리되어 있습니다.
이를 i+1, 즉 data의 첫번째 리스트에 저장하도록 합니다.
(i=0 이므로 0번째 리스트에는 저장이 불가합니다.)


for loop 구문을 통해 코스피와 코스닥 데이터를 모두 내려받도록 합니다.


for (i in 0:1) {
 
    blah blah
 
    data[[i + 1]] = ticker
    Sys.sleep(2)
 
}
data 변수를 확인해보면
첫번째 리스트에는 코스피 데이터가,
두번째 리스트에는 코스닥 데이터가 저장되어 있습니다.





data = do.call(rbind, data)

이를 do.call() 함수로 묶어주게 되면,
모든 종목의 데이터가 하나의 데이터프레임으로 정리되게 됩니다.



마지막으로 특이 종목들에 대한 클렌징 작업이 필요합니다.

data = data[which(data$액면가 != "0"), ]
data = data[!grepl("스팩", data[, 2]), ] 
data = data[substr(data[,2], nchar(data[,2]), nchar(data[,2])) != "우", ] 
data = data[substr(data[,2], nchar(data[,2]) - 1, nchar(data[,2])) != "우B", ]
data = data[substr(data[,2], nchar(data[,2]) - 1, nchar(data[,2])) != "우C", ]
data = data[substr(data[,2], nchar(data[,2]), nchar(data[,2])) != "호", ]

1. 액면가가 "0" 인 종목, 즉 ETF, ETN, 주권회사, 외국종목과 같은 종목을 제거
2. grepl() 함수를 통해 "스팩"이 들어간 종목을 제거 (스팩종목 제거)
3. 마지막 글자가 '우'인 종목을 제거 (우선주 제거)
4. 마지막 글자가 '우B'인 종목을 제거 (우선주 제거)
5. 마지막 글자가 '우C'인 종목을 제거 (우선주 제거)
6. 마지막 글자가 '호'인 종목을 제거 (선박펀드 등을 제거)

위의 과정을 거치면 어쩔수 없이
미래에셋대우, 포스코대우와 같이 그냥 끝글자가 '우'인 종목과
삼호와 같이 끝글자가 '호'인 종목도 제거되지만
사실 몇종목 되지 않기 때문에 무시하고 진행하도록 합니다.

해당 클랜징을 통해 종목의 개수가
2808 개에서 2001 개로 줄어들게 됩니다.

가장 필요한 6자리 티커를 구했으니 작업을 끝내도 좋지만,
클랜징을 통해 유용한 데이터를 더 얻어낼 수도 있습니다.




각각 3~4, 6~12 열에 해당하는 정보는
숫자 형태로 바꿀 경우 유용하게 사용할 수 있습니다.


numeric.point = c(3:4, 6:12)
  data[,numeric.point] = sapply(data[,numeric.point], function(x) {
    gsub(",", "", x) %>%
      as.numeric()
})

먼저 위치에 해당하는 곳을 numeric.point에 지정해 줍니다.

그 후, sapply()를 통해 해당 열에 function()을 적용해 주며,
gsub() 함수를 통해 "," 글자를 "" 로 변경,
그 후 as.numeric() 함수를 통해 숫자 형태로 변경해 줍니다.

그 결과는 다음과 같습니다.



숫자 형태로 깔끔하게 정리가 되었습니다.


write.csv(data, "KOR_ticker.csv")

마지막으로 csv 파일로 저장해주면
모든 작업이 끝나게 됩니다.


댓글 10개:

  1. 그닥 중요한건 아니지만
    3-5: 어떤 종목명의 끝이 “우”이고, 그 “우”를 뺀 종목명이 현재 리스트에 있으면 제외
    6: regex “[0-9]+호$”로 매칭 되면 제외
    하면 어떨까 합니다.

    답글삭제
    답글
    1. 네 우선주는 말씀하신대로 하는게 더 깔끔하게 될듯하고
      6번은 제가 정규표현식을 쓸줄 몰라서요.... ㅠㅠ

      삭제
  2. 안녕하세요 좋은글 잘보았습니다!
    그런데 혹시 '시가총액' 페이지 상단에서 초기값으로 설정되는 지표들 말고 가령 자산총계, 부채총계 값을 긁어오려면 어떤방법을 쓸수잇을까요? 지표를 바꾸어도 https://finance.naver.com/sise/sise_market_sum.nhn?sosok=0 이라는 url자체엔 변화가 없어서 계속 초기값 지표들만 크롤링이 되네요

    답글삭제
    답글
    1. 원하시는 내용으로 크롤링하시려면 POST 방식을 이용해서 원하는 지표를 쿼리로 날린다음 자료를 받아야 합니다. 말씀하신대로 자산총계와 부채총계 선택하고 (지표가 6개까지 되서 나머지 몇개 삭제하고) 작업관리자를 확인해보면, field_submit.nhn? 부분에서 POST 로 날린 쿼리로 받는 url 확인하실 수 있습니다.

      https://finance.naver.com/sise/field_submit.nhn?menu=market_sum&returnUrl=http%3A%2F%2Ffinance.naver.com%2Fsise%2Fsise_market_sum.nhn%3Fsosok%3D0&fieldIds=market_sum&fieldIds=per&fieldIds=property_total&fieldIds=roe&fieldIds=debt_total&fieldIds=listed_stock_cnt

      삭제
    2. 친절한 답변 감사합니다!!!

      삭제
  3. redirect 302 메시지가 발생하네요.

    답글삭제
    답글
    1. 위 HTTP_MOVED_TEMP return 값에 대한 처리가 필요해보입니다.

      삭제
  4. 안녕하세요. 블로그에 적어주신 내용 참고하여 epsis.kpx.or.kr 데이터를 추출하고 있습니다.
    아래의 코드를 활용하였는데 정상적으로 추출이 되지 않습니다.
    혹시 관련해서 조언해주실 수 있으신가요?

    url <- 'http://epsis.kpx.or.kr/epsisnew/'
    webpage <- xml2::read_html(url)
    print(webpage)
    kpx_table <- rvest::html_nodes(x=webpage, xpath = '//*[@id="trTbody1"]')
    elec <- rvest::html_table(kpx_table)[[1]]
    head(elec)

    답글삭제
    답글
    1. 혹시 뭐하나여쭤봐도될까요? 마지막에 blah blah가 들어가있는 곳에 그 이전에 입력했던 for ( j~구문)이라고 생각했는데 replacement has 50 rows, data has 1550 에러가뜨네요.
      blah blah 지우고 해봤더니 계속 똑같은 값만 입력되고요!

      삭제
  5. 혹시 뭐하나여쭤봐도될까요? 마지막에 blah blah가 들어가있는 곳에 그 이전에 입력했던 for ( j~구문)이라고 생각했는데 replacement has 50 rows, data has 1550 에러가뜨네요.
    blah blah 지우고 해봤더니 계속 똑같은 값만 입력되고요!

    답글삭제