Post List

2019년 9월 17일 화요일

RSelenium (셀레니움)을 이용한 동적 웹페이지 크롤링하기





'R을 이용한 퀀트 투자 포트폴리오 만들기'가 출간 되었습니다.
많은 관심 부탁드리겠습니다.

↓↓ 구매링크 ↓↓



일반적인 (정적) 웹페이지는 서버에 미리 저장된 파일을 그대로 웹페이지에 전달하므로, 손쉽게 크롤링 할 수 있습니다.


반면 동적 웹페이지는 서버에 있는 데이터들이 스크립트에 의해 가공처리한 후 생성되어 웹 페이지에 전달되므로, 웹페이지가 계속해서 바뀌게 됩니다. 따라서 일반적을 방법으로는 크롤링할 수 없습니다.

dynamic-web.png


네이버의 요약 재무제표 항목 역시 이러한 동적 웹페이지로 이루어져 있어 기존 방식으로 크롤링 하려면 매우 번거롭지만, 셀레니움을 이용할 경우 손쉽게 크롤링할 수 있습니다.

Selenium은 주로 웹앱을 테스트하는데 이용하는 프레임워크로써, webdriver라는 API를 통해 운영체제에 설치된 Chrome등의 브라우저를 제어합니다.

브라우저를 직접 동작시킨다는 것은 JavaScript를 이용해 비동기적으로 혹은 뒤늦게 불러와지는 컨텐츠들을 가져올 수 있다는 것입니다. 즉, ‘눈에 보이는’ 컨텐츠라면 모두 가져올 수 있으며, JavaScript로 렌더링이 완료된 후의 DOM결과물에 접근이 가능합니다.

(사실 몰라도 됩니다...)

먼저 아래 3개 파일을 본인의 OS에 맞게 다운로드 받은 후, 하나의 폴더에 저장해줍니다. geckodriver chromeDriver는 압축을 해제하며, selenium은 그대로 둡니다.

chromeDriver의 경우 본인이 사용하고 있는 크롬 버젼과 같은 것을 다운로드해야 하며, 해당 버젼의 확인은 크롬창에서 [도움말] -> [Chrome 정보]를 통해 확인할 수 있습니다.







윈도우에서 cmd를 통해 명령 프롬프트를 연 후, 아래 명령어를 입력합니다.

C:\Rselenium에는 파일은 다운로드 받은 폴더의 위치를 입력하며, standalone 뒤 3.141.59는 본인이 다운로드 받은 selenium-server-standalone 파일의 버젼과 같은 숫자를 입력합니다.

해당 cmd 창은 계속 열어두어야 합니다.

cd C:\Rselenium
java -Dwebdriver.gecko.driver="geckodriver.exe" -jar selenium-server-standalone-3.141.59.jar -port 4445



install.packages('RSelenium')
install.packages('seleniumPipes')
library(RSelenium)
library(seleniumPipes)
library(rvest)
library(httr)

셀레니움 관련 패키지인 RSelenium와 seleniumPipes를 인스톨 한 후, 관련 패키지들을 열어줍니다.



remDr = remoteDriver(
  remoteServerAddr="localhost",
  port=4445L,
  browserName="chrome")

remDr$open()
## [1] "Connecting to remote server"
## $acceptInsecureCerts
## [1] FALSE
## 
## $browserName
## [1] "chrome"
## 
## $browserVersion
## [1] "77.0.3865.75"
## 
## $chrome
## $chrome$chromedriverVersion
## [1] "76.0.3809.126 (d80a294506b4c9d18015e755cee48f953ddc3f2f-refs/branch-heads/3809@{#1024})"
## 
## $chrome$userDataDir
## [1] "C:\\Users\\Henry\\AppData\\Local\\Temp\\scoped_dir11848_77181124"
## 
## 
## $`goog:chromeOptions`
## $`goog:chromeOptions`$debuggerAddress
## [1] "localhost:54111"
## 
## 
## $networkConnectionEnabled
## [1] FALSE
## 
## $pageLoadStrategy
## [1] "normal"
## 
## $platformName
## [1] "windows nt"
## 
## $proxy
## named list()
## 
## $setWindowRect
## [1] TRUE
## 
## $strictFileInteractability
## [1] FALSE
## 
## $timeouts
## $timeouts$implicit
## [1] 0
## 
## $timeouts$pageLoad
## [1] 300000
## 
## $timeouts$script
## [1] 30000
## 
## 
## $unhandledPromptBehavior
## [1] "dismiss and notify"
## 
## $webdriver.remote.sessionid
## [1] "6de07eeb6dfc9fd8203ce2708342ca05"
## 
## $id
## [1] "6de07eeb6dfc9fd8203ce2708342ca05"
remDr$navigate('https://finance.naver.com/item/coinfo.nhn?code=005930&target=finsum_more')

먼저 remoteDriver() 함수를 통해 4445번 포트와 크롬을 연결시켜 주며, remDr$open() 함수를 입력하면 크롬 웹창이 열리게 됩니다. remDr$navigate() 함수 내부에 크롤링하고자 하는 사이트 주소를 입력하면 해당 주소로 이동하게 됩니다.

현재 페이지가 셀레니움에 의해 동작되므로, 자동화된 테스트 소프트웨어에 의해 제어되고 있습니다. 라는 문구가 뜹니다.





이제 Financial Summary에 해당하는 부분을 찾아나갑니다.





해당 페이지는 iframe 내부에서 javascript를 통해 타 페이지의 데이터가 들어와있는 형태이므로, 해당 iframe 내부로 접근해야 합니다.


# ID 찾아내기
frames = remDr$findElements(using = "id",
                  value = 'coinfo_cp')

print(frames)
## [[1]]
## [1] "remoteDriver fields"
## $remoteServerAddr
## [1] "localhost"
## 
## $port
## [1] 4445
## 
## $browserName
## [1] "chrome"
## 
## $version
## [1] ""
## 
## $platform
## [1] "ANY"
## 
## $javascript
## [1] TRUE
## 
## $nativeEvents
## [1] TRUE
## 
## $extraCapabilities
## list()
## 
## [1] "webElement fields"
## $elementId
## [1] "f0bfdfff-3c16-4331-ad30-6df5436064cd"
# Frame 안으로 접근
remDr$switchToFrame(frames[[1]])


remDr$findElements() 함수는 원하는 요소로 접근이 가능합니다. 위 iframe은 coinfo_cp라는 id를 사용하고 있으므로 using에는 id를, value에는 coinfo_cp를 입력하며, 이 외에도 xpath, css selector 등 다양한 html 태그를 통해 원하는 html 정보를 찾을 수 있습니다.
그 후 remDr$switchToFrame() 함수를 통해 iframe 내부로 접근합니다.
우리가 필요한 데이터는 연간 재무제표 이므로, [연간]에 해당하는 탭의 위치를 찾은 후 Xpath를 복사합니다. 해당 위치는 다음과 같습니다.
//*[@id="cns_Tab21"]




# 연간 클릭
remDr$findElement(using = 'xpath',
                  value ='//*[@id="cns_Tab21"]')$clickElement()
remDr$findElement() 함수 내부에 xpath를 사용하여 해당 위치를 찾은 후, $clickElement()를 입력하면 해당 위치를 클릭하게 됩니다. 이제 연간 재무제표가 표시된 페이지를 읽어오도록 하겠습니다.




page_parse = remDr$getPageSource()[[1]]
page_html = page_parse %>% read_html()
  1. remDr$getPageSource() 함수를 통해 페이지 소스를 읽어옵니다.
  2. read_html() 함수를 통해 HTML 정보만을 읽어옵니다. 이제 재무제표 데이터가 들어있는 테이블만 추출하면 됩니다.

Sys.setlocale('LC_ALL', 'English')
table = page_html %>% html_table(fill = TRUE)
Sys.setlocale('LC_ALL', 'Korean')
df = table[[13]]
head(df)

##         주요재무정보                                  연간
## 1       주요재무정보 2014/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2             매출액                             2,062,060
## 3           영업이익                               250,251
## 4 영업이익(발표기준)                               250,251
## 5   세전계속사업이익                               278,750
## 6         당기순이익                               233,944
##                                    연간
## 1 2015/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,006,535
## 3                               264,134
## 4                               264,134
## 5                               259,610
## 6                               190,601
##                                    연간
## 1 2016/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,018,667
## 3                               292,407
## 4                               292,407
## 5                               307,137
## 6                               227,261
##                                    연간
## 1 2017/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,395,754
## 3                               536,450
## 4                               536,450
## 5                               561,960
## 6                               421,867
##                                    연간
## 1 2018/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,437,714
## 3                               588,867
## 4                               588,867
## 5                               611,600
## 6                               443,449
##                                       연간
## 1 2019/12(E)\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                                2,305,363
## 3                                  268,387
## 4                                         
## 5                                  294,636
## 6                                  215,948
##                                       연간
## 1 2020/12(E)\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                                2,469,186
## 3                                  354,097
## 4                                         
## 5                                  381,833
## 6                                  279,894
##                                       연간
## 1 2021/12(E)\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                                2,648,131
## 3                                  446,023
## 4                                         
## 5                                  480,005
## 6                                  352,816

  1. 로케일 언어를 English로 변경합니다.
  2. html_table() 함수를 통해 편하게 테이블 데이터만 추출할 수 있습니다.
  3. 다시 로케일 언어를 Korean으로 변경합니다.
  4. 총 19개 테이블 중 13번째 테이블이 연간 재무제표에 해당합니다. 만일 다른 테이블을 추출하고자 하면 해당 숫자만 변경해주면 됩니다.

우리가 원하는 연간 재무제표 데이터가 제대로 크롤링 되었으며, 간단한 클렌징 작업만 거치면 모든 작업이 완료됩니다.

library(stringr)
library(magrittr)

rownames(df) = df[, 1]
df = df[, -1]

colnames(df) = df[1, ]
df = df[-1, ]
colnames(df) = str_sub(colnames(df), 1, 7)

df = sapply(df, function(x) {
  str_replace_all(x, ',', '') %>%
    as.numeric()
}) %>%
  data.frame(., row.names = rownames(df))

head(df)
##                    X2014.12 X2015.12 X2016.12 X2017.12 X2018.12 X2019.12
## 매출액              2062060  2006535  2018667  2395754  2437714  2305363
## 영업이익             250251   264134   292407   536450   588867   268387
## 영업이익(발표기준)   250251   264134   292407   536450   588867       NA
## 세전계속사업이익     278750   259610   307137   561960   611600   294636
## 당기순이익           233944   190601   227261   421867   443449   215948
## 당기순이익(지배)     230825   186946   224157   413446   438909   213435
##                    X2020.12 X2021.12
## 매출액              2469186  2648131
## 영업이익             354097   446023
## 영업이익(발표기준)       NA       NA
## 세전계속사업이익     381833   480005
## 당기순이익           279894   352816
## 당기순이익(지배)     275918   347589

  1. 첫번째 열을 행이름으로 설정한 후, 해당 열을 삭제합니다.
  2. 첫번재 행을 열이름으로 설정한 후, 해당 행을 삭제합니다.
  3. 열이름에서 str_sub() 함수를 이용해 1~7번째 글자만 선택합니다. (YYYY/MM)
  4. sapply() 함수 내에서 str_replace_all() 함수를 이용해 모든 콤마(,)를 없앤 후 숫자 형태로 변경합니다.

결과물을 출력해보면 클렌징 작업이 완료되었습니다.

댓글 7개:

  1. 헨리님 항상 좋은 강의 감사드립니다!

    두가지 질문이 있습니다.
    1)
    xpath 부분도 제가 따라하려고 했는데
    # 연간 클릭
    remDr$findElement(using = 'xpath',
    value ='//*[@id="cns_Tab21"]')$clickElement()
    cns_Tab21을 크롬에서 찾는 방법을 몰라 헨리님이 쓰신대로 따라했는데 페이지에서 오른쪽클릭 후 '검사(N)'를 누르고 어떻게 찾는건지요?

    2)
    df에 저장후 head(df)를 읽어오면 저는 아래와 같이 뜨는데 어떤부분이 잘못된걸까요?
    > head(df)

    X1 X2 X3 X4 X5 X6
    1 NA 전체 연간 분기 NA NA

    답글삭제
    답글
    1. 1) 개발자도구화면에서 노가다로 찾아야 됩니다... 특히나 저 페이지는 잘 확인이 안되게 되있어서 사실상 짬바로 찾아야 합니다...

      2) 테이블의 13번째 리스트가 해당 데이터가 아닌 다른 번째 리스트가 원하는 데이터 인거 같습니다. df = table[[13]] 내의 13 숫자를 일일이 바꿔보면서 한번 찾아보셔야 할듯합니다... 저도 다른 pc로 확인해보겠습니다.

      삭제
  2. 똑같이 따라하는 와중에 remDr$findElement(using = 'xpath',
    value ='//*[@id="cns_Tab21"]')$clickElement()에서 연간이 클릭되지 않고, page_parse = remDr$getPageSource[[1]]에서 Error in remDr$getPageSource[[1]] :
    object of type 'closure' is not subsettable이 발생합니다. 어떻게 해결해야 하나요?

    답글삭제
  3. 안녕하세요. Rselenium 관련 내용을 찾던중 많은 도움이 되었습니다.
    다름이아니라, 저는 Rselenium을 이용하여 통계청 자료에 접근을 하려고 하는데, 연간을 클릭하라는 부분에서 진행이 안되어서 늦은시간임에도 불구하고 댓글을 남깁니다. 통계청의 자료도 교수님께서 자바스크립트라서 배우지는 않았지만 Rselenium을 이용하면 된다고 하셔서, 헨리님의 글을 통하여 해결하고 있었는데, 막혀서 질문을 남깁니다.

    일단 사용하려는 사이트는 http://www.index.go.kr/potal/stts/idxMain/selectPoSttsIdxSearch.do?idx_cd=1009&stts_cd=100901&freq=Y
    입니다. 혹시 해결방법을 알 수 있을까해서, 이렇게 댓글을 남깁니다.
    감사합니다.

    답글삭제
  4. 헤리님, 안녕하세요.
    저는 오늘 처음으로 방문하였습니다.
    무엇보다도 따라할 수 있게 충분한 설명 감사드립니다.

    그러나 아래의 코드에서 Error가 발생하는데,
    remDr = remoteDriver(
    remoteServerAddr="localhost",
    port=4445L,
    browserName="chrome")

    remDr$open()
    remoteDriver() 함수가 없다고 합니다. 무엇이 잘못된 것인지 알려 주시면 감사하겠습니다.

    답글삭제
    답글
    1. library(httr)
      library(tidyverse)
      library(wdman)
      library(binman) #list_versions() 함수사용
      library(rvest)
      library(RSelenium)
      library(stringr)

      port=50001L #빈 곳이면 아무곳이나 반드시 정수타입 L을 붙여야 함
      driver=chrome(port,version = "88.0.4324.27") #반드시 크롬과 크롬 드라이버 버전 맞아야 함
      remote=remoteDriver(port=port,browserName="chrome") # browserName="chrome"는 생략가능
      remote$open()

      url="https://search.daum.net/search?nil_suggest=btn&w=img&DA=SBC&q=전유진"
      remote$navigate(url=url)
      ~~~
      ~~~
      #원하는 코드 짜세요
      ~~~
      ~~~
      remote$close
      driver$stop()

      삭제
  5. remDr$switchToFrame(frames[[1]]) 에서 no such frame: element is not a frame이라는 오류에서는 어떻게 대처할 수 있는 걸까요?

    제가 id를 잘못 잡은 건가 싶어서 다른 걸 잡아서 해도 같은 오류가 계속 발생합니다.. 도와주세요

    답글삭제