$html =~ s{ (나는 코드를 쓸때면 공작처럼 으스대다가, 실행을 시킬때면 곧 잘못을 인정해야 했습니다. 나는 한번도 제대로 동작하는 것을 볼 수 없었습니다. 아직까지도 내가 무엇을 하려고 했던 것인지 확신할 수 없습니다. 저 정규 표현식으로 인해 나는 HTML::TokeParser 모듈을 사용하는 법을 익히게 되었습니다. 더욱 중요한 것은, 저 정규표현식으로 인해 정규표현식이 얼마나 어려운 것인지 알게되었다는 것입니다.](?!href))*hrefs*) (&(&[^;]+;)?(?:.(?!3))+(?:3)?) ([^>]+>) } {$1 . decode_entities($2) . $4}gsexi;
/(여러분은 저것이 매치하는 것을 알 수 있습니까? 정확하게? 정말로요? 심지어 저것이 동작한다고 하더라도, 여러분은 쉽게 고칠 수 있을까요? 만약 여러분이 저것이 동작하는 것을 모른다면(그리고 공정하게 말하면, 그것은 깨어진 것임을 잊지 마세요), 그것을 이해하는데 얼마나 오랜 시간을 소요해야 할까요? 단 한줄의 코드로 여러분의 요구에 딱 맞아 떨어졌던 적이 언제가 마지막이었나요?](?!href))*hrefs*)(&(&[^;]+;)?(?:.(?!3))+(?:3)?)([^>]+>)/
select the_date as "date", round(months_between(first_date,second_date),0) months_old ,product,extract(year from the_date) year ,case when a=b then "c" else "d" end tough_one from ... where ...글을 올린 사람은 위의 SQL 에서 각각의 칼럼의 별칭(alias)을 추려내고 싶어했습니다. 이 경우 date, months_old, product, year, tough_one가 별칭(alias) 입니다. 물론 이것은 단지 하나의 예제에 불과했습니다. 실제로 많은 양의 생성된 SQL의 컬럼 별칭(alias)이 조금씩 미묘하게 변형되어 있었기 때문에, 이것을 처리하는 것은 간단한 일이 아닙니다. 그렇지만 여기서 흥미로운 점은 우리는 컬럼 별칭 말고는 무엇도 필요하지 않다는 것입니다. 이 글의 나머지 부분에서는 별칭을 찾기 위한 내용을 설명합니다.
x = (3 + 2) / y렉싱이 끝나면 여러분은 다음과 같은 일련의 토큰을 얻을 것입니다:
my @tokens = ( [ OP => "x" ], [ OP => "=" ], [ OP => "(" ], [ INT => "3" ], [ VAR => "+" ], [ INT => "2" ], [ OP => ")" ], [ OP => "/" ], [ VAR => "y" ], );여러분에게 적절한 문법이 있다면, 어쩌면 간단한 언어의 인터프리터를 만든다거나, 이 코드를 다른 프로그래밍 언어로 변환하기 위해 이 일련의 토큰을 읽을 수 있고, 토큰의 값을 기초로 동작 시킬 수도 있을 것입니다.
select the_date as "date", round(months_between(first_date,second_date),0) months_old ,product,extract(year from the_date) year ,case when a=b then "c" else "d" end tough_one from ... where ...from 키워드 다음은 어떤 것도 신경 쓸 필요가 없습니다. 이렇게 보면, 쉼표나 from 키워드 바로 앞의 것에만 집중하면 됩니다. 그러나 함수의 괄호안에도 쉼표가 있기 때문에 쉼표를 기준으로 분리하는 것으로는 충분하지 않습니다.
my $lparen = qr/(/; my $rparen = qr/)/; my $keyword = qr/(?i:select|from|as)/; # 이 문제에서 필요로 하는 키워드 전부 my $comma = qr/,/; my $text = qr/(?:w+|"w+"|"w+")/; my $op = qr{[-=+*/<>]};텍스트 매치는 무언가 부족해 보일 것입니다. 아마 여러분은 정규표현식 처리를 위해 Regexp::Common 모듈을 사용하고 싶겠지만 지금은 단순하게 그대로 두겠습니다.
sub lexer { my $sql = shift; return sub { LEXER: { return ["KEYWORD", $1] if $sql =~ /G ($keyword) /gcx; return ["COMMA", ""] if $sql =~ /G ($comma) /gcx; return ["OP", $1] if $sql =~ /G ($op) /gcx; return ["PAREN", 1] if $sql =~ /G $lparen /gcx; return ["PAREN", -1] if $sql =~ /G $rparen /gcx; return ["TEXT", $1] if $sql =~ /G ($text) /gcx; redo LEXER if $sql =~ /G s+ /gcx; } }; } my $lexer = lexer($sql); while (defined (my $token = $lexer->())) { # 토큰을 이용해 작업하기 }이것이 어떻게 동작하는지 자세히 설명하지 않아도, 이것이 최고의 해답은 아니라는 것을 말하는 것은 당연합니다. 원래의 펄 몽크스 글타래를 통해, 여러분은 원하는 것을 추출하기 위해서 데이터를 두 번 처리(two pass) 해야한다는 것을 알것입니다. 독자들의 연습을 위해 이 부분의 설명은 하지 않겠습니다.
use HOP::Lexer "make_lexer"; my @sql = $sql; my $lexer = make_lexer( sub { shift @sql }, [ "KEYWORD", qr/(?i:select|from|as)/ ], [ "COMMA", qr/,/ ], [ "OP", qr{[-=+*/]} ], [ "PAREN", qr/(/, sub { [shift, 1] } ], [ "PAREN", qr/)/, sub { [shift, -1] } ], [ "TEXT", qr/(?:w+|"w+"|"w+")/, &text ], [ "SPACE", qr/s*/, sub {} ], ); sub text { my ($label, $value) = @_; $value =~ s/^[""]//; $value =~ s/[""]$//; return [ $label, $value ]; }이것은 확실히 읽기에 전혀 쉬워 보이지 않지만, 일단 조금만 기다려 보세요.
[ $label, $pattern, $optional_subroutine ]$label 은 토큰의 이름입니다. $pattern 은 레이블이 구분하는 것을 일치시킵니다. 세 번째 인자인 $optional_subroutine 은 함수 레퍼런스 입니다. 함수 레퍼런스는 레이블과 레이블이 매치시킨 텍스트를 인자로 받은 후, 여러분이 토큰으로써 원하는 것을 토큰 이름과 실제 토큰의 값 쌍의 형태로 반환합니다.
[ "KEYWORD", qr/(?i:select|from|as)/ ],토큰을 만들기 전에 데이터를 변화시키는 예제는 다음과 같습니다:
[ "TEXT", qr/(?:w+|"w+"|"w+")/, &text ],앞서 언급한 것 처럼, 정규표현식이 좀 부족해보이지만, 일단은 그대로 두고 &text 함수에 집중하겠습니다:
sub text { my ($label, $value) = @_; $value =~ s/^[""]//; $value =~ s/[""]$//; return [ $label, $value ]; }이것은, 레이블과 그 값을 취한 다음, 값에서 처음과 끝에 나오는 인용 부호를 제거하고, 레이블과 값을 배열 레퍼런스로 반환 하는 것을 의미합니다.
[ "SPACE", qr/s*/, sub {} ],이제 여러분은 여러분의 렉서를 가지게 되었습니다. 동작하도록 해보겠습니다. 컬럼 별칭(alias)은 괄호 안에 있는 TEXT 가 아니라 쉼표나 from 키워드 바로 앞의 TEXT 임을 기억해야 합니다. 어떻게 괄호 안에 있음을 알 수 있을까요? 약간의 꼼수를 사용해보겠습니다:
[ "PAREN", qr/(/, sub { [shift, 1] } ], [ "PAREN", qr/)/, sub { [shift, -1] } ],이렇게 하면, 여는 괄호를 만나면 1을 더하고, 닫는 괄호를 만나면 1을 뺍니다. 결과 값이 0일 때면, 여러분은 괄호 밖에 있음을 알 수 있습니다.
while ( defined (my $token = $lexer->() ) { ... }토큰은 이렇게 보일 것입니다:
[ "KEYWORD", "select" ] [ "TEXT", "the_date" ] [ "KEYWORD", "as" ] [ "TEXT", "date" ] [ "COMMA", "," ] [ "TEXT", "round" ] [ "PAREN", 1 ] [ "TEXT", "months_between" ] [ "PAREN", 1 ]그 이후도 마찬가지 입니다.
01: my $inside_parens = 0; 02: while ( defined (my $token = $lexer->()) ) { 03: my ($label, $value) = @$token; 04: $inside_parens += $value if "PAREN" eq $label; 05: next if $inside_parens || "TEXT" ne $label; 06: if (defined (my $next = $lexer->("peek"))) { 07: my ($next_label, $next_value) = @$next; 08: if ("COMMA" eq $next_label) { 09: print "$valuen"; 10: } 11: elsif ("KEYWORD" eq $next_label && "from" eq $next_value) { 12: print "$valuen"; 13: last; # 끝났군요! 14: } 15: } 16: }이것은 매우 직관적이지만 몇가지 기교적인 부분이 있습니다. 각각의 토큰은 배열 레퍼런스이며 그 레퍼런스 안에는 두 개의 요소 가 있습니다. 그러므로 3 번째 줄에서 그 두 요소인 토큰 이름과 토큰 값을 명시적으로 분리합니다. 4, 5 번째 줄에서는 괄호를 처리하기 위해 앞서 말한 기교를 부립니다. 5 번째 줄에서는 또한 토큰 이름이 TEXT 가 아니면 역시 무시합니다.
#!/usr/bin/perl use strict; use warnings; use HOP::Lexer "make_lexer"; # # SQL 명령문을 HEREDOC을 이용해서 저장 # my $sql = <<"END_SQL"; select the_date as "date", round(months_between(first_date,second_date),0) months_old ,product,extract(year from the_date) year ,case when a=b then "c" else "d" end tough_one from XXX END_SQL # # 렉서 생성 # my @sql = $sql; # 지금은 하나의 sql을 처리하지만 그 이상도 가능 my $lexer = make_lexer( sub { shift @sql }, # 반복자(iterator) [ "KEYWORD", qr{ (?i:select|from|as) }x ], [ "COMMA", qr{ , }x ], [ "OP", qr{ [-=+*/] }x ], [ "PAREN", qr{ ( }x, sub { [shift, 1] } ], [ "PAREN", qr{ ) }x, sub { [shift, -1] } ], [ "TEXT", qr{ (?:w+|"w+"|"w+") }x, &text ], [ "SPACE", qr{ s* }x, sub {} ], ); # # TEXT 토큰 값의 앞 뒤 인용부호 제거 # sub text { my ( $label, $value ) = @_; $value =~ s{ ^ [""] }{}x; $value =~ s{ [""] $ }{}x; return [ $label, $value ]; } # # 렉서가 반환하는 토큰을 이용해서 별칭(alias)를 찾는다. # my $inside_parens = 0; while ( defined ( my $token = $lexer->() ) ) { my ( $label, $value ) = @$token; $inside_parens += $value if "PAREN" eq $label; next if $inside_parens || "TEXT" ne $label; if ( defined ( my $next = $lexer->("peek") ) ) { my ( $next_label, $next_value ) = @$next; if ( "COMMA" eq $next_label ) { print "$valuen"; } elsif ( "KEYWORD" eq $next_label && "from" eq $next_value ) { print "$valuen"; last; # 끝났군요! } } }컬럼 별칭의 출력 결과는 다음과 같습니다:
date months_old product year tough_one이제 여러분의 작업이 끝난 것 같나요? 아뇨. 아마도 아닐 것입니다. 이제 여러분은 첫 번째 문제에서 언급했던 많은 다른 SQL 예제가 필요할 것입니다. 아마도 &text 함수가 부족할 수도 있습니다. 아마도 여러분이 빠뜨린 다른 연산자가 있을 수도 있습니다. 아마도 SQL에 소수점이 들어간 숫자가 있을 수도 있습니다. 여러분이 직접 데이터를 렉싱해야 할 때는 실제의 데이타를 일치시키기 위해 렉서를 미세조정하는데 몇 번의 시도를 해야할 것입니다.
이전 글 : 세계의 어휘목록 : 사이트에 공동작업을 통한 번역기능을 추가하기
다음 글 : Behavior Driven Development Using Ruby (Part 3) - (1)
최신 콘텐츠