ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Rselenium으로 로또 1등 배출점 웹크롤링하기
    잡다R 2019. 12. 11. 15:26
    Rselenium으로 로또 1등 배출점 웹크롤링하기


     안녕하세요? 잡다R 두번 째 글이에요! 짝짝짝

     지난 시간에 이어서 이번 잡다R 주제도 로또에 관한 걸로 잡았습니다. 왜냐하면 제가 되게 미련을 못 버리는 스타일이라.. 크ㅡ흠.

     각설하고 이번 주제에서 렛미 두잇 할 일에 대해서 살펴볼게요.

    1. 로또 홈페이지에서 1등 배출점 정보를 웹크롤링합니다.

    2. 웹크롤링해온 데이터를 이용해 서울 지도를 그려봅니다.

     로또 1등 배출점을 웹크롤링해보고, 그 데이터를 이용해서 지도 시각화까지 해보겠습니다. 원래는 웹크롤링과 시각화를 한 번에 진행하려고 했는데, 글이 길어지는 바람에 지도 시각화는 [다음글]에서 하도록 하고, 여기서는 웹크롤링에 관한 이야기만 할게요.

     로또 홈페이지에서는 262회차부터 로또 1등 당첨자를 배출한 판매점들의 정보를 제공하고 있습니다. 각각의 판매점들은 순위, 상호명, 배출건수, 소재지, 지도보기라는 총 5개의 정보가 테이블 형식으로 나타나 있어요. 

     이건 확실한 데이터입니다. 테이블 형식이라 그런지 마음까지 편안하네요. 하지만 데이터를 받아내기 난감해 보입니다. 홈페이지에서는 csv나 excel같은 파일을 제공해주지 않거든요. 이럴 때 필요한 게 바로 웹크롤링입니다.

     웹크롤링을 한 마디로 표현하자면 인터넷에 있는 데이터를 가져오는 거에요. 웹크롤링을 잘 이용하면 웹 상에 있는 어떠한 데이터도 긁어올 수 있습니다. 물론 개인정보보호법이나 재산권 등 문제가 될만한 부분에 대해서는 미리 파악을 해보시는게 중요하겠죠? 그럼 바로 웹크롤링을 진행하겠습니다.

     먼저 필요한 라이브러리를 받아줍니다. rvest 라이브러리는 html 파일에서 우리가 원하는 데이터를 긁어올 수 있도록 도와주는 착한 라이브러리입니다.

    library(rvest)

     다음으로 우리가 원하는 데이터가 있는 url을 긁어서 담아줍니다. 제가 담은 건 로또 1등 배출점 정보가 있는 곳입니다. read_html()라는 함수를 이용하면 해당 링크에 쓰인 html 파일을 받아올 수가 있습니다.

    url <- "https://dhlottery.co.kr/store.do?method=topStoreRank&rank=1&pageGubun=L645"
    link <- read_html(url)

     html 파일을 받아왔다면 우리는 html 마크업을 통해 원하는 데이터를 얻을 수 있습니다. 

     html을 모르시는 분들을 위해 html 마크업을 잠깐 설명드리겠습니다. 위 그림에서 p라는 태그, class라는 속성, foo라는 속성값, 그리고 태그 안에 있는 내용과 닫는 태그를 확인하실 수가 있을 거에요. html 파일은 저런 모양의 태그가 트리 구조로 이루어진 모습을 하고 있습니다.

      트리구조 출처:구글이미지

     트리구조는 다음과 같은 모습입니다. 각각의 원은 노드라고 하는데, 그림이 마치 가계도 족보 같다고 해서 상위에 있는 노드는 부모 노드, 하위에 있는 노드는 자식 노드라고 합니다. html에서는 노드가 태그를 나타내니 부모 태그, 자식 태그라고 부를 수 있겠네요. 그리고 html 문서를 보시면 부모 태그가 자식 태그를 감싸고 있는 모습을 확인하실 수가 있어요. 이를 테면 <body> .. <parents> .. <son> .. </son> .. </parents> .. </body> 이런 식으로 말이죠. 그림이 아니라서 처음에는 보기 힘들 수 있습니다. 하지만 여기서 ’son은 parents의 자식 태그이고, parents는 body의 자식 태그이자 son의 부모 태그이다’라는 말을 이해하실 수만 있다면 당신은 지니어스 오브 천재입니다 휴먼.

     이 외에 html 마크업에 관한 자세한 설명을 드리기에는 시간이 너무 오래 걸릴 것 같아요. 그래서 기초적인 html 마크업 지식이 있다는 가정 하에 진행하도록 하겠습니다. 모르시는 분들은 인터넷에서 찾아보시거나 >생활코딩< 같은 사이트에서 공부해보시면 많은 도움이 될 거에요.

     다시 R로 넘어올게요. html_nodes() 함수를 이용하면 해당 태그 안에 있는 값을 얻을 수 있습니다. 그리고 파이프를 통해 자식 태그로 내려갈 수 있어요. 아래와 같이 말이죠.

    link %>% 
      html_nodes('body') %>% 
      html_nodes('.containerWrap') %>% 
      html_nodes('.contentSection') %>% 
      html_nodes('#article')
    ## {xml_nodeset (1)}
    ## [1] <div id="article" class="contentsArticle">\r\n\t\t<div class="header ...

     '.'이랑 '#'을 이용해서 태그의 class와 id 같은 속성값을 표현해 줄 수 있어요. 이를 통해 위 코드를 한번 해석해보면 body 태그 아래에 containerWrap이라는 class를 가지는 태그 밑에, contentSection이라는 class를 가지는 태그 밑에, article이라는 id를 가진 태그의 코드를 보겠다는 이야기가 됩니다. 이해가 되시나요? html을 모르는 분들을 위해 또 한 가지 말씀을 드리자면, 일반적으로 브라우저에서 여러분이 보고 있는 대부분의 내용은 body 태그 밑에 존재할 확률이 매우 높습니다. body는 태그 오브 태그, 말 그대로 태그의 선조님이라고 할 수 있죠. 만약 내가 보고 있는 페이지의 html 파일이 어떻게 생겼는 지 확인하고 싶으시다면 해당 페이지에서 마우스 우클릭 > 검사 또는 페이지 소스보기를 누르시거나 F12를 누르고 Elements를 보시면 됩니다.

     이렇게 선조부터 말단 자식 태그까지 쭉 타고 내려오면 원하는 지점에 도달할 수 있을 거에요. 정말 좋은 점은 node 또는 attr이 하나 밖에 없다면, 즉 값이 unique하면 최상위 태그부터 내려올 필요가 없이 unique한 태그부터 html 코드를 따올 수가 있습니다. 아래와 같이 말이죠.

    link %>% 
      html_nodes('#article')
    ## {xml_nodeset (1)}
    ## [1] <div id="article" class="contentsArticle">\r\n\t\t<div class="header ...

     이렇게 해도 위에 5줄짜리 코드와 값이 완전히 일치하는 것을 확인하실 수가 있어요. 자 그러면 우리가 원하는 로또 1등 배출점들의 5가지 정보가 있는 곳을 한번 찔러보도록 할게요.

    link %>% 
    #  html_nodes('body') %>% 
    #  html_nodes('.containerWrap') %>% 
    #  html_nodes('.contentSection') %>% 
    #  html_nodes('#article') %>% 
    #  html_nodes('.content_wrap') %>% 
    #  html_nodes('.group_content') %>% 
    #  html_nodes('.tbl_data') %>% 
    #  html_nodes('tbody') %>% 
    #  html_nodes('tr') %>% 
      html_nodes('td') %>% 
      html_text() %>% head
    ## [1] "1"                                                                                                                    
    ## [2] "\r\n\t\t\t\t\t\t\t\t    \t스파 \r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t    \t"                                      
    ## [3] "35"                                                                                                                   
    ## [4] "서울 노원구 상계동  666-3 주공10단지종합상가111"                                                                      
    ## [5] "\r\n\t\t\t\t\t\t\t\t\t    \r\n\t\t\t\t\t\t\t\t\t    \t스파 지도보기\r\n\t\t\t\t\t\t\t\t        \r\n\t\t\t\t\t\t\t\t\t"
    ## [6] "2"

     끝에 html_text() 함수를 써주면 태그 전체가 아닌 태그의 내용만 문자로 따로 쏙 빼올 수 있습니다. 아마 head를 풀고 보시면 75개 문자가 있는 걸 확인하실 수가 있을 거에요. 그 이유는 한 페이지에 1등 배출점이 15개씩 보여지기 때문에 15개 판매점 정보 X 5개 정보로 해서 75개가 담겼기 때문입니다. 이걸 그대로 store15라는 변수에 담아서 데이터 프레임으로 만들어보겠습니다.

     한 가지만 더 말씀 드리면 node를 따라 들어가는 걸 아래와 같이 한줄로 써줘도 됩니다. 부모 자식 태그는 띄어쓰기로 구분해줄 수 있습니다. 만약 가독성을 따진다면 위 방식이 더 좋은 거 같습니다. 이건 취향에 따라 써주세요.

    store15 <- link %>% 
      html_nodes('tbody tr td') %>% 
      html_text()

     다음으로 \t, \n, \r 등의 표현을 제거해서 데이터를 깔끔하게 정제해주도록 하겠습니다. 문자열 정리 라이브러리인 stringr을 이용할게요. stringr에 대해서는 나중에 따로 주제를 잡아서 배워보겠습니다. 일단 몰라도 따라와주세요.

    library(stringr)
    store15 <- str_replace_all(store15, "\t|\n|\r", "")
    store15 <- str_replace_all(store15, "[[:space:]]{3,}", "")
    store15 <- str_replace_all(store15, "[[:space:]]{2}", " ")

     이러고 store15를 확인해보시면

    store15 %>% head(10)
    ##  [1] "1"                                             
    ##  [2] "스파"                                          
    ##  [3] "35"                                            
    ##  [4] "서울 노원구 상계동 666-3 주공10단지종합상가111"
    ##  [5] "스파 지도보기"                                 
    ##  [6] "2"                                             
    ##  [7] "부일카서비스"                                  
    ##  [8] "34"                                            
    ##  [9] "부산 동구 범일제2동 830-195번지"               
    ## [10] "부일카서비스 지도보기"

     완전 깨끗해진 걸 확인하실 수 있습니다. 여기서 번호와 지도보기는 필요가 없으므로 제외하고, 상호명, 배출건수, 소재지를 data.frame의 한 열로 생각하고 나누어 담으면 되겠네요. store15라는 변수 안에는 75개의 문자열이 있습니다. 5개 당 하나의 판매점에 대한 정보니까 숫자 5를 이용한 데이터 분류가 필요하겠어요. 예를 들어, 2번, 7번, 12번과 같은 숫자는 5로 나눴을 때 나머지 2라는 공통점이 있는데, 전부 상호명에 대한 데이터를 가지고 있습니다. 설명을 드리지 않아도 감이 오시죠? 정말 기초적인 코딩 문제가 나왔네요. R에서 나누기의 나머지는 '%%'를 이용해주면 됩니다. 우리는 n %% 5를 이용하면 되겠네요. 그럼 3가지 정보만 따로 얻어보겠습니다. for문으로 ㄱㄱ 해볼게요.

    name <- NULL
    n <- NULL
    location <- NULL
    
    for (i in 1:length(store15)) {
      if (i %% 5 == 2) {
        name <- c(name, store15[i])
      } else if (i %% 5 == 3) {
        n <- c(n, store15[i])
      } else if (i %% 5 == 4) {
        location <- c(location, store15[i])
      }
    }
    
    store15_df <- data.frame(name, n, location)
    store15_df
    ##                name  n                                           location
    ## 1              스파 35     서울 노원구 상계동 666-3 주공10단지종합상가111
    ## 2      부일카서비스 34                    부산 동구 범일제2동 830-195번지
    ## 3    일등복권편의점 23                    대구 달서구 본리동 2-16번지 1층
    ## 4      세진전자통신 16                        대구 서구 평리동 1094-4번지
    ## 5        로또휴게실 14                    경기 용인시 기흥구 상갈동 378-1
    ## 6  GS25(양산혜인점) 12                        경남 양산시 평산동 31-5번지
    ## 7        목화휴게소 12                        경남 사천시 용현면 주문리 4
    ## 8    로또명당인주점 11                    충남 아산시 인주면 신성리 188-8
    ## 9          잠실매점 10 서울 송파구 신천동 7-18번지 잠실역 8번출구 앞 가판
    ## 10       제이복권방 10      서울 종로구 종로5가 58번지 평창빌딩 1층 103호
    ## 11     갈렙분식한식  9                      서울 중랑구 망우동 490-13번지
    ## 12       라이프마트  9                    인천 중구 항동7가 58-98번지 5호
    ## 13       버스판매소  9         서울 영등포구 영등포동4가 440번지 신세계앞
    ## 14 북마산복권전문점  9             경남 창원시 마산합포구 상남동 39-4번지
    ## 15       행운복권방  9                    경기 포천시 소흘읍 송우리 128-2

     이런식으로 담았으면 잘 된 것 같아요. 짝짝짝!

     하지만 기쁨도 잠시.. 문제는 15개의 판매점이 다가 아니라는 것입니다. 우리는 모든 판매점의 정보를 받아야 할 거에요. 중요한 점은 판매점 정보가 15개 당 한 페이지 씩 구성된 것이 아니라 한 페이지 안에서 전체 정보가 15개씩 찍혀 나오도록 되어 있는 것을 확인할 수 있습니다.

     다르게 말하자면 1번부터 15번까지 페이지랑 16번부터 30번까지 페이지의 html 주소가 다르지 않고 동일한 주소 내에서 1쪽, 2쪽, 3쪽… 이렇게 살펴볼 수 있게 되어 있습니다. 이건 js로 페이지 이벤트가 쓰여서 그렇습니다. 우리가 원하는 데이터가 있는 부분이 js로 쓰였다면 일반적인 웹크롤링과는 다른 작업이 필요합니다. Seleium을 이용해줘야 하거든요! Selenium이 무엇인지 궁금하다면, 역시 구글링을 통해 자세히 알아보시는 방법을 추천해드립니다. 찾아보지 않으실 분들은 대강 'js로 짜여진 데이터를 받아올 수 있구나' 정도로만 생각하시면 돼요.

     어쨌든 우리는 Selenium을 통해서 모든 1등 배출점의 정보를 받아올 거에요. 그 전에 for문을 얼마나 돌려야되는지, 그러니까 1등 배출점이 총 몇 쪽까지 있는지 살펴봐야 합니다.

     로또 홈페이지를 통해 확인하면 끝이 188쪽입니다. 간단하게 for (i in 1:188) { }과 같이 써줘도 되지만, 앞으로 판매점 정보는 계속 늘어날 것이고 그 과정에서 숫자가 188보다 더 커질 수 있으므로 188이라는 정수를 써주기 보다는 마지막 쪽 번호를 따내서 for문에 대입해주는 것이 바람직해보입니다. 마침 이 글의 코드를 짜고 있을 때는 분명 187쪽 밖에 없었는데, 어느새 한 쪽이 늘어났네요. 정수 대입보다 마지막 쪽수를 따오는 편이 귀찮기는 하지만 더 정확하다는 점을 아시겠죠? 먼저 그러면 마지막 쪽의 숫자인 188을 따오겠습니다. 188이라는 숫자를 따오는 방법은 html 태그를 따라 들어가는 위 방식과 똑같습니다. 얻어지는 숫자가 깨져서 나오기 때문에 하나로 숫자형으로 병합해주는 작업까지 진행하겠습니다.

    last <- link %>% 
      html_nodes('.paginate_common') %>% 
      html_nodes('a') %>% 
      html_attr('onclick') %>% 
      tail(1)
    
    end <- regmatches(last, gregexpr('[0-9]', last))
    end <- as.numeric(end[[1]])
    end <- as.numeric(paste(end[1], end[2], end[3], sep=""))
    end
    ## [1] 188

     end라는 변수에 188을 잘 담아왔습니다!

     다음으로는 RSelenium 라이브러리를 이용해서 188쪽까지의 모오든 1등 배출점 정보를 받아오도록 할게요. 중요한 건, 이 과정에서 시간이 조금 걸립니다. R마크다운으로 cmd를 조종할 수 있는 것도 모르겠어요. 따라서 저는 따로 코드를 실행하지는 않고 코드만 드리도록 하겠습니다. 여러분은 주석에 적어놓은 설명을 따라 직접 해보세요. 아울러 RSelenium과 관련해서는 아래 두 글에서 많은 도움을 얻었습니다. 정말 감사드리며 진행하면서 모르시는 부분이 있다면 아래 글을 확인해보세요!

    • RSelenium (셀레니움)을 이용한 동적 웹페이지 크롤링하기 (블로그 바로가기) / 작성자 : henry님
    • [withR]RSelenium을 이용한 웹스크래핑-텍스트 수집 (블로그 바로가기) / 작성자 : 모난 연구소님
    library(RSelenium)
    
    # 라이브러리를 실행시켰다면 C 드라이브 밑에 Rselenium이라는 폴더를 하나 만들어주세요. 
    # 그리고 cmd 창을 열고 루트를 C:\Rselenium로 바꿔줍니다.
    # 
    # cd C:\Rselenium
    # 
    # 루트를 바꿔줬다면 다음을 긁어서 실행시키세요.
    #
    # java -Dwebdriver.gecko.driver="geckodriver.exe" -jar selenium-server-standalone-3.141.59.jar -port 4445
    #
    # cmd 창은 계속 열어 두세요.
    
    # 다음으로 4445 포트를 크롬과 연결시켜주고 
    remDr <- remoteDriver(remoteServerAddr = "localhost",
                          port = 4445L,
                          browserName = "chrome")
    
    # 포트를 오픈하면 크롬창이 하나 뜰거에요. 
    remDr$open()
    
    # 로또 페에지로 그 크롬창을 연결해줍니다. 
    remDr$navigate("https://dhlottery.co.kr/store.do?method=topStoreRank&rank=1&pageGubun=L645")
    
    # 정보를 담을 빈 바구니를 하나 만들고, for문을 돌려주세요.
    # 시간이 다소 걸릴 수 있습니다.
    all_store = c() 
    
    for(i in 1:end) {  
      
      # 사용된 js 이벤트 구문을 숫자 i를 기준으로 쪼개서 합치기
      front  <- "selfSubmit(" 
      back   <- ")" 
      script <- paste(front, i, back, sep = '')
      
      # 이렇게 써주면 해당 이벤트가 실행합니다.
      pagemove <- remDr$executeScript(script, args = 1:2)
      
      # 페이지 소스 받아오기
      source   <- remDr$getPageSource()[[1]] 
      js_html  <- read_html(source)
      
      # for문이 위 작업을 1번 거치면 
      # read_html(source)는 위에서 작업한 link와 똑같습니다.
      # 즉, 1쪽의 데이터를 받아온 것과 같습니다.
      # for문이 돌아가면서 188쪽까지의 데이터를 all_store에 담아 오는 거에요.
      js_link <- html_nodes(js_html, css = 'tbody')
      stores  <- js_link %>%
        html_nodes('tr') %>% 
        html_nodes('td') %>% 
        html_text()
      
      all_store = c(all_store, stores) 
      
    }
    
    # 끝나면 Selenium을 꺼줍니다.
    # cmd창도 닫아주셔도 좋습니다.
    remDr$close

     이렇게 하면 all_store라는 변수에는 총 188쪽에 해당하는 1등 판매점 정보가 모두 담겼을 거에요. 이 다음은 쉽습니다. 위에서 진행했던 방식을 그대로 해주면 되죠. 저는 위에 Selenium을 실행시키지 않았기 때문에 all_store에 저장된 데이터가 없습니다. 따라서 이번에도 코드 실행은 하지 못합니다. 코드 실행 결과를 바로바로 보여드리면서 하고 싶지만 Selenium을 이용하느라 그러지 못하는 점이 많이 아쉽네요. 여러분은 직접 결과를 확인해보시길 바라요!

    all_store <- str_replace_all(all_store, "\t|\n|\r", "")
    all_store <- str_replace_all(all_store, "[[:space:]]{3,}", "")
    all_store <- str_replace_all(all_store, "[[:space:]]{2}", " ")

     다음으로 위에서 해준 배출점, 배출건수, 소재지만 뽑아내는 작업을 진행합니다.

    name <- NULL
    n <- NULL
    location <- NULL
    
    for (i in 1:length(all_store)) {
      if (i %% 5 == 2) {
        name <- c(name, all_store[i])
      } else if (i %% 5 == 3) {
        n <- c(n, all_store[i])
      } else if (i %% 5 == 4) {
        location <- c(location, all_store[i])
      }
    }
    
    all_store_df <- data.frame(name, n, location)

     이렇게 하면 웹크롤링으로 모든 로또 1등 판매점 정보를 가져오는 작업이 끝났습니다. 이어서 다음 글에서는 이 데이터를 가지고 지도를 그려보도록 하겠습니다. 마무리 하기 전에 다음에 웹크롤링 작업을 또다시 하기에는 너무 번거롭고 시간도 잡아먹으니까, 데이터 테이블을 csv파일로 저장해주고 끝내도록 할게요.

    write.csv(all_store_df, "store.csv",
              row.names = FALSE)

     고생하셨습니다. 다음 글에서 만나요!

    :)


Designed by Tistory.