Post List

2019년 3월 8일 금요일

R을 이용한 네이버 금융 재무테이블 크롤링 (정규표현식으로 랜덤코드 찾기)



네이버 금융의 데이터를 크롤링하는 법은 일전에도 다룬적이 있습니다.

그.러.나.

너무 많은 분들이 크롤링을 해서인지,
어느때부터 해당 페이지가 크롤링을 막기위해
여러 장치들을 추가하였습니다.

물론 셀레니움을 사용하면 손쉽게 크롤링 할 수 있지만,
기존의 올드한 방법으로도 크롤링을 할 수 있는 방법이 있습니다.




해당 페이지에서 개발자도구 화면을 연 후 '기업현황' 부분을 클릭하면
해당 테이블의 데이터를 어디서 가지고 오는지 확인할 수 있습니다.







c1010001.aspx?cmp_cd=005930&cn=

해당 부분을 클릭한 후 보이는 Request URL이 
데이터를 가져오는 URL입니다.

https://companyinfo.stock.naver.com/v1/company/c1010001.aspx?cmp_cd=005930&cn=


이번에는 해당 주소로 다시 접속한 후,
개발자도구 화면을 연 채 'Financial Summary'에서 '연간' 부분을 클릭합니다.




cF1001.aspx?cmp_cd=005930&fin_typ=0&freq_typ=....
부분의 Request URL이 해당 테이블을 가져오는 주소입니다.

해당 주소에 접근하면 다음과 같은 화면이 보입니다.





위의 알 수없는 숫자 패턴이 보입니다.
아마 크롤링을 막기위해(?) 만든 장치인거 같은데
사실 아무런 문제도 아닙니다.

하단을 보면 우리가 가지고 오고 싶은
주요재무정보 테이블이 존재합니다.


문제는 해당 페이지의 url에 있습니다.

https://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?cmp_cd=005930&fin_typ=0&freq_typ=Y&encparam=czM0QmNOazhtN0lmM1FzNzIvNWJ1UT09&id=QmZIZ20rMn



사실 이전에는 

https://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?cmp_cd=005930&fin_typ=0&freq_typ=Y

이러한 형식으로 url이 구성되어 있어서
url을 생성하기가 너무 쉬웠습니다.

fin_typ은 개별/연결 여부, freq_typ은 연간/분기 여부이므로
cmp_cd 즉 주식티커 부분만 바꾸면
원하는 데이터를 가져올수 있었죠.

그런데 새롭게 바뀐 url에는
encparam과 id라는 쿼리가 추가되었습니다.

더구나 아무런 패턴이 없이 무작위로 생성된
숫자와 문자열의 조합이며, 계속해서 변경되므로
모든 종목에 대한 해당 쿼리를 알 방법이 없습니다.




다행히 모든 힌트는 페이지 내에 있습니다.
꼭꼭 숨겨뒀지만 어째튼 우리는 찾아낼 수 있습니다.





개발자도구 화면을 다시 깨끗히 하고
(F12 눌러서 끈다음 다시 켜면 됩니다)

기업현황 탭을 눌러 다시 네트워크를 확인합니다.


c1010001.aspx?cmp_cd=005930&cn=


해당 부분에 클릭한 후, Response 부분을 확인해보면
페이지의 상세한 HTML을 확인할 수 있습니다.




encparam을 검색해보면 총 3개가 발견되며,
 그 중 3번째로 발견된 곳에서
encparam과 그 아래에 id 값이 존재함이 확인됩니다.


이제 모든 준비가 끝났으므로 이를 코드로 풀어보도록 하겠습니다.

library(httr)
library(rvest)
library(stringr)
library(readr)

url = 'https://companyinfo.stock.naver.com/v1/company/c1010001.aspx?cmp_cd=005930'
data = GET(url)
data.text = read_html(data) %>%
  html_text()


먼저 url에 해당하는 부분을 GET() 함수를 이용해 읽어오며,
read_html()과 html_text() 함수를 통해 text 부분만 추출합니다.

data.text
[1] "온라인기업정보 - 기업모니터 - 기업개요(삼성전자)\r\n\t  window.dataLayer = window.dataLayer || [];\r\n\t  function gtag(){dataLayer.push(arguments);}\r\n\t  gtag('js', new Date());\r\n\r\n\t  gtag('config', 'UA-74989022-7');\r\n\t\r\n\r\n\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\r\n\t기업현황 \r\n\t\t기업개요\r\n\t\t재무분석\r\n\t\t투자지표\r\n\t\t컨센서스\r\n\t\t업종분석\r\n\t\t섹터분석\r\n\t\t지분현황\r\n\t\t인쇄\r\n\t\r\n\r\n\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\r\n\t\r\n\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\t삼성전자\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t005930\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tSamsungElec\r\n\t\t\t\t\t\tKOSPI : 전기전자\r\n\t\t\t\t\t\tWICS : 반도체와반도체장비\r\n\t\t\t\t\t\r\n\t\t\t\r\n\t\t\t\t\t \r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\t\tEPS 6,024\r\n\t\t\t\t\t\tBPS 35,342\r\n\t\t\t\t\t\tPER 7.38\r\n\t\t\t\t\t\t업종PER 6.24\r\n\t\t\t\t\t\tPBR 1.26\r\n\t\t\t\t\t\t현금배당수익률 3.19%\r\n\t\t\t\t\t\t결산기 : 12월\r\n\t\t\t\t\t\r\n\t\t\t\r\n\t\r\n\r\n\r\n\t* PER : 전일자 보통주 수정주가 / 최근결산 EPS\r\n\t\t* 현금배당수익률 : 최근 결산 수정DPS(현금) /


data.text 변수에 html의 모든 코드가 입력되었습니다.

이제 이 중에서 우리가 원하는 encparam / id 값을 발라내야 합니다.


data.param = str_match(data.text, "encparam: '(.*)'")[2]
data.id = str_match(data.text, "id: '([a-zA-Z0-9]*)' ?")[2]


수많은 글자에서 우리가 원하는 패턴을 찾기위해서
정규표현식을 이용해주도록 합니다.



encparam값의 경우 encparam: '문자와숫자조합' 으로 이루어져 있으며
id값의 경우 id: '문자와숫자조합' ? 으로 이루어져 있습니다.

이 중 우리가 원하는 값은 '문자와숫자조합' 부분의 데이터 입니다.

이를 str_match() 함수와 정규표현식으로 찾아주면 됩니다.


> data.param
[1] "czM0QmNOazhtN0lmM1FzNzIvNWJ1UT09"
> data.id
[1] "QmZIZ20rMn"


encparam과 id에 해당하는 부분이 잘 추출되었습니다.
(값이 다른 이유는 해당 값이
시간이 지남에 따라 계속 변경되기 때문입니다.)

이제 테이블이 존재했던 페이지 url의 형식에 맞춰
url 값을 생성해 주도록 합니다.


url_fs = paste0('https://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?cmp_cd=005930&fin_typ=0&freq_typ=Y&encparam=',
                data.param,"&id=",data.id)
url_fs
[1] "https://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?cmp_cd=005930&fin_typ=0&freq_typ=Y&encparam=czM0QmNOazhtN0lmM1FzNzIvNWJ1UT09&id=QmZIZ20rMn"


생성된 url을 직접 입력해보면
데이터가 존재하는 페이지로 제대로 이동이 됩니다.




테이블 부분만 가져오기 위해 해당 부분의 xpath를 추출하도록 합니다.
그 값은 다음과 같습니다.

/html/body/table[2]


위의 url과 해당 xpath 값을 이용해 테이블을 추출하도록 하겠습니다.


data_fs = GET(url_fs)
data_table = read_html(data_fs) %>%
  html_node(xpath = '/html/body/table[2]') %>%
  html_table(fill = TRUE)

data_table 변수에 해당 테이블 만이 제대로 입력되었음이 확인됩니다.




이제 데이터 클렌징만 해주면 모든 작업이 완료됩니다.

1. 첫번째 행을 열이름으로 바꾸고 첫번째 열 삭제
2. 첫번째 열을 행이름으로 변경
3. ',' 글자를 삭제 후 모든 데이터를 숫자 형태로 변환
4. 행이름의 공백 (\r\n\t) 부분을 삭제


위 과정을 코드로 나타내면 다음과 같습니다.


colnames(data_table) = data_table[1, ]
data_table = data_table[-1, ]

rownames(data_table) = NULL
data_table = tibble::column_to_rownames(data_table, var = '주요재무정보')

for (j in 1:ncol(data_table)) {
  data_table[, j] = str_replace_all(data_table[, j], ',', '') %>% as.numeric()
}

colnames(data_table) = str_replace_all(colnames(data_table), "[\r\n\t+]" , "")


최종적으로 정리된 테이블을 확인해보면
깔끔하게 정리되어 있습니다.





네이버 금융에 존재하는 대부분의 데이터가
위와 동일한 형식으로 데이터를 가져오므로
위의 코드를 제대로 이해하고 활용만 한다면
원하는 데이터를 크롤링 하실수 있습니다. :D

댓글 3개:

  1. 글 잘 읽고 구현을 해보았는데, 잘 되는군요. 참으로 감사합니다. 그런데, 여러 업체들을 차례로 가져오려 할때는 잘 동작이 되지를 않습니다. 제가 실제로 크롬으로 열어본 페이지만 정보를 가져올 수 있을 뿐 열어보지 않은 것들은 빈 값을 돌려줍니다. 어떤 방법이 있을련지요.

    답글삭제
  2. 답을 알아냈습니다.
    # RSelenium을 설치하시고,

    if (require("RSelenium") == FALSE) {
    install.packages("RSelenium")
    require("RSelenium")
    }

    # 크롬브라우저를 여신 다음에

    driver <- rsDriver(browser=c("chrome"), chromever="73.0.3683.68")
    remDr <- driver$client

    # 매번 업체를 선택할 때 URL을 먼저 실행하고 코드를 실행하면 됩니다.

    remDr$navigate(url)

    답글삭제
    답글
    1. 아래와 같은 오류가 나는데 어떻게 처리해야하는지 모르겠네요 ㅠㅠ
      >data = GET(url)
      Error in curl::curl_fetch_memory(url, handle = handle) :
      schannel: next InitializeSecurityContext failed: SEC_E_CERT_EXPIRED (0x80090328) - 받은 인증서가 만료되었습니다.

      삭제