LoginSignup
4
5

More than 5 years have passed since last update.

PHP/Pythonによる正規表現の扱い

Last updated at Posted at 2017-05-20

PHP/Pythonで正規表現を扱う

PHPで開発する際に割と利用するpreg_xxxと正規表現を備忘録としてまとめたかったので、ここに記載。
いずれも結局は必要に応じて変更して利用しているが、それなりのベースとして利用している。
また、Python3で同様な結果が得られるかどうかも併せて記載。

PHPのpreg_matchを使用した例

日時

これについては、まだまだ精査する必要があるが、あくまで正規表現の例として。入力するケースや利用用途で都度変更している。

正規表現

/\A(\d{4})*[-\/]*(\d{1,2})[-\/]*(\d{1,2}) *((\d{1,2}):(\d{1,2})(:(\d{1,2}))*)*\Z/

表記 意味
/ デリミタ
\A 文字列の最初
() 集合として扱う
\d{4} 4桁の数値
* 直前のパターンの0回以上の繰り返し
[] 文字集合、[]内のいずれかの文字
\d{1,2} 1桁以上2桁以下の数値
\Z 文字列の最後

コードと結果

PHPの場合
<?php

function pick_date(string $date) :array {
    if (preg_match('/\A(\d{4})*[-\/]*(\d{1,2})[-\/]*(\d{1,2}) *((\d{1,2}):(\d{1,2})(:(\d{1,2}))*)*\Z/', $date, $matches)) {
        return [
            'Y' => isset($matches[1]) ?intval($matches[1]) : -1,
            'm' => isset($matches[2]) ?intval($matches[2]) : -1,
            'd' => isset($matches[3]) ?intval($matches[3]) : -1,
            'H' => isset($matches[5]) ?intval($matches[5]) : -1,
            'i' => isset($matches[6]) ?intval($matches[6]) : -1,
            's' => isset($matches[8]) ?intval($matches[8]) : -1
        ];
    } else {
        return [];
    }
}

print_r(pick_date('2017-07-03 13:15:03'));
print_r(pick_date('2017-07-3 13:01'));
print_r(pick_date('2017/07/03 13'));
print_r(pick_date('2017/07-3 13:1:3'));
print_r(pick_date('201773 13:00'));
実行結果
Array
(
    [Y] => 2017
    [m] => 7
    [d] => 3
    [H] => 13
    [i] => 15
    [s] => 3
)
Array
(
    [Y] => 2017
    [m] => 7
    [d] => 3
    [H] => 13
    [i] => 1
    [s] => -1
)
Array
(
)
Array
(
    [Y] => 2017
    [m] => 7
    [d] => 3
    [H] => 13
    [i] => 1
    [s] => 3
)
Array
(
    [Y] => 2017
    [m] => 7
    [d] => 3
    [H] => 13
    [i] => 0
    [s] => -1
)
Python3の場合

PHPと異なり、デリミタは不要。
また、並び順を意識しなければOrderedDictではなくdictでも問題ない。
PHPと同様な出力結果がfindallで得られる。

import re
from collections import OrderedDict


def pick_date(date):
    pattern = r'\A(\d{4})*[-\/]*(\d{1,2})[-\/]*(\d{1,2}) *((\d{1,2}):(\d{1,2})(:(\d{1,2}))*)*\Z'
    match = re.findall(pattern, date)
    try:
        elements = match[0]
        return OrderedDict((
            ('Y', elements[0]),
            ('m', elements[1]),
            ('d', elements[2]),
            ('H', elements[4]),
            ('i', elements[5]),
            ('s', elements[7])
        ))
    except IndexError:
        return OrderedDict()

print(pick_date('2017-07-03 13:15:03'))
print(pick_date('2017-07-3 13:01'))
print(pick_date('2017/07/03 13'))
print(pick_date('2017/07-3 13:1:3'))
print(pick_date('201773 13:00'))
出力結果
OrderedDict([('Y', '2017'), ('m', '07'), ('d', '03'), ('H', '13'), ('i', '15'), ('s', '03')])
OrderedDict([('Y', '2017'), ('m', '07'), ('d', '3'), ('H', '13'), ('i', '01'), ('s', '')])
OrderedDict()
OrderedDict([('Y', '2017'), ('m', '07'), ('d', '3'), ('H', '13'), ('i', '1'), ('s', '3')])
OrderedDict([('Y', '2017'), ('m', '7'), ('d', '3'), ('H', '13'), ('i', '00'), ('s', '')])

備考

この場合、シナリオによっては正規表現を使わずに以下の方法が良いかもしれない。厳密なものであればこちらになるだろうか。

PHPの場合
<?php

date_default_timezone_set('Asia/Tokyo');

function pick_date(string $date) : array {
    $dt = new DateTime();

    return [
        'y' => $dt->setTimestamp(strtotime($date))->format('Y'),
        'm' => $dt->format('m'),
        'd' => $dt->format('d'),
        'H' => $dt->format('H'),
        'i' => $dt->format('i'),
        's' => $dt->format('s'),
    ];
}

print_r(pick_date('2017-07-03 13:15:03'));
print_r(pick_date('2017-07-3 13:01'));
print_r(pick_date('2017/07/03 13'));
print_r(pick_date('2017/07-3 13:1:3'));
print_r(pick_date('201773 13:00'));
出力結果
Array
(
    [y] => 2017
    [m] => 07
    [d] => 03
    [H] => 13
    [i] => 15
    [s] => 03
)
Array
(
    [y] => 2017
    [m] => 07
    [d] => 03
    [H] => 13
    [i] => 01
    [s] => 00
)
Array
(
    [y] => 1970
    [m] => 01
    [d] => 01
    [H] => 09
    [i] => 00
    [s] => 00
)
Array
(
    [y] => 1970
    [m] => 01
    [d] => 01
    [H] => 09
    [i] => 00
    [s] => 00
)
Array
(
    [y] => 1970
    [m] => 01
    [d] => 01
    [H] => 09
    [i] => 00
    [s] => 00
)
Python3の場合

datetimestrptimeで一旦変換する方法を考えたが、PHPのstrtotimeと違いformatを指定する必要があるので、似たようなことができない。

emailのフォーマットバリデーション

emailのフォーマットに準拠しているかどうかバリデーションを行う。参考はこちら

正規表現

/\A^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$\Z/

表記 意味
/ デリミタ
\A 文字列の最初
^[] []内のいずれかの文字(文字集合)で始まる
a-z a~zまでの文字
() 集合として扱う
+ 直前のパターンの1回以上の繰り返し
\. ドット(".")を判別するためエスケープ
* 直前のパターンの0回以上の繰り返し
[a-z]{2,} 2桁以上のアルファベット小文字
\Z 文字列の最後

コードと結果

PHPの場合
<?php

function validate_email_format(string $email) : int {
    return preg_match('/\A^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$\Z/', $email);
}

print(validate_email_format('test@test.com'). PHP_EOL);
print(validate_email_format('.test@test.com'). PHP_EOL);
print(validate_email_format('Test@test.com'). PHP_EOL);
print(validate_email_format('test@test.c'). PHP_EOL);
print(validate_email_format('test@testcom'). PHP_EOL);
出力結果
1
0
0
0
0
Python3の場合
import re


def validate_email_format(email):
    pattern = r'\A^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$\Z'
    return 1 if re.match(pattern, email) else 0

print(validate_email_format('test@test.com'))
print(validate_email_format('.test@test.com'))
print(validate_email_format('Test@test.com'))
print(validate_email_format('test@test.c'))
print(validate_email_format('test@testcom'))
出力結果
1
0
0
0
0

URLのフォーマットバリデーション

URLのフォーマットに準拠しているかどうかバリデーションを行う(この場合、ftpも考慮)。こちらを参考にした。

正規表現

/^(https?|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?([0-9]+)?\/?/i

表記 意味
/ デリミタ
() 式集合、
^() 式集合で始まる
? 直前のパターンがあってもなくてもいい
| ORの意味
[] 文字集合、[]内のいずれかの文字
(?:) キャプチャしない集合
+ 直前のパターンの1回以上の繰り返し
* 直前のパターンの0回以上の繰り返し
i 大文字小文字を区別しない

コードと結果

PHPの場合
<?php

function pick_url(string $url) : array {
    if (preg_match('/^(https?|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?([0-9]+)?\/?/i', $url, $matches)) {
        return [
            $matches[0] ?? "",
            $matches[1] ?? ""
        ];
    } else {
        return ["", ""];
    }
}

print_r(pick_url('http://test.xxx?a=b'));
print_r(pick_url('https://test.xxx/a/b/'));
print_r(pick_url('ftp://test.xxx'));
print_r(pick_url('ftps://test.xxx'));
print_r(pick_url('https:///test.xxx'));
出力結果
Array
(
    [0] => http://test.xxx
    [1] => http
)
Array
(
    [0] => https://test.xxx/
    [1] => https
)
Array
(
    [0] => ftp://test.xxx
    [1] => ftp
)
Array
(
    [0] =>
    [1] =>
)
Array
(
    [0] =>
    [1] =>
)
Python3の場合
import re


def pick_url(url):
    pattern = r'^(https?|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?([0-9]+)?\/?'
    match = re.compile(pattern, re.IGNORECASE).findall(url)
    try:
        elements = match[0]
        return [elements[0], elements[1]]
    except IndexError:
        return ["", ""]

print(pick_url('http://test.xxx?a=b'))
print(pick_url('https://test.xxx/a/b/'))
print(pick_url('ftp://test.xxx'))
print(pick_url('ftps://test.xxx'))
print(pick_url('https:///test.xxx'))
出力結果
['http', 'test.xxx']
['https', 'test.xxx']
['ftp', 'test.xxx']
['', '']
['', '']

PHPのpreg_replaceを使用した例

単語の置き換え

この例では'abc'という単語を'ABC'と置き換えるサンプル。当時は単語単位で書き換える方法に、窮した経験があるのでメモ。実際これも厳格的ではないので、ケースによって変更している。

正規表現

/(\b)(abc)(\b)/

表記 意味
/ デリミタ
() 式集合
\b 英単語の境界となる文字にマッチ

コードと結果

PHPの場合
<?php

function replace_abc(string $s) : string {
    return preg_replace('/(\b)(abc)(\b)/', '${1}ABC${3}', $s);
}

print(replace_abc('abcd abc" dabcd abc'). "\n");
print(replace_abc('abc dabc abc d "abc"'). "\n");
print(replace_abc('abcd +abc" abc abc?'). "\n");
print(replace_abc('a!abc \'abc\' sabcs abc\'s!'). "\n");
print(replace_abc('ababc? \'abc?\' sabcs abc!?'). "\n");
出力結果
abcd ABC" dabcd ABC
ABC dabc ABC d "ABC"
abcd +ABC" ABC ABC?
a!ABC 'ABC' sabcs ABC's!
ababc? 'ABC?' sabcs ABC!?

'${1}ABC${3}'${n}はn番目にマッチした文字を意味する。

Python3の場合

PHPのようにデリミタは不要。また、置換後のフォーマットの指定方法は${1}ではなく、\1のように記載する。

import re


def replace_abc(s):
    return re.sub(r'(\b)(abc)(\b)', r'\1ABC\3', s)

print(replace_abc('abcd abc" dabcd abc'))
print(replace_abc('abc dabc abc d "abc"'))
print(replace_abc('abcd +abc" abc abc?'))
print(replace_abc('a!abc \'abc\' sabcs abc\'s!'))
print(replace_abc('ababc? \'abc?\' sabcs abc!?'))
出力結果
abcd ABC" dabcd ABC
ABC dabc ABC d "ABC"
abcd +ABC" ABC ABC?
a!ABC 'ABC' sabcs ABC's!
ababc? 'ABC?' sabcs ABC!?

PHPのpreg_replace_callback、preg_splitを使用した例

csvファイル

下記のようなダブルクォートを含むcsvのカラムを取得することを想定する。

test.csv
a,b,c
"a,b","b,c","c,d"
"a,b,\"a,b\",,c","a,,b,,",c
"a,,","a,b,\"a,b,c\",c,d,","a\"a,b\",c"
想定している結果(()は識別しやすくするために記載)
(a), (b), (c)
(a,b), (b,c), (c,d)
(a,b,\"a,b\",,c), (a,,b,,), (c)
(a,,), (a,b,\"a,b,c\",c,d,), (a\"a,b\",c)

正規表現を使わない方法

PHPの場合

ダブルクォートの中にダブルクォートがあるケースではうまくいかない。方法があるかもしれないけど、ここでは正規表現を使うということで。なお、この方法では、preg_split('/,(?!")/', $columns);で抽出するようなイメージだろうか。
こういった件について、調べてみると結構プログラミングでごりごり抽出するやり方が多かった。正規表現で実現してみる。

csv.php
<?php

$file = fopen("test.csv", "r");

if ($file) {
    while (($columns = fgetcsv($file, 0, ',', '"', '"')) !== FALSE) {
        print_r($columns);
    }
}

fclose($file);
出力結果
Array
(
    [0] => a
    [1] => b
    [2] => c

)
Array
(
    [0] => "a
    [1] => b","b
    [2] => c","c
    [3] => d"

)
Array
(
    [0] => "a
    [1] => b
    [2] => \"a
    [3] => b\"
    [4] =>
    [5] => c","a
    [6] =>
    [7] => b
    [8] => ,"
    [9] => c

)
Array
(
    [0] => "a
    [1] => ,","a
    [2] => b
    [3] => \"a
    [4] => b
    [5] => c\"
    [6] => c
    [7] => d,","a\"a
    [8] => b\"
    [9] => c"
)
Python3の場合

かなり精度が高いように思える。例のtest.csvが多少強引過ぎるように思えるが、実際に似たようなcsvに遭遇したので、何とも言えない。

import csv

with open('test.csv') as f:
    r = csv.reader(f)
    for column in r:
        print(column)
出力結果
['a', 'b', 'c']
['a,b', 'b,c', 'c,d']
['a,b,\\a', 'b\\"', '', 'c"', 'a,,b,,', 'c']
['a,,', 'a,b,\\a', 'b', 'c\\"', 'c', 'd', ',a\\"a', 'b\\"', 'c"']

正規表現を使うパターン

正規表現

/(?:\n|\r|\r\n)/

表記 意味
/ デリミタ
() 式集合、
(?:) キャプチャしない集合
| ORの意味
[] 文字集合、[]内のいずれかの文字
+ 直前のパターンの1回以上の繰り返し

※余談になるが、(?:)については、$str = preg_replace('/(?:\n|\r|\r\n)/', '', $str);といった改行コードを除去するときなどにも利用可能

コードと結果
PHPの場合
<?php

function my_generator(string $name) : Iterator {
    $from = function () use ($name) {
        $file = fopen($name, "r");

        if ($file) {
            while ($line = fgets($file)) {
                yield $line;
            }
        }
        fclose($file);
    };

    yield from $from();
}

$pattern = '/\"(?:\\\"|[^\"])+\"/';
$bks = [];
foreach (my_generator("test.csv") as $v) {
    // "で括られた値を保管し、uniqueなidで置き換える
    $columns = preg_replace_callback($pattern, function ($matches) use (&$bk) {
        $index = uniqid();
        $bk[$index] = $matches[0];
        return $index;
    }, $v);
    // 改行コードを削除
    $columns = preg_split('/,/', preg_replace("/\r|\n/", "", $columns));
    // idで置き換えていた値を元に戻す
    $new_columns = array_map(function ($column) use ($bk) {
        if (!empty($bk) && array_key_exists($column, $bk)) {
            return $bk[$column];
        } else {
            return $column;
        }
    }, $columns);
    print_r($new_columns);
}
出力結果
Array
(
    [0] => a
    [1] => b
    [2] => c
)
Array
(
    [0] => "a,b"
    [1] => "b,c"
    [2] => "c,d"
)
Array
(
    [0] => "a,b,\"a,b\",,c"
    [1] => "a,,b,,"
    [2] => c
)
Array
(
    [0] => "a,,"
    [1] => "a,b,\"a,b,c\",c,d,"
    [2] => "a\"a,b\",c"
)
Python3の場合
import re
import uuid

bks = {}


def my_generator():
    with open('test.csv') as lines:
        yield from lines


def repl(m):
    index = str(uuid.uuid4())
    bks[index] = m.group(0)
    return index

pattern = r'\"(?:\\\"|[^\"])+\"'
for k, v in enumerate(my_generator()):
    columns = re.sub(pattern, repl, v).rstrip('\r\n').split(",")
    new_columns = []
    for c in columns:
        if c in bks:
            new_columns.append(bks[c])
        else:
            new_columns.append(c)
    print(new_columns)

出力結果
['a', 'b', 'c']
['"a,b"', '"b,c"', '"c,d"']
['"a,b,\\"a,b\\",,c"', '"a,,b,,"', 'c']
['"a,,"', '"a,b,\\"a,b,c\\",c,d,"', '"a\\"a,b\\",c"']

備考

ダブルクォートの中のダブルクォートがエスケープされていないケースもあるようだ。
""a,b",c", "a,b,"a","b"", cといった表記でも対応できるようなコードにも挑戦してみたい。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5