Search

2018년 2월 18일 일요일

[Analysis] [R] Text Analysis 맛보기(1) - Google News Crawler 만들기

#1. 들어가기
가끔 세상이 어떻게 돌아가는지 알아보기 위해 구글링을 할 때가 종종 있다.
그런데, 수백 수천건의 뉴스기사를 시간순으로 정렬해 일일히 눈으로 확인하는 것은 몹시 피곤한 일이다. 그래서 이번에는 필자 대신 뉴스기사를 긁어 모아서, 얼마나 많은 단어들이 사용되었는지 확인할 수 있는 아주 간단한(?) 텍스트 분석 도우미를 만들어보고자 한다.

본 시리즈는 총 2편으로 구성할 계획이며,
1편에서는 Google News Crawler를 R로 만들어 뉴스기사를 스크랩할 예정이고,
2편에서는 KoNLP 패키지를 사용해 간단하게 word count를 구현해보고자 한다.
(추가적으로, 시간이 된다면 파이썬 버전도 만들어볼 계획)

#2. 구글 뉴스 크롤러 만들어보기
구글 뉴스 크롤러를 만들기 위해 사용되는 패키지는 총 4개로, 개발자마다 다르게 사용할 수도 있다. 또한, 다른 코드가 더 맘에 든다면 얼마든지 변경할 수 있으며, 독자분들 중에 더 효율적인 라이브러리나 코드를 찾았다면 댓글 부탁드립니다.
사용할 라이브러리는 httr, xml2, stringr, lubridate인데, 가급적 인스톨 패키지를 통해 최신으로 업데이트 하는 것을 추천한다. (특히 lubridate의 경우, 하위 버전에서 동작하지 않는 타입을 확인했음)

작성한 코드는 아래와 같으며, keyWord와 numOfPost를 변경하여 가볍게 사용해볼 수 있게 만들어두었다. 또한, 맨 하단에 csv로 내려받는 코드를 넣어두었는데, 저장 위치를 변경해서 쉽게 다운로드할 수 있다.

#########################################
# GOOGLE NEWS CRAWLER - v1.0
# - martinpark
# - ruserive@gmail.com
#########################################

# load library and user function ------------------------------------------
# install.packages(c("httr","xml2","stringr","lubridate"))
library(httr)
library(xml2)
library(stringr)
library(lubridate)
unescape_html <- function(str){
  xml2::xml_text(xml2::read_html(paste0("<x>", str, "</x>")))
}

# control keyword and number of posts -------------------------------------
# 검색어
keyWord     <- "쇼트트랙"

# 검색 뉴스 수 (너무 많을 경우, 블락당할 수 있음)
numOfPost   <- 300

# google news crawler main part -------------------------------------------
# 키워드 변환
keyWordUTF8 <- URLencode(iconv(keyWord,to='utf8'))

# 크롤링 페이지 분할
startPosts    <- seq(1,numOfPost,100)
startPosts[1] <- startPosts[1] - 1
if (numOfPost%%100==0) {
  endPosts      <- rep(100,numOfPost/100)
} else {
  endPosts      <- c(rep(100,floor(numOfPost/100)),numOfPost%%100)
}

# 결과셋 생성
result      <- data.frame(stringsAsFactors=FALSE)

for (i in seq(length(startPosts))) {
  # 뉴스 가져오기
  url         <- paste0("https://www.google.co.kr/search?hl=ko&q=",keyWordUTF8,"&tbm=nws&start=",startPosts[i],"&num=",endPosts[i])
  doc         <- content(GET(url), "text")
  # doc         <- readLines(url,warn=FALSE)
  doc_table   <- str_split(doc,"<table><tr>")
  
  for(p in seq(endPosts[i])+1) {
    # 뉴스 정제
    doc_tab     <- str_split(doc_table[[1]][p],"</div>")
    doc_tab2    <- str_split(doc_tab[[1]][1],"</a></h3>")
    
    # 뉴스 제목
    title       <- unescape_html(doc_tab2[[1]][1])
    
    # 뉴스 기사
    article     <- unescape_html(gsub("&nbsp;..."," (중략)...",doc_tab[[1]][2]))
    
    # 뉴스 제공업체
    supplier    <- str_split(unescape_html(doc_tab2[[1]][2])," - ")[[1]][1]
    
    # 뉴스 URL
    news_url    <- URLdecode(str_sub(doc_tab2[[1]][1]
                                     ,str_locate(doc_tab2[[1]][1],"\"/url")[2]+4
                                     ,str_locate(doc_tab2[[1]][1],"&")[1]-1))
    
    # 뉴스 게시 시간
    tmp_time    <- str_split(unescape_html(doc_tab2[[1]][2])," - ")[[1]][2]
    if(length(grep(" 전",tmp_time))==0){
      post_time <- as_date(tmp_time)
    } else {
      post_time <- lubridate::ymd_hms(Sys.time())
      num       <- as.integer(gsub("[^0-9]","",tmp_time))
      unit      <- gsub("([0-9]*)| 전","",tmp_time)
      if(unit=="일"){
        lubridate::day(post_time) <- lubridate::day(post_time) - num
      } else if(unit=="시간"){
        lubridate::hour(post_time) <- lubridate::hour(post_time) - num
      } else if(unit=="분"){
        lubridate::minute(post_time) <- lubridate::minute(post_time) - num
      } else if(unit=="초"){
        lubridate::second(post_time) <- lubridate::second(post_time) - num
      }
      post_time <- as_date(post_time)
    }
    
    # 데이터프레임화
    res         <- data.frame(keyword=keyWord
                              ,title=title
                              ,article=article
                              ,supplier=supplier
                              ,url=news_url
                              ,time=post_time
                              ,stringsAsFactors=FALSE)
    
    result      <- rbind(result, res)
  }
  print(paste0("Done..! (",nrow(result),"/",numOfPost,")"))

  # 빠르게 자주 사용하면 블락당함..
  Sys.sleep(3)
}

# 완성 데이터 확인
# View(result)

# csv 추출
write.csv(result, file.path("C:\\Users\\Martin\\Desktop\\google_news.csv"),row.names=FALSE,fileEncoding="CP949")

#3. 크롤링 결과 확인
더 필요한 정보가 있다면 구조를 변경하여 추가해도 되지만, 우선은 간단하게 이 정도만 크롤링 해보는 것으로 이번 장을 마칩니다.



댓글 7개:

  1. 안녕하세요! 좋은정보 정말 감사합니다.
    질문이 있는데 저코드를 돌리고 csv파일에 들어가 봤더니 날짜부분이 나오지 않습니다. 그리고 200번대 이후로는 NA라는 값이 나오는데 혹시 이유와 해결방안을 알 수 있을까요??

    답글삭제
    답글
    1. 안녕하세요, 글 잘 읽어주셔서 감사합니다.
      최근에 제가 좀 바빠서(?) 블로거에 글도 잘 못쓰고, 답변도 못해드렸네요.. 안그래도 해당 문제가 코드에 있나? 하고 점검해본 결과, 구글 자체의 문제로 보여집니다. 저도 이번에 알아차린건데, 어떤 키워드를 넣고 검색을 해봐도 100~200개 정도 검색된 이후에는 구글 자체에서 더이상 검색 결과를 띄우지 않는다는 것을 확인했습니다. 다음번에 코드를 짤 때에는 네이버 같은데에서 크롤링을 해야겠네요.. ㅎㅎ
      감사합니다.

      삭제
  2. 안녕하세요 다음과 같은 문제가 혹시 발생하는데 이유를 아실까요!
    No encoding supplied: defaulting to UTF-8.

    답글삭제
    답글
    1. 1. httr과 xml2의 인코딩을 확인해주시고, 2. Rstudio의 인코딩을 확인해주세요~

      삭제
  3. 안녕하세요, 글 잘 보았는데 지금은 Google 뉴스 크롤링 자체가 막힌건가요? 저는 1 ~ 300 전부다 NA로 뜨네요.. !

    답글삭제
    답글
    1. 음.. 제가 최근에는 R쓸일이 거의 없었어서 실행은 해보지 않았는데.. 전부 NA인거면, html 주소를 제대로 가져왔는지 한번 다시 확인해보시겠어요?

      삭제
    2. 주소가 잘못되었다는게 아니라, url을 통해 얻은 doc변수에 제대로 받아오는지 확인이요!

      삭제