본문 바로가기
Data Science/R 텍스트마이닝

텍스트마이닝 - R을 활용한 웹 크롤링 및 단어 연관 분석 (KoNLP) :: Data 쿡북

by 쿡북 2017. 1. 8.

2017.1.9 춥고 흐림. 


수정사항 : 2017-08-18, 인코딩 관련 소스라인 추가

               2017-09-11, 텍스트 마이닝 python korea 2017 에서 발표된 명사 추출 관련 자료 link , 데이터 기반의 명사 추출 기법

                               https://www.slideshare.net/kimhyunjoonglovit/pycon2017-koreannlp, 

| 들어가며 

오늘은 R을 이용해서 웹 데이터를 크롤링하고, 수집된 텍스트를 기반으로 연관 분석을 하는 과정을 공유할까 한다.

참고로 웹 크롤링은 웹 사이트가 빈번하기 바뀌기 때문에 작성하는 현 시점기준의 스크립트임을 밝힌다. 

혹 크롤링 대상 사이트에 변경이 있을 경우 해당 부분의 수정은 필요하다

| R을 활용한 웹 크롤링

오늘 해 볼 것은 

1. DAUM 의 영화 평 사이트에 기록된 감상평을 크롤링하고

2. 한 문장에 나오는 각 단어별 빈도수를 체크해 단어간의 연관성을 표현하려 한다.


우선 DAUM 영화 사이트를 들어가보자

DAUM 영화 (http://movie.daum.net/main/new)


다양한 영화들이 노출되어 있고 각 영화별 감상평을 확인할 수 있게 구성되어 있다.



많은 영화들 중에 "씽" 이라는 영화에 대해서 감상평을 크롤링 해보자


우선 영화 씽의 평점 탭을 눌러 리뷰페이지를 확인한다.   (http://movie.daum.net/moviedb/grade?movieId=99056)

다양한 리뷰가 짧게는 몇 자에서 부터 길게는 수백자 까지 적힌 글 들이 보인다.


미리 말하지만 리뷰중에 간혹 띄어쓰기를 하지 않고 올리는 사람들이 있다. 이런 경우 형태소 분석을 할 수 없어 대부분 버려진다.

혹 감상평 남기실 분들은 다른것은 몰라도 띄어쓰기 만큼은 지켜 줬으면 한다.;; 


이제 적혀있는 리뷰를 긁어올테데 여기부터 약간 html 에 대한 이해가 필요하다. 

사이트에서 마우스 우 클릭을 하고 소스보기를 누르면 페이지에 대한 html 이 보인다.

찾기로 리뷰가 시작되는 테그를 찾는다. 

필자가 이 글을 작성하는 시점에는 "<p class="desc_review">라는 테그로 리뷰가 시작하게 된다.   일단 여기까지 확인을 했으면 이제 R 로 돌아가 코딩을 시작해보자


먼저 필요한 패키지 부터 설치한다.

install.packages(c('rvest','httr','KoNLP','stringr','tm','qgraph','xml2'))

library(rvest)

library(httr)

library(KoNLP)

library(stringr)

library(tm)

library(qgraph)

library('xml2')



패키지 설치가 끝났으면 본격적인 크롤링을 위한 코드를 작성한다. 

url_base <- 'http://movie.daum.net/moviedb/grade?movieId=99056&type=netizen&page='   # 크롤링 대상 URL


all.reviews <- c() 

for(page in 1:500){    ## 500페이지 까지만 수집 (본인이 나름대로 설정하면 됨) 

  url <- paste(url_base, page, sep='')   #url_base의 뒤에 페이지를 1~500 까지 늘리면서 접근

  htxt <- read_html(url)                       # html 코드 불러오기

  comments <- html_nodes(htxt, 'div') %>% html_nodes('p')  ## comment 가 있는 위치 찾아 들어가기 

  reviews <- html_text(comments)               # 실제 리뷰의 text 파일만 추출

  reviews <- repair_encoding(reviews, from = 'utf-8')  ## 인코딩 변경

  if( length(reviews) == 0 ){ break }                              #리뷰가 없는 내용은 제거

  reviews <- str_trim(reviews)                                      # 앞뒤 공백문자 제거

  all.reviews <- c(all.reviews, reviews)                          #결과값 저장

}


##불필요 내용 필터링

all.reviews <- all.reviews[!str_detect(all.reviews,"평점")]   # 수집에 불필요한 단어가 포함된 내용 제거


주요 코드를 설명하자면


  • url_base 는 크롤링 대상 최 상위 url 이다.  url_base 뒤에 page= 이 보일 텐데 그 뒤로 페이지 번호가 붙으면서 리뷰 페이지가 넘어가게 된다.
    따라서 for 문으로 반복하여 각 페이지를 방문하면서 클롤링을 해야 한다.    
    url_base 에 페이지 번호를 붙이는 것은 "paste" 명령어가 처리한다.

  • read_html 을 해당 URL의 html 을 header 와 body 로 가져온다.

  • html_nodes 는 테그를 찾아주는 함수다.
    리뷰가 적혀 있는 "<p class="desc_review">" 테그 위치까지 찾아 내려가야 한다.
    필자가 확인하기로 <div> 테그가 먼저 desc_review 를 감싸고 있었기 때문에 <div> 를 먼저 찾는다.  
    그리고 하위의 <p> 테그를 접근해야 하기 때문에  %>%  를 붙여서 html_nodes('p')를 찾는다.
    %>% 는 선행의 결과를 후행으로 이어서 처리할 수 있도록 도와주는 역할이라고 보면 된다.
    ※ 만약 사이트가 다르거나 경로가 바뀌었다면 리뷰를 계층적으로 찾아들어갈 수 있도록 html_nodes만 여러차례 변경하면 된다.

  • html_nodes 까지 찾으면 이제 실제 리뷰만 긁어가면 되는데 html_text 명령어가 이를 처리한다.

  • 리뷰가 없는 경우와 앞뒤 공백등의 불필요한 내용은 제거한다.

  • 특이하게도 리뷰를 보게되면 "내 평점이 없습니다. 평점을 등록해주세요." 라는 문구가 하단에 지속 반복 된다.
    이는 시스템에서 default 로 넣은 문구이기 때문에 분석 대상이 아니다.
    크롤링 하다보니 같이 들어온 문구이지만 분석에는 관련이 없어서 제거가 필요하다.
    str_detect명령어로 "평점" 이 들어가는 내용은 전부 제거 처리를 했다. 

| 단어 연관 분석

위 내용이 모두 끝나면 명사와 형용사를 추출하는 function 을 만든다.

SimplePos09가 각 문장의 형태를 분리하게 되고 str_match가  가-힣 까지의 단어와 함께  N(명사) , P(형용사)  만들 가져오도록 추출한다. 

## 명사/형용사 추출 함수 생성

ko.words <- function(doc){

  d <- as.character(doc)

  pos <- paste(SimplePos09(d))

  extracted <- str_match(pos, '([가-힣]+)/[NP]')

  keyword <- extracted[,2]

  keyword[!is.na(keyword)]

}

  • str_match 는 해당 문자가 매치되는 경우만 가져오도록 해준다.



이제 분리된 단어를 기준으로 Corpus 라는 일종의 말뭉치로 끊어낸 뒤 각 단어별 노출 빈도수를  TermDocumentMatrix 를 계산한다.

options(mc.cores=1)    # 단일 Core 만 활용하도록 변경 (옵션)

cps <- Corpus(VectorSource(all.reviews))  

tdm <- TermDocumentMatrix(cps,   

                          control=list(tokenize=ko.words,   ## token 분류시 활용할 함수명 지정

                                       removePunctuation=T,

                                       removeNumbers=T,

                                       wordLengths=c(2, 6),  

                                       weighting=weightBin))  

#최종결과 확인
dim(tdm)
tdm.matrix <- as.matrix(tdm)
Encoding(rownames(tdm.matrix)) <- "UTF-8"
rownames(tdm.matrix)[1:100]
  • Corpus는 말뭉치로 텍스트 마이닝을 처리 하기위한 단위라고 생각하면 된다.
  • TermDocumentMatrix에 Corpus를 넣어야 한다.
  • wordLengths 는 최대 최소 단어의 길이를 지정하는 것으로 보통 한글이 그리 길지 않기 때문에 그것을 고려해서 설정한다. 너무 길 경우 단어가 아닌 문장들이 섞여 들어올 수 있다. 참고로 한글은 2자리 씩 차지하기 때문에 숫자로 표현시 곱하기 2를 해야 한다.
  • weightBin은 한 문장에서 동일하게 반복되는 말의 경우를 여러차례 카운트 하는 것이 하니라 한번으로 만 카운트 하게 된다. 
    예를 들어 너무 너무 좋았다 라는 표현도 너무라는 의미가 한번쓰인 것이 중요하지 강조한다고 여기서는 큰 의미가 없기 때문에 제외한다.


위 결과를 보게 되면 다음과 같이 추출된 단어가 보인다.

TermDocumentMatrix는 단어가 Document 별로 얼마나 나왔는지를 Matrix 형태로 보여주는 것이라 할 수 있다.

> tdm.matrix

              Docs

Terms          1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

  가능한       0 0 0 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0

              Docs

Terms          36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

  가능한        0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0

              Docs ... ...


> rownames(tdm.matrix)[1:100]

  [1] "가능한"       "가득"         "가득한"       "가르치"       "가사도"       "가사보고"     "가장강력한"  

  [8] "가장큰"       "가족과함"     "가족들"       "가족애"       "가족영화"     "가지"         "간만"        

 [15] "간아깝"       "간절한"       "감동"         "감동받긴"     "감동애"       "감동적"       "감상하"      

 [22] "감점"         "감정이입"     "강력한"       "강렬한"       "강추"         "강추합니다"   "개봉하"      ... ...




이제 이 결과를 시각화 하기 위해 각 단어별로 노출 빈도수가 많은 20개의 단어만을 추려낸다. 

freq.words 간 행렬 곱을 하여 서로 많이 나온 단어끼리는 수치가 높게 나오도록 계산한다.

#자주 쓰이는 단어 순으로 order 처리

word.count <- rowSums(tdm.matrix)  ##각 단어별 합계를 구함

word.order <- order(word.count, decreasing=T)  #다음으로 단어들을 쓰인 횟수에 따라 내림차순으로 정렬

freq.words <- tdm.matrix[word.order[1:20], ] #Term Document Matrix에서 자주 쓰인 단어 상위 20개에 해당하는 것만 추출

co.matrix <- freq.words %*% t(freq.words)  #행렬의 곱셈을 이용해 Term Document Matrix를 Co-occurence Matrix로 변경

> co.matrix

            Terms

Terms        영화 노래 감동 재미 기대 음악 아이들 재미있 나오 스토리 느끼 지루 생각 귀엽 애니메이션 어른 장면

  영화         30    8    4    9    5    6      2      3    4      3    4    1    3    2          1    1    1

  노래          8   29    3    3    1    3      2      2    3      2    3    2    4    3          1    1    2

  감동          4    3   15    3    2    3      0      1    0      1    1    0    1    0          1    1    0

  재미          9    3    3   14    3    3      1      2    2      2    2    2    0    0          1    0    1

  기대          5    1    2    3   12    1      0      0    0      1    1    2    2    0          0    1    1

  음악          6    3    3    3    1   12      0      1    2      1    2    0    2    1          2    0    0

  아이들        2    2    0    1    0    0     11      2    1      1    2    1    1    0  


결과적으로 각 단어끼리 노출 빈도수에 따라 많이 노출되는 경우는 연관성이 높다 판단하고 그렇지 않은것은 상대적으로 적다고 판단할 수 있다.

| 최종 출력

최종적으로는 qgraph 명령어로 시각화 한다.


qgraph(co.matrix,

       labels=rownames(co.matrix),   ##label 추가

       diag=F,                       ## 자신의 관계는 제거함

       layout='spring',              ##노드들의 위치를 spring으로 연결된 것 처럼 관련이 강하면 같이 붙어 있고 없으면 멀리 떨어지도록 표시됨

       edge.color='blue',

       vsize=log(diag(co.matrix))*2) ##diag는 matrix에서 대각선만 뽑는 것임. 즉 그 단어가 얼마나 나왔는지를 알 수 있음. vsize는 그 크기를 결정하는데 여기 인자값으로 단어가 나온 숫자를 넘겨주는 것임. log를 취한것은 단어 크기의 차이가 너무 커서 log를 통해서 그 차이를 좀 줄여준것임. 


아래 결과를 보면 영화, 노래 기대, 재미, 감동 등의 단어가 자주 그리고 서로 많이 나오는 것을 시각적으로 확인할 수 있다.
아래 그래프에 단어별로 긍/부정을 섞을 경우 영화에 대한 사람들이 느끼는 긍/부정률이 어느정도 인지도 알 수 있다. 
감성 분석에 대한 것은 다른 포스팅에서 언급하고자 한다.





[인코딩 관련 수정사항]  2017-08-18


아래 코드를 수행하다보면 all.reviews 까지는 정상적으로 글씨가 보이는데

TermDocumentMatrix 를 통과하고 부터는 한글이 전부 깨져서 보인다. 


원인은 인코딩 문제다.

Encoding(all.reviews)  을 실행 시키면 글씨가 노출된 부분에 UTF-8로 된 것을 알 수 있다. 

수집된 글의 인코딩은 UTF-8이란 얘기다. 

그러나 localeToCharset()  을 수행하면 아마도 본인 Local의 인코딩은 CP949로 보일 것이다.


어떤 이유에서인지 텍스트 마이닝 수행하다 중간부터 인코딩이 UTF-8에서 다른 것으로 변경되는 것으로 보이는데

이를 해결하려면 

코드 중간에 Encoding(rownames(tdm.matrix)) <- "UTF-8" 이 추가되어야 한다.

이 부분은 위 코드에도 수정해 놓았으니 참고 하면 될 것 같다.


[인코딩 관련 수정사항2 ]  2018-08-19

만약 맥북 이면서 위 조치사항을 했음에도 한글 오류가 나올 경우

par(family="Apple SD Gothic Neo")   ## mac option

을 추가한다.

| 전체 코드

install.packages(c('rvest','httr','KoNLP','stringr','tm','qgraph','xml2'))

library(rvest)

library(httr)

library(KoNLP)

library(stringr)

library(tm)

library(qgraph)

library('xml2')


url_base <- 'http://movie.daum.net/moviedb/grade?movieId=99056&type=netizen&page='   # 크롤링 대상 URL


all.reviews <- c() 

for(page in 1:500){    ## 500페이지 까지만 수집 (본인이 나름대로 설정하면 됨) 

  url <- paste(url_base, page, sep='')   #url_base의 뒤에 페이지를 1~500 까지 늘리면서 접근

  htxt <- read_html(url)                       # html 코드 불러오기

  comments <- html_nodes(htxt, 'div') %>% html_nodes('p')  ## comment 가 있는 위치 찾아 들어가기 

  reviews <- html_text(comments)               # 실제 리뷰의 text 파일만 추출

  reviews <- repair_encoding(reviews, from = 'utf-8')  ## 인코딩 변경

  if( length(reviews) == 0 ){ break }                              #리뷰가 없는 내용은 제거

  reviews <- str_trim(reviews)                                      # 앞뒤 공백문자 제거

  all.reviews <- c(all.reviews, reviews)                          #결과값 저장

}


##불필요 내용 필터링

all.reviews <- all.reviews[!str_detect(all.reviews,"평점")]   # 수집에 불필요한 단어가 포함된 내용 제거


Encoding(all.reviews)


options(encoding="utf-8")

## 명사/형용사 추출 함수 생성

ko.words <- function(doc){

  d <- as.character(doc)

  pos <- paste(SimplePos09(d))

  extracted <- str_match(pos, '([가-힣]+)/[NP]')

  keyword <- extracted[,2]

  keyword[!is.na(keyword)]

}



options(mc.cores=1)    # 단일 Core 만 활용하도록 변경 (옵션)

cps <- Corpus(VectorSource(all.reviews))  

tdm <- TermDocumentMatrix(cps,   

                          control=list(tokenize=ko.words,   ## token 분류시 활용할 함수명 지정

                                       removePunctuation=T,

                                       removeNumbers=T,

                                       wordLengths=c(2, 6),  

                                       weighting=weightBin

                                       ))  

#최종결과 확인

dim(tdm)

tdm.matrix <- as.matrix(tdm)

Encoding(rownames(tdm.matrix)) <- "UTF-8"


word.count <- rowSums(tdm.matrix)  ##각 단어별 합계를 구함

word.order <- order(word.count, decreasing=T)  #다음으로 단어들을 쓰인 횟수에 따라 내림차순으로 정렬

freq.words <- tdm.matrix[word.order[1:20], ] #Term Document Matrix에서 자주 쓰인 단어 상위 20개에 해당하는 것만 추출

co.matrix <- freq.words %*% t(freq.words)  #행렬의 곱셈을 이용해 Term Document Matrix를 Co-occurence Matrix로 변경


par(family="Apple SD Gothic Neo")   ## mac option

qgraph(co.matrix,

       labels=rownames(co.matrix),   ##label 추가

       diag=F,                       ## 자신의 관계는 제거함

       layout='spring',              ##노드들의 위치를 spring으로 연결된 것 처럼 관련이 강하면 같이 붙어 있고 없으면 멀리 떨어지도록 표시됨

       edge.color='blue',

       vsize=log(diag(co.matrix))*2) ##diag는 matrix에서 대각선만 뽑는 것임. 즉 그 단어가 얼마나 나왔는지를 알 수 있음. vsize는 그 크기를 결정하는데 여기 인자값으로 단어가 나온 숫자를 넘겨주는 것임. log를 취한것은 단어 크기의 차이가 너무 커서 log를 통해서 그 차이를 좀 줄여준것임. 


| 추가사항

추가로 python korea 2017에서 발표된 자연어 처리 관련 명사 추출 및 토크나이징 관련 방표 자료 링크를 걸어둔다.

Pycon2017 koreannlp : https://www.slideshare.net/kimhyunjoonglovit/pycon2017-koreannlp

pycon2017koreannlp-170809135945.pdf


| 참고 자료

마인드 스케일 텍스트 마이닝 교육



도움이 되셨다면 공감버튼을 살포시~

댓글