호돌찌의 AI 연구소

2019년에 Crawling 공부하면서 정리를 했었던 내용의 일부입니다.

 

Contents

 

JavaScript 사용된 파일 찾기 Step


  1. 수집하려는 데이터를 포함하는 웹 페이지 찾기
  1. 크롬 개발자도구 Network 탭의 'Doc'로 이동하여 관련 파일 찾기
    • 파일을 하나씩 클릭하여 오른쪽에 출력되는 'Response'에서 찾는 문자열을 검색
  1. 찾는 내용이 'Doc'에 없으면 'XHR'로 이동하여 계속 찾기
    • 파일을 하나씩 클릭하여 오른쪽에 출력되는 'Preview'에서 찾는 문자열을 검색
  1. 'XHR'에서 원하는 내용 찾으면 JSON 형태로 제공될 가능성이 높습니다.
    • jsonlite 패키지의 fromJSON() 함수를 이용해 관련 데이터 추출하여 정리.
  2.  

날짜데이터 활용 - 매우 기초


crawling하는데 있어서 크게 lubridate 패키지를 많이 사용하지는 않는 편이다. 기본 base() 로 커버가 어느정도 가능하다.

 

현재 날짜 반환

> Sys.Date()
[1] "2019-09-26"

현재 날짜 속성 확인

> Sys.Date() %>% class()
[1] "Date"

현재 날짜를 숫자 값으로 변환, 1970-01-01로 부터의 누적일 수가 반환됨

> Sys.Date() %>% as.numeric()
[1] 18165

현재 날짜를 특정 형태로 변환하려면 format() 함수 사용

> Sys.Date() %>% format(format = '%Y.%m.%d')
[1] "2019.09.26"

> # 다른 형태로 바꿀 수 있다. 
> Sys.Date() %>% format(format = '%Y/%m/%d')
[1] "2019/09/26"

> # '%Y' 대신 '%y'를 지정하면 연도 자리수가 바뀜
> Sys.Date() %>% format(format = '%y/%m/%d')
[1] "19/09/26"

> # 순서를 바꿀 수 있음 
> Sys.Date() %>% format(format = '%m/%d/%y')
[1] "09/26/19"

Date 객체에 정수를 더하면 미래 일자, 빼면 과거 일자 반환

> Sys.Date() + 30
[1] "2019-10-26"

> Sys.Date() - 30
[1] "2019-08-27"

문자열을 날짜로 변환

> as.Date(x = '2019-08-31')
[1] "2019-08-31"
 
> # 문자열의 형태가 기본형이 아닌 경우 `format` 인자에 지정
> as.Date(x = '2019.08.31', format = '%Y.%m.%d')
[1] "2019-08-31"

> # 문자열의 형태가 한글이 포함된 경우도 가능
> as.Date(x = '2019년 08월 31일', format = '%Y년 %m월 %d일')
[1] "2019-08-31"

'x' 인자에 숫자 0을 할당하면 'origin'인자에 지정된 날짜가 반환

origin으로 부터 0일 경과된 날 제시

> as.Date(x = 0, origin = '2019-01-01')
[1] "2019-01-01

생일을 입력해서 지금까지 살아온 일 수 계산 가능

> Sys.Date() - as.Date(x = '1993-02-03')
Time difference of 9731 days

 

날짜 데이터 활용 - 크롤링에 활용하기위해 날짜 벡터 생성


반복문에 사용할 날짜 벡터를 생성할 일이 종종 있다.

 

시작일자와 종료일자를 각각 지정 후 1, 2일 간격으로 생성

> bgnDate <- as.Date(x = '2019-01-01') # 시작일
> endDate <- Sys.Date() # 종료일
 
> seq(from = bgnDate, to = endDate, by = '1 day')
  [1] "2019-01-01" "2019-01-02" "2019-01-03" "2019-01-04"
  [5] "2019-01-05" "2019-01-06" "2019-01-07" "2019-01-08"
  [9] "2019-01-09" "2019-01-10" "2019-01-11" "2019-01-12"
# 중략

> seq(from = bgnDate, to = endDate, by = '2 day')
  [1] "2019-01-01" "2019-01-03" "2019-01-05" "2019-01-07"
  [5] "2019-01-09" "2019-01-11" "2019-01-13" "2019-01-15"
  [9] "2019-01-17" "2019-01-19" "2019-01-21" "2019-01-23"
# 중략

1주일, 1달, 1분기 간격으로 생성 역시 가능

> seq(from = bgnDate, to = endDate, by = '1 week')
 [1] "2019-01-01" "2019-01-08" "2019-01-15" "2019-01-22"
 [5] "2019-01-29" "2019-02-05" "2019-02-12" "2019-02-19"
 [9] "2019-02-26" "2019-03-05" "2019-03-12" "2019-03-19"
[13] "2019-03-26" "2019-04-02" "2019-04-09" "2019-04-16"
# 중략

> seq(from = bgnDate, to = endDate, by = '1 month')
[1] "2019-01-01" "2019-02-01" "2019-03-01" "2019-04-01" "2019-05-01"
[6] "2019-06-01" "2019-07-01" "2019-08-01" "2019-09-01"

> seq(from = bgnDate, to = endDate, by = '1 quarter')
[1] "2019-01-01" "2019-04-01" "2019-07-01"

이를 바탕으로 웹페이지가 요구하는 대로 조금씩 format을 바꾸면서 다루면 된다.

하나만 테스트 해본다면

> dates <- seq(from = bgnDate, to = endDate, by = '1 day')

> for (date in dates) {
+   date <- date %>% as.Date(origin = '1970-01-01') %>% format(format = '%Y%m%d')
+   print(x = date)
+ } 

[1] "20190101"
[1] "20190102"
[1] "20190103"
[1] "20190104"
[1] "20190105"
[1] "20190106"
[1] "20190107"
# 후략

 

네이버 블로그 날짜별로 수집


네이버 블로그가 상당히 편집기도 다양한 것을 써서 구조가 다양해 긁기가 꽤나 어려움

수집과정은 다음과 같이 정리할 수 있다.

  1. 검색어와 조회일자를 입력하면 해당 블로그 수를 반환하는 함수를 생성(4000개 이상은 불가능)
  1. 검색어와 조회일자 및 페이지 번호를 입력 → 해당 블로그 데이터 수집 → 데이터프레임으로 반환하는 함수 생성. (검색어 입력시 페이지당 n개)
  1. 1번에서 생성한 함수와 2번에서 생성한 함수를 조합하여 검색어와 조회일자에 해당하는 모든 블로그를 수집하는 함수를 생성
  1. 검색어와 조회구간을 설정하면 조회구간에 해당하는 날짜 벡터를 생성하고, 3번 함수로 반복문을 실행하여 조회 구간 전체 블로그 데이터를 수집

 

네이버 블로그 날짜별로 수집 실습

 

어떤 블로그를 수집을 할 지 과정 및 사이트 진단.

  1. 네이버 블로그 메인 페이지 → 검색어 지정 → 기간(조회시작일자, 조회종료일자) 선택
  1. 크롬개발자도구 → 새로고침 → Network Tab → Doc 이동 → 파일 하나도 없는 것을 확인
  1. XHR 탭으로 이동 → 파일들 하나씩 클릭 → 'SearchList.nhn'으로 시작하는 파일 확인
  1. Preview확인 하니 JSON 형태로 데이터를 제공하고 있음

 

필요한 패키지 부르기

> library(tidyverse)
> library(httr)
> library(jsonlite)
> library(rvest)

 

Step1. 검색어와 조회시작일자, 조회종료일자 입력했을 때 조건에 맞는 블로그의 수를 반환하는 사용자 함수 생성

getBlogCnt <- function(searchWord, bgnDate, endDate) { 
  
  # HTTP 요청
  res <- GET(url = 'https://section.blog.naver.com/ajax/SearchList.nhn', 
             query = list(countPerPage = 7, 
                          currentPage = 1,
                          startDate = bgnDate, 
                          endDate = endDate, 
                          keyword = searchWord), 
             add_headers(referer = Sys.getenv('NAVER_BLOG_REF')))
  
  # 응답 바디에 있는 ")]}',"을 제거하고 fromJSON() 함수에 할당 
  res %>% 
    content(as = 'text') %>% 
    str_remove(pattern = "\\)\\]\\}\\',") %>% 
    fromJSON() -> json 
  
  # 블로그 수를 추출한 다음 숫자 벡터로 변환합니다. 
  totalCount <- json$result$totalCount %>% as.numeric()
  
  # 결과를 반환합니다. 
  return(totalCount)
  
}

 

오늘 날짜로 테스트를 해봅니다.

> getBlogCnt(searchWord = '자연어처리', 
+            bgnDate = Sys.Date(), 
+            endDate = Sys.Date())
[1] 16

 

Step2. 검색어, 조회시작일자, 조회종료일자, 페이지수를 입력하면 조건에 맞는 블로그 데이터를 데이터프레임으로 반환하는 사용자 정의 함수 생성

> getBlogDf <- function(searchWord, bgnDate, endDate, page = 1) {
+   
+   # HTTP 요청
+   res <- GET(url = 'https://section.blog.naver.com/ajax/SearchList.nhn', 
+              query = list(countPerPage = 7, 
+                           currentPage = page, 
+                           startDate = bgnDate, 
+                           endDate = endDate, 
+                           keyword = searchWord), 
+              add_headers(referer = Sys.getenv('NAVER_BLOG_REF'))) # 따로 리퍼러를 제가 설정을 했습니다. 
+   
+   # 응답 바디에 있는 ")]}',"을 제거하고 fromJSON() 함수에 할당 
+   res %>% 
+     content(as = 'text') %>% 
+     str_remove(pattern = "\\)\\]\\}\\',") %>% 
+     fromJSON() -> json 
+   
+   # 데이터프레임을 출력
+   df <- json$result$searchList
+   
+   # 필요한 컬럼만 남기고 일부 컬럼명을 변경 
+   df %>% 
+     select(blogId, postUrl, noTagTitle, nickName, blogName, addDate) %>% 
+     rename(link = postUrl, 
+            title = noTagTitle, 
+            date = addDate) -> df 
+   
+   # date 컬럼에 날짜 대신 숫자 벡터로 들어 있으므로 1000으로 나눈 후, 
+   # POSIXct로 속성을 바꿔주기
+   df$date <- as.POSIXct(x = df$date / 1000, origin = '1970-01-01') # 년 월 일 시 분 초 
+   
+   # 결과 반환
+   return(df)
+   
+ }

 

오늘 날짜로 테스트 해보기

> df <- getBlogDf(searchWord = '자연어처리', 
+                 bgnDate = Sys.Date(), 
+                 endDate = Sys.Date(), 
+                 page = 1)

> View(df)

 

 

Step3. 1, 2 단계에서 사용한 함수를 이용해서 모든 블로그 데이터를 수집하는 함수를 생성

> getAllBlogDf <- function(searchWord, bgnDate, endDate) {
+   
+   # 조건에 맞는 블로그 수를 가져오기
+   blogCnt <- getBlogCnt(searchWord = searchWord, 
+                         bgnDate = bgnDate, 
+                         endDate = endDate)
+   
+   # 페이지 수를 계산
+   pages <- ceiling(x = blogCnt / 7)
+   
+   # 블로그 수와 페이지 수를 출력
+   cat('> 블로그 수는', blogCnt, '& 페이지 수는', pages, '입니다.\n')
+   
+   
+   # 만약 블로그의 수가 0이면 아래 라인을 실행하지 않게 설정 
+   if (blogCnt >= 1) {
+     
+     # 최종 결과 객체를 빈 데이터프레임으로 생성
+     result <- data.frame()
+     
+     # 반복문 실행
+     for (page in 1:pages) {
+       
+       # 현재 진행상황 출력
+       cat('>> 현재', page, '페이지 실행 중입니다.\n') 
+       
+       # 해당 페이지의 블로그 데이터를 수집한 다음 df에 할당
+       df <- getBlogDf(searchWord = searchWord, 
+                       bgnDate = bgnDate, 
+                       endDate = endDate, 
+                       page = page) 
+       
+       # 최종 결과 객체에 추가 
+       result <- rbind(result, df)
+       
+       # 1초간 멈추기
+       Sys.sleep(time = 1)
+       
+     }
+     
+     # 최종 결과 반환
+     return(result) 
+     
+   } 
+ }

오늘 날짜로 테스트해보자

> blog <- getAllBlogDf(searchWord = '자연어처리', 
+                      bgnDate = Sys.Date()-1, 
+                      endDate = Sys.Date()-1)
> 블로그 수는 16 & 페이지 수는 3 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.
>> 현재 3 페이지 실행 중입니다.

> view(blog)
> dim(blog)
[1] 16  6

 

 

Step4. 검색어와 수집기간을 정해 블로그 요약데이터와 링크를 수집해보자.

> # 검색어 설정
> searchWord <- '자연어처리'

> # 조회시작일과 조회종료일을 설정 
> bgnDate <- as.Date(x = '2019-09-21')
> endDate <- as.Date(x = '2019-09-27')

> # 조회시작일과 조회종료일로부터 벡터 생성
> dates <- seq(from = bgnDate, to = endDate, by = '1 day')
> # dates 객체를 출력
> print(dates)
[1] "2019-09-21" "2019-09-22" "2019-09-23" "2019-09-24" "2019-09-25"
[6] "2019-09-26" "2019-09-27"

> # 최종 결과 객체를 빈 데이터프레임으로 생성
> blogAll <- data.frame() # 쌓을 것

> # 반복문 실행
> for (date in dates) {
+   
+   # 반복문 실행 중 에러 발생시 다음으로 건너뛰게
+   tryCatch({
+     
+     # 반복문 안에서 date가 날짜 벡터에서 숫자 벡터로 자동 변환되기 때문에  
+     # 날짜 벡터로 강제 변환해주어야함다. 
+     date <- date %>% as.Date(origin = '1970-01-01')
+     
+     # 현재 진행상황 출력
+     date4print <- format(x = date, format = '%Y년 %m월 %d일에')
+     cat('현재', date4print, '등록된 블로그를 수집하고 있습니다.\n')
+     
+     # 해당 일자에 등록된 모든 블로그를 수집
+     blog <- getAllBlogDf(searchWord = searchWord,
+                          bgnDate = date, 
+                          endDate = date) 
+     
+     # 최종 결과 객체에 추가
+     blogAll <- rbind(blogAll, blog) 
+     
+     # 개행을 추가
+     cat('\n')
+     
+     # 1초간 멈춥니다. 
+     Sys.sleep(time = 1)
+     
+   }, error = function(e) '--> 에러가 발생하여 건너뜁니다!\n')
+   
+ }
현재 2019년 09월 21일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 11 & 페이지 수는 2 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.

현재 2019년 09월 22일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 20 & 페이지 수는 3 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.
>> 현재 3 페이지 실행 중입니다.

현재 2019년 09월 23일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 20 & 페이지 수는 3 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.
>> 현재 3 페이지 실행 중입니다.

현재 2019년 09월 24일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 22 & 페이지 수는 4 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.
>> 현재 3 페이지 실행 중입니다.
>> 현재 4 페이지 실행 중입니다.

현재 2019년 09월 25일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 17 & 페이지 수는 3 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.
>> 현재 3 페이지 실행 중입니다.

현재 2019년 09월 26일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 16 & 페이지 수는 3 입니다.
>> 현재 1 페이지 실행 중입니다.
>> 현재 2 페이지 실행 중입니다.
>> 현재 3 페이지 실행 중입니다.

현재 2019년 09월 27일에 등록된 블로그를 수집하고 있습니다.
> 블로그 수는 3 & 페이지 수는 1 입니다.
>> 현재 1 페이지 실행 중입니다.

구조를 파악해보자

> str(blogAll)
'data.frame':	107 obs. of  6 variables:
 $ blogId  : chr  "numerouno" "numerouno" "thedreamproject" "ak_2010" ...
 $ link    : chr  "https://blog.naver.com/numerouno/221655288760" "https://blog.naver.com/numerouno/221655284980" "https://thedreamproject.blog.me/221655270067" "https://blog.naver.com/ak_2010/221654737524" ...
 $ title   : chr  "공공서비스 특화 챗봇 시스템(자료:중소기업기술정보진흥원,중소기업기술로드맵 2019-2021)" "Robotic Process Automation(RPA)(자료:중소기업기술정보진흥원,중소기업기술로드맵 2019-2021)" "딥러닝 공부하는 재미" "'인공지능 강국' 미국이 중국을 뛰어넘는 의외의 배경" ...
> # 후략

 

Step5. 정말 골치 아픈 블로그 내용물 긁어보기

iframe 태그로 복잡하게 되어 있기 때문에 다 각각 긁는데 방법이 달라 문제가 있음.

정규표현식을 조금 써야하기 때문에 어느 정도 지식을 갖추고 있어야합니다.

또한 Step1~4에서 했던 데이터들을 바탕으로 이용할 예정

> # iframe url에 필요한 logNo 컬럼을 생성 
> # 뒤에 있는 숫자들을 이용해서 로그 넘버를 따온다는 뜻. 즉 blog.naver.com~~ 이후 숫자들을 이야기 하는 것이다. 
> blogAll$logNo <- blogAll$link %>% str_extract(pattern = '\\d+$')
> 
> # iframe url을 조립하여 link 컬럼에 덮어씌우기
> blogAll$link <- str_c('https://blog.naver.com/PostView.nhn', 
+                       '?blogId=', blogAll$blogId, 
+                       '&logNo=', blogAll$logNo) 
> 
> # 블로그 본문을 저장할 컬럼을 생성
> blogAll$body <- NA

 

반복문 실행하기, 분량은 적절히 눈치껏!

 

> for (i in 1:nrow(x = blogAll)) {
+   
+   # 현재 진행상황을 출력
+   cat('현재', i, '번째 블로그 본문 수집 중!\n')
+   
+   # 반복문 실행 도중 에러가 발생하면 실행을 건너뛰게~
+   tryCatch({
+     
+     # HTTP 요청 실행
+     res <- GET(url = blogAll$link[i])
+     
+     # 본문을 포함하는 CSS Selector를 출력
+     # css에 대한 경우의 수 3가지 방안을 마련해 놓기(네이버 블로그 편집기가 다양한데 3개 정도 패턴이 있는 듯)
+     res %>% 
+       content(as = 'text') %>% 
+       str_extract(pattern = 'se-main-container|__se_component_area') -> css # 패턴에 or을 삽입
+     
+     if (is.na(x = css)) {
+       css <- 'div#postViewArea' 
+     } else {
+       css <- str_c('div.', css)
+     }
+     
+     # 본문 저장
+     res %>% 
+       read_html(options = 'HUGE') %>% # HUGE 안 집어넣으면 에러가 날 수 있음!
+       html_node(css = css) %>% 
+       html_text(trim = TRUE) %>% 
+       str_remove_all(pattern = '[\n\r\t]+') %>% # 블로그 포스트가 지저분한데, 줄바꿈, 탭 등 or 조건으로 무제한 다 제거
+       str_remove_all(pattern = '( ){2,}') -> blogAll$body[i] # ( ) 공백 {2,} 2개 이상 다 쳐내기 공백 1개만 허용.
+     
+     # 1초간 쉽니다. 
+     Sys.sleep(time = 1)
+     
+   }, error = function(e) cat('  --> 본문이 없습니다!\n'))
+   
+ }

현재 1 번째 블로그 본문 수집 중!
현재 2 번째 블로그 본문 수집 중!
현재 3 번째 블로그 본문 수집 중!
현재 4 번째 블로그 본문 수집 중!
현재 5 번째 블로그 본문 수집 중!
현재 6 번째 블로그 본문 수집 중!
현재 7 번째 블로그 본문 수집 중!
# --- 후략
현재 103 번째 블로그 본문 수집 중!
현재 104 번째 블로그 본문 수집 중!
현재 105 번째 블로그 본문 수집 중!
현재 106 번째 블로그 본문 수집 중!
현재 107 번째 블로그 본문 수집 중! 

 

본문이 NA가 있는지 확인을 해보자

> loc <- which(x = is.na(x = blogAll$body)) 
> print(x = loc) # 만약 loc에서 na가 탐지되면 css를 또 찾아가야한다
integer(0) # 다행히 없는 것을 알 수 있다. 

 

내용물을 확인해 보자

> head(blogAll$body,2)
[1] "공공서비스 특화 챗봇 시스템1. 기술로드맵2. 개요가. 정의 및 필요성정의 : 챗봇은 채팅(chatting)과 로봇(robot)을 결합 한 표현이며 문자 또는 음성 기반 대화 방식으로 정보처리를 하는 시스템을 의미범위 : 날씨, 교통, 일정 등에 대한 질의응답을 하는 가상도우미부터 사용자의 패턴 분석을 통한 서비스 제공까지 그 활용범위가 점점 확대 중나. 주요 제품[ 제품분류 관점의 범위 ]전략품목제품분류 관점세부기술공공서비스 특화 챗봇 시스템기반기술패턴인식 기술- 기계에 의하여 도형, 문자, 음성 등을 식별시키는 것자연어처리 기술- 정보검색, 질의응답, 시스템 자동변역, 통역 등이 포함음성인식 및 발화 기술- 특징추출기술, 발음변환 기술, 운율제어기술 ...
[2] "Robotic Process Automation(RPA)1. 기술로드맵2. 개요가. 정의 및 필요성정의 : Robotic Process Automation(RPA)는 기본적으로 사람이 하는 표준화되어 있고 규칙에 기반한 업무를 컴퓨터가 자동적으로 할 수 있도록 전환하는 프로세스를 의미범위 : 프로세스나 작업의 유형에 따라 데이터를 처리하는 프로봇(Probots), 데이터를 수집하고 저장하는 노우봇(Knowbots) , 실시간으로 고객 질의에 답하는 가상 비서인 챗봇(Chatbots)로 분류 가능나.
## 후략 .....

 

결론

  1. javascript로 되어있을 때 table로 되어 있는 것 긁어다 쓰면 편하다
  1. 하지만 내용물 뽑는 것은 예외들이 있어서 여러 케이스들에 대해서 뽑는 일을 해봐야한다.

 

 

<Copyright 2019. @hotorch. All rights reserved.>

 

 

'Programming > R' 카테고리의 다른 글

R 정규표현식 기본문법  (0) 2021.07.17
[Crawling] R stringr 패키지 사용법  (0) 2021.07.06
[Crawling] RSelenium  (0) 2021.07.02
[Crawling] XML, JSON in R  (0) 2021.06.17
[Crawling] 용어 다지기 및 Tutorial  (0) 2021.06.05
profile

호돌찌의 AI 연구소

@hotorch's AI Labs

포스팅이 도움이 되셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!