読者です 読者をやめる 読者になる 読者になる

Perlの正規表現で名前付きキャプチャとqr//演算子を使ってみた話

少し前にこのようなツール&記事を書いたのだけど、
note103.hateblo.jp

簡単に説明すると、「URLを渡すとMarkdownの書式でそのページタイトル&URLを返してくれる」というもの。

で、そのコードについて、Perl入学式でいつもお世話になっています @xtetsuji さんから指摘があって、この extr サブルーチンの if文にある $4 ってなに? と。

sub extr {
    my $e = shift;
    my ($x, $y);
    if ($e =~ /(.*)(\(http[:s][^\s,;>]+\))(.*)/g) {
        $x = $1 if $1;
        $y = $4 if $4;  #<-これ
        if ($x && $y) { $rest = $1.$4;  #<-これ
        } elsif ($x) { $rest = $x;
        } elsif ($y) { $rest = $y;
        }
    } elsif ($e =~ /(.*)(http[:s][^\s,;>]+)(.*)/g) {
        push @urls, $2;
        $x = $1 if $1;
        $y = $3 if $3;
        if ($x && $y) { $rest = $1.$3;
        } elsif ($x) { $rest = $x;
        } elsif ($y) { $rest = $y;
        }
    }
    extr($rest) if ($rest =~ /(.*)(http[:s][^\s,;>]+)(.*)/);
}

urls/Urls.pm at da66791eccec8fc33e0bd1c7a95994deca1c66d3 · note103/urls · GitHub

んん〜……何って……何だろう……?(笑)
自分でもわからない。というか、よくよく読むと、たぶん、というかどう考えてもその正規表現のパターン部分、

    if ($e =~ /(.*)(\(http[:s][^\s,;>]+\))(.*)/g) {

この http で始まる部分を囲う丸括弧のうち、エスケープしているはずの括弧もキャプチャ対象としてカウントしてしまって、本来存在しないはずの $4 というのを書いてしまった。ということだと思うのだけど、じゃあそれを正しく数え直して $3 にすると、今度はそのif文の中身と、その下のelsif文の中身がほぼ同じになってしまって、そのまま繰り返させてしまうのはさすがに具合が悪い、というか DRY(Don't Repeat Yourself) の原則に反するという新たな問題が……。

しかも、だからといって共通する数行をif文の外に持ち出すと、こんな感じになって、

sub extr {
    my $e = shift;
    my ($x, $y);
    if ($e =~ /(.*)(\(http[:s][^\s,;>]+\))(.*)/g) {
    } elsif ($e =~ /(.*)(http[:s][^\s,;>]+)(.*)/g) {
        push @urls, $2;
    } else {
        next;
    }

    $x = $1 if $1;
    $y = $3 if $3;
    if ($x && $y) { $rest = $1.$3;
    } elsif ($x) { $rest = $x;
    } elsif ($y) { $rest = $y;
    }
    extr($rest) if ($rest =~ /(.*)(http[:s][^\s,;>]+)(.*)/);
}

とりあえず $x, $y への代入部分の重複がなくなったのは良いけれど、$1, $3 をif/else文の外に持ち出すというのは何となくわかりづらいような……という問題にさらに加えて、正規表現のパターンがほぼ同じであるにもかかわらずこれだけ短い間に3回も繰り返し書かれていて、ちょっと綺麗じゃないような……とかなんとか。

などと思っていたら、@xtetsuji さんからのさらなるアドヴァイスで、前者については「名前付きキャプチャを使えば見やすくなるかも」、そして後者については「qr//で正規表現を事前定義しておくと良い場合がある」とのこと。

なるほど。じつは名前付きキャプチャについては、ぼくもちょうどその数日前にこんなツイートをしていたぐらいで、

もう少し実践的に使ってみたいと思っていた。

プラス、qr// についてもいつも q// や qq// や qw// との違いを思い出すのにいちいち時間がかかって面倒だったので、この機会にあらためて勉強してみよう、ということで、いろいろ情報を参照しつつとりあえずこんな感じに。

sub extr {
    my $e = shift;
    my ($x, $y);

    if ($e =~ /(?<prematch>.*)(\($regexp\))(?<postmatch>.*)/g) {
    } elsif ($e =~ /(?<prematch>.*)(?<match>$regexp)(?<postmatch>.*)/g) {
        push @urls, $+{match};
    } else {
        next;
    }

    $x = $+{prematch} if $+{prematch};
    $y = $+{postmatch} if $+{postmatch};
    if ($x && $y) { $rest = $x.$y;
    } elsif ($x) { $rest = $x;
    } elsif ($y) { $rest = $y;
    }
    extr($rest) if ($rest =~ /(.*)($regexp)(.*)/);
}

で、その後に別の部分もちょっと調整しつつ、ひとまず現状(この記事を書いている時点)の最新版はこんな感じで。

DEMO

で、その動いてるところ……は、挙動を改良したわけではないので前回と同じ動画ですが、こんな様子。

1本だけ渡す場合。
f:id:note103:20160505185803g:plain

複数のURLを渡す場合。(1行に複数のURLが入っててもOK)
f:id:note103:20160505185909g:plain

謝辞

今回も @xtetsuji さんには大変お世話になりました。ありがとうございます。 :bow:

また、このあたりのやり取りはPerl入学式の運営用Slackで行われたのですが、校長の @papix さんやその他メンバーの方々も時折絡んで&盛り上げてくれて楽しみながら学ぶことができました。重ねてありがとうございます。

で、そんなPerl入学式ですが、東京・大阪・沖縄の本年度第1回の開催日が近づいてきました。
ざっと日程だけ書き出すと、こんな感じ。

  • Perl入学式 in東京 第1回
    • 2016年5月14日(土)
  • Perl入学式 in大阪 第1回
    • 2016年5月28日(土)
  • Perl入学式 in沖縄 第1回
    • 2016年6月18日(土)

東京編はもう来週ですね。

その他、詳細については公式サイトをご参照ください。
www.perl-entrance.org

ちなみに、沖縄編に関しては、記念すべき第1回開催に向けて沖縄サポーターの方が書かれたブログ記事を絶賛校正中ですので、近日中に以下で公開される予定です。
(というか僕が止めてます……すみません)
perl-entrance.blog.jp

以上です!