LPeg – Parsing Expression Grammars For Luaの使い方

LPegは Luaで文字列の検出・検索をするときに使えるLibraryです。

所謂Pattern Matching Library。文字列のfind/matchなんかに使えます。

Lua標準のPatternより強力。そしてわりと速い。

正規表現に慣れてる人には奇妙な感じがすると思います(私も最初見た瞬間にう゛ってなりました)。

ただ、再利用がしやすかったりするし、何より 呪文の様な正規表現よりも遥かに可読性が高いのが良いです。

Luaで文字列処理をする場合には使えるようになっておくと良いかと思います。

なお、LPegの元になっている Parsing Expression Grammars については正直良く理解しておりません(^^;。

不勉強で申し訳ないです。

使い方

モジュールのロード

まずはモジュールをrequireする。当たり前だけどモジュールはコンパイルしてロードできる場所に置いておかないといけない。

local lpeg = require('lpeg')

基本的なパターン

LPegでは 検索する対象=パターンを作成する事で文字列の検出・検索を行います。

以下の3種類のパターンが基本。これらをうまく組み合わせて文字列の検出・検索に使います。

一つ目。lpeg.P

これは現在の場所からマッチする文字列を直接指定する。

多分、Pは PatternのP(でも、紛らわしいので以後、この文章では単にPと言う事にする)

local phrase = lpeg.P('exact match')

これで正規表現の ‘^exact match’ だと思えば良い。

二つ目。lpeg.S

これは 文字列の中のどれか一文字にマッチするパターン。

多分Sは Setの S

+-の記号のどちらかが欲しければ以下のようにする。

local plus_minus = lpeg.S('+-')

これで正規表現の ‘^[+-]’だと思えば良い

三つ目。lpeg.R

これも文字セットの一種だけど範囲指定ができるので記述が楽になる。

範囲指定は2文字の文字列で同時にいくつも出来る。

多分、Rは RangeのR。

local alpha_num = lpeg.R('az', 'AZ', '09')

これで正規表現の ‘^[a-zA-Z0-9]’ だと思えば良い。

特殊だけど良く使うパターン

結構重要だと思うので先に解説しておく。

なんでも一文字にマッチ

Pで数値を指定すれば良い。

local any = lpeg.P(1)

正規表現の ‘^.’ です。

行頭じゃなくてもマッチするパターン

今までの説明で正規表現の場合、必ず 行頭からの一致を意味する ‘^’ がついていたと思います。

LPegで普通に作ったマッチは必ず先頭から一致する事になります。

文中のどこかに出現する 文字列を検索したい場合は こんな風にします。

-- @param p LPegで作ったマッチ
local function anywhere(p)
    return lpeg.P{ p + 1 * lpeg.V(1) }
end

local find_string = anywhere(lpeg.P('anywhere match'))

まずは関数 anywhere を定義して、

検索したいパターンを anywhereに渡して 関数から帰ってきた合成されたパターンを使います。

※ こんな感じで LPegではパターンを合成して使えます。

蛇足。関数の中でやってる事の説明。

単なる再帰です。

lpeg.V(1)は 自分自身を参照していて p + 1 * lpeg.V(1) ※V(1)の1の意味なんかはまた後で説明

  • は or
  • は 単純な連結

    anywhere = pattern | ‘.’ anywhere

現在の場所でパターンが見つから無い場合は ‘.’ で一文字先送りにして anywhereに再帰して一致を検出します。

ちょっとまて、’.’ は lpeg.P(1) じゃないのか? 単に数値の 1 が書いてあるっておかしくね?バグじゃね?って人もいると思いますが、そこはまた後で説明します。

よりLPegらしい書き方

そろそろ記述方法が鬱陶しくなってきたので、LPegらしい?書き方に変えていきます。

ここから下は 以下の宣言を前提に書く事にします。

local lpeg = require('lpeg')
local P = lpeg.P
local S = lpeg.S
local R = lpeg.R

Luaでは 「文字列リテラル一つだけ」または「テーブルコンストラクタ{}一つだけ」が引数の場合、関数呼び出しの括弧()を省略できます。

なので、この宣言を頭に書いておくと

P"word"

S"+-"

R"09"

といった書き方が可能になります。書くのが楽ちんだし、慣れると見やすい。

合成

PSRの三つを自由に組み合わせて検索する事が出来ます。

例えば ある文字列の中から 番号 とそれに続く 数字 を検索したい場合

local num_word = P"番号"
local number = R"09"
local number_phrase = num_word * number

ここでは 演算子 * を使って二つのパターンを合成し、新たな パターンを作っています。

  • は単なるマッチの連結です。上の場合 num_word の後に number が来るとマッチ、という意味になります。

当たり前ですが、再利用を考えなければ以下の様に一行でも書けます

local number_phrase = P"番号" * R"09"

演算子は ほかにもいろいろありますが、説明は後にして そろそろ検索を実行してみましょう。

検索

さて、いよいよ検索です。

作ったパターンに p:match(‘文字列’) で良いです。簡単。

CLIから実行してみます。

> number_phrase = P"number" * R"09"
> = number_phrase:match('number1')
8
> = number_phrase:match('Number1')
nil
> = number_phrase:match('number9')
8
> = number_phrase:match('numberA')
nil

マッチした場合、マッチが完了した位置のインデックスが戻ります。

行頭からの検出だけだとつまらないので文字列の途中の文字を探してみましょう。

さっきの anywhere と組み合わせてみます。

> function anywhere(p) return lpeg.P{ p + 1 * lpeg.V(1) } end
> xevious = anywhere(P"Xevious")
> = xevious:match("Pacman Galaxian Xevious Mappy")
24

とまぁ、こんな感じですが、これも面白くはないですね。

結構長くなったので、とりあえずここまでで一度区切ります。

次回は繰り返しとキャプチャ、演算子の使い方についての補足とかまとめてみよう。