A lexikai elemzés (lexing, lexical analysis) szöveges adat értelmezhető tokenekké alakítása. Az ihletet egy régi cikk1 adta, amely a Higher-order Perl2 című könyv egyik fejezetére épített, ez alapján készült a HOP::Lexer modul is, amely mindössze 60 soros, érdemes tanulmányozni.
Az alábbi program e modult használja SQL parancsok elemzésére. Egy token egy típusból és értékből (lexeme) álló rendezett n-es (tuple). A lexer szubrutin inputjaként adjuk meg a tokentípusaink definícióit (regexp mintáit), amelyek sorrendje is számít: például a sztring literálok mintájának precedenciája magasabb kell legyen a kulcsszavakénál és az azonosítókénál.
#!/usr/bin/env perl
use v5.018;
use utf8;
use strict;
use warnings;
use feature qw(unicode_strings);
use open qw(:std :utf8);
use HOP::Lexer 'string_lexer';
# sql keywords input file
my $KEYWD_FN = 'sql_keywords.txt';
# escaped sinqle quote placeholder
my $ESQP = 'ESCSINGQUOTPH';
### main ###
{
my $sql = read_sql_from_file($ARGV[0]);
my $keywords_str = read_keywords_from_file($KEYWD_FN);
# preprocessing
# replace any ''
$sql =~ s/(?<!')''(?!')/$ESQP/gs;
# replace any ''''
$sql =~ s/(?<!')''''(?!')/$ESQP$ESQP/gs;
# these cases are handled by the lexer:
# '...''' or '''...' or '''...'''
# lexing/tokenization
my $lexer = string_lexer(
$sql,
# comments
['CMT', qr{/\*(?:\*(?!/)|[^*])*\*/}, \&replace_phs],
['CMT', qr/--\N*/, \&replace_phs],
# string literals
['STR', qr/'(?:'')?[^']+(?:'')?'/, \&replace_phs],
# number literals
['NUM', qr/\b{wb}\d+\.?\d*\b{wb}/],
# commas
['COMMA', qr/,/],
# operators
['OP', qr{[-=+*/!<>|]+}],
# parentheses
['PAREN', qr/\(/, sub { [shift, 1] }],
['PAREN', qr/\)/, sub { [shift, -1] }],
# keywords
['KEYWD', qr/\b{wb}(?i:$keywords_str)\b{wb}/],
# identifiers
['IDFR', qr/(?:[\w\.]+|"\w+")/],
# whitespace (discarded)
['SPACE', qr/\s*/, sub {}],
);
my @tokens;
while (defined (my $token = $lexer->())) {
push @tokens => $token;
}
# just print the results
my $paren_level = 0;
foreach my $i (0 .. $#tokens) {
my ($label, $value) = @{$tokens[$i]};
$paren_level += $value if 'PAREN' eq $label;
print "$label\t$paren_level\t$value\n";
}
}
### subroutines ###
sub read_sql_from_file {
my $in_fn = shift // '';
open(my $fh, '<', $in_fn)
or die "Error: cannot open $in_fn: $!\n";
# slurp mode
local $/ = undef;
my $sql = <$fh>;
close $fh;
return $sql
}
sub read_keywords_from_file {
my $in_fn = shift // '';
open(my $fh, '<', $in_fn)
or die "Error: cannot open $in_fn: $!\n";
my @words = <$fh>;
close $fh;
chomp @words;
return join('|', @words)
}
sub replace_phs {
my ($label, $value) = @_;
$value =~ s/$ESQP/''/gs;
return [$label, $value];
}
Egyszerű példa SQL:
select product_id, sum(price) as "összeg"
from orders -- TODO: join products
group by product_id
A tokenizáció eredménye (a típus és az érték közé itt beékeltük a zárójelezés szintjét):
KEYWD 0 select
IDFR 0 product_id
COMMA 0 ,
KEYWD 0 sum
PAREN 1 1
IDFR 1 price
PAREN 0 -1
KEYWD 0 as
IDFR 0 "összeg"
KEYWD 0 from
IDFR 0 orders
CMT 0 -- TODO: join products
KEYWD 0 group by
IDFR 0 product_id
Természetesen önmagában egy lexikai elemző nem sokat ér, az eredményét fel kell dolgozni, mondjuk egy rekurzívan ereszkedő elemző algoritmussal (recursive descent parser). Folyt. köv.
-
Curtis Poe: Lexing Your Data. Perl.com, 2006-01-05. ↩︎
-
Dominus, Mark Jason: Higher-order Perl. Transforming programs with programs. Morgan Kaufmann, 2005. 8. fejezet, 359. o. A szerző szabadon olvashatóvá tette a könyvet honlapján. ↩︎