Featured image of post Perlの正規表現 — 基礎から応用まで

Perlの正規表現 — 基礎から応用まで

Perlの正規表現を基礎から実践まで体系的に解説します。演算子、メタ文字、キャプチャ、先読み/後読み、パフォーマンス最適化、実用的なパターン集などを網羅しています。

はじめに - Perlと正規表現の密接な関係

Perlと正規表現は切っても切れない関係にあります。Larry Wallが1987年にPerlを作った理由の一つが、強力なテキスト処理能力でした。正規表現はPerlの「第一級市民」として言語仕様に組み込まれており、他の言語では関数呼び出しが必要な処理も、Perlでは演算子として直接記述できます。

実際、多くのプログラミング言語の正規表現エンジンは「PCRE(Perl Compatible Regular Expressions)」と呼ばれ、Perl互換を謳っています。つまり、Perlの正規表現がデファクトスタンダードになっているのです。

この記事では、Perlの正規表現を基礎から応用まで体系的に解説します。

正規表現の基本演算子

Perlには正規表現のための専用演算子が用意されています。

マッチング演算子(m//)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use v5.38;

my $text = "Hello, Perl World!";

# 基本的なマッチング
if ($text =~ m/Perl/) {
    say "Found Perl!";  # これが実行される
}

# デリミタは変更可能
if ($text =~ m{World}) {
    say "Found World!";
}

# m を省略できる(デリミタが / の場合のみ)
if ($text =~ /Hello/) {
    say "Found Hello!";
}

# 否定マッチング
if ($text !~ /Python/) {
    say "Python not found!";  # これが実行される
}

置換演算子(s///)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use v5.38;

my $text = "I love Python";

# 基本的な置換
$text =~ s/Python/Perl/;
say $text;  # "I love Perl"

# グローバル置換(g修飾子)
my $message = "foo bar foo baz foo";
$message =~ s/foo/FOO/g;
say $message;  # "FOO bar FOO baz FOO"

# 非破壊的置換(r修飾子、Perl 5.14+)
my $original = "Hello World";
my $replaced = $original =~ s/World/Perl/r;
say $original;  # "Hello World" (元の文字列は変更されない)
say $replaced;  # "Hello Perl"

文字変換演算子(tr///またはy///)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use v5.38;

my $text = "Hello World";

# 小文字を大文字に変換
$text =~ tr/a-z/A-Z/;
say $text;  # "HELLO WORLD"

# 文字数をカウント
my $count = ($text =~ tr/L//);
say "Number of L: $count";  # "Number of L: 3"

# 文字を削除(d修飾子)
my $phone = "090-1234-5678";
$phone =~ tr/-//d;
say $phone;  # "09012345678"

メタ文字とキャラクタークラス

基本的なメタ文字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use v5.38;

my @patterns = (
    [qr/^Hello/,    "Hello World",    "行頭マッチ"],
    [qr/World$/,    "Hello World",    "行末マッチ"],
    [qr/H.llo/,     "Hello",          "任意の1文字(.)"],
    [qr/Hel*o/,     "Heo",            "0回以上の繰り返し(*)"],
    [qr/Hel+o/,     "Hello",          "1回以上の繰り返し(+)"],
    [qr/Hel?o/,     "Helo",           "0回または1回(?)"],
    [qr/Hel{2}o/,   "Hello",          "正確にn回({n})"],
    [qr/\d{3}-\d{4}/, "090-1234",     "数字の繰り返し"],
);

for my $p (@patterns) {
    my ($pattern, $text, $desc) = @$p;
    if ($text =~ $pattern) {
        say "✓ $desc: '$text' matches $pattern";
    } else {
        say "✗ $desc: '$text' does not match $pattern";
    }
}

キャラクタークラス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use v5.38;

# 標準的なキャラクタークラス
my %classes = (
    '\d' => '0123456789',          # 数字
    '\D' => 'abc',                 # 数字以外
    '\w' => 'Hello_123',           # 単語文字
    '\W' => '!@#$%',               # 単語文字以外
    '\s' => " \t\n",               # 空白文字
    '\S' => 'Hello',               # 空白文字以外
);

while (my ($class, $text) = each %classes) {
    if ($text =~ /$class/) {
        say "$class matches in: $text";
    }
}

# カスタムキャラクタークラス
my $hex = "A1B2C3";
if ($hex =~ /^[0-9A-Fa-f]+$/) {
    say "$hex is a valid hex string";
}

# 否定キャラクタークラス
my $no_vowels = "xyz";
if ($no_vowels =~ /^[^aeiou]+$/) {
    say "$no_vowels contains no vowels";
}

キャプチャと後方参照

基本的なキャプチャ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use v5.38;

my $email = 'user@example.com';

if ($email =~ /^([^@]+)@([^@]+)$/) {
    say "Username: $1";     # "user"
    say "Domain: $2";       # "example.com"
}

# キャプチャ変数は特殊変数に保存される
say "Full match: $&";       # "user@example.com"
say "Before match: $`";     # ""
say "After match: $'";      # ""

後方参照

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use v5.38;

# 同じ文字の繰り返しを検出
my $text1 = "bookkeeper";
if ($text1 =~ /(.)\1/) {
    say "Found repeated character: $1$1";  # "oo"
}

# HTMLタグのマッチング
my $html = '<div>content</div>';
if ($html =~ /<(\w+)>.*?<\/\1>/) {
    say "Found tag: $1";  # "div"
}

# パターン内での後方参照
my $text2 = "the the cat";
$text2 =~ s/\b(\w+)\s+\1\b/$1/g;
say $text2;  # "the cat" (重複した単語を削除)

名前付きキャプチャ(Perl 5.10+)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use v5.38;

my $date = "2025-12-05";

if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) {
    say "Year: $+{year}";      # "2025"
    say "Month: $+{month}";    # "12"
    say "Day: $+{day}";        # "05"
}

# 名前付きキャプチャを使った置換
my $log = "Error at 2025-12-05 10:30:00";
$log =~ s/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/$+{y}\/$+{m}\/$+{d}/;
say $log;  # "Error at 2025/12/05 10:30:00"

修飾子の活用

よく使う修飾子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use v5.38;

my $text = "Hello\nWorld\nPerl";

# i修飾子: 大文字小文字を区別しない
if ($text =~ /hello/i) {
    say "Case-insensitive match!";
}

# g修飾子: グローバルマッチ
my @words = $text =~ /\w+/g;
say "Words: @words";  # "Words: Hello World Perl"

# s修飾子: . が改行にもマッチ
if ($text =~ /Hello.+Perl/s) {
    say "Matched across lines!";
}

# m修飾子: ^ と $ が各行の先頭・末尾にマッチ
my @lines = $text =~ /^.+$/mg;
say "Lines found: " . scalar(@lines);  # "Lines found: 3"

# x修飾子: 空白とコメントを無視(後述)

r修飾子による非破壊的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use v5.38;

my $original = "foo bar baz";

# 従来の方法(破壊的)
my $copy1 = $original;
$copy1 =~ s/foo/FOO/;

# r修飾子を使った方法(非破壊的、推奨)
my $copy2 = $original =~ s/foo/FOO/r;

say "Original: $original";  # "foo bar baz"
say "Copy1: $copy1";        # "FOO bar baz"
say "Copy2: $copy2";        # "FOO bar baz"

# チェーンも可能
my $result = $original
    =~ s/foo/FOO/r
    =~ s/bar/BAR/r
    =~ s/baz/BAZ/r;
say "Result: $result";  # "FOO BAR BAZ"

先読み・後読み(Lookaround)

肯定先読み(Positive Lookahead)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use v5.38;

# パスワード検証: 最低8文字で、数字を含む
my $password1 = "pass1234";
my $password2 = "password";

for my $pass ($password1, $password2) {
    if ($pass =~ /^(?=.*\d).{8,}$/) {
        say "$pass: Valid password";
    } else {
        say "$pass: Invalid password";
    }
}
# 出力:
# pass1234: Valid password
# password: Invalid password

# 複数条件のチェック
my $strong_pass = "Abc123!@";
if ($strong_pass =~ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%]).{8,}$/) {
    say "$strong_pass is a strong password";
}

否定先読み(Negative Lookahead)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use v5.38;

# 特定の拡張子以外のファイルをマッチ
my @files = qw(
    document.txt
    image.jpg
    script.pl
    data.json
    backup.bak
);

my @non_backup = grep { /\.(?!bak$)\w+$/ } @files;
say "Non-backup files: @non_backup";
# "Non-backup files: document.txt image.jpg script.pl data.json"

肯定後読み(Positive Lookbehind)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use v5.38;

# 金額の数値部分のみを抽出
my $price = "Price: $1,234.56";
if ($price =~ /(?<=\$)[\d,]+\.?\d*/) {
    say "Amount: $&";  # "Amount: 1,234.56"
}

# プロトコルを除いたドメインを抽出
my $url = "https://www.example.com/path";
if ($url =~ /(?<=:\/\/)[^\/]+/) {
    say "Domain: $&";  # "Domain: www.example.com"
}

否定後読み(Negative Lookbehind)

1
2
3
4
5
6
use v5.38;

# $記号が前にない数値をマッチ
my $text = "The price is $100 but we have 50 items";
my @numbers = $text =~ /(?<!\$)\b\d+\b/g;
say "Non-price numbers: @numbers";  # "Non-price numbers: 50"

/x修飾子を使った読みやすい正規表現

複雑な正規表現は読みにくくなりがちです。/x修飾子を使うと、空白やコメントを含めることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
use v5.38;

# 読みにくい正規表現
my $email_regex_compact = qr/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

# /x修飾子で読みやすく
my $email_regex_readable = qr/
    ^                       # 行頭
    [a-zA-Z0-9._%+-]+       # ローカルパート(ユーザー名)
    @                       # アットマーク
    [a-zA-Z0-9.-]+          # ドメイン名
    \.                      # ドット
    [a-zA-Z]{2,}            # トップレベルドメイン
    $                       # 行末
/x;

my $email = "user@example.com";
if ($email =~ $email_regex_readable) {
    say "$email is valid";
}

# より複雑な例: URLの解析
my $url_regex = qr{
    ^
    (?<protocol> https? )       # プロトコル
    ://
    (?<domain>                  # ドメイン
        (?:
            [a-zA-Z0-9-]+       # サブドメイン
            \.
        )*
        [a-zA-Z0-9-]+           # ドメイン名
        \.
        [a-zA-Z]{2,}            # TLD
    )
    (?:
        :
        (?<port> \d+ )          # ポート番号(オプション)
    )?
    (?<path> /[^\s]* )?         # パス(オプション)
    $
}x;

my $url = "https://www.example.com:8080/path/to/page";
if ($url =~ $url_regex) {
    say "Protocol: $+{protocol}";
    say "Domain: $+{domain}";
    say "Port: " . ($+{port} // 'default');
    say "Path: " . ($+{path} // '/');
}

実用的なパターン集

メールアドレスのバリデーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use v5.38;

sub validate_email {
    my $email = shift;
    
    # RFC 5322準拠の簡易版
    my $regex = qr/
        ^
        [a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+   # ローカルパート
        @
        [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
        (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*
        $
    /x;
    
    return $email =~ $regex;
}

my @emails = (
    'user@example.com',
    'invalid@',
    'no-at-sign.com',
    'user+tag@example.co.jp',
);

for my $email (@emails) {
    my $status = validate_email($email) ? "✓" : "✗";
    say "$status $email";
}

URLの抽出と解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use v5.38;

my $text = <<'TEXT';
Visit https://www.example.com and http://blog.example.org:8080/post/123
or ftp://files.example.net/download
TEXT

# URLを抽出
my @urls = $text =~ m{
    \b
    (?<protocol> https? | ftp )
    ://
    (?<domain> [a-zA-Z0-9.-]+ )
    (?::(?<port>\d+))?
    (?<path> /[^\s]* )?
}gx;

say "Found URLs:";
while ($text =~ m{
    \b
    (?<protocol> https? | ftp )
    ://
    (?<domain> [a-zA-Z0-9.-]+ )
    (?::(?<port>\d+))?
    (?<path> /[^\s]* )?
}gx) {
    say "  Protocol: $+{protocol}";
    say "  Domain: $+{domain}";
    say "  Port: " . ($+{port} // 'default');
    say "  Path: " . ($+{path} // '/');
    say "  ---";
}

日付のパース

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
use v5.38;

sub parse_date {
    my $date_str = shift;
    
    # YYYY-MM-DD形式
    if ($date_str =~ m{
        ^
        (?<year>  \d{4} )
        -
        (?<month> \d{2} )
        -
        (?<day>   \d{2} )
        $
    }x) {
        return {
            year  => $+{year},
            month => $+{month},
            day   => $+{day},
            format => 'ISO8601',
        };
    }
    
    # MM/DD/YYYY形式
    if ($date_str =~ m{
        ^
        (?<month> \d{1,2} )
        /
        (?<day>   \d{1,2} )
        /
        (?<year>  \d{4} )
        $
    }x) {
        return {
            year  => $+{year},
            month => sprintf("%02d", $+{month}),
            day   => sprintf("%02d", $+{day}),
            format => 'US',
        };
    }
    
    return undef;
}

my @dates = ('2025-12-05', '12/05/2025', 'invalid');

for my $date (@dates) {
    my $parsed = parse_date($date);
    if ($parsed) {
        say "$date -> $parsed->{year}-$parsed->{month}-$parsed->{day} ($parsed->{format})";
    } else {
        say "$date -> Invalid format";
    }
}

ログファイルの解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use v5.38;

my $log = <<'LOG';
2025-12-05 10:30:15 [INFO] Application started
2025-12-05 10:30:22 [ERROR] Connection failed: timeout
2025-12-05 10:31:05 [WARN] Retrying connection
2025-12-05 10:31:10 [INFO] Connected successfully
LOG

my $log_regex = qr{
    ^
    (?<date>     \d{4}-\d{2}-\d{2} )
    \s+
    (?<time>     \d{2}:\d{2}:\d{2} )
    \s+
    \[(?<level>  \w+ )\]
    \s+
    (?<message>  .+ )
    $
}xm;

my @errors;
my @warnings;

while ($log =~ /$log_regex/g) {
    my %entry = %+;
    
    if ($entry{level} eq 'ERROR') {
        push @errors, \%entry;
    } elsif ($entry{level} eq 'WARN') {
        push @warnings, \%entry;
    }
}

say "Errors found: " . scalar(@errors);
for my $error (@errors) {
    say "  [$error->{date} $error->{time}] $error->{message}";
}

say "\nWarnings found: " . scalar(@warnings);
for my $warn (@warnings) {
    say "  [$warn->{date} $warn->{time}] $warn->{message}";
}

Perl独自の高度な機能

コード実行((?{ code }))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use v5.38;

# マッチ中にコードを実行
my $text = "123 456 789";
my $sum = 0;

$text =~ /(\d+)(?{ $sum += $1 })/g;
say "Sum of numbers: $sum";  # "Sum of numbers: 1368"

# より実用的な例: 動的なバリデーション
my $max_length = 10;
my $input = "hello";

if ($input =~ /^(.{1,$max_length})(?{ length($1) <= $max_length })$/) {
    say "$input is within length limit";
}

条件分岐((?(condition)yes-pattern|no-pattern))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use v5.38;

# HTMLタグのマッチング(閉じタグの有無を確認)
my $html1 = '<div>content</div>';
my $html2 = '<img src="test.jpg">';

my $tag_regex = qr{
    <
    (?<tag> \w+ )
    (?<attrs> [^>]* )
    >
    (?(1)                       # もしタグ名がキャプチャされていれば
        (?<content> .*? )
        </\g{tag}>              # 対応する閉じタグが必要
    )
}x;

for my $html ($html1, $html2) {
    if ($html =~ $tag_regex) {
        say "$html: Valid HTML tag";
    } else {
        say "$html: Invalid HTML tag";
    }
}

再帰的パターン((?R))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use v5.38;
use feature 'say';

# 括弧のバランスをチェック
sub check_balanced {
    my $str = shift;
    
    # (?-1)は直前のキャプチャグループを再帰的に呼び出す
    my $balanced = qr{
        \(
        (?:
            [^()]+ |
            (?-1)       # 再帰
        )*
        \)
    }x;
    
    return $str =~ /^$balanced$/;
}

my @tests = (
    '(hello)',
    '(hello (world))',
    '((nested) (groups))',
    '(unbalanced',
    '(too) many)',
);

for my $test (@tests) {
    my $status = check_balanced($test) ? "✓" : "✗";
    say "$status $test";
}

正規表現のデバッグ

use re ‘debug’を使ったデバッグ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use v5.38;
use re 'debug';

# デバッグ情報を表示
my $text = "hello world";
if ($text =~ /h(.+)d/) {
    say "Matched: $1";
}

# STDERRに詳細なマッチング情報が出力される

use re ‘debugcolor’でカラー出力

1
2
3
4
5
6
use v5.38;
use re 'debugcolor';

my $text = "test@example.com";
$text =~ /^([^@]+)@([^@]+)$/;
# カラー付きでデバッグ情報が出力される

Regexp::Debuggerモジュールの使用

1
2
3
4
5
6
7
8
use v5.38;
use Regexp::Debugger;

my $text = "The year is 2025";
$text =~ /The\s+year\s+is\s+(\d+)/;

# インタラクティブなデバッガーが起動
# ステップ実行、バックトラックの確認などが可能

パフォーマンスのTips

不要なキャプチャを避ける(非キャプチャグループ)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use v5.38;
use Benchmark qw(cmpthese);

my $text = "hello world" x 1000;

cmpthese(10000, {
    'with_capture' => sub {
        $text =~ /(hello) (world)/g;
    },
    'without_capture' => sub {
        $text =~ /(?:hello) (?:world)/g;  # (?:...) は非キャプチャ
    },
});

# without_captureの方が高速

アンカーを活用する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use v5.38;

# 遅い: テキスト全体を探索
my $text = "x" x 10000 . "target";
$text =~ /target/;

# 速い: 末尾から探索
$text =~ /target$/;

# さらに速い: 位置を限定
$text =~ /\Atarget/;  # 文字列の先頭のみ

貪欲vs非貪欲マッチの選択

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use v5.38;

my $html = '<div>content1</div><div>content2</div>';

# 貪欲マッチ: 最初の<div>から最後の</div>までマッチ
if ($html =~ /<div>.*<\/div>/) {
    say "Greedy: $&";
    # "<div>content1</div><div>content2</div>"
}

# 非貪欲マッチ: 最初の<div></div>のペアのみ
if ($html =~ /<div>.*?<\/div>/) {
    say "Non-greedy: $&";
    # "<div>content1</div>"
}

# より効率的: 否定文字クラスを使用
if ($html =~ /<div>[^<]*<\/div>/) {
    say "Optimized: $&";
    # "<div>content1</div>" (さらに高速)
}

qr//でパターンをプリコンパイル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use v5.38;

# 毎回コンパイルされる(遅い)
for my $i (1..1000) {
    my $text = "test$i";
    $text =~ /test\d+/;
}

# 一度だけコンパイル(速い)
my $pattern = qr/test\d+/;
for my $i (1..1000) {
    my $text = "test$i";
    $text =~ $pattern;
}

よくある間違いと落とし穴

1. 貪欲マッチによる予期しない結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use v5.38;

my $html = '<a href="http://example.com">link</a>';

# 間違い: 貪欲マッチで最後の"までマッチ
if ($html =~ /href="(.+)"/) {
    say "Wrong: $1";  # 'http://example.com">link</a'
}

# 正しい: 非貪欲マッチ
if ($html =~ /href="(.+?)"/) {
    say "Correct: $1";  # 'http://example.com'
}

# さらに良い: 否定文字クラス
if ($html =~ /href="([^"]+)"/) {
    say "Better: $1";  # 'http://example.com' (より高速)
}

2. . が改行にマッチしない

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use v5.38;

my $text = "line1\nline2";

# 間違い: . は改行にマッチしない(デフォルト)
if ($text =~ /line1.line2/) {
    say "Matched";
} else {
    say "Not matched";  # これが実行される
}

# 正しい: s修飾子を使う
if ($text =~ /line1.line2/s) {
    say "Matched with /s";  # これが実行される
}

3. キャプチャ変数のスコープ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use v5.38;

my $text = "hello world";

if ($text =~ /(\w+)/) {
    say "Inside: $1";  # "hello"
}

# 間違い: if ブロックの外で使う
say "Outside: $1";  # 未定義の動作(ここでは "hello" だが保証されない)

# 正しい: 変数に保存
my $match;
if ($text =~ /(\w+)/) {
    $match = $1;
}
say "Saved: $match" if defined $match;

4. /gフラグの状態管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use v5.38;

my $text = "foo bar baz";

# 間違い: 前回のマッチ位置が残る
while ($text =~ /(\w+)/g) {
    say $1;
}
# "foo", "bar", "baz"

# 次のマッチは失敗(位置が末尾に残っている)
if ($text =~ /(\w+)/g) {
    say "Found: $1";
} else {
    say "Not found";  # これが実行される
}

# 正しい: pos()でリセット
pos($text) = 0;  # または pos($text) = undef;
if ($text =~ /(\w+)/g) {
    say "Found after reset: $1";  # "foo"
}

5. メタ文字のエスケープ忘れ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use v5.38;

my $url = "http://example.com/path";

# 間違い: . はメタ文字なので任意の文字にマッチ
if ($url =~ /example.com/) {
    say "Matched (but not what we wanted)";
}

# 正しい: quotemetaまたは\Qを使う
if ($url =~ /\Qexample.com\E/) {
    say "Matched literally";
}

# または: quotemeta関数
my $literal = quotemeta("example.com");
if ($url =~ /$literal/) {
    say "Matched with quotemeta";
}

まとめ

Perlの正規表現は非常に強力で、テキスト処理の多くの場面で活躍します。この記事で紹介した内容をマスターすれば、以下のことができるようになります:

  • 基本的なパターンマッチングと置換
  • 複雑なパターンの構築と理解
  • キャプチャと名前付きキャプチャの活用
  • 先読み・後読みを使った高度なマッチング
  • /x修飾子による保守しやすい正規表現の作成
  • 実用的なパターン(メール、URL、日付など)の実装
  • Perl独自の高度な機能の活用
  • パフォーマンスを考慮した最適化
  • よくある間違いの回避

正規表現は最初は難しく感じるかもしれませんが、練習を重ねることで強力な武器になります。小さなパターンから始めて、徐々に複雑なものに挑戦していきましょう。

Perlの正規表現の世界は深淵です。この記事が、その世界への入り口となれば幸いです。Happy Pattern Matching!

参考資料

  • perlre: Perl正規表現の公式ドキュメント
  • perlretut: Perl正規表現チュートリアル
  • perlrequick: Perl正規表現クイックスタート
  • Regexp::Debugger: 正規表現デバッガー
  • YAPE::Regex::Explain: 正規表現を人間が読める形式に変換
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。