7/08/2016

[RUBY] Writing a Web Crawler with Ruby and Nokogiri(nokogiri를 이용한 웹 크롤러 만들기)

지난 포스팅에선 nokogiri를 이용한 parsing 을 했다면 이번에는 조금 더 발전 시켜서 간단한 크롤러를 만들어볼까합니다. 물론, 훨씬 좋은 라이브러리들이 있지만 가장 기본이되는 nokogiri를 잘 안다면 많은 도움이 있을 수 있겠지요.

Web Crawling이란?

물론 아시고 들어오셨겠지만, 크롤러에 대한 정의를 다시 한번 생각해보고 하는게 좋을 것 같습니다. 일반적으로 웹 크롤러라고 하면 웹을 탐색하는 프로그램, 사이트를 탐색해서 구조를 펼쳐주는 프로그램이며 웹 스파이더로 불리기도 합니다.

여기까지는 아주 기본적인 크롤러의 정의이며 여기에 Link to Link 개념이 포함되어야 개발 시 이해가 좋을 것 같습니다. 이 부분은 아래에서 보고 Wiki에서의 Web Crawler 정의를 보고 작성으로 넘어가겠습니다.

Wiki 정의
웹 크롤러(web crawler)는 조직적, 자동화된 방법으로 월드 와이드 웹을 탐색하는 컴퓨터 프로그램이다. 웹 크롤러에 대한 다른 용어로는 앤트(ants), 자동 인덱서(automatic indexers), 봇(bots), 웜(worms), 웹 스파이더(web spider), 웹 로봇(web robot) 등이 있다.

웹 크롤러가 하는 작업을 웹 크롤링(web crawling) 혹은 스파이더링(spidering)이라 부른다. 검색 엔진과 같은 여러 사이트에서는 데이터의 최신 상태 유지를 위해 웹 크롤링한다. 웹 크롤러는 대체로 방문한 사이트의 모든 페이지의 복사본을 생성하는 데 사용되며, 검색 엔진은 이렇게 생성된 페이지를 보다 빠른 검색을 위해 인덱싱한다. 또한 크롤러는 링크 체크나 HTML 코드 검증과 같은 웹 사이트의 자동 유지 관리 작업을 위해 사용되기도 하며, 자동 이메일 수집과 같은 웹 페이지의 특정 형태의 정보를 수집하는 데도 사용된다.

웹 크롤러는 봇이나 소프트웨어 에이전트의 한 형태이다. 웹 크롤러는 대개 시드(seeds)라고 불리는 URL 리스트에서부터 시작하는데, 페이지의 모든 하이퍼링크를 인식하여 URL 리스트를 갱신한다. 갱신된 URL 리스트는 재귀적으로 다시 방문한다.

A Web crawler is an Internet bot which systematically browses the World Wide Web, typically for the purpose of Web indexing (web spidering).

Web search engines and some other sites use Web crawling or spidering software to update their web content or indexes of others sites' web content. Web crawlers can copy all the pages they visit for later processing by a search engine which indexes the downloaded pages so the users can search much more efficiently.

Crawlers consume resources on the systems they visit and often visit sites without tacit approval. Issues of schedule, load, and "politeness" come into play when large collections of pages are accessed. Mechanisms exist for public sites not wishing to be crawled to make this known to the crawling agent. For instance, including a robots.txt file can request bots to index only parts of a website, or nothing at all.

As the number of pages on the internet is extremely large, even the largest crawlers fall short of making a complete index. For that reason search engines were bad at giving relevant search results in the early years of the World Wide Web, before the year 2000. This is improved greatly by modern search engines, nowadays very good results are given instantly.

Link to Link

바로 웹의 기본적이 구성 형태인데요. 각각의 링크는 서로 연결되어 하나의 웹 사이트르 만들고 서비스를 만들어갑니다.

우리는 이 링크에 대한 분석을 통해서 큰 구조를 만들 수 있는 툴을 만들것이구요, Crawler는 이전 포스팅에서도 이야기해 드렸듯이 웹과 관련된 툴을 만들 때 많이 쓰이는 부분입니다. 알아두시면 좋아요

Writing a Web Crawler 1 - Find link on page

지난번 포스팅에서 css를 이용한 parsing 기억나시나요?
그것을 이용해서 쉽게 링크를 찾아낼 수 있습니다. 바로 링크 시 많이 사용되는 a 태그를 통해 찾는다면 조금 수월하게 링크 리스트를 뽑아낼 수 있겠지요.

그래도 간단하지만 우리가 생각하기 쉽도록 함수로 만들었습니다.

def get_link(page)
 return page.css("a")
end
내용을 살펴보면 단순하게 a 태그를 찾아서 반환해주는 함수입니다.
여기서 점점 살을 붙여볼게요.

nokogiri는 map이라는 메소드를 지원합니다. 이 메소드는 하위에 데이터를 걸러줄 수 있는 역할을 하는데요 아래와 같이 href 만 뽑아낼 수 있겠지요.

def get_link(page)
 return page.css("a").map{|link|link['href']}
end
자 1차적으로 Crawling에 필요한 page를 분석해서 링크를 뽑아내는 과정은 완성되었습니다.
full code로 보고 실행해보면 제 블로그에 걸려있는 링크 주소를 얻어올 수 있습니다.

require 'open-uri'
require 'nokogiri'

def get_link(page)
 return page.css("a").map{|link|link['href']}
end

page = Nokogiri::HTML(open("http://www.hahwul.com")) 
links = get_link(page)

puts links
간단하죠?

Writing a Web Crawler 2 - Filter for My Domain(Escape Special Char)

외부 링크까지 크롤링하는 스캐너도 있으나 기본적으로는 자신의 도메인만 크롤링하는게 원칙입니다. 우리가 뽑아낸 URL 리스트에서도 분명 다른 url이 있었지요. 그래서 그 url을 제거하는 과정을 만들어보도록 하겠습니다.

1번 과정을 통해 만들어진 links는 Array 형으로 만들어져있습니다. 각각 배열에서 하나하나씩 읽어서 불러오기 쉽죠.

irb(main):022:0* links.class
=> Array
irb(main):023:0> puts links[0]
#main
=> nil
irb(main):024:0> puts links[2]
/p/introduction.html
=> nil
irb(main):025:0> puts links[4]
/search/label/Hacking?updated-max=&max-results=7
=> nil
irb(main):026:0>

자 이제 이 배열에서 각각 links 안의 값이 우리가 크롤링하는 도메인인지, 혹여나 javascript 나 다른 문자열이 있는지 확인하는 과정이 필요합니다.

아까 만든 get_link 함수를 좀 더 강화시켜보죠.

def get_link(page)
 after_link = Array.new()
 before_link = page.css("a").map{|link|link['href']}  #가공 전
 before_link = before_link.uniq  # A) 중복제거
 for index in before_link # Loop! # B) # & : 있는 줄 제거.
   if(index.index(/:|^#|^&/))
     # no run
   else
     after_link.push(index)
   end
 end
 return after_link
end
일단은 uniq 메소드를 이용해서 중복제거를 해줍니다. A 줄을 보시면됩니다.
B부터는 index 메소드를 이용해서 제가 원하는 데이터가 String에 있는지 검색하고 있다면 처리하지 않고 없으면 새로운 리스트에 누적하도록 하겠습니다.

이렇게 돌리면서 새로운 배열에는 # , : 등이 들어간 줄은 포함되지 않게되지요.
(javascript: 등등 제거용)

 #> ruby test.rb
/p/introduction.html
/search/label/Hacking?updated-max=&max-results=7
/search/label/System Hacking?updated-max=&max-results=7
/search/label/Web Hacking?updated-max=&max-results=7
/search/label/Metasploit?updated-max=&max-results=7
/search/label/Vuln%26Exploit?updated-max=&max-results=7
/search/label/Mobile?updated-max=&max-results=7
/search/label/Coulm?updated-max=&max-results=7
/search/label/Coding?updated-max=&max-results=7
/search/label/Ruby?updated-max=&max-results=7
/search/label/Python?updated-max=&max-results=7
/search/label/C?updated-max=&max-results=7
/search/label/Java?updated-max=&max-results=7
/search/label/Htmljs?updated-max=&max-results=7
/search/label/Debian?updated-max=&max-results=7
/search/label/Media?updated-max=&max-results=7
/search/label/Bug?updated-max=&max-results=7
/search/label/Tip?updated-max=&max-results=7
/search/label/Video?updated-max=&max-results=7
/search/label/Design?updated-max=&max-results=7
/p/metasploit-meterpreter-cheat-sheet.html
/search/label/Media
/p/contact.html
//www.blogger.com/rearrange?blogID=1251432539166387960&widgetType=Feed&widgetId=Feed1&action=editWidget&sectionId=main_widget1
//www.blogger.com/rearrange?blogID=1251432539166387960&widgetType=Attribution&widgetId=Attribution1&action=editWidget&sectionId=menu
/
/search?max-results=5


물론 pull url도 콜론(:)이 포함되기 때문에 걸리겠지만 고건 다음 파트에서 미리 빼두어 처리하도록 하겠습니다.

Writing a Web Crawler 3 - Filter for My Domain(push target url)

자기 도메인 코드 및 argv 설정을 추가하였습니다.
별다른건 아니구요. A와 B구간 추가하여서 쉽게 걸러낼 수 있죠.
여기부턴 솔직히 정규식과 규칙에 따라서 크롤러의 성능이 결정되는 부분입니다.
anemone 같은 좋은 프레임워크를 사용하시면 좋긴 하지만, 상세한 커스텀이나
연구 차원에서는 nokogiri에 한표를 주고싶네요.

require 'open-uri'
require 'nokogiri'

def get_link(page)
 after_link = Array.new()
 before_link = page.css("a").map{|link|link['href']}  #가공 전
 before_link = before_link.uniq  # 중복제거
 for index in before_link # Loop!
   if(index.index(/:|^#|^&|\/\//) != nil)  # A) 정규식 추가
     if(index.index($t_url) != nil)    # B) 자신의 url 제외
       after_link.push(index)
     end
   else
     after_link.push(index)
   end
 end
 return after_link
end

$t_url = "http://www.hahwul.com"   # -> 나중에 argv로 변경하세요.
page = Nokogiri::HTML(open($t_url)) 
links = get_link(page)
puts links

Writing a Web Crawler 4 - Go Link

마지막 단계인 링크 넘어가기입니다.
이 단계를 마치면 페이지 분석하는 코드에서 크롤링 코드로 업그레이드 되겠네요.
단순합니다. 아까 찾은 URL리스트를 가지고 하나하나 타면서 추가로 더 push해주면 됩니다.
다만 얼마나 깊게 들어갈지 Depth와 중복되는 URL에 대한 제거 로직을 넣어주면 되겠지요.

일단 위에서 쓰던 코드를 좀 변경했습니다. 페이지 요청을 함수로 넣어야할 것 같아서요.
(이래서 포스팅하면서 작성하는건 참.. 그래요)

여기에 반복해서 체킹할 수 있도록 재귀함수를 좀 넣어봤습니다.



솔직히 그냥 설명용으로 막 짜놓은거라.. 성능도 성능이고 예외 안된 부분이 많아 에러가 발생할 포인트가 많습니다. 그냥 참고만해주세요~

일단 go_link 라는 함수를 하나 더 만들었습니다.

def go_link(crawl_link,links)
 temp_link = links.pop()
 links = links+get_link(temp_link)
 crawl_link.push(temp_link)
 links = links-crawl_link
 puts temp_link
 if(links.size)
  go_link(crawl_link,links)
 else
  return 0
 end
end
이 친구는 루프를 계속(재귀로) 빙빙 돌면서 크롤링된 array에 크롤링할 리스트를 넣어가며 하나하나씩 크롤링합니다. 일단 돌렸을땐 잘 돌아가네요. (혹시나 문제가 있다면 코드 조금씩 고쳐보죠 ㅋ)

해서 대충 풀 코드로 보면 이렇습니다. 짜잘한 불편함을 잡느라 코드가 몇개 더 추가되었어요.

require 'open-uri'
require 'nokogiri'

def get_link(t_url)
 if(t_url.index(/^\/|^.\//))
  t_url = $tg_url+"/"+t_url
 end
 puts t_url
 page = Nokogiri::HTML(open(t_url)) 
 after_link = Array.new()
 before_link = page.css("a").map{|link|link['href']}  #가공 전
 before_link = before_link.uniq  # 중복제거
 for index in before_link # Loop!
   if(index == nil)
    break;
   end
   if(index.index(/:|^#|^&|^\/\//) != nil)
     if(index.index(t_url) != nil)
       after_link.push(index)
     end
   else
     after_link.push(index)
   end
 end
 return after_link
end

def go_link(crawl_link,links)
 temp_link = links.pop()
 links = links+get_link(temp_link)
 crawl_link.push(temp_link)
 links = links-crawl_link
 puts temp_link
 if(links.size)
  go_link(crawl_link,links)
 else
  return 0
 end
end

t_url = "http://www.hahwul.com"   # -> 나중에 argv로 변경하세요.
$tg_url = t_url
crawl_link = Array.new()
links = get_link(t_url)
go_link(crawl_link,links)

Output



#> ruby test.rb
http://www.hahwul.com
http://www.hahwul.com
http://www.hahwul.com
http://www.hahwul.com/feeds/posts/default
http://www.hahwul.com/feeds/posts/default
http://www.hahwul.com/2016/06/hacking-jdwpjava-debug-wire-protocol.html
http://www.hahwul.com/2016/06/hacking-jdwpjava-debug-wire-protocol.html
http://www.hahwul.com//search?max-results=5
/search?max-results=5
http://www.hahwul.com//
/
http://www.hahwul.com//p/contact.html
/p/contact.html
http://www.hahwul.com//search/label/Media
/search/label/Media
http://www.hahwul.com//p/metasploit-meterpreter-cheat-sheet.html
/p/metasploit-meterpreter-cheat-sheet.html
http://www.hahwul.com//search/label/Design?updated-max=&max-results=7
/search/label/Design?updated-max=&max-results=7
http://www.hahwul.com//search/label/Video?updated-max=&max-results=7
/search/label/Video?updated-max=&max-results=7

이로써 아주 간단(?)하게 Ruby Nokogiri를 이용하여 웹 크롤러를 만들어보았습니다.
사실 Anemone를 쓰면 비교도 안되게 심플하고 쉽지만, 그래도 직접 커스텀하기에는 nokogiri가 더 좋은듯 하니 꼭 익혀두세요.

시간나면 Anemone도 포스팅 해보겠습니다. (정말 크롤링이 5줄이면 끝날듯..)

코드는 git에 따로 올려두겠습니다.

Reference

https://en.wikipedia.org/wiki/Web_crawler


HAHWUL

Security engineer, Gopher and H4cker!

Share: | Coffee Me:

1 comment:

  1. 아. 마지막 결과에 동일한 path 가 여러개 보이는건.. 제가 테스트한다고 puts 코드를 좀 넣어서 그래요. (아래 부분)

    #> ruby test.rb
    http://www.hahwul.com
    http://www.hahwul.com
    http://www.hahwul.com
    http://www.hahwul.com/feeds/posts/default
    http://www.hahwul.com/feeds/posts/default

    ReplyDelete