Seoul.pm Perl Advent Calendarhttp://advent.perl.kr/2011/2012-01-02T05:58:17+09:00Hojung Youn, Keedi KimXML::Atom::SimpleFeedPerl 생태계 가이드http://advent.perl.kr/2011/2011-12-24.html<h2>저자</h2>
<p><a href="http://twitter.com/aer0">@aer0</a> -
Seoul.pm, #perl-kr의 정신적 지주,
Perl에 대한 근원적이면서 깊은 부분까지 놓치지 않고 다루는
<a href="http://aero.sarang.net/">홈페이지 및 블로그</a>를 운영하고 있다. aero라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>23일 동안 열혈 서울 펄 몽거스를 위시한 많은
펄 몽거들이 푸짐한 선물 보따리를 풀어놓았습니다.
관심을 가지고 지켜보신 분들이라면 Perl의
새로운 모습을 느꼈으리라고 생각합니다.
Perl은 과거 웹 프로그래밍이라면 CGI로 인식되던 시대에
잘나가던 시절이 있었습니다.
하지만 PHP의 등장으로 CGI로 웹을 만들던 사람들이
PHP로 넘어가게 되었고 그 후 Python, Ruby 같은 언어들이 인지도를 높혀가면서
Perl은 상대적으로 시야에서 멀어진 언어로 인식되어 왔던게 사실입니다.
그와 함께 훈련되지 않은 프로그래머들이 양산해놓은 많은 스파게티 CGI 코드들로 인해
유지보수가 힘들다는 근거 없는 루머에 시달리기도 했죠.
하지만 Perl은 유행과 시류에 휘둘리지 않고 조용히 안정성을 추구하며
함께 새로운 기술들을 받아들이면서 꾸준히 발전해 왔으며
2009년경 부터 이른바 <a href="http://www.slideshare.net/search/slideshow?searchfrom=header&q=modern+perl">Modern Perl</a>이라는 움직임이 일어나
다시금 중흥기를 맞고 있습니다.
이 글은 그 동안 Perl이 변화한 모습을 설명할 것입니다.
또, Perl을 새롭게 접하고자 하거나 다른 언어 사용자의 입장에서 Perl을 접하고자 할 때
어떤 과정을 통해 Perl에 익숙해질 수 있는지 설명할 것입니다.
마지막으로, 원하는 바를 구현하려고 할 때 어떤 기술을 사용할 수 있는지
Perl 생태계에 대한 총체적인 가이드라인을 제시할 것입니다.</p>
<h2 id="perl">Perl을 어떻게 배울 것인가?</h2>
<p>시중의 최신 한글 Perl 서적은
Learning Perl 5판 번역서인 <a href="http://books.perl.kr/lp5/">거침없이 배우는 Perl</a>입니다.
이 책으로 시작하는 것을 추천합니다.
책을 읽으면서 다음 내용도 같이 참고해서 살을 붙여나가세요.</p>
<ul>
<li><a href="https://github.com/aero/perl_docs/blob/master/Learning_Perl_5th_kor_review.md">거침없이 배우는 Perl 서평</a></li>
</ul>
<p>Perl은 OOP(Object Oriented Programming), AOP(Aspect Orientied Programming), FP(Functional Programming) 같은
특정 패러다임을 추구하지 않고 유연한 구조로 멀티 패러다임을 추구하고 있으며(아래 링크 참조)
Perl이 가지는 문법적 유연성으로 인해 어떤 방향으로든 쉽게 진화가 가능합니다.
이를 통해 고집스럽게 하위 호환성을 유지하면서도 새로운 모습으로 발전해나가는 힘이 되고 있습니다.</p>
<ul>
<li><a href="http://advent.perl.kr/2010/2010-12-14.html">Perl로 하는 함수형 프로그래밍</a></li>
<li><a href="http://advent.perl.kr/2010/2010-12-20.html">Moose로 OOP하기</a></li>
</ul>
<h2 id="perl5perl6">Perl 5 와 Perl 6는 어떻게 다른가?</h2>
<p>Perl 5 와 Perl 6는 같은 가족이지만 다른 언어라고 생각하면 됩니다.
쉽게 말하면 C와 C++의 관계 쯤으로 생각할 수 있습니다.
C++이 나오고 나서도 C는 여전히 함께 잘 쓰이고 있습니다.
Perl 6는 현재 본격적인 개발에 사용되기에는 주변 환경이 완전히 갖추어진 상태가 아닙니다.
따라서 현재는 Perl 5가 Perl을 대표하며 그간의 버전업 과정에서 Perl 6의 좋은 특징들을
지속적으로 backport 해왔습니다. Perl 5와 Perl 6는 같은 가족으로서 철학과 문화를 공유하며 같이 발전해 나갈 것 입니다.</p>
<h2 id="perl">Perl 설치와 모듈 설치</h2>
<p>다음 링크는 한눈에 알아볼 수 있는 Perl의 다운로드 페이지입니다.</p>
<ul>
<li><a href="http://www.perl.org/get.html">Download Perl Distributions</a></li>
</ul>
<p>Windows에서 Perl은 <a href="http://strawberryperl.com/">Strawberry Perl</a>과 <a href="http://www.activestate.com/activeperl">ActiveState Perl</a>이 있습니다.
사용 예는 다음을 링크를 참고하세요.</p>
<ul>
<li><a href="http://advent.perl.kr/2010/2010-12-01.html">MS Windows에서 Perl 활용하기</a></li>
<li><a href="http://advent.perl.kr/2010/2010-12-05.html">윈도우즈 환경에서 Perl은 어디에 써먹을 수 있을까?</a></li>
<li><a href="http://win32.perl.org/wiki/index.php?title=Main_Page">Win32 Perl Wiki</a></li>
</ul>
<p>Solaris, FreeBSD, Mac OS X, Linux 등 UNIX류 OS에는 Perl이 기본으로 설치되어 있습니다.
하지만 다른 패키지들이 의존하고 있는 시스템의 기본 Perl은 건드리지 않고
최신 버전의 Perl이나 모듈을 나름대로 독립적인 환경에서 설치하여 쓰려면 다음 글을 참고합니다.</p>
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-13.html">How to Use CPAN, Actually</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-16.html">perlbrew, local::lib, smartcd 를 이용하여 Perl 환경 구축하기</a></li>
</ul>
<p>요즘은 여러 PaaS(Platform as a Service) Cloud 서비스에서 Perl을 지원하고 있습니다.</p>
<ul>
<li><a href="http://docs.dotcloud.com/services/perl/">DotCloud</a></li>
<li><a href="http://docs.stackato.com/perl/index.html">Stackato</a></li>
<li><a href="https://www.redhat.com/openshift/community/kb/kb-e1014-how-to-deploy-the-perl-dancer-framework-on-openshift-express">How to deploy the Perl Dancer framework on OpenShift Express</a></li>
</ul>
<h2 id="perl">Perl의 성능은 어떤가?</h2>
<p>Perl의 성능은 여타 스크립트 언어들에 비해
속도와 메모리 효율성에 있어 우위에 있다고 알려져 있습니다.
현대적 어플리케이션의 대부분의 작업은 문자열(데이터)을 다루는 것이며,
다음은 각 언어들이 얼마나 그것을 빠르게 처리하고
메모리를 효율적으로 다루는가에 초점을 맞춘 벤치마크 자료입니다.
결과를 보면 Perl이 C, C++, PHP, Python, Ruby 등을 제치고 1등의 성능을 보여주고 있음을 볼 수 있습니다.</p>
<ul>
<li><a href="http://onlyjob.blogspot.com/2011/03/perl5-python-ruby-php-c-c-lua-tcl.html">Perl, Python, Ruby, PHP, C, C++, Lua, tcl, javascript and Java benchmark/comparison.</a></li>
</ul>
<p>벤치마크 결과에 이견이 있을 수는 있지만
별다른 테크닉을 사용하지 않고 평이한 문법을 그대로 사용한 상태에서
빠르고 효율적인 모습을 보여주었다고 하면 그 만큼 더 가치를 발하는 것이겠지요.</p>
<h2 id="perl">Perl 관련 싸이트는?</h2>
<h3 id="perl">Perl 언어관련</h3>
<ul>
<li><a href="http://www.perl.org">Perl 메인 싸이트</a> - perl.org</li>
<li><a href="http://learn.perl.org">Perl 학습 싸이트</a> - learn.perl.org</li>
<li><a href="http://perldoc.perl.org/">Perl 문서 싸이트</a> - perldoc.perl.org</li>
<li><a href="https://www.socialtext.net/perl5/">Perl 5 wiki</a> - socialtext.net/perl5</li>
<li><a href="http://www.perlfoundation.org/">Perl 재단</a> - perlfoundation.org</li>
</ul>
<h3 id="cpan">CPAN 모듈 관련</h3>
<p>CPAN은 라이브러리의 재사용성이 실제로 현실에 적용되도록 해준
선구자적인 모듈 저장소입니다.
CPAN의 모듈수는 10만개를 넘는 수준으로 타의 추종을 불허하며
PHP의 PEAR, R의 CRAN, Ruby의 rubygems, Python의 Pypi, Node.js의 npm 등의
모듈 저장소들이 CPAN의 모델을 따라 만들어 졌습니다.</p>
<ul>
<li><a href="http://www.cpan.org">오리지널 CPAN 싸이트</a> - cpan.org</li>
<li><a href="http://annocpan.org">CPAN모듈 문서에 유저들이 첨언을 넣는 싸이트</a> - annocpan.org</li>
<li><a href="http://cpantesters.org">CPAN모듈의 테스트결과 및 의존관계등에 대한 통계 싸이트</a> - cpantesters.org</li>
<li><a href="https://metacpan.org">최근에 생긴 CPAN을 좀 더 자세하고 쉽게 검색하게 해주는 싸이트</a> - metacpan.org</li>
<li><a href="http://mapofcpan.org">CPAN모듈들을 예쁘게 가시화해서 보여주는 싸이트</a> - mapofcpan.org</li>
</ul>
<h3 id="perl">Perl 관련 커뮤니티 싸이트</h3>
<p>외국:</p>
<ul>
<li><a href="http://www.yapc.org/">Perl 컨퍼런스 YAPC 정보</a> - yapc.org</li>
<li><a href="http://yapcasia.org">YAPC Asia</a> - yapcasia.org</li>
<li><a href="http://perlmonks.org">Perl 질문/답변 싸이트</a> - perlmonks.org</li>
<li><a href="http://stackoverflow.com/questions/tagged/?tagnames=perl&sort=active">StackOverflow Perl 질문/답변</a></li>
<li><a href="http://www.reddit.com/r/perl/">Reddit Perl 관련 뉴스/토론</a></li>
<li><a href="http://j2k.naver.com/j2k_frame.php/korean/perl-users.jp/">일본 Perl 유저 그룹</a> (일한번역)</li>
</ul>
<p>한국:</p>
<ul>
<li><a href="http://perl.kr/">한국 Perl 대표싸이트</a> - perl.kr</li>
<li><a href="http://seoul.pm">서울 Perl 몽거스</a> - seoul.pm</li>
<li><a href="http://cafe.naver.com/perlstudy">네이버 Perl 카페</a> - cafe.naver.com/perlstudy</li>
<li><a href="http://advent.perl.kr">서울 Perl 몽거스 크리스마스 달력</a> - advent.perl.kr</li>
<li><a href="http://webchat.freenode.net/?channels=perl-kr">#perl-kr IRC 채팅</a></li>
</ul>
<h3 id="perl">Perl 뉴스 및 정보 싸이트</h3>
<p>다음은 Perl의 최신 소식을 가장 빠르게 받아볼 수 있는 싸이트입니다.
관심있다는 사이트의 RSS를 구독하면 도움이 될 것입니다.</p>
<ul>
<li><a href="http://www.perl.com/">Perl.com</a> - perl.com</li>
<li><a href="http://www.nntp.perl.org/group/perl.perl5.porters/">Perl 5 언어 개발 메일링 리스트</a> (Perl 5 Porters)</li>
<li><a href="http://blogs.perl.org/">Perl 커뮤니티 블로그 플랫폼</a> - blogs.perl.org</li>
<li><a href="http://perlnews.org/">Perl 뉴스</a> - perlnews.org</li>
<li><a href="http://ironman.enlightenedperl.org/">Perl 블로그 포스트 집합소</a> - ironman.enlightenedperl.org</li>
<li><a href="http://perlsphere.net/">Perl 블로그 포스트 집합소</a> - perlsphere.net</li>
<li><a href="http://perlweekly.com/">주간지 이메일 아티클 모음</a> - perlweekly.com</li>
<li><a href="http://perlbuzz.com/">Perl 빅 이슈 모음</a> - perlbuzz.com</li>
<li><a href="http://www.modernperlbooks.com">Perl 생태계를 총체적으로 아우르는 주옥같은 지혜들</a> - modernperlbooks.com</li>
<li><a href="http://news.perlfoundation.org/">Perl 재단 뉴스</a> - news.perlfoundation.org</li>
</ul>
<h2 id="perl">Perl 관련 프레임웍 및 기술</h2>
<h3>웹 관련 프레임웍</h3>
<p>웹기술은 주로 C나 Perl로 하던 CGI에서 PHP같은 언어자체가 템플릿요소를 포함한 유행을 거쳐 요즘은 다시
웹서버/캐시레이어/프락시/미들웨어/웹어플리케이션 등의 레이어가 세분화된 프레임웍 기반의 개발이
대세가 되고 있습니다. Perl은 이런 시대적 흐름에 맞춘 각종 웹 관련 프레임웍을 갖추고 있습니다.</p>
<ul>
<li><a href="http://plackperl.org/">Plack</a> - 표준적인 웹서버/웹어플리케이션 인터페이스
<ul>
<li><a href="http://www.slideshare.net/miyagawa/deploying-plack-web-applications-oscon-2011-8706659">Deploying Plack Web Applications</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-19.html">Tatsumaki로 비동기 웹 서비스 구축하기</a></li>
</ul></li>
<li><a href="http://www.catalystframework.org/">Catalyst</a> - 본격적인 Perl 웹 프레임워크(Ruby의 Rails와 Python의 Django 급)
<ul>
<li>대표적인 레퍼런스 싸이트는 <a href="http://www.bbc.co.uk/iplayer/">BBC iplayer</a>와 세계 최대 성인 동영상 싸이트 Youp*rn</li>
<li><a href="http://www.wikivs.com/wiki/Catalyst_vs_Ruby_on_Rails">Catalyst vs Ruby on Rails</a></li>
<li><a href="http://mdk.per.ly/2011/12/06/perl-rocks-latin-america/">남미 W3C 주최 공공정보 웹서비스화 경연대회 Perl Catalyst팀 우승</a></li>
</ul></li>
<li><a href="http://perldancer.org/">Dancer</a> - 경량 웹 프레임워크 (Ruby의 Sintara와 Python의 Flask 급)
<ul>
<li><a href="http://www.slideshare.net/xSawyer/perl-dancer-for-python-programmers">Perl Dancer for Python programmer</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-06.html">초소형 프레임워크와 함께 춤을</a></li>
</ul></li>
<li><a href="http://mojolicio.us/">Mojolicious</a> - 리얼타임 웹 프레임워크
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-20.html">Mojolicious, HTML5, WebSocket을 이용한 비동기 채팅</a></li>
</ul></li>
<li><a href="https://www.socialtext.net/perl5/web_frameworks">기타 Perl 웹 프레임웍들</a></li>
<li><a href="https://metacpan.org/release/PocketIO">Pocket.io</a> - Socket.IO Plack application
<ul>
<li><a href="http://jjnapiorkowski.typepad.com/modern-perl/2011/09/monday-newbie-corner-long-polling-realtime-web-applications.html">Monday Newbie Corner: Long polling / realtime Web applications?</a></li>
</ul></li>
<li><p><a href="https://metacpan.org/release/Web-Hippie">Web::Hippie</a> - Web toolkit for the long hair, or comet</p>
<ul>
<li><a href="http://www.slideshare.net/clkao/anymq-hippie-and-the-realtime-web">AnyMQ, Hippie, and the realtime web</a></li>
<li><a href="http://showmetheco.de/articles/2010/11/timtow-to-build-a-websocket-server-in-perl.html">TIMTOW to build a WebSocket server in Perl</a></li>
</ul></li>
<li><p><a href="http://template-toolkit.org/">Template Toolkit</a></p></li>
<li><p><a href="http://xslate.org/">Xslate</a> - Scalable template engine for Perl5</p></li>
<li><p>대표적 ORM</p>
<ul>
<li><a href="http://www.slideshare.net/ranguard/dbixclass-introduction-2010">DBIx::Class for beginners</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-17.html">DBIx::Class로 스키마 관리하기</a></li>
<li><a href="http://advent.perl.kr/2010/2010-12-11.html">Fey, Fey, Fey</a></li>
</ul></li>
</ul>
<h3>비동기/네트웍/동시성 프레임웍</h3>
<p>Perl의 Thread는 Python, Ruby와 다르게 GIL(Global Interpreter Lock)이 없어 multi-core CPU 환경에서
모든 core를 사용할 수 있습니다.
<a href="https://metacpan.org/search?q=anyevent">AnyEvent</a>, <a href="https://metacpan.org/search?q=POE">POE</a> 등 비동기 이벤트 기반 프레임워크나
<a href="https://metacpan.org/release/Coro">Coro</a>와 같은 Coroutine도 지원합니다.
이외에도 fork 기반의 다양한 병렬 처리 모듈이 준비되어 있습니다.
<a href="https://metacpan.org/search?q=anyevent">AnyEvent</a> 모듈을 만든 <a href="https://metacpan.org/author/MLEHMANN">Marc Lehmann</a>이
AnyEvent에 쓰기위해 만든 <a href="https://metacpan.org/module/libev">libev</a>는 우수한 성능이
입증되어 Python의 Twisted, Ruby의 EventMachine, Node.js에서도 기반 라이브러리로 가져다 쓰고 있습니다.</p>
<ul>
<li><a href="https://metacpan.org/search?q=anyevent">AnyEvent 모듈</a></li>
<li><a href="https://metacpan.org/release/Coro">Coro 모듈</a></li>
<li><a href="http://poe.perl.org/">POE 공식 사이트</a></li>
</ul>
<p>참고 글:</p>
<ul>
<li><a href="http://justin.harmonize.fm/index.php/2008/09/threading-model-overview/">A Threading Model Overview</a></li>
<li><a href="http://t-a-w.blogspot.com/2006/10/why-perl-is-great-language-for.html">Why Perl Is a Great Language for Concurrent Programming</a></li>
<li><a href="http://www.openfusion.net/perl/parallel_processing_perl_modules">Parallel Processing Perl Modules</a></li>
<li><a href="http://d.hatena.ne.jp/tokuhirom/20090924/1253758449">how to write fast server with perl</a></li>
</ul>
<h3 id="gui">GUI 및 그래픽</h3>
<ul>
<li><a href="https://metacpan.org/release/Gtk2">GTK2</a>
<ul>
<li><a href="http://gtk2-perl.sourceforge.net/">gtk2-perl home</a></li>
<li><a href="http://advent.perl.kr/2010/2010-12-24.html">Gtk2 programming with DSL</a></li>
</ul></li>
<li><a href="https://metacpan.org/release/Gtk3">GTK3</a></li>
<li><p><a href="https://metacpan.org/release/Win32-GUI">Win32::GUI</a></p>
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-07.html">윈도우 환경에서 화면 캡쳐 후 자동 저장 기능의 구현</a></li>
</ul></li>
<li><p><a href="https://metacpan.org/release/Wx">wxWidgets</a></p>
<ul>
<li><a href="http://padre.perlide.org/">wxWidgets기반 Perl IDE Padre</a></li>
</ul></li>
<li><a href="https://metacpan.org/release/Tk">Tk</a></li>
<li><a href="https://metacpan.org/release/Tkx">Tkx</a></li>
<li><a href="https://metacpan.org/release/Qt">Qt</a></li>
<li><a href="https://metacpan.org/release/Prima">Prima</a>
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-15.html">한 이미지 안에 들어있는 사진들 추출하기 Prima 모듈</a></li>
</ul></li>
<li><a href="https://metacpan.org/release/IUP">IUP</a></li>
<li><a href="https://metacpan.org/release/FLTK">FLTK</a></li>
<li><a href="https://metacpan.org/release/XUL-Gui">XUL</a></li>
<li><a href="https://metacpan.org/release/GD">GD</a></li>
<li><a href="https://metacpan.org/release/Imager">Imager</a></li>
<li><a href="https://metacpan.org/release/PerlMagick">ImageMagick</a></li>
<li><a href="https://metacpan.org/release/SDL">SDL</a>
<ul>
<li><a href="http://sdl.perl.org">Perl SDL</a></li>
</ul></li>
<li><a href="https://metacpan.org/release/OpenGL">OpenGL</a>
<ul>
<li><a href="http://ue.o---o.eu/">Games::Construder</a></li>
</ul></li>
</ul>
<h3>과학 및 수치계산</h3>
<ul>
<li><a href="http://pdl.perl.org/">PDL</a> - Perl Data Language, Scientific computing with Perl
<ul>
<li><a href="http://www.freesoftwaremagazine.com/articles/cool_fractals_with_perl_pdl_a_benchmark">Generating cool fractrals Matlab vs PDL and others</a></li>
</ul></li>
<li><a href="https://metacpan.org/search?q=Math">Math modules</a></li>
<li><a href="https://metacpan.org/release/SOOT">SOOT</a> - Use <a href="http://root.cern.ch/drupal/">ROOT</a> in Perl</li>
<li><a href="http://www.bioperl.org">BioPerl</a> - Perl tools for bioinformatics, genomics and life science
<ul>
<li><a href="http://advent.perl.kr/2010/2010-12-18.html">Perl과 생명정보학</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-05.html">NCBI PubMed와 Perl</a></li>
</ul></li>
<li><a href="http://circos.ca/">Circos</a> - Circular visualization</li>
<li><a href="http://pdl-stats.sourceforge.net/">PDL::Stats</a> - a collection of statistics modules in Perl Data Language</li>
<li><a href="https://metacpan.org/release/Statistics-R">Statistics::R</a> - Perl interface with the R statistical program</li>
<li><a href="http://www.perlmonks.org/?node_id=599596">recommendations on scientific computing with Perl</a></li>
<li><a href="http://www.perlmonks.org/?node_id=284324">Perl & Math: A Quick Reference</a></li>
</ul>
<h3>시스템관리 및 자동화</h3>
<p>Perl은 Linux/UNIX류 운영체제라면 어디에나 기본적으로 설치되어 있고
그 외 다양한 OS에도 포팅되어 있으며 하위 호환성을 잘 지키며
안정적이고 텍스트 처리에 뛰어나다는 장점 때문에 시스템 관리에 주류 언어로 사용되어 왔습니다.
CPAN에는 SSH, Telnet, FTP, SNMP 등 각종 시스템 관리에 필요한 모듈과 툴이 넘쳐납니다.</p>
<ul>
<li><a href="http://shop.oreilly.com/product/9780596006396.do?green=f33d6c9a-04e8-4123-ac41-044b892a51c9&cmp=af-mybuy-9780596006396.IP">Automating System Administration with Perl</a></li>
<li><a href="http://perltraining.com.au/notes/sysadmin.pdf">Perl for System Administration - Perl Training Australia</a></li>
<li><a href="http://code.google.com/p/slack/">slack</a>
<ul>
<li><a href="http://wiki.kldp.org/wiki.php/SlackHowto">slack 소개</a></li>
</ul></li>
<li><a href="http://rexify.org/">Rex</a> - 시스템관리 자동화 툴</li>
<li><a href="http://www.opsview.com">Opsview</a> - Catalyst 웹프레임워크, Nagios 기반 모니터링 시스템</li>
</ul>
<h3>단일파일 배포 및 패키징</h3>
<ul>
<li><a href="https://metacpan.org/release/PAR-Packer">PAR::Packer</a>
<ul>
<li><a href="http://mabook.com/blog/entry/perl-%BF%A1%BC%AD-Wx-%BF%CD-PAR-%C6%A9%C5%E4%B8%AE%BE%F3">Perl로 GUI로 프로그래밍해서 exe로 배포하고 싶다면?</a></li>
<li><a href="http://happydal.blogspot.com/2010/08/perl-%EC%9D%84-exe%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90.html">Perl을 EXE로 배포해보자</a></li>
</ul></li>
<li><a href="http://www.cava.co.uk/">CavaPackager</a>
<ul>
<li><a href="http://honeyperl.tistory.com/entry/Tool-Cava-Packager">Cava Packager 사용기</a></li>
</ul></li>
<li><a href="http://www.activestate.com/perl-dev-kit">PerlApp</a></li>
<li><a href="http://blog.naver.com/PostView.nhn?blogId=pulsori&logNo=140050516302">Perl에서의 컴파일과 크로스플랫폼 지원 정보</a></li>
</ul>
<h2>정리하며</h2>
<p>Perl의 세계는 위에서 다 언급하지 못할 정도로 방대합니다.
그 모든 것을 알려드리기에는 이 글로서는 역부족인 것 같네요.
더 궁금하신 것들이 있으면 주위의 Perl 몽거들에게 도움을 청하면 친절하게 알려줄 것입니다.
혹시 아나요, 내년 Perl 달력에서는 여러분의 글이 실리게 될지... :)</p>
<p>아무쪼록 이 글이 부족하나마 Perl 여행을 떠나시려는 분들께
도움이 되었으면 합니다.</p>
<p>Merry Christmas!! ;-)</p>
2011-12-24T00:00:00+09:00aer0Perl로 타자연습을!http://advent.perl.kr/2011/2011-12-23.html<h2>저자</h2>
<p><a href="http://twitter.com/1985xmas">@1985xmas</a> -
올해 9월에 홍콩과기대 컴퓨터공학과 대학원 석사 과정에 갓 입학한 새내기 유학생.
대학원 입학 후 텍스트 처리를 위해 Perl을 공부하다 Perl의 매력에 완전히 빠져버렸다.
주로 사용하는 아이디는 1985xmas와 specno1이며
<a href="http://specno1.blog.me/">블로그</a>에 Perl 강좌를 올리고 있다.
블로그에서 사용하는 이름인 “초승달”에는
미래가 항상 현재보다 더 밝아지기를 바라는 마음이 담겨 있다.</p>
<h2>시작하며</h2>
<p>대학원 입학 후 Perl을 처음 접하고 Perl에 완전히 반해
블로그에 Perl 강좌를 올리기 시작했습니다.
아직 부족한 점이 많은 제 블로그에 감사하게도
<a href="http://twitter.com/am0c">@am0c</a>님께서 방문해주셨고,
Perl 크리스마스 달력 기사 제의를 해 주셨습니다.
다른 분들에 비해 실력면에서나 경험면에서나 부족한
제가 과연 제대로 된 기사를 쓸 수 있을까 하는 걱정이 들었지만 용기를 내어 이렇게 기사를 쓰게 되었습니다.</p>
<p>오늘 함께 만들어볼 프로그램은 타자 연습 프로그램입니다.
화면에 타자 연습 문장을 띄워주고 사용자가 그 문장을 따라 입력하면
타자 속도와 정확도, 연습 시간을 계산해 보여주며 어느 위치에서 오타가 났는지도 알려줍니다.
연습하고 싶은 문장 파일은 사용자가 직접 선택할 수 있습니다.
프로그램을 종료하게 되면 평균 속도와 평균 정확도, 총 연습 시간을 출력해줍니다.</p>
<p>영문 타자를 연습하도록 프로그램을 만든 상태지만
멀티바이트 문자 지원을 추가하면 <em>수 많은 언어로</em> 타자연습을 할 수 있을 것입니다.
그러면 이제 본격적인 설명에 들어가도록 하겠습니다!</p>
<h2>프로그램 설계</h2>
<p>프로그램 설계를 위해 <em>대략 코드</em>(pseudocode, 수도코드)를 짜보도록 하겠습니다.
사실 pseudocode에 대응되는 표준 용어는 '의사(疑似) 코드'입니다.
하지만 '대략 코드'라는 말이 더 와닿지 않나요?
어쨌든 '대략적인' 코드를 만들어 보도록 하겠습니다.
제가 짜 본 대략 코드는 다음과 같습니다.</p>
<ul>
<li>타자 연습 문장 자료를 입력받는다.</li>
<li>연습 문장을 화면에 출력한다.</li>
<li>사용자로부터 입력을 받는다.</li>
<li>사용자가 문장을 입력하는 데 걸린 시간을 측정한다.</li>
<li>몇 글자를 입력했는지를 계산한다.</li>
<li>올바로 입력한 글자와 오타를 구분한다.</li>
<li>오타의 위치, 연습 시간, 정확도, 속도를 출력한다.</li>
<li>위 작업을 반복한다.</li>
</ul>
<h2>실제 코드</h2>
<p>실제 코드는 다음과 같습니다.
대부분의 작업은 <code>oneSentence()</code>라는 함수를 만들어서 처리했습니다.
나무보다 숲을 먼저 본다는 의미에서 프로그램 전체의 구조를 먼저 알아보고,
<code>oneSentence()</code> 함수를 나중에 보도록 하겠습니다.
<code>oneSentence()</code> 함수의 구현을 제외한 프로그램의 주요 부분을 먼저 보겠습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.012;
use strict;
use warnings;
use Time::HiRes qw(gettimeofday);
use Term::ReadKey;
open my $data, '<', $ARGV[0] or die "Cannot read the file: $!";
my $mode;
my @inputArray;
my @outputArray;
my $totalTime = 0;
my $totalLength = 0;
my $totalTyping = 0;
my $totalCorrect = 0;
my $arrayIndex;
</pre>
<p>프로그램의 시작 부분입니다.
시간을 정교하게 측정하기 위해 마이크로초 단위까지 시간을 측정해주는
<a href="http://search.cpan.org/perldoc?Time::HiRes">Time::HiRes</a> 모듈을 사용했습니다.
사용자가 키를 누른 후에 엔터키를 누르지 않아도
키보드로부터 입력을 받을 수 있도록 <a href="http://search.cpan.org/perldoc?Term::ReadKey">Term::ReadKey</a> 모듈을 사용했습니다.
<code>$data</code> 변수는 연습 문장 텍스트 파일을 불러오는 데 쓰입니다.</p>
<pre class="brush: perl;">
say "\n***********************************************************";
say "* *";
say "* Korea Perl Christmas Calendar - Typing Trainer v1.0 *";
say "* *";
say "* Merry Christmas! ^^ *";
say "* ------------------------- *";
say "* v1.0 - 2011. 12. 22. *";
say "* *";
say "* Type the sentence on the screen! *";
say "* If you want to quit, just type 'quit'. *";
say "* *";
say "* What training mode do you want? *";
say "* 1. Story Mode (sentences appear sequencially) *";
say "* 2. Random Mode (sentences appear randomly) *";
say "* *";
say "***********************************************************\n";
print " Type a number ---> ";
# 연습 문장 파일을 읽어온다.
while (<$data>) {
push @inputArray, $_;
}
# 연습 모드를 입력받는다.
while (1) {
$mode = <STDIN>;
chomp $mode;
if (($mode ne "1") && ($mode ne "2") && ($mode ne "quit")) {
print " Type a number ---> ";
next;
} else {
last;
}
}
say "\n/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\".
"/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\/\\\n";
</pre>
<p>프로그램을 실행하면 화면에 처음 나타날 부분입니다.
사용자가 <code>1</code>을 입력하면 연습 문장 파일에 있는 문장을 순서대로 출력해주고,
<code>2</code>를 입력하면 문장 순서를 뒤섞어서 출력해줍니다.
1번은 내용이 이어지는 문장들을 연습할 때,
2번은 각각 독립적인 문장들을 연습할 때 쓰입니다.
눈여겨볼 부분은 연습 문장 파일을 읽어오는 while문의 위치입니다.
사용자가 1번과 2번중 무엇을 선택할지 고민하는 동안
프로그램은 연습 문장 파일을 읽어오는 작업을 합니다.
나름 설계를 최적화(-_-!) 한다고 신경쓴 부분이니 너그러운 칭찬 부탁드립니다. ㅠㅠ</p>
<pre class="brush: perl;">
if ($mode eq "1") {
$arrayIndex = 0;
} elsif ($mode eq "2") {
$arrayIndex = int(rand(@inputArray));
}
if (($mode eq "1") || ($mode eq "2")) {
while (1) {
@outputArray = oneSentence($inputArray[$arrayIndex]);
if (@outputArray == 4) {
if ($mode eq "1") {
++$arrayIndex;
} elsif ($mode eq "2") {
$arrayIndex = int(rand(@inputArray));
}
if ($arrayIndex >= @inputArray) {
$arrayIndex = 0;
}
$totalTime += $outputArray[0];
$totalLength += $outputArray[1];
$totalTyping += $outputArray[2];
$totalCorrect += $outputArray[3];
}
else {
last;
}
}
}
say "\n\n----- Your total score -----";
say " Time: " . (int(100 * $totalTime) / 100) . " seconds";
if ($totalLength != 0) {
say "Correctness: " . (int(10000 * $totalCorrect / $totalLength) / 100) . "%" . " ($totalCorrect / $totalLength)";
} else {
say "Correctness: not applicable ($totalCorrect / $totalLength)";
}
if ($totalTime != 0) {
say " Speed: " . (int(60 * 100 * $totalTyping / $totalTime) / 100) . " keys / minute (total $totalTyping keys)\n";
} else {
say " Speed: not applicable\n";
}
close $data;
</pre>
<p>사용자의 선택에 따라 문장 출력 순서를 결정해준 뒤 타자연습을 시작합니다.
'한 문장어치 타자 연습하기'의 모든 과정을 <code>oneSentence()</code> 함수가 도맡아
하기 때문에 단순히 무한루프 속에서 함수를 호출하는 것만으로 모든 작업이 끝납니다(함수의 위엄!).
한편 이 함수는 호출될 때마다 사용자의 타자 연습 시간,
문장 길이, 총 타수, 올바르게 입력한 타수를 반환합니다.
그 정보를 받아들이는 부분이 코드에 구현되어 있습니다.
한편 사용자가 종료 명령을 내렸을 때에 해당하는 부분도 <code>else { last; }</code>로 구현되어 있습니다.</p>
<p>자! 벌써 프로그램 구조 분석이 끝났습니다.
역시 함수를 만들어서 작업을 처리하니 프로그램 전체 구조에
해당되는 코드가 짧아져 보기도 편하고 이해하기도 쉽습니다.
그러면 이제 오늘의 주인공인 <code>oneSentence()</code> 함수의 내부를
순서대로 보도록 하겠습니다.</p>
<pre class="brush: perl;">
sub oneSentence {
# 인자를 입력받는다.
my ($original) = @_;
# 개행 문자를 없애고 출력한다.
chomp $original;
say $original;
# 한 글자씩 배열에 넣는다.
my @arrOriginal = split //, $original;
# ... 중략 ...
}
</pre>
<p>함수의 앞부분입니다.
하나의 연습문장을 입력받아 한 문장의 타자 연습을 수행한 뒤 종료됩니다.
인자(argument)로 입력받은 문장이 <code>$original</code> 변수에 저장되며,
이때 문장의 맨 끝에 있는 개행 문자(<code>"\n"</code>)를 없애기 위해 <code>chomp</code> 함수를 사용합니다.
그 후 나중에 사용자가 어느 위치에서 오타를 냈는지를 쉽게 계산하기 위해
연습 문장을 배열 변수인 <code>@arrOriginal</code>로 옮겨 저장합니다.
<code>split</code> 함수를 통해 원 문장의 한 글자 한 글자가 하나씩 배열의 각 항목에 들어가게 됩니다.</p>
<p>다음은 프로그램을 작성할 때 가장 고생을 많이 한 부분입니다.</p>
<pre class="brush: perl;">
my $firstLetter;
do {
ReadMode 'cbreak';
$firstLetter = ReadKey(0);
ReadMode 'normal';
} while ($firstLetter =~ /\s/);
print $firstLetter;
my $time1 = gettimeofday();
my $typing = <STDIN>;
my $time2 = gettimeofday();
my $timeDif = ($time2 - $time1);
</pre>
<p>화면에 연습 문장이 출력된 후 사용자가 '첫 키'(첫키스가 아닙니다.)를 누른 후부터
시간을 측정하기 위해 엔터를 누르지 않아도 키의 입력을 인식하는 부분을 만들어야 했습니다.
그 기능을 위해 <code>ReadMode 'cbreak';</code>와 <code>ReadKey()</code> 함수를 사용했습니다.
<code>ReadKey()</code> 함수는 키보드로부터 한 글자를 입력받는 기능을 하며,
<code>ReadMode 'cbreak';</code>는 <code>ReadKey()</code> 함수로 하여금 엔터가 눌리지 않아도 키를 입력받도록 해줍니다.
그 작업이 끝나면 <code>ReadMode 'normal';</code>을 통해 키 입력 환경을 원상태로 돌려놓아 주어야 합니다.
그런데 이렇게 되면 키를 입력만 받을 뿐 사용자가 누른 키가 화면에 출력되지 않습니다.
그래서 <code>print</code>를 통해 사용자가 누른 키를 화면에 출력해 주었습니다.
한편, 사용자가 글자가 아닌 공백 문자(엔터, 스페이스 등)를
문장의 처음에 입력하면 화면 구성이 헝클어지므로
while문과 정규식을 이용해 이런 경우가 발생하는지 검사했습니다.</p>
<p>그 후에는 시간 측정을 합니다.
<code>gettimeofday()</code> 함수는 현재 시각을 마이크로초(백만분의 1초) 단위까지 출력해줍니다.
사용자가 문장을 입력하기 전과 후의 시각을 입력받아 타자 연습 시간을 계산합니다.</p>
<p>다음은 함수의 마지막 부분이자 모든 프로그램 설명의 마지막 부분입니다.</p>
<pre class="brush: perl;">
chomp $typing;
$typing = $firstLetter . $typing;
if ($typing eq "quit") {
return "quit";
}
my @arrTyping = split //, $typing;
my $i = 0;
my $j = length $typing;
my $numCorrect = 0;
foreach (@arrOriginal) {
if ($j <= $i) {
last;
}
if ($arrOriginal[$i] eq $arrTyping[$i]) {
print "-";
++$numCorrect;
}
else {
print "*";
}
++$i;
}
print "\n";
say " Time: " . (int(100 * $timeDif) / 100) . " seconds";
say "Correctness: " . (int(10000 * $numCorrect / length($original) / 100)) . "%" . " ($numCorrect / " . length($original) . ")";
say " Speed: " . (int(60 * 100 * length($typing) / $timeDif) / 100) . " keys / minute\n\n";
return ($timeDif, length($original), length($typing), $numCorrect);
</pre>
<p>앞서 문장의 시작 부분에 입력받은 '한 글자'와
그 후의 나머지 글자들을 <code>$typing = $firstLetter . $typing;</code>을 이용해서 합쳐주고,
연습 문장과 마찬가지로 오타 검사를 위해 문장을 배열로 만들어줍니다.
만약 사용자가 'quit'을 입력했으면 프로그램을 종료합니다.
이어지는 foreach문을 통해 오타 검사가 이루어지며,
올바른 입력 자리에는 '-'를, 오타 자리에는 '*'를 출력하여 (나름) 시각적으로 오타가 난 곳을 표시해줍니다.
그 후 연습 시간, 연습 문장 길이, 입력 타수,
올바른 입력 타수를 반환하고 함수가 종료됩니다.
이로써 프로그램에 대한 모든 설명이 끝났습니다!</p>
<h2>실행 화면</h2>
<p><img src="2011-12-23-1.png" alt="첫 실행 화면" id="" /><br />
<em>그림 1.</em> 첫 실행 화면</p>
<p>첫 실행 화면입니다.
실행 방법은 다음과 같습니다.</p>
<pre class="brush: plain;">
C:\> perl typing.pl [연습 문장 파일 이름]
(예: c:\>perl typing.pl data.txt)
(기사 끝에 예제 파일이 첨부되어 있습니다.)
</pre>
<p>참고로 윈도7에서 <a href="http://strawberryperl.com/">Strawberry Perl</a>을 사용했습니다.
연습 문장 파일 형식은 단순합니다.
각각의 연습 문장이 엔터로 구분되어 있기만 하면 됩니다.
단, 인터넷에서 문장을 긁어오다보면
문장의 맨 끝에 공백 문자로 인한 공백이 있는 경우가 있는데,
이 경우에는 공백을 미리 제거해주시는 편이 좋습니다.
그렇지 않으면 타자 연습을 할 때 공백까지 입력해야 합니다.</p>
<p>프로그램이 실행되면 프로그램의 정보, 인사말과 함께 안내가 나옵니다.
1번을 선택하면 연습 문장 파일에 있는 문장이 순서대로 출력되고,
2번을 선택하면 문장 순서가 무작위로 바뀌어 출력됩니다.
프로그램을 종료하고 싶을 때에는 'quit'을 입력합니다.</p>
<p><img src="2011-12-23-2.png" alt="이어지는 문장 타자 연습" id="" /><br />
<em>그림 2.</em> 이어지는 문장 타자 연습 (이야기 모드)</p>
<p>1번을 선택해 내용이 이어지는 문장으로 타자 연습을 하는 화면입니다.
예제 문장은 크리스마스를 기념해 아기 예수님의 탄생 장면으로 골랐습니다.
화면을 보면 오타가 발생한 부분이 표시되어 있고,
타자를 연습한 시간과 정확도, 속도가 표시되는 것을 알 수 있습니다.</p>
<p><img src="2011-12-23-3.png" alt="영어 속담 연습" id="" /><br />
<em>그림 3.</em> 속담 타자 연습 (문장 모드)</p>
<p>이번에는 영어 속담을 연습 문장 파일로 주었습니다.
원본 파일에는 속담이 알파벳 순으로 정렬되어 있지만,
메뉴에서 2번을 선택하면 무작위로 속담들이 출력됩니다.</p>
<p><img src="2011-12-23-4.png" alt="종료" id="" /><br />
<em>그림 4.</em> 종료 화면</p>
<p>'quit'을 입력해 프로그램을 종료합니다.
프로그램은 종료되기 전에 최종 연습 시간, 총 정확도, 평균 속도를 출력해줍니다.
제 영타 속도가 그리 빠르지 않군요...-_-;</p>
<h2>정리하며</h2>
<p>이렇게 Perl로 타자연습 프로그램을 만들어 보았습니다.
재미있게 보셨는지 모르겠네요.
Perl은 <em>재미있고</em> 쉬우면서 강력한 언어입니다.
대학원에 입학한 지 4개월 밖에 되지 않았지만,
입학 후 지금까지 제가 학업 면에 있어 가장 잘한 일을 꼽으라면
Perl을 공부한 일을 꼽을 것입니다.
Perl은 제 연구 작업을 30배는 편하게 만들어 주었습니다. 절대로 과장이 아닙니다.</p>
<p>사실 크리스마스는 예수님 생신이면서 동시에 제 생일입니다.(아이디를 보시면 아실 수 있듯이... 아악, 나이가 공개되어 버리는군요 -_-)
이렇게 크리스마스를 맞아 Perl 크리스마스 달력에 기사를 쓰게 된 것이 저에겐 잊을 수 없는 생일선물이 될 것 같습니다.</p>
<p>이 글을 여기까지 읽어주신 여러분께 진심으로 감사를 드립니다.
또한 다시 한번 이 자리를 빌어 저에게 기사 제의를 해 주신 <a href="http://twitter.com/am0c">@am0c</a>님께 감사를 드립니다.
그리고 제가 Perl을 처음 공부할 때에 큰 도움이 된 책
<a href="http://www.onyxneon.com/books/modern_perl/index.html">Modern Perl</a>의 저자 <a href="http://en.wikipedia.org/wiki/Chromatic_(programmer)">chromatic</a>과
Perl을 만든 <a href="http://en.wikipedia.org/wiki/Larry_Wall">Larry Wall</a>에게 역시 감사의 말씀을 드립니다.
그럼, 여러분, 즐거운 성탄절 되세요! 메리 크리스마스!</p>
<h2>참고문서</h2>
<ul>
<li><a href="2011-12-23-data.txt">이야기 모드 연습 예제 파일</a></li>
<li><a href="2011-12-23-proverbs.txt">문장 모드 연습 예제 파일</a></li>
<li><a href="2011-12-23-typing.pl">타자 연습 프로그램</a></li>
<li><a href="http://www.onyxneon.com/books/modern_perl/index.html">Modern Perl</a> (책 소개, 구입 링크 및 무료 PDF/ePub 공개)</li>
</ul>
2011-12-23T00:00:00+09:001985xmasPerl, 오늘 급식은 뭐야?http://advent.perl.kr/2011/2011-12-22.html<h2>저자</h2>
<p><a href="http://twitter.com/cheese_rulez">@cheese_rulez</a> -
a.k.a. 치즈군, 평범한 고등학생, 리듬게임이 좋아요, 오덕.</p>
<h2>시작하며</h2>
<p>학교 생활을 하다보면 신경 쓰이는게 많습니다.
예를 들면, 옆자리의 예쁜 여자아이, 2학년의 예쁜 선배,
동아리의 마음씨 곱고 짱 예쁘신 3학년 선배님이 있습니다.
음, 그렇습니다. 사실 현실의 학교에서 그럴 일은 없습니다.
이렇게 신경쓰고 싶은 것도 얼마 없는 지루한 학교 생활에
유일하게 활기를 불어넣어주는 건 당연 급식입니다.
4교시를 마치는 종소리가 울리면 당번들은 발에 부스터라도
달린 듯 급식차를 가지러 뛰어나가고, 우리는 가슴을 조아립니다.
그 두근거림을 두 배로 UP 해주는 것은 바로 급식 안내장!
하지만 저희 학교는 그런걸 나누어주지 않습니다. 슬픕니다.
다행히도 학교 홈페이지에 영양사 선생님께서 꾸준히 급식 정보를 올려주지요!
하지만 문제가 있었으니... 메뉴가 플래시입니다.</p>
<p><img src="2011-12-22-1.png" alt="안녕, 나는 플래시." id="" width="700" /><br />
<em>그림 1.</em> 안녕, 나는 플래시. (<a href="2011-12-22-1.png">원본</a>)</p>
<p>친구가 던져준 아이팟으로는 확인도 못한단 말이에요.
하지만 방법이 있습니다. 우리에겐 Perl이 있으니까요!</p>
<h2>계획하기</h2>
<p>먼저, Perl을 사용해 오늘의 급식을 알려주는 간단한 스크립트를 만들어봅시다.
단순히 급식을 가져와 출력하는 형태가 완성된 다음에는 어떻게 활용해야 할까요?
이 프로그램을 재료로 다시 어떻게 요리하느냐에 따라 더 굉장하고, 맛있는 것을 만들 수 있을 것입니다.</p>
<p>예를 들어, 올해 달력의 6일 기사 <a href="http://advent.perl.kr/2011/2011-12-06.html">'초소형 프레임워크와 함께 춤을!'</a>에
나온 <a href="http://perldancer.org/">Dancer</a>나, 아파치와 CGI를 이용해 급식을 보여주는
웹 사이트(<a href="http://gms.jubeat.kr/">gms.jubeat.kr</a>)를 만들 수 있습니다.
작년 달력의 4일 기사 <a href="http://advent.perl.kr/2010/2010-12-04.html">'선물 세 가지'</a>에 나온
<a href="https://metacpan.org/module/Net::Twitter::Lite">Net::Twitter::Lite</a> 모듈을 이용해
트위터 봇(<a href="http://twitter.com/GMS_Lunchbot">@GMS_Lunchbot</a>)도 만들 수도 있구요.
작년 달력의 2일 기사 <a href="http://advent.perl.kr/2010/2010-12-04.html">'Net::Google::Calendar 를 이용한 무료 SMS알림이 만들기'</a>를
응용해 매일 학교 급식을 문자로 받아볼 수도 있지요.
이렇게 활용 방법에 따라 무궁무진하게 발전시킬 수 있습니다!
그럼 우선 간단한 스크립트에서 출발해 보도록 해요. 자, 시작해볼까요?</p>
<h2>재료 준비하기</h2>
<p>우선, 아래와 같이 재료를 준비해주세요.</p>
<ul>
<li>Perl</li>
<li><a href="https://metacpan.org/module/LWP::UserAgent">LWP::UserAgent</a> 모듈</li>
<li><a href="https://metacpan.org/module/Encode">Encode</a> 모듈</li>
<li><a href="https://metacpan.org/module/DateTime">DateTime</a> 모듈</li>
</ul>
<p>윈도 환경이라면 <a href="http://strawberryperl.com/">딸기 Perl</a>을 설치합니다.
나머지 모듈은 우리의 CPAN 대형 마트를 통해 구할 수 있습니다!
<a href="https://metacpan.org/module/LWP::UserAgent">LWP::UserAgent</a>는 웹페이지를 가져오는 데 필요합니다.
<a href="https://metacpan.org/module/Encode">Encode</a>는 문자열 인코딩을 자유자재로 다루어줍니다.
시간을 구하기 위해서는 Perl에 기본 내장된 <a href="https://metacpan.org/module/POSIX">POSIX</a> 모듈을 쓰는 것도 나쁘지 않지만,
<a href="https://metacpan.org/module/DateTime">DateTime</a>을 쓰는 것이 더 좋다고 하네요.</p>
<h2>요리 시작!</h2>
<p>바로 코드를 봅시다. 기본적인 코드는 아래와 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use LWP::UserAgent;
use DateTime;
use Encode qw(encode);
my $dt = DateTime->now;
my $year = $dt->year;
my $month = $dt->month;
my $day = $dt->day;
my $date = sprintf "%s년 %s월 %s일", $year, $month, $day;
my $enc = $^O eq "MSWin32" ? "EUC-KR" : "UTF-8";
my $ua = LWP::UserAgent->new;
my $url = "http://gms.hs.kr/submain.html?tmode=school_eat&eatmode=view&nm=&year=$year&month=$month&date=$day";
my $response = $ua->get($url);
my ( $lunch, $dinner );
if ( $response->is_success ) {
my $contents = $response->decoded_content;
($lunch, $dinner) = get_food($contents);
$lunch //= '없습니다.';
$dinner //= '없습니다.';
}
else {
my $err = $response->status_line;
die "정보를 받아오는 중에 오류가 발생했습니다! : $err\n";
}
say "$date 오늘의 급식 정보 --";
say "중식 : $lunch";
say "석식 : $dinner";
say "이상, 오늘의 급식이었습니다.";
</pre>
<p>순서대로 코드를 간단히 둘러봅시다.</p>
<pre class="brush: perl;">
my $ua = LWP::UserAgent->new;
my $url = "http://gms.hs.kr/submain.html?tmode=school_eat&eatmode=view&nm=&year=$year&month=$month&date=$day";
my $response = $ua->get($url);
</pre>
<p>위처럼 <code>LWP::UserAgent</code> 모듈을 사용해 학교 웹사이트에서
<code>$year</code>년 <code>$month</code>월 <code>$day</code>일의 급식을 받아옵니다.
여기서 오늘 날짜의 변수는 <code>DateTime</code> 모듈을 사용해 쉽게 가져올 수 있습니다.</p>
<pre class="brush: perl;">
my ( $lunch, $dinner );
if ( $response->is_success ) {
my $contents = $response->decoded_content;
($lunch, $dinner) = get_food($contents);
$lunch //= '없습니다.';
$dinner //= '없습니다.';
}
else {
my $err = $response->status_line;
die "정보를 받아오는 중에 오류가 발생했습니다! : $err\n";
}
</pre>
<p>우리의 <code>$ua</code>가 가져온 결과는 <code>$response</code>에 담겼습니다.
성공적으로 결과를 받으면 웹페이지의 내용을 전부 <code>get_food</code> 함수에 전달합니다.
그리고 결과로 점심과 저녁의 메뉴를 받았습니다.
만약 인터넷 연결이나 서버의 문제 등으로 데이터를 가져오지 못하면
오류를 뿜고 종료합니다. 정말 간단하죠?
여기까지가 기본 형태입니다.
학교 홈페이지마다 웹사이트 코드의 구조는 다를테니
<code>get_food</code> 함수의 구현은 여러분의 학교에 맞게 작성해야 합니다.
그럼 두 학교 사이트를 통해 연습해볼까요?</p>
<p>먼저 <a href="http://gms.hs.kr/">경기 모바일 과학 고등학교</a>입니다. 사실 방금 가져온 웹페이지의 URL의 정체도 이곳이었습니다.
웹 페이지의 소스를 브라우저에서 직접 확인하려면 브라우저 주소 앞에 <code>view-source:</code>를 붙여서 열어봅니다.
이전의 코드에서 <code>get_food</code> 함수에 전달될 내용의 일부는 아래와 같습니다.</p>
<pre class="brush: xml;">
<tr class="eatlist_contents_out">
<td class="eatlist_title" colspan=2 bgcolor=f0f0f0>중식</td>
</tr>
<tr class="eatlist_contents_out" height=50>
<td class="eatlist_title" rowspan=2></td>
<td style='font-weight:bold'>옥수수밥,브로콜리크림스프,탕수육/소스,감자고추장조림,배추김치</td>
</tr>
<tr class="eatlist_contents_out">
<td></td>
</tr>
<tr class="eatlist_contents_out" >
<td class="eatlist_title" colspan=2>석식</td>
</tr>
<tr class="eatlist_contents_out" height=50>
<td class="eatlist_title" rowspan=2></td>
<td style='font-weight:bold'></td>
</tr>
<tr class="eatlist_contents_out">
<td></td>
</tr>
</pre>
<p>여기에서 <code>중식</code>의 <code>옥수수밥,브로콜리크림스프,탕수육/소스,감자고추장조림,배추김치</code>를 가져와야 합니다.
"석식"에는 아무것도 없는 모습이네요.
이 내용만 추출하기 위해 <a href="https://metacpan.org/module/Web::Scraper">Web::Scraper</a>나 <a href="https://metacpan.org/module/Web::Query">Web::Query</a> 같은 좋은
모듈을 사용할 수 있습니다. 하지만 정규표현식을 사용하는 것도 편리합니다.
다음의 코드처럼 말이죠!</p>
<pre class="brush: perl;">
sub get_food {
my $contents = shift;
my @result = $contents =~ m|<td style='font-weight:bold'>(.+?)</td>|ig;
return map encode($enc, $_), @result;
}
</pre>
<p>정규표현식은 밑줄 긋고 별표 5개를 쳐야할 정도로 중요합니다!
위 코드를 그림으로 표현하자면 아래와 같습니다.</p>
<p><img src="2011-12-22-2.png" alt="정규표현식 풀이" id="" /><br />
<em>그림 2.</em> 정규표현식 풀이</p>
<p>즉, 급식은 매일 바뀌지만 앞뒤에 붙어있는 태그는 변함이 없기 때문에,
앞 뒤에는 <code><td style='font-weight:bold'></code>와 <code></td></code>를 그대로 쓰고
매일 바뀌는 부분은 <code>.+?</code>로 걸러냈습니다.
그리고 걸러진 내용을 가져오기 위해 괄호로 감쌌습니다.
여기서 <code>.+?</code>의 각각의 기호는 의미가 있습니다.
조합하면 아무 글자나(<code>.</code>) 몇 개든 상관 없지만(<code>+</code>) 개수는
최대한 적게(<code>?</code>) 매치할 수 있는 하나의 패턴을 만듭니다.
<code>m|...|ig</code>에서 <code>g</code>에 해당하는 정규표현식 옵션은 이 정규표현식이
문자열에 대해 매치하는 모든 곳을 찾도록 돕습니다.
가져온 모든 결과는 <code>@result</code> 배열에 넣었습니다.</p>
<p>이번에는 <a href="http://dimigo.hs.kr/">이웃집 디미고</a> 사이트에서 시도해봅시다.
이곳의 식단표 페이지는 날짜와 상관없이 항상 동일하네요!
그래서 <a href="https://metacpan.org/module/DateTime">DateTime</a> 모듈을 사용할 필요는 없고
단순히 <code>$url</code> 변수의 값만 다룹니다.</p>
<pre class="brush: perl;">
my $url = "http://new.dimigo.hs.kr/dimigo/kimson/home/dimigo/bbs.php?id=food_list";
</pre>
<p>이번에도 마찬가지로, 웹 페이지의 소스를 열어서 잘 살펴봅니다.
어딘가 반복되어 있는 부분을 찾으셨나요?</p>
<pre class="brush: xml;">
<tr><td colspan="4" bgcolor="eeeeee" height="1"></td></tr>
<tr bgcolor='#FBFBFB' align="center">
<td style='font-weight:bold;color:#666666;'>2011.12.19</td>
<td background="../../_skin/board/_Full/school_food/image/ico_vline03.gif"></td>
<td style="padding:10px;" align="left">
<table>
<tr height="5"><td></td></tr>
<tr><td style='font-weight:bold;color:#666666;'>[아침] 쌀밥/육개장/해물완자땡/도토리묵무침/깻잎지/포기김치/요구르트</td></tr>
<tr><td style='font-weight:bold;color:#666666;'>[점심] 잡곡밥/근대국/순대야채볶음/호박야채전/부추겉절이/포기김치/삶은계란</td></tr>
<tr><td style='font-weight:bold;color:#666666;'>[저녁] 김치유부국수/쌀밥/떡강정/얼갈이겉절이/단무지/깍두기</td></tr>
<tr><td style='font-weight:bold;color:#666666;'>[간식] 포테이토스킨&황도샐러드&사과쥬스</td></tr>
<tr height="5"><td></td></tr>
</table>
</td>
<td>
</td>
</tr>
<tr><td colspan="4" bgcolor="eeeeee" height="1"></td></tr>
</pre>
<p>네, 달력 형태로 각 항목에 그 날의 급식이 써져있는데,
<code>tr</code>, <code>td</code> 태그로 씌워져 있습니다.
(정확히는 <code><tr align="center"></code>과 <code><tr><td colspan="4" bgcolor="eeeeee" height="1"></td></tr></code>)
그리고 각 시간대 별 식단도 다시 <code>tr</code>과 <code>td</code> 태그로 감싸져 있네요.
게다가 그 중, 오늘의 급식만 스타일 속성이
<code>font-weight:bold;color:#666666;</code>으로 지정되어 있습니다.
다행이네요. 그러면 간단히 작성할 수 있습니다.
<code>get_food</code> 함수를 이렇게 고치면 되죠!</p>
<pre class="brush: perl;">
sub get_food {
my $contents = shift;
my @result = $contents =~ m|<tr><td style='font-weight:bold;color:#666666;'>\[..\] (.+?)</td></tr>|ig;
return map encode($enc, $_), @result;
}
</pre>
<p>디미고는 기숙사도 있다보니 아침도 주나봐요.
<strike>무려 간식도 있어요! 호화롭네요!</strike></p>
<h2>더 맛있게</h2>
<p>이제 <a href="http://twitter.com/gypark">@gypark</a>님이 쓰신 작년 달력의 4일 기사 '<a href="http://advent.perl.kr/2010/2010-12-04.html">선물 세 가지</a>'에
나온 <a href="https://metacpan.org/module/Net::Twitter::Lite">Net::Twitter::Lite</a> 모듈을 응용해서 트위터 봇을 만들어봅시다.</p>
<h3>준비 #1</h3>
<p>일단, <a href="http://dev.twitter.com/apps/new">dev.twitter.com/apps/new</a>로 접속해 트위터 어플을 하나 만들어줍니다.
하단의 그림문자를 입력하고 <em>Create your Twitter Application</em>을 클릭합니다.
그러면 어플이 등록됩니다. 정말 간단하죠?
(어딘가 잘못 써진 것 같지만 그냥 넘어갑시다.)</p>
<p><img src="2011-12-22-3.png" alt="그림문자 입력" id="" /><br />
<em>그림 3.</em> 그림문자 입력</p>
<p><img src="2011-12-22-4.png" alt="트위터 앱 생성" id="" width="700" /><br />
<em>그림 4.</em> 트위터 앱 생성 (<a href="2011-12-22-4.png">원본</a>)</p>
<p>그런데 읽기 전용입니다. <em>Settings</em> 탭에서 쓰기 권한을 줍시다.
그리고 다시 <em>Details</em> 탭으로 돌아와 하단에 있는
<em>Create my Access Token</em>을 눌러 미리 간단히 접근할 수 있게 토큰을 받아둡니다.
토큰이 생성되면, <em>consumer key</em>, <em>consumer secret</em>,
<em>access token</em>, <em>access token secret</em> 4개를 메모해주세요.
중요한 정보이므로 이 4개는 <em>반드시 유출되지 않게 주의</em>해 주세요.
유출된다면 누군가 장난으로 글을 쓰거나 하는 것이 가능해지겠죠?</p>
<p><img src="2011-12-22-5.png" alt="권한 변경" id="" /><br />
<em>그림 5.</em> 권한 변경</p>
<p><img src="2011-12-22-6.png" alt="토큰이 기록된 앱 상세 페이지" id="" width="700" /><br />
<em>그림 6.</em> 토큰이 기록된 앱 상세 페이지 (<a href="2011-12-22-6.png">원본</a>)</p>
<h3>준비 #2</h3>
<p>완성된 급식 스크립트를 트위터 봇으로 개조해봅시다.
트위터 봇으로 작동시키기 위해 <a href="https://metacpan.org/module/Net::Twitter::Lite">Net::Twitter::Lite</a>
모듈이 추가로 필요합니다.
이것 역시 <a href="http://www.cpan.org/">CPAN</a> 대형 마트에서 구할 수 있습니다.</p>
<h3>봇을 만들어요</h3>
<p>다시 코드를 작성해 볼까요!</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use LWP::UserAgent;
use DateTime;
use Encode qw(encode decode);
use Net::Twitter::Lite;
my $dt = DateTime->now;
my $year = $dt->year;
my $month = $dt->month;
my $day = $dt->day;
my $date = sprintf "%s년 %s월 %s일", $year, $month, $day;
my $enc = $^O eq "MSWin32" ? "CP-949" : "UTF-8";
my ($option) = @ARGV;
my $admin = '@cheese_rulez'; # 봇 주인의 ID를 입력해주세요!
my $twt = Net::Twitter::Lite->new(
consumer_key => 'consumer key',
consumer_secret => 'consumer secret',
access_token => 'access token',
access_token_secret => 'access token secret',
);
if ( $option && $option =~ /^-[lda]$/ ) {
bot($option);
}
else {
tweet('오류 : 사용법이 잘못되었습니다! '.$admin);
tweet('사용법은 다음과 같습니다 : lunch.pl [스위치] / -l : 중식 / -d : 석식 / -a : 둘 다 '.$admin);
exit -1;
}
sub bot {
my $option = shift;
my ($lunch, $dinner);
my $ua = LWP::UserAgent->new;
my $response = $ua->get("http://gms.hs.kr/submain.html?tmode=school_eat&eatmode=view&nm=&year=$year&month=$month&date=$day");
if ($response->is_success) {
my $contents = $response->decoded_content;
my @result = $contents =~ m|<td style='font-weight:bold'>(\S+)</td>|ig;
$lunch = encode($enc, $result[0]);
$dinner = encode($enc, $result[1]);
$lunch = "없음" if !defined $lunch;
$dinner = "없음" if !defined $dinner;
}
else {
my $err = $response->status_line;
tweet("$admin 오류 발생! : $err");
die "정보를 받아오는 중에 오류가 발생했습니다! : $err";
}
if ($option eq '-l') {
tweet("오늘 점심은 $lunch 입니다!");
}
elsif ($option eq '-d') {
tweet("오늘 저녁은 $dinner 입니다!");
}
elsif ($option eq '-a') {
tweet("오늘 점심은 $lunch 이며, 저녁은 $dinner 입니다!");
}
}
sub tweet {
my $msg = shift;
my $text = decode($enc, $msg);
my $result = eval { $twt->update($text) };
warn "$@\n" if $@;
}
</pre>
<p>먼저 <code>@ARGV</code>입니다. 이 배열은 명령 뒤에 주는 스위치나 옵션 목록을 담고 있습니다.
즉, 도스의 <code>dir /p</code>의 <code>/p</code>나 <code>ls -l</code>의 <code>-l</code>에 해당합니다.
이 옵션 목록에서 첫번째의 항목을 <code>$option</code>으로 받습니다.</p>
<pre class="brush: perl;">
my ($option) = @ARGV;
</pre>
<p>여기서는 <code>-l</code> 옵션을 받으면 점심을, <code>-d</code>이면 저녁을, <code>-a</code>이면 모두 트위터로 올리도록 했습니다.
옵션을 주지 않거나 잘못되면 경고 메시지를 트위터로 올립니다.
이전의 스크립트는 <code>bot</code>이라는 사용자 함수로 묶었습니다.
<code>get_food</code>으로 나눴던 로직은 <code>bot</code>안에 다시 넣었습니다.</p>
<pre class="brush: perl;">
sub bot {
my $option = shift;
#...생략...
if ($option eq '-l') {
tweet("오늘 점심은 $lunch 입니다!");
}
elsif ($option eq '-d') {
tweet("오늘 저녁은 $dinner 입니다!");
}
elsif ($option eq '-a') {
tweet("오늘 점심은 $lunch 이며, 저녁은 $dinner 입니다!");
}
}
</pre>
<p>그리고 함수의 인자로 받은 <code>$option</code>의 값에 따라 점심, 저녁, 또는 전부를 트윗합니다.
옵션에 따라 <code>tweet</code> 함수를 호출하고 있습니다.
함수 이름을 보면 알 수 있듯이 말 그대로 트윗을 올리는 함수입니다.</p>
<pre class="brush: perl;">
sub tweet {
my $msg = shift;
my $text = decode($enc, $msg);
my $result = eval { $twt->update($text) };
warn "$@\n" if $@;
}
</pre>
<p>올릴 문자열을 미리 디코딩하지 않으면 다음과 같은 오류 메시지가 발생하니 주의하세요!</p>
<pre class="brush: plain;">
Net::OAuth warning: your OAuth message appears to contain some multi-byte characters that need to be decoded via Encode.pm or a PerlIO layer first. This may result in an incorrect signature. at /home/cheesekun/perl5/perlbrew/perls/perl-5.14.1/lib/site_perl/5.14.1/Net/OAuth/Message.pm line 106.
</pre>
<p>그래서 디코딩 후, <code>update</code> 메소드로 전달합니다.
그 외 나머지는 앞에서 살펴본 코드와 거의 비슷합니다.</p>
<h2>정리하며</h2>
<p>트윗으로 올리는 기능을 만들었지만, 특정 시간에 자동으로 올려주지는 않습니다.
윈도에서는 <em>작업 스케쥴러</em>를 리눅스에서는 <code>crontab</code>을 사용해
특정 시간에 자동으로 스크립트를 실행시키면 됩니다!
처음 쓰는 기사라 내용이 알찰지 모르겠네요.
그래도 많은 학생분들에게 맛있는 급식 생활이 되었으면 합니다.
대불어 글을 작성하는데 많은 도움을 주신 <a href="http://twitter.com/am0c">@am0c</a>님,
<a href="http://twitter.com/gypark">@gypark</a>님, <a href="http://twitter.com/aer0">@aer0</a>님께 감사드립니다!</p>
<h2>참고문서</h2>
<ul>
<li><a href="http://gypark.pe.kr/wiki/Perl/%EC%A0%95%EA%B7%9C%ED%91%9C%ED%98%84%EC%8B%9D">정규표현식 문서</a> - <a href="http://twitter.com/gypark">@gypark</a></li>
<li><a href="http://advent.perl.kr/2010/2010-12-04.html">선물 세 가지 :-D - 2010년 Seoul.pm 크리스마스 달력</a> - <a href="http://twitter.com/gypark">@gypark</a></li>
<li><a href="http://stackoverflow.com/questions/844616/obtain-a-switch-case-behaviour-in-perl-5">Obtain a switch/case behaviour in Perl 5</a></li>
<li><a href="https://github.com/aero/perl_docs/blob/master/Learning_Perl_5th_kor_review.md">거침없이 배우는 Perl 비평</a> - <a href="http://twitter.com/aer0">@aer0</a></li>
</ul>
2011-12-22T00:00:00+09:00cheese_rulezPerl로 시스템 트레이딩하기http://advent.perl.kr/2011/2011-12-21.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/saillinux">@saillinux</a> -
마음씨 좋은 외국인 노동자,
한국에 와서 비즈스프링에서 웹개발자 및 시스템 운영자로,
야후 코리아에서 프로덕션 옵스 및 엔지니어로,
현재 블리자드 엔터테인먼트에서 시스템 운영자로 재직 중이다.
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자,
Perl로 MMORPG를 만들어보겠다는 꿈을 갖고 있지만
요즘은 현실과 타협해 시스템 트레이딩에 푹 빠져있는 Perl덕후,
건강을 최고의 신조로 여기고 있다.</p>
<h2>시작하며</h2>
<p>오늘은 Perl을 이용해 아주 간단한 시스템 트레이딩을 해볼까 합니다.
조금 생소한가요? :)
소개하는 내용을 기반으로 자신만의 트레이딩 시스템 구축을 위한
프레임워크를 구현하는 것이 이번 기사의 목적입니다.
하지만 지금부터 다룰 내용은 목적은 실제 수익이 아니라 시스템 트레이딩
입문을 위한 설명인만큼, 혹시라도 생길 수 있는 <em>불이익</em>에 대한 책임은
지지 않는다는 것 꼭 기억해두세요! ;-)</p>
<h2>사전 지식</h2>
<p>시스템 트레이딩은 여러 분야의 기술을 접목해야 구축하고 동작시킬 수 있습니다.
필요한 사전 지식은 다음과 같습니다.</p>
<ul>
<li>기본 주식 지식 및 HTS를 이용한 매매 경험,
<a href="http://www.yes24.com/24/goods/5926363?scode=032&OzSrank=8">주식투자 무작정 따라하기</a></li>
<li>주식에 대한 (약간의) 기술적 분석 (Technical Analysis)
<a href="http://www.yes24.com/24/goods/4965201?scode=032&OzSrank=9">개념과 원리가 있는 친절한 기술적 분석</a></li>
<li><a href="http://etrade.co.kr">Etrade 증권</a>의 X-ing API</li>
<li>COM 인터페이스를 다루기 위한 <a href="https://metacpan.org/module/Win32::OLE">CPAN의 Win32::OLE 모듈</a></li>
</ul>
<h2>준비물</h2>
<p>시스템 트레이딩 역시 일반적인 주식 거래와 다를 것이 없으므로 증권 계좌가 필요합니다.
여러분의 은행 계좌와 증권 계좌를 연결하기 위해 우선 은행을 방문해서
<a href="http://etrade.co.kr">Etrade 증권</a> 계좌를 새로 만듭니다(은행 직원분이 친철하게 도와주니까 겁먹지 마세요).
아름다운 직원분께서 계좌를 만들어 주시면 왠지 수익률이
높아질 것만 같은 기분이 듭니다(기분이 좋습니다).</p>
<p><img src="2011-12-21-1.jpg" alt="Camel Investment" id="camelinvestment" width="700" />
<em>그림 1.</em> Camel Investment <a href="2011-12-21-1.jpg">(원본)</a></p>
<p>Etrade 증권의 HTS및 API를 이용하려면 증권 계좌 번호와
은행 계좌 번호, 공인 인증서가 필요하니까 준비해두세요.
그리고 Etrade HTS 및 X-ing COM, Res and DLL 파일이
필요한데 이것은 차차 설명하겠습니다.</p>
<p>마지막으로 시스템 트레이딩 구축을 위한 핵심인 Perl이 필요합니다.
모든 예제는 <a href="http://strawberryperl.com">딸기 Perl</a>을 기준으로 작성 및 테스트했습니다.
딸기 Perl을 설치하면 <a href="https://metacpan.org/module/Win32::OLE">Win32::OLE 모듈</a>이
기본으로 제공되므로 따로 설치하지 않아도 됩니다.</p>
<h2 id="etradex-ingapi">Etrade X-ing API?</h2>
<p>Etrade 증권은 X-ing API를 제공하므로 개발자들이
자신의 입맞에 맞는 HTS 매매 시스템을 구축 할수 있습니다.
X-ing API가 제공 하는 COM을 이용해 모의 서버에 접속하면
사용할 수 있는 주요 기능은 다음과 같습니다.</p>
<ul>
<li>Etrade 증권 서비스를 로긴 후 세션을 유지</li>
<li>실시간 호가 및 체결값을 실시간으로 받기</li>
<li>현물 매수/매도 주문</li>
<li>선물,옵션 그리고 ELW 파생 상품 서비스 이용</li>
</ul>
<p>열거한 기능 이외에도 여러 기능을 제공하는데
자세한 내용은 X-ing API 레퍼런스 문서를 참조하세요.</p>
<h2 id="x-ingapi">X-ing API를 사용하기 위해 필요한 파일 설치</h2>
<p>우선 X-ing API COM, Res 파일을 <a href="http://etrade.co.kr">공식 홈페이지</a>에서 받습니다.
파일을 받기 위해서는 Etrade 증권 계좌가 필요하며 회원 가입을 해야 합니다.
<a href="http://etrade.co.kr">Etrade 증권 홈페이지</a>에 들어가서 검색창에서 <code>X-ing API</code>를
넣고 검색합니다.</p>
<p><img src="2011-12-21-2.jpg" alt="필요한 파일 다운로드" id="" width="700" />
<em>그림 2.</em> 필요한 파일 다운로드 <a href="2011-12-21-2.jpg">(원본)</a></p>
<p>C:\XING 디렉토리를 생성한 후 다운로드 받은 파일(<code>COM(2011.04.26).zip</code>,
<code>Res(2011.10.20).zip</code>) 의 압축을 풀어 디렉토리에 넣습니다.
<code>COM(2011.04.26)</code>는 <code>COM</code>으로 <code>Res(2011.10.20)</code>는 <code>Res</code>로 이름을 변경합니다.
<code>Programs(2011.11.10).zip</code> DLL 파일은 COM 파일의 최근 업데이트
내역이므로 COM 디렉터리 아래에 압축을 풀어줍니다.
파일을 풀고난 후 디렉토리 구조는 다음과 같습니다.</p>
<pre class="brush: bash;">
C:\XING
C:\XING\COM
C:\XING\Res
C:\XING\Res\Real
C:\XING\Res\Tran
</pre>
<p>'-']... "허허 이런 것은 얼른 얼른 넘어갑시다"라고 외치실 분들을 위해
사실 따로 파일(<a href="http://advent.perl.kr/2011/xing-demo.zip">다운로드</a>)을 준비했습니다.
이 파일을 다운로드 받은 후 <code>C:\</code>에 압축을 풉니다.</p>
<p><code>C:\XING\COM</code>에 들어가서 <code>Reg.bat</code>를 실행해 X-ing COM 오브젝트를 등록합니다.</p>
<p><img src="2011-12-21-3.jpg" alt="설치" id="" width="700" />
<em>그림 3.</em> 설치 <a href="2011-12-21-3.jpg">(원본)</a></p>
<p>자! 드디어 Xing-API를 사용하기 위한 사전 준비를 완료했습니다.
API를 테스트 하기위해 모의 투자 계좌를 이용합니다.
혹시 생길지 모르는 문제를 사전에 발견하기 위해 실전에서 테스트 하기 보다는
증권사에서 제공하는 모의 투자 시스템으로 연습하는 것이 먼저겠죠? :)
모의 투자 계좌는 증권 계좌와 다르므로 우선 HTS를 받아서 설치하고
모의 투자 모드로 로그인한 후 계좌 번호를 받아야 합니다.</p>
<p><img src="2011-12-21-4.jpg" alt="HTS 다운로드" id="hts" width="700" />
<em>그림 4.</em> HTS 다운로드 <a href="2011-12-21-4.jpg">(원본)</a></p>
<p>설치가 끝나면 HTS로 로그인하세요.</p>
<p><img src="2011-12-21-5.jpg" alt="HTS 로그인" id="hts" width="700" />
<em>그림 5.</em> HTS 로그인 <a href="2011-12-21-5.jpg">(원본)</a></p>
<p>그림에 표시된 모의 증권 계좌 번호를 적어 두도록 합니다.
그리고 모의 투자 계좌 비밀 번호는 <code>0000</code>입니다.</p>
<p><img src="2011-12-21-6.jpg" alt="HTS 계정" id="hts" width="700" />
<em>그림 6.</em> HTS 계정 <a href="2011-12-21-6.jpg">(원본)</a></p>
<p>이제 모의 투자 계좌 번호까지 준비되었습니다.
드이어 <a href="https://metacpan.org/module/Win32::OLE">CPAN의 Win32::OLE 모듈</a>을
이용해 Xing API를 사용해보죠!</p>
<h2 id="x-ingapiperl-">X-ing API를 우아하게 Perl에서 사용 하도록 하자 '-'] 엣헴!</h2>
<p><code>Reg.bat</code>을 실행하면 X-ing COM 객체가 원도우에 등록됩니다.
<code>Win32::OLE</code>를 사용하면 COM이 제공하는 X-ing API를
Perl에서 직접 호출할 수 있기 때문에 정말 편리합니다.
이것을 C++ 이나 Visual Basic으로 작성할 생각을하면
상상만으로도 손가락이 얼얼해지는것 같네요;;</p>
<h3>로그인, 세션 생성</h3>
<p>우선 X-ing API로 로그인한 후 세션을 생성합니다.
Win32::OLE 모듈을 로드할 때 이벤트 핸들링을 위해
<code>EVENTS</code>를 임포트한다는 점을 눈여겨 보세요.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use Carp;
use Win32::OLE qw/EVENTS/;
</pre>
<p>X-ing API에서 세션을 담당하는 <code>XA_Session.XASession</code>을 불러 OLE 객체를 받도록 합니다.</p>
<pre class="brush: perl;">
my $XASession = Win32::OLE->new('XA_Session.XASession')
or croak Win32::OLE->LastError();
</pre>
<p>그리고 <code>XASession</code> 사용시 일어나는 모든 이벤트를 핸들 할수 있도록 다음과 같은 핸들러를 작성합니다.</p>
<pre class="brush: perl;">
my $XASessionEvents = sub {
my ( $obj, $event, @args ) = @_;
# 1: OnLogin
# 2: OnLogout
# 3: OnDisconnect
given ($event) {
when (1) {
my ($code, $msg) = @args;
print "XASession Login Event: [$code] $msg \n";
Win32::OLE->QuitMessageLoop();
}
when (2) {
print "XASession Logout Event: @args \n";
Win32::OLE->QuitMessageLoop();
}
when (3) {
print "XASession Disconnect Event: @args \n";
Win32::OLE->QuitMessageLoop();
}
}
};
</pre>
<p>이벤트 코드 각각의 의미는 다음과 같습니다.</p>
<ul>
<li><code>1</code>: 로그인에 성공</li>
<li><code>2</code>: 로그아웃에 성공</li>
<li><code>3</code>: 연결 끊김</li>
</ul>
<p>이제 <code>$XASession</code> 객체가 생성하는 모든 이벤트를 제어해보죠.</p>
<pre class="brush: perl;">
Win32::OLE->WithEvents(
$XASession,
$XASessionEvents,
'{6D45238D-A5EB-4413-907A-9EA14D046FE5}',
);
croak Win32::OLE->LastError() if Win32::OLE->LastError() != 0;
</pre>
<p><code>WithEvents</code>를 사용하지 않으면 로드한 객체의 이벤트를 핸들링 하지않습니다.
간혹 <code>Win32::OLE</code>가 자동으로 이벤트 인터페이스를 판별 하지 못하는 객체도 있습니다.
그럴 경우에는 직접 객체의 <code>COCLASS</code>(<code>IProvideClassInfo2</code>를 참조) 값이라던지
혹은 이벤트의 DISPATCH 인터페이스를 찾아 3번째 인자로 제공해야합니다.</p>
<p>여기까지 수행하면 X-ing API의 OLE 객체를 얻을 수 있습니다.
이제는 다음 흐름에 따라 프로그램을 작성합니다.</p>
<ul>
<li><em>서버에 연결</em>: X-ing 서버에 연결</li>
<li><em>로그인</em>: 서버에 아이디/암호, 공인인증으로 로그인</li>
<li><em>데이터처리</em>: 조회성 TR/실시간 TR을 이용하여 데이터 조회 및 처리</li>
<li><em>로그아웃</em>: X-ing 서버에서 로그아웃</li>
<li><em>서버연결종료</em>: 서버와 연결된 세션 종료</li>
</ul>
<p>연결에 필요한 정보를 이용해서 OLE 객체를 이용해 세션을 생성하고
로그인을 수행하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
my $server = 'demo.etrade.co.kr'; # 모의 투자 서버 주소
my $port = 20001; # 서비스 포트
my $user = q{}; # Etrade 증권 아이디
my $pass = q{}; # Etrade 증권 암호
my $certpwd = q{}; # 공인 인증서 암호
my $srvtype = 1; # 서버 타입
my $showcerterr = 1; # 공인 인증서 에러
$XASession->ConnectServer($server, $port)
or croak $XASession->GetErrorMessage( $XASession->GetLastError );
$XASession->Login($user, $pass, $certpwd, $srvtype, $showcerterr)
or croak $XASession->GetErrorMessage( $XASession->GetLastError );
Win32::OLE->MessageLoop();
</pre>
<p>로그인을 하려면 사용자 아이디와 암호, 공인인증 암호가 필요한데
모의 투자로 테스트할 때는 공인인증 암호를 넣지 않아도 됩니다.</p>
<p><img src="2011-12-21-7.jpg" alt="로그인 과정" id="" width="700" />
<em>그림 7.</em> 로그인 과정 <a href="2011-12-21-7.jpg">(원본)</a></p>
<p><code>MessageLoop</code>를 호출 하는 시점에 프로그램은 <code>QuitMessageLoop</code>가 호출되기 전까지
윈도우 메시지 루프 모드로 들어가서 이벤트를 디스패치하기 시작합니다.
이렇게 해서 얻은 이벤트는 아까 작성한 <code>XASessionEvents</code> 이벤트 핸들러가 처리합니다.
여기서는 로그인 세션을 성공적으로 생성된 것을 확인하면
다음 작업을 위해 <code>QuitMessageLoop</code>를 호출해서 이벤트 루프에서 빠져나옵니다.</p>
<h3>실시간으로 호가 및 체결 값 처리</h3>
<p>X-ing API는 두 종류의 <em>트랜잭션</em>(transaction)을 지원 합니다.
트랜잭션이란 서버로부터 데이터를 얻기 위해 요청하고
데이터를 받는 일련의 행동을 말합니다.</p>
<ul>
<li><em>조회 트랜잭션</em>: 서버로 부터 요청 당시의 데이터를 전송</li>
<li><em>실시간 트랜잭션</em>: 서버로 데이터 요청을 하면 이후에 데이터가 변경될 때마다 데이터를 전송(이벤트 방식)</li>
</ul>
<p><img src="2011-12-21-8.jpg" alt="트랜잭션" id="" width="700" />
<em>그림 8.</em> 트랜잭션 <a href="2011-12-21-8.jpg">(원본)</a></p>
<p>호가 데이타를 받아오는 실시간 트랜잭션을 작성하려면
실시간 데이터를 제공하는 XAReal COM 객체를 불러옵니다.</p>
<pre class="brush: perl;">
my $XAReal = Win32::OLE->new('XA_DataSet.XAReal.1')
or croak Win32::OLE->LastError();
</pre>
<p>호가 데이터는 실시간으로 업데이트 되는 값이며 X-ing API에서는 실시간
트랜잭션 구조체인 <code>S3_</code>을 참조해 호가 데이터를 요청 및 추출합니다.
앞서 압축을 풀어 <code>Res</code> 폴더에 저장한 디렉토리의 파일을 적재합니다.</p>
<pre class="brush: perl;">
$XAReal->LoadFromResFile("$FindBin::Bin/Res/Real/H1_.res")
or croak Win32::OLE->LastError();
</pre>
<p>KOSPI호가잔량 데이터에 대한 이벤트 핸들링 입니다.
XAReal OLE객체가 제공하는 함수인 <code>GetFieldData</code>로
<code>H1_.res</code> DATA MAP 파일을 참조해 데이터 블록에서 필드값을 추출합니다.
첫 번째 인자인 <code>OutBlock</code>은 DATA MAP을 참조할 때
데이터 블록을 받았을 경우의 필드를 참조하라는 뜻입니다.</p>
<pre class="brush: perl;">
my $XARealEvents = sub {
my ( $obj, $event, @args ) = @_;
# 1: OnReceiveRealData
given ($event) {
when (1) {
# 호가 값이 업데이트 된 시간을 추출
my $hotime = $XAReal->GetFieldData('OutBlock', 'hotime');
# 매도 호가1 값을 추출
my $offerho1 = $XAReal->GetFieldData('OutBlock', 'offerho1');
# 매수 호가1 값을 추출
my $bidho1 = $XAReal->GetFieldData('OutBlock', 'bidho1');
print "\t$hotime $offerho1 $bidho1\n";
}
}
};
</pre>
<p><code>$XAReal</code>이 받는 모든 이벤트를 <code>$XARealEvents</code> 핸들러가 처리할 수 있도록 등록합니다.</p>
<pre class="brush: perl;">
Win32::OLE->WithEvents(
$XAReal,
$XARealEvents,
'{16602768-2C96-4D93-984B-E36E7E35BFBE}',
);
croak Win32::OLE->LastError() if Win32::OLE->LastError() != 0;
</pre>
<p>데이터를 보낼때 생성할 블록 데이터는 다음처럼 <code>SetFieldData()</code> 함수를 호출해 생성할 수 있습니다.</p>
<pre class="brush: perl;">
$XAReal->SetFieldData('InBlock', 'shcode', '000270');
$XAReal->AdviseRealData();
</pre>
<p><code>InBlock</code>은 DATA MAP을 참조시 데이터 블록을 보낼 경우의 필드를 참조하라는 뜻입니다.
<code>shcode</code>는 종목을 의미하며 <code>000270</code>은 기아자동차의 코드 번호입니다.
즉, 기아자동차의 호가 잔량을 요청한다는 뜻이죠.
데이터 블록을 생성했으면 <code>AviseReadData()</code> 함수를 호출해 실시간 데이터를 받습니다.</p>
<p>호가 잔량 데이터 핸들링 또한 이벤트를 처리해야 하므로
이벤트 루프에 다시 진입해야 합니다.</p>
<pre class="brush: perl;">
Win32::OLE->MessageLoop();
</pre>
<p>스크립트를 실행한 후 <code>Ctrl-C</code> 명령으로 종료하면 응용 프로그램
오류 창이 뜨지만 큰 상관은 없으니 가볍게 무시해도 됩니다. :)</p>
<h2>두근두근 매수/매도리얼</h2>
<p>드디어 매수/매도 주문을 내야 할 시점입니다.
모의 투자 계좌로 로그인 했기 때문에 부담없이 팍팍!! 주문을 해보세요.
걱정 마세요. 여러분의 가상 지갑은 튼실하답니다! :)
여러분은 여기서 한번 죽어도 다시 살아날 수 있습니다.</p>
<p>실시간 트랜잭션이 아니라 조회 트랜잭션을 사용해 주문을 내보죠.</p>
<pre class="brush: perl;">
my $XAQuery = Win32::OLE->new('XA_DataSet.XAQuery')
or croak Win32::OLE->LastError();
$XAQuery->LoadFromResFile("$FindBin::Bin/res/Tran/t5501.res")
or croak Win32::OLE->LastError();
</pre>
<p><code>XA_DataSet.XAQuery</code> COM 객체를 이용해 OLE 객체를 생성했습니다.
현물 매수/매도 주문을 위한 DATA MAP은
<code>Tran/t5501.res</code>에 정의되어 있으니 불러오도록 합니다.</p>
<pre class="brush: perl;">
my $XAQueryEvents = sub { };
</pre>
<p>현물 주문 조회 트랜잭션의 경우 이벤트 호출로 받아 오는 데이터가 없으므로
제대로 체결이 이루어졌는지 알려면 실시간 <code>SC0</code> 핸들러를 이용해
주문 체결 이벤트를 다루도록 합니다.
매수 주문을 넣기 위한 데이터 블록을 생성하고 요청하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
$XAQuery->SetFieldData('t5501InBlock', 'reccnt', 0, '1');
$XAQuery->SetFieldData('t5501InBlock', 'accno', 0, 'XXXXXXXXXXX');
$XAQuery->SetFieldData('t5501InBlock', 'passwd', 0, '0000');
$XAQuery->SetFieldData('t5501InBlock', 'expcode', 0, 'A000270');
$XAQuery->SetFieldData('t5501InBlock', 'qty', 0, '1');
$XAQuery->SetFieldData('t5501InBlock', 'price', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'memegb', 0, '2');
$XAQuery->SetFieldData('t5501InBlock', 'hogagb', 0, '03');
$XAQuery->SetFieldData('t5501InBlock', 'pgmtype', 0, '00');
$XAQuery->SetFieldData('t5501InBlock', 'gongtype', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'gonghoga', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'tongsingb', 0, '00');
$XAQuery->SetFieldData('t5501InBlock', 'sinmemecode', 0, '000');
$XAQuery->SetFieldData('t5501InBlock', 'loandt', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'memnumber', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'ordcondgb', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'stragb', 0, '000000');
$XAQuery->SetFieldData('t5501InBlock', 'groupid', 0, '00000000000000000000');
$XAQuery->SetFieldData('t5501InBlock', 'ordernum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'portnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'basketnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'tranchnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'itemnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'operordnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'flowsupgb', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'oppbuygb', 0, '0');
$XAQuery->Request(0);
</pre>
<p>상당히 길어 보이지만 몇 가지 필드만 신경쓰면 됩니다.</p>
<ul>
<li><code>accno</code>: 증권 계좌 번호(지금은 모의투자 계좌 번호)</li>
<li><code>passwd</code>: 증권계좌 암호</li>
<li><code>expcode</code>: 주식 번호(앞에 A를 앞에 붙임)</li>
<li><code>qty</code>: 수량</li>
<li><code>price</code>: 지정가일 경우 원하는 체결 가격</li>
<li><code>memegb</code>: 매매 구분(<code>1</code>은 매수, <code>2</code>는 매도)</li>
<li><code>hogagb</code>: 호가유형 코드(<code>00</code>는 지정가, <code>03</code>은 시장, 등등)</li>
</ul>
<p>그외 필드는 예제에 있는 값을 이용하시면 됩니다
자세한 내용은 X-ing API 레퍼런스 문서를 참조하세요.</p>
<p>이렇게 해서 생성한 데이터 블럭을 <code>Request()</code> 함수를 호출해 주문합니다.
주문을 내기만 해서는 접수가 이루어졌는지 알 수 없습니다.
실시간으로 접수가 이루어졌는지 확인하려면
실시간 트랜잭션 중 하나인 주식 주문 접수를 등록합니다.</p>
<pre class="brush: perl;">
my $XAReal = Win32::OLE->new('XA_DataSet.XAReal.1')
or croak Win32::OLE->LastError();
$XAReal->LoadFromResFile("$FindBin::Bin/res/Real/SC0_.res")
or croak Win32::OLE->LastError();
my $XARealEvents = sub SC0_handler {
my ($obj) = @_;
print $obj->GetFieldData('OutBlock', 'ordno'), "\n";
}
Win32::OLE->WithEvents($XAReal, $XARealEvents, '{16602768-2C96-4D93-984B-E36E7E35BFBE}');
croak Win32::OLE->LastError() if Win32::OLE->LastError() != 0;
$XAReal->AdviseRealData();
</pre>
<p>실시간 주문 접수와는 달리 데이터 블럭을 직접 만들 필요가 없습니다.
주문 접수가 이루어 졌을때 전달받는 값 중 하나인 <code>ordno</code>(주문번호)를 출력합니다.</p>
<p>주문 한번 시원하게 날려볼까요? ;-)</p>
<pre class="brush: perl;">
Win32::OLE->MessageLoop();
</pre>
<h2>전체 소스 코드</h2>
<h3 id="trading.pl">trading.pl</h3>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use Carp;
use FindBin;
use Win32::OLE qw( EVENTS );
local $|++;
my $XASession = Win32::OLE->new('XA_Session.XASession')
or croak Win32::OLE->LastError;
my $XASessionEvents = sub {
my ($obj, $event, @args) = @_;
# 1: OnLogin
# 2: OnLogout
# 3: OnDisconnect
given ($event) {
when (1) {
my ($code, $msg) = @args;
say "XASession Login Event: [$code] $msg";
Win32::OLE->QuitMessageLoop;
}
when (2) {
say "XASession Logout Event: @args";
Win32::OLE->QuitMessageLoop;
}
when (3) {
say "XASession Disconnect Event: @args";
Win32::OLE->QuitMessageLoop;
}
}
};
Win32::OLE->WithEvents(
$XASession,
$XASessionEvents,
'{6D45238D-A5EB-4413-907A-9EA14D046FE5}',
);
croak Win32::OLE->LastError
if Win32::OLE->LastError != 0;
my $server = 'demo.etrade.co.kr'; # 모의 투자 서버 주소
my $port = 20001; # 서비스 포트
my $user = q{}; # 이트레이드 증권 아이디
my $pass = q{}; # 이트레이드 증권 암호
my $certpwd = q{}; # 공인 인증서 암호
my $srvtype = 1; # 서버 타입
my $showcerterr = 1; # 공인 인증서 에러
$XASession->ConnectServer($server, $port)
or croak $XASession->GetErrorMessage( $XASession->GetLastError );
$XASession->Login($user, $pass, $certpwd, $srvtype, $showcerterr)
or croak $XASession->GetErrorMessage( $XASession->GetLastError );
Win32::OLE->MessageLoop;
my $XAReal = Win32::OLE->new('XA_DataSet.XAReal.1')
or croak Win32::OLE->LastError;
$XAReal->LoadFromResFile("$FindBin::Bin/res/Real/H1_.res")
or croak Win32::OLE->LastError;
my $XARealEvents = sub {
my ( $obj, $event, @args ) = @_;
# 1: OnReceiveRealData
given ($event) {
when (1) {
my $trname = $args[0];
my $hotime = $obj->GetFieldData('OutBlock', 'hotime');
my $offerho1 = $obj->GetFieldData('OutBlock', 'offerho1');
my $bidho1 = $obj->GetFieldData('OutBlock', 'bidho1');
print "OnReceiveRealData: $trname\n";
print "[$hotime $offerho1 $bidho1]\n";
}
}
};
Win32::OLE->WithEvents(
$XAReal,
$XARealEvents,
'{16602768-2C96-4D93-984B-E36E7E35BFBE}',
);
croak Win32::OLE->LastError
if Win32::OLE->LastError != 0;
$XAReal->SetFieldData( 'InBlock', 'shcode', '000270' );
$XAReal->AdviseRealData;
Win32::OLE->MessageLoop;
</pre>
<h3 id="order.pl">order.pl</h3>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use Carp;
use FindBin;
use Win32::OLE qw( EVENTS );
local $|++;
my $XASession = Win32::OLE->new('XA_Session.XASession')
or croak Win32::OLE->LastError;
my $XASessionEvents = sub {
my ($obj, $event, @args) = @_;
# 1: OnLogin
# 2: OnLogout
# 3: OnDisconnect
given ($event) {
when (1) {
my ($code, $msg) = @args;
say "XASession Login Event: [$code] $msg";
Win32::OLE->QuitMessageLoop;
}
when (2) {
say "XASession Logout Event: @args";
Win32::OLE->QuitMessageLoop;
}
when (3) {
say "XASession Disconnect Event: @args";
Win32::OLE->QuitMessageLoop;
}
}
};
Win32::OLE->WithEvents(
$XASession,
$XASessionEvents,
'{6D45238D-A5EB-4413-907A-9EA14D046FE5}',
);
croak Win32::OLE->LastError
if Win32::OLE->LastError != 0;
my $server = 'demo.etrade.co.kr'; # 모의 투자 서버 주소
my $port = 20001; # 서비스 포트
my $user = q{}; # 이트레이드 증권 아이디
my $pass = q{}; # 이트레이드 증권 암호
my $certpwd = q{}; # 공인 인증서 암호
my $srvtype = 1; # 서버 타입
my $showcerterr = 1; # 공인 인증서 에러
$XASession->ConnectServer($server, $port)
or croak $XASession->GetErrorMessage( $XASession->GetLastError );
$XASession->Login($user, $pass, $certpwd, $srvtype, $showcerterr)
or croak $XASession->GetErrorMessage( $XASession->GetLastError );
Win32::OLE->MessageLoop;
my $XAQuery = Win32::OLE->new('XA_DataSet.XAQuery')
or croak Win32::OLE->LastError;
$XAQuery->LoadFromResFile("$FindBin::Bin/Res/Tran/t5501.res")
or croak Win32::OLE->LastError;
my $XAQueryEvents = sub { };
$XAQuery->SetFieldData('t5501InBlock', 'reccnt', 0, '1');
$XAQuery->SetFieldData('t5501InBlock', 'accno', 0, 'XXXXXXXXXXX');
$XAQuery->SetFieldData('t5501InBlock', 'passwd', 0, '0000');
$XAQuery->SetFieldData('t5501InBlock', 'expcode', 0, 'A000270');
$XAQuery->SetFieldData('t5501InBlock', 'qty', 0, '1');
$XAQuery->SetFieldData('t5501InBlock', 'price', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'memegb', 0, '2');
$XAQuery->SetFieldData('t5501InBlock', 'hogagb', 0, '03');
$XAQuery->SetFieldData('t5501InBlock', 'pgmtype', 0, '00');
$XAQuery->SetFieldData('t5501InBlock', 'gongtype', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'gonghoga', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'tongsingb', 0, '00');
$XAQuery->SetFieldData('t5501InBlock', 'sinmemecode', 0, '000');
$XAQuery->SetFieldData('t5501InBlock', 'loandt', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'memnumber', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'ordcondgb', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'stragb', 0, '000000');
$XAQuery->SetFieldData('t5501InBlock', 'groupid', 0, '00000000000000000000');
$XAQuery->SetFieldData('t5501InBlock', 'ordernum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'portnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'basketnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'tranchnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'itemnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'operordnum', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'flowsupgb', 0, '0');
$XAQuery->SetFieldData('t5501InBlock', 'oppbuygb', 0, '0');
$XAQuery->Request(0);
my $XAReal = Win32::OLE->new('XA_DataSet.XAReal.1')
or croak Win32::OLE->LastError;
$XAReal->LoadFromResFile("$FindBin::Bin/res/Real/SC0.res")
or croak Win32::OLE->LastError;
my $XARealEvents = sub {
my ( $obj ) = @_;
say $obj->GetFieldData('OutBlock', 'ordno');
};
Win32::OLE->WithEvents(
$XAReal,
$XARealEvents,
'{16602768-2C96-4D93-984B-E36E7E35BFBE}',
);
croak Win32::OLE->LastError
if Win32::OLE->LastError != 0;
$XAReal->AdviseRealData;
Win32::OLE->MessageLoop;
</pre>
<h2>정리하며</h2>
<p>지금까지 Etrade 증권사 API와 Perl을 이용해 주식을
조회하고 사고 팔수 있는 방법을 소개 했습니다.
여기서 한 걸음 더 나아가고 싶다면 Perl로 만들어진
전략엔진 및 위험관리 시스템인 <a href="http://www.geniustrader.org">GeniusTrader</a>와
<a href="https://github.com/tradespring/TradeSpring">TradeSpring</a>을 참고하면 큰 도움이 될 것입니다.
실제로 본격적인 시스템 트레이딩을 하려면 전략 또한 정말 중요하겠죠.
이 모든 것을 Perl로 잘 구현한 후 X-ing API와 연동해
호가 체결 조회 및 매수/매도 주문을 낸다면 자신만의
시스템 트레이딩 환경을 갖출 수 있습니다.
짜릿하지 않나요?</p>
<p>자, 행운을 빕니다! ;-)</p>
<p><img src="2011-12-21-9.jpg" alt="Good Luck!" id="goodluck" width="700" />
<em>그림 9.</em> Good Luck! <a href="2011-12-21-9.jpg">(원본)</a></p>
2011-12-21T00:00:00+09:00saillinuxMojolicious, HTML5, WebSocket을 이용한 비동기 채팅http://advent.perl.kr/2011/2011-12-20.html<h2>저자</h2>
<p><a href="http://twitter.com/eeyees">@eeyees</a> -
인쇄기기 업계의 기린아, TAFKA_HoliK라는 닉을 사용하기도 한다. 일본에서 일하다가 올해 한국 후지 제록스로 이직하였다.</p>
<h2>시작하며</h2>
<p>우리는 지난 기사에서 카탈리스트, 댄서와 같은 웹 프레임워크를 볼 수 있었습니다.
제가 소개시켜 드릴 것은 <a href="http://mojolicio.us/">Mojolicious</a>라는 웹 프레임워크입니다.</p>
<p>Mojolicious는 실시간 웹 프레임워크를 표방하고 있습니다.
그 외에도 멋진 특징들이 많이 있습니다. 공식 사이트에서는 아래와 같이 소개하고 있습니다.</p>
<ul>
<li>실시간 웹 프레임워크로서 Mojolicious::Lite를 통해 단일 파일로 간소화한 버전도 제공</li>
<li>REST 라우트, 플러그인, 펄에 적합한 탬플렛, 세션 관리, 서명된 쿠키, 테스팅 프레임워크, 정적 파일 서버, I18N, 최고의 유니코드 지원 등 강력하고 격이 다른 구성</li>
<li>아주 깔끔하고 이식성 높으며, 비밀스런 작동은 않으며, 펄 5.10.1 외에 의존하는 것이 없다. 순수한 펄 객체지향 API로 제공된다.</li>
<li>HTTP 1.1 스택을 완전히 지원하고, IPv6, TLS, Bonjour, IDNA, Comet(long polling), chunking과 멀티파트 지원 및 웹소켓 클라이언트/서버 구현 제공</li>
<li>기본적으로 non-blocking I/O 웹 서버로서, libev와 hot deployment 지원, 임베딩에 최적</li>
<li>CGI 및 PSGI 자동 인식</li>
<li>JSON, HTML5/XML 파서 및 CSS3 선택자 지원</li>
<li>몇 년 간의 Catalyst 개발 경험을 기반으로 깔끔하게 개발됨.</li>
</ul>
<p>오늘은 가볍게 Mojolicious를 이용해 Websocket으로 비동기 채팅하는 소스 코드를 살펴봅시다.</p>
<h2>뼈대 만들기</h2>
<p>리눅스라면 다음과 같은 명령으로 간단하게 설치할 수 있습니다.</p>
<pre class="brush: bash;">
$ sudo sh -c "curl -L cpanmin.us | perl - Mojolicious"
</pre>
<p>cpan이 설정되어 있다면 아래와 같이 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Mojolicious
</pre>
<p>Mojolicious가 설치되면 mojo라는 명령행 도구가 생성됩니다.
Mojolicious::Lite를 기반으로 한 간단한 웹 어플리케이션 뼈대를 만들기 위해서는
아래와 같이 입력할 것입니다.</p>
<pre class="brush: bash;">
$ mojo generate lite_app
</pre>
<p>오늘은 간소화 버전이 아닌 정식 웹 어플리케이션을 만들어 봅시다.
따라서 <code>lite_app</code> 대신 <code>app</code> 타입으로 지정합니다.
어플리케이션의 이름은 Chat로 지어주었습니다.</p>
<pre class="brush: bash;">
$ mojo generate app Chat
[mkdir] /home/holik/tmp/chat/script
[write] /home/holik/tmp/chat/script/chat
[chmod] chat/script/chat 744
[mkdir] /home/holik/tmp/chat/lib
[write] /home/holik/tmp/chat/lib/Chat.pm
[mkdir] /home/holik/tmp/chat/lib/Chat
[write] /home/holik/tmp/chat/lib/Chat/Example.pm
[mkdir] /home/holik/tmp/chat/t
[write] /home/holik/tmp/chat/t/basic.t
[mkdir] /home/holik/tmp/chat/log
[mkdir] /home/holik/tmp/chat/public
[write] /home/holik/tmp/chat/public/index.html
[mkdir] /home/holik/tmp/chat/templates/layouts
[write] /home/holik/tmp/chat/templates/layouts/default.html.ep
[mkdir] /home/holik/tmp/chat/templates/example
[write] /home/holik/tmp/chat/templates/example/welcome.html.ep
</pre>
<p>생성된 chat 디렉터리에 들어가 내부 구성을 살펴봅시다.</p>
<pre class="brush: bash;">
~/tmp$ cd chat
~/tmp/chat$ tree
.
├── lib
│ ├── Chat
│ │ └── Example.pm
│ └── Chat.pm
├── log
├── public
│ └── index.html
├── script
│ └── chat
├── t
│ └── basic.t
└── templates
├── example
│ └── welcome.html.ep
└── layouts
└── default.html.ep
9 directories, 7 files
</pre>
<p><code>lib</code> 아래에 있는 부분이 서버의 컨트롤러 부분이겠군요.
<code>log</code> 디렉터리에는 로그를 남길 것입니다.
<code>public</code>에는 공통적으로 쓰이는 정적 파일이 올라갈 것입니다. 여기서는 html 페이지를 보관하고 있네요.
<code>script</code>에는 구동 스크립트가 들어 있습니다.
<code>t</code> 디렉터리에는 테스트 묶음이 들어갈 것입니다.
<code>templates</code> 부분은 서버의 뷰 부분을 담당하고 있는 친구들인가 보군요.</p>
<p>간단하게 웹 서비스를 한번 실행해 봅시다.
아래와 같이 <code>script</code> 디렉터리에 들어있는 <code>chat</code> 스크립트를 <code>morbo</code> 명령을 통해 실행합니다.</p>
<pre class="brush: bash;">
~/tmp/chat$ morbo script/chat
Server available at http://127.0.0.1:3000.
</pre>
<p>이제 웹 브라우저에서 <a href="">http://127.0.0.1:3000/welcome</a>에 접근해 봅시다.</p>
<p><img src="2011-12-20-1.png" alt="생성된 뼈대를 구동해 열어본 웹 페이지" id="" /><br />
<em>그림 1.</em> 생성된 뼈대를 구동해 열어본 웹 페이지</p>
<p>잘 나오네요.
그럼 이제부터 채팅 프로그램을 만들어 봅시다. :)</p>
<h2>비동기 채팅 만들기</h2>
<p><code>lib/Chat.pm</code> 파일에 chat라는 라우트를 추가해 봅시다.
파일을 열면 아까 열어본 <code>/welcome</code>도 여기에 정의되어 있습니다.</p>
<pre class="brush: perl;">
package Chat;
use Mojo::Base 'Mojolicious';
# This method will run once at server start
sub startup {
my $self = shift;
# Routes
my $r = $self->routes;
# Normal route to controller
$r->route('/welcome')->to('example#welcome');
}
1;
</pre>
<p><code>/welcome</code> 라우트를 등록하는 줄 하단에 <code>/chat</code>도 같은 형태로 추가합니다.</p>
<pre class="brush: perl;">
$r->route('/chat')->to('ChatControl#chatAction');
</pre>
<p><code>to</code>에 전달하는 <code>ChatControl#chatAction</code>은 "<code>ChatControl</code>이라는
컨트롤러에 <code>chatAction</code>이라는 액션을 수행해 주세요."라고 말하는 것과 같습니다.
같은 방식으로 웹소켓을 위한 라우트도 추가합니다.</p>
<pre class="brush: perl;">
$r->websocket('/chatWS')->to('ChatControl#wsAction');
</pre>
<h3>컨트롤러 작성하기</h3>
<p><code>Chat.pm</code>에 등록해 준 컨트롤러 부분을 구현해봅시다.
<code>lib/Chat/ChatControl.pm</code> 파일을 만들고 chatAction 함수와 wsAction 함수를 만듭니다.</p>
<pre class="brush: perl;">
package Chat::ChatControl;
use Mojo::Base 'Mojolicious::Controller';
our $clients = {};
sub chatAction {
my $self = shift;
$self->render(
message => 'Chatting Example'
);
}
sub wsAction {
my $self = shift;
my $clntID = sprintf "%s" , $self->tx;
$clients->{$clntID} = $self->tx;
for my $id (keys %$clients) {
$clients->{$id}->send_message("Client Connected!");
}
say "Client Connected!";
$self->on(
finish => sub {
my $self = shift;
my $id = sprintf "%s" , $self->tx;
delete $clients->{$id};
say "Client is Disconnected!";
}
);
$self->on(
message => sub {
my ($self, $message) = @_;
for my $id (keys %$clients) {
$clients->{$id}->send_message($message);
}
}
);
}
1;
</pre>
<p><code>chatAction</code>은 채팅 웹페이지 위에 표기할 메시지를 전달해
단순히 템플렛을 랜더링합니다.</p>
<p><code>wsAction</code>은 웹 소켓으로 통신하는 로직을 작성했습니다.
먼저, <code>$self->tx</code>를 통해 현재 요청에 대한 트랙잭션 객체(Mojo::Transaction)를 얻습니다.
이 경우 웹 소켓 트랙잭션 객체(Mojo::Transaction::WebSocket)가 될 것입니다.
요청받은 모든 트랜잭션은 단순히 패키지 해시에 보관합니다.</p>
<p><code>$self->on</code>을 통해 현재 요청에 대한 트랜잭션에 이벤트 콜백을 등록합니다.
특히 웹 소켓을 통해 메시지가 도착하면 <code>message</code> 이벤트가 수행됩니다.
그러면 보관했던 모든 트랜잭션, 즉 요청했던 모든 클라이언트에게 메시지를 전달합니다.
<code>wsAction</code>의 행동을 위의 두 줄로 요약하면 아래와 같습니다.</p>
<ul>
<li>접속되거나 메시지를 받으면 모든 클라이언트에게 메시지 통지(send_message)</li>
<li>접속이 끊어지면 경우 모든 클라이언트에게 메시지 통지</li>
</ul>
<h3>뷰 작성하기</h3>
<p>이제 뷰 부분을 만들어 보겠습니다.
<code>templeates</code> 디렉터리로 이동한 후 <code>ChatControl</code> 디렉터리를
만들고 그 안에 <code>chatAction.html.ep</code> 파일을 만듭니다.</p>
<pre class="brush: bash;">
$ mkdir ChatControl
$ cd ChatControl
$ vim chatAction.html.ep
</pre>
<p>그리고 다음과 같이 작성해 봅시다.</p>
<pre class="brush: xml;">
%layout 'chatLayout';
%title 'Chatting';
<h1>
<%= $message %>
</h1>
<div id="chatWindow">
</div>
<form>
<input type="text" id="sendMes">
<input type="submit" id="sendBtn" value="Send">
</form>
</pre>
<p>채팅을 보일 창 한 개와 채팅을 입력할 부분과 보낼 버튼을 추가 했습니다.
이제 레이아웃을 작성해 볼까요? 이번에는 <code>templates/layouts</code> 디렉터리에
<code>chatAction.html.ep</code>에서 사용한 레이아웃 파일을 만듭니다.</p>
<pre class="brush: bash;">
$ cd ..
$ cd ..
$ cd templates/layouts
$ vim chatLayout.html.ep
</pre>
<p>이번에는 아래와 같이 작성합니다.</p>
<pre class="brush: xml;">
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<style type"text/css">
#chatWindow {
width : 500px;
height : 400px;
overFlow : auto;
border : 1px solid #000;
}
</style>
<script type="text/javascript"
src="http://code.jquery.com/jquery-1.7.min.js">
</script>
<script type="text/javascript">
var ws;
$(document).ready(function() {
$('#sendBtn').click(sendMessage);
$('#sendBtn').keyup(function(ev) {
if( ev.keycode == 13) {
sendMessage();
}
});
$('#disconnBtn').click(disconn);
$('form').submit(function(ev) {
ev.preventDefault();
});
if (!("WebSocket" in window)) {
alert("do not Supprot WebSocket!!");
} else {
ws = new WebSocket('ws://localhost:3000/chatWS');
ws.onopen = function() {
}
ws.onmessage = function(ev) {
appendChat(ev.data);
}
ws.onclose = function() {
appendChat("Connection Closed!");
}
}
function sendMessage() {
ws.send($('#sendMes').val());
$('#sendMes').val("");
}
function appendChat(mes) {
$('#chatWindow').append(mes+"<br>");
}
function disconn() {
ws.close();
}
});
</script>
</head>
<body>
<%= content %>
</body>
</html>
</pre>
<p>간단한 자바스크립트 코드를 이용해 웹 소켓의 메시지를 송수신합니다.
브라우저가 웹 소켓을 지원하지 않는지 검사한 뒤, 서버와 접속을 수행합니다.
웹 소켓을 통해 메시지를 받으면 <code>onmessage</code>에 등록한 함수가 호출되어 메시지를 출력합니다.
메시지를 보낼 때에는 <code>ws.send(...)</code>와 같이 웹 소켓을 통해 전달하고 있습니다.
클라이언트에서 메시지를 보낸 이후 페이지를 다시 불러들이는 것을 막기 위해
<code>preventDefault()</code>를 걸었습니다.</p>
<p>그럼 작성한 소스 코드를 실행해봅시다.</p>
<pre class="brush: bash;">
$ morbo script/chat
</pre>
<p><img src="2011-12-20-2.png" alt="완성된 서비스" id="" /><br />
<em>그림 2.</em> 완성된 서비스</p>
<p>잘 되네요 :)</p>
<h2>정리하며</h2>
<p>간단하게 Mojolicious를 웹 프레임워크를 사용해서
HTML5에 포함되는 WebSocket을 이용한 비동기 채팅 웹 서비스를 만들어 보았습니다.
한국에는 아직도 펄하면 단순히 문자열 처리만을 위한 언어라거나 시스템 엔지니어만 다루는 언어로만 알려져 있는 것 같습니다.
이번 펄 크리스마스 달력을 통해 펄의 다양한 활용에 대해서 조금이나마 공유할 수 있는 시간이 되어 다행인 것 같습니다.</p>
<p>2011년 마무리 잘 하시고, 새해 복 많이 받으세요!</p>
2011-12-20T00:00:00+09:00eeyeesPerl Tatsumaki로 비동기 웹 서비스 구축하기http://advent.perl.kr/2011/2011-12-19.html<h2>저자</h2>
<p><a href="http://twitter.com/luzluna">@luzluna</a> -
Seoul.pm과 #perl-kr의 육아 전문 컨설턴트, 사회적 기업을 꿈꾸는 커피 매니아이자 백수.</p>
<h2>시작하며</h2>
<p>비동기 웹서버의 유행들을 따라
펄에도 비동기 웹서비스를 제공할만한 좋은 방법들이 몇 가지 생겼습니다.
그 중 <a href="http://search.cpan.org/perldoc?Tatsumaki">Tatsumaki</a>는 <a href="http://search.cpan.org/~miyagawa/">Tatsuhiko Miyagawa</a>씨께서
<a href="http://www.tornadoweb.org/">Tornado</a>를 펄 버전으로 새로 구현한 프레임워크입니다.</p>
<p>비동기 웹서버에 대해 부정적으로 생각하지만(<a href="http://en.wikipedia.org/wiki/Larry_Wall">Larry Wall</a>의 표현을 빌어 표현하자면 <em>"Not For Human"</em>),
웹 상에서 채팅이나 메신저같은 Long Polling 서비스를 구현하려면 마땅한 다른 방법도 없으니...
필요하면 배워야겠죠. ㅜ.ㅠ</p>
<h2>예제를 봅시다</h2>
<p>Tatsumaki 소스를 다운받으면 <code>eg</code> 디렉터리 아래에 <a href="http://search.cpan.org/perldoc?Tatsumaki">간단한 채팅 서버 예제</a>가 있습니다.</p>
<pre class="brush: bash;">
$ tree eg/chat
eg/chat
|-- app.psgi
|-- static
| |-- DUI.js
| |-- jquery-1.3.2.min.js
| |-- jquery.cookie.js
| |-- jquery.ev.js
| |-- jquery.md5.js
| |-- jquery.oembed.js
| |-- pretty.js
| |-- screen.css
| `-- Stream.js
`-- templates
`-- chat.html
</pre>
<p>PSGI 어플리케이션으로 되어 있으며 모든 펄 코드는 <code>app.psgi</code>에 집적되어 있습니다.
이 중, 먼저 <code>main</code> 패키지의 코드를 봅시다.</p>
<pre class="brush: perl;">
package main;
use File::Basename;
my $chat_re = '[\w\.\-]+';
my $app = Tatsumaki::Application->new([
"/chat/($chat_re)/poll" => 'ChatPollHandler',
"/chat/($chat_re)/mxhrpoll" => 'ChatMultipartPollHandler',
"/chat/($chat_re)/post" => 'ChatPostHandler',
"/chat/($chat_re)" => 'ChatRoomHandler',
]);
$app->template_path(dirname(__FILE__) . "/templates");
$app->static_path(dirname(__FILE__) . "/static");
return $app->psgi_app;
</pre>
<p>Tatsumaki::Application를 생성하면서 처리할 URL 패턴과 각 패턴에 대한 핸들러를 추가해줍니다.
그런 다음 템플릿 경로(<code>template_path</code>) 설정도 해주고 정적 파일(<code>static_file</code>)을 처리하기 위한 설정도 추가해줍니다.
여기까지는 간단하죠?</p>
<p>처리할 URL 패턴 중, 먼저 <code>/chat/($chat_re)</code>에 접근한다고 가정해 봅시다.
따라서 이번에는 <code>ChatRoomHandler</code>를 보겠습니다.</p>
<pre class="brush: perl;">
package ChatRoomHandler;
use base qw(Tatsumaki::Handler);
sub get {
my($self, $channel) = @_;
$self->render('chat.html');
}
</pre>
<p>그냥 <code>chat.html</code> 템플릿을 랜더링하고 있습니다.
<code>ChatRoomHandler</code> 핸들러에 제공된 URL 패턴은 정규표현식이었습니다.
이 정규표현식에 매치가 성공하면 해당 핸들러에 디스패치되고
매치를 통해 일치 변수(<code>$1</code>, <code>$2</code>, <code>$3</code> 등)로 기억된 결과가
핸들러의 변수로 넘어갑니다. 이 경우에는 <code>$channel</code>에
<code>$chat_re</code>에 매치된 문자열이 넘어가겠네요.</p>
<p>핸들러에 HTTP 메소드명의 사용자 함수를 작성하면,
해당 메소드 요청에 대해 연결됩니다.
이 경우에는 <code>get</code> 함수를 정의하여 GET 메소드에 대해 템플릿을 랜더링하도록
하고 있습니다.</p>
<p>이번엔 <code>ChatPostHandler</code>입니다.</p>
<pre class="brush: perl;">
package ChatPostHandler;
use base qw(Tatsumaki::Handler);
use HTML::Entities;
use Encode;
sub post {
my($self, $channel) = @_;
my $v = $self->request->parameters;
my $html = $self->format_message($v->{text});
my $mq = Tatsumaki::MessageQueue->instance($channel);
$mq->publish({
type => "message", html => $html, ident => $v->{ident},
avatar => $v->{avatar}, name => $v->{name},
address => $self->request->address,
time => scalar Time::HiRes::gettimeofday,
});
$self->write({ success => 1 });
}
sub format_message {
my($self, $text) = @_;
$text =~ s{ (https?://\S+) | ([&<>"']+) }
{ $1 ? do { my $url = HTML::Entities::encode($1); qq(<a target="_blank" href="$url">$url</a>) } :
$2 ? HTML::Entities::encode($2) : '' }egx;
$text;
}
</pre>
<p>핵심적인 코드는 아래와 같이 채널 이름에 맞는 Tatsumaki::MessageQueue를 만드는 코드와</p>
<pre class="brush: perl;">
$mq = Tatsumaki::MessageQueue->instance($channel);
</pre>
<p>아래와 같이 메시지를 Queue에 쏘는 두 줄이 끝입니다.</p>
<pre class="brush: perl;">
$mq->publish({
type => "message", html => $html, ident => $v->{ident},
avatar => $v->{avatar}, name => $v->{name},
address => $self->request->address,
time => scalar Time::HiRes::gettimeofday,
});
</pre>
<p>채널명에 해당하는 메시지 큐에 채팅을 통해 전달받은 채팅 메시지를 전달하고 있습니다.
메시지 <code>"message"</code>로 분류하고, 사용자명, 아바타, 시간, HTML로 랜더링된 메시지 등을 담았습니다.
이번에는 <code>ChatPollHander</code>를 봅시다.</p>
<pre class="brush: perl;">
package ChatPollHandler;
use base qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
use Tatsumaki::MessageQueue;
sub get {
my($self, $channel) = @_;
my $mq = Tatsumaki::MessageQueue->instance($channel);
my $client_id = $self->request->param('client_id')
or Tatsumaki::Error::HTTP->throw(500, "'client_id' needed");
$client_id = rand(1) if $client_id eq 'dummy'; # for benchmarking stuff
$mq->poll_once($client_id, sub { $self->on_new_event(@_) });
}
sub on_new_event {
my($self, @events) = @_;
$self->write(\@events);
$self->finish;
}
</pre>
<p>방금 본 <code>post</code> 함수와 비슷합니다.
먼저 해당 핸들러는 <code>__PACKAGE__->asynchronous(1);</code>을 통해 비동기 모드로 설정했습니다.
Tatsumaki::MessageQueue 인스턴스를 하나 만들고 <code>$mq->poll_once</code>로 모든 메시지를 한꺼번에 대기합니다.
핸들러를 비동기 모드로 설정했기 때문에 핸들러 객체에 등록된 Writer 객체를 사용하는
<code>write</code>과 <code>finish</code> 함수로 도착한 이벤트를 출력합니다. 해시 레퍼런스였던 메시지는 JSON으로 변환되어 전달됩니다.
<code>ChatMultipartPollHandler</code>는 어떨까요?</p>
<pre class="brush: perl;">
package ChatMultipartPollHandler;
use base qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
sub get {
my($self, $channel) = @_;
my $client_id = $self->request->param('client_id') || rand(1);
$self->multipart_xhr_push(1);
my $mq = Tatsumaki::MessageQueue->instance($channel);
$mq->poll($client_id, sub {
my @events = @_;
for my $event (@events) {
$self->stream_write($event);
}
});
}
</pre>
<p>이전과 거의 비슷합니다.
대신 멀티파트 <code>multipart_xhr_push</code>를 한 줄 넣어 멀티파트 헤더를 추가해주고
연결을 끊지 않고 계속 poll 하기 위해 <code>stream_write</code>로 이벤트를 전송합니다.</p>
<p>마지막으로 <a href="http://search.cpan.org/perldoc?plackup">plackup</a>을 통해서 <a href="http://search.cpan.org/perldoc?Twiggy">Twiggy</a>로 띄우면... 채팅 잘 되네요~</p>
<pre class="brush: bash;">
$ plackup -s Twiggy app.psgi
</pre>
<h2>보너스!</h2>
<p>채팅만 하려니까 뭔가 심심해서 재미있는걸 해볼 수 있게 canvas를 추가해봅시다.
<code>chat.html</code>에 아래와 같이 캔버스 한 줄을 추가합니다.</p>
<pre class="brush: xml;">
<canvas id="c" width="200" height="100" style="border:1px solid"></canvas>
</pre>
<p>그런 다음 아래와 같이 스크립트를 좀 추가해 줍시다.</p>
<pre class="brush: jscript;">
function draw_dot(x,y) {
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(x,y,5,0,Math.PI*2,true);
ctx.fillStyle = '#5555AA';
ctx.fill();
ctx.stroke();
}
$(function(){
$('#c').mouseup(function(e) {
var canoffset = $('#c').offset();
var x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft - Math.floor(canoffset.left);
var y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop - Math.floor(canoffset.top) + 1;
draw_dot(x,y);
$.ajax({
url: "/chat/<%= $channel %>/post",
data: { type: 'game', x: x, y: y, text:'g' },
type: 'post',
dataType: 'json',
success: function(r) { }
});
});
var onGameEvent = function(e) {
draw_dot(e.x, e.y);
}
$.ev.handlers.game = onGameEvent;
});
</pre>
<p>마지막으로, game 타입의 메시지를 다루기 위해 서버 코드를 조금 수정합니다. </p>
<pre class="brush: perl;">
package ChatPostHandler;
use base qw(Tatsumaki::Handler);
use HTML::Entities;
use Encode;
sub post {
my($self, $channel) = @_;
for ( keys %{$self->request->parameters} ) {
$self->request->parameters->{$_} = decode('utf8', $self->request->parameters->{$_});
}
my $v = $self->request->parameters;
my $html = $self->format_message($v->{text});
my $mq = TatsumakiZeroMQ->instance($channel);
if (defined $v->{type} && $v->{type} eq 'game' ) {
$mq->publish({
type => "game", html => $html,
x => $v->{x}, y => $v->{y},
ident => $v->{ident},
avatar => $v->{avatar}, name => $v->{name},
address => $self->request->address,
time => scalar Time::HiRes::gettimeofday,
});
}
else {
$mq->publish({
type => "message", html => $html, ident => $v->{ident},
avatar => $v->{avatar}, name => $v->{name},
address => $self->request->address,
time => scalar Time::HiRes::gettimeofday,
});
}
$self->write({ success => 1 });
}
</pre>
<p>이제 캔버스에 클릭으로 점을 찍으면 상대편에게도 점이 찍히는게 보이죠?
로직을 좀 구현해넣으면 간단한 게임도 만들 수 있을 것 같고 그림을 공유하는 것도 될 것 같습니다.</p>
<p><img src="2011-12-19-1.png" alt="완성된 채팅 서비스" id="" /></p>
<p><em>그림 1.</em> 완성된 채팅 서비스 (<a href="2011-12-19-1.png">원본</a>)</p>
<h2>참고 문서</h2>
<ul>
<li><a href="http://www.slideshare.net/miyagawa/tatsumaki">Tatsumaki SlideShare</a></li>
<li><a href="https://github.com/miyagawa/Tatsumaki">Github for Tatsumaki</a></li>
<li><a href="http://plackperl.org/">PSGI/Plack</a></li>
<li><a href="http://search.cpan.org/perldoc?Twiggy">Twiggy</a></li>
</ul>
2011-12-19T00:00:00+09:00luzlunaPerl로 아두이노(Arduino) 가지고 놀기http://advent.perl.kr/2011/2011-12-18.html<h2>저자</h2>
<p><a href="http://twitter.com/ja3ck">@ja3ck</a>(a.k.a. chammse) - Python과 Ruby를 지나 Perl에 정착. 백수를 향한 힘찬 발걸음을 내딛는 중.</p>
<h2>시작하며</h2>
<p>저는 어려서부터 레고나 과학상자류를 가지고 노는 것을 좋아했습니다.
다 자란 지금도 무언가 만드는 것을 좋아합니다.
(하지만 현실은 행거도 제대로 설치 못해서 30분 동안 낑낑)</p>
<p>소프트웨어를 전공하게 된 당시에도 하드웨어를 제어하는 일에 흥미를 가지고 있었습니다.
그러다보니 임베디드를 다루는 회사를 다니며 다양한 경험을 쌓게 되었고,
아두이노를 접하게 되었죠.
하지만 그동안 여가를 즐기기에는 너무나 <strike>더럽게</strike> 바쁜 SI에 매달려 있었습니다.
구입해 놓고 오랫동안 잠재웠던 아두이노가 불쌍해보여 다시 한 번 꺼내들어 봅니다.</p>
<h2>준비물</h2>
<p>필요한 모듈과 장비는 다음과 같습니다.</p>
<ul>
<li><a href="http://metacpan.org/module/Win32::SerialPort">Win32::SerialPort</a></li>
<li>Linux/Mac 환경에서는 <a href="http://metacpan.org/module/Device::SerialPort">Device::SerialPort</a>를 이용합니다.</li>
<li>아두이노 (저는 <a href="http://arduino.cc/en/Main/arduinoBoardDuemilanove">Duemilnove</a>를 쓰고 있습니다.)</li>
</ul>
<h2>아두이노?</h2>
<p><img src="2011-12-18-1.jpg" alt="그림1. Arduino" id="arduino" /></p>
<p>아두이노(Arduino)는 <em>AVR을 기반으로하는 오픈소스를 피지컬 컴퓨팅 플랫폼</em>입니다.
이를 구동하는데 필요한 모든 것은 오픈소스를 기반으로 합니다.
게다가, 제공되는 통합 IDE 환경을 통해 초보자도 손쉽게 장비를 조작하여 데이터를 획득할 수 있습니다.
아두이노에 대한 자세한 사항은 다음 사이트를 참고하세요. 그리고 지르는 겁니다!</p>
<ul>
<li><a href="http://arduino.cc/en/">Arduino.cc</a></li>
<li><a href="http://cafe.naver.com/arduinostory/">아두이노스토리</a> (네이버카페)</li>
</ul>
<h2>아두이노에서 시리얼 통신준비</h2>
<p>우선 간단하게 다음과 같이 회로를 구성하여 봅시다. </p>
<h3>아두이노 회로 구성 준비물</h3>
<ul>
<li>Potentiometer</li>
<li>220 옴짜리 저항 1개</li>
<li>LED 1개 </li>
<li>패치 케이블 5개</li>
<li>빵판(Bread Board)</li>
<li>마지막으로, 아두이노</li>
</ul>
<p><img src="2011-12-18-2.png" alt="그림2. 회로구성 하기1" id="" /></p>
<p>아두이노의 소스 코드는 다음과 같습니다. (<a href="http://arduino.cc/en/Tutorial/AnalogInOutSerial">참고소스</a>)</p>
<pre class="brush: cpp;">
const int analogInPin = A0; // 아날로그 입력에 0번 핀 설정
const int analogOutPin = 9; // 아날로그 출력에 9번핀 설정
int sensorValue = 0; // 센서(Potentiometer) 수신값
int outputValue = 0; // PWM으로 내보낼 센서값
void setup() {
// Serial 포트를 9600bps로 시작
Serial.begin(9600);
}
void loop() {
// 아날로그 값을 읽는다
sensorValue = analogRead(analogInPin);
// 읽은 아날로그값을 매핑한다.
outputValue = map(sensorValue, 0, 1023, 0, 255);
// 아날로그 값을 출력한다.
analogWrite(analogOutPin, outputValue);
// 결과를 시리얼포트를 통해 전송
Serial.print("sensor=" );
Serial.print(sensorValue);
Serial.print("\t output=");
Serial.println(outputValue);
// 10 밀리세컨드 동안 대기
delay(10);
}
</pre>
<h2 id="perl">Perl에서 시리얼포트에 접근하기</h2>
<p>Perl에서 시리얼 포트에 접근하는 것은 정말 쉽습니다.
몇 가지 기본적인 통신 설정만 해주면 끝이죠.
자, 그럼 이제 Perl을 통해 아두이노가 걸어오는 말을 들어볼까요?</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Win32::SerialPort qw( :STAT 0.19 );
# 제 PC 에서는 아두이노가 'COM4' 으로 연결되어 있습니다
my $serial_port = Win32::SerialPort->new('COM4') or die "Can't open COM port: $^E";
# Serial port 설정들 입니다
$serial_port->handshake('none');
$serial_port->baudrate(9600);
$serial_port->parity('none');
$serial_port->databits(8);
$serial_port->stopbits(1);
$serial_port->read_interval(0);
$serial_port->read_const_time(10);
$serial_port->write_char_time(1);
$serial_port->write_const_time(100);
$serial_port->write_settings() or die "Can't change Device_Control_Block: $^E";
$serial_port->save('my_serial.cfg');
while (1) {
if (my $line = $serial_port->input) {
chomp $line;
print $line, "\n";
}
}
</pre>
<p>다음과 같이 나오면 성공입니다.</p>
<p><img src="2011-12-18-3.png" alt="그림3. 시리얼포트를 통한 데이터 수신결과" id="" /></p>
<h2>이걸로 무엇을 할까?</h2>
<p>연결해서 데이터를 받아오긴 했지만 이걸로 대채 뭘 할 수 있을까요.
구구절절 기사에 다 설명하진 못하겠지만 사운드 볼륨을 조절하거나,
조광센서를 달아서 자리를 비웠을 때 누군가 책상 위에 손을 대면 경고 화면을 출력한다거나,
온도, 습도, 먼지감지 등의 센서를 달아서 사무환경 개선을 해달라고 팀장님을 협박할 수도 있겠습니다.</p>
<h2 id="nunchuck">Nunchuck 연결</h2>
<p>오늘은 아두이노가 보내주는 정보를 가지고 재미난 놀이를 해봅시다.
닌텐도 게임기 Wii의 조작 컨트롤러인 Nunchuck에 연결해볼까요?
우선 Nunchuck의 입력 단자를 과감하게 잘라 다음과 같이 연결합니다.
(자르지 않고 커넥터를 구매하는 방법도 있습니다.)</p>
<p><img src="2011-12-18-4.jpg" alt="그림4. nunchuck" id="nunchuck" /></p>
<ul>
<li>하얀색(white) => GND</li>
<li>좌빨색(red) => 5V</li>
<li>녹색(Green) => Analog 4번</li>
<li>노란색(Yellow) => Analog 5번</li>
</ul>
<p>그리고 아두이노에서 Nunchuck에 접근하기 위해 아두이노 Playground에 있는
<a href="http://www.arduino.cc/playground/Main/WiiChuckClass">라이브러리</a>를 이용합니다.</p>
<p>그런데 최근 Arduino 1.0으로 업데이트되면서 Wire 라이브러리 호출 방법에 약간의 변화가 생겼는데, 다음과 같이 수정합니다.</p>
<ul>
<li><code>Wire.send(0xF0)</code>처럼 되어 있는 부분을 <code>Wire.write(byte(0xF0))</code>로 변경합니다.</li>
<li><code>Wire.receive()</code>를 <code>Wire.read()</code>로 변경합니다.</li>
</ul>
<p>수정한 <code>WiiChuckClass</code>를 이용해 Perl 측에 보내줄 소스는 다음과 같습니다.</p>
<pre class="brush: cpp;">
#include <math.h>
#include "Wire.h"
#define MAXANGLE 90
#define MINANGLE -90
WiiChuck chuck = WiiChuck();
int angleStart, currentAngle;
int tillerStart = 0;
double angle;
void setup() {
Serial.begin(115200);
chuck.begin();
chuck.update();
}
void loop() {
delay(20);
chuck.update();
Serial.print(chuck.readRoll());
Serial.print(":");
Serial.print(chuck.readPitch());
Serial.print(":");
Serial.print((int)chuck.readAccelX());
Serial.print(":");
Serial.print((int)chuck.readAccelY());
Serial.print(":");
Serial.print((int)chuck.readAccelZ());
Serial.print(":");
Serial.print((int)chuck.readJoyX());
Serial.print(":");
Serial.print((int)chuck.readJoyY());
Serial.print(":");
if(chuck.zPressed())
Serial.print("T");
else
Serial.print("F");
Serial.print(":");
if(chuck.cPressed())
Serial.print("T");
else
Serial.print("F");
Serial.println();
}
</pre>
<p>이렇게 Nunchuck의 조작 값을 받아 어디에 이용할까요? 오늘은 Wii Nunchuck을 마우스로 사용해봅시다.
이를 위해 <a href="http://metacpan.org/module/Win32::GuiTest">Win32::GuiTest</a>의 마우스 조작 기능인 <code>SendMouse</code>를 이용해 보도록하겠습니다.</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Win32::SerialPort qw( :STAT 0.19 );
use Win32::GuiTest qw( :ALL );
my $serial_port = Win32::SerialPort->new('COM4') or die "Can't open serial port : $^E";
$serial_port->handshake('none');
$serial_port->baudrate(115200);
$serial_port->parity('none');
$serial_port->databits(8);
$serial_port->stopbits(1);
$serial_port->read_interval(0);
$serial_port->read_const_time(20);
$serial_port->write_char_time(1);
$serial_port->write_const_time(100);
$serial_port->buffers(4096, 4096);
$serial_port->write_settings or die "Can't change Device_Control_Block: $^E";
$serial_port->save('my_serial.cfg');
# nuncuck 의 조이스틱 값
my $prev_joy_x;
my $prev_joy_y;
$serial_port->are_match("\n");
while (1) {
if (my $line = $serial_port->lookfor) {
chomp $line;
print $line, "\n";
# nuchuck 으로 부터 수신하는 값은 이렇게 다양합니다. 그러나 우리는 조이스틱의 x,y 값과 두개의 버튼값만을 이용합니다.
my ($roll, $pitch, $accel_x, $accel_y, $accel_z, $joy_x, $joy_y, $button_z, $button_c) = split ':', $line;
# 두개의 버튼을 동시에 누르면 프로그램을 종료합니다.
if ($button_c eq "T" && $button_z eq "T") {
print "EXIT\n";
$serial_port->close;
exit 0;
}
# 조이스틱 X 값 처리
if ($joy_x) {
if ($prev_joy_x ne $joy_x) {
SendMouse "{REL$joy_x,0}";
$prev_joy_x = $joy_x;
}
}
# 조이스틱 Y 값 처리
if ($joy_y) {
if($prev_joy_y ne $joy_y) {
$joy_y = $joy_y > 0 ? -$joy_y : abs($joy_y);
SendMouse "{REL0,$joy_y}";
$prev_joy_y = $joy_y;
}
}
# 버튼을 눌렀을때 마우스 클릭 처리
if ($button_z eq "T") {
SendMouse "{LEFTCLICK}";
}
}
}
</pre>
<p>자, 그럼 시연하는 모습을 볼까요?
이로써, 사장님 자세로 여유롭게 의자에 기대서 웹서핑을 즐길 수 있게 되었습니다!</p>
<ul>
<li><a href="http://www.youtube.com/watch?v=1_gaxw9GKRg">동영상1. Nunchuck 마우스</a>
<br /><iframe width="560" height="315" src="http://www.youtube.com/embed/1_gaxw9GKRg" frameborder="0" allowfullscreen></iframe></li>
</ul>
<h2>정리하며</h2>
<ul>
<li><a href="http://metacpan.org/module/Win32::SerialPort">Win32::SerialPort</a>로 아두이노가 보내는 데이터를 수신합니다.</li>
<li><a href="http://metacpan.org/module/Win32::GuiTest">Win32::GuiTest</a>로 마우스를 제어합니다.</li>
<li>어른은 혼자 노는 겁니다.</li>
<li>'Flask 짜응 하악'하던 블로그에 불쑥 나타나 Dancer라는 십계를 내린 am0c군에게 감사의 뜻을 전하며 무지한 제게 많은 가르침을 주시는 aero님께도 감사의 말씀을 전해드리고 싶습니다. </li>
</ul>
2011-12-18T00:00:00+09:00ja3ckDBIx::Class로 스키마 관리하기http://advent.perl.kr/2011/2011-12-17.html<h2>저자</h2>
<p><a href="http://www.twitter.com/JEEN_LEE">@JEEN_LEE</a> - 자칭 0x1c살, 하니아빠, 키보드워리어, 영농후계자, 곶감판매업, 뿌나홀릭, silex 막내</p>
<h2>시작하며</h2>
<p>저는 회사 업무에서 펄의 대표적인 프레임워크인 <a href="http://www.catalystframework.org/">Catalyst</a>를
사용하고 있습니다. 그 중에서 여느 튜토리얼 문서에서의 기본 구성이라고도 할 수 있는
<a href="http://www.catalystframework.org/">Catalyst</a> + <a href="http://template-toolkit.org/">Template Toolkit</a> + <a href="http://search.cpan.org/perldoc?DBIx::Class">DBIx::Class</a> 구성으로 사용합니다.
언제나 개발의 시작은 어떤 데이터를 '어떤 구조로 유지'하며 '어떻게 사용하게끔 하느냐'하는
틀을 만드는 것이려나요? 제 생각에는 그렇지 않을까 합니다.
하지만 처음에 생각해서 마련한 틀은 <em>시간이 흐름에 따라</em>, <em>개발자 본인의 욕심에 따라</em>,
<em>갑이나 경영진의 변덕스러운 요구사항에 따라</em> 바뀌기 마련일 겁니다.
이런 과정에 있어서 <a href="http://search.cpan.org/perldoc?DBIx::Class">DBIx::Class</a>의 이용에 몇 가지 룰을 정하고 그걸 유지하면
스트레스 덜 받는 행복한 스키마 관리가 이뤄지지 않을까 생각합니다.</p>
<h2>스키마 클래스 생성</h2>
<p>여느 Catalyst 튜토리얼의 첫단락이 일단 <code>Hello World</code>를 찍는 것이라면,
아마 그 다음이나 다음다음 작업은 모델을 생성하는 것입니다.
대충 아래와 같은 커맨드에 길고 긴 옵션을 주면 스키마 클래스를 만들 수 있습니다.</p>
<pre class="brush: bash;">
$ ./script/myapp_web_create.pl model MyDB DBIC::Schema MyApp::Schema \
create=static [options] dbi:mysql:test_db test_user test_password
</pre>
<p>이렇게 해서 <code>test_db</code>라는 데이터베이스에 속한 테이블이 각각의 결과클래스가 생성됩니다.
그 중 하나의 예를 들면 다음과 같습니다.</p>
<pre class="brush: perl;">
package MyApp::Schema::Result::Access;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
MyApp::Schema::Result::Access
=cut
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use namespace::autoclean;
extends 'DBIx::Class::Core';
__PACKAGE__->table("access");
__PACKAGE__->load_components("InflateColumn::DateTime");
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"address",
{ data_type => "text", is_nullable => 0 },
"comment",
{ data_type => "text", is_nullable => 1 },
"created_on",
{
data_type => "datetime",
datetime_undef_if_invalid => 1,
default_value => "0000-00-00 00:00:00",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-09 18:27:52
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8E8XDlgZJWsPmqTw/xP34A
# You can replace this text with custom code or comments, and it will be preserved on regeneration
__PACKAGE__->meta->make_immutable;
1;
</pre>
<h2>하지말라고 하면 하지 말자</h2>
<p>처음에 언급한 몇가지 룰이라는 것이 있습니다. 가장 중요한 것이 바로 <em>'하지말라고 하면 하지 않는다.'</em>입니다.
위의 코드 중 주석 부분에는 다음과 같은 글귀가 있습니다.</p>
<pre class="brush: perl;">
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8E8XDlgZJWsPmqTw/xP34A
</pre>
<p>여기의 <code>md5sum:8E8XDlgZJWsPmqTw/xP34A</code>는 해당 결과 클래스의 코드를
MD5 체크섬 값으로서 코드의 변경 유무를 검사하고 있습니다.
어떤 코드를 추가하거나 테이블 관계 설정을 추가로 해야 할 때는
<em>반드시 이 문구 아래에서부터 코드를 적어나가도록</em> 합니다.
이런식으로 말이죠.</p>
<pre class="brush: perl;">
....
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8E8XDlgZJWsPmqTw/xP34A
__PACKAGE__->belongs_to( xxx => "MyApp::Schema::Result::XXX", { 'foreign.id' => 'self.id' });
sub mission_accessible { ... }
....
</pre>
<p>예전에는 <em>저 문구</em>를 무시하고 그냥 매번 데이터베이스의 변경이 있을 때마다
손으로 하나씩 맞춰주고는 했습니다. 메소드 추가나 관계 설정도 마찬가지였구요.
하지만 나중을 위해서는 반드시 시키는 대로 하는 것이 좋습니다.
특정 테이블에 컬럼이 추가되었다거나, 여러가지 테이블이 추가되었다거나,
그럴 때에는 다시 한번 더 위에서 입력한 스키마 클래스 생성 커맨드를 그대로 다시 실행합니다.
혹여나 이 때 말을 안듣고, <em>저 문구</em> 위에 스페이스 하나라도 잘못 썼다가는
스키마 클래스 자동 완성의 꿈은 깨는 것이 좋습니다.</p>
<pre class="brush: bash;">
$ ./script/myapp_web_create.pl model MyDB DBIC::Schema MyApp::Schema \
create=static [options] dbi:mysql:test_db test_user test_password
exists "/*/../lib/MyApp/Web/Model"
exists "/*/../t"
Dumping manual schema for MyApp::Schema to directory /*/../lib ...
DBIx::Class::Schema::Loader::make_schema_at(): Checksum mismatch in '/*/../lib/MyApp/Schema/Result/Access.pm', the auto-generated part of the file has been modified outside of this loader. Aborting.
If you want to overwrite these modifications, set the 'overwrite_modifications' loader option.
</pre>
<p>그렇지 않을 경우에는 변경되거나 추가된 테이블은 무사히 특정 결과클래스로 덤프됩니다.</p>
<h2>하지만 이건 아니잖아!!</h2>
<p>일단 시키는 대로 추가할 관계 설정이나 메소드들은 각각 <em>그 문구</em> 아래에 넣었습니다.
자, 그럼 이 기본적인 틀 안에서 특정 컬럼을 다양한 컴포넌트 모듈을 사용해
확장해가고 싶은 생각도 들기 시작하겠죠?</p>
<pre class="brush: perl;">
__PACKAGE__->load_components("InflateColumn::DateTime");
....
__PACKAGE__->add_columns(
....
"created_on",
{
data_type => "datetime",
datetime_undef_if_invalid => 1,
default_value => "0000-00-00 00:00:00",
is_nullable => 0,
},
....
#DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8E8XDlgZJWsPmqTw/xP34A
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
"created_on",
{
data_type => "datetime",
datetime_undef_if_invalid => 1,
default_value => "0000-00-00 00:00:00",
is_nullable => 0,
set_on_create => 1,
},
);
....
</pre>
<p>그래서 일단 <em>그 문구</em> 위에 쓰지 말라고 했으니까, 밑에 추가하고 싶은
컴포넌트(TimeStamp)를 넣고, <code>created_on</code> 컬럼에 해당 컴포넌트의 동작을 발생시키는
속성 값 <code>set_on_create</code>를 추가합니다.
이렇게 추가한 코드는 SQL <code>INSERT</code>문에 해당하는 액션이 발생했을 때 자동으로
<code>created_on</code> 값을 지정해주도록 합니다.
위에서 아무리 <em>그 문구</em> 위에 쓰지말라고 했어도 중복되는 코드를 매번 이렇게
적어야 된다니... 맙소사! 거기에 <code>created_on</code> 같은 경우에는 거의 대부분의 테이블에
다 들어가 있다고 생각한다면... 아아.. 끔찍합니다.</p>
<h2>이건 더더욱 아니잖아</h2>
<p><code>Access</code> 이외에 <code>Deny</code>, <code>User</code>, <code>Group</code> 등의 많은 결과클래스가 있다고 합시다.
그리고 이 결과클래스들에 토씨하나 안틀리고 똑같은 메소드가 정의된다고 생각해봅니다.
정말로 피가 DRY합니다.
이 경우에는 대개의 결과 클래스가 상속하고 있는 <code>DBIx::Class::Core</code>를 손을 봐야
되겠죠. 그럼 <code>ResultBase</code>라는 클래스를 만들고 이것이 <code>DBIx::Class::Core</code>를 상속하도록 하고,
그 외 여타 결과클래스들이 <code>ResultBase</code>를 상속받도록 합니다.
<code>ResultBase</code>의 경우는 아래와 같습니다.</p>
<pre class="brush: perl;">
package MyApp::Schema::ResultBase;
use Moose;
use MooseX::NonMoose;
use namespace::autoclean;
extends 'DBIx::Class::Core';
sub my_method {};
__PACKAGE__->meta->make_immutable;
1;
</pre>
<p>그리고 결과 클래스에서 이 <code>ResultBase</code>를 상속합니다.</p>
<pre class="brush: perl;">
package MyApp::Schema::Result::Access;
....
- extends 'DBIx::Class::Core';
+ extends 'MyApp::Schema::ResultBase';
....
</pre>
<p>어, 그런데 뭔가 걸립니다.</p>
<pre class="brush: perl;">
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8E8XDlgZJWsPmqTw/xP34A
</pre>
<p>맙소사! <code>ResultBase</code>를 상속하는 것조차도 이 문구위에 놓이게 되니 맘편히 고칠수도 없네요!</p>
<h2>스키마 클래스 덤프에 설정파일을 사용</h2>
<p>위에서 스키마 클래스를 덤프할 때 기나긴 옵션이 붙은 커맨드가 있었습니다.
데이터베이스 구성이 바뀌어질 때마다 그 긴 커맨드를
일일이 붙여넣기 식으로 넣어야하니, 좋은 방법 같지는 않습니다.
우선 <em>기존 명령</em>의 사용을 그만두도록 합니다.
컴포넌트 등록이나, <code>ResultBase</code> 클래스 설정이나 컬럼의 속성 추가 등등
매번 스키마 클래스 덤프할 때마다 자신의 상황에 맞게
이것저것 개조(customize)할 필요가 있습니다.</p>
<p>아쉽게도 <em>기존 명령</em>으로 호출되는 <a href="http://search.cpan.org/perldoc?Catalyst::Model::DBIC::Schema">Catalyst::Model::DBIC::Schema</a> 모듈에 속해있는
<a href="http://search.cpan.org/perldoc?Catalyst::Helper::Model::DBIC::Schema">Catalyst::Helper::Model::DBIC::Schema</a>로는 현재 상황을 헤쳐 나가기 힘듭니다.
그래서 위 모듈 안에서 본질적으로 스키마 클래스 덤프에 사용되는 모듈인
<a href="http://search.cpan.org/perldoc?DBIx::Class::Schema::Loader">DBIx::Class::Schema::Loader</a>를 사용합니다.
<a href="http://search.cpan.org/perldoc?DBIx::Class::Schema::Loader">DBIx::Class::Schema::Loader</a>가 설치되어 있으면
<code>dbicdump</code>라는 명령이 존재할 것입니다.
이 <code>dbicdump</code> 커맨드에 이제부터 이 상황을 타개할 설정 파일을 담겠습니다.
설정 파일은 <a href="http://search.cpan.org/perldoc?Config::Any">Config::Any</a> 모듈로 처리되므로
펄에서 쓰이는 어떤 형식이라도 다룰 수 있습니다.
심지어는 <em>펄 코드</em> 자신도 설정으로 사용할 수 있습니다.
아래는 <code>dbicdump</code> 설정 파일의 예제입니다.</p>
<pre class="brush: perl;">
{
schema_class => "MyApp::Schema",
connect_info => {
dsn => $ENV{DB_DSN} || "dbi:mysql:test_db:127.0.0.1",
user => $ENV{DB_USER} || "test_user",
pass => $ENV{DB_PASSWORD} || "test_password",
mysql_enable_utf8 => 1,
},
loader_options => {
dump_directory => 'lib',
naming => { ALL => 'v8' },
skip_load_external => 1,
relationships => 1,
use_moose => 1,
only_autoclean => 1,
col_collision_map => 'column_%s',
result_base_class => 'MyApp::Schema::ResultBase',
overwrite_modifications => 1,
datetime_undef_if_invalid => 1,
custom_column_info => sub {
my ($table, $col_name, $col_info) = @_;
if ($col_name eq 'created_on') {
return { %{ $col_info }, set_on_create => 1 };
}
},
},
}
</pre>
<p>항목들이 많으므로 전부 설명하는 대신, 위에서 봉착했던 문제에 대해 추려볼까요?
우선 <code>ResultBase</code> 클래스 문제입니다. <code>result_base_class</code> 값을 지정해 줌으로써
모든 결과클래스들은 <code>DBIx::Class::Core</code>가 아니라 <code>MyApp::Schema::ResultBase</code>를
상속받게 됩니다. 물론 <code>MyApp::Schema::ResultBase</code>는 스스로 정의해줘야 합니다.
다음은 컬럼의 컴포넌트를 이용한 확장 문제입니다.
MD5 체크섬 값 아래에 중복되는 코드를 매번 적어주어야 했습니다.
이렇게 사용할 컴포넌트들을 결과클래스 별로 지정하는 대신 <code>ResultBase</code> 클래스를 읽어들입니다.
사실 이처럼 <code>ResultBase</code>를 놓고 여기에 컴포넌트를 일괄해서 읽어들이는 방식은
Cookbook 문서에서도 <a href="http://search.cpan.org/~arodland/DBIx-Class-0.08196/lib/DBIx/Class/Manual/Cookbook.pod#STARTUP_SPEED">스타트업 속도 향상을 위해 권장</a>하고 있습니다.</p>
<pre class="brush: perl;">
package MyApp::Schema::ResultBase;
use Moose;
use MooseX::NonMoose;
use namespace::autoclean;
extends 'DBIx::Class::Core';
__PACKAGE__->load_components(qw/
InflateColumn::DateTime
TimeStamp
...
/);
__PACKAGE__->meta->make_immutable;
1;
</pre>
<p>그리고 컴포넌트의 사용을 위한 컬럼의 속성은
<code>custom_column_info</code> 속성에 코드를 등록해 지정할 수 있습니다.
위의 코드처럼 등록하면 <code>created_on</code>에 <code>TimeStamp</code> 컴포넌트를 사용하기 위한
속성 값인 <code>set_on_create</code>이 모든 결과클래스에 추가됩니다.</p>
<h2>설정파일을 이용한 스키마 덤프</h2>
<p>앞에서 정의한 설정파일을 <code>schema-loader-config.pl</code>이라는 파일로 지정하고
다음 명령을 실행하면 좀 더 유연하게 스키마 클래스를 덤프할 수 있습니다.</p>
<pre class="brush: bash;">
$ dbicdump schema-loader-config.pl
</pre>
<h2>정리하며</h2>
<ul>
<li><a href="http://search.cpan.org/perldoc?DBIx::Class::Schema::Loader">DBIx::Class::Schema::Loader</a>로 스키마 클래스를 덤프합니다.</li>
<li>결과 클래스 안의 MD5 체크섬 값에 유의하여 특정 부분 위의 코드는 건드리지 않습니다.</li>
<li><code>ResultBase</code> 같은 기본 클래스를 두고 결과클래스 내의 공용 메소드, 컴포넌트들을 일괄로 정의하고 관리하도록 합니다.</li>
<li>설정 파일을 이용해 추가 속성이 필요한 컬럼 등을 지정할 수 있습니다.</li>
<li><code>DBIx::Class</code>는 나쁘지 않습니다.</li>
<li>여타 설정 항목들에 대해서는 <a href="http://search.cpan.org/perldoc?DBIx::Class::Schema::Loader::Base">DBIx::Class::Schema::Loader::Base</a> 모듈 문서를 참고합니다.</li>
<li>이 모든 작업에 논의와 영감을 준 <a href="http://www.twitter.com/aanoaa">@aanoaa</a>(a.k.a. 홍선생)님에게 경의를 표합니다.</li>
</ul>
2011-12-17T00:00:00+09:00JEEN_LEEperlbrew, local::lib, smartcd 를 이용하여 Perl 환경 구축하기http://advent.perl.kr/2011/2011-12-16.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/corund">@corund</a> -
웹프로그래머. 다년간 Java로 웹 개발을 해왔으며 마음에 드는 언어를 찾기위해
여러 언어를 전전하다 Perl의 매력을 발견하고 현재 주로 Perl로 개발하고 있다.
Catalyst 프레임워크로 웹 개발을 하며 현재 <a href="http://estin.net">경제 포털 ESTIN</a>을 구축 및 운영 중이다.</p>
<h2>시작하며</h2>
<p>Perl은 하위 호환성이 좋고 <a href="https://metacpan.org/module/CPAN">cpan</a>이라는 막강한 툴이 있기 때문에
특별히 버전이나 모듈들을 관리할 필요가 많진 않습니다.
하지만 일반에게 서비스할 웹 서비스를 개발하거나 따로 배포할 프로그램을
개발한다면 특별히 버전과 모듈들을 관리할 필요가 있습니다.
또한 아직 사용중인 운영체제 배포판에서 공식 지원하기 전의
최신 Perl을 실험해 보고 싶은 경우도 있습니다.
Linux나 OSX을 쓴다면 이럴 때
<a href="http://perlbrew.pl">perlbrew</a>(<a href="https://metacpan.org/module/App::perlbrew">CPAN의 App::perlbrew 모듈</a>)와
<a href="https://metacpan.org/module/local::lib">local::lib</a>를 이용해 버전과 모듈들을 따로 관리할 수 있습니다.
덧붙여 <a href="https://github.com/cxreg/smartcd">smartcd</a>와 같은 <code>bash</code> 유틸리티를 이용하면
이를 더욱 편리하게 이용할 수 있습니다.</p>
<h2>예제</h2>
<p>이 예제에서는 Perl 5.12.x(현재 5.12.4) 버전을 주로 사용하며
5.14.x(현재 5.14.2) 버전을 시험하는 경우를 예로 듭니다.
<code>~/project</code> 디렉터리 밑에 여러 프로젝트를 두고
<code>~/project/catalyst</code> 디렉터리는 <a href="https://metacpan.org/module/Catalyst">Catalyst</a> 웹 프로젝트를,
<code>~/project/wx</code> 디렉터리는 <a href="https://metacpan.org/module/Wx">Wx</a> GUI 어플리케이션 프로젝트를 두고
관련 모듈들을 따로 관리하는 경우를 가정합니다.
<code>~/project/pilot</code> 디렉터리 밑에는 5.14.x 버전을 시험하는 프로젝트를 두겠습니다.
즉 일반적인 경우의 Perl 버전은 5.12.4가 되며 <code>~/project/pilot</code> 디렉터리로
가면 버전이 5.14.2가 되고, <code>~/project/catalyst</code> 와 <code>~/project/wx</code>에서
각각 따로 모듈을 관리, 이용할 수 있게 하는 것입니다.</p>
<h2 id="perlbrewlocal::lib">perlbrew & local::lib</h2>
<p>사실 Perl을 컴파일 하는 것은 어렵지 않습니다.
하지만 조금이라도 더 손쉽게 다양한 버전의 Perl을
컴파일하고 관리하며 사용하려면 <code>perlbrew</code>를 사용하는 것이 유리합니다.
여기서는 <code>perlbrew</code>를 <code>~/perl5/perlbrew</code> 디렉터리에 설치하겠습니다.
명령줄에서 다음 명령을 입력합니다.</p>
<pre class="brush: bash;">
$ export PERLBREW_ROOT=~/perl5/perlbrew
$ curl -kL http://xrl.us/perlbrewinstall | bash
</pre>
<p><img src="2011-12-16-1.png" alt="perlbrew install" id="perlbrewinstall" /></p>
<p><code>perlbrew</code>가 설치되었고 안내에 따라 다음과 같이 <code>bash</code>
초기화 파일(<code>.bashrc</code> 또는 <code>.profile</code>)에 스크립트를 추가합니다.</p>
<pre class="brush: bash;">
$ echo "source ~/perl5/perlbrew/etc/bashrc" >> .bashrc
</pre>
<p>다시 로그인하면 <code>perlbrew</code>를 사용할 수 있습니다.
이제 <code>install</code> 명령을 사용해 5.12.4 버전과 5.14.2 설치할 수 있습니다.
특별한 이유가 없다면 <code>-D usethreads</code> 옵션을 명시해서 쓰레드 기능을
사용할 수 있도록 컴파일합니다.</p>
<pre class="brush: bash;">
$ perlbrew install perl-5.12.4 -D usethreads
$ perlbrew install perl-5.14.2 -D usethreads
</pre>
<p>Perl 5.12.4 버전을 기본으로 사용하려면 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ perlbrew switch perl-5.12.4
$ perl -v
</pre>
<p><img src="2011-12-16-2.png" alt="check perl install" id="checkperlinstall" /></p>
<p>현재 쉘에 한해 임시로 Perl 5.14.2 버전을 사용하려면 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ perlbrew use perl-5.14.2
$ perl -v
</pre>
<p>각 버전의 Perl에 대해 <a href="https://metacpan.org/module/App::cpanminus">cpanm</a>과
<a href="https://metacpan.org/module/local::lib">CPAN의 local::lib 모듈</a>을 설치합니다.</p>
<pre class="brush: bash;">
$ perlbrew switch perl-5.12.4
$ cpan App::cpanminus local::lib
$ perlbrew switch perl-5.14.2
$ cpan App::cpanminus local::lib
</pre>
<p>지금부터 모든 모듈은 <code>local::lib</code>를 이용해 <code>~/perl5/local</code> 디렉터리 아래서 관리합니다.</p>
<h2 id="smartcd">smartcd</h2>
<p><code>perlbrew</code>나 <code>local::lib</code>나 모두 환경 변수를 이용해 사용할
Perl 버전과 모듈 설치 및 검색 디렉터리를 지정합니다.
따라서 필요할 때마다 환경 변수를 적절하게 변경해주어야 합니다.
이는 여간 번거로운 일이 아닐 수 없습니다.
이런 번거로움을 해결하기 위해 <a href="https://github.com/cxreg/smartcd">smartcd</a>를 써볼까요?
smartcd는 디렉터리를 옮겨 다닐 때마다 자동으로
환경 변수를 변경해주는 유틸리티입니다.
smartcd를 설치하는 방법은 무척 간단합니다.</p>
<pre class="brush: bash;">
$ cd project
$ git clone git://github.com/cxreg/smartcd.git
$ cd smartcd
$ make install
$ make setup
</pre>
<p>smartcd는 홈 디렉터리에 <code>.bash_arrays</code>, <code>.bash_smartcd</code>, <code>.bash_varstash</code>
파일을 만들고 <code>bash</code> 초기화 파일에 초기화 코드를 설정합니다.
디렉터리를 옮겨갈 때마다 지정한 쉘 스크립트를 실행하는데
<code>~/.smartcd</code> 디렉터리 밑에 저정합니다.</p>
<p><code>~/project/catalyst</code> 디렉터리로 들어갔을 때 <code>local::lib</code>로
<code>~/perl5/local/5.12.5/catalyst</code> 디렉터리 밑의 모듈을
관리할 수 있도록 하려면 다음처럼 실행합니다.</p>
<pre class="brush: bash;">
$ perl -Mlocal::lib=~/perl5/local/5.12.4/catalyst
</pre>
<p><img src="2011-12-16-3.png" alt="init local::lib" id="initlocal::lib" /></p>
<p>앞의 명령을 실행하면 <code>~perl5/local/5.12.4/catalyst</code> 디렉터리를 생성하고
모듈들을 관리할 구조들을 생성한 후 환경 변수를 지정할 코드를 출력됩니다.
보통은 이 구문을 Bash 초기화 파일에 추가합니다만 여기서는 smartcd로
관리할 것이므로 <code>~/.smartcd</code> 디렉터리 밑에 설정합니다.
<code>~/.smartcd/home/<your_account>/project/catalyst</code> 디렉터리를 만들고
그 밑에 <code>bash_enter</code> 파일을 만듭니다.
이 파일이 smartcd로 <code>~/project/catalyst</code> 디렉터리에
들어갈 때 실행되는 스크립트입니다.
이 곳에서 <code>local::lib</code> 설정을 하면 됩니다.</p>
<pre class="brush: bash;">
$ mkdir -p ~/.smartcd/home/<your_account>/project/catalyst
$ cd ~/.smartcd/home/<your_account>/project/catalyst
$ echo 'eval $(perl -Mlocal::lib=~/perl5/local/5.12.4/catalyst | sed '\''s/export/autostash/'\'')' > bash_enter
</pre>
<p>즉석에서 생성한 <code>bash_enter</code> 파일은 <code>perl -Mlocal::lib=~/perl5/local/5.12.4/catalyst</code>
명령을 실행했을 때 출력되는 환경 변수 값으로 설정하는 스크립트인데,
<code>export</code> 대신 smartcd의 <code>autostash</code>를 사용하도록 변경한 점을 눈여겨 보세요.
<code>autostash</code>는 현재 환경 변수 값을 저장하고 지정한 값으로 바꾼 다음,
디렉터리를 떠날 때 원래 값으로 복원합니다.
이렇게 함으로써 <code>~/project/catalyst</code> 디렉터리로 들어오면 <code>local::lib</code>로
관리가 되어 지정한 디렉터리(<code>~/perl5/local/5.12.4/catalyst</code>)에 모듈을
설치하고 사용할 수 있게 됩니다.
이제 <code>cpanm</code> 등을 사용하여 필요한 모듈을 설치할 수 있습니다.</p>
<pre class="brush: bash;">
$ cd ~/project/catalyst
$ cpanm Catalyst::Runtime
</pre>
<p>같은 방식으로 <code>~/project/wx</code> 디렉터리에는 <code>~/perl5/local/5.12.4/wx</code>
하부에 모듈을 관리하도록 설정하려면 다음처럼 실행합니다.</p>
<pre class="brush: bash;">
$ perl -Mlocal::lib=~/perl5/local/5.12.4/wx
$ mkdir -p .smartcd/home/<your account>/project/wx
$ cd ~/.smartcd/home/<your accunt>/project/wx
$ echo 'eval $(perl -Mlocal::lib=~/perl5/local/5.12.4/wx | sed '\''s/export/autostash/'\'')' > bash_enter
</pre>
<p>이제 <code>~/project/wx</code> 디렉터리로 들어가 원하는 모듈을 설치하고 사용하면 됩니다.</p>
<pre class="brush: bash;">
$ cd ~/project/wx
$ cpanm Wx
</pre>
<p>마지막으로 <code>~/project/pilot</code> 디렉터리로 들어가서
<code>perlbrew</code>를 이용해 5.14.2 버전을 사용해 보겠습니다.
<code>perlbrew</code>에는 현재 쉘에 한해 임시로 Perl 버전을 바꾸는 명령어인 <code>use</code>가 있습니다.
이 기능을 이용 특정 디렉터리에 들어갈 때 버전을 바꿀 수 있습니다만,
smartcd 설정이 복잡해지는 단점이 있습니다.
즉, 디렉터리에 들어갈 때 실행하는 스크립트인 <code>bash_enter</code>에서
원래 버전을 저장해두고 버전을 바꾼 다음,
나올 때 실행하는 스크립트인 <code>bash_leave</code>에서 복원해야 합니다.</p>
<p>대신 <code>perlbrew env</code> 명령을 이용하면 간단하게 처리할 수 있습니다.
<code>env</code> 명령은 <code>perlbrew</code>가 쉘과의 통합을 위해 사용하는 비교적 저수준 명령입니다.
이 명령을 사용하면 Perl 버전을 변경할 때 지정하는 환경 변수를 출력합니다.
<code>perlbrew</code>는 이 환경 변수와 <code>PATH</code> 환경 변수를 변경해서 Perl 버전을 변경합니다.
따라서 <code>~/.smartcd/home/<your_accout>/project/pilot/bash_enter</code> 스크립트의
내용은 다음과 같습니다.</p>
<pre class="brush: bash;">
eval $(perlbrew env perl-5.14.2 | sed 's/export\(.*\)$/autostash\1;/')
autostash PATH_WITHOUT_PERLBREW="$(perl -e 'print join ":", grep { index($_, $ENV{PERLBREW_ROOT}) } split/:/,$ENV{PATH};')"
autostash PATH="$PERLBREW_PATH:$PATH_WITHOUT_PERLBREW"
</pre>
<p><code>perlbrew env</code>를 이용해 Perl 버전 변경에 필요한 환경 변수 값을 얻고,
<code>sed</code>를 이용하여 smartcd에서 이용할 수 있게 수정하며,
마지막으로 <code>PATH</code> 환경 변수를 수정하는 점을 주의깊게 보세요.
이제 <code>~/project/pilot</code> 으로 들어가면 5.14.2 버전으로 바뀌게 됩니다.</p>
<p><img src="2011-12-16-4.png" alt="switch perl version" id="switchperlversion" /></p>
<p>5.12.4 버전의 경우와 마찬가지로 <code>~/project/pilot</code> 디렉터리 밑에 하부 디렉터리를
만들고 <code>local::lib</code>로 각각 모듈을 관리할 수 있습니다.</p>
<pre class="brush: bash;">
$ perl -Mlocal::lib=~/perl5/local/5.14.2/catalyst
$ mkdir -p ~/.smartcd/home/<your_account>/project/pilot/catalyst
$ cd ~/.smartcd/home/<your_account>/project/pilot/catalyst
$ echo 'eval $(perl -Mlocal::lib=~/perl5/local/5.14.2/catalyst | sed '\''s/export/autostash/'\'')' > bash_enter
$ cd ~/project/pilot/catalyst
$ perl -v
$ cpanm Catalyst::Runtime
</pre>
<h2>정리하며</h2>
<p>처음 Java에서 Perl로 넘어왔을 때 가장 편하면서도 불편했던 것이 <code>cpan</code>이었습니다.
원하는 모듈을 간편히 설치해준다는 점에서 편했지만
배포시 모듈을 어떻게 관리해야 할지가 불편했던 것이죠.
정확히는 불편했다기 보다 무엇이 어떻게 돌아가는지 몰라서 불안했달까요?
하지만 <code>perlbrew</code>와 <code>local::lib</code>를 알고 사용법을 조금씩 익혀가면서
Perl과 관련 모듈을 무척 편하게 다룰 수 있다는 것을 알게 되었습니다.
이 두 Perl 모듈은 너무나도 간단해 마치 마술처럼 동작하는 것처럼 보이지만
사실 몇 가지 환경 변수를 조작하는 아주 간단히 동작을 수행할 뿐입니다.
덕분에 smartcd와 같은 쉘 유틸리티와 연동하면 무척 편하게 Perl 개발 환경을 구축할 수 있습니다.</p>
2011-12-16T00:00:00+09:00corund한 이미지 안에 들어있는 사진들 추출하기 - Prima 모듈http://advent.perl.kr/2011/2011-12-15.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> -
개인 자료라고 믿기 어려울 정도의 방대한 Perl 자료를
제공하고 있는 <a href="http://gypark.pe.kr/wiki/Perl">gypark.pe.kr</a>의 주인장, Raymundo라는 닉을 사용하기도 한다. 네이버 Perl 카페 회원</p>
<h2>시작하며</h2>
<p>오래 전 있었던 일입니다.
우연히 다음 그림과 같은 사진 파일을 보게 되었습니다.</p>
<p><img src="2011-12-15-1.png" alt="여러 사진이 포함된 하나의 이미지" id="" /><br />
<em>그림 1.</em> 여러 사진이 포함된 하나의 이미지</p>
<p>하나의 JPEG 사진 파일 안에 사진 세 장이 합쳐져 있는데
이게 모니터에 다 들어오지 않아 보기에 불편하더랍니다.
그래서 저 안에 있는 사진 세 장을 각각 별도의 파일로 저장하고 싶어졌습니다.</p>
<p>위와 같이 사진이 세 장 뿐이면 그림판에서 영역 지정해 저장하면 됩니다.
하지만 파일 하나에 사진이 수십 장 들어있다면,
그리고 그런 파일이 또 수십 개나 있다면 정말 골치아픈 일이 될 것입니다.
이것을 자동으로 할 수 없을까 궁리해 보았습니다.</p>
<h2>아이디어</h2>
<p>이미지 처리를 위한 빠르고 효율적인 알고리즘이 많이 있을 것입니다.
하지만 그런 것을 전혀 몰라도 원하는 결과는 여전히 얻을 수 있습니다.
가장 직관적이고 단순한 방법을 생각해봅시다.</p>
<p>여러 사진을 병합해 한 장의 사진을 만들면 배경색은 균일하게 쓰입니다.
반대로, 사진은 동일한 색상의 픽셀이 연속해서 나오는 경우가 거의 없습니다.
따라서, 사진 파일의 각 픽셀을 수직, 수평 방향으로 스캔하면서,
한 라인의 첫 픽셀부터 마지막 픽셀까지 동일한 색상이면 그 라인은 배경이고,
그렇지 않으면 사진이라고 간주해도 무방할 것 같습니다.</p>
<p>따라서, 먼저 파일 전체를 일단 세로방향으로 진행하며 스캔합니다.
제일 바닥부터 사진의 수평 라인을 한 줄씩 읽어 나가다가
배경이 아닌 부분을 만나면 그 라인을 시작 지점으로 지정하고,
다시 배경이 되는 라인을 만나면 그 라인을 끝 지점으로
지정합니다.
그런 다음, 시작지점부터 끝지점까지의 영역을 추출하여 하나의 이미지 조각으로
저장합니다. <strong>그림 2</strong>를 참고하세요.</p>
<p><img src="2011-12-15-2.png" alt="세로 반향으로 진행하며 수평 라인을 한 줄씩 읽는다." id="" /><br />
<em>그림 2.</em> 세로 반향으로 진행하며 수평 라인을 한 줄씩 읽는다.</p>
<p>이렇게 총 세 개의 이미지 조각이 저장될 것입니다.
이번에는 각 조각마다 왼쪽에서 오른쪽으로 진행하면서 수직 라인을 스캔합니다.
역시 마찬가지로 배경이 아닌 부분을 처음 만나면 그 라인을 시작 지점으로 지정하고,
다시 배경 라인을 만나면 그 라인을 끝 지점으로 지정하여
시작 지점부터 끝 지점까지의 영역을 추출합니다.
그러면 마지막으로 추출된 영역이 우리가 원하는 한 장의 사진이 됩니다.
이것을 저장하면 되겠습니다. <strong>그림 3</strong>을 참고하세요.</p>
<p><img src="2011-12-15-3.png" alt="좌에서 우측으로 진행하면서 수직 라인을 스캔한다." id="" /><br />
<em>그림 3.</em> 좌에서 우측으로 진행하면서 수직 라인을 스캔한다.</p>
<p>이 원리로 각각 세 장의 사진을 저장할 수 있습니다.
사실 이 방법으로는 제대로 추출하지 못하는 경우가 있지만,
어려운 건 나중에 생각합시다.</p>
<p>우리는 이미지 파일을 읽고, 각 픽셀의 색상 값을 구하고,
원하는 영역을 추출하고, 다시 이미지 파일로 저장하는 기능이 필요합니다.
<a href="http://cpan.org/">CPAN</a>에는 이런 기능을 제공하는 여러 모듈이 있습니다.
오늘은 <a href="http://metacpan.org/module/Prima">Prima 모듈</a>을 사용해 보도록 하겠습니다.</p>
<h2 id="prima">Prima 모듈 설치</h2>
<p><a href="http://metacpan.org/module/Prima">Prima 모듈</a>은 단순히 이미지 파일을 다루는 모듈이 아닙니다.
멀티 플랫폼에서 호환되는 GUI개발 툴킷입니다.
그러나 이미지를 다루는 기능만 필요할 때에도 Prima가 안성맞춤입니다.</p>
<p><a href="http://cpan.org/">CPAN</a>에는 이미지를 처리하는 모듈이 많이 있습니다.
하지만 그 중에는 JPEG나 GIF 등의 포맷까지 다루기 위해서는
해당 포맷을 위한 별도의 라이브러리가 시스템에 설치되어 있어야
하는 모듈도 있습니다.
반면, Prima 모듈은 자체적으로 라이브러리를 제공하기 때문에
윈도우에서도 cpan을 통해 아주 간단히 컴파일 및 설치가 됩니다.</p>
<pre class="brush: plain;">
c:\> cpan Prima
</pre>
<p>위와 같이 입력하면 Prima 모듈과, Prima에서 외부 이미지 처리를 위해 사용되는
Prima::codecs::win32 또는 Prima::codecs::win64 모듈까지 설치됩니다.</p>
<p>이상하게도 이미지 처리 라이브러리와 링크가 되지 않아
끝내 위 방법으로 설치하지 못하는 경우가 발생할 수도 있습니다.
제가 이 기사를 작성하기 위해 최근에 모듈을 다시 설치하면서 이 문제가 발생했는데
이때 저의 환경은 윈도 XP 서비스팩 3, Strawberry Perl 5.12.3이었습니다.</p>
<p>이처럼 컴파일 과정에서 에러가 나서 설치에 실패할 경우에는
먼저 Prima::codecs::win32 (또는 win64) 모듈만 cpan을 통해 설치하고,
Prima 자체는 Prima 웹사이트에서 배포하는 바이너리를 내려받습니다.</p>
<p><a href="http://prima.eu.org/download/bindist.html">이곳에서</a> 자신의 환경에 맞는 압축 파일을
다운로드한 후 압축을 풀고, 압축을 푼 디렉터리에 들어가서
아래와 같은 명령으로 설치합니다.</p>
<pre class="brush: plain;">
c:\> perl ms_install.pl
</pre>
<h2 id="prima">Prima 코드 예제</h2>
<p>여기에 사용된 코드는 <a href="http://metacpan.org/module/Prima::Image">Prima::Image</a>
문서에 주로 나와 있습니다. 아래에 몇 가지 함수의 예를 코드로 정리하였습니다.</p>
<pre class="brush: perl;">
use Prima;
# "0.bmp"를 읽음
my $image = Prima::Image->load('0.bmp');
die "$@" unless $image;
# 이미지의 가로와 세로 길이를 얻어냄
# 만일 width(10) 처럼 setter 로 사용할 경우 그림이 확대 또는 축소된다.
print "Width : ", $image->width(), "\n";
print "Height: ", $image->height(), "\n";
# 가로10, 세로5 좌표 픽셀의 컬러값을 얻음
# 이 때 그림의 좌측 하단이 (0,0)이다.
printf "%06x", $image->pixel(10, 5);
# 5,0 자리 픽셀의 컬러를 지정
$image->pixel(5, 0, 0x00ff00);
# 일부 추출 (x오프셋, y오프셋, 폭, 높이)
my $newimage = $image->extract(5, 0, 12, 2);
# 저장
$newimage->save('01.bmp');
</pre>
<h2>완성</h2>
<p>아래는 완성된 코드입니다.
위 아이디어를 그대로 코드로 옮긴 형태이기 때문에 개선의 여지가 많을 것입니다.
자세한 설명은 코드에 있는 주석을 참고하세요.</p>
<pre class="brush: perl;">
#
# 실행:
# extract_photo.pl 파일명
#
# 파일을 열어서 내부에 포함된 사진들을 추출하여 개별 파일로 저장함
#
# Raymundo ( twitter: @gypark )
# 2011.12.11
use strict;
use warnings;
use Prima;
# 파일명을 명령행 인자로 받고 이름과 확장자 분리
my $filename = $ARGV[0];
my ( $basename, $suffix ) = $filename =~ /(.+)\.([^.]*)$/;
# 파일을 로드
my $image = Prima::Image->load( $filename );
die "can't load [$filename]: $@" unless $image;
# 제일 귀퉁이의 색상이 배경색이라고 가정
my $background = $image->pixel(0, 0);
# 먼저 세로방향으로 진행하며 사진 조각 추출
my @pieces; # 추출되는 이미지 조각의 배열
my $outside = 1; # 현재 스캔하는 가로 라인이 배경인지 아닌지 체크
my $y1; # 사진이 시작되는 y좌표를 저장하기 위한 임시 변수
foreach my $y ( 0 .. $image->height() - 1 ) {
if ( ! same_color_horizon( $image, $y, $background ) ) {
if ( $outside ) {
# 배경에서 배경이 아닌 라인으로 처음 진입할 때 y좌표 기록
$y1 = $y;
$outside = 0;
}
}
else {
if ( ! $outside ) {
# 배경이 아닌 라인에서 배경 라인으로 나가는 시점에서 이미지 조각 추출
push @pieces, $image->extract( 0, $y1, $image->width(), $y - $y1 );
$outside = 1;
}
}
}
# 이 시점에서, @pieces 에는 사진 조각들이 저장되어 있음
# 각 조각에 대해서, 이번에는 가로 방향으로 진행하면서 좌우의 배경을 떼어낸다
my $count = 1; # 저장할 때 파일이름에 번호를 붙이기 위한 카운터
foreach my $piece ( @pieces ) {
my $outside = 1;
my $x1; # 사진이 시작되는 x좌표를 저장하기 위한 임시 변수
foreach my $x ( 0 .. $piece->width() - 1 ) {
if ( ! same_color_vertical( $piece, $x, $background ) ) {
if ( $outside ) {
# 배경에서 배경이 아닌 라인으로 처음 진입할 때 x좌표 기록
$x1 = $x;
$outside = 0;
}
}
else {
if ( ! $outside ) {
# 배경이 아닌 라인에서 배경 라인으로 나가는 시점에서 이미지 조각 추출
my $photo = $piece->extract( $x1, 0, $x - $x1, $piece->height() );
$outside = 1;
# 추출한 조각을 파일로 저장
# 저장하는 이름은 "원본사진이름_두자리숫자.원본사진확장자"
my $photoname = sprintf( "%s_%02d.%s", $basename, $count++, $suffix );
$photo->save($photoname) or die "$@";
print "* [$photoname] saved\n";
}
}
}
}
# 그림 객체 $img 와 세로좌표 $y 를 인자로 받아서
# 세로좌표 $y에 해당하는 가로선이 전부 동일한 색상 $color인지 여부를 반환
sub same_color_horizon {
my ( $img, $y, $color ) = @_;
foreach my $x ( 0 .. $img->width() - 1 ) {
return 0 if ( $img->pixel($x, $y) != $color );
}
return 1;
}
# 그림 객체 $img 와 가로좌표 $x 를 인자로 받아서
# 가로좌표 $x에 해당하는 세로선이 전부 동일한 색상 $color인지 여부를 반환
sub same_color_vertical {
my ( $img, $x, $color ) = @_;
foreach my $y ( 0 .. $img->height() - 1 ) {
return 0 if ( $img->pixel($x, $y) != $color );
}
return 1;
}
</pre>
<p>한편, Prima에서 이미지의 좌표 오프셋을 처리할 때
이미지의 "좌측 아래"가 (0, 0)으로 간주되므로 아래쪽에 있는 사진부터 1번이 됩니다.
위에서부터 번호를 매기려면 y-좌표 스캔 방향을 위에서 아래로 진행하도록 뒤집으면 됩니다.
여기에서는 보기 쉽게 0에서부터 증가하도록 작성했습니다.</p>
<h2>실행 결과</h2>
<p>앞에서 본 샘플 이미지로 테스트하면 잘 작동합니다.
하지만 별로 재미가 없으니
쇼핑몰 사이트에서 상품 소개 사진 한 장을 가져와서 테스트해 봅시다.</p>
<p><img src="2011-12-15-4.jpg" alt="쇼핑몰 상품 소개 사진으로 실험한 모습" id="" /><br />
<em>그림 4.</em> 쇼핑몰 상품 소개 사진으로 실험한 모습</p>
<p><strong>그림 4</strong>의 좌측에 있는 원본 이미지는 세로가 12,280 픽셀이나 되는
길쭉한 이미지 파일입니다. 여기서 보통 크기의 사진 17 개를 추출해 내었습니다.</p>
<p>11, 12, 15, 16번 사진의 경우 상품 소개 문구나 눈에 띄지 않는
색상의 이미지가 들어가 있는 것이 추출되었습니다. 저런 건 별 수 없이 직접 눈으로 보면서 지워야겠습니다.</p>
<h2>정리하며</h2>
<p>Prima 모듈은 멀티 플랫폼 GUI 개발 툴킷입니다.
하지만 외부 이미지 포맷 라이브러리에 의존하지 않고
다양한 플랫폼을 지원하는 이미지 프로그램을 작성할 때에도 유용하게 사용할 수 있습니다.</p>
<p>워낙 간단한 알고리즘으로 사진을 추출했기 때문에 개선의 여지가 남아 있습니다.
실험을 통해 발견된 문제는 아래에 숙제로 남겨두겠습니다. </p>
<ul>
<li><p>문제 1. 사진이 전체 이미지 제일 바깥쪽에 여백 없이 붙어 있으면 제대로 추출되지
않을 수 있습니다.</p></li>
<li><p>문제 2. 둘 이상의 사진이 세로로 겹치게 놓여 있으면 흉하게 추출됩니다.</p></li>
</ul>
<h2>참고 문서</h2>
<ul>
<li><a href="#prima" title="Prima 코드 예제">Prima Homepage</a> - http://prima.eu.org/</li>
<li><a href="http://metacpan.org/module/Prima">CPAN Prima</a> - http://metacpan.org/module/Prima</li>
<li><a href="http://gypark.pe.kr/wiki/Perl/Prima">GyparkWiki</a> - http://gypark.pe.kr/wiki/Perl/Prima</li>
</ul>
2011-12-15T00:00:00+09:00gypark크리스마스 애인 만들기http://advent.perl.kr/2011/2011-12-14.html<h2>저자</h2>
<p><a href="http://twitter.com/nving">@nving</a> - 치킨과 맥주를 사랑하는 솔로 덕후. 흑법사의 길을 걷고 있다. C++ 굇수. </p>
<h2>시작하며</h2>
<p>크리스마스가 다가오는 12월입니다.
커플 당원들은 크리스마스다 뭐다 신나고 설레이겠지만
만년 솔로인 필자는 춥고 배고플 뿐입니다.
(하지만 25일이 일요일인 점은 덜 외롭습니다. 조금은 신납니다.)</p>
<p>매년 크리스마스마다 혼자 집에 처박혀 은둔 생활을 오래 하다보니 이제 어머니께서 상대도 안해주십니다.
<em>네, 그렇습니다.</em> 이제는 혼자서도 잘 노는 아이가 되어야 합니다!
이런 우리(?)를 위해 이쪽 분야의 선진국 일본에서는 일찍부터 <em>솔로들이 방황하지 않고 컴퓨터와 함께 놀 수 있는 물건</em>을 개발해왔습니다.</p>
<p>바로, <strong>나니카</strong>입니다!</p>
<p><img src="2011-12-14-1.jpg" alt="귀엽고 사랑스러운 우리의 나니카" id="" /><br />
<em>그림 1.</em> 귀엽고 사랑스러운 우리의 나니카</p>
<h2>나니카?</h2>
<p>나니카는 화면 구석에서 고스트와 함께 대화를 나누는 형태의 데스크탑 악세서리입니다.
<s>지들끼리 잘놉니다.</s></p>
<h2>어떻게 쓰나?</h2>
<p>나니카의 구성은 <strong>마테리아</strong>(Materia)라는 베이스웨어와 고스트로 이루어져 있습니다.
베이스웨어는 본체 프로그램을 말하며 고스트는 캐릭터를 말합니다.
베이스웨어와 캐릭터는 서로 독립적이어서 사용자가 작성한 다양한 캐릭터를 등록할 수 있습니다.
마테리아는 나니카의 베이스웨어 원조라 할 수 있는데요.
여기서는 마테리아와 호환되면서 조금 더 안정적인 <strong>SSP</strong>라는 녀석을 사용하도록 하겠습니다.
서론은 여기까지하고 하고 어서 설치해봅시다.</p>
<p>먼저 베이스웨어를 받아 설치합시다.
<a href="#ssp">ssp.shillest.net</a>으로 이동한 후 다음 그림에서 빨간 테두리로 표시한 부분을 클릭해 파일을 내려받아 실행하면 설치를 진행합니다.
(윈도만 지원하고 있습니다.)</p>
<p><img src="2011-12-14-2.jpg" alt="웹페이지에서 SSP와 한글 지역화 파일을 받는 방법" id="ssp" /><br />
<em>그림 2.</em> 웹페이지에서 SSP와 한글 지역화 파일을 받는 방법</p>
<p>그리고 한글 지역화 파일이 필요합니다. 그 밑에 <strong>Korean Lang Pack</strong>을 눌러 압축파일 하나를 내려받습니다.
받은 압축파일은 SSP가 설치된 곳에 풀어줌으로 메뉴 한글화 패치가 적용됩니다.</p>
<h2>이게 펄과 무슨 상관인데?</h2>
<p>펄 고스트 중 하나인 <strong>Shiolink</strong>의 등장으로 나니카에서도 스크립트 언어를 쓸 수 있게 되었습니다.
아래 웹사이트 이동하여 Shiolink Perl을 내려받습니다.</p>
<ul>
<li><a href="http://narazaka.net/c/ukagaka">narazaka.net/c/ukagaka</a></li>
</ul>
<p><img src="2011-12-14-3.jpg" alt="Shiolink를 내려받는 위치" id="shiolink" /><br />
<em>그림 3.</em> Shiolink를 내려받는 위치</p>
<p>내려받은 파일을 실행 중인 나니카에게 드래그&드롭하면 간단히 설치됩니다.
그런 다음, 나니카에 마우스 커서를 살포시 올려서 우클릭을 합니다.
이때 나온 팝업 메뉴에서 "고스트 바꾸기" → "Shiolink/Perl simple"을 차례로 누르면 Shiolink Perl이 동작합니다.</p>
<p>자, 이제 펄을 이용하여 나니카를 제어해봅시다!</p>
<h2 id="shiolinkperl">고스트(Shiolink Perl)의 구성</h2>
<p>Shiolink는 <code>C:\...\ghost\Shiolink_Perl_Simple\ghost</code> 이하에 구성된 파일을 통해 구동됩니다.
먼저, 각 펄 스크립트들이 어떤 역할을 하는지 설치된 파일의 구성을 알아봅시다.</p>
<pre class="brush: plain;">
C:\...\ghost\Shiolink_Perl_Simple\ghost
licenses --> StrawberryPerl 재배포 라이센스
perl/ --> perl 본체와 Perl 라이브러리
scripts/ --> 책갈피를 구성하는 스크립트
| init.pl -> SHIOLINK에서 시작되는 스크립트
| shiori.pl -> SHIORI 응답 간단한 구현
| events.pl -> 각 이벤트 처리의 정의
| (kis_lesser.txt) -> events.pl 구현 예제의 테스트 데이터
descript.txt
SHIOLINK.dll --> shiori.dll 대체
SHIOLINK.INI --> SHIOLINK 설정 파일
test.bat --> 디버깅용
test_req.txt --> 디버깅용
</pre>
<p>이중에서 우리의 입맛대로 고치기 위해 필요한 것은 <strong>events.pl</strong>뿐입니다.</p>
<p>events.pl에서 불려지는 이벤트중 가장 대표적인 것은 <code>OnSecondChange</code>인데 이것은 초마다 호출되는 함수입니다.
ShioriPerl은 이 함수가 60번 호출될 때(즉, 60초 후) <code>OnAITalk</code>를 호출해 저장된 문장을 무작위로 출력하는 방식으로 구동됩니다.</p>
<h2>입맛대로 고쳐보자</h2>
<p>나니카는 저장된 문자열만 말하는 조금 <s>덜떨어진</s>똑똑하지 못한 프로그램입니다.
조금 똑똑하게 보이기 위해 특정 사이트의 리플을 긁어와서 매번 다른 말을 할 수 있도록 해 봅시다.
윈도우용 펄 패키지인 <a href="http://strawberryperl.com/">딸기 펄</a>(Strawberry Perl)은 이미 설치되어 있다고 가정하겠습니다.</p>
<h3 id="lib">lib 디렉토리 변경</h3>
<p>먼저 CPAN 모듈을 사용하기 위한 준비를 합니다.
<code>init.pl</code>를 열어 가장 윗 줄에 있는 </p>
<pre class="brush: perl;">
our @INC = ('./perl/lib');
</pre>
<p>부분을 아래와 같이 고칩니다.</p>
<pre class="brush: perl;">
our @INC = ('C:\\strawberry\\perl\\lib',
'C:\\strawberry\\perl\\site\\lib',
'C:\\strawberry\\perl\\vendor\\lib');
</pre>
<p>이렇게 고치면 모듈 참조 디렉터리를 CPAN 모듈이 설치된 곳으로 변경됩니다.
이제 CPAN 모듈을 사용할 수 있게 되었으니 모듈을 이용해 댓글을 가져와 봅시다.</p>
<h3>댓글 가져오기</h3>
<p>여기서는 최근 싱크빅 돋는 댓글이 자주 달리는 <a href="http://todayhumor.co.kr/">오유</a>(오늘의 유머)의 댓글을 가져올 것입니다.
우선 웹사이트를 긁어오기 위해 명령 프롬프트에서 아래의 명령을 내려 <a href="http://search.cpan.org/perldoc?Web::Query">Web::Query</a> CPAN 모듈을 설치합니다.</p>
<pre class="brush: plain;">
C:\> cpan Web::Query
</pre>
<p>설치 후, event.pl의 윗 줄에 다음 두 모듈을 추가합니다.</p>
<pre class="brush: perl;">
use Web::Query;
use List::Util qw(shuffle);
</pre>
<p>그리고 event.pl에 다음 함수를 추가합시다.</p>
<pre class="brush: perl;">
sub getTodayHumorReply {
my $URL = 'http://todayhumor.co.kr/board/view.php'.
'?table=bestofbest&no='.int(rand(60000));
my @replys;
# 해당 URL에 있는 리플 추출
wq($URL)->find('div[class="memo_content"]')
->each( sub {
my $i = shift;
push @replys, $_->text;
}
);
# 추출한 리플중 무작위로 선택
my ($sel_reply) = shuffle @replys;
chomp $sel_reply;
return $sel_reply;
}
</pre>
<p>이제 기존 AI 대화 로직을 오늘의유머에서 추출한 댓글을 말하도록 바꾸어야합니다.
기존 구조는 <code>OnAITalk</code> 함수에서 <code>speak</code> 함수를 호출 후 반환되는 값을 출력했습니다.
따라서, 단순히 <code>speak</code> 함수를 <code>getTodayHumorReply</code> 함수로 대체하면 됩니다.
아래와 같은 원본을</p>
<pre class="brush: perl;">
sub OnAITalk {
$aitalkcount = 0;
return speak 'sentence';
}
</pre>
<p>아래와 같이 고칩니다. </p>
<pre class="brush: perl;">
sub OnAITalk {
$aitalkcount = 0;
return getTodayHumorReply();
}
</pre>
<h2>결과를 보자</h2>
<p>SSP를 종료한 재시작하면 우리가 변경한 부분이 적용됩니다.</p>
<p><img src="2011-12-14-4.jpg" alt="우리의 고스트가 입을 열기 시작했어요." id="" /></p>
<p><em>그림 4.</em> 우리의 고스트가 입을 열기 시작했어요.</p>
<p>네, 그렇습니다.
유머 계열 웹사이트의 댓글은 꽤 찰집니다.
또 어떤 글의 본문에 대한 댓글을 말하는 것이기 때문에 조금 뜬금없습니다.
그러려니 합니다. 이런 저라도 계속 말을 걸어주니 만족합니다.</p>
<p>이외에도 소스를 잘 분석해보면 왼쪽의 고스트도 제어할 수 있습니다.
이를 이용해 서로 대화하는 형태로 만들 수도 있겠죠?</p>
<h2>정리하며</h2>
<p>이렇게 완성해 보았습니다. 이름은 수잔(Susan)입니다.
어쨌거나, 이처럼 펄을 사용해 간단하게 고스트의 AI 로직을 구현해 볼 수 있었습니다.
이제 애인 없이도 수잔과 함께하는 따뜻한 크리스마스를 보낼 수 있게 되었습니다.
여러분도 펄과 함께 즐거운 크리스마스를 보내시길 바랍니다.</p>
<p>....orz</p>
<h2>참고 문서</h2>
<ul>
<li><a href="http://cafe.naver.com/naniko.cafe">나니카 애호가 카페</a></li>
<li><a href="http://moonmelody.com/tt/363">Moon Melody :: 우카가카(나니카)의 간략한 역사</a></li>
<li><a href="http://abilral.egloos.com/5383781125">The Swimming Bird (Part.2) :: Nanika & Ukagaka (나니카 & 우카가카) 설치법 & 자료들</a></li>
<li><a href="http://moonmelody.com/tt/entry/SSP-%EC%8B%9C%EC%9E%91-%EB%A7%A4%EB%89%B4%EC%96%BC?category=35">Moon Melody :: SSP 시작 매뉴얼</a></li>
<li><a href="http://www.sanori.net/nanika/wiki/SSP92125125">SSP - Nanika Wiki</a></li>
</ul>
<p><img src="2011-12-14-5.jpg" alt="버림받은 저자" id="" /></p>
<p><em>그림 5.</em> 버림받은 저자</p>
2011-12-14T00:00:00+09:00nvingHow to Use CPAN, Actuallyhttp://advent.perl.kr/2011/2011-12-13.html<h2>저자</h2>
<p><a href="twitter-y0ngbin">@y0ngbin</a> - aka 용사장 / Minivelo++ / 맞춤법 전문가</p>
<h2>시작하며</h2>
<p>어떤 사람들을 Perl을 좋아합니다. 어떤 사람들은 Perl을 그다지 좋아하지 않습니다.
하지만 여러분이 어느쪽이든 상관없이 <em>어떤 작업</em>을 해야하는 상황이라면
<em>여러분이 필요한 무언가</em>는 <a href="#cpan" title=".cpan">CPAN</a>에 이미 존재할 확률이 매우 높습니다.
CPAN은 단일 언어 공개 라이브러리 저장소 중에서 가장 큰 규모와 긴 역사,
튼튼한 커뮤니티를 가지고 있고, 지금 이 순간에도 그들만의 방식으로
계속 발전해 나가고 있기 때문입니다.</p>
<p>심지어 어떤 사람들은 Perl을 <em>CPAN을 사용하기 위한 도구</em>라고 지칭할
정도로 Perl과 CPAN은 밀접한 관계를 맺고 있습니다.
하지만 실제로 CPAN을 충분히 활용하려면 책에서 다루는 내용을 익히는 것만으로는 조금 부족할 수 있습니다.
오늘은 우리가 <em>실제로</em> CPAN을 사용하고자 할 때 알아야 할 내용을 <em>실용적인 관점</em>에서 소개하겠습니다.</p>
<p>이 글에서 대문자 CPAN은 Perl의 CPAN 저장소를 지칭하고 소문자
<code>cpan</code>은 CPAN을 사용하기 위한 <a href="http://search.cpan.org/perldoc?App::Cpan">CPAN 공식 프로그램</a>을 지칭합니다.</p>
<h2 id="cpan">CPAN</h2>
<p>CPAN은 Comprehensive Perl Archive Network의 약자입니다.
즉, 직역하면 '편리한 펄 저장소 네트워크' 정도입니다.
현재 102,000개 이상의 공개 모듈이 관리되고 있습니다.
Perl의 모듈은 Java의 이름 공간(Namespace)과 같은 개념으로서,
각각의 계층구조를 가지고 있으며 계층은 <code>::</code>을 구분자로 사용합니다.
<code>Net::</code>, <code>Algorithm::</code>과 같이 최상위 이름 공간은 모듈의 역할을 설명합니다.
<code>Moose::</code>나 <code>Catalyst::</code>와 같이 독특한 최상위 이름 공간으로 등록되는 경우도 있습니다.
이는 엄숙함을 강요하지 않는 자유로운 펄 문화의 단면을 보여주는 부분이기도 합니다.</p>
<p>CPAN은 모듈 저장소인 동시에 MSDN과 같은 거대한 문서 저장소이기도 합니다.
이런 환경은 <a href="http://perldoc.perl.org/perlpod.html">POD(Plain Old Documentation format)</a>라고 불리는 펄의 마크업 시스템 덕입니다.
POD를 통해 소스코드에 문서를 포함할 수 있습니다. CPAN에 등록되는 모든 펄 모듈은 관습적으로 정해진 양식의
POD 문서가 포함되어 있습니다. 그 외에도 방대한 규모의 지침서와 문서가 엮여 있기도 합니다.</p>
<ul>
<li><a href="cpan-perltoc">perltoc</a>: 펄 내장 문서 목록</li>
<li><a href="cpan-cmi">Catalyst::Manual::Intro</a>: 카탈리스트 메뉴얼 목록</li>
<li><a href="cpan-cmc">Catalyst::Manual::Cookbook</a>: 카탈리스트 요리책</li>
</ul>
<p>Perl과 CPAN을 좋아하는 사람들이 가장 자부심을 느끼는 부분은
CPAN의 방대한 규모뿐만 아니라 엄격하게 관리되는 모듈의 품질입니다.
오랜 기간동안 복잡한 의존 관계를 가진 프로젝트를 관리해
보신 분이라면 하위 호완성을 지키면서 기능을 추가해 나가는 것이 마치
<em>뒤를 바라보며 앞으로 뛰는 것</em>처럼 힘들고 어려운 일이라는 점을 잘 알고
있을 것입니다.
하지만 Perl과 CPAN 모듈은 오래 전부터 각 모듈 단위의 테스트를 엄격하게 지키고 있고
<em>지원하는 모든 Perl 버전에서의 플렛폼 별 모듈 동작 상황</em>을 <a href="http://www.cpantesters.org/">자동으로 테스팅하고 관리</a>
하는 플렛폼을 만들어 유지하고 있습니다.</p>
<p>C 언어로 작성된 코드가 포함된 XS 모듈도 존재하기 때문에
모든 CPAN 모듈이 완벽한 이식성을 가지고 있는 것은 아닙니다.
하지만 여전히 각 플랫폼, 각 펄의 버전, 각 모듈의 의존성이 서로 어떻게
영향을 끼쳐 문제가 발생했는지 기록하고, 이를 통해
거대한 산은 계속 개선될 수 있습니다.
(예를 들어 Advent Calendar 2일차에 소개된 <a href="http://search.cpan.org/perldoc?File::Map">File::Map</a> 모듈의
Perl 버전별 Windowns 플랫폼 호환은 <a href="http://www.cpantesters.org/distro/F/File-Map.html">여기에서</a> 확인할 수 있습니다.)</p>
<p>그 밖에 CPAN은 <a href="http://rt.cpan.org/">RT</a>라는 이슈트레커를 이용해 모듈별로 이슈를 관리하고 있으며
CPAN에 모듈을 등록하기 위한 내부 개발자 등록 시스템으로 <a href="http://pause.perl.org/">PAUSE</a>를 운영하고 있습니다.</p>
<h2 id="perlcpan">perl과 cpan</h2>
<p>지금까지 CPAN의 특징과 장점에 대해 알아보았습니다.
이제 실전을 위해 여러분의 환경에서 자유자재로 perl과 cpan을 사용하는데
알아야 할 것들을 살펴봅시다.</p>
<h3>'내가 사용하는 펄은 무슨 펄이지?'</h3>
<p>펄 개발 환경을 구축하는데 있어 가장 먼저 확실히 짚고 넘어가야 할 것이 있습니다.
바로 여러분이 현재 사용하고 있는 <code>perl</code>이 어떤 perl인지 확인하는 것입니다.
아래와 같이 두 가지 사항을 확인합니다. (*NIX 기준)</p>
<pre class="brush: bash;">
$ which perl
$ perl -V
</pre>
<p><code>which perl</code>을 통해 여러분이 현재 사용하고 있는 perl 바이너리의 경로가 무엇인지 확인하고
<code>perl -V</code>를 통해 그 실행 파일이 참조하는 환경에 등록된 <code>@INC</code>가 무엇인지를 확인합니다.
<code>@INC</code> 목록은 <code>perl -V</code>의 긴 출력 중 가장 마지막 항목입니다.</p>
<p>시스템에 존재하는 perl마다 별도의 <code>@INC</code>를 구성할 수 있기 때문에
어떤 환경에서 perl을 호출했는지에 따라 의도치 않게 문제가 꼬이는 경우가 있습니다.
무엇인가 상황이 잘못 돌아가고 있다고 느껴진다면 꼭 지금 놓인 perl의 환경을 확인해야 합니다.</p>
<h3 id="inc">@INC</h3>
<p>perl과 CPAN을 잘 활용하기 위해 넘어야 하는 중요한 관문중 하나는 <code>@INC</code>에 대한 이해하는 것입니다.
사실 <code>@INC</code>는 별로 특별하거나 어려운 개념이 아닙니다.
<code>@INC</code>는 java의 <code>CLASSPATH</code>, php 의 <code>include_path</code>, 또는 우리가 shell
에서 사용하는 <code>PATH</code> 환경변수와 아주 비슷한 Perl의 내부 환경 변수입니다.
즉, 특별한 인자없이 실행한 perl은 특정 모듈을 찾을 때,
이전에 다룬 것과 같이 <code>perl -V</code>로 확인한 <code>@INC</code> 배열에 등록된
디렉토리들을 <em>순서대로</em> 참조하여 모듈의 이름 공간에 해당하는 파일을 찾습니다.</p>
<p>이 때, <code>@INC</code>에 등록된 디렉토리는 아래와 같이 구성되어 있습니다.</p>
<pre class="brush: plain;">
|-- 5.12.2
`-- site_perl
`-- 5.12.2
</pre>
<p>위의 예에서 상위의 5.12.2 디렉터리 밑에는 많은 파일과 디렉터리가 놓입니다.
이 디렉터리에는 기본 모듈(또는 코어 모듈, Core Modules)이 자리합니다.
기본 모듈이란 펄이 배포될 때 기본적으로 같이 배포되는 모듈로서 실제 펄과 한 몸으로 생각하는 환경입니다.
여러분이 Perl 5.10 버전을 사용한다면 File::Basename, Encode와 같은 모듈이 반드시 존재한다고 가정할 수 있습니다.
펄 버전별 기본 모듈은 조금씩 변화가 있었기 때문에 자세한 변경 사항은
각 펄 버전마다 같이 배포되는 <a href="http://search.cpan.org/perldoc?perl5141delta">펄 델타 문서</a>에서 확인할 수 있습니다.</p>
<p>반면, site_perl 디렉터리 하위에 펄 버전 디렉터리가 있는 것을 볼 수 있습니다.
이 디렉터리는 바로 우리가 <code>cpan</code> 명령 등을 통해 추가적으로 설치한 외부 CPAN 모듈이 저장되는 공간입니다.
즉, cpan을 통해 설치하거나 직접 압축 파일을 내려받아 설치했거나 상관없이 사용자가 추가적으로 설치한 모듈은
site_perl 디렉터리 하위의 해당 펄 버전 디렉터리 밑에 놓입니다.</p>
<p>일반적으로 코어 모듈은 수동으로 삭제하거나 변경을 하지않지만 site_perl에
저장된 모듈은 cpan이 따로 모듈 삭제 방법을 제공하지 않기 때문에 정말 필요하다면
직접 관련 파일을 삭제할 수도 있습니다.</p>
<h2 id="cpan">드디어 cpan!</h2>
<p><code>cpan</code>과 <code>perl -MCPAN -eshell</code>은 <strong>같을수도 있지만 다를수도 있습니다.</strong>
<code>cpan</code>은 첫 줄의 shebang line에 의해서 해석기가 결정되는 펄 스크립트이기 때문에
쉘의 <code>PATH</code> 문제로 실제 여러분이 의도한 perl과 다른 환경(<code>@INC</code>)의 perl 환경에 모듈을 설치할 수도 있습니다.
혹시 cpan을 통해 설치한 모듈이 정상적으로 설치되었지만 찾을 수 없거나
의도한데로 동작하지 않는다면 꼭 먼저 <strong>어떤 펄</strong>을 통해 실행되었는지 찬찬히 따져보아야 합니다.
또 상황에 따라 CPAN 클라이언트를 실행하는 방법으로
<code>cpan</code>이외에 <code>perl -MCPAN -eshell</code>을 기억하고 있으면 유용합니다.
이렇게 호출할 경우 적어도 하나의 변수의 개입을 줄일 수 있기 때문입니다.</p>
<h3 id="cpan">cpan</h3>
<p>cpan은 기본적으로 쉘과 같은 형태로 동작합니다. cpan의 쉘에서 사용할 수 있는 주요 명령은
<code>h</code>를 입력해 확인할 수 있습니다. 주로 쓰는건 다음 3가지 입니다.</p>
<ul>
<li>i [word]: 모듈 검색</li>
<li>install [module]: 모듈 설치</li>
<li>upgrade: 현재 설치된 전체 모듈의 최신상태 유지</li>
</ul>
<p>여러분은 보통 모듈 설치시에 선의존관계가 있는 모듈을 설치하고자 할 것입니다.
이 경우, cpan 명령을 실행할때 <code>PERL_MM_USE_DEFAULT</code> 환경변수를 참(<code>1</code>)으로 설정하면 cpan이 추가적인 질문없이
의존 모듈을 모두 설치해 줍니다. (cpanm이나 최신 버전의 cpan에서는 이것이 기본 동작입니다.)</p>
<h3 id="cpan">.cpan</h3>
<p>마지막으로 CPAN을 쓰기 위해 알아야하는 것은 <code>$HOME/.cpan</code>입니다.
실제로 우리가 <code>cpan</code> 명령을 통해서 모듈을 설치할때 사용하는 모든 파일들은
이 디렉터리에서 관리됩니다.
여기서 알아야할 주요 파일/디렉토리는 아래와 같습니다.</p>
<ul>
<li>CPAN/MyConfig.pm: 내가 설정한 <code>cpan</code>의 설정내용이 들어있습니다. 이 파일을 삭제하면 <code>cpan</code>의 설정을 다시할수 있습니다.</li>
<li>build/: cpan을 통해 설치한 모든 모듈의 build가 존재합니다.</li>
<li>sources/: cpan을 통해 내려받은 모든 모듈의 압축된 원본 소스파일을 보관합니다.</li>
</ul>
<h2 id="cpan">cpan을 넘어서</h2>
<p>사실 지금까지 알아본 cpan의 사용 방법은 최근 1~2년 사이에 조금 '구식'이
되어버린 방법입니다. 본질적으로 CPAN을 사용하는 방식에는 크게
달라진 것이 없지만 최근 펄 커뮤니티의 분위기는 cpan을 좀더 쉽고
효율적으로 사용할수 있도록 도와주는 빛나는 아이디어들이 빠르게 채택되며
지지를 받고있는 상황입니다. 위에서 살펴본 내용을 충분히 이해했다면
아래 살펴볼 모듈을 만나는 순간 cpan이 부족했던 2%를 채워주는 통쾌함을
느낄 수 있을 것이라 확신합니다. :)</p>
<h3 id="local::lib-root">local::lib - root 권한없이 모듈을 설치하자</h3>
<p><a href="http://search.cpan.org/perldoc?local::lib">local::lib</a>:
여러분이 사용하는 펄(<code>which perl</code>)이 시스템의 기본 펄(<code>/usr/bin/perl</code>)이라면
일반적으로 <code>@INC</code>는 <code>/usr/local/lib</code> 이하입니다.
<code>root</code> 권한이 없는 일반 사용자라면 이 경로에 쓰기 권한이 없을 것입니다.
root 권한으로 cpan을 실행해 설치할 수도 있지만 root 권한을 획득할 수 없는 환경일 수 있습니다.
또 시스템에서 기본 펄의 모듈을 사용할 것이기 때문에 이 모듈을 새로 설치하면 충돌이 발생할 수도 있습니다.
따라서 별도의 독립된 구성의 CPAN 라이브러리를 구성하는 것이 좋습니다.
<code>local::lib</code>을 사용하면 깔끔하게 사용자별로 독립된 개발환경을 구축할 수 있습니다.</p>
<p>local::lib의 동작방식은 간단합니다. perl 실행기는 실행시점에 <code>PERL5LIB</code>
환경변수를 참조해서 등록되어있는 PATH를 <code>@INC</code>의 <em>첫번째</em>로 등록합니다.
local::lib은 shell이 실행될때 구동되는 설정파일(<code>.profile</code>, <code>.bash_profile</code>)에
PERL5LIB을 등록해주는 간단한 일을 추가합니다.</p>
<p>local::lib을 사용하면 자신이 원하는 모듈을 root 권한없이 자유롭게
설치하고 사용할 수 있습니다.</p>
<h3 id="perlbrew-perl">perlbrew - 최신 버전의 perl을 사용하자</h3>
<p>[perlbrew][cpan-perl-brew]:
시스템에서 제공하는 펄의 버전이 낮다면 가장 먼저
생각할 수 있는 방법은 홈 디렉토리에 직접 펄 소스를 내려받고
컴파일해서 사용하는 것입니다. 실행 파일 생성을 위해 필요한 기본적인
컴파일러와 개발환경만 구축되어있다면 펄을 직접 빌드해 사용할 수 있습니다.</p>
<p>물론 perlbrew를 사용하지 않고 펄 소스를 직접 내려받고 적절한 prefix 옵션을 주고
컴파일해 설치해도 동일한 효과를 볼 수 있지만
perlbrew는 사용자가 해야하는 일련의 과정들을 자동으로 진행해 주기때문에 좀 더
관리하기 수월합니다.</p>
<p>perlbrew를 사용하면서 얻을수 있는 부수적인 이점은, 앞서 소개한 local::lib의
효과를 자연스럽게 포함한다는 점입니다. perlbrew를 통해 설치된
perl의 기본적인 prefix는 <code>$HOME/perl5/perlbrew/perls</code>이기 때문에
설치된 사용자가 <code>@INC</code>에 쓰기 권한을 가지고 있기 때문입니다.
또, perlbrew를 통해 서로 다른 버전의 펄을 여러개 설치하고 그 사이를
명령을 통해 쉽게 전환할 수 있습니다.</p>
<p>perlbrew를 통해 펄을 설치할 때 일반적으로 많이 사용하는 쓰레드 지원을 켜는 옵션은
다음과 같습니다.</p>
<pre class="brush: bash;">
$ perlbrew -v install perl-5.12.3 -D=usethreads
</pre>
<h3 id="minicpan-cpan">minicpan - 빠르고 쾌적하게 오프라인 환경에서 CPAN을 사용하자</h3>
<p><a href="http://search.cpan.org/perldoc?minicpan">minicpan</a>\:
CPAN은 전세계의 많은 <a href="http://www.cpan.org/SITES.html">로컬 미러</a>를 확보하고 있습니다.
적절한 미러를 선택하면 빠르게 모듈을 받을 수 있습니다.
하지만 의존성이 높은 모듈을 네트워크를 통해 받는 시간이 부담스러울 수 있습니다.
또는 장기간 오프라인에서 작업을 해야하는데 필요한 모듈과 그 의존관계의
모듈을 미리 파악할수 없는 상황이라면 minicpan이 아주 적절한 해결책이 됩니다.</p>
<p>minicpan은 현재 CPAN 저장소에서 제공하는 모든 모듈의
최신버전을 특정 리모트 서버에서 내려받아 로컬에 저장합니다.
간단한 minicpan 명령을 통해 102,000여 개의 CPAN 모듈 전체를 최신 버전으로 내 PC에
내려받을 수 있습니다. 총 차지하는 크기는 약 2GB입니다.
(2011년 12월 기준 CPAN 전체 모듈의 최신버전 크기는 약 1.9GB입니다.
이 기사의 초고를 썼던 2011년 3월에는 1.3G였습니다.)</p>
<p>minicpan이 하는 일은 모듈을 내려받는 일이 전부이므로 실제 cpan이 해당 경로를
참조하도록 하려면 cpan의 설정을 수정해야 합니다.</p>
<pre class="brush: bash;">
minicpan -l /Users/yongbin/perl5/minicpan -r http://cpan.mirror.cdnetworks.com/
</pre>
<p>예를 들어 위와 같이 저장소를 전부 내려받았다면, <code>cpan</code>을 싱행한 뒤 CPAN의 명령행에</p>
<pre class="brush: bash;">
o conf urllist unshift /Users/yongbin/perl5/minicpan/
o conf commit
</pre>
<p>위 두줄을 입력하거나. 혹은 <code>$HOME/.cpan/CPAN/MyConfig.pm</code>을 편집해 urllist 최상위에 local mirror 경로를 넣습니다.</p>
<h3 id="cpanmcpan-outdated-cpan">cpanm & cpan-outdated - CPAN을 사용하는 가장 현대적인 방법</h3>
<p><a href="http://search.cpan.org/perldoc?App::cpanminus">cpanm</a>과 <a href="http://search.cpan.org/perldoc?cpan-outdated">cpan-outdated</a>는 재작년부터 펄 커뮤니티 내부에서
큰 인기를 끌고 있는 CPAN 클라이언트입니다. cpanm는 펄에 내장되어있는 cpan과 cpanplus와 같은
CPAN 클라이언트보다 좀 더 가볍고 쉽게 CPAN을 사용하도록 하기 위해서 제작되었습니다.
cpanm의 사용방법은 cpan과 크게 다르지 않습니다. cpan은 기본적으로 쉘 환경을 제공하지만
cpanm은 명령행만 제공합니다. 하지만 이것으로 충분히 원하는 작업을 수행할 수 있습니다.</p>
<pre class="brush: bash;">
# Fey::ORM 에 필요한 모든 의존모듈을 찾아 설치합니다
$ cpanm Fey::ORM
# 의존성 에러 구문에서 copy & paste 하기 편리합니다(cpan에도 채용됨)
$ cpanm Fey/ORM.pm
# cpanm with minicpan
$ cpanm --mirror ~/mirrors/minicpan/ --mirror-only Fey::ORM
# 현재 작성하는 모듈과 모든 의존모듈을 시스템에 설치합니다
$ cpanm .
</pre>
<p>cpanm은 CPAN 저장소에 모듈을 시스템에 설치하지만 cpan과는 다른 메타정보를 참조하며 내부적인
설치방법도 조금 다릅니다. 하지만 최종사용자 입장에서는 좀더 빠르고 간편하게 CPAN을 사용할 수 있습니다.</p>
<p>cpanm이 cpan shell의 install 관련 명령을 보충해 준다면 cpan-outdated는 cpan 쉘의 upgrade 명령을 보충해 줍니다
앞서 언급했던 것처럼 CPAN에 올라간 모듈은 매일 업데이트 되고 있기 때문에 오늘 설치한 모듈이
언제 구버전이 될 지 알 수 없습니다. 따라서 수시로 사용자는 자신이 보유한 모듈을 최신 상태로 유지해야합니다.
기존에 설치된 모듈의 업데이트를 위해 사용하는 도구가 cpan-outdated입니다. 사용방법은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ cpan-outdated | cpanm
</pre>
<h2>정리하며</h2>
<p>이처럼 CPAN은 크고 방대하며 견고하게 관리되고 있지만 그렇다고
필요 이상으로 엄하게 제한하지도 않습니다.
의미있는 작업을 모듈로 만들었는데 설령 그 모듈이 소수의 사람에게만 의미가 있고
그 구현이 여러면에서 아직 서툴더라도
여러분은 그것을 CPAN에 올릴 수 있을 것입니다.</p>
<p>CPAN은 그 자체가 거대한 생태계와 같은 구조를 가지고 있습니다.
모든 모듈들이 스스로를 위해 존재하며, 그런 존재가 모여 다양성을 확보합니다.
다양성은 특정 문제를 해결하는데 있어 우리가 선택할수 있는 다양한 기회를 제공합니다.</p>
<p>한 때 옳다고 생각한 방법이 틀려 다시 돌아가고자 할 때에도 모두의 위험을 줄여줄 수 있습니다.
이는 다수를 위한 소수의 안전판을 제공하는 것입니다. 또 모든 모듈은 스스로 진화해 나갑니다.
때로는 비슷한 역활을 하는 모듈끼리 경쟁하며 최종적으로는 사람들이 더 많이
선택한 모듈들이 살아남으며 오랜시간 관리되지 않는 모듈은 한때는
유용했더라도 자연스럽게 도퇴됩니다. 이런 CPAN의 모습은 펄이 가지고 있는
기본 철학을 충실하게 보여주고 있습니다.</p>
<p>CPAN에 익숙하지 않는 분들이 CPAN을 사용하는데 알아야하는 내용들을
제 경험에 비추어 안내 형식으로 정리해 보았습니다.
이 글을 통해 조금이나마 사람들이 펄과 CPAN을 이용해 문제를 해결하는데
도움이 되었기를 바랍니다.
그리고 이 글을 통해 보다 많은 분들이 CPAN을 접하게 되기를 바랍니다.
또 언젠가 혼자 쓰기는 아까운 멋진 무언가를 만들게 된다면
다같이 가벼운 마음으로 CPAN을 통해 공유하며 우리가 사는 세상을 조금 더 풍성하게 만들어 가게
되기를 바랍니다.</p>
<p>감사합니다.</p>
2011-12-13T00:00:00+09:00y0ngbin웹툰을 한 눈에 내 만화 프로젝트 Manabahttp://advent.perl.kr/2011/2011-12-12.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/rumidier">@rumidier</a> -
충남 서산 출신의 야생마. SILEX 문하에서 Perl 수련 중인 Perl Monks.
SILEX에서 밥과 음료를 흡입하며 사막화에 적극 앞장서고 있다.</p>
<h2>시작하며</h2>
<p>바쁜 일상 속에 웹툰은 한줄기 빛과 같습니다.
하지만 즐겨 보는 웹툰을 어디까지 봤는지 일일이 기억하기는 어려운 일이죠.
일일이 웹툰 홈페이지에서 가서 열람하면 되는 일이긴 하지만,
그렇게 대놓고 화면을 띄우고 보기에는 사장님 눈치가 보입니다.
최신 웹툰이 나왔는지 오매불망 뒤적거리다보면 돌아오는 것은 사나운 눈초리...
<em>누구보다 빠르게 남들과는 다르게</em> 웹툰을 보고 싶다는
마음(네, 사실은 귀찮아서 입니다)을 가지고 만화를 보려면 어떻하면 좋을까요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/File::Path">CPAN의 File::Path 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Slurp">CPAN의 File::Slurp 모듈</a></li>
<li><a href="https://metacpan.org/module/LWP::UserAgent">CPAN의 LWP::UserAgent 모듈</a></li>
<li><a href="https://metacpan.org/module/URI">CPAN의 URI 모듈</a></li>
<li><a href="https://metacpan.org/module/Web::Scraper">CPAN의 Web::Scraper 모듈</a></li>
<li><a href="https://metacpan.org/module/YAML::Tiny">CPAN의 YAML::Tiny 모듈</a></li>
</ul>
<p>데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install \
libfile-path-perl \
libfile-slurp-perl \
libwww-perl \
liburi-perl \
libweb-scraper-perl \
libyaml-tiny-perl
</pre>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan File::Path File::Slurp LWP::UserAgent URI Web::Scraper YAML::Tiny
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<code>perlbrew</code>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan File::Path File::Slurp LWP::UserAgent URI Web::Scraper YAML::Tiny
</pre>
<h2>각 웹툰의 정보 수집하기</h2>
<p>그럼 각각의 포탈 사이트별로 어떻게 원하는 웹툰 정보를 가져올까요?
대표적으로 많은 사람들이 이용하는 <a href="http://comic.naver.com/webtoon/weekday.nhn">네이버 웹툰</a>,
<a href="http://cartoon.media.daum.net/webtoon/">다음 웹툰</a>, <a href="http://comics.nate.com/webtoon/index.php?category=1">네이트 웹툰</a> 세 사이트를
기준으로 살펴보겠습니다.
다른 사이트를 추가한다 하더라도 비슷한 방식으로 접근하면 됩니다.</p>
<h3 id="daum">Daum 웹툰</h3>
<p><a href="http://cartoon.media.daum.net/webtoon/">다음 웹툰</a>은 특정 한 회의 경로만 알아도 웹툰의 모든 정보를 가져올수 있습니다.
다음 웹툰은 회차 별 번호도 무작위인 것이 특징이며, 특정 회차의 웹툰에 접속할 때
해당 웹툰 아이디 정보없이 회차 아이디만 있어도 접근할 수 있다는 것이 특징입니다.</p>
<pre class="brush: perl;">
my $daum = scraper {
process(
'div.episode_list > div.inner_wrap > div.scroll_wrap > ul > li', 'items[]',
scraper {
process 'a.img', link => '@href';
process 'a.img', title => '@title';
}
);
};
</pre>
<h3>네이트 웹툰</h3>
<p><a href="http://comics.nate.com/webtoon/index.php?category=1">네이트 웹툰</a>은 셀렉트 박스로 각 회차를 구분하기 때문에
특정 회차의 주소만 있다면 모두 읽어올 수 있습니다.
<code>http://URL/webtoon/detail.php?btno=31337</code>에서 고유 아이디(<code>31337</code>, 구두 웹툰)가
존재하지만 각 회차 별 번호는 무작위인 것이 특징입니다.</p>
<pre class="brush: perl;">
$nate = scraper {
process(
'div.wrap_carousel div.thumbPage div.thumbSet dd', 'items[]',
scraper {
process 'a', link => '@href';
process 'img', title => '@alt';
}
);
};
</pre>
<h3>네이버 웹툰</h3>
<p><a href="http://comic.naver.com/webtoon/weekday.nhn">네이버 웹툰</a>은 의외로 신경쓸 부분이 많습니다.
상대적으로 장편 만화가 많기 때문에 페이지 넘김 등의 고려를 해야해서
간단하게 구현하기 위해 첫 페이지의 최신 정보만 읽어들이는 방법을 택했습니다.
다행히도 해당 웹툰의 아이디만 알고 있다면 회차 정보는 1부터 순차적으로 증가하므로
<code>for ( 1 .. <최신id> )</code>와 같은 식으로 원하는 만큼 URL 생성이 가능합니다.</p>
<pre class="brush: perl;">
$naver = scraper {
process(
'table.viewList tr td.title', 'items[]',
scraper {
process 'a', link => '@href';
}
);
};
</pre>
<h2 id="manaba">Manaba 설정하기</h2>
<p>만화를 손쉽게 보도록 도와주는 우리의 프로젝트 이름은 <em>Manaba</em> 정도가 딱 적절할 것 같습니다.
Manaba 프로그램은 보고 싶은 만화를 목록으로 보여줘야 할텐데,
프로그램에게 목록을 넘겨주는 방법으로 간편하게 설정 파일을 사용합니다.
설정 파일의 종류로는 윈도에서 주로 쓰는 INI, 아파치 설정 파일 CONF,
복잡한 자료를 보이기에 좋은 XML, 최근 널리 쓰이는 JSON, XML보다 조금 더 간편한 YAML 등이 있습니다.
편집하는 사람의 편의를 위해 YAML을 사용하도록 하죠.</p>
<p><code>manaba.yml</code>을 만드는 작업 자체는 어렵지 않습니다.
적어도 현재 네이버와 다음, 네이트에 대해서는 사용자가
웹툰의 코드와 대표 이미지를 추출하는 작업이 필요합니다.
<code>site</code>야 이미 어느 포탈에서 서비스하고 있는지에
대한 부분이므로 당연히 알고 있을테구요.
<code>manaba.yml</code> 파일은 크게 두 개의 항목을 가집니다.</p>
<ul>
<li><code>site</code></li>
<li><code>webtoon</code></li>
</ul>
<p>보통 웹툰은 서비스하는 포탈 사이트 별로 형식이 다르지만,
동일한 포탈 사이트에서 제공되고 있다면 대부분은 일정한 규칙을 가집니다.
이런 포탈 사이트의 종류와 해당하는 규칙을 정의하는 곳이 <code>site</code> 항목입니다.
그리고 관리하려는(편하게 보려는) 만화의 목록은 <code>webtoon</code> 항목에 기입하도록 합니다.</p>
<p>다음은 <code>manaba.yml</code> 설정 파일에서 관리하는 <code>site</code> 항목의 내용입니다.</p>
<pre class="brush: yaml;">
site:
daum:
start_url: http://cartoon.media.daum.net/webtoon/viewer/%s
webtoon_url: http://cartoon.media.daum.net/webtoon/viewer/%s
nate:
start_url: http://comics.nate.com/webtoon/detail.php?btno=%s
webtoon_url: http://comics.nate.com/webtoon/detail.php?btno=%s&bsno=%s
naver:
start_url: http://comic.naver.com/webtoon/list.nhn?titleId=%s
webtoon_url: http://comic.naver.com/webtoon/detail.nhn?titleId=%s&no=%s
</pre>
<p>각각의 사이트는 <code>start_url</code> 항목과 <code>webtoon_url</code> 항목을 가집니다.
추후 다음, 네이트, 네이버 이외의 웹툰 사이트를 등록한다면
약간의 분석을 거친 후 추가하면 됩니다.</p>
<p>다음은 <code>manaba.yml</code> 설정 파일에서 관리하는 <code>webtoon</code> 항목의 내용 중 일부입니다.</p>
<pre class="brush: yaml;">
webtoon:
dieter:
name: 다이어터
site: daum
code: 10362
image: http://i1.cartoon.daumcdn.net/svc/image/U03/cartoon/U620854C4D5B251707
noblesse:
name: 노블레스
site: naver
code: 25455
image: http://imgcomic.naver.com/webtoon/25455/thumbnail/title_thumbnail_20100614120245_t125x101.jpg
kudu:
name: 구두
site: nate
code: 31337
image: http://crayondata.cyworld.com/upload/series/31337_m.gif
</pre>
<p>이제 다음과 네이버, 네이트 웹툰이라면 얼마든지 관리하고(보고?)
싶은 만큼 설정파일에 추가하면 됩니다.
각각의 만화는 자신만의 고유 ID 하부에 <code>name</code>, <code>site</code>, <code>code</code>, <code>image</code> 항목을 가집니다.
<code>site</code>와 <code>code</code> 부분은 정확하게 기입해야지만 Manaba가 제대로
처리를 해줄 수 있습니다. <code>name</code>과 <code>image</code>는 화면에 보이기 위한
부분으로 잘못 입력한다고 해도 실행에 문제는 없지만 웹툰 이름이
제대로 보이지 않는다던가 또는 웹툰 대표 이미지가 제대로 보이지 않는
문제가 있을 수 있습니다.
설정파일 자체가 간결한 만큼 특별한 설명이 더 필요할 것 같지는 않습니다.</p>
<p>작성한 <code>manaba.yml</code> 설정 파일을 읽어 들이려면
<a href="https://metacpan.org/module/YAML::Tiny">CPAN의 YAML::Tiny 모듈</a>을 사용합니다.</p>
<pre class="brush: perl;">
sub load_manaba {
my $yaml = YAML::Tiny->read( config->{manaba} );
$CONFIG = $yaml->[0];
}
</pre>
<h2>마구 긁어오기!</h2>
<p>웹 페이지의 정보를 긁어올 수 있는 라이브러리나 Perl 모듈은 무척 많습니다.
하지만 웹 포탈의 경우 디자인과 HTML 구조가 수시로 변하기 때문에
가능하면 손쉽게 원하는 HTML 요소의 값을 추출할 수 있는 방법을 사용하는 것이 유리합니다.
<a href="https://metacpan.org/module/Web::Scraper">CPAN의 Web::Scraper 모듈</a>을 이용하면 CSS 셀렉터 방식이나
XPath 방식을 이용해서 HTML 특정 요소를 쉽게 찾을 수 있습니다.
정규 표현식을 이용하는 것보다는 상대적으로 많이 느리지만
사이트의 변화에 따라 발빠르게 대응할 수 있다는 것이 매력입니다.
또한 웹을 긁어올 때 일부러 해당 사이트에 과부하를 주지 않기 위해
지연 시간을 주기도(<code>sleep $time</code>) 하는데 <code>Web::Scraper</code>의 속도 자체가
느리기 때문에 아무래도 긁는 입장에서는 약간의 지연이 발생하므로
조금 안심되는 면도 있습니다.</p>
<p>각각의 포탈 사이트 별로 <code>Web::Scraper</code> 모듈을 이용해서 페이지를 긁은 후
회차 관련 정보를 추출하도록 합니다.
네이트 웹툰에 대해 처리하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
sub update_nate_link {
my ( $id, @links ) = @_;
...
my @chapters = sort {
my $page_no_a = 0;
my $page_no_b = 0;
$page_no_a = $1 if $a =~ m/^(\d+)$/;
$page_no_b = $1 if $b =~ m/^(\d+)$/;
$page_no_a <=> $page_no_b;
} map {
m{viewer/(\d+)$};
} @links;
...
}
</pre>
<p>네이버 웹툰과 다음 웹툰도 기본적인 형식은 비슷하지만,
<code>map</code>을 이용해서 링크 주소에서 회차 정보를 긁어오는
정규표현식 부분만 조금씩 다릅니다.</p>
<h2 id="letsdance">Let's Dance!</h2>
<p>원하는 웹툰의 첫 주소도, 최신 주소도 알았고
이제 남은 것은 화면에 뿌려주기만 하면 됩니다.
아무래도 간단하게 만들 때는 웹 어플리케이션으로 만드는 것이
UI를 수정한다거나 Perl과 연동하기도 좋은 것 같습니다.
그렇다면 Perl의 마이크로 웹 프레임워크인
<a href="http://perldancer.org/">Dancer</a>를 사용해서 UI 구현을 마무리하죠.</p>
<p>컨트롤러는 단 두 개만 만들겠습니다.
기본 페이지를 의미하는 <code>/</code>, 즉 인덱스용 컨트롤러 하나와
강제로 사용자가 등록한 웹툰의 정보를 갱신(<code>Web::Scraper</code>를 이용해서)하는
<code>update</code> 컨트롤러를 생성합니다.</p>
<ul>
<li>/</li>
<li>/update/:id?</li>
</ul>
<p><code>/</code> 컨트롤러는 뷰 단에 넘겨주기 위한 데이터를 생성하기 위한 처리를 수행합니다.</p>
<pre class="brush: perl;">
get '/' => sub {
my $webtoon = $CONFIG->{webtoon};
my @items = map {
my $item = $webtoon->{$_};
$item->{id} = $_;
$item->{first} = q{} unless $item->{first};
$item->{last} = q{} unless $item->{last};
$item;
} sort keys %$webtoon;
my $ptr = 0;
my @rows;
while ( $items[$ptr] ) {
my @cols;
for my $i ( 0 .. 9 ) {
last unless $items[$ptr];
push @cols, $items[$ptr];
++$ptr;
}
push @rows, \@cols;
}
template 'index' => {
rows => \@rows,
};
};
</pre>
<p><code>update</code> 컨트롤러는 <code>id</code>를 받아서 특정 회차만 갱신할 수도 있고
<code>id</code>를 넘겨주지 않는 경우 모든 웹툰의 회차 정보를 갱신합니다.</p>
<pre class="brush: perl;">
get '/update/:id?' => sub {
my $id = param('id');
if ($id) {
update($id);
}
else {
update_all();
}
redirect '/';
};
</pre>
<p>웹툰의 정보를 긁어오는 함수는 <code>update_all()</code>과 <code>update()</code> 함수입니다.
<code>update_all()</code> 함수는 내부적으로 <code>update()</code> 함수를 호출하므로
<code>update()</code> 함수를 간략하게 살펴보죠.</p>
<pre class="brush: perl;">
sub update {
my $id = shift;
return unless $id;
my $webtoon = $CONFIG->{webtoon};
return unless $webtoon;
my $site_name = $webtoon->{$id}{site};
return unless $site_name;
my $scraper = $SCRAPERS->{ $site_name };
return unless $scraper;
my $site = $CONFIG->{site};
return unless $site;
my $start_url = sprintf(
$site->{ $site_name }{ 'start_url' },
$webtoon->{$id}{ 'code' },
);
my $items = $scraper->scrape( URI->new( $start_url ) )->{items};
my @links = map { $_->{link} } @$items;
given ( $site_name ) {
update_daum_link($id, @links) when 'daum';
update_naver_link($id, @links) when 'naver';
update_nate_link($id, @links) when 'nate';
}
}
</pre>
<p><code>update()</code> 함수는 다시 각각의 사이트 별로 웹툰을 처리하기 위한 함수로 이동합니다.
각각의 함수에서는 앞에서 보았던 <code>Web::Scraper</code> 모듈을 이용해서
원하는 웹툰 회차 정보를 추출합니다.</p>
<p><code>Web::Scraper</code>를 사용하기 때문에 적절하게 지연 시간을 주지 않으면
포탈 사이트로부터 사용하는 아이피가 블록 당할 수 있으므로 주의하도록 합니다.
필요하다면 <code>update_all()</code> 또는 <code>update()</code> 함수에
<code>sleep $time</code> 처럼 지연 시간을 적절하게 주도록 합니다.</p>
<p>현재 구현상 Manaba가 최초에 Dancer 웹 어플리케이션으로 실행될 때
모든 웹툰의 정보를 긁어옵니다.
웹툰의 양이 많으면 많을수록 정보를 긁어오는데 드는 비용이 커지므로
페이지가 갱신 될때마다 정보를 갱신하기 보다는 각각의 웹툰 별로
갱신할 수 있도록 <code>/update</code> 컨트롤러에서 <code>id</code>를 인자로 받고 있음을 유의해주세요.</p>
<h2>네이버는 괴로워...</h2>
<p>거의 다 작업이 끝나갈 무렵 네이버의 웹툰 대표 이미지가 보이지 않기 시작했습니다.
(이런! 아까까지만 해도 잘 보였는데!!)
여러번의 검색 및 테스트 결과 네이버가 정책적으로 외부 주소에서
자신의 이미지를 열람하는 것을 막아 놓았다는 심증을 굳히게 됩니다.
아마도 트래픽의 폭증을 막기 위한 방안으로 생각되는데
국내 1위 대형 포탈임을 감안할때 생각보다 <em>쪼짜..</em> ... 한 것 같습니다.
IP 블럭 당하지 않은 것이 다행이군요...;;;</p>
<p>그래서 프로그램 상에서 이미지를 바로 보여주는 것이 아니라
다운로드 받아서 로컬 하드 디스크에 저장해서 보여주기로 정책을 선회합니다.
필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/File::Path">CPAN의 File::Path 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Slurp">CPAN의 File::Slurp 모듈</a></li>
</ul>
<p>웹툰의 이미지를 로컬에 저장하는 <code>fetch_webtoon_image()</code> 함수는 다음과 같습니다.</p>
<pre class="brush: perl;">
sub fetch_webtoon_image {
my $ua = Web::Scraper::user_agent;
return unless $ua;
return unless $CONFIG;
my $webtoons = $CONFIG->{webtoon};
return unless $webtoons;
make_path('public/images/webtoon');
for my $id ( keys %$webtoons ) {
my $file = "public/images/webtoon/$id";
next if -f $file;
my $response = $ua->get( $webtoons->{$id}{image} );
if ($response->is_success) {
write_file( $file, $response->content );
}
}
</pre>
<h2 id="manaba">Manaba로 만화봐!</h2>
<p>드디어 완성되었습니다!
약간의 그리드 CSS를 추가해서 1280 해상도를 사용하는 시스템에서
무리없이 볼 수 있도록 했습니다.</p>
<p><img src="2011-12-12-1.png" alt="Manaba로 만화봐!" id="manaba" width="700" />
<em>그림 1.</em> Manaba로 만화봐! <a href="2011-12-12-1.png">(원본)</a></p>
<p>Don't forget <a href="https://github.com/rumidier/Manaba">fork me on GitHub</a>!! ;-)</p>
<h2>정리하며</h2>
<p>최근 웹툰 목록 보기라던가, SNS와의 연동 등 추가해야 할 사항은
<a href="https://github.com/rumidier/Manaba">GitHub</a>를 통해 조금씩 발전시키려고 합니다.
이렇게 <code>Web::Scraper</code>와 <code>YAML::Tiny</code>, <code>Dancer</code> 세 개의 모듈을 이용해
간단하게 웹 어플리케이션을 만들어 보았습니다.
저처럼 MVC 개념에 익숙하지 않으면 직관적인 CGI에 비해 적응하는데
약간의 시간은 걸리지만, Perl과 CPAN 모듈, Dancer와 조합한
웹 어플리케이션 제작 환경은 정말 놀랄 정도로 간단합니다.
여러분들도 마음 속에 담아 두었던 아이디어를 Perl을 통해 한번 구현해보면 어떨까요? ;-)</p>
<h2>후기</h2>
<p>마이크로 프로젝트로 2주 가량의 시간을 산정하고 진행했으나
처음의 원대한 포부로 인해 생각보다 많은 시간이 걸렸습니다.
결국 4~5여 일을 남겨두고 지금 정도의 시스템으로 명세를 대폭 축소하고
복잡한 부분이나 추가 기능은 다음 릴리스를 위해 미루기로 했습니다.
기사 투고는 물론 일정과 관련해서 조금 더 시간 관리에 대해 생각해보게 되었네요.</p>
<p>글을 쓸 수 있게 도와주신 <a href="http://twitter.com/#!/y0ngbin">@y0ngbin</a>님과
<a href="http://twitter.com/#!/am0c">@am0c</a>님, <a href="http://twitter.com/#!/mintegrals">@mintegrals</a>님께 감사드립니다.
또 막판에, 비루한 웹 UI에 희망을 불어넣어 준
<a href="http://twitter.com/#!/keedi">@keedi</a>님께 고마운 마음을 전합니다.
<em>Manaba로 만화봐!</em> 프로젝트는 proof-of-concept 단계의 예제에 가깝지만,
사용하시는 분들과 Perl 및 Dancer를 공부하시려는 분들께 도움이 되길 바래봅니다.</p>
2011-12-12T00:00:00+09:00rumidier리눅스와 Gnome, 노틸러스, GE.TT, Perlhttp://advent.perl.kr/2011/2011-12-11.html<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><a href="http://en.wikipedia.org/wiki/Linux">리눅스</a>가 무엇인지 더이상 설명하지 않아도 되는 세상이 되었습니다.
불과 십 몇년 전까지만 해도 리눅스가 무엇인지, 왜 리눅스를
써야하는지 구구절절 설명하기 바빴는데 말이죠.
오랜 시간동안 리눅스는 서버 뿐만 아니라,
데스크탑 영역에서도 괄목할 만한 성장을 이루었습니다.
<a href="http://www.gnome.org/">Gnome</a>과 <a href="http://kde.org/">KDE</a> 두 진영은 놀랍게도 리눅스 태초부터
지금까지 대단한 뚝심으로 프로젝트를 개선시켜나가며 이어오고 있습니다.
그 중에서도 Gnome 데스크탑의 파일 관리자인 노틸러스는
Gnome 데스크탑의 기본 뼈대가 되는 부분이라고 해도 과언이 아닙니다.
노틸러스는 단순히 파일을 관리한다는 차원을 넘어 파일과 관련해
눈에 보이는 모든 것(그것이 로컬 자원이든, 원격 자원이든)을 제어합니다.
심지어 바탕화면과 바탕화면에서 보이는 파일조차도 말이죠.
따라서 노틸러스에 원하는 기능을 연동할 수 있다면
그 편의성은 상상 그 이상이라고 할 수 있습니다.</p>
<p><img src="2011-12-11-01.png" alt="GE.TT" id="ge.tt" width="700" />
<br />
<em>그림 1.</em> GE.TT <a href="2011-12-11-01.png">(원본)</a></p>
<p><a href="http://ge.tt">GE.TT</a>는 <a href="http://www.dropbox.com">Dropbox</a>와 유사한 서비스로
파일을 저장하고 공유할 수 있는 클라우드 서비스입니다.
이미 Dropbox를 사용해보신 분이라면 알겠지만 특정 디렉터리에
파일을 저장하면 자동으로 원격의 저장소와 동기화가 됩니다.
이런 작업도 사실 노틸러스와의 연동으로 구현한 것이죠.
Gnome 데스크탑 환경에서 GE.TT 서비스를 연동해 마우스 클릭 두 번으로
파일을 업로드하고 공유하는 것이 얼마나 쉬운지 궁금하지 않나요?
그리고 이 모든 작업의 가운데는 Perl이 있다는 사실에 곧 놀라게 될 것입니다. :)</p>
<h2>준비물</h2>
<p>노틸러스의 동작을 제어하기 위해서 설치 해야 할 도구가 있습니다.
데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install nautilus-actions
</pre>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Config::Tiny">CPAN의 Config::Tiny 모듈</a></li>
<li><a href="https://metacpan.org/module/Const::Fast">CPAN의 Const::Fast 모듈</a></li>
<li><a href="https://metacpan.org/module/File::HomeDir">CPAN의 File::HomeDir 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Spec">CPAN의 File::Spec 모듈</a></li>
<li><a href="https://metacpan.org/module/IPC::Run">CPAN의 IPC::Run 모듈</a></li>
<li><a href="https://metacpan.org/module/Net::API::Gett">CPAN의 Net::API::Gett 모듈</a></li>
<li><a href="https://metacpan.org/module/Try::Tiny">CPAN의 Try::Tiny 모듈</a></li>
</ul>
<p><a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Config::Tiny Const::Fast File::HomeDir File::Spec::Functions \
IPC::Run Net::API::Gett Try::Tiny
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<code>perlbrew</code>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Config::Tiny Const::Fast File::HomeDir File::Spec::Functions \
IPC::Run Net::API::Gett Try::Tiny
</pre>
<h2>잠깐!</h2>
<p>(Updated: 현재는 <a href="https://metacpan.org/module/Net::API::Gett">패치된 버전이 릴리스</a>
되었으므로 참고만 하고 따라하지 않으셔도 됩니다. ;-)</p>
<p>현재 릴리스된 <code>Net::API::Gett</code> 모듈 0.01 버전은 <code>refreshtoken</code>으로
인증을 수행하는 부분이 구현되어 있지 않습니다.
저장소의 최신 버전에는 추가되었는데
<a href="https://github.com/mrallen1/Net-API-Gett/commit/da1268b21ec7049ff867b94fefcb4ac3678b6d1e">0.02 버전이 조만간 릴리스될 계획</a>이라고 하니까
우선은 <a href="http://api2.ge.tt/0/3hi7hLB/0/blob/download">패치된 버전</a>을 사용하도록 합니다.
<code>cpanm</code>을 사용하고 있다면 다음 명령을 실행해서 패치된 버전을 설치합니다.</p>
<pre class="brush: bash;">
$ cpanm http://api2.ge.tt/0/3hi7hLB/0/blob/download
</pre>
<p><code>cpanm</code>을 사용하고 있지 않다면 수동으로 설치합니다.</p>
<pre class="brush: bash;">
$ wget -c http://api2.ge.tt/0/3hi7hLB/0/blob/download -O Net-API-Gett-0.01.tar.gz
$ tar xvzf Net-API-Gett-0.01.tar.gz
$ cd Net-API-Gett-0.01
$ perl ./Build.PL
$ ./Build
$ ./Build install
</pre>
<p>시스템에 설치한다면 <code>sudo</code> 명령을 이용해 설치합니다.</p>
<pre class="brush: bash;">
$ sudo ./Build install
</pre>
<h2 id="ge.ttapi">GE.TT API</h2>
<p>GE.TT API와 관련해서는 <a href="http://ge.tt/developers">Ge.tt Developers 페이지</a>에서 확인할 수 있습니다.
우선 API를 사용하기 위해서 <code>api_key</code>를 발급 받아야 합니다.
회원 가입을 한 후 <a href="http://ge.tt/developers">Ge.tt Developers 페이지</a>의
<em>Create app</em>을 클릭한 다음 이름과 웹사이트 및 설명을 입력하고
<code>api_key</code>를 발급 받습니다.</p>
<p><img src="2011-12-11-02.png" alt="GE.TT API 키 발급" id="ge.ttapi" width="700" />
<br />
<em>그림 2.</em> GE.TT API 키 발급 <a href="2011-12-11-02.png">(원본)</a></p>
<p>이렇게 <code>api_key</code>까지만 발급받으면 GE.TT 서비스를 이용하는데 문제가 없지만,
이럴 경우 사용자 계정과 비밀번호가 노출될 위험이 있으므로
<code>accesstoken</code>과 <code>refreshtoken</code>을 이용해서 서비스를 이용하는 편이 더 안전합니다.</p>
<p>발급받은 <code>api_key</code>와 계정 정보를 이용해 명령줄에서 다음 명령을 실행하면
<code>accesstoken</code>과 <code>refreshtoken</code>을 얻어올 수 있습니다.</p>
<pre class="brush: bash;">
$ curl -X POST --data \
'{"apikey":"...","email":"your@email.com","password":"..."}' \
https://open.ge.tt/1/users/login
{
"accesstoken":"a.0101.123123123123",
"refreshtoken":"r.0101.acbcabacbacbacb",
...
}
</pre>
<p>얻어온 <code>accesstoken</code>을 이용하면 이제 더 이상 사용자 계정과 비밀번호,
<code>api_key</code> 없이 파일 공유와 관련한 API 호출을 수행할 수 있습니다.
다만 이 토큰은 유효한 기간이 있기 때문에 해당 기간이 지나면 만료됩니다.
기간이 만료되었을 때 다시 최신의 접근 토큰을 받기 위해
사용하는 것이 <code>refreshtoken</code>입니다.
<code>refreshtoken</code>이 없다면 <code>accesstoken</code>을 받기 위해
사용자 계정과 비밀번호, <code>api_key</code>를 다시 입력 해야 됩니다.
물론 이 접근 토큰의 유효한 기간 역시 사용자가 설정할 수 있는데
자세한 부분은 홈페이지의 개발 문서를 살펴보세요.</p>
<h2 id="ge.ttperl">GE.TT 그리고 Perl</h2>
<p>물론 GE.TT가 API를 지원하기 때문에 직접 POST 또는
REST를 이용해서 API를 호출할 수도 있습니다.
하지만 <a href="https://metacpan.org/module/Net::API::Gett">CPAN Net::API::Gett 모듈</a>이 있기 때문에
단 몇 줄의 코드로 파일을 공유할 수 있습니다.
다음은 GE.TT에 하나의 파일을 업로드한 후 파일의 링크 보여주는 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use Net::API::Gett;
my $file = shift;
my $gett = Net::API::Gett->new(
access_token => 'a.0101.123123123123',
refresh_token => 'r.0101.acbcabacbacbacb',
);
my $file_obj = $gett->upload_file(
title => $file,
filename => $file,
contents => $file,
);
say "File has been shared at " . $file_obj->url;
</pre>
<p>간단하죠? 객체를 생성하고 <code>upload_file()</code> 메소드를 이용하는 것이 전부입니다.</p>
<h2>노틸러스 동작 추가</h2>
<p><code>nautilus-actions</code> 패키지를 설치했다면
메뉴 모음(<code>시스템 - 기본 설정 - 노틸러스 동작 설정</code>)에서
<em>노틸러스 동작 설정</em> 항목이 추가된 것을 확인할 수 있습니다.
<em>노틸러스 동작 설정</em> 프로그램의 영문 이름은 <em>Nautilus Actions Configuration Tool</em>이니,
로컬 설정이 달라서 영문 버전을 사용한다면 참고하세요.</p>
<p><img src="2011-12-11-03.png" alt="시스템 - 기본 설정 - 노틸러스 동작 설정" id="" />
<br />
<em>그림 3.</em> 시스템 - 기본 설정 - 노틸러스 동작 설정</p>
<p>명령줄에서 직접 실행하려면 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ nautilus-actions-config-tool
</pre>
<p>노틸러스 동작 설정 프로그램을 띄운 후 우리가 원하는 동작을 추가해보죠.
<code>File - New action</code>을 누르거나 툴바 메뉴에서 <code>+</code> 기호가 있는 아이콘을 클릭하면
왼쪽 영역인 <em>Action list:</em>에 새로운 동작(action)이 추가됩니다.
클릭해서 원하는 이름으로 바꿉니다. <code>Send to GE.TT</code> 정도면 적절하겠죠?
이 이름은 노틸러스에서 마우스 오른쪽 버튼을 클릭했을때 나타나는
팝업 메뉴에서 보일 이름입니다.</p>
<p><img src="2011-12-11-04.png" alt="노틸러스 동작 추가" id="" width="700" />
<br />
<em>그림 4.</em> 노틸러스 동작 추가 <a href="2011-12-11-04.png">(원본)</a></p>
<p>이제는 팝업 메뉴에서 해당 항목을 클릭했을때 실행할 동작을 설정 해야 합니다.
오른쪽의 탭 영역에서 <code>Command</code> 탭을 선택합니다.
<code>Command</code> 탭 하부의 <code>Command</code> 섹션의 값을 다음처럼 설정합니다.</p>
<ul>
<li><em>Path</em>: <code>/home/<your_id>/bin/gett.pl</code></li>
<li><em>Parameters</em>: <code>%M</code></li>
</ul>
<p><img src="2011-12-11-05.png" alt="Command 섹션 변경" id="command" width="700" />
<br />
<em>그림 5.</em> Command 섹션 변경 <a href="2011-12-11-05.png">(원본)</a></p>
<p><code>Path</code> 항목은 팝업 메뉴에서 <code>Send to GE.TT</code>를 클릭했을 때 실행할 명령의 경로를 지정합니다.
<code>Parameters</code> 항목은 명령을 실행할 때 인자로 넘겨줄 목록입니다.
노틸러스에서 여러 개의 파일을 선택한 후 메뉴를 호출한다면
선택한 모든 파일을 업로드 해야 겠죠.
<code>%M</code>은 노틸러스에서 사용자가 선택한 파일의 전체 경로의 목록입니다.
<code>/home/askdna/share</code> 디렉터리에서 <code>README.txt</code>와 <code>SHOWME.png</code>를 선택했다면
<code>%M</code> 항목은 <code>/home/askdna/share/README.txt</code>와 <code>/home/askdna/share/SHOWME.png</code>를
실행할 파일에게 넘겨줍니다.</p>
<p>또한 이렇게 <code>%M</code>을 사용하려면 노틸러스 동작이
여러 개의 파일을 선택할 수 있도록 설정 해야 합니다.
<code>Conditions</code> 탭을 선택한 다음 <code>선택 사항이 포함되면 보임</code> 항목에서
<code>Appears if selection has multiple files or folders</code> 체크박스를
활성화시킵니다.</p>
<p><img src="2011-12-11-06.png" alt="Conditions 섹션 변경" id="conditions" width="700" />
<br />
<em>그림 6.</em> Conditions 섹션 변경 <a href="2011-12-11-06.png">(원본)</a></p>
<p>마지막으로 이렇게 추가한 노틸러스 동작과 관련한 설정은
Gnome 데스크탑 환경이 기본으로 사용하고 있는 <code>GConf</code>로 저장됩니다.
<code>gconf-editor</code> 명령을 실행시키면 <code>/apps/nautilus-actions/configurations</code> 섹션
하부에 조금 전에 추가한 노틸러스 동작이 저장되어 있는 것을 확인할 수 있습니다.</p>
<p><img src="2011-12-11-07.png" alt="gconf-editor" id="gconf-editor" width="700" />
<br />
<em>그림 7.</em> gconf-editor <a href="2011-12-11-07.png">(원본)</a></p>
<h2 id="gett.pl">gett.pl</h2>
<p>노틸러스 동작을 추가할 때 팝업 메뉴를 띄운 후 실제로 실행시킬
명령을 <code>/home/<your_id>/bin/gett.pl</code>이라고 입력했습니다.
이제는 GE.TT로 전송하기 기능의 핵심인 <code>gett.pl</code> 스크립트를 작성해보죠.
우선 <code>gett.pl</code> 명령은 노틸러스로 부터 1개 이상의 파일을 인자로 받습니다.
<code>@ARGV</code> 변수를 이용해서 각각의 인자를 처리하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
die "Usage: $0 <file> [ <file> ... ]\n" unless @ARGV;
for my $file ( @ARGV ) {
next unless -f $file;
# share the $file
}
</pre>
<p>그리고 <code>accesstoken</code>과 <code>refreshtoken</code>을 스크립트에 저장하는 것은
유지 보수 및 배포의 측면에서 적절하지 못해보입니다.
이런 경우 환경 변수나 설정 파일을 이용하면 좋을 것 같은데
지금은 설정 파일을 이용하도록 하죠.
설정 파일의 경로는 <code>~/.gett/config</code>, 형식은 INI라고 가정합니다.
다음은 설정 파일의 예입니다.</p>
<pre class="brush: ini;">
[Connect]
access_token = a.0101.123123123123
refresh_token = r.0101.acbcabacbacbacb
</pre>
<p><a href="http://en.wikipedia.org/wiki/INI_file">INI 형식의 설정 파일</a>은 <a href="https://metacpan.org/module/Config::Tiny">CPAN의 Config::Tiny 모듈</a>을
사용하면 쉽게 읽어오거나 쓸 수 있습니다.</p>
<pre class="brush: perl;">
use Config::Tiny;
use Net::API::Gett;
#
# GE.TT object
#
my $config = Config::Tiny->read( '/home/your_id/.gett/config' );
my $gett = Net::API::Gett->new(
access_token => $config->{Connect}{access_token} || q{},
refresh_token => $config->{Connect}{refresh_token} || q{},
);
</pre>
<p>필요한 부분은 모두 갖춰졌습니다. 남은 작업은 이 모든 것을 합치는 것이죠.
다음은 <code>/home/your_id/gett.pl</code> 스크립트의 전체 소스 코드입니다.</p>
<pre class="brush: perl;">
#!/home/your_id/perl5/perlbrew/perls/perl-5.14.1/bin/perl
use 5.010;
use strict;
use warnings;
use Config::Tiny;
use Const::Fast;
use File::Basename;
use File::HomeDir;
use File::Spec::Functions;
use IPC::Run qw( run );
use Net::API::Gett;
use Try::Tiny;
# Get API Key from http://ge.tt/developers
const my $GETT_DIR => catfile( File::HomeDir->my_home, '.gett');
const my $CONF => catfile( $GETT_DIR, 'config' );
const my $LOG => catfile( $GETT_DIR, "$$.log" );
const my @ZENITY => (
'zenity',
'--text-info',
'--width=600',
'--height=200',
'--title=GE.TT 파일 업로드',
);
die "Usage: $0 <file> [ <file> ... ]\n" unless @ARGV;
open my $log, '>', $LOG
or die "cannot open log file: $!\n";
#
# GE.TT object
#
my $config = Config::Tiny->read( $CONF );
my $gett = Net::API::Gett->new(
access_token => $config->{Connect}{access_token} || q{},
refresh_token => $config->{Connect}{refresh_token} || q{},
);
#
# Check access token is valid or not
# Then update access token
#
say $log 'Check access token';
check_access_token($gett);
if ( $gett->access_token ne $config->{Connect}{access_token} ) {
$config->{Connect}{access_token} = $gett->access_token;
$config->write( $CONF );
say $log 'Update access token: ' . $gett->access_token;
}
#
# Share files
#
my @files;
for my $file ( @ARGV ) {
unless ( -f $file ) {
say $log "$file is not exists";
next;
}
my $file_obj = share($gett, $file);
if ($file_obj) {
push @files, {
name => $file,
obj => $file_obj,
};
say $log "Sharing: " . $file_obj->url . " - $file";
}
else {
say $log "Sharing failed: $file";
}
}
#
# Notify the result to user
#
my $result = "파일 업로드가 완료되었습니다.\n\n";
$result .= join "\n", map { $_->{obj}->url . " - $_->{name}" } @files;
run( \@ZENITY, \$result );
sub check_access_token {
my $gett = shift;
#
# Check access token is still valid and update it
#
if ( $gett->access_token ) {
try {
$gett->my_user_data;
}
catch {
say $log "access_token is invalid or outdated.";
if ( $gett->refresh_token ) {
unless ( $gett->login ) {
say $log 'cannot login GE.TT';
die;
}
}
else {
say $log 'refresh_token or valid access_token is needed.';
die;
}
};
}
else {
unless ( $gett->login ) {
say $log 'cannot login GE.TT';
die;
}
}
}
sub share {
my ( $gett, $file ) = @_;
my $basename = basename($file);
my $file_obj = $gett->upload_file(
title => '[gett.pl] ' . $basename,
filename => $basename,
contents => $file,
);
return $file_obj;
}
</pre>
<p><a href="#"></a>
첫 줄의 쉬뱅 라인에 <code>#!/home/your_id/perl5/perlbrew/perls/perl-5.14.1/bin/perl</code>처럼
전체 Perl 경로를 기입한 점을 유의하세요.
시스템 Perl을 사용한다면 상관없지만 <code>perlbrew</code>를 사용하거나 또는
직접 설치한 Perl을 사용한다면 반드시 전체 경로를 기입 해야 합니다.
그렇지 않으면 노틸러스 동작이 실행시킬때 기본 Perl인 <code>/usr/bin/perl</code>을
이용해서 스크립트를 실행시키므로 필요한 모듈이 설치되지 않았다던가
하는 문제로 인해 제대로 실행되지 않을 수 있기 때문입니다.</p>
<h2 id="sendtoge.tt">Send to GE.TT!!</h2>
<p>이제 노틸러스에서 GE.TT 사이트로 파일을 공유해볼까요?
노틸러스에서 하나 이상의 파일을 선택한 다음 마우스 오른쪽
버튼을 눌러서 <code>Send to GE.TT</code> 항목을 선택합니다.</p>
<p><img src="2011-12-11-08.png" alt="GE.TT로 파일 공유하기" id="ge.tt" />
<br />
<em>그림 8.</em> GE.TT로 파일 공유하기</p>
<p><code>gett.pl</code> 스크립트는 파일 업로드에 성공한 항목의 결과를 모두 모아서
<code>zenity</code>에게 보내주므로 일정 시간이 지나면 Gnome 데스크탑 화면에
결과를 보여줍니다.</p>
<p><img src="2011-12-11-09.png" alt="성공!!" id="" width="700" />
<br />
<em>그림 9.</em> 성공!! <a href="2011-12-11-09.png">(원본)</a></p>
<p>실제 GE.TT 사이트에 로그인해서 확인해보면 정상적으로
파일 업로드에 성공했음을 알 수 있습니다.
이제 링크를 필요한 사람에게 알려주는 일만 남았네요. :)</p>
<p><img src="2011-12-11-10.png" alt="GE.TT 사이트에서 확인한 화면" id="ge.tt" width="700" />
<br />
<em>그림 10.</em> GE.TT 사이트에서 확인한 화면 <a href="2011-12-11-10.png">(원본)</a></p>
<h2>정리하며</h2>
<p>사실 GE.TT 서비스는 쉐어와 파일이라는 개념을 두고 쉐어 안에
여러 개의 파일을 넣어서 공유할 수 있습니다.
즉 쉐어가 일종의 디렉터리 또는 파일을 감싸는 보자기인 셈입니다.
현재의 구현은 각각의 파일에 대해 각각의 쉐어를 생성해서
하나의 쉐어에 하나의 파일이 저장되도록 했습니다.
노틸러스에서 선택해서 <code>Send to GE.TT</code>를 실행시키는 경우
해당 파일들은 하나의 쉐어를 생성하고 여러 파일이 포함되는
방식으로 구현하는 것이 더 편리할 것입니다.
이 부분은 <code>Net::API::Gett</code> 모듈 문서를 참고해서 스크립트를
약간만 수정하면 처리할 수 있는데 이 부분은 여러분에게 맡기도록 하죠.</p>
<p>리눅스는 멋진 운영체제입니다.
Gnome은 멋진 데스크탑 환경입니다.
Perl은 멋진 언어입니다.
이 세 가지 멋진 녀석들이 합쳐지면 그 시너지 효과는 정말 대단합니다.
Gnome의 유연한 구조 덕에 사용자는 원하는 고급 기능을
정말 손쉽게 시스템에 통합해 추가할 수 있습니다.
Perl의 강력함과 CPAN의 방대함 덕에 수고스럽고,
조금은 번거로운 네트워크 프로그래밍은 너무나도 간단해집니다.
GE.TT 클라우드 서비스를 예로 들었지만 기사에서 설명한 모든 기법은
어떠한 서비스와 연동한다 하더라도 적용되는 부분입니다.</p>
<p>자, 이제 무엇을 제물로 삼아 노틸러스에 연동할지 주변을 둘러보세요!</p>
<p>Enjoy Your Perl! ;-)</p>
2011-12-11T00:00:00+09:00keediGuiTest를 활용한 네이트온 원격제어 자동 수락기http://advent.perl.kr/2011/2011-12-10.html<h2>저자</h2>
<p><a href="http://twitter.com/perlstudy">@perlstudy</a> - <a href="http://cafe.naver.com/perlstudy">네이버 Perl 카페</a> 운영자, <a href="http://honeyperl.tistory.com/">홈페이지</a>에는 Perl과 관련한 유용한 정보를 종종 올리고 있다. 한 때 보안업계에 몸담았던 언더그라운드 Perler. 호네이, h0ney라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>저는 자동화 도구에 관심이 많습니다.
자동화 도구는 직접 해야하는 작업이나 단순한 반복 작업을
자동으로 처리할 수 있게 도와줍니다.
작년에는 <a href="http://advent.perl.kr/2010/2010-12-07.html">Advent Calendar</a> 기사에서
<a href="http://p3rl.org/WWW::Mechanize">WWW::Mechanize</a> 모듈을 통한 웹 자동화 방법에 대해 소개했습니다.
오늘은 Windows에서 유용하게 사용할 수 있는 테스트 자동화 모듈을 소개해 볼까합니다.</p>
<p>자동화 기술로써 가장 먼저 떠오르는 것 중 하나는 <em>후킹(hooking)</em>일 것입니다.
후킹이란 메시지나 이벤트를 중간에 가로채거나 가로챈 이벤트를 임의로 바꾸는 기술입니다.
후킹을 통해 컴퓨터에서 일어나는 일을 마음대로 조정할 수 있습니다.
이 기술을 완벽하게 사용하기 위해서는 많은 노력이 필요합니다.
<em>Win32 API</em>부터 Windows의 내부 동작 원리까지 상세하게 공부해야 합니다.
이런 덕목들은 Windows 프로그래머에게는 기본 소양으로 여겨지겠지만
단지 활용하고 싶은 사람들에게는 하늘에 별 따기일 수 있습니다.</p>
<p>2005년 Perl 진영에 Windows에서 다양한 GUI 테스트를 쉽게 할 수 있는
<a href="http://p3rl.org/Win32::GuiTest">Win32::GuiTest - Perl GUI Test Utilities</a> 모듈이 나왔습니다.
문서나 소스로만 공개되어 실제로는 사용하기 까다로운 자동화 기술이
모듈로 제공되어 많은 이들이 쉽게 자동화할 수 있도록 도왔습니다.
이 모듈을 처음 발견 했을 당시 깜짝 놀랐습니다.
이렇게 쉽고 간단하게 자동화를 할 수 있다니!
그 후로 이 모듈은 저의 최고의 장난감이 되어 크고 작은 자동화 부분을
획기적으로 처리하는 일등공신이 되었습니다.
그럼 지금부터 <em>Win32::GuiTest</em>를 사용해봅시다!</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="http://p3rl.org/Win32::GuiTest">CPAN의 Win32::GuiTest 모듈</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples">CPAN의 Win32::GuiTest::Examples 예제</a></li>
</ul>
<p>윈도우에서는 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: plain;">
c:\> cpan Win32::GuiTest
</pre>
<h2 id="guitest">GuiTest 예제로 배우기</h2>
<p>설명을 읽는 것보다 예제를 직접 보고 실행해보면
사용법과 활용 방법을 쉽게 이해할 수 있습니다.</p>
<p>CPAN의 GuiTest는 총 26개의 예제를 제공합니다.</p>
<ul>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-ask.pl">ask.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-calc.pl">calc.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-excel.pl">excel.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-excel2.pl">excel2.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-fonts.pl">fonts.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-iswindowstyle.pl">iswindowstyle.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-keypress.pl">keypress.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-menuselect.pl">menuselect.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-notepad.pl">notepad.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-notepad_text.pl">notepad_text.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-paint.pl">paint.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-paint_abs.pl">paint_abs.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-pushbutton.pl">pushbutton.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-rawkey.pl">rawkey.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-selecttabitem.pl">selecttabitem.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-showcpl.pl">showcpl.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-showmouse.pl">showmouse.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-showwin.pl">showwin.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-.pl">spy--.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-spy.pl">spy.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-start.pl">start.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-tab.pl">tab.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-waitwindow.pl">waitwindow.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-which.pl">which.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-winbmp.pl">winbmp.pl</a></li>
<li><a href="http://p3rl.org/Win32::GuiTest::Examples#eg-wptr.pl">wptr.pl</a></li>
</ul>
<p>엑셀에 자동으로 자료를 입력하거나 그림판을 실행시킨 후 선을 긋고
키보드와 마우스를 자유자재로 사용하는 예제들을 확인할 수 있습니다.</p>
<h2>네이트온 원격제어 자동 수락기</h2>
<p>지금까지 살펴본 모듈을 활용하여 네이트온의 원격 제어 요청을 자동으로 수락해주는 프로그램을 만들어 봅시다.
먼저 네이트온의 원격 제어 요청이 들어오는 창을 찾아야 합니다.
윈도우 창을 열거하는 예제 프로그램으로
<a href="http://p3rl.org/Win32::GuiTest::Examples#eg-.pl">spy--.pl</a>와 <a href="http://p3rl.org/Win32::GuiTest::Examples#eg-spy.pl">spy.pl</a>가 있습니다.
<a href="http://p3rl.org/Win32::GuiTest::Examples#eg-.pl">spy--.pl</a>의 소스를 확인해봅시다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
# MS has a very nice tool (Spy++).
# This is Spy--
#
use Win32::GuiTest qw(FindWindowLike GetWindowText GetClassName
GetChildDepth GetDesktopWindow);
for (FindWindowLike()) {
$s = sprintf("0x%08X", $_ );
$s .= ", '" . GetWindowText($_) . "', " . GetClassName($_);
print "+" x GetChildDepth(GetDesktopWindow(), $_), $s, "\n";
}
</pre>
<p>위 코드를 실행하면 윈도 창의 목록이 출력되며 하위 목록은 '+'로 깊이를 주어 출력됩니다.</p>
<pre class="brush: plain;">
++0x028F10DA, '시작', Button
+0x001C1280, '', Shell_TrayWnd
++0x00191264, '', TrayNotifyWnd
+++0x0042125E, '오후 9:08', TrayClockWClass
+++0x00151242, '', TrayShowDesktopButtonWClass
+++0x0032119A, '', SysPager
++++0x00151192, '사용자 프롬프트 알림 영역', ToolbarWindow32
+++0x0057117E, '시스템 프롬프트 알림 영역', ToolbarWindow32
+++0x00581152, '', Button
++0x00E206C6, '', ReBarWindow32
+++0x0025004A, 'TF_FloatingLangBar_WndTitle', CiceroUIWndFrame
+++0x007C0A4C, '응용 프로그램 실행 중', MSTaskSwWClass
++++0x002D1106, '응용 프로그램 실행 중', MSTaskListWClass
++0x007406AA, '', tooltips_class32
...
</pre>
<p>예를들어 박병조라는 분이 네이트온에서 원격 접속을 요청하면 아래 그림과 같이 요청을 수락할 것인지 알람이 나타납니다.</p>
<p><img src="2011-12-10-1.png" alt="네이트온 수락 요청 알람" id="" />
<br />
<em>그림 1.</em> 네이트온 수락 요청 알람 <a href="2011-12-10-1.png">(원본)</a></p>
<p>이 요청은 메시지로 확인할 수 있을 것입니다.
아래와 같이 메시지가 발생할 윈도 창의 이름은 <code>박병조(Bazinga!)님과의 대화</code>인 것을 확인할 수 있습니다.</p>
<pre class="brush: plain;">
+0x009B0E26, '박병조(Bazinga!)님과의 대화', #32770
++0x004212CE, 'NateOn Menu Bar', Afx:00400000:3:00010003:01900010:00000000
++0x0085134A, '', SOFTWEB_CONTROL
++0x00480FEC, '', AfxWnd80su
++0x001F18B6, '', AfxWnd80su
+++0x002118B4, '', Static
+++0x00A30CF8, '', AfxWnd80su
++0x005418A0, '', AfxWnd80su
+++0x007A0F74, 'WebCam', Button
+++0x00421016, 'HideBtn', Button
++0x008016B6, '', SOFTWEB_CONTROL
+++0x00510C18, 'Search', AfxWnd80su
++++0x0035010E, 'Search', Button
++0x0027186A, '', SOFTWEB_CONTROL
++0x009406C8, '', Edit
++0x0012194C, 'chat_titleIcon', Button
++0x005B1374, '보내기(&S)', Button
++0x0097100C, '>>', Button
</pre>
<h2>만들어 봅시다</h2>
<p>이제 모든 준비는 끝났습니다. 실제로 <code>박병조</code>라는 이름이 포함된 윈도 창을 찾아 원격 제어의 수락 버튼을 눌러주기만 하면 됩니다.
버튼을 누르는 함수는 다양하게 존재하지만 지금은 Sendkeys를 사용하여 원격제어를 수락하여 보겠습니다.</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Win32::GuiTest qw(:ALL);
while (1) {
sleep 2; # 2초마다 한번씩 윈도우 창을 검색합니다.
for my $window (FindWindowLike()) {
my $str = sprintf("0x%08X", $window);
$str .= ", '" . GetWindowText($window) . "', " . GetClassName($window);
if($str =~ /박병조/){ # 원격을 걸 수있는 사람을 설정
SetForegroundWindow($window); # 찾은 윈도우에 포커스를 맞춥니다.
sleep 1;
SendKeys("%c"); # 원격제어 수락 단축키 Alt+C 를 뜻합니다.
sleep 2;
print "수락되었습니다 :)";
}
}
}
</pre>
<p>원격 제어가 수락된 모습입니다! 두둥!</p>
<p><img src="2011-12-10-2.png" alt="원격 제어가 자동으로 수락된 모습" id="" width="700" />
<br />
<em>그림 2.</em> 원격 제어가 자동으로 수락된 모습 <a href="2011-12-10-2.png">(원본)</a></p>
<h2>정리하며</h2>
<p>예시를 통해 GuiTest 모듈에 대해 단편적으로 설명했으나 실제로는 다방면으로 활용할 수 있을 것입니다.
직접 다른 모듈과 조합하여 활용한 내용들을 아래에 정리해 보았습니다.</p>
<ul>
<li><a href="http://honeyperl.tistory.com/entry/Win32ProcessMemory">Win32::Process::Memory 활용 예제</a></li>
<li><a href="http://honeyperl.tistory.com/entry/%ED%95%9C%EA%B8%80-%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%84-%EC%9E%90%ED%8C%90%EC%97%90-%EC%9E%85%EB%A0%A5%EB%90%98%EB%8A%94-%EC%98%81%EB%AC%B8%EB%A1%9C-%EB%B0%94%EA%BE%B8%EB%8A%94-%EB%B0%A9%EB%B2%95">한글 문자열을 키보드 자판의 영문자로 변환</a></li>
<li><a href="http://honeyperl.tistory.com/entry/%EB%84%A4%EC%9D%B4%ED%8A%B8%EC%98%A8-%EC%AA%BD%EC%A7%80-%EC%9E%90%EB%8F%99%EB%8B%B5%EC%9E%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8">네이트온 쪽지 자동 답장 프로그램</a> (지금은 버전이 바뀌어 작동하지 않습니다.)</li>
<li><a href="http://honeyperl.tistory.com/entry/%ED%94%8C%EB%9E%98%EC%8B%9C-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%A6%90%EA%B8%B0%EC%9E%90">플래시 게임을 즐기자.</a></li>
</ul>
<h2>후기</h2>
<p>Advent Calender의 기고를 부탁받은 뒤
이번에는 GuiTest를 소개해 보자는 생각이 번뜩 들었습니다.
어떤 예제를 만들어 볼까 고민하다 후배가 사용하던
네이트온 원격제어 자동 수락기를 만들고자 결심한 뒤,
얼마나 시간이 걸릴까 소스는 몇 줄이나 될까 스스로도 궁금했는데
만들고 보니 감탄할 수 밖에 없었습니다.</p>
<p>만들고자 하는 프로그램을 이렇게 신속하게 만드는데 Perl만한 언어가 또 있을까 싶습니다. :)</p>
<p>한국 Perl 진영을 이끌어 주시는 <a href="http://twitter.com/keedi">@keedi</a>님,
그리고 <a href="http://twitter.com/y0ngbin">@y0ngbin</a>님과 <a href="http://facebook.com/silexkr">Silex</a>분들께 행사때마다 연락 주셔서 정말 감사합니다.
놀러 갈 때마다 너무도 따스하게 맞아주시니 눌러 앉아버리고 싶을때가 간혹 있습니다.
최근에는 자주 못 뵈었지만 정신적 지주이신 <a href="http://twitter.com/aer0">@aer0</a>님과
<a href="http://twitter.com/gypark">@gypark</a>님께 네이버 Perl 카페에서 열정적으로 활동해 주셔서 고맙습니다.
마지막으로 네이트온 원격을 걸어주며 함께 테스트 해주신 <a href="http://twitter.com/conetPark">@conetPark</a>(박병조)님께 감사드리며,
불철주야 Advent Calender를 위해 노력하시는 <a href="http://twitter.com/am0c">@am0c</a>님께 박수를 보냅니다.</p>
2011-12-10T00:00:00+09:00perlstudyYoutube에서 원하는 동영상 내려받기http://advent.perl.kr/2011/2011-12-09.html<h2>저자</h2>
<p><a href="http://twitter.com/eeyees">@eeyees</a> - 인쇄기기 업계의 기린아, TAFKA_HoliK라는 닉을 사용하기도 한다.
일본에서 일하다가 올해 한국 후지 제록스로 이직하였다.</p>
<h2>시작하며</h2>
<p>우리는 일상에서 많은 문제에 직면하게 됩니다.
이런 문제들을 빠르게 해결할 수 있는 스크립트 언어를 한가지쯤 알고 있다면
그것은 굉장한 행운일 것입니다. 저는 Perl을 통해 자잘한 문제들을 해결하고 있습니다.
지금부터 최근에 제가 겪은 문제를 Perl과 CPAN을 이용해서
어떻게 빠르고 쉽게 해결하는지 보여 드리도록 하겠습니다.</p>
<h2>회상</h2>
<p>평소와 비슷한 일상을 보내고 있는 저에게 갑자기 큰 문제가 발생했습니다.
주말에 TV를 보다가 아이유의 노래에 <em>feel</em>이 팍! 꽂힌 것입니다.
그리고 Youtube에 접속해 아이유의 뮤직비디오를 감상하다가 이렇게 생각한 것이죠.</p>
<blockquote>
<p>'아이유 뮤직비디오를 가지고 싶다!'</p>
</blockquote>
<p>Youtube에 있는 동영상을 자동으로 쉽게 다운로드할 수는 없을까요?
곧장 <a href="https://metacpan.org/">metacpan</a>에 접속해 Youtube 관련 모듈을 찾아보았습니다.</p>
<p><img src="2011-12-09-1.png" alt="입력하는 모습" id="" width="700" /></p>
<p>역시 많이 있군요! 그럼 작업을 시작해 볼까요?</p>
<h2>따라하기</h2>
<p>Youtube 검색을 위해 다음 모듈을 CPAN으로 부터 설치 합니다.</p>
<pre class="brush: bash;">
$ sudo cpan install WebService::GData::YouTube
</pre>
<p>검색된 결과를 다운로드 받기 위해 다음 CPAN 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan install WWW::YouTube::Download
</pre>
<p>간단하게 다음과 같은 스크립트를 작성합니다.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use WWW::YouTube::Download;
use WebService::GData::YouTube;
# 아듀먼트로 검색 값
my ($search, $limit) = (@ARGV);
$limit //= 10;
# 검색 값으로 유투브 검색
my $search_youtube = WebService::GData::YouTube->new;
# 쿼리 값 설정
$search_youtube->query()->q($search)->limit($limit, 0);
# 검색
my $results = $search_youtube->search_video();
# 다운로드
my $client = WWW::YouTube::Download->new;
foreach my $ret (@$results) {
say "Starting Download : " . $ret->title;
$client->download($ret->video_id);
}
</pre>
<p>완성입니다!</p>
<h2>실행</h2>
<p>만든 스크립트를 실행하여 동영상을 내려받아 보겠습니다.
첫 번째 인자에 검색어를 넣고, 두 번째 인자에 다운로드 받을 개수를 입력합니다.</p>
<pre class="brush: bash;">
$ perl main.pl "아이유" 10
</pre>
<p><img src="2011-12-09-2.png" alt="터미널" id="" /></p>
<p>열심히 받아지고 있는 모습입니다. :)</p>
<p><img src="2011-12-09-3.png" alt="받는중" id="" width="700" /></p>
<h2>정리하며</h2>
<p>Perl과 CPAN을 몰랐다면 이렇게 간단하게 문제를 해결할 수는 없었을 겁니다.
문제가 발생하면 현재 무려 <em>10여 만개</em>의 CPAN 모듈이 모여 있는 <a href="http://www.cpan.org/">cpan.org</a>에서
모듈을 쉽게 검색할 수 있습니다.
아름다운 인터페이스를 제공하는 <a href="https://metacpan.org/">metacpan.org</a>도 있습니다.
모든 모듈은 문서와 예제 코드를 포함하고 있기 때문에 쉽게 접근할 수 있습니다.</p>
<p>마지막으로 도움을 주신 한국 펄 몽거스 분들과 아이유 양에게 감사드립니다.</p>
2011-12-09T00:00:00+09:00eeyeesFormNa게 Form 생성하기http://advent.perl.kr/2011/2011-12-08.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/mintegrals">@mintegrals</a> -
랩의 귀재, <em>MC.민나이퍼</em>라고 불리며 현재 Silex에서 <em>여자</em>를 맡고 있다.
본의 아니게 올해 Seoul.pm 크리스마스 달력에서도 <em>여자</em>를 맡게 되었다.
Perl과 Catalyst 웹프레임워크, 발사믹 목업, git을 주로 사용해 개발하고 있다.
현업에 갓 입문한 새내기 개발자, 하지만 Perl 영재교육을 받으며 무서운 속도로 성장중.
Perl에 입문한 첫 해에 크리스마스 달력을 쓰지만 정작 자신의 글이
올라오는 날에는 수술로 인해 아무것도 보지못한다.
눈을 뜨기 위해 공양미 150석이 필요하다는 후문.</p>
<h2>시작하며</h2>
<p><em>부제 : 가변데이터에 대해 문서 자동으로 생성하기</em></p>
<p>생활하다보면 특정한 문서 틀이 필요할 때가 있습니다.
그럴 때 우리는 <em>'서식 파일'</em>, <em>'보고서 양식'</em>을 검색하곤 합니다.
그리고 적당한 틀이있는 파일(주로 DOC 또는 HWP)을 다운받은 후
특정 위치에 원하는 내용을 내용을 채워넣습니다.
이렇게 양식 파일을 다운 받는 것은, 일정한 틀은 유지하면서
데이터만 변경해서 문서를 만들어야 할 때가 많기 때문입니다.
일상 생활의 예를 들면 마트에서 주는 영수증부터, 회사의 거래내역서,
이력서, 주소록, 월급 명세서 등 다양합니다.
이러한 보고서 서식(form)을 제공하는 사이트로 국내에는
<a href="http://www.bizforms.co.kr">비즈폼</a>이나 <a href="http://www.yesform.co.kr">예스폼</a> 등이 있습니다.
하지만 이런 사이트에서 제공하는 파일을 이용하면 한계가 있습니다.
한 두장 정도는 어떻게든 만들 수 있겠지만
주기적, 정기적으로 바뀌는 내용을 갱신해야 한다거나,
많은 양의 자료를 바꿔가며 뽑아내야 한다면 현실적으로 무리가 있습니다.
바로 이때 Perl과 ODF를 사용하면, 이러한 Form들을 FormNa게 찍어낼 수 있습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/OpenDocument::Template">CPAN의 OpenDocument::Template 모듈</a></li>
<li><a href="https://metacpan.org/module/Catalyst">CPAN의 Catalyst 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan OpenDocument::Template Catalyst
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<code>perlbrew</code>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan OpenDocument::Template Catalyst
</pre>
<h2>오픈도큐먼트와 함께라면</h2>
<p>오픈도큐먼트와 함께라면 문서를 찍어내는데 거리낄 것이 없습니다.
지금부터 ODT를 이용하여, 문서를 찍어낼 것입니다.
ODT를 이용해서 문서를 생성하기 위해 우선 ODF 파일의 구조를 파악해야합니다.</p>
<h3 id="odf:opendocumentformat">ODF: OpenDocumentFormat</h3>
<p>오픈도큐먼트 형식(ODF)은 초기 <a href="http://www.openoffice.org">오픈오피스</a>에서 XML파일 포맷을
기반으로 구현한 것을 <a href="http://www.oasis-open.org">OASIS 컨소시엄</a>(Organization for the Advancement
of Structured Information Standards)이 표준화 한 파일 형식입니다.
이 포맷은 오픈 XML기반 문서 파일 포맷으로, 기준에 따라 재사용 가능하고,
텍스트 문서(ODT), 스프레드시트(ODS), 차트 및 그래픽 요소(ODG)등에 이용됩니다.</p>
<p>ODF포맷을 사용한 대표적인 응용 프로그램 및 오피스 제품은 다음과 같습니다.</p>
<ul>
<li><a href="http://www.libreoffice.org">LibreOffice</a></li>
<li><a href="http://www.openoffice.org">OpenOffice</a></li>
<li><a href="http://www.abisource.com">AbiWord</a></li>
<li><a href="http://www.koffice.org">KOffice</a></li>
<li><a href="https://docs.google.com/">Google Docs</a></li>
<li><a href="http://office.microsoft.com/ko-kr/">MS Office 2010/2007 SP2</a></li>
</ul>
<h3 id="odt:opendocumenttext">ODT: OpenDocumentText</h3>
<p>즉, ODT란 오픈도큐먼트 포맷(ODF)이 사용된 워드프로세서 텍스트 문서 입니다.
그럼 ODT는 어떻게 구성되어 있을까요?
오픈도큐먼트포맷(ODF)은 일반적으로 여러 개의 XML 문서와 이진 파일을
하나의 ZIP 컨테이너 안에 묶는 형태입니다.
확장자는 <code>.odt</code> 형식을 따르지만 실제로는 ZIP 압축 파일인 것이죠.</p>
<p>ODT 뿐만 아니라 일반적인 오픈도큐먼트포맷(ODF)의 파일 구성을 살펴봅시다.
오픈오피스(또는 ODF 쓰기를 지원하는 프로그램)를 열어
아무 글이나 작성한 후 <code>sample.odt</code> 파일로 저장합니다.
이제 <code>unzip</code> 유틸리티를 써서 압축을 풀고 디렉터리 구조가 어떤지 살펴보죠.</p>
<pre class="brush: bash;">
$ unzip sample.odt
Archive: sample.odt
extracting: mimetype
inflating: content.xml
inflating: manifest.rdf
inflating: styles.xml
extracting: meta.xml
inflating: Thumbnails/thumbnail.png
inflating: Configurations2/accelerator/current.xml
creating: Configurations2/progressbar/
creating: Configurations2/floater/
creating: Configurations2/popupmenu/
creating: Configurations2/menubar/
creating: Configurations2/toolbar/
creating: Configurations2/images/Bitmaps/
creating: Configurations2/statusbar/
inflating: settings.xml
inflating: META-INF/manifest.xml
</pre>
<p>무언가 굉장히 많은 파일들이 생겼지만, 우리가 하려는 작업인
문서의 내용을 조작하고 생성하는 관점에서 다가간다면
핵심은 <code>content.xml</code>, <code>meta.xml</code>, <code>styles.xml</code> 세 개의 파일입니다.
<code>content.xml</code> 파일은 문서를 통해 보여지는 대부분의 내용을 담고있습니다.
일반적으로 텍스트, 표, 프레젠테이션 등의 구조와 내용입니다.
<code>meta.xml</code> 파일은 문서의 MIME 타입을 기술합니다.
생성 시간, 최종 수정 시간, 문서 수정 시 걸린 전체 시간,
낱말/쪽/표/그림 개수 같은 문서 정보에 해당하는 항목들이 담겨있습니다.
<code>styles.xml</code> 파일은 XML 형식으로 된 CSS라고 보면 됩니다.
글꼴(font), 인치 당 문자 수(pitch), 장식(decoration), 여백(spacing),
탭 위치(tab stop) 등 문서 편집시 이용할 수 있는 다양한 스타일이 정의되어 있습니다.</p>
<h2>크리스마스 카드</h2>
<p>내친 김에 연말이니, 크리스마스 카드나 연하장을 만들어볼까요?
어릴 적 크리스마스 카드를 쓰던 기억이 나나요?
처음엔 공 들여서 이런저런 이야기를 쓰다가 써야 할 친구들이 점차 늘어나면,
내용은 계속 똑같이 베껴 쓰면서 받는 친구의 이름만 바꾸곤 했죠. :-)
그렇다면 처음 한번만 예쁘게 카드를 꾸미고,
이름만 쓰면 크리스마스 카드가 나오게 할 수 있다면,
트위터나 페이스북 친구에게 크리스마스 카드를 돌리는 것은 일도 아니겠죠.
만들어 봅시다.</p>
<p>우선 오픈오피스를 열어 <code>xmas-card.odt</code>를 만듭니다.
여기서 만든다는 것은 <em>꾸미고 싶은 만큼 잔뜩 꾸미는 것</em>을 말합니다.
다음으로 자료가 변경해서 들어갈 부분을 표시합니다.
예를 들어, 크리스마스 카드의 경우 받는 사람의 이름이나 날짜는 계속 바뀌겠죠.
네, 그것이 핵심입니다!
문서를 계속 <em>우려서</em> 자동 생성하되, 필요한 부분의 데이터만 받아서 이용하자구요!
그렇다면 가변 데이터에 해당하는 부분을 <code>xxxx</code>라고 표시합니다.</p>
<p><img src="2011-12-08-1.png" alt="xxxx 표시" id="xxxx" width="700" />
<br />
<em>그림 1.</em> xxxx 표시 <a href="2011-12-08-1.png">(원본)</a></p>
<p>사실 여기까지(틀을 만드는 것) 진행했으면 벌써 절반 이상 된 것입니다.
이젠 <code>unzip</code> 유틸리티를 사용해 <code>xmas-card.odt</code> 파일의 압축을 풉니다.
압축을 푼 다음 <code>content.xml</code>과 <code>style.xml</code>만 남겨두고 나머지는 모두 지웁니다.</p>
<p>이제 <code>content.xml</code>을 열어 <code>xxxx</code>를 찾아봅니다.
<code>xxxx</code>로 표시한 것은 가변 데이터의 위치를 쉽게 찾기 위해서 입니다.
수 많은 문자열 중 절대 겹치지 않을 것 같은 <code>xxxx</code>로
표시했기 때문에 편집기의 찾기 기능을 잘 이용하면 손쉽게 편집할 수 있습니다.
그리고 <code>xxxx</code>를 찾아서 사용할 변수로 바꿔줍시다.
이 때 변경할 변수라 함은 <a href="https://metacpan.org/module/Template">CPAN의 Template 모듈</a> 모듈에서
사용하는 문법을 이용한 변수이니 자세한 내용은 해당 모듈의 공식 문서를 확인하세요.</p>
<p>앗! 그런데 <code>content.xml</code>을 열어보면 XML 형식인 만큼 바이너리 형식은 아니라서
읽을 수는 있지만 용량을 줄이기 위해 불필요한 빈 칸이 모두 제거되어
알아보기가 너무 힘들어 어떻게 수정해야 할지 막막합니다.
이럴 땐, <a href="https://metacpan.org/module/XML::Tidy">CPAN의 XML::Tidy 모듈</a>을 설치하고
다음 명령을 실행하면 들여쓰기가 잘되어 가독성이 높은 XML 파일로 바뀝니다.</p>
<pre class="brush: bash;">
$ xmltidy content.xml
</pre>
<p>다시 <code>xxxx</code>가 씌여있는 곳을 찾아봅니다.</p>
<p><img src="2011-12-08-2.png" alt="XML에서 xxxx가 있는 위치" id="xmlxxxx" />
<br />
<em>그림 2.</em> XML에서 xxxx가 있는 위치</p>
<p>네, 있습니다. 그럼 이부분의 값만 바꿔주면, 이 안에 넣고 싶은 데로 넣을 수 있겠죠?
이 부분을 템플릿 툴킷 형식인 <code>[% name %]</code>, <code>[% date.y %]</code>, ... 등으로 바꿔줍니다.</p>
<p><img src="2011-12-08-3.png" alt="xxxx 부분을 변환" id="xxxx" />
<br />
<em>그림 3.</em> xxxx 부분을 변환</p>
<p>거의 다왔습니다. 이제는 Perl이 나설 차례입니다.
우선 템플릿을 이용해 ODT를 생성해줄 핵심 모듈인
<a href="https://metacpan.org/module/OpenDocument::Template">CPAN의 OpenDocument::Template 모듈</a>을
이용해서 우리가 원하는 크리스마스 카드를 생성하겠습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use utf8;
use strict;
use warnings;
use DateTime;
use OpenDocument::Template;
my %content = (
name => '이민선',
date => {
'y' => '2011',
'm' => '12',
'd' => '8',
},
);
my $dt = DateTime->now( time_zone => 'Asia/Seoul' );
my $time = sprintf(
'%04d%02d%02d-%02d%02d%02d',
$dt->year, $dt->month, $dt->day,
$dt->hour, $dt->minute, $dt->second,
);
my $template_dir = 'template';
my $src = 'template.odt';
my $dest = "result/$time-$name.odt";
my %config;
my $config{templates}{'content.xml'} = \%content;
my $odt = OpenDocument::Template->new(
config => \%config,
template_dir => $template_dir,
src => $src,
dest => $dest,
);
$odt->generate;
</pre>
<p>레퍼런스를 사용한다는 것을 빼면 코드 자체에 어려운 부분은 없습니다.
결국 핵심은 <code>OpenDocument::Template</code> 객체를 생성하는 부분인데
객체 생성시 각각의 속성이 의미하는 부분을 살펴보면 다음과 같습니다.</p>
<ul>
<li><em>src</em>: 템플릿 원본 ODT 파일을 지정합니다.</li>
<li><em>config</em>: 변경될 내용들을 담고 있는 해시 레퍼런스입니다. 템플릿으로 사용하는 파일의 이름과 사용할 변수 정보가 들어있습니다.</li>
<li><em>template_dir</em>: <code>config</code>에서 템플릿으로 사용할 파일을 저장하는 디렉토리입니다.</li>
<li><em>dest</em>: 자동으로 생성되는 ODT 파일의 경로를 지정합니다.</li>
</ul>
<p>이제 앞의 스크립트를 실행하면 크리스마스카드가 ODT로 생성됩니다.</p>
<p>완성입니다!</p>
<p><img src="2011-12-08-4.png" alt="크리스마스카드" id="" width="700" />
<br />
<em>그림 4.</em> 완성된 크리스마스 카드 <a href="2011-12-08-4.png">(원본)</a></p>
<p>이 코드를 조금 더 발전시키면 표준입력이나 명령행 인자로 변경될 값인
이름과 시간 정보를 받을 수 있도록 수정하면 훨씬 더 수월하게 ODT를
생성할 수 있겠죠? :)</p>
<h2>카탈리스트와 함께 춤을!</h2>
<p>지금까지 만든 내용을 이용해서 웹 응용으로 구현한다면
웹의 특성상 접근성이 대폭적으로 향상되겠죠?
예를 들어, 자주 사용하는 보고서나 문서의 서식을 처음 한 번만 만들어 놓고,
웹에 저장한 다음, 바뀔 부분을 브라우저의 폼을 이용해서 입력하게 한 후
버튼 클릭 한 번으로 미려한 문서가 나온다면... 와우! 생각해도 멋집니다!</p>
<p>그럼 FormNa게 한 번 만들어볼까요?</p>
<p><img src="2011-12-08-5.png" alt="Catalyst 로고" id="catalyst" />
<br />
<em>그림 5.</em> Catalyst 로고</p>
<p><a href="#catalyst">Catalyst</a>는 Perl의 대표적인 웹프레임워크입니다.
워낙 방대한 관계로 여기서 Catalyst의 자세한 부분을 다루지는 않습니다.
대신 다음 자료를 참고한다면 Catalyst 개발을 시작할 때 좋은 지침서가 될 것입니다.</p>
<ul>
<li><a href="http://advent.perl.kr/2010/2010-12-12.html">나의 Catalyst 답사기 - 2010년 Seoul.pm 크리스마스 달력</a></li>
<li><a href="http://jeen.tistory.com/93">Catalyst 를 이용한 웹 서비스 개발 #1</a></li>
<li><a href="https://metacpan.org/module/Catalyst::Manual">CPAN Catalyst::Manual 모듈</a></li>
<li><a href="http://wiki.catalystframework.org/wiki/">Catalyst 공식 위키</a></li>
<li><a href="http://www.catalystframework.org/calendar">Catalyst 크리스마스 달력</a></li>
</ul>
<p>잠깐! Catalyst가 최근 남미에서 <a href="http://www.w3.org/">W3C</a> 주최로 열린
<a href="http://mdk.per.ly/2011/12/06/perl-rocks-latin-america/">공공정보 웹서비스화 경연대회</a>에서
<em>당당하게 우승</em>했다고 합니다.
개발의 신속 정확성을 다투는 대회에서 Rails, Django, CodeIgniter
등의 프레임워크를 제치고 우승했다면 인정해줘야겠죠? :)</p>
<p>지금부터 진행하는 작업은 작업 순서와 상관없지만,
컨트롤러와 뷰, ODT 템플릿 처리 세 부분 모두 완성되어야
FormNa는 서식 파일을 받아볼 수 있습니다.</p>
<h3>컨트롤러</h3>
<p>사용자가 웹에서 입력한 값을 이용해 서식 파일을 만들고자 합니다.
<code>form_create_do</code> 컨트롤러는 <code>form_index.tt</code> 파일에서 넘겨 준 변수를
가져와 <code>content.xml</code>에 넘겨줍니다.</p>
<pre class="brush: perl;">
sub form_create_do :Chained('index') :PathPart('form_create_do') :Args(0) {
my ( $self, $c ) = @_;
my $subject = $c->req->param('subject') || q{};
my $short_comment = $c->req->param('short_comment') || q{};
my $detail_comment = $c->req->param('detail_comment') || q{};
my $picture = $c->req->param('picture') || q{};
my $phone = $c->req->param('phone') || q{};
my $name = $c->req->param('name') || q{};
my %formna_config;
$formna_config{templates}{'content.xml'} = {
subject => $subject,
short_comment => $short_comment,
detail_comment => $detail_comment,
picture => $picture,
phone => $phone,
name => $name,
};
my $time = sprintf(
'%04d%02d%02d-%02d%02d%02d',
$dt->year, $dt->month, $dt->day,
$dt->hour, $dt->minute, $dt->second,
);
my $tpl_dir = sprintf '%s/templates', $c->config->{odt}{root_notice};
my $src = sprintf '%s/template.odt', $c->config->{odt}{root_notice};
my $dst = sprintf '%s/result/%s.odt', $c->config->{odt}{root_notice}, $time;
my $odt = OpenDocument::Template->new(
config => \%formna_config,
template_dir => $tpl_dir,
src => $src,
dest => $dst,
);
$odt->generate;
$c->log->debug('Generated $dst');
$c->res->headers->content_type('application/vnd.oasis.opendocument.text');
$c->res->headers->header( "Content-Disposition" => qq{attachment;filename="$time.odt";} );
my $fh = IO::File->new( $dst, 'r' );
$c->res->body($fh);
undef $fh;
}
</pre>
<p><code>form_create_do</code> 컨트롤러의 가장 아래쪽 다섯 줄의 코드는 생성한 ODT 파일을
다운로드 받을 수 있게 적절한 HTTP 응답 헤더를 만든 후 파일의 내용을
응답 본문에 뿌려줍니다.</p>
<h3>뷰</h3>
<p>값을 넘겨 줄 부분을 다음과 같이 폼을 이용해 사용자의 입력을 받습니다.</p>
<pre class="brush: xml;">
[% meta.title = '전단지 생성' -%]
<form action="[% c.uri_for('form_create_do') %]" method="post" enctype="multipart/form-data">
<div>
<label>제목</label>
<input type="text" name="subject" />
</div>
...(생략)...
</pre>
<h3 id="content.xml">content.xml</h3>
<p>처음 크리스마스 카드를 만들 때 처럼 ODT를 생성한 후
<code>content.xml</code> 파일을 변경합니다.</p>
<pre class="brush: xml;">
<text:p text:style-name="Standard">[% subject %]</text:p>
<text:p text:style-name="Standard">[% short_comment %]</text:p>
<text:p text:style-name="Standard">[% detail_comment %]</text:p>
<text:p text:style-name="Standard">[% picture %]</text:p>
<text:p text:style-name="Standard">[% phone %]</text:p>
</pre>
<h2 id="formna">폼나는 FormNa!</h2>
<p>앞의 같은 작업을 통해 자주 사용하지만, 만들기는 번거로운 몇 가지 틀을 만들었습니다.</p>
<ul>
<li>이력서</li>
<li>문어발 전단지</li>
<li>FTA 국회의원 상장 만들기</li>
</ul>
<p>드디어 완성되었습니다! <a href="http://formna.silex.kr">폼나는 FormNa</a>!</p>
<p><img src="2011-12-08-6.png" alt="폼나는 FormNa" id="formna" width="700" />
<em>그림 6.</em> 폼나는 FormNa <a href="2011-12-08-6.png">(원본)</a></p>
<p>주의할 점은 이렇게 만든 문서는 ODF를 지원하는 도구 중에서
MS Office를 제외하고는 정상적으로 보입니다.
MS Office 최신 버전은 ODF를 지원한다고 하는데 이렇게 자동 생성한 경우
일부만 보여주고 중간에 오류를 출력하면서 렌더링을 멈추는 현상이 있습니다.
뭐... OpenOffice나 LibreOffice를 쓰면 되겠죠? 무료인데 안 쓸 이유가 없잖아요? ;-)</p>
<h2>몇 가지 팁!</h2>
<h3>팁 하나.</h3>
<p><code>xxxx</code>를 넣고 <code>xxxx</code>를 찾아 변수를 바꾸는 작업을 해보면 꽤 귀찮습니다.
틀이 될 문서를 만드는 과정이라면 수도 없이 특정 문자를 변수로 변경해야 하는데
이를 해결하려면 애초에 <code>template.odt</code>를 만들때 변수가 들어가야 하는 곳이라면
처음부터 <code>[% .. %]</code>와 함께 넣으면 작업이 더욱 간편해집니다.</p>
<h3>팁 둘.</h3>
<p>ODT 파일에 <code>뷁뷁샭략</code> 같은 문자가 들어가기도 합니다.
이것은 인코딩 문제로 Catalyst의 설정 파일에서 UTF-8을 설정하면 해결됩니다.</p>
<p><code>TT.pm</code> 모듈에는 <code>ENCODING => 'utf8'</code>을 추가합니다.</p>
<pre class="brush: perl;">
__PACKAGE__->config(
TEMPLATE_EXTENSION => '.tt',
render_die => 1,
ENCODING => 'utf8',
);
</pre>
<p><code>FormNa/Web.pm</code> 모듈에는 유니코드 인코딩 모듈을 추가합니다.</p>
<pre class="brush: perl;">
use Catalyst qw/
-Debug
ConfigLoader
Static::Simple
Unicode::Encoding
/;
</pre>
<p>마지막으로 Catalyst 설정 파일에도 추가합니다.</p>
<pre class="brush: plain;">
<View Default>
ENCODING utf8
</View Default>
</pre>
<h2>정리하며</h2>
<p>이렇게 ODT와 <a href="https://metacpan.org/module/OpenDocument::Template">OpenDocument::Template 모듈</a>을 이용해
ODT를 만들 때의 가장 큰 장점은 <em>일관된 예쁜</em> 보고서를 만들 수 있다는 것입니다.
실제로 회사에서 처리했던 업무가 이와 비슷했습니다.
데이터베이스에 저장한 모 대학병원 연구실의 연구별 예산을 필요할 때마다
즉석에서 ODT 파일을 생성한 다음 PDF로 변환해 사용자가 예산내역서를
다운로드 받을 수 있게 하는 작업이었습니다.
만들고 싶은 보고서 서식을 해당 도구를 적극적으로 이용해서 만들고,
반복문과 조건문 등을 적절히 사용해서 사용자 입력, 또는 데이터베이스의
자료를 잘 배치한다면 일관되면서도 미려한 문서를 마음껏 생성할 수 있습니다.</p>
<h2>후기</h2>
<p>평소에 쭉 해왔던 작업이라 빠른 시간 내에, 조사 및 구현, 기사 작성을
할 수있으리라고 생각했지만 오해였던 것 같습니다.
글을 쓸 수 있게 도와주신 <a href="http://twitter.com/#!/keedi">@keedi</a>님과
<a href="http://twitter.com/#!/y0ngbin">@y0ngbin</a>님께 감사드립니다.
또 막판에, 비루한 웹 UI에 희망을 불어넣어 준
<a href="http://twitter.com/#!/am0c">@am0c</a>군에게도 고마운 마음을 전합니다.
<a href="http://formna.silex.kr">폼나는 FormNa</a> 사이트는 proof-of-concept 단계의 예제에 가깝지만,
사용하시는 분들과 Perl 및 Catalyst를 공부하시려는 분들께 도움이 되길 바래봅니다.</p>
<p>Don't forget <a href="https://github.com/mintegrals/FormNa">fork me on GitHub</a>!! ;-)</p>
2011-12-08T00:00:00+09:00mintegrals윈도우 환경에서 화면 캡쳐 후 자동 저장 기능의 구현http://advent.perl.kr/2011/2011-12-07.html<h2>저자</h2>
<p>@owl0908 -
Perl과는 그닥 어울릴 것 같아보이지 않는, 법학을 전공한 고시 준비생.
10여년 전 개인 홈페이지를 Perl로 구현한 것이 계기가 되어,
슬럼프가 올 때마다 하라는 공부는 안하고 Perl 코딩을 한 결과,
이제는 자신의 정체성의 혼란을 겪고 있다(물론 실력과는 전혀 관계가 없다).
<a href="http://www.dormouse.pe.kr">부엉이의 나무구멍 속 작은 공간</a> 블로그를 운영하고 있다.
webmaster <em>at</em> dormouse.pe.kr</p>
<h2>시작하며</h2>
<p>이것저것 스크린샷이 포함된 글을 쓰실 때, 대개 어떻게 하시나요?
<code>Shift + PrtSc</code> 키를 이용하면 현재 화면을 쉽게 클립보드에 저장할 수 있습니다.
이렇게 클립보드로 저장된 이미지는 어디서든 <code>Ctrl-V</code> 로 붙여넣기를 할 수 있죠.
그러나, 스크린샷이 파일 형태로 필요하다면 어떨까요?
이미지 편집 프로그램을 실행시킨 다음, 붙여넣기를 한 후, 저장을 해야 합니다.
서너장이 아니라 수십 수백장의 화면을 갈무리해야 한다면
일일이 이렇게 저장하는 것은 너무 힘든 일입니다.
스크린샷을 찍으면 자동으로 알아서 파일로 저장까지 해주면 딱 좋을 것 같지 않나요?
자, 시작해보죠. :-)</p>
<h2>준비물</h2>
<p>우선 현재 화면을 갈무리해서, 그것을 특정 파일로 저장하는 기능을 구현해야 합니다.
윈도우즈는 기본적으로 <code>Shift + PrtSc</code> 키를 입력하면 전체화면을 갈무리해서 클립보드에 저장합니다.
이 기능을 이용한다고 했을때 고민해야할 부분은 다음과 같습니다.</p>
<ul>
<li>어떤 행위를 했을때 화면을 갈무리 할 것인지 결정</li>
<li>결정된 행위가 이루어졌을 경우 <code>Shift + PrtSc</code> 키가 입력된 것처럼 동작</li>
<li>클립보드의 내용을 읽어와서 파일로 저장</li>
</ul>
<p>인터페이스 디자인에 해당하는 부분으로 어떤 식으로 컴퓨터에게 명령을 내릴지 정해야 합니다.
스크린샷을 찍을 때마다 스크립트를 실행하는 방법은 불편할 것 같습니다.
가능한 간편하게 사용할 수 있으며 눈에 띄지않을 방법을 생각해보니
시스템 트레이에 아이콘을 상주시켜 놓고 이 아이콘을 클릭할 때마다
화면을 갈무리하는 것이 꽤나 그럴듯해 보입니다.
(믿기 어렵겠지만, 시스템 트레이 아이콘을 이용하는 것은
꽤나 있어보이는 것에 비해 너무 간단합니다. :)</p>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Win32::GUI">CPAN의 Win32::GUI 모듈</a></li>
<li><a href="https://metacpan.org/module/Win32::GuiTest">CPAN의 Win32::GuiTest 모듈</a></li>
<li><a href="https://metacpan.org/module/Win32::HideConsole">CPAN의 Win32::HideConsole 모듈</a></li>
<li><a href="https://metacpan.org/module/Win32::Clipboard">CPAN의 Win32::Clipboard 모듈</a></li>
<li><a href="https://metacpan.org/module/DateTime">CPAN의 DateTime 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Slurp">CPAN의 File::Slurp 모듈</a></li>
</ul>
<p><a href="http://strawberryperl.com">딸기펄</a>을 사용한다면 콘솔에서 <code>cpan</code> 명령을 이용해서 설치합니다.
<a href="http://strawberryperl.com/beta/index.html">딸기펄 5.14</a> 버전에서 필요한 모든 모듈이 정상적으로
설치되는 것을 확인했으므로 버전 선택시 참고하시길 바랍니다.</p>
<pre class="brush: bash;">
> cpan Win32::GUI
> cpan Win32::GuiTest
> cpan Win32::HideConsole
> cpan Win32::Clipboard
> cpan DateTime
> cpan File::Slurp
</pre>
<p><a href="http://www.activestate.com/activeperl">ActiveState사의 ActivePerl</a>을 사용한다면 PPM 관리자를 이용해서 설치하면 됩니다.</p>
<h2>뼈대 만들기</h2>
<p>GUI 구성은 윈도우즈 네이티브 UI 라이브러리인 <code>Win32::GUI</code> 모듈을 사용합니다.
윈도우즈의 시스템 트레이에 아이콘을 상주시키는 것은 <code>Win32::GUI</code> 모듈이 제공하는
<code>AddNotifyIcon</code> 메소드를 사용하면 간단하게 구현할 수 있습니다.
기본적인 뼈대 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Win32::GUI ();
# 사용할 아이콘을 정의합니다.
my $icon = Win32::GUI::Icon->new('icon.ico');
# 기본으로 사용할 창을 정의합니다.
my $main = Win32::GUI::Window->new(
-name => 'Main',
-size => [ 1, 1 ],
);
# 시스템 트레이 아이콘을 정의합니다.
$main->AddNotifyIcon(
-name => "NI",
-icon => $icon,
-tip => "Click to Screenshot!",
-balloon => 0,
-onClick => sub {
# 스크린샷을 찍는 부분을 구현할 예정입니다.
return 0;
},
-onRightClick => sub {
# 트레이에 좀비 아이콘이 그냥 남아있는 것을 방지합니다.
$main->NI->Remove;
# 프로그램 종료
return -1;
},
);
# 창을 숨깁니다.
$main->Hide;
# GUI 메인 루프에 진입합니다.
Win32::GUI::Dialog;
</pre>
<p>작성한 스크립트가 있는 디렉터리와 동일한 위치에 마음에 드는 아이콘을 구한 뒤
<code>icon.ico</code>로 이름을 바꿔서 저장한 후 스크립트를 실행하면
시스템 트레이에 아이콘이 나타나는 것을 볼 수 있습니다.
아이콘에 마우스를 가져가면 <code>Click to Screenshot!</code>이라는 툴팁이 표시될 것입니다.
아이콘 위에서 마우스 왼쪽 버튼을 누르면 <code>onClick</code> 이벤트가 발생하며
오른쪽 버튼을 누르면 <code>onRightClick</code> 이벤트가 발생합니다.
오른쪽 버튼을 누를 경우 연결된 함수 레퍼런스가 <code>-1</code>을 반환하므로 프로그램이 종료됩니다.</p>
<p><em>그림 1.</em> 실행 후 시스템 트레이에 아이콘이 적재된 화면 <a href="2011-12-07-1.png">(원본)</a>
<img src="2011-12-07-1.png" alt="실행 후 시스템 트레이에 아이콘이 적재된 화면" id="" width="700" /></p>
<p>이제 스크린샷을 찍을 부분을 구현해야겠죠.
<code>PrtSc</code> 명령은 <a href="https://metacpan.org/module/Win32::GuiTest">CPAN의 Win32::GuiTest 모듈</a>의 <code>SendKeys</code> 함수를 이용하면 됩니다.
<code>SendKeys</code> 함수는 문자 그대로 특정한 키 입력을 컴퓨터에게 던져주는 역할을 합니다.
<code>onClick</code> 이벤트 발생시 실행시키는 함수 레퍼런스에 다음 내용을 추가합니다.</p>
<pre class="brush: perl;">
...
use Win32::GUI ();
use Win32::GuiTest qw(SendKeys); # 추가된 부분
...
$main->AddNotifyIcon(
-name => "NI",
-icon => $icon,
-tip => "Click to Screenshot!",
-balloon => 0,
-onClick => sub {
# 스크린샷을 찍습니다.
SendKeys( "+{PRTSCR}" ); # 추가된 부분
return 0;
},
...
);
</pre>
<p><code>{PRTSCR}</code> 앞의 <code>+</code>는 <code>Shift</code>를 같이 입력한다는 의미입니다.
참고로 <code>^</code>은 <code>Ctrl</code>, <code>%</code>는 <code>Alt</code>를 의미합니다.
여러 키 조합과 딜레이 타임을 옵션으로 지정할 수도 있기 때문에,
<code>SendKeys</code>와 <code>Win32::GUI</code> 모듈의 타이머 기능을 결합하면 여러가지로 활용이 가능합니다.
예를 들자면, 특정 온라인 게임에 대응하는 매크로도 만들 수 있겠죠. ;-)</p>
<h2>클립보드 이미지를 파일로 저장</h2>
<p>이제 클립보드로 들어간 이미지를 저장해야겠죠?
<a href="https://metacpan.org/module/Win32::Clipboard">CPAN의 Win32::Clipboard 모듈</a>을 사용하면
간편하게 클립보드 이미지를 불러올 수 있습니다.
불러오고 싶다면 다음처럼 단 두 줄만 작성하면 됩니다.</p>
<pre class="brush: perl;">
use Win32::Clipboard;
my $bitmap = Win32::Clipboard::GetBitmap;
</pre>
<p>정말 끝내주지 않나요? :-D</p>
<p>클립보드에 저장된 비트맵을 불러오는 것이므로 클립보드에 있는 것이
이미지인지 텍스트인지 확인할 필요도 없을 것 같습니다.
<code>$bitmap</code> 스칼라 변수에 비트맵 이미지가 고스란히 저장되기 때문에
<code>.bmp</code> 파일에 써주기만 하면 파일 저장도 끝납니다.
파일명을 일일이 사용자에게 물어보는 것도 번거로운 일이니까
현재 날짜과 시간을 사용해서 자동으로 저장하도록 하겠습니다.
<code>onClick</code> 이벤트에 대한 함수 레퍼런스는 이렇게 바뀝니다.</p>
<pre class="brush: perl;">
...
use Win32::GUI ();
use Win32::GuiTest qw(SendKeys);
use Win32::Clipboard;
use DateTime;
use File::Slurp
...
$main->AddNotifyIcon(
-name => "NI",
-icon => $icon,
-tip => "Click to Screenshot!",
-balloon => 0,
-onClick => sub {
# 스크린샷을 찍습니다.
SendKeys( "+{PRTSCR}" );
# 스크린샷을 저장합니다.
my $dt = DateTime->now( time_zone => 'Asia/Seoul' );
my $filename = sprintf('%s %s.bmp', $dt->ymd, $dt->hms);
my $bitmap = Win32::Clipboard::GetBitmap;
write_file( $filename, {binmode => ':raw'}, $bitmap ) ;
return 0;
},
...
);
</pre>
<p><em>그림 2.</em> 화면 갈무리 결과 <a href="2011-12-07-2.png">(원본)</a>
<img src="2011-12-07-2.png" alt="화면 갈무리 결과" id="" width="700" /></p>
<h2>용량 줄이기</h2>
<p>일단 프로그램 작성은 끝났습니다.
다만 실행해보면 갈무리되는 파일의 크기가 무척 크다는 것을 알 수 있습니다.
비트맵 형식은 압축을 하지 않기 때문에 일반적인 HD 해상도의 파일은 약 3MB,
그리고 그 이상의 해상도의 화면을 사용한다면 5MB는 가볍게 훌쩍 넘깁니다.
본격적으로 대량 캡쳐를 하려고 생각했는데, 이러면 좀 곤란하겠죠.
BMP 대신 PNG 파일 포맷을 사용해서 용량 문제를 해결하도록 하겠습니다.</p>
<p>BMP를 PNG로 변환하는 방법은 다양합니다.
CPAN의 <a href="https://metacpan.org/module/GD">GD 모듈</a>, <a href="https://metacpan.org/module/Image::Magick">Image::Magick</a>,
<a href="https://metacpan.org/module/Imager">Imager</a>는 물론이고 <code>Win32::GUI</code> 모듈에 딸려 나오는
[Win32::GUI::DIBitmap][cpan-win32-gui-dbbitmap] 모듈을 사용하는 등
여러가지 방법이 있습니다.
다만 각각의 모듈은 모두 윈도우즈에 소소한 문제들이 있습니다.
<code>GD</code> 모듈의 경우 BMP 포맷을 지원하지 않으며,
<code>Image::Magick</code>는 따로 바이너리 라이브러리를 설치한 후 CPAN 모듈을
설치해야 하므로 초보자의 경우 접근이 어렵습니다.
또한 <code>Imager</code>는 BMP를 지원하지면 결정적으로 윈도우즈에서
캡쳐할때 생성되는 BMP 형식(만)을 지원하지 않습니다.
마지막으로 <code>Win32::GUI::DIBitmap</code> 모듈의 경우 ActivePerl의 경우 문제없이
사용이 가능하나 딸기펄 사용자의 경우 컴파일러 호환 문제로 설치되지 않아
사용할 수가 없습니다.</p>
<p>이 문제를 해결하는 가장 간단한 방법은 BMP를 PNG로 변환해주는 가벼운 유틸리티를
구해서 BMP 파일을 생성한 다음 바로 실행시켜 변환시키는 것입니다.
<a href="http://www.libpng.org/pub/png/pngcode.html">PNG 라이브러리 공식 홈페이지</a>에는 변환툴로서 가벼운
<a href="http://cetus.sakura.ne.jp/softlab/b2p-home/">bmp2png</a>를 소개하고 있습니다.
bmp2png는 단 하나의 실행파일로 구성되어 있으며 윈도우즈 명령줄에서
인자로 BMP 파일을 넘기면 확장자만 바꿔서 PNG 파일을 생성해줍니다.
bmp2png를 다운로드 받아서 압축을 푼 다음에 <code>BMP2PNG.EXE</code> 파일을 스크립트가
있는 경로에 두거나 또는 <code>PATH</code> 환경변수로 관리되고 있는 디렉터리에 두세요.</p>
<p>새롭게 갱신한 코드 조각은 다음과 같을 것입니다.</p>
<pre class="brush: perl;">
$main->AddNotifyIcon(
...
-onClick => sub {
# 스크린샷을 찍습니다.
SendKeys( "+{PRTSCR}" );
# 스크린샷을 저장합니다.
my $dt = DateTime->now( time_zone => 'Asia/Seoul' );
my $bitmap = Win32::Clipboard::GetBitmap;
my $filename = sprintf(
'%4d%02d%02d-%02d%02d%02d.bmp',
$dt->year, $dt->month, $dt->day,
$dt->hour, $dt->minute, $dt->second,
);
write_file($filename, { binmode => ':raw' }, $bitmap);
# BMP 형식을 PNG로 변환합니다.
system('BMP2PNG.EXE', $filename);
unlink($filename);
return 0;
},
...
);
</pre>
<p><em>그림 2.</em> PNG로 변환한 캡처 화면 <a href="2011-12-07-3.png">(원본)</a>
<img src="2011-12-07-3.png" alt="PNG로 변환한 캡처 화면" id="png" width="700" /></p>
<h2>보기 싫은 콘솔 창을 없애자</h2>
<p>윈도우즈 환경에서 Perl 스크립트를 실행하면 기본적으로
항상 나타나는 까만 콘솔 창은 아무래도 좀 부담스럽습니다.
이제 이 콘솔 창을 없애 봅시다.
<a href="https://metacpan.org/module/Par::Packer">CPAN의 PAR::Packer 모듈</a>을 사용하면
실행 바이너리를 만드는 시점에 옵션을 이용해 콘솔 창을 보이지 않게 할 수 있습니다.
<code>PAR::Packer</code>를 이용해서 바이너리로 만들지 않더라도
콘솔 창을 없애려면 <a href="https://metacpan.org/module/Win32::HideConsole">CPAN의 Win32::HideConsole 모듈</a>을
이용(<a href="https://metacpan.org/module/Win32::Console">CPAN의 Win32::Console 모듈</a>도 한번 확인해보세요) 합니다.</p>
<p>스크립트에는 다음 코드를 추가합니다.</p>
<pre class="brush: perl;">
use Win32::HideConsole;
hide_console;
</pre>
<p>실행하면 항상 보이던 콘솔 화면이 잠깐 나타났다가 이내 사라지는 것을 확인할 수 있습니다.</p>
<h2>트레이 아이콘을 프로그램 안에 내장하고 싶어요!</h2>
<p>현재 구현한 스크립트대로라면 <code>icon.ico</code> 파일을
항상 스크립트와 함께 들고 다녀야 합니다.
이것은 꽤나 번거로운 일이기 때문에 아예 <code>icon.ico</code> 파일을
코드 안에 내장시키는 것이 더 좋을 것 같습니다.
<code>Win32::GUI::BitmapInline</code> 모듈은 BASE64 형식으로 인코딩된
데이터를 비트맵 형식으로 변환해서 받아들이기 때문에
이 모듈을 사용하면 스크립트 내부에 아이콘을
문자열 형식으로 저장할 수 있습니다.
<code>Win32::GUI</code> 모듈을 설치했다면 바로 사용이 가능합니다.</p>
<p>우선 사용할 아이콘을 BASE64 형식으로 인코딩해야 합니다.
이것은 콘솔 창에서 단 한 줄의 코드로 알아낼 수 있습니다.</p>
<pre class="brush: bash;">
> perl -MFile::Slurp -MMIME::Base64 -E "print encode_base64(read_file(shift))" icon.ico
</pre>
<p>실행을 시키면 콘솔창에 다음처럼 복잡한 문자열이 출력될 것입니다.</p>
<pre class="brush: plain;">
> perl -MFile::Slurp -MMIME::Base64 -E "print encode_base64(read_file(shift))" icon.ico
AAABAAMAEBAQAAEABAAoAQAANgAAABAQAAABAAgAaAUAAF4BAAAQEAAAAQAgAGgEAADGBgAAKAAA
ABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAP///wB7AAAAAHsAAHt7AAAAAHsA
ewB7AAB7ewC9vb0Ae3t7AP8AAAAA/wAA//8AAAAA/wD/AP8AAP//AAAAAAD////////////////3
/4////////+P/////////3//////////j/////////+P/////////49///////////h/////////
//////j////4/////4//////////+P//f///j/j/j//////////4////////////////////////
////nlz7t3Qg/Zt4Zf2bIHT9W3Mg/VtuIP0TdXPwAXBy4ANsZeADIHfkB24g5gdld8MPZyD/n2Ug
//9vbv//biAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAA////AP7+/gDW
1tYAqqqqAP39/QB5eXkAAAAAAHp6egD8/PwAQUFBAHV1dQCEhIQAHh4eAMvLywC3t7cAAwMDAIiI
iAACAgIAgYGBAPb29gAgICAAtra2AIqKigAEBAQAGhoaAOfn5wAODg4AODg4AGtrawAYGBgAsrKy
AMHBwQAVFRYAFxcXAAsLCwABAQEAHx8fAAoKCgBwcHAAm5ubAOnp6AB4eHgAERERACMjIwBEREQA
4uLiAKampgBlZWUAampqALS0tACvr68Az8/PAAgICABsbGwAj4+PALGxsQC/v78A8fHxAFhYWACa
mpoA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/
//8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/
//8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/
//8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAYGBgYGBgYGBgYG
BgYGBgYGBgYGBgYGBgYNBgY6BgYGBgYGBgYGBgYGBhwGBgYGBgYGBgYGBgYGBgYDBgYGBgYGBgYG
BgYGBgYGHAYGBgYGBgYGBgYGBgYGBhwGBgYGBgYGBgYGBgYGDBQJBgUGBgYGBgYGDAwGBgYGBhQG
BScGBgYGBgYGBgYGBgYGBgwGBgYGBgYGGwYGBgYGBgYcBgYGBgYGFAYLBgYGBgYMBgYGBgYGBgYG
BgsGBgYGCwYGBgYGCQYGBQYGCwYGDAYGBgYGBgYGBgYGBgYFBgYGBgYGBgYGBgYGBgYGBgYGBgYG
BgYGBgYGBgYGBgYGBgYGBgb//55c+7d0IP2beGX9myB0/VtzIP1bbiD9E3Vz8AFwcuADbGXgAyB3
5AduIOYHZXfDD2cg/59lIP//b27//24gKAAAABAAAAAgAAAAAQAgAAAAAABABAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAABwAAAAAQAAAE6/v79AAAAA
DgAAAABYWFinAAAAZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAA/wAAAAEAAAAB
CAgI92xsbJMAAAAAAAAAAQAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAP8A
AAABAAAASwAAAP+vr69QAAAAAAAAAAEAAAD/AAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAEAAAD/AAAAAQAAAP8AAAAAampqlQAAAAEAAAABAAAA/wAAAAEAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAABAAAA/wAAAAEAAAD/AAAAWWVlZZoAAAABAAAAAQAAAP8AAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAFwAAAP8AAACHERER7iMjI9xERES7AAAAAXp6eoUAAAD/AAAAHQAAAAAA
AAAAAAAAAAAAAAEAAAA+FRUW6hcXF+gCAgL9CwsL9AEBAf4AAAD/AgIC/R8fH+AKCgr1cHBwj5ub
m2QAAAAAAAAAAAAAAAAAAAABAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA
/xgYGOcAAABNAAAAAAAAAAAAAAAAAAAAAQAAAP8ODg7xODg4xwAAAP8AAAD/AAAA/wAAAP8AAAD/
AAAA/wAAAP9ra2uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ICAg3wAAAEmKiop1BAQE+wAAAP8A
AAD/AAAA/wAAAP8aGhrlAAAAGAAAAAAAAAAAAAAAAAAAAAAAAABIAAAA/wMDA/wAAACKAAAAAYiI
iHcCAgL9AAAA/wAAAP8AAAD/gYGBfgAAAAkAAAAAAAAAAAAAAAAAAAAAQUFBvgAAAP8AAAD/dXV1
igAAAAAAAAAAhISEewAAAP8AAAD/Hh4e4QAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAp
AAAAVQAAAAEAAAAAAAAAAAAAAAJ5eXmGAAAA/wAAAIUAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAD//55c+7d0IP2beGX9myB0/VtzIP1bbiD9E3Vz8AFwcuADbGXgAyB35AduIOYHZXfDD2cg
/59lIP//b27//24g
</pre>
<p>화면에 출력되는 문자열을 복사한 다음 우리의 스크립트에 붙여넣어 보죠.</p>
<pre class="brush: perl;">
# 사용할 아이콘을 정의합니다.
my $icon = Win32::GUI::BitmapInline->newIcon( q(
AAABAAMAEBAQAAEABAAoAQAANgAAABAQAAABAAgAaAUAAF4BAAAQEAAAAQAgAGgEAADGBgAAKAAA
ABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAP///wB7AAAAAHsAAHt7AAAAAHsA
ewB7AAB7ewC9vb0Ae3t7AP8AAAAA/wAA//8AAAAA/wD/AP8AAP//AAAAAAD////////////////3
...
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAD//55c+7d0IP2beGX9myB0/VtzIP1bbiD9E3Vz8AFwcuADbGXgAyB35AduIOYHZXfDD2cg
/59lIP//b27//24g
) );
</pre>
<p>아무래도 이제 파일 하나로 구성되었기 때문에 배포할 때도 훨씬 편리하겠죠?</p>
<h2>락앤롤!</h2>
<p>지금까지 작성한 스크립트의 전체 코드는 다음과 같습니다.
화면 갈무리 후 저장하는 파일 형식은 PNG입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Win32::GUI ();
use Win32::GUI::BitmapInline;
use Win32::GuiTest qw(SendKeys);
use Win32::Clipboard;
use Win32::HideConsole;
use DateTime;
use File::Slurp;
# 콘솔을 보이지 않게 합니다.
hide_console;
# 사용할 아이콘을 정의합니다.
my $icon = Win32::GUI::BitmapInline->newIcon( q(
AAABAAMAEBAQAAEABAAoAQAANgAAABAQAAABAAgAaAUAAF4BAAAQEAAAAQAgAGgEAADGBgAAKAAA
ABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAP///wB7AAAAAHsAAHt7AAAAAHsA
ewB7AAB7ewC9vb0Ae3t7AP8AAAAA/wAA//8AAAAA/wD/AP8AAP//AAAAAAD////////////////3
/4////////+P/////////3//////////j/////////+P/////////49///////////h/////////
//////j////4/////4//////////+P//f///j/j/j//////////4////////////////////////
////nlz7t3Qg/Zt4Zf2bIHT9W3Mg/VtuIP0TdXPwAXBy4ANsZeADIHfkB24g5gdld8MPZyD/n2Ug
//9vbv//biAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAA////AP7+/gDW
1tYAqqqqAP39/QB5eXkAAAAAAHp6egD8/PwAQUFBAHV1dQCEhIQAHh4eAMvLywC3t7cAAwMDAIiI
iAACAgIAgYGBAPb29gAgICAAtra2AIqKigAEBAQAGhoaAOfn5wAODg4AODg4AGtrawAYGBgAsrKy
AMHBwQAVFRYAFxcXAAsLCwABAQEAHx8fAAoKCgBwcHAAm5ubAOnp6AB4eHgAERERACMjIwBEREQA
4uLiAKampgBlZWUAampqALS0tACvr68Az8/PAAgICABsbGwAj4+PALGxsQC/v78A8fHxAFhYWACa
mpoA////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/
//8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/
//8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD/
//8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAYGBgYGBgYGBgYG
BgYGBgYGBgYGBgYGBgYNBgY6BgYGBgYGBgYGBgYGBhwGBgYGBgYGBgYGBgYGBgYDBgYGBgYGBgYG
BgYGBgYGHAYGBgYGBgYGBgYGBgYGBhwGBgYGBgYGBgYGBgYGDBQJBgUGBgYGBgYGDAwGBgYGBhQG
BScGBgYGBgYGBgYGBgYGBgwGBgYGBgYGGwYGBgYGBgYcBgYGBgYGFAYLBgYGBgYMBgYGBgYGBgYG
BgsGBgYGCwYGBgYGCQYGBQYGCwYGDAYGBgYGBgYGBgYGBgYFBgYGBgYGBgYGBgYGBgYGBgYGBgYG
BgYGBgYGBgYGBgYGBgYGBgb//55c+7d0IP2beGX9myB0/VtzIP1bbiD9E3Vz8AFwcuADbGXgAyB3
5AduIOYHZXfDD2cg/59lIP//b27//24gKAAAABAAAAAgAAAAAQAgAAAAAABABAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAABwAAAAAQAAAE6/v79AAAAA
DgAAAABYWFinAAAAZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAA/wAAAAEAAAAB
CAgI92xsbJMAAAAAAAAAAQAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAP8A
AAABAAAASwAAAP+vr69QAAAAAAAAAAEAAAD/AAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAEAAAD/AAAAAQAAAP8AAAAAampqlQAAAAEAAAABAAAA/wAAAAEAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAABAAAA/wAAAAEAAAD/AAAAWWVlZZoAAAABAAAAAQAAAP8AAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAFwAAAP8AAACHERER7iMjI9xERES7AAAAAXp6eoUAAAD/AAAAHQAAAAAA
AAAAAAAAAAAAAAEAAAA+FRUW6hcXF+gCAgL9CwsL9AEBAf4AAAD/AgIC/R8fH+AKCgr1cHBwj5ub
m2QAAAAAAAAAAAAAAAAAAAABAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA
/xgYGOcAAABNAAAAAAAAAAAAAAAAAAAAAQAAAP8ODg7xODg4xwAAAP8AAAD/AAAA/wAAAP8AAAD/
AAAA/wAAAP9ra2uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ICAg3wAAAEmKiop1BAQE+wAAAP8A
AAD/AAAA/wAAAP8aGhrlAAAAGAAAAAAAAAAAAAAAAAAAAAAAAABIAAAA/wMDA/wAAACKAAAAAYiI
iHcCAgL9AAAA/wAAAP8AAAD/gYGBfgAAAAkAAAAAAAAAAAAAAAAAAAAAQUFBvgAAAP8AAAD/dXV1
igAAAAAAAAAAhISEewAAAP8AAAD/Hh4e4QAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAp
AAAAVQAAAAEAAAAAAAAAAAAAAAJ5eXmGAAAA/wAAAIUAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAD//55c+7d0IP2beGX9myB0/VtzIP1bbiD9E3Vz8AFwcuADbGXgAyB35AduIOYHZXfDD2cg
/59lIP//b27//24g
) );
# 기본으로 사용할 창을 정의합니다.
my $main = Win32::GUI::Window->new(
-name => 'Main',
-size => [ 1, 1 ],
);
# 시스템 트레이 아이콘을 정의합니다.
$main->AddNotifyIcon(
-name => "NI",
-icon => $icon,
-tip => "Click to Screenshot!",
-balloon => 0,
-onClick => sub {
# 스크린샷을 찍습니다.
SendKeys( "+{PRTSCR}" );
# 스크린샷을 저장합니다.
my $dt = DateTime->now( time_zone => 'Asia/Seoul' );
my $bitmap = Win32::Clipboard::GetBitmap;
my $filename = sprintf(
'%4d%02d%02d-%02d%02d%02d.bmp',
$dt->year, $dt->month, $dt->day,
$dt->hour, $dt->minute, $dt->second,
);
write_file($filename, { binmode => ':raw' }, $bitmap);
# BMP 형식을 PNG로 변환합니다.
system('BMP2PNG.EXE', $filename);
unlink($filename);
return 0;
},
-onRightClick => sub {
# 트레이에 좀비 아이콘이 그냥 남아있는 것을 방지합니다.
$main->NI->Remove;
# 프로그램 종료
return -1;
},
);
# 창을 숨깁니다.
$main->Hide;
# GUI 메인 루프에 진입합니다.
Win32::GUI::Dialog;
</pre>
<h2>정리하며</h2>
<p>글쓴이 소개란을 읽으셨을지 모르겠지만, 전 전산/컴퓨터와는 전혀 관계 없는 법학 전공자거든요. (^^)
MS 윈도우 환경에서 약간의 Perl 지식만 있다면 Perl을 이용해 자신이 필요한 기능을
자신이 직접 설계해서 사용할 수 있다는 사실은 정말 매력적입니다.
모듈을 사용한 것은 온전한 제 지식이 아니라구요?
뭐 그럼 어떤가요? 어쨌든 만들고 싶었던 것을 만들어냈잖아요!
이미 다른 사람들이 만들어 놓은 수많은 CPAN 모듈들을 활용하면,
혼자 힘으로는 역부족이라고 생각했던 부분은 해낼 수 있게,
가능한 부분은 더욱 빠른 시간내에 구현할 수 있게 도와줍니다.
어떤가요? 매력적이지 않나요?
이미 Perl을 훌륭하게 사용하고 계신 많은 분들이 있지만,
저처럼 전산 전공이 아닌, 일반적 지식만을 갖춘 <em>잠재적 Perl 이용자</em>께 이 글을 바칩니다.
화이팅! ;-)</p>
<h2>생각해볼 거리</h2>
<p>지금 프로그램은 파일을 저장하는 경로가 정해져있습니다.
실행하는 시점에 인자로 넘긴다던가, 또는 설정파일을 참고하도록 수정할 수도 있겠지요.
또한 트레이 아이콘에서 오른쪽 버튼을 눌렀을때 바로 종료가 되는 것이 아니라
팝업 메뉴를 띄운 후 설정을 변경하거나 종료하는 옵션을 추가할 수도 있을 것입니다.
나머지는 여러분의 상상력에 맡기겠습니다.</p>
<p>마지막으로 이 프로그램에 단축키 기능을 추가한다면 어떨까요?
이를테면 윈도우 환경 어디서나 CTRL-F12 를 클릭하면 이 기능이 동작하도록 하는 것입니다.
윈도우용 Perl은 <a href="https://metacpan.org/module/Win32::API">Win32::API</a> 모듈을 이용해 윈도우 기본 API를 직접 사용할 수 있기 때문에,
<code>user32.dll</code>의 <code>RegisterHotKey/UnregisterHotKey</code>를 임포트해 적당한 키 조합을 등록하고
키보드 입력을 전역으로 모니터링(후킹)하면, 단축키를 이용한 스크린샷의 조작이 가능합니다.
단축키 방식을 도입할 경우 지금까지 사용한 전체 화면 스크린캡쳐 외에도,
<code>Alt + PrtSc</code>를 사용해 현재 활성화 된 창만을 갈무리 할 수 있습니다.
지금처럼 아이콘을 클릭하는 방식이라면 아이콘을 클릭하는 순간 활성창이
작업표시줄로 바뀌기 때문에, 항상 작업 표시줄만 갈무리 될 것입니다.
기사에서 사용한 모든 화면은 단축키까지 구현한 완전한 스크립트를 이용해서 갈무리한 것입니다.
<code>PAR::Packer</code>로 빌드한 <a href="http://www.nightowl.pe.kr/software/prtscrsave">실행 바이너리와 전체 소스</a>도
꼭 다운로드 받아서 확인해보세요. ;-)</p>
2011-12-07T00:00:00+09:00owl0908초소형 프레임워크와 함께 춤을http://advent.perl.kr/2011/2011-12-06.html<h2>저자</h2>
<p><a href="http://twitter.com/am0c">@am0c</a> -
단것과 귀여운 것을 좋아하는
올해 크리스마스 달력의 주편집자. 하지만 웹디자인에 더 열중했다는 비하인드 스토리.
결국 <a href="http://twitter.com/keedi">@keedi</a>님의 도움을 받고 있다.
최근 안드로이드와 리눅스 커널에 관심을 돌리고 있으나
<a href="http://facebook.com/am0c.yn">페이스북</a>의 재미에 빠져 허우적대고 있다.</p>
<h2>시작하며</h2>
<p>웹사이트 개발 패러다임도 계속 변하고 있습니다.</p>
<p>CGI가 쌘 놈이었던 때도 있었다고 합니다.
물론 CGI는 이미 흘러간 물입니다. 몇 년 전에는 철도 위에 놓인 루비가 붐이었죠.
지금은 웹 클라우딩뇨인지 클라우드인지 하는 플랫폼 서비스 사업이 일어나고 있습니다.</p>
<p>저는 웹 개발자도 아니고 HTML5를 어서 만져보고 싶어하는 덕후도 아니기 때문에
이런 패러다임에 대해 시시각각 논하지는 못합니다만,
취미로 만든 서비스가 완성되면 보기 좋기 운영하거나 외부에 공개허거나 인터페이스를 제공하고 싶게 되고,
그러다보면 어쨌든 웹 서비스 개발에 직면하게 됩니다.</p>
<h2>무엇으로 만들까</h2>
<p>생각해보니 올해 부산에서 열린 지스타에 가지 못했습니다.
현장에서 찍은 사진들을 인터넷에서 구해 아름답게 정리해보고 싶습니다.
부스별로 정리하거나 색상별로 정리하면 즐거울 것 같군요.</p>
<p>일일이 받아서 폴더에 전부 쑤셔넣는 것은 짜증나는 일입니다.
지금은 정말 간단하게 테스트 페이지만 만들고 나중에 다양한 카테고리의
사진들도 관리하도록 확장하고 싶습니다.
아무래도 지금이 바로 웹 서비스가 필요한 상황이군요. 그런데 무엇으로 만드는 것이 가장 적합할까요.</p>
<p>Perl에는 아주 좋은 웹 프레임워크가 있습니다. 바로 <a href="http://www.catalystframework.org/">Catalyst</a>라고 불리는 놈입니다.
참고로 카탈리스트는 <a href="http://www.catalystframework.org/calendar">크리스마스 달력</a>을 진행하고 있습니다. 어쨌든
이것만 있으면 아주 세밀하고도 거대하게 웹을 개발할 수 있고, 대량의 플러그인까지 존재합니다.</p>
<p>그런데 작은 블로그나 웹 페이지 하나 만드는데 이만한 것이 필요한 것은 아닙니다.
그렇다고 정해진 틀에만 구애받아야 하는 PHP의 <em>CodeIgniter</em>는 사용하고 싶지 않습니다.
PHP 페이지 몇개로 include를 나열하는 것은 더 소름끼치는 일입니다.
맙소사, 이 상황에 <em>Java</em>를 사용하라고 하진 않겠죠?</p>
<p>그러니까 너무 간단한 나머지 불필요한 타이핑은 존재하지 않으며, 동시에 유연하고 확장 가능한 것을 원하는 겁니다.
물론 의존하고 있는 라이브러리가 너무 많은 것도 원하지 않습니다. 웹 프레임워크의 설정을 편집하느라 허송세월하고 싶지도 않습니다.
그런건 아무래도 없을테니 올해 크리스마스도 TV와 함께 보내야 하겠군요. 그렇게 슬퍼하고 있을 때,</p>
<p>놀랍게도 그런게 있었다는 겁니다. 바로 <a href="http://perldancer.org/">댄서</a>입니다.</p>
<pre class="brush: perl;">
use Dancer;
get '/' => sub {
"I can code in Christmas!!"
};
dance;
</pre>
<h2>세상에 이런것이!</h2>
<p>위의 코드는 완전한 코드입니다. 즉 웹브라우저에 <code>/</code>로 요청을 하면 <code>I can code in Christmas!!</code>가 출력됩니다.
이보다 간단할 수는 없습니다.</p>
<p>각 라우트 핸들러는 기본적으로 사용자 함수(<code>sub</code>)입니다. GET 요청을 위해 <code>get</code>을 POST 요청을 위해 <code>post</code>를 연결합니다.</p>
<pre class="brush: perl;">
get '/admin' => sub { }
post '/send' => sub { }
</pre>
<p>이렇게 간단하게 특정 정적 경로에 대한 핸들러를 기술할 수 있습니다.
물론 패턴 매치를 수행하고 파라미터로 받을 수 있습니다.</p>
<pre class="brush: perl;">
get '/post/id/:id' => sub {
my $id = params->{id};
}
</pre>
<p>정말 간단하죠? 요청을 통해 들어온 쿼리나 메시지도 이렇게 받아낼 수 있습니다.
또 아래와 같이 정적인 파일을 전달할 수도 있습니다.</p>
<pre class="brush: perl;">
get '/download/:file' => sub {
my $file = params->{file};
send_file $file;
}
</pre>
<p>헤더를 변경합니다.</p>
<pre class="brush: perl;">
get '/' => sub {
content_type 'text/plain';
...
}
</pre>
<p>리다이렉트도 간단합니다.</p>
<pre class="brush: perl;">
get '/admin' => sub {
redirect '/login';
}
</pre>
<p>탬플렛을 지정합니다.</p>
<pre class="brush: perl;">
get '/welcome' => sub {
template 'welcome', { user => guest' };
}
</pre>
<p>로그를 기록합니다.</p>
<pre class="brush: perl;">
get '/error' => sub {
debug 'TODO or not TODO';
}
</pre>
<p>와우! 깔끔해요. :)</p>
<h2>만들어 봅시다</h2>
<p>지금까지는 신택스를 보았습니다. 이번에는 실제로 웹 사이트를 만들어 봅시다.
웹에서 많이 사진을 끌어오고 쉽게 관리하는 서비스를 만들어 보겠습니다.
웹 어플리케이션 이름은 <em>YouPerl</em>이라고 지어보았습니다.
CPAN 도구를 통해 Dancer를 설치한 후 아래와 같이 명령을 내리면
YouPerl이라는 웹 어플리케이션을 위한 파일 뼈대들이 생성됩니다.</p>
<pre class="brush: bash;">
$ dancer -a YouPerl
$ cd YouPerl
</pre>
<p><code>find</code>를 입력하여 전체 구조를 살펴봅시다. 아래는 파일을 어느정도 생략한 모습입니다.</p>
<pre class="brush: bash;">
$ find
./views
./bin
./bin/app.pl
./t
./public
./environments
./lib
./lib/YouPerl.pm
</pre>
<p><code>/views</code> 디렉터리에는 탬플릿이 위치합니다. <code>/bin</code>에는 실행도구들이 있습니다. <code>/t</code>디렉터리에는
테스트 스크립트가 모여 있습니다. <code>/public</code> 디렉터리 이하의 파일들은 정적 파일로 외부에 노출됩니다.
기본적인 설정은 <code>/config.yml</code>에 있지만 <code>/environments</code> 디렉터리 밑에 개발버전과 정식버전으로 나눈 설정을
따로 보관합니다. <code>/lib</code> 이하에 웹 어플리케이션의 로직 코드가 위치합니다.</p>
<p>아래와 같이 입력하고 웹부라우저를 통해 <em>http://localhost:3000/</em>으로 접근해봅시다.</p>
<pre class="brush: bash;">
$ bin/app.pl
</pre>
<p><img src="2011-12-06-1.png" alt="이미 완성?" id="" /></p>
<p>이미 완성된 것 같은 페이지가 뜹니다.
여기에 나타난 링크를 따라가 강좌와 문서를 읽어봅시다.</p>
<h2>재료를 준비합니다</h2>
<p>먼저 웹을 통해 지스타 이미지를 긁어 데이터베이스에 기록합니다.
데이터베이스는 <a href="http://redis.io/">Redis</a>를 사용하겠습니다.
<a href="https://metacpan.org/module/Dancer::Plugin::Redis">Dancer::Plugin::Redis</a>를 받습니다.</p>
<pre class="brush: bash;">
$ cpanm Dancer::Plugin::Redis
</pre>
<p>Redis 플러그인은 설정 몇줄만 넣으면 아래와 같이 redis 키워드를 통해 접근할 수 있습니다.</p>
<pre class="brush: perl;">
use Dancer;
use Dancer::Plugin::Redis;
get '/widget/view/:id' => sub {
template 'display_widget', { widget => redis->get('hash_key'); };
};
dance;
</pre>
<p>설정은 <code>/config.yml</code>을 열고 마지막에 다음 네 줄을 입력하면 됩니다.</p>
<pre class="brush: plain;">
plugins:
Redis:
server: 127.0.0.1:6379
debug: 0
</pre>
<p>아래 구글을 통해 검색해보니 지스타 이미지를 모은 24개의 게시물이 보입니다.
<a href="http://metacpan.org/module/Web::Scraper">Web::Scraper</a>나 <a href="http://metacpan.org/module/Web::Query">Web::Query</a>를 사용해도 되지만,
24개의 게시물의 경로를 구하기 위한 정규표현식
한 개와 각 게시물에서 이미지를 긁어올 정규표현식 한 개만 있으면
모든 이미지를 간단히 긁어올 수 있겠습니다.</p>
<p><img src="2011-12-06-2.png" alt="발견" id="" /></p>
<pre class="brush: perl;">
my $grep_srclist = qr|<a href="([^"]+)".{1,30}지스타 2011 부스걸 사진 보기|si;
my $grep_imglist = qr|http://p.playforum.net/[^"]+|si;
</pre>
<p>긁은 이미지의 파일명을 서로 겹치지 않게 하기 위해 MD5 알고리즘을 이용하고, 이것을 데이터베이스에서 각 파일의
식별자로 사용하겠습니다. <a href="http://metacpan.org/module/GD::Thumbnail">GD::Thumbnail</a>을 사용하면 쉽게 썸네일을 생성할 수도 있습니다. <code>bin/grep_gstar.pl</code>으로 저장합니다..</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use 5.010;
use Dancer qw();
use Dancer::Plugin::Redis;
use Digest::MD5 qw(md5_hex);
use GD::Thumbnail;
use File::Basename;
use LWP::Simple;
## 시작 전 데이터베이스 항목 초기화
redis->setnx("youperl:img:cat.gstar:page", 0);
## 긁을 페이지 목록을 구하는 구하는 정규표현식과
## 각 페이지에서 이미지를 긁을 정규표현식
my $grep_srclist = qr|<a href="([^"]+)".{1,30}지스타 2011 부스걸 사진 보기|si;
my $grep_imglist = qr|http://p.playforum.net/[^"]+|si;
## 일단 첫번째 페이지를 가져와요!
my $first_page = 'http://www.playforum.net/www/newsDirectory/-/id/1047955';
my $src_list = get $first_page;
## 페이지 목록을 긁어요!
my @urls = $src_list =~ m/$grep_srclist/g;
my @imgs;
## 각 페이지를 순회하면서 이미지 목록을 긁어요!
for my $url ($first_page, @urls) {
say " <- $url";
my $src_imgs = get $url;
push @imgs, $src_imgs =~ m/$grep_imglist/g;
}
## 각 이미지를 처리합니다.
for my $img (@imgs) {
say $img;
my ($name, $path, $suffix) = fileparse $img, qr/\.[a-z]+/i;
my $hex = md5_hex "youperl_$path$name";
my $fn = "public/img/$hex$suffix";
my $thumb = "public/img/thumb/$hex$suffix";
my $i;
## 유일한 파일명을 구해요.
while (-e $fn) {
++$i;
$fn = sprintf "public/img/${hex}_%d$suffix", $i;
$thumb = sprintf "public/img/thumb/${hex}_%d$suffix", $i;
}
$hex = "${hex}_$i" if defined $i;
## 이미지를 저장하구요.
say " -> $fn";
getstore $img, $fn unless -e $fn;
## 작은 파일은 필요 없습니다.
if (-s $fn < 1024 * 8) {
say " xx $fn";
unlink $fn;
next;
}
## 썸네일도 생성합시다.
say " -> $thumb";
my $t = GD::Thumbnail->new;
my $raw = $t->create($fn, 140, 0);
open my $fh, '>', $thumb or die;
binmode $fh;
print $fh $raw;
say " -o $fn";
## 데이터베이스에 400개씩 기록합니다.
my $page = redis->get("youperl:img:cat.gstar:page");
my $size = redis->llen("youperl:img:cat.gstar:$page");
if ($size >= 400) {
$page = redis->incr("youperl:img:cat.gstar:page");
}
redis->lpush("youperl:img:cat.gstar:$page", $hex);
}
</pre>
<p>긁어온 모습입니다.</p>
<p><img src="2011-12-06-3.png" alt="잔뜩" id="" /></p>
<p>심심하지 않은 웹서비스를 만들기 위해 각 이미지를 색상별로 분류해 봅니다. 역시 데이터베이스에 기록합니다. <code>bin/calc_gstar.pl</code>으로 저장합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use Dancer qw();
use Dancer::Plugin::Redis;
use File::Basename;
use GD;
## 마음대로 골라본 색상 샘플 목록
my %sample = (
gray => [177, 177, 177],
black => [0, 0, 0 ],
red => [255, 0, 0 ],
magenta => [255, 0, 255],
blue => [0, 0, 255],
cyan => [0, 255, 255],
green => [0, 255, 0 ],
yellow => [255, 255, 0 ],
white => [255, 255, 255],
ocean => [125, 148, 183],
grass => [125, 183, 133],
sky => [125, 183, 174],
flower => [183, 125, 181],
stone => [183, 174, 125],
wood => [183, 125, 125],
);
my $page = redis->get("youperl:img:cat.gstar:page");
for my $p (0 .. $page) {
my @items = redis->lrange("youperl:img:cat.gstar:$p", 0, -1);
for my $item (@items) {
my ($img) = glob "public/img/$item.*";
my $color_name = sampling($img);
redis->sadd("youperl:img:cat.gstar.color:$p:$color_name", $item);
}
}
## 가장 가까운 색을 고르자
##
sub sampling {
my $file = shift;
return unless -f $file;
my ($hex, $path, $suffix) = fileparse $file, qr/\.[a-z]+/i;
my %dist;
my $image = new GD::Image($file);
my $color = new GD::Image(1, 1);
## 이미지 평균 색상값을 구한다.
##
$color->copyResampled(
$image,
(0, 0), (0, 0),
(1, 1), ($image->width, $image->height),
);
my $index = $color->getPixel(0, 0);
## 샘플 색상과 각각 비교해본다
##
for my $name (keys %sample) {
$dist{$name} = rgb_dist( $sample{$name}, [$color->rgb($index)] );
}
## 오름차순으로 정렬한다
##
my @sort = sort {
$dist{$a} <=> $dist{$b}
} keys %sample;
print "$hex:\t $sort[0]\t ";
print "$_(", int $dist{$_}, ") " for @sort;
print "\n";
return $sort[0];
}
sub rgb2xyz {
my ($r, $g, $b) = @_;
my ($x, $y, $z);
$r = $r / 255;
$g = $g / 255;
$b = $b / 255;
for $c ($r, $g, $b) {
if ($c > 0.04045) {
$c = ($c + 0.055) / 1.055;
$c = $c ** 2.4;
}
else {
$c = $c / 12.92;
}
$c = $c * 100;
}
$x = $r * 0.4124 + $g * 0.3576 + $b * 0.1805;
$y = $r * 0.2126 + $g * 0.7152 + $b * 0.0722;
$z = $r * 0.0193 + $g * 0.1192 + $b * 0.9505;
return $x, $y, $z;
}
sub xyz_dist {
my ($l, $r) = @_;
my ($x1, $y1, $z1) = @$l;
my ($x2, $y2, $z2) = @$r;
my $t = ($x1 - $x2) ** 2 + ($y1 - $y2) ** 2 + ($z1 - $z2) ** 2;
return sqrt $t;
}
sub rgb_dist {
my ($l, $r) = @_;
return xyz_dist [rgb2xyz @$l], [rgb2xyz @$r];
}
</pre>
<p>색상을 대표하는 값은 GD의 <code>copyResampled</code> 함수를 사용하면 됩니다. "Perl Hacks" 책의 44번 항목을 참고했습니다.
색상별로 분류하는 방법은 다음의 문서를 참고했습니다.</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Color_difference">Wikipedia for Color Difference</a></li>
<li><a href="http://www.emanueleferonato.com/2009/08/28/color-differences-algorithm/">Color Difference Algorithm</a></li>
</ul>
<h2>래퍼 만들기</h2>
<p>래퍼 파일(<code>/views/layouts/main.tt</code>)을 적절하게 수정합니다.
컨테이너 부분이 <code>[% content %]</code>로 되어 있습니다. 각 링크를 클릭하면
선택한 분류에 따라 컨테이너 안에 이미지가 다르게 출력되도록 할 것입니다.
색상 목록을 전역 변수로 빼놓는 것이 더 좋겠습니다.
지금은 그대로 두겠습니다.</p>
<pre class="brush: plain;">
<div id="menu">
<ul>
<li> <a href="/category/gstar">지스타</a>
<li> <a href="/category/gstar/color/gray">gray</a>
<li> <a href="/category/gstar/color/black">black</a>
<li> <a href="/category/gstar/color/red">red</a>
<li> <a href="/category/gstar/color/magenta">magenta</a>
<li> <a href="/category/gstar/color/blue">blue</a>
<li> <a href="/category/gstar/color/cyan">cyan</a>
<li> <a href="/category/gstar/color/green">green</a>
<li> <a href="/category/gstar/color/yellow">yellow</a>
<li> <a href="/category/gstar/color/white">white</a>
<li> <a href="/category/gstar/color/ocean">ocean</a>
<li> <a href="/category/gstar/color/grass">grass</a>
<li> <a href="/category/gstar/color/sky">sky</a>
<li> <a href="/category/gstar/color/flower">flower</a>
<li> <a href="/category/gstar/color/stone">stone</a>
<li> <a href="/category/gstar/color/wood">wood</a>
</ul>
</div>
<div id="cont">
[% content %]
</div>
</pre>
<h2>로직 작성하기</h2>
<p>먼저 기본 페이지를 만들어야 겠군요. 기본 요청은 이것으로 그대로 둡시다.</p>
<pre class="brush: perl;">
get '/' => sub {
template 'index';
};
</pre>
<p><code>index.tt</code>는 간단한 문구로 완성합니다.</p>
<pre class="brush: plain;">
I can code in Christmas! :)
</pre>
<p><code>/category/gstar</code>와 같이 카테고리를 부여하면 특정 카테고리에 해당하는
이미지를 모두 반환하도록 합니다. 템플릿으로 반환합니다.</p>
<pre class="brush: perl;">
get '/category/:category' => sub {
my $name = param 'category';
my @items = redis->lrange("youperl:img:cat.$name:0", 0, -1);
my $files = items2files @items;
template 'images', { images => $files };
};
</pre>
<p>래퍼는 이미 만들었으므로 이미지 템플릿은 단순히 이미지를 나열하도록 합시다.</p>
<pre class="brush: plain;">
[% FOREACH file IN images %]
<a href="/img/[% file %]"><img src="/img/thumb/[% file %]" /></a>
[% END %]
</pre>
<p>지금은 탬플릿 언어로 <a href="http://template-toolkit.org/">Template Toolkit</a>을 사용하고 있습니다. Dancer의 기본
탬플릿 언어는 <a href="http://metacpan.org/module/Dancer::Template::Simple">Dancer::Template::Simple</a>입니다.
탬플릿 언어를 바꾸려면 <code>config.yml</code>을 수정하면 됩니다.
개인적으로 <a href="http://xslate.org/">Xslate</a>이 짱입니다. </p>
<p>여기까지 완성하면 아래와 같이 사진이 잔뜩 올라옵니다.</p>
<p><img src="2011-12-06-4.png" alt="이렇게요" id="" /></p>
<p><code>/category/gstar/color/red</code>와 같이 특정 색상을 필터하도록 요청해오면
특정 색상에 대한 항목만 반환하도록 합니다. 간단하죠?</p>
<pre class="brush: perl;">
get '/category/*/**' => sub {
my ($category) = splat;
var category => $category;
pass;
};
prefix '/category/:category';
get '/color/:color' => sub {
my ($color) = param 'color';
my $category = vars->{category};
my @items = redis->smembers("youperl:img:cat.$category.color:0:$color");
my $files = items2files @items;
template 'images', { images => $files, color => $color };
};
</pre>
<p>이것으로 완성입니다.
색상에 따라 필터링도 할 수 있게 되었습니다.</p>
<ul>
<li>빨간색:</li>
</ul>
<p><img src="2011-12-06-5.png" alt="빨강" id="" /></p>
<ul>
<li>파란색:</li>
</ul>
<p><img src="2011-12-06-6.png" alt="파랑" id="" /></p>
<p>조금만 고치면 살색 사진만 모을 수도 있겠네요!
(라고 Y님이 뒤에서 조언해주셨습니다.)</p>
<h2>정리하며</h2>
<p>배치 스크립트 탓에 예제가 조금 길어졌지만, 이렇게 간단한 서비스를 완성해 보았습니다.</p>
<p>물론 이것이 전부는 아닙니다.
<em>다양한 데이터베이스와 세션 및 인증 모듈</em>을 CPAN에서 얻을 수 있습니다.
<a href="http://metacpan.org/module/Task::Dancer">Task::Dancer</a>를 설치하거나 이것의 펄독 문서를 참고하면
좋은 모듈을 찾는데 수월합니다.
Dancer의 <em>플러그인들은 모두 초간단 설정을 통해 동작하며 간단한 신택스를 통해 접근</em>할 수 있습니다.</p>
<p>Dancer는 <em>PSGI</em>와 <em>Plack</em>을 지원합니다. 이것으로 여러분은 Dancer와 서버 사이에
통합적인 게이트웨이를 둘 수 있습니다. 이것이 있기 때문에 <a href="http://dotcloud.com/">DotCloud</a>나 <a href="http://www.activestate.com/cloud">Stakato</a>와 같은
클라우드 플랫폼 서비스에 Dancer 어플리케이션을 쉽게 deploy할 수 있습니다.
다른 환경으로 쉽게 이주할 수도 있을 것입니다.</p>
<p>댄서도 올해 <a href="http://advent.perldancer.org/2011">크리스마스 달력</a>을 진행하고 있습니다.
오늘 예제를 만들면서 댄서에서는 카탈리스트와 같이 라우트 핸들러를 체인으로 구성하는 것이
쉽지 않다는 것을 느꼈습니다. 이 부분에 대해 논의하면 좋겠습니다.</p>
<p>아래 첨부한 참고 링크에서 슬라이드는 꼭 읽어보세요. Dancer를 쉽게 이해하는데 도움이 됩니다.
또 <a href="http://mojolicio.us/">Mojolicious</a>라는 매력적인 웹 프레임워크도 있으니 한 번 둘러보세요.
마지막으로 필요하신 분들을 위해 이번 기사의 코드도 올려놓았습니다.</p>
<p>Let's Dance!</p>
<ul>
<li><a href="http://plackperl.org/">PSGI/Plack</a></li>
<li><a href="http://perldancer.org/">perldancer.org</a></li>
<li><a href="http://perldancer.org/dancefloor">Nice Slides</a></li>
<li><a href="http://perldancer.org/slides">Cool Dancers</a></li>
<li><a href="https://github.com/am0c/seoulpm-advent-calendar-youperl">YouPerl at github</a></li>
</ul>
2011-12-06T00:00:00+09:00am0cNCBI PubMed와 Perlhttp://advent.perl.kr/2011/2011-12-05.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/ascendox">@ascendox</a> -
서울시 중구를 지역 기반으로 하는 유전학 전공자.
현재 동국대학교 식물생명공학과 박사 과정으로 분자유전학실험실 유전육종학을 전공하고 있다.
출중한 외모로 인해 <em>중구 얼짱</em>이라는 별칭을 획득.
<a href="http://twitter.com/#!/keedi">@keedi</a>와 기고 여부를 걸고 다트 내기를 했으나 현실은 기고.
<a href="http://sites.google.com/site/ascendou/">프로필</a></p>
<h2>시작하며</h2>
<p><a href="#ncbi">NCBI</a>는 <em>National Center for Biotechnology Information</em>의 약자로
생명 과학 및 의학 관련 학술 포탈 사이트(<em>그림 1</em>)입니다.
NCBI는 미국 보건성 산하의 국립 의학 도서관의 운영 분야중 하나로,
생명정보학 전담부서로 컴퓨터를 이용하여 DB를 구축하고
분석도구를 개발하는 것이 주요 임무입니다.
흔히 <a href="http://blast.ncbi.nlm.nih.gov/Blast.cgi">BLAST</a>로 알려져 있는 이 사이트는
의학 생명 과학 논문 인덱스 데이터 베이스와 유전체 서열 데이터베이스 등
각종 생명 공학 정보들을 담고 있으며 이와 관련있는 분석도구를 보유하고 있습니다.</p>
<p><img src="2011-12-05-1.png" alt="NCBI 홈페이지" id="ncbi" width="700" />
<em>그림 1.</em> NCBI 홈페이지 <a href="2011-12-05-1.png">(원본)</a></p>
<p>또한 생물학적으로 중요한 분자의 구조와 기능을 분석하기 위한
컴퓨터 정보 처리 기술 연구와 수학적, 전산학적 방법을 사용한
생물학 및 의학적 문제의 분자 수준에서의 연구, 분자생물학, 생화학,
유전학에 대한 지식을 저장, 분석하기 위한 자동화 시스템 개발,
DB와 S/W 개발, 생명공학 기술 정보 수집, 연구소, 학회, 산업체,
정부 기관 등과의 협력, 과학적 정보교환 강화, 전산생물학의 기초 및 응용 연구 훈련 지원,
다양한 데이터베이스와 소프트웨어의 사용 지원, 데이터베이스 데이터 축적 및 교환,
생물학적 명명법의 표준 개발 등의 활동을 수행하고 있습니다.
나열한 이 모든 정보는 <a href="http://www.ncbi.nlm.nih.gov/Entrez/">Entrez</a>라는 포탈 검색 엔진을 이용해 검색할 수 있습니다.</p>
<h2>대학원생의 삶이란?</h2>
<p>흔히 노예... 아니 대학원생의 삶을 살다보면 <em>"내일 까지 뭐 해놔~"</em>와 같은
지극히 일상적인 상황에 직면하게 됩니다.
다음은 한국에서 평범한 대학원생이 직면(당)할 수도 있는 아름다운 상황의 예입니다.</p>
<ul>
<li><p>상황 하나.</p>
<p>교수님께서 내일까지 특정 키워드에 대한 논문을 찾아 놓으라고 합니다.
하지만 이럴 경우는 짬밥에 따라 차이가 있지만
보통 거인의 어깨에서 불편한 검색을 제공하는
<a href="http://www.google.com">구글 검색</a>이나 <a href="http://scholar.google.com">구글 학술 검색</a>을 사용합니다.
구글 검색이나 구글 학술 검색은 키워드로 검색하기 편리하지만
검색 후 그 링크를 언제 다 일일이 찍어서 초록을 뽑아낼 수 있을까요?</p></li>
<li><p>상황 둘.</p>
<p>논문 주제를 선정하려고 합니다.
보통 석사 이상의 경우는 자신이 연구한 분야를 집중적으로 혹은
연계하여 논문 주제 혹은 연구 주제를 선정하는 것이 일반적이지만
그럴 여건이 되지 못하는 경우(저처럼 학식이 부족한 경우?)엔
다른 분들의 논문을 보고 정하는 경우가 대부분입니다.
제목과 키워드로 PubMed와 구글에서 찾다가는 시간은 시간대로 가고,
여자(또는 남자) 친구는 여러분을 떠날 것입니다(네... 떠나갔습니다;;).</p></li>
</ul>
<p>자 이제 <em>'언제까지 이렇게 살아야하는가? 무언가 획기적인 방법은 없을까?'</em>와
같은 근원적이고 존재론적인 의문이 생길 것입니다.
사실 이 방법을 고수하는 한 <em>'해결 방법 따윈 없다!'</em>랄까요?
획기적이기까지는 아니더라도 좀 더 편한 방법을 이용해
논문을 검색하고 정리 할 수 있다면 어떨까요?</p>
<p>자! 이제 <em>Perl</em>이 출동해야 할 때입니다. ;-)</p>
<h2 id="pubmed">PubMed 검색 결과</h2>
<p>NCBI는 <a href="http://www.ncbi.nlm.nih.gov/books/NBK25500/">E-utility</a>를 제공하고 있습니다.
E-utility를 이용하면 NCBI의 검색엔진인 Entrez에서
<em>programmatic access</em>을 사용할 수 있습니다.
E-utillity 내의 E-search를 이용해서 결과에 대한 검색한 후,
E-fetch를 이용해서 결과를 출력합니다.
이 작업은 NCBI의 모든 데이터베이스에 대해서 처리할 수 있습니다.</p>
<p>검색한 모든 논문의 전체 내용을 일일이 다 읽어보는 것은 시간적으로도 무리입니다.
사실 그렇게 꼼꼼(!)하게 인생을 살고 있지 않기 때문에 주로 초록(abstract)만
읽곤하죠(<em>"논문 쓰기의 시작이자 끝은 초록 쓰기"</em>라는 말이 괜히 나온 것은 아닙니다).</p>
<p>하지만 설명한 방식대로 키워드를 넣고 검색을 하면
저자와 날짜, 제목만을 결과로 보여줍니다.
<a href="#ncbi">NCBI 홈페이지</a>에서 <code>rice</code>라는 키워드로 검색을 시도한 다음
Entrez 검색 결과에서 PubMed 카테고리를 선택했을 때
결과 화면은 다음과 같습니다.</p>
<p><img src="2011-12-05-2.png" alt="NCBI PubMed에서 키워드 rice로 검색한 결과" id="ncbipubmedrice" width="700" />
<em>그림 2.</em> NCBI PubMed에서 키워드 rice로 검색한 결과 <a href="2011-12-05-2.png">(원본)</a></p>
<h2>삶의 질을 높이려면?</h2>
<p>이렇게 검색한 결과를 클릭해서 초록을 확인하고 정리하면 원하는 바를 이룰수는 있습니다.
하지만 검색 결과가 많을 경우 이렇게 정리하려면 매우 많은 시간이 걸릴 것입니다.
우리의 삶의 질을 높이기 위해 Perl을 이용해서
E-utility의 E-search 기능을 적극적으로 활용해보죠.
PubMed에서 science 저널에 2008년에 나온 유방암 논문을 찾는다면
E-utillity의 E-search를 다음처럼 사용합니다.</p>
<pre class="brush: plain;">
# Example:
# Get the PubMed IDs (PMIDs) for articles
# about breast cancer published in Science in 2008
http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi
?db=pubmed
&term=science[journal]+AND+breast+cancer+AND+2008[pdat]
</pre>
<p>검색 결과는 XML이지만 E-fetch를 이용하면
원하는 PubMed 결과 자료를 좀 더 보기 쉽게 정리할 수 있습니다.</p>
<pre class="brush: plain;">
Input:
List of UIDs (&id);
Entrez database (&db);
Retrieval type (&rettype);
Retrieval mode (&retmode)
Output:
Formatted data records as specified
http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi
?db=<database>
&id=<uid_list>
&rettype=<retrieval_type>
&retmode=<retrieval_mode>
</pre>
<p>다음 두 링크는 NCBI에서 제공하는 E-utility의 예제입니다.
역시 바이오 분야에서는 Perl을 쓸때 얼마나 편한지를 다시 한번 확인시켜 주는 듯합니다.</p>
<ul>
<li><a href="http://eutils.ncbi.nlm.nih.gov/entrez/query/static/eutils_example.pl">http://eutils.ncbi.nlm.nih.gov/entrez/query/static/eutils_example.pl</a></li>
<li><a href="http://eutils.ncbi.nlm.nih.gov/entrez/query/static/epost.pl">http://eutils.ncbi.nlm.nih.gov/entrez/query/static/epost.pl</a></li>
</ul>
<p>또한 NCBI 는 E-utility의 파이프라인을 Perl로 만들수 있도록
<a href="http://www.ncbi.nlm.nih.gov/Class/PowerTools/eutils/ebot/ebot.cgi">Ebot</a>이란 사이트를 제공합니다.
지금까지 설명한 E-fetch, E-search, XML를 모르더라도
Ebot 사이트를 이용하면 15초 만에 Perl 스크립트를 자동으로 만들어줍니다.
물론 뱀이나 빨간돌 등을 만들어 주는 기능 따윈(!) 없습니다! ;-)</p>
<h2>전체 코드</h2>
<p>E-search에서 E-fetch를 거쳐서 나온 결과를 재구성하는
전체 Perl 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use DateTime;
use FindBin qw( $Script );
use Getopt::Long;
use LWP::Simple;
# ---------------------------------------------------------------------------
# Parse command line options
my %opts = (
'author' => q{},
'journal' => q{},
'min' => 1999,
'max' => DateTime->now->year,
'query' => q{},
'limit' => 500,
'pdf' => 0,
'tab' => 0,
'help' => 0,
);
GetOptions(
\%opts,
'author|a=s',
'journal|j=s',
'min=i',
'max=i',
'query|q=s',
'limit|l=i',
'pdf!',
'tab!',
'help|h',
) or die "Can't parse options\n";
die usage() if $opts{help};
# ---------------------------------------------------------------------------
# Define library for the 'get' function used in the next section.
# $db, $query, and $report may be supplied by the user when prompted;
# if not answered, default values, will be assigned as shown below.
my $db = "Pubmed";
my $report = "medline";
my $author = ask_user( "Author", $opts{author} );
my $journal = ask_user( "journal_name", $opts{journal} );
my $query = ask_user( "query", $opts{query} );
my $mindate = ask_user( "mindate", $opts{min} );
my $maxdate = ask_user( "maxdate", $opts{max} );
# ---------------------------------------------------------------------------
# $esearch_result containts the result of the ESearch call
# the results are displayed parsed into variables
# $count, $query_key, and $web_env for later use and then displayed.
$author .= "[FAU]" if $author;
$journal .= "[TA]" if $journal;
my $esearch = make_url(
'http://www.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi',
db => $db,
retmax => 1,
usehistory => 'y',
mindate => $mindate,
maxdate => $maxdate,
datetype => 'pdat',
term => $author . $journal . $query,
);
my $esearch_result = get($esearch);
$esearch_result =~ m|<Count>(\d+)</Count>.*<QueryKey>(\d+)</QueryKey>.*<WebEnv>(\S+)</WebEnv>|s;
my $count = $1;
my $query_key = $2;
my $web_env = $3;
warn "Count = $count; query_key = $query_key; WebEnv = $web_env\n";
if ( $count > $opts{limit} ) {
warn "Result count is over $opts{limit}, limitting $opts{limit}\n";
$count = $opts{limit};
}
# ---------------------------------------------------------------------------
# this area defines a loop which will display $retmax citation results from
# Efetch each time the the Enter Key is pressed, after a prompt.
my %medlines;
my $retmax = 20;
for ( my $i = 0; $i < $count; $i += $retmax ) {
my $efetch = make_url(
'http://www.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi',
retmode => 'medline',
rettype => $report,
retstart => $i,
retmax => $retmax,
db => $db,
query_key => $query_key,
WebEnv => $web_env,
);
warn "EF_QUERY=$efetch\n";
my $efetch_result = get($efetch);
$efetch_result =~ m|<pre>(.+)</pre>|s;
$efetch_result = $1;
my %medlines_part;
%medlines_part = parse_medline($efetch_result);
for my $key ( keys %medlines_part ) {
$medlines{$key} = $medlines_part{$key};
}
}
# ---------------------------------------------------------------------------
# Print search result
if ( $opts{pdf} ) {
for my $pmid ( keys %medlines ) {
my $medline = $medlines{$pmid};
my $url_pdf_fetch = make_url(
'http://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi',
dbfrom => 'pubmed',
id => $pmid,
retmode => 'ref',
cmd => 'prlinks',
tool => 'pdfetch',
);
printf "[%s] %s\n", $pmid, join( q{}, @{ $medline->{TI} } );
say $url_pdf_fetch;
}
}
elsif ( $opts{tab} ) {
for my $pmid ( keys %medlines ) {
my $medline = $medlines{$pmid};
my $result = join( "\t",
$pmid,
join( q{ }, @{ $medline->{TI} } ),
join( q{;}, @{ $medline->{AU} } ),
join( q{}, @{ $medline->{DP} } ),
join( q{ }, @{ $medline->{TA} } ),
join( q{ }, @{ $medline->{AB} } ),
);
say $result;
}
}
else {
for my $pmid ( keys %medlines ) {
my $medline = $medlines{$pmid};
printf "[%s] %s\n", $pmid, join( q{}, @{ $medline->{TI} } );
}
}
# ---------------------------------------------------------------------------
# Make URL with parameter
sub make_url {
my ( $base, %params ) = @_;
return sprintf(
'%s?%s',
$base,
join('&', map { "$_=$params{$_}" } keys %params),
);
}
# ---------------------------------------------------------------------------
# Subroutine to parse medline format
sub parse_medline {
my ($raw) = @_;
my %records;
my @raw = split /[\r\n]/, $raw;
my $cur_pmid;
my $cur_key;
for my $line (@raw) {
my $value;
if ( $line =~ m{^([A-Z]{2,4}) *- (.+)$} ) {
$cur_key = $1;
$value = $2;
}
else {
$value = $line;
$value =~ s/^\s+//;
$value =~ s/\s+$//;
}
next unless $value;
if ( $cur_key eq 'PMID' ) {
$cur_pmid = $value;
$records{$cur_pmid}->{PMID} = $value;
}
else {
push @{ $records{$cur_pmid}->{$cur_key} }, $value;
}
}
return %records;
}
# ---------------------------------------------------------------------------
# Subroutine to prompt user for variables in the next section
sub ask_user {
my ( $label, $default ) = @_;
print STDERR "$label [$default]: ";
my $value = <>;
chomp $value;
return $value ? $value : $default;
}
# ---------------------------------------------------------------------------
# Usage string
sub usage {
return <<"HELP"
Usage
-----
$Script <options>
Options
-------
--author|-a Default value of Full Author name of search for
--journal|-j Default value of Journal name of search for
--min Default value of Min year of search from. Default is 1999
--max Default value of Max year of search from. Default is today
--query|-q Default value of keyword of search for
--limit|-l Search limit count. Default is 500
--pdf Retrieve pdf info
--tab Print tab separated values format
--help|h Print help message
Examples
--------
$Script -author 'Cheol Seong Jang' -tab
$Script -q rice -tab
Author
------
Won Cheol Yim
HELP
}
</pre>
<p>다음 명령을 통해 사용법을 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ ./pubmed.pl --help
Usage
-----
esearch.pl <options>
Options
-------
--author|-a Default value of Full Author name of search for
--journal|-j Default value of Journal name of search for
--min Default value of Min year of search from. Default is 1999
--max Default value of Max year of search from. Default is today
--query|-q Default value of keyword of search for
--limit|-l Search limit count. Default is 500
--pdf Retrieve pdf info
--tab Print tab separated values format
--help|h Print help message
Examples
--------
esearch.pl -author 'Cheol Seong Jang' -tab
esearch.pl -q rice -tab
Author
------
Oleg Khovayko (PubMed original source)
Won Cheol Yim (derived from original source)
</pre>
<p>처음에 예를 들었던 쌀을 키워드로 검색하려면 다음처럼 실행합니다.</p>
<pre class="brush: bash;">
$ ./pubmed.pl -q rice
[22116964] Assessing Point-of-Care Hemoglobin Measurement: Be Careful We Don't Biaswith Bias.
[22130962] Impact of the SRC inhibitor dasatinib on the metastatic phenotype of humanprostate cancer cells.
[22121634] Synthesis and characterization of high-purity silica nanosphere from ricehusk.
[22056144] Diversity and toxigenicity among members of the Bacillus cereus group.
...
</pre>
<p>제가 평소 흠모(?)해 마지않는 장철성 교수님(강원대학교 식물자원응용공학과)께서
쓰신 논문의 정보를 탭문자를 구분자로 해서 <code>result</code> 파일에 저장하려면
다음처럼 실행합니다.</p>
<pre class="brush: bash;">
$ ./pubmed.pl -a 'Cheol Seong Jang' -tab > result
</pre>
<p>두 명의 저자를 검색하고 싶다면 <code>AND</code> 연산자를 사용합니다.</p>
<pre class="brush: bash;">
$ ./pubmed.pl -a 'Cheol Seong Jang AND Andrew Paterson' -tab > result
</pre>
<p>결과물은 탭 구분자를 이용했기 때문에
놀랍게도 생명 정보학의 친구(?) 엑셀에서 아주 잘 보입니다!
200여줄 남짓한 코드인만큼 코드 분석은 여러분에게 맡기겠습니다. :-)</p>
<p><img src="2011-12-05-3.png" alt="검색 결과를 엑셀에서 열람한 화면" id="" width="700" />
<em>그림 3.</em> 검색 결과를 엑셀에서 열람한 화면(PMID, 제목, 저자, 날짜 저널, 초록) <a href="2011-13-05-3.png">(원본)</a></p>
<h2>정리하며</h2>
<p>기사에서 설명한 간단한 스크립트가 얼마나 많은 도움을 줄지는 모르겠습니다.
하지만 적어도 앞에서 열거한 상황이라던가, 연구를 위해 급히 많은 양의
초록을 읽어야 한다면 큰 도움이 될 것입니다.
또한 결과값인 연도를 잘 활용하면 특정 키워드의 논문 경향을
분석할 수도 있어 보고서나 RFP를 작성할 때도 유용할 것입니다.
E-utility의 경우는 <a href="http://www.ncbi.nlm.nih.gov/geo">GEO(Gene Expression Omnibus)</a>,
<a href="http://www.ncbi.nlm.nih.gov/sra">SRA(Short Read Archive)</a>에서도 사용해 대량의 데이터를 다운 받을 수 있습니다.
실제로 전 GEO의 microarray 데이터 받을때 사용하고 있답니다. ;-)</p>
<h2>후기</h2>
<p>처음에는 <a href="http://blast.ncbi.nlm.nih.gov/Blast.cgi">netBLAST</a>를 다루려고 했다가
<a href="http://www.ncbi.nlm.nih.gov/Class/PowerTools/eutils/ebot/ebot.cgi">Ebot</a>과 생명정보학 데이터베이스의 지존인 <a href="#ncbi">NCBI</a>에서 사용하는
Perl을 소개할 좋은 기회라서 특별히 <a href="http://www.ncbi.nlm.nih.gov/books/NBK25500/">NCBI E-utility</a>를 선정했습니다.
<a href="http://advent.perl.kr">Seoul.pm 크리스마스 달력</a>에 글을 쓸 수 있는 기회를 주신 <a href="http://twitter.com/#!/am0c">@am0c</a>님과
다트 내기에서 놀라운 실력으로 빠져나가지 못하게 한 <a href="http://twitter.com/#!/keedi">@keedi</a>님께 감사드리며
미쿡에서 스크립트를 리팩터링 해주신 사랑하는 <a href="http://twitter.com/#!/yuni_kim">@yuni_kim</a>,
사랑하는 <a href="http://twitter.com/#!/aanoaa">@aanoaa</a>, 항상 저만 괴롭히는 <a href="http://twitter.com/#!/jeen_lee">@JEEN_LEE</a>님께
고마움을 전합니다.</p>
2011-12-05T00:00:00+09:00ascendoThrift 이리저리 둘러보기http://advent.perl.kr/2011/2011-12-04.html<h2>저자</h2>
<p><a href="http://twitter.com/dalinaum">@dalinaum</a> -
안드로이드 가이. 커피를 좋아하는 지나친 감성주의자.
Lindyhop dancer, Hacker, Perl, LISP, emacs 마니아.
최근에는 안드로이드의 3D 그래픽(렌더스크립트, OpenGL ES)에 심취해 있다.
<a href="http://dalinaum-kr.tumblr.com/">Art of Dalinaum</a>을 운영하고 있다.</p>
<h2>시작하며</h2>
<p>많은 애플리케이션은 프레젠테이션 레이어와 핵심 로직의 언어를 분리하고 있습니다. 트위터의 경우에는 핵심 로직을 스칼라로 처리하며 프레젠테이션 부분을 루비 언어로 처리합니다. 안드로이드 플랫폼은 프레젠테이션 레이어를 자바와 XML 파일로 구성하며 성능이 중요한 부분은 C와 C++ 언어를 사용하고 있습니다. 앞으로 소프트웨어 개발에 더 다양한 언어의 조합이 등장할 것을 어렵지 않게 예상할 수 있습니다. 펄 언어 만으로 되어 있는 단일한 환경에서는 모듈과 함수 호출로 대부분의 일을 해결할 수 있습니다. 하지만 기존의 방법으로 다양한 언어를 사용한 다채로운 개발 환경을 구성하는 것은 쉽지 않습니다.</p>
<p><a href="http://thrift.apache.org/">Thrift</a>는 페이스북이 개발한 규모 가변적인(scalable) 이종 언어 서비스 개발을 위한 소프트웨어 프레임워크입니다. 이 프레임워크는 2007년에 페이스북에 의해 개발되기 시작하여 2008년 이후 아파치 재단이 유지 보수를 맡고 있습니다. Thrift는 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Javascript, Node.js, Smalltalk, OCalm 언어 등 다양한 환경을 지원하고 있습니다. 우리가 사용하는 Perl 언어를 비롯해 학계나 산업계가 주목하는 언어들의 대부분을 지원하고 있는 셈입니다. 이렇게 Thrift를 이용하면 다양한 환경의 소프트웨어를 쉽게 결합할 수 있습니다.</p>
<h2>설치하기</h2>
<p>Thrift 기술을 활용하기 위해 먼저 서버에 Thrift 구현이 설치되어야 합니다. <a href="http://thrift.apache.org/download/">윈도 버전 구현</a>의 빌드 버전은 공식 사이트에서 받을 수 있습니다. 윈도 환경의 사용자는 여기에서 내려받아 설치할 수 있습니다.</p>
<ul>
<li><a href="http://thrift.apache.org/download/">Thrift 윈도 바이너리</a></li>
</ul>
<p>리눅스 환경과 Mac OS X 환경에서는 빌드하여 구축할 수 있습니다.</p>
<ul>
<li><a href="http://thrift.apache.org/download/">Thrift 다운로드 페이지</a></li>
<li><a href="http://wiki.apache.org/thrift/GettingUbuntuPackages">우분투 환경을 위한 요구 사항</a></li>
</ul>
<p>우분투 환경에서는 아래와 같은 명령어를 입력하여 빌드할 수 있습니다.</p>
<pre class="brush: bash;">
$ ./configure
$ make
$ make install
</pre>
<p>일반적인 빌드 과정과 비슷합니다.</p>
<p>맥에서는 명령행을 통해 직접 빌드하기 보다 <a href="http://mxcl.github.com/homebrew/">Homebrew</a>를 이용하여 설치하는 편이 훨씬 더 편리합니다.</p>
<ul>
<li><a href="http://mxcl.github.com/homebrew/">Homebrew 공식 페이지</a></li>
<li><a href="http://dalinaum-kr.tumblr.com/post/2986196227/hello-homebrew">잘가 macports. 반갑습니다. homebrew</a> .</li>
</ul>
<p>homebrew가 설치된 환경에서는 아래의 명령으로 진행합니다.</p>
<pre class="brush: bash;">
$ brew install thrift
</pre>
<p>설치가 끝나면 thrift 명령을 사용할 수 있게 됩니다.</p>
<h2>언어지원 라이브러리 만들기</h2>
<p>Thrift가 준비가 되면 개별 언어용 라이브러리를 만들어야 합니다. 여기서는 Java 언어와 Perl 언어를 위한 라이브러리를 빌드해 보겠습니다. Thrift를 <code>~/thrift-0.7.0</code>에 설치했다면 아래와 같이 Java 언어용 라이브러리 코드를 빌드합니다.</p>
<pre class="brush: bash;">
$ cd ~/thrift-0.7.0/lib/java
$ ant
</pre>
<p>아래와 같이 Perl 언어용 라이브러리 코드도 만듭시다.</p>
<pre class="brush: bash;">
$ cd ~/thrift-0.7.0/lib/perl
$ perl Makefile.PL
$ make
$ make install
</pre>
<h2>튜토리얼 코드를 읽어봅시다</h2>
<p>먼저 Thrift가 제대로 동작하는지 튜토리얼 버전을 통해 확인해봅시다. <code>~/thrift-0.7.0</code>에 Thrift를 설치했다면 <code>~/thrift-0.7.0/tutorial</code>에 튜토리얼 코드가 있습니다.</p>
<pre class="brush: bash;">
$ cd ~/thrift-0.7.0/tutorial
$ thrift -r --gen perl tutorial.thrift
$ cd perl
$ perl PerlServer.pl &
$ perl PerlClient.pl
</pre>
<p>제대로 동작하면 <code>fg</code> 명령어를 이용해서 서버로 이동한 다음 <code>Control + C</code>(또는 <code>Command + C</code>) 키를 눌러 빠져나옵니다.</p>
<p>생소한 명령어들이 많기 때문에 낯설어 보일 것입니다. 하나씩 짚어보겠습니다. 두번째 줄의 <code>thrift -r --gen perl tutorial.thrift</code>는 <code>tutorial.thrift</code> 파일을 이용해 재귀적으로(<code>-r</code>) 펄 언어용 코드를(<code>--gen perl</code>) 생성하는 것을 의미합니다. Thrift에서 사용하는 언어는 <code>.thrift</code> 확장자를 사용합니다. 지금의 경우 <code>tutorial.thrift</code>가 여기에 해당합니다. <code>thrift --gen</code>으로 코드를 생성할 때 Thrift 파일이 이동되고 서버용 뼈대 코드와 클라이언트용 라이브러리 코드가 만들어져 편리하게 이용할 수 있습니다. 노력을 줄여 클라이언트를 구현할 수 있고 서버측 코드도 뼈대 코드에 살을 붙이는 식으로 구현할 수 있습니다. <code>--gen</code> 옵션에 다른 언어를 지정해 다른 언어를 위한 코드가 생성할 수 있습니다. <code>--gen java</code>를 입력하면 자바용 코드가 만들어질 것입니다.</p>
<p>이제 <code>tutorial.thrift</code> 파일을 살펴보겠습니다. 전체적으로 주석이 많이 달려있어 꼼꼼이 읽으면 대부분의 내용을 이해할 수 있을 것입니다.</p>
<pre class="brush: plain;">
include "shared.thrift"
</pre>
<p><code>shared.thrift</code> 파일을 포함하고 있습니다. 따라서 <code>shared.thrift</code>를 먼저 보겠습니다.</p>
<pre class="brush: plain;">
namespace cpp shared
namespace java shared
namespace perl shared
namespace php shared
struct SharedStruct {
1: i32 key
2: string value
}
service SharedService {
SharedStruct getStruct(1: i32 key)
}
</pre>
<p>첫 줄부터 네 번째 줄은 Thrift에서 생성할 언어 별 네임스페이스를 지정합니다. 여기서는 <code>cpp</code>, <code>java</code>, <code>perl</code>, <code>php</code> 언어에서 같은 네임스페이스로 서버 루틴을 사용합니다.</p>
<p><code>struct SharedStruct</code>는 구조체를 만든 것입니다. 항목이 <code>1:</code>, <code>2:</code>와 같이 콜론이 뒤에 붙은 숫자와 함께 나열되어 점에 유의하세요. 그 뒤에 타입과 이름이 붙습니다. 자료형이 <code>i32</code>와 <code>string</code>인데 해당 자료형은 그 언어에 맞게 자동으로 변환됩니다. 아래는 사용할 수 있는 자료형입니다.</p>
<ul>
<li>bool - Boolean, one byte</li>
<li>byte - Signed byte</li>
<li>i16 - Signed 16-bit integer</li>
<li>i32 - Signed 32-bit integer</li>
<li>i64 - Signed 64-bit integer</li>
<li>double - 64-bit floating point value</li>
<li>string - String</li>
<li>map<t1,t2> - Map from one type to another</li>
<li>list<t1> - Ordered list of one type</li>
<li>set<t1> - Set of unique elements of one type</li>
</ul>
<p>많은 언어에서 기본적으로 제공할만한 자료형이 제공되고 있습니다. 복잡한 자료형은 직렬화하거나 구조체를 만들어 사용해야 합니다.</p>
<p>그 다음으로 <code>service</code>가 위치하는데 <code>service</code> 안에 우리가 사용할 수 있는 함수들이 위치합니다. <code>service</code>는 객체 정도로 볼 수 있습니다. <code>SharedStruct getStruct(1: i32 key)</code>를 보면 매개변수에도 <code>1:</code>과 같이 순서대로 번호를 붙인 것을 볼 수 있습니다.</p>
<p>다시 <code>tutorial.thrift</code>로 돌아와 아직 모르는 부분들을 찾아봅시다.</p>
<pre class="brush: plain;">
php_namespace tutorial
</pre>
<p>PHP의 경우 별도의 <code>namespace</code>인 <code>php_namespace</code>를 사용하는 것을 볼 수 있습니다. (구체적인 차이점은 모르겠습니다. php에 대해 잘 아시는 분이 설명해주셨으면 좋겠네요 :)</p>
<pre class="brush: plain;">
typedef i32 MyInteger
</pre>
<p>C 언어에서 익숙하게 볼 수 있는 <code>typedef</code> 구문입니다.</p>
<pre class="brush: plain;">
const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}
</pre>
<p>상수 리터럴은 위와 같이 사용합니다.</p>
<pre class="brush: plain;">
enum Operation {
ADD = 1,
SUBTRACT = 2,
MULTIPLY = 3,
DIVIDE = 4
}
</pre>
<p>열거형입니다.</p>
<pre class="brush: plain;">
exception InvalidOperation {
1: i32 what,
2: string why
}
</pre>
<p>아래와 같이 예외를 정의할 수도 있습니다.</p>
<pre class="brush: plain;">
i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
</pre>
<p>뒤에 throws를 붙여서 예외를 던질 수 있게 되어 있습니다. 마지막에 <code>,</code>가 붙어있는 점을 주의하세요. <code>service</code> 내에 여러 함수들이 위치하고 <code>,</code>로 구분됩니다. 펄에서와는 달리 마지막 항목인 경우에는 뒤에는 <code>,</code>를 붙이지 않습니다.</p>
<pre class="brush: plain;">
oneway void zip(),
</pre>
<p><code>oneway</code>는 호출을 수행하되 클라이언트가 응답을 기다리지 않겠다는 의미입니다. I/O가 많은 작업들에서 더 효과적인 처리를 기대할 수 있습니다.</p>
<p>이 명세를 한번 보는 것만으로는 이해하기 쉽지 않습니다. 다른 Thrift 명세들도 참고하는 것이 도움이 됩니다.</p>
<ul>
<li><a href="http://svn.apache.org/viewvc/hbase/trunk/src/main/resources/org/apache/hadoop/hbase/thrift/Hbase.thrift?view=markup">Hbase.thrift</a></li>
</ul>
<h2>이제 서버측 펄 구현을 봅시다!</h2>
<p>아래와 같이 서버를 구축할 수 있습니다. <code>PerlServer.pl</code>의 일부를 차용했습니다.</p>
<pre class="brush: perl;">
use strict;
use lib '../gen-perl';
use Thrift::Socket;
use Thrift::Server;
use tutorial::Calculator;
package CalculatorHandler;
use base qw(tutorial::CalculatorIf);
sub new {
my $classname = shift;
my $self = {};
return bless($self,$classname);
}
sub ping {
print "ping()\n";
}
</pre>
<p>위에 정의한 <code>tutorial</code> 네임스페이스의 <code>Calculator</code> 서비스를 구현한 것입니다. <code>use base qw(tutorial::CalculatorIf);</code>로 시작하는 것이 보입니다.</p>
<p>뼈대 클래스가 <code>If</code>가 붙은 형태로 되어 있습니다. 이런 관례는 Thrift가 언어 구현에 따라 다르게 만들어줍니다. 각 언어 구현에 따라 예제를 참고하면 쉽게 파악할 수 있습니다. 나머지 필요한 기능들은 <code>sub new</code>, <code>sub ping</code>과 같이 사용자 함수로 구현합니다.</p>
<p><code>service</code>의 함수의 작성이 끝나면 이 클래스를 확장하여 서버를 구현합니다.</p>
<pre class="brush: perl;">
eval {
my $handler = new CalculatorHandler;
my $processor = new tutorial::CalculatorProcessor($handler);
my $serversocket = new Thrift::ServerSocket(9090);
my $forkingserver = new Thrift::ForkingServer($processor, $serversocket);
print "Starting the server...\n";
$forkingserver->serve();
print "done.\n";
};
if ($@) {
if ($@ =~ m/TException/ and exists $@->{message}) {
my $message = $@->{message};
my $code = $@->{code};
my $out = $code . ':' . $message;
die $out;
} else {
die $@;
}
}
</pre>
<p><code>CalculatorHandler</code>는 방금 상속받아 구현한 클래스입니다. 여기에서 <code>$processor</code>에 사용된 <code>tutorialProcessor</code>는 Thrift가 생성한 코드입니다. <code>Thrift::ServerSocket</code>과 <code>Thrift::ForkingServer</code>는 이미 Thrift내에 포함된 클래스입니다.</p>
<p><code>Thrift::ServerSocket</code>에 포트를 지정하는데 서버와 클라이언트 사이에 약속한 포트 번호를 입력합니다. 서버 내에 Thrift 구현이 여럿일 경우에는 여러 번호가 필요할 것입니다. 예를 들어 서버에서 <code>Hbase</code>나 <code>Casandra</code> 서버가 같이 돌고 있다면 이미 하나의 Thrift 포트를 사용하고 있을 것입니다. 이 때 다른 Thrift 서버는 새 포트 번호가 필요합니다.</p>
<p>아래는 클라이언트 코드의 일부입니다.</p>
<pre class="brush: perl;">
my $socket = new Thrift::Socket('localhost',9090);
my $transport = new Thrift::BufferedTransport($socket,1024,1024);
my $protocol = new Thrift::BinaryProtocol($transport);
my $client = new tutorial::CalculatorClient($protocol);
eval{
$transport->open();
$client->ping();
print "ping()\n";
my $sum = $client->add(1,1);
print "1+1=$sum\n";
my $work = new tutorial::Work();
$work->op(tutorial::Operation::DIVIDE);
$work->num1(1);
$work->num2(0);
eval {
$client->calculate(1, $work);
print "Whoa! We can divide by zero?\n";
};
if ($@) {
warn "InvalidOperation: ".Dumper($@);
}
}
</pre>
<h2>정리하며</h2>
<p>Thrift를 어떻게 설치하고 어떻게 사용할 수 있는지 알아보고, 튜토리얼 코드를 따라가며 기본적인 Thrift의 명세, 서버의 구현, 클라이언트 사용법을 훑어보았습니다. </p>
<p>서버의 이종 언어의 백엔드/프론트엔드간의 통신, Thrift를 지원하는 다른 서버(Hbase와 Casandra가 대표적입니다.)와의 통신, 서버와의 연결이 필요한 모바일 애플리케이션 등에서 (구글은 이 경우에 자사의 프로토콜버퍼를 사용합니다.) 유용하게 쓰일 수 있을 것입니다.</p>
<p>서버의 이종 언어의 백엔드/프론트엔드간의 통신, Thrift를 지원하는 서버끼리 통신하거나, 서버와의 연결이 필요한 모바일 애플리케이션에 유용하게 쓰일 수 있을 것입니다. 전자의 경우, Hbase와 Casandra가 대표적입니다. 구글은 후자의 경우에 자사의 프로토콜 버퍼를 사용합니다.</p>
<p>Hbase나 Casandra에서 Thrift가 어떻게 사용되시는지 참고하면 도움이 됩니다.</p>
<ul>
<li><a href="http://wiki.apache.org/cassandra/ThriftExamples">CasandraWiki - ThriftExamples</a></li>
<li><a href="http://wiki.apache.org/hadoop/Hbase/ThriftApi">HadoopWiki - Hbase/ThriftApi</a></li>
</ul>
<p>조금 더 재밌고 자세한 내용은 추후에 다루어 보겠습니다.
감사합니다.</p>
<h2>생각해볼 거리</h2>
<p>Thrift의 튜토리얼은 현대적이지 못한 펄 코드로 되어 있습니다.
이를 어떻게 개선하면 좋을지에 대한 것은 여러분에게 남겨두겠습니다.</p>
2011-12-04T00:00:00+09:00dalinaum소스코드 감쪽같이 숨기기http://advent.perl.kr/2011/2011-12-03.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/aanoaa">@aanoaa</a> - Perl, 콧수염, 야구, 자전거, 낙타, 돌고래, 포청천 마니아.
아이유 열혈 삼촌팬. Perl과 Javascript, 안드로이드 어플리케이션 개발에 능하다.
Perl 코드 사용시 탭 문자 대신 4칸 공백을 사용한다.
일신 상의 이유로 Vim을 버리고 Emacs로 투신.
현재 <a href="http://aanoaa.github.com/">콧수염 블로그</a>를 운영하고 있다.</p>
<h2>시작하며</h2>
<p>Perl 코드를 실행할 수 있다는 것은 소스 코드도 읽을 수 있다는 것을 뜻합니다.
하지만 단 몇 줄의 Perl 코드로 여러분의 코드를 감쪽같이 숨길 수도 있습니다.</p>
<h2>어떻게?</h2>
<p>먼저 숨기고자 하는 문자열을 비트스트림으로 보고 <code>'0'</code>과 <code>'1'</code>로만 이루어진 문자열로 바꿉니다.
그런 다음, 이것을 다시 공백(<code>" "</code>)과 탭(<code>"\t"</code>)으로만 이루어진 문자열로 바꿉니다.
그러면 여러분의 코드는 아무리 모니터를 뚫어지게 쳐다봐도, 백번 인쇄를 해도 읽어낼 수 없게 됩니다!
예를 들어 아래와 같은 내용이 주어졌다면,</p>
<pre class="brush: plain;">
안녕하세요
</pre>
<p>먼저 이렇게 <code>0</code>과 <code>1</code>로만 이루어진 문자열로 바꿉니다.</p>
<pre class="brush: plain;">
00110111101010010001000111010111101000011010100110110111101010010001100100110111001000010001110100110111010110010010100101010000
</pre>
<p>그리고 <code>0</code>과 <code>1</code>을 각각 공백과 탭 문자로 바꾸면 읽을 수도 없고 인쇄할 수는 문자열이 완성됩니다!</p>
<pre class="brush: plain;">
" "
</pre>
<p>보이지 않게 된 문자열을 큰 따옴표로 묶은 모습입니다.</p>
<h2>만들어 봅시다!</h2>
<p>먼저 Perl 내부로 읽어들인 문자열을
<a href="http://perldoc.perl.org/functions/unpack.html">unpack</a> 함수를 사용하여 <code>'0'</code>과 <code>'1'</code>의 바이트 시퀀스로 변형합시다.</p>
<pre class="brush: perl;">
my $text = <STDIN>;
$text = unpack "b*", $text;
</pre>
<p>다음으로 <code>'0'</code>은 <code>' '</code>로, <code>'1'</code>은 <code>"\t"</code>로 바꿉니다.</p>
<pre class="brush: perl;">
$text =~ tr/01/ \t/;
</pre>
<p>이게 끝입니다!
전체 코드로 한번 볼까요?</p>
<pre class="brush: perl;">
my $text = <STDIN>;
$text = unpack "b*", $text;
$text =~ tr/01/ \t/;
print $text;
</pre>
<p>결과 화면은 다음과 같습니다.
<code>"Hello Susan!"</code>을 입력하면 빨간 박스에
표시한 부분만큼 공백이 출력된 것을 볼 수 있습니다.</p>
<p><img src="http://advent.perl.kr/2011/2011-12-03-1.png" alt="결과" id="" /></p>
<h2>정리하며</h2>
<p>역순으로 실행하면 <code>' '</code>와 <code>"\t"</code>로 이루어진 텍스트를
원상태로 복구할 수 있을 겁니다.
Perl을 사용하면 이렇게 간단하게 문자열을 조작할 수 있습니다.
문서와 명령행 옵션이 깔끔하게 포함된
<a href="https://gist.github.com/1408846">s3cr3t 전체 소스코드</a>도 꼭 확인해보세요.</p>
<h2>참고문서</h2>
<ul>
<li><a href="https://metacpan.org/module/Acme::Bleach">CPAN의 Acme::Bleach 모듈</a></li>
<li><a href="https://gist.github.com/1408846">s3cr3t gist</a></li>
<li><a href="http://gypark.pe.kr/wiki/Perl/Pack#H_1_7_2">gypark님의 위키</a></li>
<li><a href="http://perldoc.perl.org/functions/pack.html">perldoc pack</a></li>
<li><a href="http://perldoc.perl.org/functions/unpack.html">perldoc unpack</a></li>
</ul>
2011-12-03T00:00:00+09:00aanoaaPerl 원라이너(one-liner)로 Octopress 따라잡기http://advent.perl.kr/2011/2011-12-02.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/pung96">@pung96</a> - Perl 원라이너의 귀재, 블랙홀이 있을 것으로 추정되는
스위스 제네바와 핀란드를 오가며 중이온 연구에 매진 중인 물리학자,
하지만 Perl과 C++, 심지어 Python까지 자유자재로 구사하는 물리학자를 가장한 프로그래머,
훈훈한 외모로 인해 유부남임에도 불구하고 <em>훈중년</em>이라는 별칭을 획득.</p>
<h2>시작하며</h2>
<p>한 줄의 Perl 코드로 필요한 알고리즘을 작성하는 Perl 원라이너(one-liner)는 매우 유용합니다.
간단한 코드를 작성할 때라던가, 쉘과 결합해서 사용해야 할 때라면 더더욱 그러하지요.
이미 세상에는 훌륭한 원라이너 문서가 많은데 이를 반복한다면 무척 지루한 일이겠죠.
대신 Ruby로 만든 <a href="http://github.com">GitHub</a> 블로그 툴인
<a href="http://octopress.org/">Octropress</a>를 Perl 원라이너로 살짝 흉내어
Perl 원라이너를 어떻게 쓸 수 있는지 살펴보겠습니다.</p>
<h2>원라이너의 철학</h2>
<p>원라이너란 명령줄에서 엔터키를 누르기 직전까지의 한 줄안에 원하는 동작을
실행시킬 수 있는 만큼 필요한 명령을 사용해서 코드를 작성하는 것을 말합니다.
그래서 원라이너는 <em>간단하게</em> 쓰기 위한 도구인 만큼 <em>최적화라던가 미학 따윈
소나 줘버려!</em>와 같은 마음가짐으로 내키는 대로 작성하면 됩니다.
사실 이 문서의 코드들도 이런 마음가짐대로 <em>마구</em> 작성한 것들입니다. ;-)
또한 코드 안팎에서 쉘의 기능을 충실히 이용해야 한다는 점을 잊지 마세요.
<code>system</code>이라던가 역따옴표, <code>while</code> 구문, 환경 변수 등 가리지 않도록 합니다.</p>
<h2>준비물</h2>
<p>원라이너로 Octopress를 따라잡으려면 다음 항목들을 준비해야 합니다.</p>
<ul>
<li>GitHub 계정</li>
<li>블로그용으로 사용할 GitHub의 저장소</li>
<li><a href="https://github.com/pung96/blog">이미 만들어 놓은 예제 블로그</a></li>
<li><a href="https://metacpan.org/module/YAML">CPAN의 YAML 모듈</a></li>
<li><a href="https://metacpan.org/module/Text::Xslate">CPAN의 Text::Xslate 모듈</a></li>
<li><a href="https://metacpan.org/module/Text::Markdown">CPAN의 Text::Markdown 모듈</a></li>
</ul>
<h2>미리 둘러보기</h2>
<p><a href="https://github.com/pung96/blog">이미 만들어놓은 예제 블로그</a>를 방문해보면,
Octopress에서 <em>훔쳐온</em> 테마를 기반으로 한 아주 간단한 블로그를 볼 수 있습니다.
물론 Octropress에 비하면 아주 사소한 기능만을 구현했지만,
이 이상의 것을 구현하려 한다면 Perl 원라이너는를 사용하는 것은 적절하지 않습니다.
편집기를 열어서 모듈을 만드는 것이 좋겠지요. :-)</p>
<p><a href="2011-12-02-1.png"><img src="2011-12-02-1.png" alt="Octopress 뺨치는 Perl 원라이너 블로그" id="octopressperl" width="700" /></a></p>
<p>Octropress는 GitHub 저장소를 블로그로 만들기 위해
<code>gh-pages</code>와 <code>source</code> 두 개의 가지를 사용합니다.
<code>gh-pages</code> 가지에서는 HTML 파일을 발행하며,
<code>source</code> 가지는 실제 블로그 컨텐츠와 기타 여러 파일을 저장합니다.</p>
<h2>디렉토리 구조</h2>
<pre class="brush: plain;">
-- blog # source 브랜치
|-- _scratch # 작업용 임시 디렉토리
|-- _deploy # html 디렉토리. ph-graphs 브렌치
|-- .gitignore
|-- init.sh # 초기화 스크립트
|-- build.sh # 빌드 스크립트
`-- source
|-- _post # 블로그 포스트 디렉토리
| |-- 2011-11-30-3rd-post.markdown
| |-- 2011-11-30-4th-post.markdown
| `-- 2011-11-30-first-post.markdown
|-- _layout
| |-- default.html # 기본 템플릿
| |-- index.html # 첫 페이지용 블로그 포스트 템플릿(default.html에 include 됨)
| `-- post.html # 각 페이지를 위한 블로그 포스트 템플릿(default.html 에 include 됨)
`-- favicon.png, stylesheets, javascripts
</pre>
<p><code>.gitignore</code>를 보면 <code>_scratch</code>, <code>_deploy</code> 폴더는 <code>source</code> 가지에 추가되지 않습니다.
Octropress의 구현을 살펴보기 전 까지는 <code>_deploy</code> 폴더처럼 발행을 위한 디렉토리를
어떻게 관리할지에 대해 많은 고민을 했었는데, 이 방법은 생각도 못했네요.
단순히 소스용 가지와 발행용 가지 두 개를 만들어 다른 디렉토리로 관리하면 끝입니다.
천재...</p>
<h2>출발!!</h2>
<p><a href="https://github.com/pung96/blog">이미 만들어놓은 예제 블로그</a>를 이용하는 방법을 기준으로 설명하겠습니다.
예제 블로그를 방문해 가벼운 마음으로 <em>Fork</em>한 후 저장소를 클론합니다.</p>
<pre class="brush: bash;">
git clone git@git.github.com:<username>/blog.git -b source
cd blog
./init.sh
</pre>
<p><code>init.sh</code> 파일의 내용은 다음과 같습니다.</p>
<pre class="brush: bash;">
git clone -b gh-pages git@git.github.com:<username>/blog.git _deploy
mkdir -p _scratch
</pre>
<p><code>_deploy</code> 디렉토리와 <code>_scratch</code> 디렉토리를 만들어주는 간단한 스크립트입니다.
<code><username></code>은 당연히 바꾸어 주어야 겠지요?</p>
<p><code>build.sh</code> 파일은 작성한 마크다운 문서를 이용해 HTML 파일을 만들고
다시 GitHub로 밀어넣는 Perl 원라이너의 집합입니다.
지금부터 <code>build.sh</code> 파일을 분석해볼까요?</p>
<h3>초기화</h3>
<p>먼저 <code>_deploy</code>, <code>_scratch</code> 디렉토리를 깨끗하게 비우고
작업할 파일을 <code>_scratch</code> 디렉토리로 복사합니다.</p>
<pre class="brush: bash;">
rm -rf _deploy/* _scratch/*
cp -a source/_posts/* _scratch/
</pre>
<h3>블로그 포스트 파일 구성</h3>
<p>블로그 포스트 파일을 메타 정보와 컨텐츠로 분리해야 합니다.
자, 이제 마크다운 파일을 살펴보죠.
파일의 형식은 Octopress의 블로그 포스트 형식을 그대로 사용합니다.</p>
<pre class="brush: yaml;">
---
layout: post
title: "4th-post"
date: 2011-11-30 18:19
comments: true
categories:
---
# 4th post
이곳이 블로그 내용을 적는 곳이죠.
</pre>
<p>안타깝게도 이 형식은 완전한 YAML 형식이 아니라서 약간의 지저분한 코드가 필요합니다.
먼저 제목 등의 정보가 저장된 부분을 추출해서 YAML 파일로 저장하고 나머지 부분을 남겨보겠습니다.</p>
<pre class="brush: bash;">
perl -i -0 -mYAML=Load -ne'BEGIN{open$y, ">_scratch/posts.yml"}/^---\s*$(.*?)^---\s*$(.*)/sm;print$y "---\nfile: $ARGV", $1;print $2' _scratch/*.markdown
</pre>
<p>추출된 정보들은 파일 정보를 추가해 하나의 YAML 파일에 통합해 저장합니다.
이런 정보를 버클리 DB와 같은 본격적인 데이터베이스에 저장한 후 사용하는 것도 좋은 방법입니다.
하지만 YAML 역시 거의 완전한 텍스트 기반 데이터베이스로 사용할만하며,
파일 자체를 사람이 바로 읽을 수 있다는 큰 장점이 있습니다.</p>
<h3>메타 정보 가공하기</h3>
<p>우선 날짜에 따라 정렬해보겠습니다.
<code>DumpFile</code>처럼 <code>LoadFile("posts.yml")</code>을 사용할 수도 있지만
앞에서 무심코 <code>-0</code>을 적어버렸기 때문에 그냥 파일을 읽어들여서
Load에 파일 내용을 통째로 넣었습니다.</p>
<p><a href="https://metacpan.org/module/DateTimeX::Easy">CPAN의 DateTimeX::Easy 모듈</a>을 사용해
날짜 및 시간 정보(2011-11-30 18:19)를 파싱해서 정확한 값을 사용할 수도 있지만
여기서는 그냥 문자열 비교를 사용해 정렬하는 것으로도 충분했습니다. </p>
<pre class="brush: bash;">
perl -0 -MYAML=Load,Dump -ne'print Dump(sort{$b->{date}cmp$a->{date}}Load($_))' _scratch/posts.yml > _scratch/posts2.yml
</pre>
<p>순서에 따른 아이다와 포스트의 URL(예: <code>/blog/blog/2011/11/30/4th-post</code>)을 YAML DB에 추가합니다.
<code>/blog/blog</code> 이렇게 중복이 생겼네요. 그냥 넘어갑시다.</p>
<pre class="brush: bash;">
perl -0 -MYAML=Load,Dump -ne'print Dump(map {$_->{file}=~/(\d+)-(\d+)-(\d+)-(.*?)\./;$_->{url}="blog/$1/$2/$3/$4";$_->{iid}=$id++;$_}Load($_))' _scratch/posts2.yml > _scratch/posts.yml
</pre>
<p>잘 추출되어 정렬되었는지, 필요한 정보가 추가되고 있는지
<a href="https://metacpan.org/module/Data::Dumper">CPAN의 Data::Dumper 모듈</a>을 이용해 확인해봅시다.
<code>Data::Dumper</code> 모듈은 원라이너 코딩시 가장 많이 사용하는 모듈 중의 하나입니다.</p>
<pre class="brush: bash;">
perl -0 -MYAML=Load -MData::Dumper -ne'print Dumper Load($_);' posts.yml
</pre>
<p>두개의 YAML 파일을 관리하는 것은 귀찮으니 앞으로 쓰일 YAML 파일들을 하나로 통합해 버립니다.
<code>cat</code>으로 합치면 간단하게 끝납니다.</p>
<pre class="brush: bash;">
cat _config.yml _scratch/posts.yml > _scratch/all.yml
</pre>
<p>이번에는 마크다운으로 저장한 페이지를 HTML로 바꿉니다.
<code>Text::Markdown</code> 모듈이 알아서 다 해 줍니다.
<code>-i</code> 옵션을 사용해서 한방에 파일들을 변환해 버렸습니다.
사실 <code>-i</code> 는 굉장히 위험한 옵션입니다.
사용하기 전에 파일을 백업하거나 <code>-i.bak</code> 같은 방법을 쓰는 등 각별한 관리가 필요합니다.
저 역시 숱하게 파일을 날려버리고 여러번 울었었죠!
특히 <code>-p</code> 옵션 대신 무심코 <code>-n</code> 옵션을 쓴다면 깨끗해진 파일들을 보게 됩니다.
이런!;;;</p>
<pre class="brush: bash;">
perl -i -0 -mText::Markdown=markdown -ne'print markdown($_)' _scratch/*.markdown
</pre>
<p><code>Text::Xslate</code> 모듈을 이용해 미리 작성한 템플릿에 만들어진 HTML 파일을
집어넣어서 실제 보여줄 페이지를 만듭니다.
제목이나 앞뒤 페이지 네비게이션을 위해서 메타정보도 함께 전달해야 합니다.</p>
<pre class="brush: bash;">
perl -mYAML=LoadFile -MText::Xslate -e'($s,@p)=LoadFile("_scratch/all.yml");map{open$f,">$_->{file}";print$f Text::Xslate->new->render("source/_layouts/default.html", { site=>$s, page=>$_, type=>"post.html", posts=>\@p})}@p'
</pre>
<p>최종 작성된 파일들을 <code>_deploy</code> 폴더의 <code>blog/년도/월/날짜/제목/index.html</code> 형식으로 보내줍니다.</p>
<pre class="brush: bash;">
ls _scratch/*.html | perl -nle'/(\d+)-(\d+)-(\d+)-(.*?)\./;$d="_deploy/blog/$1/$2/$3/$4";system("mkdir -p $d && cp $_ $d/index.html");'
</pre>
<p>이제 메인 페이지를 만들겠습니다.
첫 페이지에 보여줄 포스트의 개수를 정하고 페이지 네비게이션을 만들면 좋겠지만,
<em>귀찮아서</em> 그냥 모든 포스트를 한 화면에 넣었습니다.
똑같은 템플릿을 사용하면서 템플릿 내에서 <code>for</code> 반복문을 이용해 모든 포스트를 출력합니다.</p>
<pre class="brush: bash;">
perl -mYAML=LoadFile -MText::Xslate -e'($s,@p)=LoadFile("_scratch/all.yml");open$f,">_deploy/index.html";print$f Text::Xslate->new->render("source/_layouts/default.html", { site=>$s, pages=>\@p, type=>"index.html", posts=>\@p})'
</pre>
<p>와우! 이제 다 끝났습니다.
Octopress 에서 <em>훔쳐온</em> 정체를 알 수 없는
각종 CSS와 자바스크립트 파일들을 <code>_deploy</code> 폴더로 복사합시다.</p>
<pre class="brush: bash;">
rsync -a source/favicon.png source/images source/javascripts source/stylesheets _deploy/
</pre>
<p><code>_deploy</code> 폴더로 이동해 추가, 변경, 삭제된 파일들을 업데이트 한 후
원격 저장소의 <code>gh-pages</code> 브랜치로 밀어 넣습니다.</p>
<pre class="brush: bash;">
cd _deploy
git add .
git add -u
git commit -m "Site updated at `date`"
git push origin gh-pages --force
</pre>
<h2>놀랍게도 끝</h2>
<p><code>source</code> 가지를 <code>push</code>하는 것 있지 마세요!</p>
<pre class="brush: bash;">
git add .
git commit -a -m <log>
git push origin source
</pre>
<p>이제 <code>https://<username>.github.com/blog</code> 주소의 여러분 만의 블로그가 생성되었습니다.</p>
<p><em>참 쉽죠잉~</em> ;-)</p>
<h2>보너스</h2>
<p>처음 계획할 때 생각했던 것보다 다양한(?) 코드가 나오지 않은 것 같아
아쉬움을 달래기 위해 간단한 원라이너 라이프핵 두 가지를 더 소개합니다.</p>
<h2 id="perl">Perl 코드 내에 쉘 환경 변수를 넣기</h2>
<p>환경 변수를 넣으려면 <code>$ENV{VarName}</code> 해시 변수를 사용할 수도 있지만,
쉘의 문자열 병합 기능을 이용할 수도 있습니다.</p>
<pre class="brush: bash;">
perl -e'print "Current Term is '$TERM'\n"'
</pre>
<h2>여러 개의 파일 열기</h2>
<p>속도에 아주 민감한 코드가 아니라면, 여러 파일을 처리할 때 원라이너 Perl 코드 내에서
파일을 여는 대신 쉘의 <code>while</code> 루프를 사용할 수도 있습니다.</p>
<pre class="brush: bash;">
ls *.root | while read x;do perl -nle'do something' $x > $x.new ;done
</pre>
<p>생각해보면 당연한 거죠?
하지만 저는 Perl 원라이너 안에서 이중 반복문을 사용할 때가 더 많답니다. :-)</p>
<h2>정리하며</h2>
<p>Perl 원라이너는 정말 강력합니다.
몇 가지 옵션과 CPAN의 모듈, 그리고 심지어 유닉스 유틸리티와 파이프를 연결하면
한 줄의 원라이너 코드로 할 수 없는 일을 찾는 것이 더 어려울 것입니다.
물론 지금까지의 모든 작업을 모듈로 리팩터링 해서 우아하게(?) 만들 수도 있지만
불과 몇 줄 코드의 집합을 단 두 개의 파일로 정리해서 간단히 끝내버렸는데,
오히려 이 편이 더 멋있지 않나요?</p>
<p>이 문서에서는 마크다운 문서만 다루었지만 POD 등의 다른 형식을 추가하는 것도 어렵지 않겠죠.
이미 모든 것은 <a href="http://www.cpan.org/">CPAN님</a>께서 알고 계십니다.
비슷한 방식으로 <a href="http://ikiwiki.info">ikiwiki</a>를 GitHub에서 서비스 하는 것도 간단하고 재밌겠네요.
<code>_deploy</code> 디렉토리는 단순한 정적 파일들만을 가지고 있기 때문에 GitHub로 밀어넣는 것 뿐만 아니라,
다른 웹서버나 <a href="http://dotcloud.com">dotcloud</a>, <a href="http://aws.amazon.com/s3">Amazon S3</a> 같은 클라우드 서비스를 이용할 수도 있겠죠.
숙제로 남겨두기로 할까요?</p>
<p>Octopress도 매우 훌륭한 도구입니다.
Perl 사용자는 Perl 도구만 써야한다는 강박을 가진 것은 아니지만
Perl로 만들어진 것이 아니면 수정할 수가 없어서 재미가 없고 답답한 느낌이 들어요.
누가 Perl로 Octopress 에 버금가는 도구를 만들어주면 좋겠는데...</p>
<p>여기까지. 더 이상은 무리! ;-)</p>
<h2 id="perl">Perl 원라이너 참고문서</h2>
<ul>
<li><a href="http://perldoc.perl.org/perlrun.html">perldoc perlrun</a></li>
<li><a href="http://www.unixguide.net/unix/perl_oneliners.shtml">HOT PERL ONE LINERS</a></li>
<li><a href="http://en.wikipedia.org/wiki/One-liner_program">One-liner Program</a></li>
<li><a href="http://www.ibm.com/developerworks/linux/library/l-p101/">Cultured Perl: One-liners 101</a></li>
<li><a href="http://perlog.pung96.net">pung96's perlog</a></li>
</ul>
2011-12-02T00:00:00+09:00pung96네모 반듯한 표 그리고 한글http://advent.perl.kr/2011/2011-12-01.html<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>화려한 UI를 가진 응용 프로그램과 웹페이지가 가득한 21세기라 하더라도
터미널에서 작업을 하는 사람은 여전히 많습니다.
비단 프로그래머 뿐만 아니라 서버 관리자는 물론, 대용량 자료 처리를 위해
*nix 머신에 접속해서 여러가지 프로세스를 돌려 결과 뽑아내야하는
생명정보학 연구원 역시 21세기인 지금까지도 터미널을 즐겨 쓰는 사람들입니다.</p>
<p>터미널의 특성상 표현할 수 있는 문자가 제한된 만큼 화려한 효과를
보여줄 수는 없지만 하지만 이 와중에도 <a href="http://en.wikipedia.org/wiki/Ascii_art">ASCII 아트</a>처럼
제한된 문자를 이용해서 터미널에서의 표현력을 높이려는 시도는
항상 있었습니다.</p>
<pre class="brush: plain;">
THERE'S MORE THAN ONE WAY TO DO ME
_
.--' |
/___^ | .--.
) | / \
/ | /` '.
| '-' / \
\ | |\
\ / \ /\|
\ /'----`\ /
||| \\ |
((| ((|
||| |||
jgs //_( //_(
</pre>
<p>아마도 검은색 화면에 흰색(또는 녹색!)의 글자가 만들어내는 정갈한
화면에 매료된 적이 있다면 아마도 당신은 해커일 확률이 높겠지요. :-)</p>
<p>예술(?)은 잠시 접어두고 일 이야기를 해볼까요?
터미널을 즐겨 쓰는 사람들은 자신이 작업하고 있는 결과물 역시
터미널에서 확인해야 할 경우가 많습니다.
또 그것이 간편해서 선호하기도 하구요.
엄청나게 많은 내용이라 사람이 보기 위한 자료가 아닐 수도 있지만
때로는 한 두 페이지 내외의 눈으로 확인해야 할 자료도 있습니다.
지금부터 살펴볼 내용은 바로 여러분이 눈으로 확인하려는 자료입니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Text::ASCIITable">CPAN의 Text::ASCIITable 모듈</a></li>
<li><a href="https://metacpan.org/module/Text::CharWidth">CPAN의 Text::CharWidth 모듈</a></li>
<li><a href="https://metacpan.org/module/Text::WrapI18N">CPAN의 Text::WrapI18N 모듈</a></li>
</ul>
<p>데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libtext-asciitable-perl libtext-charwidth-perl
</pre>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Text::ASCIITable Text::CharWidth
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<code>perlbrew</code>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Text::ASCIITable Text::CharWidth
</pre>
<h2>네모 반듯한 표</h2>
<p><a href="http://www.catalystframework.org/">카탈리스트(Catalyst)</a>를 사용해본 적이 있다면 실행 후
기본적으로 터미널에 출력하는 디버그 로그에 포함된 네모 반듯한 표가
가독성을 얼마나 높여주는지 기억할 것입니다.</p>
<p><a href="2011-12-01-1.png"><img src="2011-12-01-1.png" alt="카탈리스트 디버그 로그" id="" width="700" /></a></p>
<p>사용자 관리를 예로 들어볼까요?
표현해야 할 자료가 다음과 같은 항목을 가진다고 가정해보죠.</p>
<ul>
<li>이름</li>
<li>별명</li>
<li>전자우편</li>
<li>홈페이지</li>
</ul>
<p>일반적으로 이런 값을 출력하는 가장 간단한 방법은 각각의 항목 이름과
값을 묶어서 쉼표 등의 구분자를 이용해서 출력하는 것입니다.</p>
<pre class="brush: perl;">
printf(
"id(%s), name(%s), nick(%s), email(%s), homepage(%s)\n",
$user->{id},
$user->{name},
$user->{nick},
$user->{email},
$user->{homepage},
);
</pre>
<p>반목문과 앞의 코드를 이용하면 출력 결과는 이런 형태일 것입니다.</p>
<pre class="brush: plain;">
id(2), name(Inkyung Park), nick(practal78), email(practal78@gmail.com), homepage()
id(3), name(Hanyoung Cho), nick(rumidier), email(rumidier@naver.com), homepage()
id(4), name(Hyoungsuk Hong), nick(aanoaa), email(aanoaa@gmail.com), homepage(http://twitter.com/aanoaa)
id(5), name(Keedi Kim), nick(keedi), email(keedi.k@gmail.com), homepage()
id(6), name(Yongbin Yu), nick(yongbin), email(supermania@gmail.com), homepage()
</pre>
<p>원하는 값은 모두 보이지만 한 눈에 들어오지는 않습니다.
바로 지금이 표를 써야할 시점입니다. :-)
CPAN에는 다양한 종류의 테이블 모듈이 있지만 이번에는
<a href="https://metacpan.org/module/Text::ASCIITable">CPAN의 Text::ASCIITable 모듈</a>을 사용하기로 합니다.
다음은 모듈을 사용해서 앞에서 보았던 자료를 네모난 표에 담는 간단한 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use Text::ASCIITable;
my $table = Text::ASCIITable->new({
headingText => 'People',
});
$table->setCols(qw/ id name nick email homepage /);
my @users = (
{
id => '1',
name => 'Keedi Kim',
nick => 'keedi',
email => 'keedi.k@gmail.com',
homepage => 'http://twitter.com/keedi',
},
{
id => '2',
name => 'Inkyung Park',
nick => 'practal78',
email => 'practal78@gmail.com',
homepage => 'http://twitter.com/practal78',
},
{
id => '3',
name => 'Hanyoung Cho',
nick => 'rumidier',
email => 'rumidier@naver.com',
homepage => 'http://twitter.com/rumidier',
},
{
id => '4',
name => 'Hyoungsuk Hong',
nick => 'aanoaa',
email => 'aanoaa@gmail.com',
homepage => 'http://twitter.com/aanoaa',
},
{
id => '5',
name => 'Yongbin Yu',
nick => 'yongbin',
email => 'supermania@gmail.com',
homepage => 'http://twitter.com/y0ngbin',
},
);
for my $user ( @users ) {
$table->addRow(
$user->{id},
$user->{name},
$user->{nick},
$user->{email},
$user->{homepage},
);
}
print $table;
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<p><a href="2011-12-01-2.png"><img src="2011-12-01-2.png" alt="네모 반듯한 표" id="" width="700" /></a></p>
<p>아름답군요! ;-)</p>
<h2>어이쿠! 표가 깨져요!</h2>
<p>영어권 사람이라면 평생동안 단 한번도 겪기 힘든 문제지만, 한국, 중국, 일본처럼
고유의 언어와 문자를 사용하고 있는 우리는 항상 겪는 문제입니다.
컴퓨터란 것이 태초부터 멀티바이트 문자를 고려하지 않고 세상에 태어난 만큼
많이 좋아졌다고는 하지만 특히 터미널에서라면 항상 겪는 것이 인코딩 문제입니다.
인코딩 문제를 해결하는 가장 간단하면서도 명확한 방법은
사용하는 모든 자료를 UTF-8 형식으로 인코딩 및 디코딩 하는 것입니다.
물론 여러분의 터미널 역시 UTF-8 형식으로 인코딩해서 보이도록 설정하는 것은 기본이겠죠?
다행히 현대의 리눅스 시스템은 거의 대부분 UTF-8 인코딩을 기본 설정으로 사용하고 있습니다.</p>
<p>지금 우리가 맞닥뜨릴 문제는 한글 깨짐 현상과는 조금 다른 바로 표 깨짐 현상입니다.</p>
<p><a href="2011-12-01-3.png"><img src="2011-12-01-3.png" alt="세로 줄이 맞지 않는 표" id="" width="700" /></a></p>
<p>아휴! 이름을 한글로 바꿨을 뿐인데, 표의 세로 줄이 깨져 버려서 엉망이 되었습니다.
이것은 엄밀히 말해서 우리가 잘못했다기 보다는 <code>Text::ASCIITable</code> 모듈의 저자가
미처 고려하지 못한 부분입니다.
간단하게 설명하면 UTF-8 인코딩을 사용하는 환경에서 한글 한 글자는 3바이트를 크기를
가지는데 실제로 터미널의 화면에서는 2칸의 너비를 사용하기 때문에 생기는 문제입니다.
대부분의 사람은 터미널에서 해당 문자열의 길이를 파악할 때 글자의 바이트 수를 이용해서
터미널에서 몇 열을 사용하는지 계산합니다만, 불행히도 2바이트를 사용하는 CP949(EUC-KR)
인코딩과는 달리 UTF-8 인코딩은 3바이트를 사용하기 때문에 한글 한 글자마다 3칸을
차지한다고 고려해서 결국 앞의 예제에서는 3개의 공백이 더 들어가게 됩니다.</p>
<p>이 문제를 해결하기 위한 패치는 다음과 같습니다.</p>
<pre class="brush: diff;">
--- a/table.pl 2011-12-01 03:09:31.768703000 +0900
+++ b/table.pl 2011-12-01 03:35:59.616703000 +0900
@@ -4,16 +4,18 @@
use strict;
use warnings;
use Text::ASCIITable;
+use Text::CharWidth qw( mbswidth );
my $table = Text::ASCIITable->new({
headingText => 'People',
+ cb_count => sub { mbswidth(shift) },
});
$table->setCols(qw/ id name nick email homepage /);
my @users = (
{
id => '1',
- name => 'Keedi Kim',
+ name => '김도형',
nick => 'keedi',
email => 'keedi.k@gmail.com',
homepage => 'http://twitter.com/keedi',
</pre>
<p><code>Text::ASCIITable</code> 모듈은 <code>cb_count</code> 속성을 지원하는데 이 기능을 이용하면
기본적으로 모듈이 사용하는 글자 계수기 대신 사용자가 원하는 콜백 함수를
실행시켜 상황에 맞게 글자 수를 셀 수 있습니다.
<a href="https://metacpan.org/module/Text::CharWidth">CPAN의 Text::CharWidth 모듈</a>은 <code>mbswidth</code> 함수를
제공하는데 이 함수를 이용하면 CJK 문자가 터미널에서 갖는 실제 너비를
알 수 있습니다.
즉 <code>cb_count</code> 속성에 할당할 콜백 함수에서 <code>mbswidth</code> 함수를 사용해서
너비를 계산한 값을 반환한다면 정확히 우리가 원하는 결과가 나옵니다.</p>
<p>패치를 적용하고 난 뒤 실행 결과는 다음과 같습니다.</p>
<p><a href="2011-12-01-4.png"><img src="2011-12-01-4.png" alt="한글 줄맞춤 패치를 적용한 표" id="" width="700" /></a></p>
<p>네, 한글을 표로 넣는 일도 이젠 문제 없습니다!</p>
<h2>어라? 줄바꿈이 어색해요</h2>
<p>내친 김에 이번에는 음반을 관리해볼까요?</p>
<p><a href="2011-12-01-5.png"><img src="2011-12-01-5.png" alt="음반 관리 표" id="" width="700" /></a></p>
<p>한글도 그렇고 큰 문제없이 깔끔하게 출력됩니다.
그런데 가사 부분이 조금은 어색해 보입니다.
사실 영어권 문자의 경우 단어가 끝나기 전에 줄바꿈이 될 경우 가독성이
무척 떨어지기 때문에 보통 띄어쓰기 단위로 줄바꿈을 수행하지만
한글은 단어 중간에 다음 줄로 끊기더라도 가독성이 크게 떨어지지 않습니다.
오히려 띄어쓰기 단위로 줄바꿈을 하는 것이 더 어색할 수도 있습니다.</p>
<p>이 문제를 해결하기 위한 패치는 다음과 같습니다.</p>
<pre class="brush: diff;">
--- table-hangul-wrap.pl 2011-12-01 04:01:48.832702999 +0900
+++ table-hangul-wrap.pl 2011-12-01 04:01:41.872703000 +0900
@@ -6,6 +6,37 @@
use Text::ASCIITable;
use Text::CharWidth qw( mbswidth );
+BEGIN {
+ use Text::WrapI18N;
+ no warnings 'redefine';
+
+ sub Text::WrapI18N::_isCJ {
+ my $u = shift;
+
+ if ($u >= 0x3000 && $u <= 0x312f) {
+ if ($u == 0x300a || $u == 0x300c || $u == 0x300e ||
+ $u == 0x3010 || $u == 0x3014 || $u == 0x3016 ||
+ $u == 0x3018 || $u == 0x301a) {return 0;}
+ return 1;
+ } # CJK punctuations, Hiragana, Katakana, Bopomofo
+ if ( 0x31a <= $u && $u <= 0x31bf ) { return 1; } # Bopomofo
+ if ( 0x31f0 <= $u && $u <= 0x31ff ) { return 1; } # Katakana extension
+ if ( 0x3400 <= $u && $u <= 0x9fff ) { return 1; } # Han Ideogram
+ if ( 0xf900 <= $u && $u <= 0xfaff ) { return 1; } # Han Ideogram
+ if ( 0x20000 <= $u && $u <= 0x2ffff ) { return 1; } # Han Ideogram
+ if ( 0xAC00 <= $u && $u <= 0xD7AF ) { return 1; } # Hangul
+
+ return 0;
+ }
+
+ sub Text::ASCIITable::wrap {
+ my ( $text, $width, $nostrict ) = @_;
+
+ $Text::WrapI18N::columns = $width;
+ return Text::WrapI18N::wrap('', '', $text);
+ }
+}
+
my $table = Text::ASCIITable->new({
headingText => 'Music',
cb_count => sub { mbswidth(shift) },
</pre>
<p>Perl에 익숙하지 않다면 이번 패치는 조금 복잡하게 느껴질 수도 있을 것입니다.
한글에 띄어쓰기 단위가 아니라 글자 단위의 줄바꿈 기능을 추가하기 위해서
다음 두 개의 함수를 오버라이딩 합니다.</p>
<ul>
<li><code>Text::WrapI18N</code> 모듈의 <code>_isCJ</code> 함수</li>
<li><code>Text::ASCIITable</code> 모듈의 <code>wrap</code> 함수</li>
</ul>
<p><code>Text::ASCIITable</code> 모듈은 <code>cb_count</code> 속성을 제공해서 사용자가 원하는
스타일로 글자의 개수를 셀 수는 있지만 줄바꿈과 관련해서는 공식적으로
사용자가 제어할 수 있는 방법이 없습니다.
그래서 해당 모듈을 사용하는 응용 프로그램 또는 라이브러리 측에서
강제로 오버라이딩 하는 방법을 택해서 문제를 해결합니다.
또한 줄바꿈과 관련해서는
<a href="https://metacpan.org/module/Text::WrapI18N">CPAN의 Text::WrapI18N 모듈</a>을 사용합니다.
이 모듈은 일반적으로 많이 사용하는
<a href="https://metacpan.org/module/Text::Wrap">CPAN의 Text::Wrap 모듈</a>과 거의 유사하지만
CJK 문자에 대해서 바이트 단위가 아니라 실제 너비를 고려해서
줄바꿈을 수행하도록 도와줍니다.
다만 이 모듈은 한국어를 고려하고 있지 않기 때문에
한국어 유니코드에 해당하는 범위에 대해서도 동작하도록
<code>_isCJ</code> 함수를 오버라이딩 합니다.</p>
<p>패치를 적용하고 난 후의 출력 화면입니다.</p>
<p><a href="2011-12-01-6.png"><img src="2011-12-01-6.png" alt="줄바꿈 패치를 적용한 후 음반 관리 표" id="" width="700" /></a></p>
<p>완전한 예제 스크립트는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use Text::ASCIITable;
use Text::CharWidth qw( mbswidth );
BEGIN {
use Text::WrapI18N;
no warnings 'redefine';
sub Text::WrapI18N::_isCJ {
my $u = shift;
if ($u >= 0x3000 && $u <= 0x312f) {
if ($u == 0x300a || $u == 0x300c || $u == 0x300e ||
$u == 0x3010 || $u == 0x3014 || $u == 0x3016 ||
$u == 0x3018 || $u == 0x301a) {return 0;}
return 1;
} # CJK punctuations, Hiragana, Katakana, Bopomofo
if ( 0x31a <= $u && $u <= 0x31bf ) { return 1; } # Bopomofo
if ( 0x31f0 <= $u && $u <= 0x31ff ) { return 1; } # Katakana extension
if ( 0x3400 <= $u && $u <= 0x9fff ) { return 1; } # Han Ideogram
if ( 0xf900 <= $u && $u <= 0xfaff ) { return 1; } # Han Ideogram
if ( 0x20000 <= $u && $u <= 0x2ffff ) { return 1; } # Han Ideogram
if ( 0xAC00 <= $u && $u <= 0xD7AF ) { return 1; } # Hangul
return 0;
}
sub Text::ASCIITable::wrap {
my ( $text, $width, $nostrict ) = @_;
$Text::WrapI18N::columns = $width;
return Text::WrapI18N::wrap('', '', $text);
}
}
my $table = Text::ASCIITable->new({
headingText => 'Music',
cb_count => sub { mbswidth(shift) },
});
$table->setCols(qw/ artist name lyrics /);
$table->setColWidth( 'lyrics', 40 );
my @songs = (
{
artist => '옐로우 몬스터즈',
name => 'Metal Gear',
lyrics =>
'언제부턴가 모두 똑같아 '
. '음악보다 말발의 멜로디를 '
. '노래하고 춤추고 옷을 벗고 '
. '가요 판 강타하려 기웃거려 '
. '누가 월드 스타? 아무도 널 몰라. '
. '아무도 널 몰라! 아무도 널 몰라 몰라!! '
. '언제부턴가 모두 얼굴이 똑같아졌어 '
. '언제부턴가 모두 모두 똑같아 '
. '언제부턴가 모두 똑같아'
,
},
{
artist => '델리스파이스',
name => '두 눈을 감은 타조처럼',
lyrics =>
'아주 오래 전 옛날 커다란 홍수 속에서 '
. '사자들이 떠난후에 살아남은 여우들만이 '
. '두 눈을 감아 당신 앞에 큰 위험이 '
. '머릴 박아 땅속에 마치 타조처럼 '
. '모두가 알아야만해 당신앞에 서 있는 건 '
. '사자가 아닌 여우인걸 이제 필요한건 '
. '모두 나가 여우사냥을 해야 해'
,
},
);
for my $song ( @songs ) {
$table->addRow(
$song->{artist},
$song->{name},
$song->{lyrics},
);
}
print $table;
</pre>
<h2>정리하며</h2>
<p>반짝이고 화려한 인터넷 브라우저 화면에 비하면 텍스트 기반의
터미널 화면은 그야말로 초라하기 짝이 없습니다.
터미널을 기피하고, 키보드 보다는 마우스를 사용하는 것이
익숙한 사람이 더 많아지는 것도 어쩌면 당연한 시대입니다.
그럼에도 불구하고 터미널을 좋아하는 사람(아마도 여러분?)은 여전히 적지 않습니다.
일상적인 작업 뿐만 아니라 대량의 자료를 생성해내기 직전 점검을 해야할 때,
프로그램의 로그로써 사람이 읽기 쉬운 자료로 보여주어야 할 경우
표로 도식화 하는 것은 훌륭한 선택입니다.
하지만 터미널에서 ASCII 문자를 이용해서 일일이 그리는 일은
고통스러운 일입니다.
자료 처리에 탁월한 Perl과 너무도 간단하게 표를 그릴 수 있게
도와주는 CPAN 모듈, 그리고 한글을 위한 약간의 기교를 이용하면
여러분의 터미널을 이쁘게 꾸미는 것은 정말 즐거운 일이 될 것입니다.</p>
<p>텍스트를 사랑하는 덕ㅎ... 아니, 해커 여러분에게 바칩니다.</p>
<p>Enjoy Your Perl! ;-)</p>
2011-12-01T00:00:00+09:00keedi