@keedi - Seoul.pm 리더, Perl덕후, 거침없이 배우는 펄의 공동 역자, keedi.k at gmail.com
Seoul.pm 펄 크리스마스 달력이 시작된지도 벌써 4년째입니다. 매년 12월마다 크리스마스 바로 전 날까지 기사가 이어져 오며 펄은 물론 펄과 연관된 다양한 주제를 다루고 있습니다. 올해 역시 24개의 기사가 이어진다면 이제는 거의 100여개에 가까운 기사가 모이는 셈입니다. 매일 하나의 기사가 열리길 기다리며 클릭하는 즐거움은 크리스마스 달력의 취지에 맞기는 하나 첫 페이지에서 기사의 목록을 한 번에 훑어볼 수 있도록 시각적으로 제공하지는 않아 시간이 지난 후 특정 기사를 찾기는 여간 힘들지 않습니다. 편집진의 게으름 때문(!?)인지 올해까지도 여전히 전체 기사 목록은 제공되지 않고 있죠.
"배고픈 자가 우물을 판다"라는 말도 있는데 Seoul.pm 펄 크리스마스 달력을 구미에 맞게 정리해볼까요?
필요한 모듈은 다음과 같습니다.
직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.
$ sudo cpan Mojo::UserAgent
사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.
$ cpan Mojo::UserAgent
Mojo::UserAgent
모듈은 Mojolicious 모듈의
하부 모듈이기 때문에 설치를 하면 Mojolicious
모듈이 설치됩니다.
Mojolicious는 웹프레임워크지만
의존 모듈이 없고 매우 가볍기 때문에 널리 사용되고 있습니다.
사실 제 경우 간단한 HTTP 요청을 처리할때 대부분의 경우 CPAN의 HTTP::Tiny 모듈을 즐겨 사용합니다.
하지만 요청 후 받은 응답의 HTML을 체계적으로 처리해야 하는 경우라면
Mojo::UserAgent 모듈과 Mojo::DOM 모듈을 사용하죠.
Mojo::UserAgent
와 Mojo::DOM
두 모듈 모두 Mojolicious 모듈에 속해있는 하부 모듈입니다.
웹페이지를 단순히 긁어오는 것이 아니라 제목이나, 저자, 첫 문단 등을 명확하게 구분하려면 단순한 문자열 비교나
정규표현식 보다는 HTML의 DOM 트리 구조를 이용해 각각의 요소에 접근하는 편이 유리합니다.
HTTP::Tiny
모듈은 HTTP 프로토콜 처리 그 자체만 다루는데 비해 Mojo::UserAgent
모듈은
Mojo::DOM
모듈과 연동해 HTML 문서의 DOM 트리를 다룰 수 있기 때문에 이런 류의 작업에는 딱이죠.
#!/usr/bin/env perl # # FILE: seoulpm-advent-calendar-digest.pl # use v5.16; use utf8; use strict; use warnings; use Mojo::UserAgent; say Mojo::UserAgent ->new ->get('http://advent.perl.kr/2013/2013-12-06.html') ->res ->text;
앞의 예제는 여섯째 날 기사인 "알록달록 perldoc" 기사의 HTML 코드를 화면에 출력합니다.
$ chmod 755 seoulpm-advent-calendar-digest.pl $ ./seoulpm-advent-calendar-digest.pl <!doctype html> <html xmlns:fb="http://ogp.me/ns/fb#"> <head> <title>여섯째 날: 알록달록 perldoc | R.I.P. @am0c - Seoul.pm 펄 크리스마스 달력 #2013</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="description" content="Seoul.pm Perl Advent Calendar 2013" /> ...
Mojo::UserAgent
모듈로 받아온 HTML에서 DOM 트리에 접근하는 코드는 다음과 같습니다.
my $url = "http://advent.perl.kr/2013/2013-12-06.html"; my $dom = Mojo::UserAgent->new->get($url)->res->dom;
다행히(?) Seoul.pm 펄 크리스마스 달력의 기사는 전체적으로 비슷한 구조를 가집니다. HTML 코드 내부를 살펴보면 다음과 같은 구조로 이루어져 있습니다.
<!doctype html> <html xmlns:fb="http://ogp.me/ns/fb#"> <head> <title>여섯째 날: 알록달록 perldoc | R.I.P. @am0c - Seoul.pm 펄 크리스마스 달력 #2013</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="description" content="Seoul.pm Perl Advent Calendar 2013" /> ... 메타 태그 ... 자바스크립트 ... CSS </head> <body> ... <div id="wrap"> <div class="nav top"> ... 네비게이션 및 메뉴 ... 메뉴 </div> <div id="cont"> <h1>여섯째 날: 알록달록 perldoc</h1> <h2>저자</h2> <p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후, <a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p> <h2>시작하며</h2> <p>대부분의 프로그래머는 자신만의 선호하는 편집기가 있기 마련입니다. 제각각의 이유를 가지고 편집기 또는 통합 개발 환경을 선택하겠지만 그 무수한 이유중 빠질 수 없는 것이 바로 문법 강조 기능입니다. 유닉스를 만들던 걸출한 해커들이야 분명히 검은 바탕에 흰 글씨로 엄청난 시스템을 만들었다고는 하지만 이러니저러니 해도 자신이 사용하는 소스 코드에 일관된 규칙으로 시각적으로 편안한 색깔이 덧대어진다면 가독성이 높아지는 것은 자명할 것입니다.</p> ... </div> ... 푸터 </div> </body> </html>
언듯 복잡해보이지만 우리에게 필요한 모든 정보는 <div id="cont">...</div>
사이에 있습니다.
우리가 사용할 HTML 요소는 모두 div#cont
하부에 있기 때문에
at
메소드를 사용해서 좀 더 하부의 DOM 트리를 선택해보죠.
my $url = "http://advent.perl.kr/2013/2013-12-06.html"; my $dom = Mojo::UserAgent->new->get($url)->res->dom->at('#cont');
정리할 목록을 정하면 어떤 항목들을 추출해야 할지 명확해지겠죠? 다음 항목을 추출한다고 생각해보죠.
우선 제목은 div#cont
항목 중 h1
요소에 해당하는 문자열입니다.
$dom
트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.
$dom->at('h1')->all_text
저자는 div#cont
항목 중 첫 번째 h2
요소 바로 다음 단락의 문자열입니다.
$dom
트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.
$dom->at('h2')->next->all_text
목차는 div#cont
항목 중 h2
요소에 해당하는 문자열 목록입니다.
사실 기사에서 h3
태그를 사용하는 경우도 있지만 목차 생성시에는 가장 큰 제목인 h2
항목만을
고려한다고 가정(h3
요소를 추출하는 것 역시 그리 어렵지는 않습니다. :)하죠.
$dom
트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.
my @toc; $dom->find('h2')->each(sub { return if $_->all_text eq '저자'; push @toc, $_->all_text; });
요약은 div#cont
항목 중 두 번째 h2
요소와 세 번째 h2
요소 사이의 모든 요소가 포함하는 문자열입니다.
$dom
트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.
my @desc; for ( my $e = $dom->find('h2')->[1]->next; $e; $e = $e->next ) { last if $e->type eq 'h2'; push @desc, $e->all_text; }
맙소사! 추출이 모두 끝났습니다. ;-)
내친 김에 명령줄에서 사용자의 입력을 받아 지정한 기사를 요약할 수 있도록 수정해보죠. 전체 코드는 다음과 같습니다.
#!/usr/bin/env perl # # FILE: seoulpm-advent-calendar-digest.pl # use v5.16; use utf8; use strict; use warnings; use Mojo::UserAgent; binmode STDOUT, ':utf8'; my $y = shift; my $d = shift; die "Usage: $0 <year> <day>\n" unless $y && $d; my $url = sprintf "http://advent.perl.kr/%d/%d-12-%02d.html", $y, $y, $d; my $dom = Mojo::UserAgent->new->get($url)->res->dom->at('#cont'); my $author = get_author($dom); my $title = get_title($dom); my $desc = get_desc($dom); my @toc = get_toc($dom); print <<"END_DIGEST"; 주소: $url 제목: $title 저자: $author 목차: @{[ join "\n ", @toc ]} 요약: $desc END_DIGEST sub get_author { shift->at('h2')->next->all_text } sub get_title { shift->at('h1')->all_text } sub get_toc { my $dom = shift; my @toc; $dom->find('h2')->each(sub { return if $_->all_text eq '저자'; push @toc, $_->all_text; }); return @toc; } sub get_desc { my $dom = shift; my @desc; for ( my $e = $dom->find('h2')->[1]->next; $e; $e = $e->next ) { last if $e->type eq 'h2'; push @desc, $e->all_text; } return join( "\n\n", @desc ); }
요약할 달력 기사를 지정할 수 있기 때문에 실행할 때 연도와 날짜를 명령줄 인자로 넘겨줍니다.
$ ./seoulpm-advent-calendar-digest.pl 2013 6
그림 1은 터미널에서 실행한 결과를 갈무리한 것입니다.
그림 1. 달력 기사 요약 결과 (원본)
간단하죠? :)
사실 Seoul.pm 펄 크리스마스 달력의 요약판이 존재하지 않은 덕분(?)에 @gypark님께서는 요약 페이지를 이미 제공하고 있으며, 이에 그치지 않고 요약 페이지를 만드는 법까지도 상세한 단계별 설명과 함께 소개하고 있습니다. @luzluna님께서는 펄 문서화 위키에 연도별 요약 페이지를 만들어주시기도 하셨죠. 어떤 측면(?)에서는 Seoul.pm 크리스마스 달력 요약판이 없었던 것이 더 긍정적이지 않았나 싶기까지 하군요. :-)
Mojolicious 모듈이 제공하는 Mojo::UserAgent 모듈과 Mojo::DOM 모듈을 이용하면 HTML 문서에서 필요한 항목을 추출해 요약하는 작업이 무척 수월해집니다. 물론 두 모듈을 사용하기 위한 DOM 트리와 CSS 선택자에 대한 최소한의 이해(또는 공부)는 필수겠죠?
Enjoy Your Perl! ;-)
EOT
Artwork by
@namanvara,
Hyungsuk Hong
& Inkyung Park.
Designed by
Hojung Youn
& Keedi Kim.
Articles by
Seoul Perl Mongers.
Edited by
Keedi Kim.
Hosting generously sponsored by
Yuni Kim.
Sponsored by
SILEX.
.-''' __ __ / \/ \/ \ =-_- | \. -____- / \ // /|| '' //| //|| == = == ==