#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の場合
datetime
のstrptime
で一旦変換する方法を考えたが、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のカラムを取得することを想定する。
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);
で抽出するようなイメージだろうか。
こういった件について、調べてみると結構プログラミングでごりごり抽出するやり方が多かった。正規表現で実現してみる。
<?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
といった表記でも対応できるようなコードにも挑戦してみたい。