[R 텍스트 마이닝] 1. 블로그 글 크롤링

수진·2020년 3월 1일
6

R

목록 보기
10/12
post-thumbnail

  안녕하세요! R로하는 텍스트 마이닝 포스팅을 해보려 합니다. 👏👏👏

  크롤링이나 텍스트마이닝 방법은 배운적이 있으나 제대로 써먹어 본 적은 없었기 때문에 이번에 하나의 주제를 가지고 크롤링>데이터 전처리>텍스트마이닝시각화 과정을 나누어 정리해 보겠습니다.

  주제는 최근 이슈인 코로나 19 바이러스 입니다. 이에 대한 최근 70개글을 블로그에서 모으고, 블로그 포스팅 내용에 대한 텍스트 마이닝을 해보도록 하겠습니다.

👩‍🔧패키지 라이브러리

  크롤링에서 필요한 패키지를 라이브러리 하였습니다. httrrvest는 크롤링에 필수적인 패키지 이고, jsonlitejson형식을 R에서 읽을 수 있도록 변환 해주는 기능이 있는 패키지 입니다.

library(httr)
library(rvest)
library(jsonlite)

👩‍🔧첫 페이지 테스트 크롤링

  저는 네이버 블로그의 글을 크롤링 해올 거에요. 총 70개의 글을 가지고 분석한다고 했는데 이유는, 제 컴퓨터의 성능이 매우 좋지 않아서 입니다😡😑. 네이버 블로그는 검색 시 한 페이지당 7개의 블로그 결과를 제출하는데 그냥 딱 10개 페이지 까지만 해보려구요.

  총 10개 페이지의 글을 모두 취합하기에 앞서, 일단 첫 페이지가 잘 긁어와지는지 테스트 합니다.

# GET 방식으로 요청을 보내보았습니다.
> httr::GET(url   = "https://section.blog.naver.com/ajax/SearchList.nhn",
+           query = list("countPerPage" = "7",
+                        "currentPage"  = 1,
+                        "endDate"      = "2020-03-01",
+                        "keyword"      = "코로나",
+                        "orderBy"      = "sim",
+                        "startDate"    = "2020-02-23",
+                        "type"         = "post"),
+           add_headers("referer" = "https://section.blog.naver.com/Search/Post.nh")) %>% 
+   print()

# 응답 결과, 상태가 200으로 정상적이고 콘텐츠 타입은 json입니다.
Response [https://section.blog.naver.com/ajax/SearchList.nhn?countPerPage=7&currentPage=1&endDate=2020-03-01&keyword=%EC%BD%94%EB%A1%9C%EB%82%98&orderBy=sim&startDate=2020-02-23&type=post]
  Date: 2020-03-01 06:28
  Status: 200
  Content-Type: application/json;charset=UTF-8
  Size: 16.6 kB
)]}',
{"result":{"searchDisplayInfo":{"authUrlType":"LOGIN","authUrl":"https://nid.naver.com/nidlogin.login?svctype=128&a_ve...

# json형식은 jsonlite 패키지의 fromJSON 함수를 사용하면 됩니다.
# 그런데 위 출력된 내용에서, {"result" 전에 )]}', 라는 특수문자가 있기 때문에
# 이 특수문자를 먼저 삭제해주어야 에러가 나지 않고 명령이 잘 먹힙니다.
# 아래와 같이 말입니다.
> data %>% 
>   httr::content(as = "text") %>% 
>   str_remove(pattern = '\\)\\]\\}\',') %>% 
>   jsonlite::fromJSON() 

  무튼, 1페이지 테스트가 이상이 없으니 for문으로 10페이지까지 크롤링 해보겠습니다.

👩‍🔧for문을 활용한 크롤링

  순서는 아래와 같습니다.

  1) 크롤링 데이터를 저장해줄 빈 df을 만든다.
  2) 네이버 블로그의 검색 결과를 크롤링한다. 단, 이때 페이지 내 글 내용을 긁어와지지 않는다. (검색 결과이므로)
  3) 따라서, 검색결과를 크롤링한 데이터를 토대로 또 각 블로그 게시글 내용을 크롤링한다.

크롤링 데이터를 저장해줄 빈 df을 만든다.

> Nblog <- c()

네이버 블로그의 검색 결과를 크롤링한다.

  네이버 블로그의 검색결과는 아래 이미지와 같이 제공하는 콘텐츠의 내용이 한정적입니다. 따라서 우선 검색 결과를 통해 글의 한정적인 정보를 먼저 취합하고, 이걸 통해서 다시 실제 블로그 게시물로 접근하여 콘텐츠 자체를 끌어야와 합니다.

> for (i in 1:10) {
+   httr::GET(url   = "https://section.blog.naver.com/ajax/SearchList.nhn",
+             query = list("countPerPage" = "7",
+                          "currentPage"  = i, # 페이지숫자를 i로 지정
+                          "endDate"      = "2020-03-01",
+                          "keyword"      = "코로나",
+                          "orderBy"      = "sim",
+                          "startDate"    = "2020-02-23",
+                          "type"         = "post"),
+             add_headers("referer" = "https://section.blog.naver.com/Search/Post.nh")) %>% 
+     httr::content(as = "text") %>% # text 형식으로 변환
+     str_remove(pattern = '\\)\\]\\}\',') %>% # 특수문자 제거
+     jsonlite::fromJSON() -> naverBlog # json을 R 데이터 프레임으로 변환
+   
+   data <- naverBlog$result$searchList # 크롤링 내용을 data에 먼저 저장
+   Nblog <- dplyr::bind_rows(Nblog, data) # 원래 만들어 두었던 Nblog에 계속해서 추가
+   
+   cat(i, "번째 페이지 정리 완료\n")
+   Sys.sleep(time = 3)
+ }
1 번째 페이지 정리 완료
2 번째 페이지 정리 완료
3 번째 페이지 정리 완료
4 번째 페이지 정리 완료
5 번째 페이지 정리 완료
6 번째 페이지 정리 완료
7 번째 페이지 정리 완료
8 번째 페이지 정리 완료
9 번째 페이지 정리 완료
10 번째 페이지 정리 완료

# Nblog에 잘 취합이 되었는지 dplyr::glimpse를 통해 데이터 구조를 확인합니다.
> dplyr::glimpse(Nblog)
Observations: 70
Variables: 13
$ blogId        <chr> "china_lab", "kimassi", "sorak123", "cuf1105", "kescomiri", "blood_info", "hackiss", "dgeduon",…
$ logNo         <dbl> 221829180324, 221828772426, 221825534630, 221827376797, 221822722642, 221824363270, 22182759666…
$ gdid          <chr> "90000003_0000000000000033A60CABA4", "90000003_0000000000000033A606724A", "90000003_00000000000…
$ postUrl       <chr> "https://blog.naver.com/china_lab/221829180324", "https://blog.naver.com/kimassi/221828772426",…
$ title         <chr> "中 우한 <strong class=\"search_keyword\">코로나</strong> 확진자, ‘봉쇄령’ 뚫고 베이징 이동 外", "<strong class=\"s…
$ noTagTitle    <chr> "中 우한 코로나 확진자, ‘봉쇄령’ 뚫고 베이징 이동 外", "코로나19, 중국내 한국인 차별딱지?", "코로나19 검사 음성→양성 왜 바뀔까?", "코로나 19 예방을…
$ contents      <chr> "中 우한 <strong class=\"search_keyword\">코로나</strong> 확진자, ‘봉쇄령’ 뚫고 베이징 이동 ⓒ상하이저널 중국 정부가 <strong …
$ nickName      <chr> "차이나랩", "별당마녀", "수학여우", "신협중앙회", "한국전기안전공사", "레드스토리", "메디포럼", "진실샘", "안산시", "공터", "대한민국 행정안전부",…
$ blogName      <chr> "CHINA LAB", "마녀와 찐빵", "호수옆길", "신협블로그", "한국전기안전공사", "헌혈로 나를 채우는 Red Storyㅣ대한적십자사", "살아가는 이야기", …
$ profileImgUrl <chr> "https://blogpfthumb-phinf.pstatic.net/MjAxNzAyMjdfOTcg/MDAxNDg4MTg2NzMzMzA4.nJcLCDVwojeI3heMUn…
$ addDate       <dbl> 1582874400000, 1582857720000, 1582687980000, 1582782900000, 1582534260000, 1582620840000, 15827…
$ thumbnails    <list> [<data.frame[4 x 3]>, <data.frame[4 x 3]>, <data.frame[4 x 3]>, <data.frame[4 x 3]>, <data.fra…
$ hasThumbnail  <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE,# 위 구조를 보고 필요한 열만 선택하고, 이후 편의를 위해 이름을 간단하게 바꾸어줍니다.
# 더불어 블로그 자체로 들어가 크롤링 하기 위한 url을 형식을 새로 만들었고
# 콘텐츠가 들어갈 빈 열도 만들어 주었습니다.
> Nblog <- Nblog %>% 
+   dplyr::select(1, 2, 4, 6, 9) %>% 
+   dplyr::rename(id      = blogId,
+                 no      = logNo,
+                 posturl = postUrl,
+                 title   = noTagTitle,
+                 name    = blogName) %>% 
+   dplyr::mutate(url        = stringr::str_glue("http://blog.naver.com/PostView.nhn?blogId={id}&logNo={no}"), # 블로그 게시글 크롤링을 위한 url
+                 contents   = NA) # 콘텐츠가 들어갈 열

# 다시 데이터 구조를 확인해서 잘 붙었는지 봅니다.
> dplyr::glimpse(Nblog)
Observations: 70
Variables: 7
$ id       <chr> "china_lab", "kimassi", "sorak123", "cuf1105", "kescomiri", "blood_info", "hackiss", "dgeduon", "cit…
$ no       <dbl> 221829180324, 221828772426, 221825534630, 221827376797, 221822722642, 221824363270, 221827596669, 22…
$ posturl  <chr> "https://blog.naver.com/china_lab/221829180324", "https://blog.naver.com/kimassi/221828772426", "htt…
$ title    <chr> "中 우한 코로나 확진자, ‘봉쇄령’ 뚫고 베이징 이동 外", "코로나19, 중국내 한국인 차별딱지?", "코로나19 검사 음성→양성 왜 바뀔까?", "코로나 19 예방을 위한 가…
$ name     <chr> "CHINA LAB", "마녀와 찐빵", "호수옆길", "신협블로그", "한국전기안전공사", "헌혈로 나를 채우는 Red Storyㅣ대한적십자사", "살아가는 이야기", "대구교육…
$ url      <glue> "http://blog.naver.com/PostView.nhn?blogId=china_lab&logNo=221829180324", "http://blog.naver.com/Po…
$ contents <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,

각 블로그 게시글 내용을 크롤링한다.

# 각 블로그 url을 활용하여 크롤링해오는 for문을 만들었습니다.
# for문은 에러발생시 작동을 멈추는데요, 이 때 tryCatch() 함수를 사용하면 작동을 멈추는 대신 다른 행동을 명령할 수 있습니다.
# for문을 돌려보니 몇 개 게시글에서 오류가 발생하여 tryCatch()를 추가해주었습니다.
> for(i in 1:nrow(Nblog)){
+   tryCatch({
+     Nblog$contents[i] <- httr::GET(url = Nblog$url[i]) %>% 
+     xml2::read_html() %>% 
+     rvest::html_nodes(css = "div.se-main-container")%>% 
+     html_text(trim = TRUE)
+     
+   cat(i, "번째 블로그 글 내용 취합 완료\n")
+   Sys.sleep(time = 3)
+   
+   }, error = function(e) cat(' --> 에러\n'))
+ }
1 번째 블로그 글 내용 취합 완료
2 번째 블로그 글 내용 취합 완료
3 번째 블로그 글 내용 취합 완료
4 번째 블로그 글 내용 취합 완료
5 번째 블로그 글 내용 취합 완료
6 번째 블로그 글 내용 취합 완료
7 번째 블로그 글 내용 취합 완료
8 번째 블로그 글 내용 취합 완료
9 번째 블로그 글 내용 취합 완료
10 번째 블로그 글 내용 취합 완료
11 번째 블로그 글 내용 취합 완료
 --> 에러 # <- 이렇게 에러가 생겼어요.
 --> 에러
14 번째 블로그 글 내용 취합 완료
15 번째 블로그 글 내용 취합 완료
...생략...
66 번째 블로그 글 내용 취합 완료
67 번째 블로그 글 내용 취합 완료
68 번째 블로그 글 내용 취합 완료
69 번째 블로그 글 내용 취합 완료
70 번째 블로그 글 내용 취합 완료

# 에러가 생긴 글 중 첫번째걸 들어가보니, 글 자체가 없는 것은 아니고 css가 다르더군요. 
# 그래서 다시 변경된 css로 돌려줍니다.
> which(is.na(Nblog$contents)) -> naData

# 위 for문을 고대로 복사해서 i에 대입될 문자만 변경해주었습니다.
> for(i in naData){
+   tryCatch({
+     Nblog$contents[i] <- httr::GET(url = Nblog$url[i]) %>% 
+       xml2::read_html() %>% 
+       rvest::html_nodes(css = "div#postViewArea")%>% 
+       html_text(trim = TRUE)
+     
+     cat(i, "번째 블로그 글 내용 취합 완료\n")
+     Sys.sleep(time = 3)
+     
+   }, error = function(e) cat(' --> 에러\n'))
+ }
12 번째 블로그 글 내용 취합 완료
13 번째 블로그 글 내용 취합 완료
23 번째 블로그 글 내용 취합 완료
 --> 에러
 
# 또 에러가 떠서 얘도 css가 다른지 들어가보았더니 그냥 그사이에 비공개로 바뀌었더군요.
# 당연히 비공개 글은 크롤링할 수 없습니다.
# 그래서 이 데이터는 삭제했습니다.
> na.omit(Nblog) -> Nblog

  아래 그림과 같이 Nblog에 데이터들이 저장되었습니다.

👩‍🔧RData 저장

  여기서 작업을 멈추고 다음에 나머지를 할경우, R Data 파일로 저장해 주어야 합니다. 그냥 코드만 저장하면 다시 돌려주어야 하는데, 그 사이에 블로그 글들이 삭제되거나 크롤링에 필요한 요소들이 바뀔 수 있기 때문입니다.
  CSV나 엑셀, text로 저장도 가능하지만, 추후 작업 용도로는 R Data가 좀 더 편리합니다.

> save(Nblog, file="Nblog.R")

👩‍🔧마무리

  이렇게 크롤링을 쭉 하다보면 생각보다 시간이 오래 걸립니다. ㅠ_ㅠ 따라서 크롤링 할 일이 많다면! 사용자 함수로 저장하는 것도 아주 좋은 방법이에요. 😃 그럼 이만 마무리 하겠습니다. 😃

profile
안녕하세요! 이것저것 하고 있습니다 👩‍🔧

2개의 댓글

comment-user-thumbnail
2020년 5월 13일

안녕하세요! 수업 과제를 하면서 도움을 얻고자 방문했어요. 정보 주셔서 감사합니다. 질문이 있는데요~
초반에 특수문자를 삭제하기 위해서 알려주신 코드대로
data %>%
httr::content(as = "text") %>%
str_remove(pattern = '\)\]\}\',') %>%
jsonlite::fromJSON()
와 같이 돌렸는데 아래와 같은 오류가 뜹니다.
Error in httr::content(., as = "text") : is.response(x) is not TRUE
해결방안이 있을까요? ㅠㅠ

답글 달기
comment-user-thumbnail
2021년 10월 21일

안녕하세요! 현재 커뮤니티 글을 크롤링 하는 과정 중 어려움에 있어 질문드리고 싶어 댓글 남깁니다!
혹시 Cat(i, ~~~) ~~이 부분의 내용은 어느 것을 보고 작성해야 하는지 궁금합니다!

답글 달기