4. 노코기리 튜토리얼
그럼 이제 노코기리 잼에 대해서 알아보겠습니다.
튜토리얼에 사용될 소스는 실제 동국대학교 학사 공지사항입니다. 챕터 3에서 일반 공지와 알림 공지('!')가 같은 내용을 담고 있어서 문제가 된다고 했는데, 이 곳에서 실제 소스를 살펴보면서 그 문제도 다룹니다.
살펴볼 소스는 다음과 같습니다. 이와 같은 소스 파일을 보고 싶을 때는 원하는 페이지에서 우클릭 후 '소스파일 보기' 혹은 '검사'로 확인 가능합니다. 그리고 이 소스를 다른 텍스트 에디터같은 곳으로 옮겨서 보고 싶을 때는 '검사'에서 'HTML 형태로 복사'같은 기능을 사용해서 복사를 하거나 직접 복사해서 사용하시면 됩니다. 'HTML 형태로 복사' 같은 기능은 특정 태그 안의 내용들만 살펴보고 싶을 때 자주 사용되는 기능입니다.
<tr>
<td>4472</td>
<td class="title">
<a href="view.jsp?spage=1&boardId=3638&boardSeq=14745726&id=kr_010801000000&column=&search=&categoryDepth=&mcategoryId=0">[국내교류] 2016학년도 1학기 한양대학교 수학안내</a>
<img src="/Web-home/manager/images/mbsPreview/icon_new.gif" alt="N" title="새글">
</td>
<td>탁상민</td>
<td>2016-01-22</td>
<td>126</td>
<td>
<img src="/mbs/kr/images/board/ico_file.gif" alt="파일">
</td>
</tr>
위에 가져온 소스 파일은 하나의 게시물에 관련된 것을 가져온 겁니다. 그 것도 일반적인 공지의 경우를 가져온 겁니다. 다른 경우는 따로 다룰 예정입니다. 조금만 기다려주세요. 그러면 다음에는 /app/modles/notice.rb/ 파일을 살펴보겠습니다.
class Notice < ActiveRecord::Base
end
여기서 DB에 입력하는 INSERT 방식을 보면 ORM 방식이라고 해서 다른언어에서 직접 소스에 넣어서 사용하는 SQL 쿼리 문장과 다른 것을 확인하실 수 있습니다. 이에 대한 내용은 아래 문서를 참고하시기 바랍니다.
(https://www.coffeepowered.net/2009/01/23/mass-inserting-data-in-rails-without-killing-your-performance/)
뭐 위의 공지 하나를 크롤링해오는 소스 파일을 구현해보면 다음과 같이 나옵니다.
require 'nokogiri'
require 'open-uri'
class Notice < ActiveRecord::Base
validates :link, :uniqueness => true
#학사공지
url = "http://동국대학교 학사 공지 1 페이지이므로 생략"
#노코기리 파서로 HTML 형태로 분류한 형태의 데이터를 data 변수에 저장한다.
data = Nokogiri::HTML(open(url))
#.css selector로 tbody 안의 tr 내용들만 가져온다.
@notices = data.css('tbody tr')
#각 tr을 for문으로 끝까지 반복하면서 작업을 진행한다.
@notices.each do |notice|
#첫 번째 요소가 숫자라면, 우리가 대상으로 하는 공지이므로 추출한다. 아니면 넘어간다.
if notice.css('td')[0].text.strip =~ /\A\d+\z/
Notice.create(
#제목
:title => notice.css('td.title a').text.strip,
#링크
:link => "http://www.dongguk.edu/mbs/kr/jsp/board/" + notice.css('td.title a')[0]['href'].strip,
#작성자
:writer => notice.css('td')[2].text.strip,
#작성일
:created_on => notice.css('td')[3].text.strip
)
else
next
end
end
end
직접쳐서 오타가 있을 수도 있지만 하나씩 살펴볼거니까 아래 내용에 집중해주세요.
require 'nokogiri'
require 'nokogiri'
require 'open-uri'
우선 nokogiri와 open-uri 잼을 사용하려면 이렇게 위에 선언을 해주거나 Gemfile 설정이 필요합니다. 이건 뭐 그냥 보험상 해주고 넘어가시면 됩니다. 가끔 프로젝트에서 잼 설정을 잘못하시면, 잼이 없다고 나오니까 이렇게 미리 사용한다고 해놓은 겁니다. 물론 Gemfile에 포함 시키시고 $bundle install 명령을 내리시거나 자체적으로 잼을 설치하시고 그 환경에서 프로젝트를 만들고 나서 생각할 문제입니다. 잼이 뭔지 전혀 모르겠다면 제 블로그의 다른 글을 참고하시거나 구글링으로 해결하시기 바랍니다.
validates :link, :uniqueness => true
이 부분은 유니크 키 설정으로 중복처리를 검사한다고 선언한것을 의미합니다. 예를 들어서 같은 대상을 여러번 크롤링하는 명령을 내렸을 때 이미 같은 공지가 DB에 있으므로 이 건 무시한다라는 의미입니다.
data = Nokogiri::HTML(open(url))
여기서 url에는 동국대학교 공지 1페이지에 해당하는 경로가 쓰여있겠죠? 이 페이지에 open-uri가 접속해서 노코기리 파서가 HTML 형태를 기준으로 Tree 구조를 만들어서 data에 담는다는 의미입니다. 무슨 말인지 잘 모르겠다구요? 아래 소스를 보시죠.
<html>
<head></head>
<body>
<div class="hello"></div>
</body>
</html>
이 소스를 보면, html tag 안에 head tag 그리고 body tga가 있습니다. 그리고 그 안에 div tag가 있는데, 그 div tag는 "hello"라는 class로 지정되어 있습니다. 만약에 이러한 소스가 한 줄로 표현이 되어 있더라도 노코기리가 속성 하나하나를 파악하고 분석해서 어느 태그 안에 어떤 것들이 있고 어떤 속성이 있는지들을 자체적으로 걸러내서 깔끔한 형태로 저장하는 작업을 도와줍니다. 그리고 역으로 노코기리를 활용해서 td tag를 모두 찾아라! class="hello"만 찾아라! 등의 명령을 내릴 수 있습니다.
@notices = data.css('tbody tr')
여기서는 .css 라는 놈이 보입니다. 메소드로 사용되고 있죠. 이 메소드에 대해 알아보겠습니다.
.css selector
.at_css와 .css가 있는데 .at_css는 하나의 값(보통 첫 번째 값)만 그리고 .css는 해당하는 모든 값들을 가져옵니다. 위에 소스를 보면 .css를 사용하였습니다. 그 이유는 .css로 가져온 구조를 여러 방식으로 활용할 예정이기 때문입니다. 계층적인 html 구조가 있다면 위 처럼 띄어쓰기로 상위와 하위 요소를 구분할 수 있습니다. 그 이외에도 다음과 같이 사용이 가능합니다. css를 아시는 분들은 자신이 원하는 방식으로 응용하여 사용하시면 됩니다. 왜냐하면 class, id 등등 기본적인 html과 css에서 사용하고 있는 것들을 그대로 문법으로 활용하고 있기 때문입니다. 문법 예시를 몇 가지 살펴보도록 하죠.
@doc.css('title') : "@doc에 있는 것들 중 모든 title tag를 가져온다."
아래는 위의 명령어에 대한 결과 예시입니다.["<title>example 1</title>", "<title>example 2</title>"]
@doc.css('div.featured a') : "div tag 중 class가 featured인 곳 중 a tag 요소를 가져온다."
- doc.css("div#footer_inner strong")[0] : div tag 중 id가 footer_inner인 곳 중 strong으로 감싸고 있는 부분을 추출한다."
자시헤 보면 '.' ',' '#' 이런 문자들이 보입니다. 위에서도 말씀드렸지만, css/stylesheet에서 실제로 사용되는 문법들입니다. 직관적으로 사용하기 위해서 nokogiri에서 만든 형태입니다. 때문에 이름도 css selector입니다.
이와 비슷하게 .xpath 라는 것도 있는데 xpath는 xml 파싱에 사용됩니다. 이 튜토리얼에서는 다루지 않겠습니다.
그럼 이제부터 세부적으로 접근해보겠습니다. tbody 안의 tr에 접근했죠? 위의 모든 값은 계층별로 접근이 가능합니다. 그러므로 for 문으로 각각의 td tag에 접근해보겠습니다. 그 전에 if 문에 있는 /\A\d+\z/라는 문장이 있습니다. 이건 문자를 의미하는데, 문자가 아닌 건 숫자라고 생각하고 숫자인지를 판별할 때 사용하는 문장입니다. 주석을 보시면 아실 수 있을 겁니다.
if notice.css('td')[0].text.strip =~ /\A\d+\z/
#something
else
next
end
td tag의 첫 번째 요소를 text(string) 형태로 추출하는데, 특수 부호같은 것은 알아서 제거를 해주라는 의미로 .strip 을 사용합니다. 벗었다는 의미죠? 그리고 그렇게 걸러낸 스트링 값이 숫자라면 어떻게 하고 아니면 어떻게 하라는 문장입니다. next는 continue; 같은 의미로 해당 차례의 명령어를 수행하지 말고 다음 차례로 넘어가라는 의미입니다.
- .text는 해당 tag 안의 text값을 가져오는 것을 의미합니다.
- .strip은 값에서 tab이나 쓸대없는 space 등을 제거하는 기능을 합니다. 특히나 .strip은 상황에 따라서 확인을 하고 사용하시는게 좋습니다. 노코기리도 모든 상황에 100% 완벽하게 적용되지 않기 때문에 제거하지 않고 넘어가는 경우도 있습니다.
- Notice.create() : Notice는 모델이고, create()는 튜플 하나를 생성한다는 의미입니다. 즉 row가 하나 늘어나겠죠?
- :title => xxx : Notice 테이블에 col 값 중 하나인 title 속성에 xxx를 할당한다는 의미입니다.
그럼 나머지 소스를 살펴보도록 합시다.
:title => notice.css('td.title a').text.strip
#<td class="title"><a> 안에 있는 텍스트를 가져옵니다.
:link => "http://www.dongguk.edu/mbs/kr/jsp/board/" + notice.css('td.title a')[0]['href'].strip
#요소 검색으로 저장된 각 URL을 보면 위의 http 부분은 생략되어 있고 나머지 뒷 부분만 저장되어 있습니다.
#즉, 내부 로직으로 앞의 값을 자동으로 붙여서 해당 링크로 들어간다는 의미죠. 그래서 "" 부분을 더하여 저장합니다.
#<td class="title"><a> 안의 첫 번째 요소 중 [href] 값을 순수하게 가져옵니다.
writer => notice.css('td')[2].text.strip
#세 번째 <td> 안의 텍스트를 순수하게 가져옵니다.
created_on => notice.css('td')[3].text.strip
#네 번째 <td> 안의 텍스트를 순수하게 가져옵니다.
주석이 곧 설명입니다. 이것으로 노코기리 사용법에 대한 튜토리얼은 마칩니다. 다음 챕터에서는 이렇게 가져온 결과물을 직접 확인해보는 작업을 해보겠습니다. 추가적으로 필요한 사항이나 궁금한 사항들은 이메일나 블로그로 문의주시고 원문 서적을 보고 싶다면, 아래 링크의 내용을 참고하십시오.
(https://classic.scraperwiki.com/docs/ruby/ruby_css_guide/)