聰明不如鈍筆
총명불여둔필
assignment KindeR

최대한 친절하게 쓴 R 크롤러 만들기


살다 보면 가끔 인터넷에 있는 자료를 통째로 가져와서 데이터를 뽑아내고 싶다는 생각이 들 때가 있습니다.


이런 작업은 크롤링(crawling)이라고 부르기도 하고 (웹) 스크래이핑(scraping)이라고 부르기도 합니다.


전 세계에서 승객이 가장 많았던 공항 50군데를 시각화하면서 맛배기로 크롤링을 소개해 드렸던 적이 있습니다.


이번에는 한번 신문 기사를 긁어오는 과정을 알아보겠습니다. 


이번에도 사용할 도구는 역시 R입니다. R 공식 홈페이지는 "R는 통계 계산과 그래픽에 활용하는 무료 소프트웨어 환경(R is a free software environment for statistical computing and graphics)"이라고 밝혀두고 있습니다. 


러니 누구든 공짜로 이 프로그램을 사용할 수 있습니다. 아직 컴퓨터에 R가 없다면 이 다운로드 페이지에서 내려받아 설치하시면 됩니다. 그냥 다른 프로그램을 설치하실 때하고 똑같은 과정입니다.


아, 준비물이 하나 더 있습니다. 웹 브라우저입니다. 웹페이지 소스를 볼 수 있는 기능이 들어 있는 브라우저라면 어떤 것이든 관계 없습니다. 보통은 웹 브라우저에 저런 기능이 다 들어 있습니다.


저는 구글 크롬을 사용할 예정입니다. 역시 혹시나 크롬이 필요한데 설치하지 않으셨다면 이 링크를 찾아가시면 됩니다. (제가 추천하는 크롬 확장 프로그램을 같이 설치하셔도 좋습니다.)


신문 기사를 긁어올 때는 △원하는 기사를 상세하게 찾아서 △검색 결과에 나온 링크를 정리한 뒤 △해당 페이지에서 기사 내용을 긁어오는 세 단계를 거치는 게 일반적입니다.


넓은 의미에서 '인터넷 게시판'이 어떻게 생겼는지 떠올려 보면 각종 커뮤니티 사이트나 블로그, 인터넷 카페도 비슷한 과정을 거치면 된다는 걸 짐작할 수 있습니다. 한번 차근차근 천천히 해보겠습니다.


URL은 내 친구

이번에 목표로 세운 건 제가 여태 쓴 '베이스볼 비키니' 모두 가져오기.


원하는 자료만 골라 오려면 정밀하게 검색하는 것도 중요합니다. 저는 이 칼럼을 쓸 때 기자가 누군지 알려주는 '바이라인'에 e메일 주소 대신 페이스북 계정 주소(fb.com/bigkini)를 넣었습니다. 다른 기자가 기사에 'bigkini'라는 낱말을 쓸 확률은 사실상 제로에 가까울 터.


동아닷컴(www.donga.com)에서 'bigkini'로 검색했습니다. 그다음 신문에 나온 기사만 보여주도록 선택했습니다



검색을 마치셨으면 인터넷 주소(URL)를 한번 분석할 필요가 있습니다. 위 화면이 나왔을 때 제 주소창에는 이렇게 떠 있었습니다.

http://news.donga.com/search?check_news=1&more=1&sorting=1&range=3&search_date=&query=bigkini


주소를 자세히 보시면 '&낱말=숫자 또는 낱말' 구조가 이어지는 게 눈에 띕니다.


기사를 찾을 때는 검색 결과가 한 페이지로 끝나는 일이 드물죠?


맨 아래로 내려서 검색 결과 2 페이지를 찾아 보겠습니다. URL은 다시 이렇게 나옵니다.

http://news.donga.com/search?p=16&query=bigkini&check_news=1&more=1&sorting=1&search_date=1&v1=&v2=&range=3


뭐가 뭔지 잘 모르겠으니 다시 1 페이지로 돌아오겠습니다. URL이 이렇게 바뀐 걸 알 수 있습니다.

http://news.donga.com/search?p=1&query=bigkini&check_news=1&more=1&sorting=1&search_date=1&v1=&v2=&range=3


맨 처음에 쓴 URL하고 보여주는 결과는 똑같은데 구조가 다릅니다. 검색 결과가 여러 페이지로 나올 때는 페이지 사이를 왕복해 보면 좋습니다. URL 구조가 좀더 분명히 들어오기 때문입니다.


현재 주소에서는 '&낱말=숫자 또는 낱말' 구조를 하나씩 지우고 다시 페이지를 불러 보시면 각 단위가 어떤 구실을 하는지 알 수 있습니다. 눈치가 빠른 분이라면 '&query=bigkini'는 검색한 낱말이 'bigkini'라는 걸 알려주고 있다는 걸 알아채셨을 겁니다.


또 맨 처음에는 &query=bigkini가 URL 맨 끝에 있었는데 다시 불러왔을 때는 앞으로 자리를 옮겼네요? 이건 이 단위끼리 자리를 바꿔도 검색 결과에 영향을 끼치지 않는다는 증거입니다.


이것 저것 지우고 옮겨 보니 아래처럼 URL을 바꿔도 처음 본 검색 결과하고 똑같은 결과가 나온다는 걸 알 수 있었습니다.

http://news.donga.com/search?query=bigkini&more=1&range=3&p=1


이 주소로 페이지를 불러온 상태에서 2페이지로 가봤더니 URL이 이렇게 나왔습니다.

http://news.donga.com/search?p=16&query=bigkini&check_news=1|2|3|6|7|8|9|12|14&more=1&sorting=1&search_date=1&v1=&v2=&range=3


처음에도 2페이지에 갔을 때 'p=16이 눈에 띄었는데 이번에도 그러네요. 줄이고 줄인 URL 맨 끝에 있는 p만 16으로 바꿔 주소창에 넣어 봤습니다. 예상대로 2페이지하고 같은 결과가 나옵니다.


좀더 확인해 보시면 3페이지는 p=31, 4페이지는 p=46, 5페이지는 p=61로 숫자가 15씩 늘어난다는 걸 알 수 있습니다. 한번에 보여주는 검색 결과가 15개거든요.


여기까지 오셨으면 이 글을 처음 시작할 때 말씀 드린 세 단계 중에서 첫 단계는 끝내신 겁니다. 수고하셨습니다.


아직은 별로 어려운 게 없었습니다. 아마 앞으로도 그럴 겁니다. 그래도 혹시 모르니 심호흡 한번 하시고 스크롤을 내려보겠습니다.



print("Hello, R")

이제부터는 컴퓨터 프로그래밍이라고 부르기도 하고 코딩(coding)이라고 부르기도 하는 과정을 시작할 겁니다. 내가 이러저러한 게 하고 싶다고 컴퓨터에게 알려주는 게 바로 프로그래밍입니다.


예전에 썼던 글에서 그림을 가져오면 이렇게 print("Hello, World")라고 치는 것만으로 (한번도 코딩을 해보지 않으셨다면) 여러분은 생애 첫 프로그램을 완성하실 수 있습니다.



자, 우리는 여기서 URL을 가지고 뭔가 해보려고 합니다. 일단은 아래처럼 URL 여섯 줄을 만들어 내야 합니다.

http://news.donga.com/search?query=bigkini&more=1&range=3&p=1
http://news.donga.com/search?query=bigkini&more=1&range=3&p=16
http://news.donga.com/search?query=bigkini&more=1&range=3&p=31
http://news.donga.com/search?query=bigkini&more=1&range=3&p=46
http://news.donga.com/search?query=bigkini&more=1&range=3&p=61
http://news.donga.com/search?query=bigkini&more=1&range=3&p=76


맨 끝에 나온 숫자만 빼고는 전부 똑같은 구조입니다. 앞 부분은 놔두고 맨 끝만 반복적으로 갈아끼우면 되겠죠?


이럴 때 쓰라고 있는 게 문자 그대로 반복문입니다. 우리는 1, 16, 31, 46, 61, 76을 반복적으로 만들어야 합니다. 이걸 수식으로 표현하면 어떻게 될까요?


 1 = 0 × 15 + 1
16 = 1 × 15 + 1
31 = 2 × 15 + 1…


처럼 표현할 수 있습니다. 역시 맨 앞에 숫자만 하나씩 커지고 나머지는 변화가 없습니다.


R에서는 for가 반복문을 만드는 대표적인 명령어(함수)입니다.


아래처럼 써서 R에 입력하면 우리가 원하는 값을 얻을 수 있습니다. x, y는 그냥 수학 방정식에 등장하는 것처럼 대표적인 문자 기호일 뿐 다른 의미가 있는 건 아닙니다.

for(x in 0:5){
 y = x * 15 + 1
 print(y)
 }


이 반복문 실행이 모두 끝나고 나서 print(y) 또는 y라고 입력하시면 76이 나옵니다. 맨 마지막 값만 저장하고 있는 겁니다. 같은 방식으로 x를 치면 5가 나옵니다.우리는 저 링크 6개를 전부 저장해 두고 싶습니다. 이럴 때는 미리 그릇을 하나 만들어 놓고 거기 담으면 됩니다. 이런 그릇을 '변수'라고 합니다. 위에 나온 x, y도 변수입니다.


한번 x <- c(1, 2, 3, 4, 5) 라고 입력하신 다음 x를 치시면 저 숫자가 차례로 나올 겁니다. 여기서 c는 묶는다(concatenate)는 뜻입니다.


만약 x[1]이라고 치면 어떻게 될까요? 이때는 1이 나옵니다. x 중에서 첫 번째 값을 불러오라는 뜻이거든요. x[3]은? 네, 3이 나옵니다.


여기까지 이해하셨을 걸로 믿고 링크 6개를 저장하는 코드를 만들어 보겠습니다.

basic_url <- 'http://news.donga.com/search?query=bigkini&more=1&range=3&p='
 urls <- NULL
 for(x in 0:5){
 urls[x+1] <- paste0(basic_url, x*15+1)
 }


맨 첫 줄은 변하지 않는 부분을 컴퓨터에게 알려주는 겁니다. R에서 '<-' 또는 '='는 어떤 변수에 어떤 값을 넣으라는 뜻입니다.


다음 줄은 urls라는 방에 NULL을 넣으라는 뜻일 터. NULL은 빈 방이라는 뜻입니다. 미리 이런 변수를 만들어주지 않은 채로 '손님을 받으라'고 하면 R가 그 방을 못 찾아서 에러를 내거든요. 


urls[x+1]은 urls 중에서 몇 번째 칸인지 정하는 겁니다. x는 0에서 5까지 움직이는데 R에서 변수에는 0번째 칸이 없습니다. 그래서 하나씩 자리를 키워주는 겁니다. 


다음 나오는 paste0는 '붙여넣기'입니다. 이 변수와 저 변수를 붙이라는 뜻입니다. 그냥 paste를 쓰면 변수 사이에 한 칸을 띄웁니다. 여기서는 그러면 안 뇌니까 paste0을 써서 변수 사이를 붙였습니다.


결과를 확인해 볼까요?

urls
[1] "http://news.donga.com/search?query=bigkini&more=1&range=3&p=1"
[2] "http://news.donga.com/search?query=bigkini&more=1&range=3&p=16"
[3] "http://news.donga.com/search?query=bigkini&more=1&range=3&p=31"
[4] "http://news.donga.com/search?query=bigkini&more=1&range=3&p=46"
[5] "http://news.donga.com/search?query=bigkini&more=1&range=3&p=61"
[6] "http://news.donga.com/search?query=bigkini&more=1&range=3&p=76"


생각처럼 잘 나왔죠? 이제 여러분은 한 걸음 더 크롤러(크롤링을 하는 프로그램) 완성에 가까이 다가가신 겁니다.



갑자기 웬 HTML?

저 URL 여섯 개는 검색 결과 페이지를 찾아가는 주소입니다. 우리가 필요한 건 기사 그 자체로 가는 URL입니다. 이제 그 주소를 찾아볼 겁니다.


인터넷 페이지는 보통 하이퍼텍스트 마크업 언어(HTML) 문서로 만듭니다.


그렇다고 HTML이 뭔지 당장 배우실 필요는 없습니다. 지금은 HTML 문서 특징만 이해하시면 됩니다.


먼저 HTML 문서는 보통 트리(tree) 구조로 돼 있습니다.


만약 윈도 탐색기로 '내 음악' 폴더에 들어 있는 어떤 음악 파일을 재생한다고 해보겠습니다. 이러려면 사용자는 내 문서 → 내 음악 → 해당 음악 파일 순서로 찾아갈 겁니다. 이게 바로 트리 구조입니다.


또 한 가지 특징은 HTML은 태그(tag)라는 녀석을 한 데 합친 결과물이라는 점입니다. 아래는 이 블로그에 쓴 'WWW의 스물 다섯 번째 생일'에서 태그 하나를 가져온 겁니다.

<a href="http://info.cern.ch/hypertext/WWW/TheProject.html" target="_blank" class="tx-link">세계 최초 웹페이지</a>


맨 앞에 있는 'a'가 바로 태그입니다. a는 링크를 만드는 구실을 합니다.


그다음에 있는 href, target, class 같은 녀석은 속성(attribute)입니다. 이 링크를 어떻게 보여줘야 할지 설정을 해주는 것.


'세계 최초 웹페이지'는 실제로 화면에 나오는 텍스트(text)입니다.


HTML에서는 태그를 끝낼 때 시작한 태그로 닫아줘야 합니다. </a>가 들어간 이유입니다.


구체적으로 href는 실제로 가야 할 URL을 담고 있습니다.


target="_blank"는 이 링크를 새 창에서 띄우라는 뜻입니다.


클래스(class)는 게시물에서 이 부분을 "tx-link"라고 미리 정한 모양대로 보여주라는 뜻입니다.


이어 나온 '세계 최초 웹페이지'라는 일곱 글자를 표시할 때 글자색은 어떻게 하고, 밑줄은 어떻게 치라는 의미입니다. 


한번 실제로 HTML 문서를 열어볼까요?


검색 결과 맨 처음에 나온 페이지에서 F12(크롬 기준)를 눌러 소스를 보겠습니다. 


 

문서가 너무 길어 보기 불편할 때는 소스 보기 창 맨 위에 있는 화살표 모양을 눌러 놓고 웹 페이지에서 원하는 부분을 클릭하면 그 부분이 나와 있는 코드로 이동합니다.


우리가 찾으려는 건 실제 기사로 이동하는 링크, 그러니까 a 태그입니다.


이걸 찾아 보니까 searchCont라는 클래스가 붙은 디비전(divisodn) 아래 링크가 들어 있다는 걸 알 수 있습니다.


이제 이 링크를 전부 긁어 오면 스크래이핑 두 번째 단계를 끝낼 수 있습니다.



rvest %>% 만세!

R로 크롤링을 할 때 가장 많이 쓰는 패키지는 rvest입니다.


아직 이 패키지를 설치하지 않으셨대도 걱정하실 거 없습니다. 그냥 install.packages("rvest")라고 R 입력창(콘솔)에 쳐넣기만 하면 됩니다.


그러고 나면 미러 선택창이 나오는데 아무 거나 고르셔도 됩니다.


R에서는 패키지를 설치만 했다고 바로 쓸 수 있는 건 아닙니다. 패키지를 메모리에 불러오는 과정이 필요합니다.


이번에도 쉽습니다. library('rvest')라고 입력하면 끝입니다.

install.packages('rvest')
library(rvest)


이제 R에 HTML을 불러와야겠죠? 이때 쓰는 함수는 read_html입니다. 한번 첫 번째 검색 페이지를 불러와 보면:

read_html(urls[1])
{xml_document}
<html lang="ko">
[1] <head>\n<title>동아닷컴</title>\n<meta name="keywords" content="뉴스, 기사, 속보 ...
[2] <body>\r\n<div class="skip"><a href="#contents">본문바로가기</a></div


잘 됩니다. 왜 urls[1]이라고 썼는지 모르는 분은 아니 계시겠죠? 그냥 두면 날아가니까 이 결과를 html이라는 변수에 넣어놓겠습니다.

html <- read_html(urls[1])


이제 기사로 가는 링크를 찾을 차례. rvest에서 특정 태그를 찾을 때는 html_node(s)를 쓰면 됩니다. s가 붙으면 같은 태그를 전부 찾고, 빼면 맨 처음에 나오는 하나만 찾습니다.


우리가 제일 먼저 찾아야 하는 건 'searchCont'라는 class가 붙은 div 태그입니다. 이건 이런 식으로 입력하면 됩니다.

html_nodes(html, '.searchCont')


'searchCont' 앞에 점(.)을 찍은 건 찾고자 하는 게 class라는 걸 알려주는 기능을 합니다.


실제로 크롤링할 때는 태그 속성 중에 class나 id를 이용할 때가 많습니다. class는 앞에 .을 찍고 id는 #을 붙여주면 그만입니다.


'html2 <- html_nodes(html, '.searchCont')'를 써서 이 결과를 html2라는 변수에 넣겠습니다.


html2에서 우리가 찾아야 하는 건 a 태그. 이번에는 html3에 저장합니다.

html3 <- html_nodes(html2, 'a')

이번에는 태그를 찾는 거니까 .이나 #을 쓰지 않았습니다. 결과는?


html3
{xml_nodeset (55)}
 [1] <a href="http://news.donga.com/3/all/20170202/82676578/1" target="_bl ...
 [2] <a href="http://news.donga.com/3/all/20170202/82676578/1" target="_bl ...
 [3] <a href="http://web.donga.com/pdf/pdf_viewer.php?vcid=2017020245A2601 ...
 [4] <a href="http://news.donga.com/3/all/20170202/82676578/1" target="_bl ...
 [5] <a href="http://news.donga.com/3/all/20170126/82600548/1" target="_bl ...
(이하 생략)


뭔가 2% 부족합니다. 우리가 진짜 찾으려는 건 맨 처음 검색 결과 주소를 저장했을 때처럼 저 href 속성 안에 들어 있는 URL이거든요.


이번에도 문제가 없습니다. rvest에는 속성을 가져오는 html_attr 함수도 있으니까요. links라는 변수에 URL만 담아 보겠습니다.

links <- html_attr(html3, 'href')
links
 [1] "http://news.donga.com/3/all/20170202/82676578/1"
 [2] "http://news.donga.com/3/all/20170202/82676578/1"
 [3] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017020245A2601"
 [4] "http://news.donga.com/3/all/20170202/82676578/1"
 [5] "http://news.donga.com/3/all/20170126/82600548/1"
(이하 생략)


원하는 결과가 나왔습니다. 그런데 어차피 html → html2 → html3 → links 순서로 자료를 이동할 건데 이렇게 한 줄, 한 줄 치면 좀 피곤하지 않은가요?


그래서 세상에 나온 게 '파이프(pipe)'라는 녀석입니다.


현실 세계에서 파이프가 한 곳에서 다른 곳으로 물 같은 액체를 보낼 수 있게 도와주는 것처럼 이 녀석도 함수에서 다른 함수로 자료를 손쉽게 보내주는 구실을 합니다.


게다가 HTML이 트리 구조로 돼 있기 때문에 파이프를 쓰면 직관적으로 이해하기도 쉽습니다.


R에서 파이프를 쓰려면 dplyr라는 패키기자 필요합니다. 마찬가지로 설치하고 불러줍니다.

install.packages('dplyr')
library('dplyr')


deplyr를 설치하고 나면 HTML 문서를 읽어서 links에 저장하는 데 이 한 줄이면 충분합니다. R에서 파이프는 '%>%'로 표시합니다.

links <- html %>% html_nodes('.searchCont') %>% html_nodes('a') %>% html_attr('href')


여러 번에 나눠쳤던 걸 한 줄에 몰아 넣은 구조입니다.


파이프는 이 방 저 방 만들지 않고 한번에 끝낼 수 있다는 게 역시 제일 큰 장점입니다. 또 머릿속에 HTML 문서 구조가 있으면 입력하기도 훨씬 수월합니다.



URL에서 U는 유니크?

사실 우리가 links라는 방에 넣어둔 URL은 여전히 2% 부족합니다. 다시 볼까요?

links
[1] "http://news.donga.com/3/all/20170202/82676578/1"
[2] "http://news.donga.com/3/all/20170202/82676578/1"
[3] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017020245A2601"
[4] "http://news.donga.com/3/all/20170202/82676578/1"
[5] "http://news.donga.com/3/all/20170126/82600548/1"

(이하 생략)


[1], [2]가 똑같고 [4], [5]도 마찬가지입니다. 이건 기사 제목에만 링크가 들어 있는 게 아니라 대표 이미지(섬네일)에도 링크가 있는데 이를 거르지 않고 한번에 크롤링해서 생긴 일입니다. 


이번에도 걱정하실 거 없습니다. R에서는 중복 자료를 정리해 보여주는 unique라는 함수가 있거든요.


처음부터 파이프를 써서 'links <- html %>% html_nodes('.searchCont') %>% html_nodes('a') %>% html_attr('href') %>% unique()'라고 입력했어도 아래하고 같은 결과가 나왔을 겁니다.

 links <- unique(links)
links
[1] "http://news.donga.com/3/all/20170202/82676578/1"
[2] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017020245A2601"
[3] "http://news.donga.com/3/all/20170126/82600548/1"
[4] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017012645A2601"
[5] "http://news.donga.com/3/all/20170120/82482150/1"

(이하 생략)


이번에도 좀 이상합니다. [1]하고 [2]가 스타일이 달라 보입니다. 실제로 저 주소를 브라우저에 입력해 보면 [1] 같은 형태는 인터넷 기사로 가는 반면 [2]는 지면 PDF로 연결한다는 걸 확인할 수 있습니다. 저런 건 빼야겠죠?


그때 도와주는 게 grep이라는 함수입니다. 딱 봐도 주소에 pdf가 들어가 있을 때 빼면 됩니다.


그러면 먼저 pdf가 들어간 걸 찾아 보겠습니다.

grep("pdf", links)
 [1]  2  4  6  8 10 12 14 16 18 20 22 24 26 28 30


이상하게(?) 숫자가 나옵니다. 이 숫자는 pdf가 들어간 행을 나타냅니다. 위를 보시면 정말 [2], [4]에 pdf가 들어있네요


 grep은 특정한 문자(열)가 들어간 자료 순서를 알려준다는 사실을 알 수 있습니다. 만약 links[grep("pdf", links)]라고 입력하면 어떤 결과가 나올까요?

links[grep("pdf", links)]
[1] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017020245A2601"
[2] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017012645A2601"
[3] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2017012045A2601"
[4] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2016122845A3001"
[5] "http://web.donga.com/pdf/pdf_viewer.php?vcid=2016122245A3101"

(이하 생략)


pdf가 들어간 링크만 골라 보여줍니다. 위에서 변수 이름 옆에 []를 치면 그 순서에 맞는 자료만 보여준다는 걸 확인했습니다.


이번에는 [grep("pdf", links)]라고 입력했으니까 그 결과에 따라 순서를 골라 보여준 겁니다.


우리가 원하는 pdf가 들어간 자료만 남기는 게 아니라 빼는 겁니다. 특정 자료만 뺄 때는 []사이에 빼기(-) 표시를 하면 됩니다.


links[-grep("pdf", links)]처럼 말입니다.

links[-grep("pdf", links)]
[1] "http://news.donga.com/3/all/20170202/82676578/1" 
[2] "http://news.donga.com/3/all/20170126/82600548/1" 
[3] "http://news.donga.com/3/all/20170120/82482150/1" 
[4] "http://news.donga.com/3/all/20161228/82065527/1" 
[5] "http://news.donga.com/3/all/20161222/81980312/1"

(이하 생략)


좋습니다. 이제 이걸 links에 담으면 되니까 links <- links[-grep("pdf", links)]를 치면 끝입니다.


(이 꼭지 제목은 사실과 다릅니다. URL은 Uniform Resource Locator를 줄인 말입니다.)



Back at one

이제 겨우 딱 한 페이지에서 기사로 가는 URL을 찾았습니다. 여섯 페이지에서 똑같은 작업을 반복해야 하죠. 뭘 써야 할까요?


네, 정답은 반복문입니다. 


이때는 이미 urls에 URL 여섯 개가 들어 있는 상태이기 때문에 x 같은 변수를 따로 지정할 필요는 없습니다. 그냥 '변수 in 변수' 형태로 표현하면 그만입니다.


우리는 urls라는 변수에 URL을 담아 놓았으니 'url in urls'라고 표현해 보겠습니다.


아, 뭐 잊은 게 없냐고 묻는 분이 계시네요. 맞습니다. 칸이 있는 방(변수)을 쓰려면 미리 빈 방이라고 한번 선언을 해줘야 합니다.

links <- NULL
for(url in urls){
 html <- read_html(url)
 links <- c(links, html %>% html_nodes('.searchCont') %>% html_nodes('a') %>% html_attr('href') %>% unique())
 }


낯선 표현이 하나 있습니다. c(links, 블라블라) 형태네요. c는 자료를 묶는다는 뜻이라고 위에서 확인했습니다.


이 줄은 links라는 변수에 자료를 계속 더하라는 뜻입니다.


예를 들어:

x <- 1
x <- 2
x
[1] 2


이렇게 치고 나서 x를 확인해 보면 2가 나옵니다. 맨 마지막에 입력한 값이 나오는 것.


이때 1과 2를 모두 가지고 있는 변수를 만들고 싶다면 아래처럼 치면 됩니다.

x <- 1
x <- c(x, 2)
x
[1] 1 2


아직 PDF 링크는 그대로 남아 있으니까 grep까지 적용하고 나서 확인해 보시면 links안에 URL이 88개 들어 있는 걸 확인하실 수 있습니다. 처음에 검색 결과에 나왔던 바로 그 숫자입니다.

length(links)
[1] 88


length는 이름 그대로 자료 길이(숫자)를 알려주는 함수입니다. length(urls)를 치면? 처음에 링크 6개를 넣어 뒀으니까 6이 나옵니다. 이제 슬슬 목적지가 보입니다.



Misson Complete

URL이 있으니까 HTML 문서를 열어봐야겠죠?


개별 기사 소스를 확인하니까 'article_txt'라는 클래스로 된 div 안에 기사 본문이 들어 있는 게 보입니다.



클래스로 찾을 때는 .을 찍는다는 것만 기억하시면 어렵지 않습니다. 바로 앞에 했던 작업과 다른 건 본문 텍스트를 긁어 오는 거니까 html_text 함수를 써야 한다는 것뿐입니다.


그러면 이렇게 정리가 끝납니다.

txts <- NULL
for(link in links){
  html <- read_html(link)
  txts <- c(txts, html %>% html_nodes('.article_txt') %>% html_text())
 }
txts
[1] "\n            심심풀이로 봤습니다, 프로야구 감독들
[2] "\n            \n             “차우찬이 95억 원씩이나 받
[3] "\n            현실 안맞는 일률 보상규정 때문에 충분히
[4] "\n            11개 종합지 올 시즌 기사 3742건 분석\n 
[5] "\n            \n             올해 가장 비효율적으로 돈을

(이하 생략)


축하드립니다. 여러분은 지금 생애 첫 크롤러(스크래이퍼)를 완성하셨습니다.


이제 이 프로그램을 잘 보완하시면 (이론적으로는) 구글이나 네이버 같은 검색 엔진도 만들 수 있습니다.


이렇게 얻은 결과를 나중에 또는 다른 프로그램에 활용하고 싶을 수도 있으니까 저장만 하면 끝입니다.


이럴 때는 보통 CSV(Comma Separated Values)라는 형태를 많이 씁니다. 이 형식은 문자 그대로 쉼표(comma)로 구분한 값이라는 뜻인데요, 그냥 쉽게 이해하시려면 텍스트(.txt) 파일 한 형태라고 보시면 됩니다.

write.csv(txts, "text.csv")


이렇게 입력하시면 여러분이 지정한 R 작업 폴더(윈도 기본은 '내 문서')에 text.csv 파일이 생긴 걸 확인하실 수 있습니다.



ABC, DEF…

클로링한 자료는 여러분이 활용하시기 나름입니다. txts[4]에 등장하는 건 이 기사입니다.


텍스트 데이터로 할 수 있는 가장 기본이라고 할 수 있는 낱말 구름(word cloud)을 그렸습니다.



이 때는 R가 아니라 파이선(python)이라는 프로그래밍 언어를 썼는데 방식은 R하고 100% 똑같다고 해도 과언이 아닙니다. 말(언어)이 다르니 문법이 조금 다를 뿐이었습니다.


이 글은 크롤링 혹은 웹 스크래이퍼가 뭔지 전혀 모르지만 한번 그런 걸 만들어 보고 싶어하는 분들께 도움이 될까 해서 써봤습니다.


사실 저도 이런 건 만들어 보고 싶은 마음은 퍽 예전부터 있었는데, 코딩은 ABC도 잘 모르다 보니 어디서부터 시작해야 할지 막막했거든요. 


이런 분들께 도움을 드리려고 최대한 자세하게 쓰다 보니 글이 너무 길어졌습니다.


모쪼록 도움을 받는 분이 계시길 바라며 코드를 정리해 남깁니다.


아래 코드를 그냥 가져다 R 콘솔에 붙이기만 하셔도 똑같은 결과는 얻으실 수 있습니다.


install.packages("rvest")
install.packages("dplyr")
library(rvest)
library(dplyr)

basic_url <- 'http://news.donga.com/search?query=bigkini&more=1&range=3&p='

urls <- NULL
for(x in 0:5){
  urls[x+1] <- paste0(basic_url, as.character(x*15+1))
  }

links <- NULL
for(url in urls){
  html <- read_html(url)
  links <- c(links, html %>% html_nodes('.searchCont') %>% html_nodes('a') %>% html_attr('href') %>% unique())
  }
links <- links[-grep("pdf", links)]

txts <- NULL
for(link in links){
  html <- read_html(link)
  txts <- c(txts, html %>% html_nodes('.article_txt') %>% html_text())
  }

write.csv(txts, "text.csv")


댓글, 107

  • 이전 댓글 더보기
  • 댓글 수정/삭제
    2020.06.07 15:11

    비밀댓글입니다

    •  수정/삭제 kini
      2020.06.12 11:08 신고

      '원래 그렇다'고 말씀드릴 수 있습니다.
      서버 쪽 반응이 그때그때 다를 때가 있거든요.

  • 댓글 수정/삭제
    2020.06.11 22:01

    비밀댓글입니다

    •  수정/삭제 kini
      2020.06.12 11:49 신고

      외교부 전자도서관 홈페이지는 진짜 자료를 꽁꽁 숨겨놓아서 일반적인 방법으로는 긁어오기가 어렵습니다.

      유저 에이전트를 지정해야 하고 또 DOM(Document Object Model) 조작도 필요합니다.

      만약 크롤링 연습이 목적이시라면 이보다 쉬운 사이트부터 도전해 보시는 걸 추천드리고 싶습니다.

      만약 그냥 그래프를 그려보는 게 목적이시라면 자료가 몇 개 되지 않으니 일일이 입력해도 되지 않을까요?

      워드 클라우드 관련해서는 아래 포스트가 도움이 될 수 있습니다.

      https://kuduz.tistory.com/1090

  • 댓글 수정/삭제
    2020.06.15 10:26

    비밀댓글입니다

    •  수정/삭제 kini
      2020.06.21 19:20 신고

      그냥 저 사이트가 유독 어려울 뿐 크롤링 자체가 어려운 건 아닙니다.
      나머지도 손에 익기만 하면 크게 어렵지 않습니다.
      언젠가 성공하시면 그때 다시 알려주시기를 -_-)/

  • 댓글 수정/삭제
    2020.07.22 15:04

    비밀댓글입니다

    •  수정/삭제 kini
      2020.07.27 12:56 신고

      네이버 쇼핑 코드를 열어 보니 데이터를 json 형태로 정리했네요.
      이럴 때는 위에 쓴 방법이 작동하지 않을 수 있습니다.

  • 댓글 수정/삭제
    2020.07.27 13:57

    비밀댓글입니다

    •  수정/삭제 kini
      2020.08.03 00:09 신고

      이 포스트에 나온 방법으로는 어렵습니다.
      네이버 API 사용법을 구글링 해보시면 도움을 받으실 수 있을 겁니다.

  • 댓글 수정/삭제 로지
    2020.08.08 17:29

    안녕하세요, 포스팅 잘 읽었습니다. r 크롤링에 대한 친절한 설명 감사드려요.

    덕분에 네이버 블로그 글을 대상으로 r 크롤링을 시도했는데, 에러가 해결되지 않아서 질문 드립니다.

    links <- NULL
    (for(url in urls){
    html <- read_html(url)
    links <- c(links, html %>% html_nodes('.review') %>% html_nodes('a') %>% html_attr('href') %>% unique())
    })
    links <- links[-grep("pdf", links)]
    이 부분을 실행할 때,
    Error in open.connection(x, "rb") : HTTP error 500라는 오류가 뜹니다.

    혹시 해결에 도움이 될까하여, 위의 답글로 남기셨던 내용을 참고하여,
    ua <- user_agent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 RuxitSynthetic/1.0 v5923957439 t55095 ath889cb9b1 altpub smf=0')
    result <- GET(urls[1], ua)
    read_html(result, encoding='gb18030')
    를 추가해서 실행했는데,
    Error in read_html.response(result, encoding = "gb18030") :
    Internal Server Error (HTTP 500).
    추가정보: 13건의 경고들이 발견되었습니다 (이를 확인하기 위해서는 warnings()를 이용하시길 바랍니다).
    라는 에러 메시지를 받았습니다.

    혹시 해결 방법을 알려주실 수 있나요?
    감사합니다.

    •  수정/삭제 kini
      2020.08.14 11:07 신고

      'pdf'가 들어간 부분은 현재 예시에서만 필요한 내용입니다.

      links <- c(links, html %>% html_nodes('.review') %>% html_nodes('a') %>% html_attr('href') %>% unique())

      이 부분을 크롤링하시려는 사이트에 맞게 바꾸셔야 합니다.

  • 댓글 수정/삭제 sh
    2020.08.09 02:45

    진짜 너무 감사해요 큰 도움 얻고 갑니다!

  • 댓글 수정/삭제 Kim
    2020.10.10 01:02

    안녕하세요? 늘 좋은 포스팅 잘 보면서 공부하고 있습니다. 감사합니다. 다름이 아니오라 이번 포스팅도 열심히 따라가다가 html2 <- html_nodes(html, '.searchCont') 명령어를 적었을 때 막히는 부분이 생겨서 이렇게 댓글을 답니다.

    위의 값을 실행하면,

    Error in UseMethod("xml_find_all") :
    클래스 "function"의 객체에 적용된 'xml_find_all'에 사용할수 있는 메소드가 없습니다

    라는 메시지가 뜨는데, 어떻게 해결할 수 있을까요?ㅠㅠ

    •  수정/삭제 kini
      2020.10.14 00:12 신고

      먼저 이 코드를 입력하셔야 합니다.

      html <- read_html(url[1])

      제가 말로만 설명을 드리고 코드를 안 적어 놓았었네요.

  • 댓글 수정/삭제
    2020.10.27 22:30

    비밀댓글입니다

    •  수정/삭제 kini
      2020.10.29 16:37 신고

      인코딩 문제가 있는 모양입니다.

      먼저 Sys.setlocale('LC_CTYPE', 'ko_KR.UTF-8')

      이렇게 입력하고 작업을 해보시고 안 되면

      Sys.setlocale('LC_CTYPE', 'ko_KR.CP949')

      이라고 해보시겠어요?

  • 댓글 수정/삭제 frocximo
    2021.02.16 13:39

    https://www.imdb.com/list/ls016522954/?ref_=ttls_vm_dtl&st_dt=&mode=detail&page=1&sort=user_rating,desc

    영화 싸이트입니다 평점 또는 년도가 없는 영화가 있습니다 영화제목, 년도, 평점, 시놉시스, 배우로 크롤링하려고 합니다 각 항목을 크롤링하면 수량이 맞질 않아요 영화제목이 91개면 년도는 87개 평점은 90개입니다 아마 달아 놓지를 않아서 그런거 같은데 이럴때는 어떻게 해야 하나요?

  • 댓글 수정/삭제
    2021.04.22 09:53

    비밀댓글입니다

  • 댓글 수정/삭제 foemdos19
    2021.05.14 23:10

    좋은 정보감사합니다
    저는 selenium을 써서 기사제목을 크롤링하는데 페이지가 한페이지만 크롤링이됩니다. 그래서 님의 글을 보고 for문을 만들어봤는데 페이지는 봐뀌는데 모든 페이지에서 제목을 가져오는건 여전히 안되네요.. 괜찮으시면 한번 봐주실수있나요?

    ---------------------------------------------------------


    library(httr)
    library(rvest)
    library(RSelenium)


    #서버 연결#
    remD <- remoteDriver(port = 4445L, browserName = 'chrome')
    remD$open() # 서버에 연결


    n<-11
    for(i in 1:n){

    i<-i+1
    remD$navigate(paste0("https://search.naver.com/search.naver?where=news&sm=tab_pge&query=%EB%8C%80%EC%9E%85&sort=0&photo=0&field=0&pd=0&ds=&de=&cluster_rank=126&mynews=0&office_type=0&office_section_code=0&news_office_checked=&nso=so:r,p:all,a:all&start=",10*i+1)) # 해당 홈페이지로 이동
    html<-remD$getPageSource()[[1]]
    html<-read_html(html) # 페이지의 소스 읽어 오기
    naver_tittle<-html%>%html_nodes(".news_tit") %>%
    html_text() # 선택한 노드를 텍스트 화
    }}
    naver_tittles <- append(naver_tittle,naver_tittles )
    naver_tittles [ 1 : 10 ] # 1 ~ 10 개 가져 오기

    naver_tittles<-gsub("\n","",naver_tittle) # 데이터 정제 1
    naver_tittles<-trimws(naver_tittle) # 데이터 정제 2


    naver_tittles # 데이터 출력

    #csv로 저장#
    write.csv(naver_tittle,file="D:/naver.csv",row.names=FALSE,quote=FALSE)

    •  수정/삭제 kini
      2021.05.15 23:32 신고

      저보다는 selenium 패키지를 다룬 분께 여쭤보시는 게 나을 듯합니다.

  • 댓글 수정/삭제 foemdos19
    2021.05.16 17:16

    for문에 빈방 설정과 c를 안해준게 문제네요.. 님 글을 자세히 다시 보고 해보니 성공했습니다. 감사합니다^^

  • 댓글 수정/삭제
    2021.05.17 16:19

    비밀댓글입니다

  • 댓글 수정/삭제 r초보
    2021.07.07 15:33

    키니님 글 설명이 너무 쉬워서 자주 이용합니다 미리 감사합니다!
    그런데 html2의 결과값이 아래 형식으로 나오게 되서, 후속 작업을 하지 못하고 있습니다ㅠㅠ 어떤 문제가 있는 걸까요? 어딜 찾아도 나오질 않네요

    > html2<-html_nodes(html, '.searchCont')
    > html2
    {xml_nodeset (0)}

    •  수정/삭제 kini
      2021.07.27 14:24 신고

      아, 이 댓글을 건너 뛰었네요.
      포스트에 나온 것과 같은 페이지를 작업하고 계신 게 맞나요?

  • 댓글 수정/삭제 송만석
    2021.07.24 07:01

    안녕하십니까 반갑습니다. 가끔 선생님이 알려 주시는 마이닝과 관련한 정보를 얻기 위해 방문합니다. 다름이 아니고 영화 또는 쇼핑몰에서 평점대별 예를 들어 5점, 4점, 3점, 2점, 1점 등을 분류하여 크롤링하려면 어떻게 하면 좋을까요 전체적으로는 크롤링을 할 수 있습니다만, 자질이 부족한가 같이 묶여 크롤링되어 나옵니다. 방법을 알려 주시면 대단히 감사하겠습니다.

  • 댓글 수정/삭제 송만석
    2021.07.27 07:49

    안녕하신지요
    먼저 저의 질문에 관심을 보여 주셔서 감사를 드립니다.
    사이트 주소는
    https://kr.iherb.com/r/missha-m-perfect-cover-b-b-cream-spf-42-pa-no-23-natural-beige-1-7-oz-50-ml/79051?p=1&sort=6&lang=ko-KR&isShowTranslated=true
    입니다.
    위 사이트가 반듯이라고는 할 수 없으나 대상을 화장품으로 할 예정이라서 네이버에서 하나를 선정하였습니다.
    여기에서 현재 시간으로 4,137의 구매후기가 표시되어 있습니다. 이 중에서 별 5개 평가는 3,428, 별 4개 평가는 490, 별 3개 평가는 176, 별 2개 평가는 77, 그리고 별 1개 평가가 90개로 각각 표시되어 있습니다. 저가 궁금하게 여쭤 보고 싶은 것은 각 평점 대별로 크롤링하고 싶습니다. 저가 가지고 있는 책들이나 웹에서 검색을 해 봐도 명확한 해결 방법이 보이지 않군요,
    각각의 별점 평가된(예, 5점, 4점, 3점, 2점, 1점) 것을 가지고 동일한 별점에서 크롤링할 페이지 수를 지정하여도 결과는 이 상품에 대한 전체 리뷰가 나오더군요 그래서 하나씩 긁어 보기도 하였습니다.
    도움을 주시면 대단히 감사하겠습니다.

    •  수정/삭제 kini
      2021.07.27 14:27 신고

      크롤링을 먼저 하신 다음에 별점별로 자료를 나누면 안 되는 이유가 혹시 따로 있나요?

  • 댓글 수정/삭제 송만석
    2021.07.28 11:24

    바쁘신데 답글 해 주셔서 감사드립니다.
    그것은 아닙니다. 저가 잘 몰라서 그렇습니다. 만약 별점을 포함하여 전부를 크롤링한다면, 예를 들어, 엑셀(csv?)로 저장한 다음 나중에 따로 별점을 기준으로 새로이 분류하는 것인지요. 그런데 저가 선생님이 소개하고 있는 다른 포스팅을 살펴 봤습니다만, 이러한 문제를 알려주시는 것은 보이지 않는군요. 혹시 있다면 알려주시면 감사하겠습니다.

    •  수정/삭제 kini
      2021.08.01 17:07 신고

      예, 물론입니다.

      크롤링이 오래 걸린다면 필요한 것만 긁어오시는 편이 나은 게 사실입니다.

      그런데 이미 전체 크롤링을 하셨다면 필요한 것만 빼시는 쪽이 나아 보이는데요?

      이렇게 접급하시려면 아래 포스트가 도움이 될 수 있습니다.

      https://kuduz.tistory.com/1084

  • 댓글 수정/삭제 서진희
    2021.08.12 13:44

    안녕하세요 선생님, 본문 따라 네이버 뉴스 크롤링 하던 도중, 뉴스 본문 txt 반복 추출 단계에서 장애가 나네요...

    아래와 같이 개별 링크로 했을 때는 텍스트가 출력이 됩니다.
    news_links[1] %>%
    read_html() %>%
    html_nodes('div.viewContentWrap > div.viewContent') %>%
    html_text()

    그런데 반복문으로 만드니까 에러가 납니다... 뭘 까요 흠...
    참고로 txts 변수에 기사 본문 txt값이 들어간 것도 확인이 됩니다.

    "Error in read_xml.raw(raw, encoding = encoding, base_url = base_url, as_html = as_html, : input conversion failed due to input error, bytes 0xFE 0xB0 0xED 0x20 [6003]"

    txts <- NULL

    for(link in news_links){
    html <- read_html(link)
    txts <- c(txts, html %>%
    html_nodes('div.viewContentWrap > div.viewContent') %>%
    html_text())
    }

    거진 몇 시간째 머리싸메고 있네요... 조언 부탁드립니다 선생님!

  • 댓글 수정/삭제
    2021.08.22 11:04

    비밀댓글입니다

account_circle
vpn_key
web

security

mode_edit
KindeR | 카테고리 다른 글 더 보기