2016/10/18 20:58:19

PHP正規表現で「言明」を使ってみる

php-logo
目次(クリックするとジャンプします)
  • 1:どうやって抽出しよう?
  • 2:言明をつかって抽出してみる
  • 2.1:肯定の言明
  • 2.2:否定の言明
  • 3:戻り読み言明
  • 3.1:否定の戻り読み言明
  • 3.2:戻り読みは固定長で
  • 3.3:選択肢などの場合は固定長であればOK
  • 4:まとめ

どうやって抽出しよう?

たとえばこんな例文があったとします。(変な例文)

mackell- Herring- tuna+ amberjack= Horsemackerel- Pacificsaury+

単語の末尾になにやら記号がついていますが、「-」と「+」がついた単語を記号は無しで抽出したいと思います。

どうやって抽出したらいいのだろか?

まず思いついたのはこんな感じなパターン

/[a-z]+[-|+]/

でもこれで抽出すると

array(5) {
  [0]=>
  array(1) {
    [0]=>
    string(8) "mackell-"
  }
  [1]=>
  array(1) {
    [0]=>
    string(7) "erring-"
  }
  [2]=>
  array(1) {
    [0]=>
    string(5) "tuna+"
  }
  [3]=>
  array(1) {
    [0]=>
    string(13) "orsemackerel-"
  }
  [4]=>
  array(1) {
    [0]=>
    string(12) "acificsaury+"
  }
}

記号ついてきちゃいますね…。別途記号を削る処理をするべきか…。

言明をつかって抽出してみる

こんな時のために「言明(assertion)」というテクニックがあります。「言明」とは場所を特定するパターンというか…目印を設定するというか…そんな感じのパターンです。

assertionには主張するという意味がありますから、「ここだと主張する」と言ったような意味なのかな。

言明で指定されたパターンは要素として抽出されません。なので今回のように「目印にしたい文字自体はいらない」という場合に便利です。

言明には「肯定の言明(positive assertion)」と「否定の言明(negative assertion)」があります。

以下のような感じです。(patternは例文)

肯定の言明 否定の言明
意味 patternなやつ patternじゃないやつ
書き方 (?=pattern) (?!pattern)

肯定の言明

実際にさっきの例文を使って言明をつかってみます。まず肯定の言明から

/[a-z]+(?=-|+)/

ちょっと複雑ですが、(?=-|+)の部分が言明のパターンです。バックスラッシュはクオートでは「または」を表しますので、これは「-+がある位置」という言明になります。

スクリプトは以下です。

実行結果は以下。

array(5) {
  [0]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
  [1]=>
  array(1) {
    [0]=>
    string(6) "erring"
  }
  [2]=>
  array(1) {
    [0]=>
    string(4) "tuna"
  }
  [3]=>
  array(1) {
    [0]=>
    string(12) "orsemackerel"
  }
  [4]=>
  array(1) {
    [0]=>
    string(11) "acificsaury"
  }
}

ちゃんと記号は無しで単語を抽出できていますね。

否定の言明

では同じ例文でパターンを変えたスクリプトで否定の言明を試してみます。

否定の場合「じゃない」になるので、この場合は「-+じゃない」≒「=じゃない」 にしたらどうでしょう。

/[a-z]+(?!=)/

抽出してみます。

array(6) {
  [0]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
  [1]=>
  array(1) {
    [0]=>
    string(6) "erring"
  }
  [2]=>
  array(1) {
    [0]=>
    string(4) "tuna"
  }
  [3]=>
  array(1) {
    [0]=>
    string(8) "amberjac"
  }
  [4]=>
  array(1) {
    [0]=>
    string(12) "orsemackerel"
  }
  [5]=>
  array(1) {
    [0]=>
    string(11) "acificsaury"
  }
}

あれおかしいな…。全部抽出されている。でもよくみると=が付いていたamberjackkがありません。

直接=と接しているのはkなので、(?!=)kの手前を指すことになります。つまり今回の場合では「cまでを抽出する」ということになります。

amberjac k=

否定の言明で未知の単語を抽出するのは難しいかもしれません。単語がわかっていて存在する個数を計りたい時なんかには良いかもしれません。

例えばこんなの。

mackell- mackell- mackell+ mackell= mackell- mackell+

こんな感じのパターンで抽出してみます。

/mackell(?!=)/

スクリプトはこう。

array(5) {
  [0]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
  [1]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
  [2]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
  [3]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
  [4]=>
  array(1) {
    [0]=>
    string(7) "mackell"
  }
}
string(57) "=が末尾に付いていないmackellの数は5でした"

戻り読み言明

例えばこんな感じのパターンがあったとします。

/(?!foo)bar/

今までの経験から考えると、foo以外に続くbarにマッチしそうですが、これはどこのbarでもマッチしてしまいます。

パターンは左から評価されるので、fooがあろうがなかろうが、barのパターンは生きてしまうようです。

こんなときのために「戻り読みの言明」という方法があります。

イメージとしては

まずbarが見つかる→barの前の文字列にfooがあるかな?→ないよ→パターンにマッチするbarだ!!

という感じです。戻って確認する感じなので戻り読みなんでしょうね。きっと…。

肯定の戻り読みの言明 否定の戻り読みの言明
意味 戻って読んでpatternなやつ 戻って読んでpatternじゃないやつ
書き方 (?<=pattern) (?<!pattern)

肯定の戻り読み言明

テストしてみます。まずは肯定から。

こんな感じの例文です。

foobar yoobar foobar moobar foobar

fooが前に付いているbarを抽出してみます。正しく抽出できれば3つbarが抽出されるはずです。

/(?<=foo)bar/

スクリプトはこう。

結果はこうなりました。

array(3) {
  [0]=>
  array(1) {
    [0]=>
    string(3) "bar"
  }
  [1]=>
  array(1) {
    [0]=>
    string(3) "bar"
  }
  [2]=>
  array(1) {
    [0]=>
    string(3) "bar"
  }
}

ちゃんと抽出できていますね。

否定の戻り読み言明

では同じ例文とパターンだけ変えたスクリプトで否定の戻り読み言明も試してみます。

fooが付いていないbarを抽出します。2つbarが抽出されるはずです。

/(?<!foo)bar/

実行してみました。

array(2) {
  [0]=>
  array(1) {
    [0]=>
    string(3) "bar"
  }
  [1]=>
  array(1) {
    [0]=>
    string(3) "bar"
  }

ちゃんと2つ抽出されていますね。

戻り読みは固定長で

戻り読み言明の場合、パターンは「固定長」である必要があります。

たとえばこんな感じでfooの後に続くパターンが可変長であると戻り読み言明は失敗します。

/(?<!foo)[a-z]+/

実際さっきのスクリプトで試してみました。

array(5) {
  [0]=>
  array(1) {
    [0]=>
    string(6) "foobar"
  }
  [1]=>
  array(1) {
    [0]=>
    string(6) "yoobar"
  }
  [2]=>
  array(1) {
    [0]=>
    string(6) "foobar"
  }
  [3]=>
  array(1) {
    [0]=>
    string(6) "moobar"
  }
  [4]=>
  array(1) {
    [0]=>
    string(6) "foobar"
  }
}

全部抽出されていますよね。言明が失敗しています。

選択肢などの場合は固定長であればOK

長さが変わらなければいいので、選択肢などは各パターンの長さが違っていてもちゃんと使えます。

こんな例文で試してみます。

foobar yoobar foobar moogoobar foobar

選択肢はで表します。この場合foo以外のものに続くbargoobarが抽出できるはずです。

/(?<!foo)bar|goobar/

さっきのスクリプトで実行してみました。

array(2) {
  [0]=>
  array(1) {
    [0]=>
    string(3) "bar"
  }
  [1]=>
  array(1) {
    [0]=>
    string(6) "goobar"
  }
}

ちゃんと抽出できていますね。

まとめ

言明は「目印にしたい文字自体はいらない」抽出の場合に便利。

肯定の言明 否定の言明 肯定の戻り読み言明 否定の戻り読み言明
意味 fooが続くbar fooが続かないbar fooに続くbar foo以外に続くbar
パターン bar(?=foo) bar(?!foo) (?<=foo)bar (?<!foo)bar

戻り読みの言明の場合は可変長パターンは失敗する。