태그:
Module1Add my vote for this tag REPL2Add my vote for this tag 방법론1Add my vote for this tag 스터디2Add my vote for this tag 새태그
, 모든 태그

Devel::REPL로 하는 심장이 좋아지는 개발

REPL 에 대한 간단한 정의를 하자면 Read Eval Print Loop 의 약자로써 현재 사용되고들 있는 스크립팅언어들의 interactive interpreter shell 의 다른 명칭(예전 이름) 이라고 이해 하면 좋겠다.

REPL은 예전 Lisp나 Scheme 에서 개발자가 직접 간단한 코드를 직접 입력하여 바로바로 결과값을 볼수 있게끔 해주는 편의성을 제공해주는 툴이 되겠다. 이미 Python (python, ipython)이나 Ruby (irb)는 Interactive 한 환경을 제공하고 있으며 Perl 또한 perl debugger 를 통해 비슷한 환경을 예전 부터 제공해왔었다.

아쉽게도 perl debugger 를 통해 interactive한 환경을 누리고 있는 개발자들이 드물며 이러한 이유가 프로그램을 키워나가거나 프로토타입핑 용도가 아닌 debugger인 용도로만 사용이되어 온듯하다. 그래서 그럴수 밖에 없었던 간략한 이유와 모자른 부분을 채워줄수 있는 모듈인 Devel::REPL에 대한 설명을 해보겠다.

Devel::REPL은 http://chainsawblues.vox.com/ 의 Matt Trout씨가 자신이 가지고 있는 이메일들을 필터 하기 위해 인터랙티한 환경에서 작업을 할수 없을까 모색 하다가 직접 만들게 된 모듈이 되겠다.

이 스터디의 주 목표는 Perl 을 이미 사용들 하고 계시거나 Perl 에 대해 좀더 알기 위한 분들에게 Perl 에도 인터랙티브한 환경이 있다는것을 전달하는것이며 그리고 하나의 예를 통해 interactive 한 환경을 통한 즐거운 개발을 시연 하도록 하겠다. Perl 의 수많은 모듈들을 인터랙티브한 환경에서 실행해 볼수 있는것만큼 큰 즐거움을 없을것이다.

다른 REPL 모듈과 특히 perl debugger와 Devel::REPL의 차이점

우선 Devel::REPL을 사용함으로 perl debugger나 다른 REPL 모듈등이 제공해주지 못하는 부분을 리스트 해보겠다.

  • perl debugger 에선 입력한 코드의 결과값을 보기 위해선 x, p 명령어를 이용해서 확인 할수 있겠다. 그에 비해 Devel::REPL은 바로바로 변수 이름을 입력하거나 객체를 생성하거나 함수를 실행할때 리턴값들을 자동으로 출력해 줄수 있겠다.

  • Multi line 지원인데 perl debugger에선 함수 선언을 할때 우리가 원하는 라인넘김을 사용할수가 없겠다 대신 \ 기호를 이용해 (keedi님, pung96님께 감사~) 라인넘김대신 사용하여 여러 라인으로 구성된 함수를 지정해 주어야 하지만 Devel::REPL에선 MultiLine? ::PPI 플러긴을 통해 라인넘김을 지원하게 되었다 (아직 문제가 되는 부분들이 있다 나중에 설명을).

  • Devel::REPL은 플러긴을 지원하는 시스템이 되겠다. MooseX? ::Object::Pluggable 를 이용하여 perl로 직접 추가 기능등을 구현 할수 있는가에 비해 perl debugger는 이미 완성되어 있는 perl interpreter의 추가 기능이라 부가적인 기능을 개발하기가 매우 힘들것이다.

  • 플러긴이 아직 많지는 않아 리스트만 해보겠다 이름을 통해 직관적으로 각 기능들이 무엇을 하는지 알수 있겠다. 이런 점들이 Devel::REPL이 perl debugger 보다 인터랙티브 환경에 더 적합한 점이 되겠으며 다른 언어의 REPL 환경보다 저 좋게 구현 할수 있다는 장점이 있겠다.
    • CompletionDriver
    • MultiLine
    • Colors.pm
    • Commands.pm
    • Completion.pm
    • DDS.pm
    • DumpHistory.pm
    • FancyPrompt.pm
    • History.pm
    • Interrupt.pm
    • LexEnv.pm
    • NewlineHack.pm
    • OutputCache.pm
    • Packages.pm
    • ReadLineHistory.pm
    • Refresh.pm
    • ShowClass.pm
    • Timing.pm
    • Turtles.pm

  • Devel::REPL에선 모든 입력들이 use strict 환경에서 적용된다.

  • Devel::REPL이 사용하는 모듈중에 namespace::clean이라는 모듈을 사용하는데 우리가 개발하는 모듈이 바뀔때마다 다시 리로딩 하여 이미 기존에 있는 package namespace를 refresh 할수 있는 기능이 있는것 같다. (확인 필요)

  • startup script을 사용할수 있다 (perl debugger 도 있는지 확인 필요)

Devel::REPL이 perl debugger보다 부족한점을 리스트 해보겠다.

  • perl debugger는 perl 이 설치된 시점에 이미 사용할수 있다는 장점이 있는가에 비해 Devel::REPL은 CPAN에서 직접 설치를 해줘야한다는 점이 있겠다 그리고 설치 모듈 수도 Moose를 요구하는 부분이 있어 많은 수의 설치가 실행된다. 물론 ubuntu에선 package로 제공되고 있지만 아쉽게도 버젼이 낮은듯하다.

  • Devel::REPL에 비해 속도가 매우 빠른듯 하다. 특히 실해되는 시점에서 부터.

  • perl debugger는 perl -de0명령어 하나로 접근성이 매우 좋고 기억하기가 편하다 그에 Devel::REPL은 perl -Ilib -MDevel::REPL -e "Devel::REPL->new->run;" 이라는 좀 적지 않은 perl one liner를 이용해 접근해야 하는 불편한 점이 있다.

  • perl debugger는 기본적으로 Lexical 변수를 저장할수 있다 그외 비해 Devel::REPL은 LexEnv? 플러긴을 통해 이를 지원한다.

  • Devel::REPL은 break, continue, watch 기능을 지원하지 않는다.

  • Devel::REPL은 symbol table에 있는 package name에 등록되어 있는 함수랑 변수를 따로 보여주는 기능이 없다. 대신에 직접 keys %main::를 입력하면 해당 값들이 나오겠다.

  • 이미 emacs의 gud 환경에서 perl debugger를 기가막히게 사용할수 있다. (물론 Stylish에서 이미 Devel::REPL 모드를 emacs 용으로 구현해 놓았다. 대신 perl 5.10이 필요하다)

여러 종류의 perl을 위한 REPL 환경을 제공하는 모듈이 CPAN 에 많이 올라와 있지만 특히 완성도가 좀 있는 Perl Console 같은것도 있지만 현재 Devel::REPL이 모던 스크립팅 언어 추세를 좀더 이해하고 플러긴 시스템을 지원한다는점에 있어 선택하게 되었다. 현재 인기상승중인 Moose를 이용해 개발 되었으며 플러긴 개발을 매우 쉽게 해줄수 있는 인터페이스를 제공하고 있는 점을 고려 해본다면 아직 미완성인 부분들이 있지만 현존하는 모든 언어들의 REPL중에서 매우 좋은 측에 선다고 생각된다.

Devel::REPL을 이용한 간단한 web scraping 예제

보통 우리가 perl script을 작성 하고 테스트 할때 바로바로 실행해서 확인 하는데엔 무리가 없지만 웹에서 컨텐츠를 가져오거나 여러 모듈을 실험해 볼때는 이런 방법은 시간을 많이 소요하게된다. 특히 웹에서 컨텐츠를 가져오는 작업은 보통 받아온 컨텐츠를 이리저리 굴려 보면서 원하는 값을 긁어 오는 작업에 집중해야 하는데 추출하는 로직을 수정해야 할때 마다 웹에서 컨텐츠를 가져와야 하게 된다면 매우 피곤하고 단순하면서 코딩 흐름을 방해하는 부분이 되겠다. 이를 해결해 주는 것이 Devel::REPL이 되겠다!!

우선 re.pl (Devel::REPL을 스크립으로 돌릴시 기본적으로 사용되는 이름이 되겠다. 여기서 re.pl이 REPL이라고 지적해주신 yuni님께 감사~) 의 이름을 가진 스크립안에 아래와 같은 코드를 작성에 Devel::REPL을 돌리도록 하겠다.

use strict;
use warnings;
use Devel::REPL;

my $repl = Devel::REPL->new;
$repl->load_plugin($_) for qw(
                               History
                               DumpHistory
                               LexEnv
                               MultiLine::PPI
                               Commands
                               Completion
                               DDS
                               FancyPrompt
                               ShowClass
                               Timing
                            );
$repl->run;

이 스크립을 perl re.pl로 돌리면 아래와 같은 interactive shell 환경이 실행 될것이다. 여기서 몇개의 주요 플러긴을 간략하게 소개 하도록 하겠다.

  • History.pm : bash 의 history기능과 비슷하다 생각하면 되겠다 리스팅은 안해 주지만 !를 prefix주고 숫자를 주면 해당 라인이 재실행 되겠다.
  • LexEnv? .pm : REPL에서 lexcial 변수로 정의되는 값들을 보존하게 해준다 (perl은 package변수와 lexical 변수가 구분된다 여기서 package 변수는 eval로 실행되면 symbol table로 저장되어 세션에서 보존되지만 lexcical 변수는 eval은 하고난뒤 삭제되어 이를 방지해주는 플러긴이 되겠다.)
  • DumpHistory? .pm : :dump라고 repl에 입력하면 지금까지 입력했던 값들을 리스트한다.
  • MultiLine? ::PPI.pm : 여러라인으로 구성된 함수를 선언하거나 구성된 값을 입력시 필요한 플러긴이 되겠다.
  • DDS.pm : 변수값들을 Data::Dump::Streamer 모듈을 이용하여 Data::Dumper와 같은 출력을 보여주는 유용한 플러긴
  • Timing.pm : 실행된 라인이 얼마나 걸렸는지 걸린 시간을 보여주는 아주 유용한 플러긴 (리얼타임 프로파일링)
  • FancyPrompt? .pm : 루비의 irb와 같은 prompt를 제공해준다.
  • 그외 플러긴은 나중에 소개

re.pl(main):075:0> $track_nodes[0]->findnodes('./tr')->[1]->findvalue('./td')
Took 0.0673990249633789 seconds.
$XML_XPathEngine_Literal1 = 6/6/2008 9:01:22 AMArtist :DJ Fuma buy;

re.pl(main):076:0>

여기서 scraping할 테스트 페이지는 내가 주로 듣는 라디오 playlist를 보여주는 페이지가 되겠는데 여기서 recently played items에 있는 track 정보들을 가져와서 출력해주는 예제가 되겠다.

우선 아래가 REPL을 통해 구현된 완성된 코드가 되겠다.

#!/usr/bin/perl
use strict;
use warnings;
use WWW::Mechanize;
use HTML::TreeBuilder::XPath;

my $url = "http://www.lounge-radio.com/code/pushed_files/recently.html";
my $mech = WWW::Mechanize->new;
$mech->get($url) or die "eeek no such page to retrieve! $!";

my $tree = HTML::TreeBuilder::XPath->new;
$tree->parse($mech->content) or die "No contents to parse";
$tree->eof;

my @track_nodes = $tree->findnodes('//tbody[@class="table"]');

foreach my $track (@track_nodes) {
    print "-"x25,"\n";
    foreach my $tr ($track->findnodes('./tr')) {
      my $td = $tr->findvalue('./td');
      $td =~ /
               (
                 \d+\/\d+\/\d+
                 \s
                 \d+:\d+:\d+\s\w{2}
               )      # match datetime and capture
               Artist # match Artist
               \s     # match whitespace
               :      # match  delimiter
               (.*)   # match the artist name and capture
               \s+    # match any length of whitespace
               buy    # match buy
             /ox
               and print "Date: $1\nArtist: $2\n";
      $td =~ /Track\s:(.*)/o and print "Track: $1\n";
      $td =~ /Album\s:(.*)/o and print "Album: $1\n";
    }
    print "-"x25,"\n";
}

우선 $tree->eof; 라인까지는 REPL이 필요없어도 쉽게 작성할수 있는 코드가 되어서 복사해서 repl에 붙여 넣으면 되겠다.

중간에 $mech->get($url) or die "eeek no such page to retrieve! $!"; 부분에서 2초 가까이 걸리는것을 알수 있겠다.

re.pl(main):023:0> $mech->get($url) or die "eeek no such page to retrieve! $!";
Took 2.48410296440125 seconds.
$HTTP_Response1 = HTTP::Response=HASH(0x9561674);

REPL의 장점은 우리가 관심있는 데이타를 실제 스크립을 다시 로딩 안해도 지속적인 작업을 받아온 데이타에 할수 있다는 점이 되겠다.

이제 웹에서 컨텐츠를 WWW::Mechanize로 가져온 상태고 이를 HTML::TreeBuilder::XPath로 파싱을 한 상태가 되겠다.

이제 여기서 부터 REPL의 기능을 최대한 누려 최대한 적은 양의 시간과 노력을 들여 개발을 시작해 보도록 하겠다.

우선 우리가 원하는 트랙 정보의 XPATH 값의 '//tbody[@class="table"]' 으로 쉽게 가져올수 있다는것을 그림으로 확인할수 있겠다.

<< 그림 firebug xpath >>

각 트랙을 담고 있는 테이블을 @track_nodes에 아래의 코드를 이용해 담도록 하겠다.

my @track_nodes = $tree->findnodes('//tbody[@class="table"]');

REPL에서 확인하면 몇개의 노드가 존재 하는지 알수가 있다 하지만 아쉽게도 아직 어떤 값들이 객체에 저장되어 있는가 알수가 없다.

re.pl(main):029:0> my @track_nodes = $tree->findnodes('//tbody[@class="table"]');
Took 0.279267072677612 seconds.
$ARRAY1 = [
            HTML::Element=HASH(0x95e0b30),
            HTML::Element=HASH(0x95e5964),
            HTML::Element=HASH(0x95edfa8),
            HTML::Element=HASH(0x95f3b80),
            HTML::Element=HASH(0x95fc35c),
            HTML::Element=HASH(0x9600fdc),
            HTML::Element=HASH(0x96082bc),
            HTML::Element=HASH(0x9610c08),
            HTML::Element=HASH(0x9614690),
            HTML::Element=HASH(0x961ddb4)
          ];

이 객체들이 어떤 메소드를 제공하는지 symbol table을 검색하여 확인 할수 있으며 객체 종류를 아는 이상 CPAN의 문서를 검색해서 알수 있겠다. 심지어 플러긴을 추가 해서 실시간으로 객체에 대한 문서를 끄집어 낼수도 있는 기능도 차후에나올수도 있겠다. 이런 플러긴 개념으로 인해 다른 REPL에선 상상하기 힘든 부분도 고려하게 되는 점이 매력적이다.

우선 첫번째 트랙에 무엇이있는지 확인해 보도록 하자

re.pl(main):030:0> $track_nodes[0]
Took 0.00596117973327637 seconds.
$HTML_Element1 = HTML::Element=HASH(0x95e0b30);

이것을 가지고 이제 이리저리 만져 보도록 하겠다. 우선 HTML::Element라는 객체가 첫번째 트랙으로 저장되어 있는걸 알수 있지만 HTML::Element은 우리에게 약간 생소한 모듈이기 때문에 어떤 메소들 제공하는지 확인 하도록 하자.

keys %HTML::Element라고 REPL에 치면 아래와 같은 매우 긴 리스트가 나오겠다.

$ARRAY1 = [
            'PRUNE_UP',
            '__ANON__',
.
.
.
생략
.
            'look_down',
            'insert_element',

상당히 많은 리스트가 나온다 그래서 기억에 HTML관련 함수 가 있었던 것을 기억하고 아래와 같이 검색 해보았다.

re.pl(main):041:0> grep /html/i, keys %HTML::Element::
Took 0.0062408447265625 seconds.
$ARRAY1 = [
            'as_HTML',
            'html_uc'
          ];

as_HTML이란 함수를 발견 했다 이를 이욯해 출력을 해보겠다.

re.pl(main):043:0> $track_nodes[0]->as_HTML
Took 0.0187687873840332 seconds.
<tbody "waking="&quot;Waking" -="-" 6/6/2008="6/6/2008" 9:01:22="9:01:22" @="@" album:="Album:" am.="AM." by="by" class="table" dj="DJ" from="from" fuma="Fuma" fuma's="Fuma&#39;s" lounge="Lounge" onmouseout="oMO(this)" onmouseover="oMOv(this)" played="played" title="Fuma&#39;s Lounge v3 - " up"="Up&quot;" up""="Up&quot;&quot;" v3="v3" waking="Waking"><tr><td width="111"></td><td align="left" rowspan="5" width="0"></td><td align="left" rowspan="5" valign="middle" width="42">

... 생략

매우 지저분한 HTML 코드가 출력이 되었다 어떤 데이타가 들어 있는지 좀더 쉽게 볼수 있도록 해보자 메뉴얼을 좀더 확인 해보니 as_HTML에 인자값을 넣어 인덴트를 할수 있다는것을 확인 할수 있었다. 그래서 아래와 같이 다시 한번 돌려 보았다.

re.pl(main):070:0> $track_nodes[0]->findnodes('./tr')->[1]->as_HTML(undef, "\t")
Took 0.0138430595397949 seconds.
<tr>
        <td align="left" class="artist" rowspan="3" valign="middle">
                <div align="center">6/6/2008 9:01:22 AM</div>
        </td>
        <td align="left" class="artist" rowspan="3" valign="middle">
        </td>
        <td align="left" class="artist" valign="middle"><img height="12" src="imgs/spacer.gif" width="1" /><b>Artist :</b></td>
        <td align="left" class="artist" valign="middle"><img height="12" src="imgs/spacer.gif" width="1" /><b>DJ Fuma</b></td>
        <td align="left" class="artist" rowspan="3" valign="middle">
                <div align="center"><a "="&quot;" class="style1" dj="DJ" fuma="Fuma" href="Error: Field &#39;BUYITMS&#39; not found" onclick="return clickreturnvalue()" onmouseover="dropdownmenu(this, event, &#39;anylinkdj_fuma_-_fumas_lounge_v3_-_waking_up_-_fumas_lounge_v3_-_waking_up.mp3&#39;)" target="_blank" title="buy ! Fuma&#39;s Lounge v3 - " up"="Up&quot;" von="von" waking="Waking"> buy</a></div>
        </td>
</tr>

여기서 확실히 내가 원하는 값인 6/6/2008 9:01:22 AM 그리고 Artist값이 DJ Fuma를 어디서 가져올수 있는지 쉽게 보여 주었다. as_HTML에 이런 기능이 있다는것을 바로바로 알수 있게된 점이 매우 기쁘다.

<< 여기서 곧바로 HTML::Tidy를 로드해서 html chunk를 파싱해서 tidy해주는것을 보여줌 그랬지만 HTML::Tidy가 업데이트가 안된지 오래되어서 test를 패스 못해서 각하.. 아이디어 제공좀...>>

여기서 내가 원하는 값들이 tr태그로 행으로 구분되어 있고 정작 원하는 값들은 td 태그안에 포함되어 있는 것을 확인 하였다.

다시 노드 검색을 ./tr XPATH값을 이용해 가져온 후 이 값을 findvalue를 이용해 안에 있는 텍스트 값을 가져올수 있겠다.

어떤 위치에 내가 원하는 값이 있는지 여러번의 시도가 필요 했는데 곧바로 REPL에 대고 실험 할수 있어서 매우 편했다.

re.pl(main):054:0> $track_nodes[0]->findnodes('./tr')->[0]->findvalue('./td')
Took 0.0159001350402832 seconds.
$XML_XPathEngine_Literal1 = \do { my $v = '' };
bless( $XML_XPathEngine_Literal1, 'XML::XPathEngine::Literal' );

re.pl(main):055:0> $track_nodes[0]->findnodes('./tr')->[1]->findvalue('./td')
Took 0.0143728256225586 seconds.
$XML_XPathEngine_Literal1 = 6/6/2008 9:01:22 AMArtist :DJ Fuma buy;

re.pl(main):056:0> $track_nodes[0]->findnodes('./tr')->[2]->findvalue('./td')
Took 0.0106561183929443 seconds.
$XML_XPathEngine_Literal1 = Track :Fuma's Lounge v3 - "Waking Up";

re.pl(main):057:0> $track_nodes[0]->findnodes('./tr')->[3]->findvalue('./td')
Took 0.0108120441436768 seconds.
$XML_XPathEngine_Literal1 = Album :Fuma's Lounge v3 - "Waking Up";

re.pl(main):058:0> $track_nodes[0]->findnodes('./tr')->[4]->findvalue('./td')
Took 0.00907802581787109 seconds.
Runtime error: Can't call method "findvalue" on an undefined value at (eval 241) line 8.

여기서 첫번째 값에서 트랙이 플레이된 시간과 아티스트 이름을 알수 있는데 이를 정규식으로 잡아 보도록 하겠다.

우선 날짜를 아래와 같이 구할수가 있었다.

re.pl(main):060:0> $track_nodes[0]->findnodes('./tr')->[1]->findvalue('./td') =~ /(\d+)\/(\d+)\/(\d+)/
Took 0.0140421390533447 seconds.
$ARRAY1 = [
            ( 6 ) x 2,
            2008
          ];

그리고 정규식을 조금씩 조금씩 키워 나가 아래와 같이 만들수 있었다.

re.pl(main):061:0>track_nodes[0]->findnodes('./tr')->[1]->findvalue('./td') =~ /(\d+\/\d+\/\d+\s\d+:\d+:\d+\sAM|PM)Artist\s:(.+)/
Took 0.0144000053405762 seconds.
$ARRAY1 = [
            '6/6/2008 9:01:22 AM',
            'DJ Fuma buy'
          ];

이를 응용해 내가 원하는 값을 위와 같은 방법으로 가져올수 있었었고 지금까지 내가 사용한 라인들을 모아 아까 완료되었던 코드들을 완성할수 있었다.

여기서 소개된 툴들과 방법론은 현대 스크립팅언어를 대표하는 Python과 Ruby에선 이미 기본적으로 사용되는 것이었었다. perl도 기본적으로 제공을 하고 있었지만 많은 인지도를 얻지를 못한듯하다. 허나 Devel::REPL을 이용하여 플러긴을 제작할수 있고 CPAN에 잠재워진 수많은 모듈들을 이를 통해 매우 손쉽게 접근할수 있는점을 생각하면 다른 언어에서 제공하는 REPL과는 수준이 다르다는 것을 알수가 있겠다.

아직 perl의 언어 특성상 완벽한 REPL은 지원하지 못하지만 (package변수랑 lexical변수가 나주어져 있다던지, 클로져 구분이라던지..) PPI나 Moose를 통한 수준 높은 툴들에 위해 금방 해결 되라라 믿는다.

Topic attachments
I Attachment Action Size Date Who Comment
jpgjpg repl01.jpg manage 28.0 K 07 Jun 2008 - 00:56 HeeWonKim  
Topic revision: r9 - 10 Oct 2009 - 03:12:27 - MaryPortillo
 
This site is powered by the TWiki collaboration platformCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback