sironekotoroの日記

Perl で楽をしたい

Perl入学式の小ネタ:正規表現の文字境界

前振り

あなたはあるブログプラットフォームの開発者です。

昨今流行している新型コロナウイルス「COVID-19」について投稿される記事も多くなってきました。

掲載される情報は正しいものもありますが、アクセス数狙いの過激な文言を含む記事もあります。

そのため、COVID-19に関する記事については画面の上部に「信頼しうるソースへのリンク」を掲示することになりました。

開発者は「covid」「COVID-19」「コロナウイルス」「新型コロナ」「発熱」など、いくつかの単語を用意し、記事を正規表現で検索します。

検索結果にマッチした記事だった場合、画面に注意書きを掲載する、という仕組みを作りました。

今回はCOVIDという文字を含む記事、という想定で書いてみました。判定には以下の正規表現を用います。

$entry =~ /COVID/i 
#!/usr/bin/env perl
use strict;
use warnings;

my @entries = (
    'No.1: covid-19の症状について',
    'No.2: COVID-19の症状について',
    'No.3: COVID19の症状について',
    'No.4: COVIDの症状について',
);

for my $entry (@entries) {
    if ( $entry =~ /COVID/i ) {
        print "WARNING!: $entry\n";
    }
}

# 実行結果
# WARNING!: No.1: covid-19の症状について
# WARNING!: No.2: COVID-19の症状について
# WARNING!: No.3: COVID19の症状について
# WARNING!: No.4: COVIDの症状について

Perl入学式の第3回で学習した範囲です。 /i とオプションをつけることで、大文字小文字にかかわらずマッチさせることが可能です。

しかし・・・?

実装後、いくつかのCOVID-19とは関係ない記事に警告が表示されているとの報告がユーザーから寄せられました。

調査の結果、ニコニコ動画のURLを含む記事も正規表現にマッチする結果となっていることが判明しました。

#!/usr/bin/env perl
use strict;
use warnings;

my @entries
    = (
    '<a href="https://www.nicovideo.jp/watch/sm9">再ブレイク!レッツゴー陰陽師</a>'
    );

for my $entry (@entries) {
    if ( $entry =~ /COVID/i ) {
        print "WARNING!: $entry\n";
    }
}

# 実行結果
# WARNING!: <a href="https://www.nicovideo.jp/watch/sm9">再ブレイク!レッツゴー陰陽師</a>

なるほど、URLの一部に covid という語が含まれています。

本来のCOVID という語のみを検出し、niconico動画のURLを除外するためにはどうすれば良いでしょうか?

文字境界の /b

正規表現の対象語の前後(あるいは必要なところ)に '/b' を置くことで、より精緻にマッチさせることが可能になります。

\b Match a word boundary

文字の境界とはなんぞや?というと下記のリンク先にも書いてありますが、こういうことです。

単語境界(\b)は\W にマッチングする文字列の始まりと終わりを 連想するような、片側を \w、もう片側を \W で挟まれている点です。

perldoc.jp

\w\W についてはPerl入学式でも解説してあります。

\w ... アルファベット, 数字, アンダーバーの1文字 [a-zA-Z0-9_]と同じ意味です.

\W ... アルファベット, 数字, アンダーバー以外の1文字 [^a-zA-Z0-9_]と同じ意味です.

github.com

以上を踏まえて、正規表現を書き直してみます。

$entry =~ /\bCOVID/i 
#!/usr/bin/env perl
use strict;
use warnings;

my @entries
    = (
    'No.1: covid-19の症状について',
    'No.2: COVID-19の症状について',
    'No.3: COVID19の症状について',
    'No.4: COVIDの症状について',
    'No.5: <a href="https://www.nicovideo.jp/watch/sm9">再ブレイク!レッツゴー陰陽師</a>',
    );

for my $entry (@entries) {
    if ( $entry =~ /\bCOVID/i ) {
        print "WARNING!: $entry\n";
    }
}

# 実行結果
# WARNING!: No.1: covid-19の症状について
# WARNING!: No.2: COVID-19の症状について
# WARNING!: No.3: COVID19の症状について
# WARNING!: No.4: COVIDの症状について

無事、niconico動画のURLについてはマッチしなくなりました。

今回、COVIDの前にだけ \b を置いたのは、COVIDの後に「-(ハイフン)数字」や「数字」が続くパターンがあり、文字の境界である \w, \W の判別を行なっていないためです。

  • ハイフンが0個以上ある
  • 数字が0個以上ある

という条件を追加することで、より正確なにマッチさせることが可能になります。(マッチしたものは使わないので?:を使っています)

$entry =~ /\bCOVID(?:(-)?\d+)?\b/i

というわけで

Perl入学式 in東京 第5回は延期となってしまいましたが、こういった小ネタを拾っていきたいと思っています。

後、「これをするにはどうしたら良いか?」とか「これをやりたい」「書いてみたがうまく動かない」みたいなのがある方、Perl入学式Slackに書き込んでいただくと、寄ってたかって回答が来ます。ぜひご利用ください。

うまく動かないコードなどはblogやgistなどに貼り付けていただくと、より質の高い回答が来ると思います。

docs.google.com

元ネタ

noteの深津さんのtweetでした。