Ruby Nokogiri를 이용한 Web Parsing

웹 서비스를 탐색하는 도구들을 만들다 보면 많이 접하게 되는 작업이 하나 있습니다. 바로 HTML, XML 등 구조화된 문서를 파싱하는 작업인데요. 오늘은 Ruby의 강력한 파싱 라이브러리인 Nokogiri에 대해 이야기할가 합니다.

Web Parsing

Web Parsing이란 웹 페이지의 Response를 분석하여 Object화 하는 과정입니다. 어떻게 보면 Deserialize와 같은 개념이죠. 그래서 우리가 이러한 작업을 거치게 되면 웹 브라우저가 HTML 등을 읽어 화면에 표현하듯이 우리의 코드도 이를 파싱하여 데이터를 처리할 수 있게 됩니다.

간단하게는 원하는 Object를 찾는 행위부터 구현한다면 브라우징 엔진까지도 가능하겠죠.

Nokogiri

Nokogiri는 Ruby의 HTML/XML Parser입니다. Ruby 특유의 유연하고 쉬운 문법과 결합되어 정말 편리하게 사용할 수 있습니다. 또한 Ruby 라이브러리지만 실제 파싱 구간은 native parser를 사용하기 떄문에 속도 또한 빠릅니다.

https://nokogiri.org

웹 요청과 관련된 툴(예를들어 WVS, Crawler 등등)을 만들때 활용하면 정말 유용한 라이브러리죠.

Installation

nokogiri를 사용하기 위해선 nokogiri gem 설치가 필요합니다. ruby-gem이 설치되어 있다면 gem 명령을 통해 쉽게 설치할 수 있습니다.

gem install nokogiri

Troubleshooting

설치 과정 중 에러가 발생하는 경우도 있습니다. 에러에 따라 대응 방법이 대부분 다르겠지만, 공통적으로 많이 발생하는 에러에 대한 해결 방법입니다.

C 에러

apt-get install build-essential patch 

이외

apt-get install ruby-dev zlib1g-dev liblzma-dev

Usage

아무튼 이제 설치가 잘 되었는지 확인해볼까요? IRB를 열어 nokogiri를 불러봅니다.

irb
# irb(main):001:0> require 'nokogiri'
# => true
# irb(main):002:0> 

true 가 나온다면 정상적으로 로드된 것입니다.

Case Study

Load HTML/XML

nokogiri를 가지고 HTML 코드를 파싱하는 방법은 여러가지가 있습니다.

From the Internet

아무래도 nokogiri가 가장 많이 사용되는 것이 원격지에서 받아 바로 파싱하는 방법일텐데요. open-uri를 이용하여 간단하게 받아올 수 있습니다.

require 'open-uri'
page = Nokogiri::HTML(open("http://www.hahwul.com/"))

restclient를 이용해서도 받아올 수 있겠네요.

require 'nokogiri'
require 'restclient'

page = Nokogiri::HTML(RestClient.get("http://www.hahwul.com"))  
puts page.class   # => Nokogiri::HTML::Document

From a File

doc = File.open("blossom.xml") { |f| Nokogiri::XML(f) }

From a Code

html_doc = Nokogiri::HTML("<html><body><h1>Mr. Belvedere Fan Club</h1></body></html>")
xml_doc  = Nokogiri::XML("<root><aliens><alien><name>Alf</name></alien></aliens></root>")

CSS Selector

nokogiri를 통해 파싱된 Object에선 CSS Selector를 이용한 Object 조회가 가능합니다.

title = page.at_css "title"
# <Nokogiri::XML::Element:0x17ac6f4 name="title" children=[#<Nokogiri::XML::Text:0x17ac4ec "HAHWUL :: 하훌">

제가 읽어온 주소의 title content 영역의 데이터가 읽어진 것을 확인할 수 있네요.

<div id="funstuff">
   <p>Here are some entertaining links:</p>
   <ul>
      <li><a href="http://youtube.com">YouTube</a></li>
      <li><a data-category="news" href="http://reddit.com">Reddit</a></li>
      <li><a href="http://kathack.com/">Kathack</a></li>
      <li><a data-category="news" href="http://www.nytimes.com">New York Times</a></li>
   </ul>
</div>
page.css('li')
# <li><a href="http://youtube.com">YouTube</a></li>
# <li><a data-category="news" href="http://reddit.com">Reddit</a></li>
# <li><a href="http://kathack.com/">Kathack</a></li>
# <li><a data-category="news" href="http://www.nytimes.com">New York Times</a></li>

page.css('li')[0].text
# YouTube

Id and Tag

제 블로그 내 HTML 코드 중 짧은것을 하나 예시로 할까합니다. 위에 시간데이터와 View 를 표기하는 영역인데요, id를 Stats1_content라고 지정했었습니다.

<div id='Stats1_content' style='display: none;'>
<font color='black'><span class='hahwul count text-counter-wrapper' id='Stats1_totalCount'>
</span> ː Views</font>
</div>

자 이제 nokogiri를 이용해서 안에 내용을 읽어와볼까요?

page.css("div#Stats1_content")
irb(main):040:0* page.css("div#Stats1_content")
=> [#<Nokogiri::XML::Element:0x135eb24 name="div" attributes=[#<Nokogiri::XML::Attr:0x135eac0 name="id" value="Stats1_content">, #<Nokogiri::XML::Attr:0x135eaac name="style" value="display: none;">] children=[#<Nokogiri::XML::Text:0x135e37c "\n">, #<Nokogiri::XML::Element:0x135e2b4 name="font" attributes=[#<Nokogiri::XML::Attr:0x135e228 name="color" value="black">] children=[#<Nokogiri::XML::Element:0x135da6c name="span" attributes=[#<Nokogiri::XML::Attr:0x135d8a0 name="class" value="hahwul count text-counter-wrapper">, #<Nokogiri::XML::Attr:0x135d88c name="id" value="Stats1_totalCount">] children=[#<Nokogiri::XML::Text:0x135cae0 "\n">]>, #<Nokogiri::XML::Text:0x135c784 " ː Views">]>, #<Nokogiri::XML::Text:0x135c3d8 "\n">]>]

css를 이용해서 읽어옴과 동시에 div 안에 데이터들이 파싱된 형태로 로드되었고, 사용자는 더 상세하게 파싱해서 사용할 수도 있습니다.

select 하여 직접 사용하기

이번 포스팅 내용 중 마지막 부분입니다. nokogiri는 parsing 및 검색을 통해 찾아낸 데이터에 대해 select 할 수 있고, select를 하게되면 그 데이터 내 값이나 행위들을 자유롭게 다룰 수 있게됩니다.

test = page.css("div#Stats1_content").select
puts test.class

위와같이 css로 찾은 후 select 메소드를 사용하면 return 값으로 선택된 객체가 잡히죠. test 변수에 넣고 test 내 메소드를 통해서 값들을 호출할 수 있습니다.

irb(main):041:0> test = page.css("div#Stats1_content").select
=> #<Enumerator: [#<Nokogiri::XML::Element:0x135eb24 name="div" attributes=[#<Nokogiri::XML::Attr:0x135eac0 name="id" value="Stats1_content">, #<Nokogiri::XML::Attr:0x135eaac name="style" value="display: none;">] children=[#<Nokogiri::XML::Text:0x135e37c "\n">, #<Nokogiri::XML::Element:0x135e2b4 name="font" attributes=[#<Nokogiri::XML::Attr:0x135e228 name="color" value="black">] children=[#<Nokogiri::XML::Element:0x135da6c name="span" attributes=[#<Nokogiri::XML::Attr:0x135d8a0 name="class" value="hahwul count text-counter-wrapper">, #<Nokogiri::XML::Attr:0x135d88c name="id" value="Stats1_totalCount">] children=[#<Nokogiri::XML::Text:0x135cae0 "\n">]>, #<Nokogiri::XML::Text:0x135c784 " ː Views">]>, #<Nokogiri::XML::Text:0x135c3d8 "\n">]>]:select>
irb(main):042:0> puts test.class
Enumerator
=> nil

Reference