Más contenido relacionado
La actualidad más candente (20)
Similar a 正規表現リテラルは本当に必要なのか? (20)
正規表現リテラルは本当に必要なのか?
- 1. 正規表現リテラルは
本当に必要なのか?
Makoto Kuwata
kwa@kuwata-lab.com
http://www.kuwata-lab.com/
copyright© 2014 kuwata-lab.com all rights reserved
PyConJP 2014
ver 1.1 (2014-09-17): スライドを追加・加筆
- 2. 発表の背景
✓ 2013年末、プログラミング言語における「正規
表現リテラルの必要性」が支持を集める
http://togetter.com/li/603521
http://blog.kazuhooku.com/2013/12/blog-post.html
✓ Pythonは正規表現リテラルがないけど、別に困
ってないよ?
…と説明しても聞いちゃくれないPerler/Rubyist/JavaScripterと、
正規表現リテラルがなくて困ってるJavaユーザと、どんなときでも
叩かれるPHPerが入り乱れた、異種言語間お笑いバトル
copyright© 2014 kuwata-lab.com all rights reserved
- 9. 正規表現リテラルがあると、正規表現は書きやす
い?
## 正規表現リテラルあり
/[a-z]+/.exec(string) # JavaScript
## 正規表現リテラルなし
(new RegExp("[a-z]+")).exec(string) # JavaScript
copyright© 2014 kuwata-lab.com all rights reserved
- 10. 正規表現リテラルがなくても、正規表現を書きやす
くできる
## 正規表現リテラルなし
function re(pattern) { # JavaScript
return new RegExp(pattern);
}
re("[a-z]+").exec(string)
関数やライブラリで解決できる
copyright© 2014 kuwata-lab.com all rights reserved
- 12. raw文字列リテラル (Python) やシングルクォー
ト(JS, PHP)でも発生しない
## JavaScript
new RegExp('^dd:dd:dd$')
## Python
re.match(r"^dd:dd:dd$", timestr)
## PHP
preg_match('/^dd:dd:dd$/', timestr)
copyright© 2014 kuwata-lab.com all rights reserved
- 13. ただしraw文字列相当のない言語、お前はダメだ
## Java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
Pattern pat = Pattern.compile("^dd:dd$");
Matcher m = pat.matcher(timestr);
二重バックスラッシュが辛いです ;(
copyright© 2014 kuwata-lab.com all rights reserved
- 15. とはいえ、raw文字列リテラルを導入するには
言語仕様の拡張が必要
## Java
Pattern pat = Pattern.compile(r"^dddd$");
Matcher m = pat.matcher(input);
raw文字列リテラルがほしいけど、
言語仕様を拡張しないと無理じゃん ;(
copyright© 2014 kuwata-lab.com all rights reserved
- 16. そもそも、バックスラッシュでないとだめなん?
## 先行事例: Cのprintf()やJavaのString.format()
String.format("s=%s, n=%d", s, n);
メタキャラクタとして「%」を使っている!
二重バックスラッシュのような問題がない!
copyright© 2014 kuwata-lab.com all rights reserved
- 17. バックスラッシュ以外を使えば、Javaでも正規表
現が書きやすくなる! (最初は違和感あるけど慣れの問題)
## http://kwatch.houkagoteatime.net/blog/2013/12/28/java-regex/
import static benry.rexp.Rexp.rexp;
import benry.rexp.Matched;
String pat = "^(`d`d`d`d)-(`d`d)-(`d`d)$";
Matched m = rexp(pat).match(str);
if (m != null) {
System.out.println(m.get(1));
}
copyright© 2014 kuwata-lab.com all rights reserved
「」のかわりに
「`」を使ってる
(変更も可能)
- 19. ✓ 正規表現リテラルのないJavaでは、
正規表現が書きにくい
✓ 正規表現リテラルのあるPerlやRubyでは、
正規表現が書きやすい
✓ だから、プログラミング言語に
正規表現リテラルは必要だ!
・問題:解決方法 = 1:N
(*注)
・one of themでしかない方法を、only oneな方法だと勘違いしてる
(*注) もちろん、複数の解決方法の間では優劣が存在する。この場合なら、「正規表現リテ
ラルが必要」と主張するには、それが他の方法より優れていることを説明する必要がある。
copyright© 2014 kuwata-lab.com all rights reserved
← わかる
← わかる
← その理屈はおかしい
- 20. ここまでのまとめ
✓ 正規表現を書きやすくする言語機能は、ひとつで
はない
正規表現リテラル、raw文字列リテラル
✓ 正規表現リテラルよりraw文字列リテラルのほう
が望ましい
正規表現以外にも利用可能だし、言語仕様も肥大化しない
✓ そもそも、バックスラッシュを使わなければいい
言語仕様の拡張が必要ないので、今すぐ使える方法
copyright© 2014 kuwata-lab.com all rights reserved
- 22. 正規表現リテラルなら、コンパイルは1度だけ
## Ruby
100.times do
filename =~ /.(png|gif|jpe?g)$/
end
100回コンパイルされたりはしない
(ただし埋め込み式のある場合は別)
copyright© 2014 kuwata-lab.com all rights reserved
- 23. 正規表現リテラルがない場合はどうなる?
## Python
for _ in range(100):
re.search(r'.(png|gif|jpe?g)', filename)
re.search() の中で毎回コンパイル
されてそうだから、遅いのでは?
copyright© 2014 kuwata-lab.com all rights reserved
- 24. ライブラリがキャッシュすれば無問題
## Python
_cache = {}
def _compile(patstr):
## キャッシュがあればそれを返す
try:
キャッシュを活用
return _cache[patstr]
except KeyError:
pass
## なければコンパイルして
## キャッシュする
pat = sre_compile(patstr)
_cache[patstr] = pat
return pat
def search(patstr, s):
pat = _compile(patstr)
return pat.search(s)
def sub(patstr, rep, s):
pat = _compile(patstr)
return pat.sub(rep, s)
注:実際の正規表現ライブラリ (re.py) では、正規表現フラグつきでキャッシュしたり、
キャッシュが大きくなりすぎるとパージするなど、もっと複雑である。
copyright© 2014 kuwata-lab.com all rights reserved
他の関数は _compile()
を呼び出す
- 25. どうしても気になる場合は、正規表現オブジェク
トを変数で保持すればよい
## Python
rexp = re.compile(r'.(png|gif|jpe?g)')
for _ in range(100):
re.search(rexp, filename)
キャッシュから取り出す
オーバーヘッドがなくなる
(通常は気にするほどではない)
copyright© 2014 kuwata-lab.com all rights reserved
- 26. なおCPythonでは、正規表現ライブラリより
文字列関数のほうがかなり速い
copyright© 2014 kuwata-lab.com all rights reserved
startswith()
endswith()
isdigit()
文字列関数正規表現
文字列関数の速度を100
としたときのグラフ
https://gist.github.com/kwatch/f923fb5a71da3f69eccb
https://gist.github.com/kwatch/0132268e0c38741fe59a
https://gist.github.com/kwatch/e1bc95fcc6cb75c60c94
- 27. また正規表現に対する高度な最適化は、リテラル
の有無ではなく、処理系の評価戦略次第
// Rust (http://doc.rust-lang.org/regex/)
#![feature(phase)]
#[phase(plugin)]
extern crate regex_macros;
extern crate regex;
正規表現文字列をコンパイル時に評価
→ 正規表現リテラルがなくても
コンパイル時に間違いを検出
→ 正規表現リテラルがなくても
バイナリを生成可能
fn main() {
let re = regex!(r"^d{4}-d{2}-d{2}$");
assert_eq!(re.is_match("2014-01-01"), true);
}
copyright© 2014 kuwata-lab.com all rights reserved
- 28. ここまでのまとめ
✓ キャッシュを使えば、正規表現が毎回コンパイル
されることはない
正規表現リテラルがなくても充分な性能は出せる
✓ 正規表現より文字列関数のほうが高速
少なくともCPythonではそう
✓ リテラルがなくても正規表現のコンパイル時評価
は可能
「リテラルの有無」と「処理系の評価戦略」は、基本的に別個の話
copyright© 2014 kuwata-lab.com all rights reserved
- 31. 文字列関数で済む範囲であれば、正規表現より
文字列関数のほうが読みやすい、わかりやすい
## 正規表現
re.match(r"^d+$", input)
re.match(r"^http://", string)
re.search(r".(png|gif|jpg)$", filename)
毎日コード書いてる人なら
覚えられるだろうけど・・・
## 文字列関数
input.isdigit()
string.startswith("http://")
filename.endswith((".png", ".gif", ".jpg"))
初級者でもわかりやすい!
copyright© 2014 kuwata-lab.com all rights reserved
- 32. また正規表現は落とし穴も多いので、初級者には
つらいことも
## Ruby
string =~ /.html$/
厳密には間違い
(正解は /.htmlz/ )
## Ruby
string.end_with?(".html")
初級者でも間違えない!
copyright© 2014 kuwata-lab.com all rights reserved
- 33. とはいえ、上達するにつれ、正規表現を避けるこ
とはできない
## 正規表現
pat = r"^(d{4})-(dd)-(dd)[ T](dd):(dd):
dd)$"
re.match(pat, input)
文字列関数でこれを書くのは
つらい
copyright© 2014 kuwata-lab.com all rights reserved
- 35. コア言語仕様を肥大化させて
でも文字列関数を減らすこと
がそんなに重要なの?
それ正規表現リテラル
じゃなくて正規表現の
メリットじゃないの?
“正規表現リテラルがあれば
文字列関数を減らせるし、
処理系も単純にできるよ!”
誰にとってのメリットなの?
ときどきしかコードを書かない
ライトユーザにも嬉しいことなの?
(科学者、統計学者、CGデザイナ、etc)
そんな簡単な話ではないはず・・・
「/..../」のパースは単純なの?
文字列関数が減るかわりに
別の複雑さが増えてない?
copyright© 2014 kuwata-lab.com all rights reserved
- 36. ここまでのまとめ
✓ 文字列関数だけのほうが学習コストは低い
学習コスト: 正規表現 >>> 文字列関数
✓ とはいえ正規表現の勉強はどのみち必要
学習コスト: 正規表現 < 文字列関数+正規表現
✓ 正規表現やリテラルの得失は一概には言えない
だれにとってのメリット?どのくらいのメリット?
✓ そもそも「正規表現リテラルの得失」と「正規表
現の得失」は別の話
ちゃんと分けて議論しましょう
copyright© 2014 kuwata-lab.com all rights reserved
- 38. 第2部:Python正規表現ライブラリの
問題点と解決案
✓ re.match()とre.search()の2つがある
✓ ライブラリの使い方が2系統ある
✓ 正規表現がいつもキャッシュされてしまう
✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
- 39. 第2部:Python正規表現ライブラリの
問題点と解決案
✓ re.match()とre.search()の2つがある
✓ ライブラリの使い方が2系統ある
✓ 正規表現がいつもキャッシュされてしまう
✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
- 40. re.match()は先頭からのマッチングしかできない、
re.search()なら途中からのマッチングも可
## これはマッチする
re.match(r"(d+)", "123abc")
re.search(r"(d+)", "abc123")
## これはマッチしない!(先頭にないので)
re.match(r"(d+)", "abc123")
re.match(r"pat", str) は re.search(r"^pat", str) で代用できる。
re.match() は混乱のもとだし、いらないのでは?
copyright© 2014 kuwata-lab.com all rights reserved
- 41. 第2部:Python正規表現ライブラリの
問題点と解決案
✓ re.match()とre.search()の2つがある
✓ ライブラリの使い方が2系統ある
✓ 正規表現がいつもキャッシュされてしまう
✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
- 42. 追加スライド
Pythonの正規表現ライブラリは、使い方が2系統
存在する
re.compile().xxxx() 系re.xxxx() 系
大抵の正規表現操作が、モジュールレベルの関数
と、 コンパイル済み正規表現のメソッドとして提
供されることに注意して下さい。関数は正規表現
オブジェクトのコンパイルを必要としない近道です
が、いくつかのチューニング変数を失います。
copyright© 2014 kuwata-lab.com all rights reserved
“
”
引用元: http://docs.python.jp/3.3/library/re.html
- 43. しかも、両者は似ているようで微妙に違う ;(
これは困る
## re.compile().xxxx() 系
re.compile(pat, flags).match(string, pos, endpos)
re.compile(pat, flags).sub(repl, string, count)
## re.xxxx() 系
re.match(pat, string, flags)
re.sub(pat, repl, string, count, flags)
re.sub() は、 Python2.6では正規表
現フラグが指定できなかった
copyright© 2014 kuwata-lab.com all rights reserved
追加スライド
開始位置と終了位置が、re.compile().match()
では指定できるが re.match() ではできない
- 44. ところで re.xxxx() のやっていることは、内部で
re.compile().xxxx() を呼び出しているだけ
def compile(pattern, flags=0):
return _compile(pattern, flags)
def match(pattern, string, flags=0):
return _compile(pattern, flags)
.match(string)
def sub(pattern, repl, string, count=0, flags=0):
return _compile(pattern, flags)
.sub(repl, string, count)
copyright© 2014 kuwata-lab.com all rights reserved
- 45. だったら、全部 re.compile().xxxx() を使うように
すれば、re.xxxx() をなくして一本化できるよね?
re.compile()へのショートカット
rx = re._compile
## マッチング
m = re.match(r"(d+)", "123abc") # before
m = rx(r"(d+)").match("123abc") # after
## 文字列置換
re.sub(r".gif$", ".png", filename) # before
rx(r".gif$").sub(".png", filename) # after
copyright© 2014 kuwata-lab.com all rights reserved
- 46. 一本化できれば、「似てるけど微妙に違う2系統」
が共存しなくてすむ
## re.compile().xxxx() 系
re.compile(pat, flags).match(string, pos, endpos)
re.compile(pat, flags).sub(repl, string, count)
等価 (当然)
## rx().xxxx() 系
rx = re._compile
rx(pat, flags).match(string, pos, endpos)
rx(pat, flags).sub(repl, string, count)
copyright© 2014 kuwata-lab.com all rights reserved
追加スライド
- 48. 第2部:Python正規表現ライブラリの
問題点と解決案
✓ re.match()とre.search()の2つがある
✓ ライブラリの使い方が2系統ある
✓ 正規表現がいつもキャッシュされてしまう
✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
- 50. 特に、たくさんの正規表現がデータとして与えられ
ると、キャッシュが無駄に肥大化してしまう
urlpatterns = patterns('',
url(r'^posts/$', "..."),
url(r'^posts/new$', "..."),
url(r'^posts/(?P<id>d+)$', "..."),
url(r'^posts/(?P<id>d+)/comments$', "..."),
url(r'^posts/(?P<id>d+)/edit$', "..."),
...
copyright© 2014 kuwata-lab.com all rights reserved
)
コンパイルするとすべて強制的にキャッシュされる
→ キャッシュする必要のないデータによって
キャッシュが肥大化する
- 51. Pythonの正規表現ライブラリは、キャッシュが肥
えすぎるとすべてパージしてしまう!
_cache = {}
_MAXCACHE = 512
キャッシュが肥大化する
→ キャッシュがパージされる
→ 性能低下 ;(
def _compile(pattern, flags):
...(snip)...
p = sre_compile.compile(pattern, flags)
if not bypass_cache:
if len(_cache) >= _MAXCACHE:
_cache.clear()
_cache[type(pattern), pattern, flags] = p
return p
copyright© 2014 kuwata-lab.com all rights reserved
- 52. キャッシュせずにコンパイルする機能が、公式に用
意されるとうれしい
class HTMLHelper(object):
_ESCAPE = re.sre_compile.compile(r"[&<>"']")
これならキャッシュしないので、
キャッシュの無駄な肥大化を防げる
(しかしunofficialなので使用には注意すること)
copyright© 2014 kuwata-lab.com all rights reserved
- 53. 第2部:Python正規表現ライブラリの
問題点と解決案
✓ re.match()とre.search()の2つがある
✓ ライブラリの使い方が2系統ある
✓ 正規表現がいつもキャッシュされてしまう
✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
- 54. 複数の正規表現にマッチさせるとき、こう書きたい
## ほんとはこう書きたい
if m = re.match(pat1, text):
x, y = m.groups()
elif m = re.match(pat2, text):
y, z = m.groups()
elif m = re.match(pat3, text):
z, x = m.groups()
文法エラー:
Pythonでは代入文は式ではないので、
if文の条件式には書けない
copyright© 2014 kuwata-lab.com all rights reserved
- 55. でもPythonではこう書くしかない
m = re.match(pat1, text)
if m:
x, y = m.groups()
copyright© 2014 kuwata-lab.com all rights reserved
else:
m = re.match(pat2, text)
if m:
y, z = m.groups()
else:
m = re.match(pat3, text)
if m:
z, x = m.groups()
if文のネストが深くなる
- 56. 関数+return や、while文+break という手も
あるが、あまり嬉しくはない
while 1:
m = re.match(pat1, text)
if m:
x, y = m.groups()
break
m = re.match(pat2, text)
if m:
y, z = m.groups()
break
m = re.match(pat3, text)
if m:
z, x = m.groups()
break
copyright© 2014 kuwata-lab.com all rights reserved
break
if文のネストは減ったけど、
トリッキーで間違えやすい
- 58. copyright© 2014 kuwata-lab.com all rights reserved
実装はこちら
class matching(object):
def __init__(self, string):
self.string = string
self.matched = None
http://bit.ly/matching_py
def match(self, pattern, flags=0):
self.matched = re.compile(pattern, flags)
.match(self.string)
return self.matched
def groups(self, *args):
return self.matched.groups(*args)
- 60. おまけ: benry.rexp
https://pypi.python.org/pypi/benry
from benry.rexp import rx
## re.compile() へのショートカット
m = rx(r'pat', rx.I).match(string, start, end)
## キャッシュせずにコンパイル
rexp = rx.compile(r'pat', rx.I)
## 連続したマッチング
m = rx.matching(string)
if m.match(r'^(dddd)-(dd)-(dd)$'):
Y, M, D = m.groups()
else m.match(r'(dd)/(dd)/(dddd)$'):
M, D, Y = m.groups()
copyright© 2014 kuwata-lab.com all rights reserved