正規表現リテラルは本当に必要なのか?
DESCRIPTION
PyConJP2014発表資料。 ・正規表現リテラルは、**あれば便利だけどなくても困らない**(ライブラリでカバーできる)ことを説明する。 ・Pythonの正規表現が抱える問題点とその解決案を紹介する。TRANSCRIPT
copyright© 2014 kuwata-lab.com all rights reserved
正規表現リテラルは本当に必要なのか?
Makoto [email protected]
http://www.kuwata-lab.com/
PyConJP 2014
ver 1.1 (2014-09-17): スライドを追加・加筆
copyright© 2014 kuwata-lab.com all rights reserved
発表の背景✓ 2013年末、プログラミング言語における「正規表現リテラルの必要性」が支持を集めるhttp://togetter.com/li/603521
http://blog.kazuhooku.com/2013/12/blog-post.html
✓ Pythonは正規表現リテラルがないけど、別に困ってないよ?…と説明しても聞いちゃくれないPerler/Rubyist/JavaScripterと、正規表現リテラルがなくて困ってるJavaユーザと、どんなときでも叩かれるPHPerが入り乱れた、異種言語間お笑いバトル
え|マジ
リテラルないの?
正規表現
のはPHPまでだよね!
あれなしで許される
キモ|イ
キャハハ
ハハハハ
Pythonの評判
copyright© 2014 kuwata-lab.com all rights reserved
発表の目的✓ 正規表現リテラルは、あれば便利だけどなくても困らない(ライブラリでカバーできる)ことを説明する
✓ Pythonの正規表現ライブラリが抱える問題点とその解決案を紹介する
copyright© 2014 kuwata-lab.com all rights reserved
発表の対象者✓ Pythonのことをよく知らないPerlerやRubyistやJavaScripter
✓ 他言語との違いが気になるPythonista
✓ 二重バックスラッシュにイライラしてるJavaユーザ
copyright© 2014 kuwata-lab.com all rights reserved
第1部:正規表現リテラルに関する誤解
copyright© 2014 kuwata-lab.com all rights reserved
第1部:正規表現リテラルに関する誤解
✓ 誤解:正規表現リテラルがあったほうが書きやすい✓ 誤解:正規表現リテラルがあったほうが性能がよい✓ 誤解:正規表現のほうが文字列関数より学習コストが低い
copyright© 2014 kuwata-lab.com all rights reserved
第1部:正規表現リテラルに関する誤解
✓ 誤解:正規表現リテラルがあったほうが書きやすい✓ 誤解:正規表現リテラルがあったほうが性能がよい✓ 誤解:正規表現のほうが文字列関数より学習コストが低い
copyright© 2014 kuwata-lab.com all rights reserved
## 正規表現リテラルあり/[a-z]+/.exec(string) # JavaScript
## 正規表現リテラルなし(new RegExp("[a-z]+")).exec(string) # JavaScript
正規表現リテラルがあると、正規表現は書きやすい?
copyright© 2014 kuwata-lab.com all rights reserved
## 正規表現リテラルなしfunction re(pattern) { # JavaScript return new RegExp(pattern);}re("[a-z]+").exec(string)
正規表現リテラルがなくても、正規表現を書きやすくできる
関数やライブラリで解決できる
copyright© 2014 kuwata-lab.com all rights reserved
## 正規表現リテラルあり/^\d\d:\d\d$/ # JavaScript
## 正規表現リテラルなしnew RegExp("^\\d\\d:\\d\\d$") # JavaScript
二重バックスラッシュ問題を発生させないためには正規表現リテラルが必要?
copyright© 2014 kuwata-lab.com all rights reserved
## JavaScriptnew RegExp('^\d\d:\d\d:\d\d$')
## Pythonre.match(r"^\d\d:\d\d:\d\d$", timestr)
## PHPpreg_match('/^\d\d:\d\d:\d\d$/', timestr)
raw文字列リテラル (Python) やシングルクォート(JS, PHP)でも発生しない
copyright© 2014 kuwata-lab.com all rights reserved
## Javaimport java.util.regex.Pattern;import java.util.regex.Matcher;Pattern pat = Pattern.compile("^\\d\\d:\\d\\d$");Matcher m = pat.matcher(timestr);
二重バックスラッシュが辛いです ;(
ただしraw文字列相当のない言語、お前はダメだ
copyright© 2014 kuwata-lab.com all rights reserved
【問題点】Javaで正規表現が書きにくい(二重バックスラッシュがつらい)
【解決策】正規表現リテラル
【解決策】raw文字列リテラル
導入すると、言語仕様が肥大化してしまう
正規表現以外にも利用可能、かつ言語仕様が肥大化しない
copyright© 2014 kuwata-lab.com all rights reserved
## JavaPattern pat = Pattern.compile(r"^\d\d\d\d$");Matcher m = pat.matcher(input);
raw文字列リテラルがほしいけど、言語仕様を拡張しないと無理じゃん ;(
とはいえ、raw文字列リテラルを導入するには言語仕様の拡張が必要
copyright© 2014 kuwata-lab.com all rights reserved
## 先行事例: Cのprintf()やJavaのString.format()String.format("s=%s, n=%d", s, n);
そもそも、バックスラッシュでないとだめなん?
メタキャラクタとして「%」を使っている!二重バックスラッシュのような問題がない!
copyright© 2014 kuwata-lab.com all rights reserved
## 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));}
「\」のかわりに「`」を使ってる(変更も可能)
バックスラッシュ以外を使えば、Javaでも正規表現が書きやすくなる! (最初は違和感あるけど慣れの問題)
copyright© 2014 kuwata-lab.com all rights reserved
【問題点】Javaで正規表現が書きにくい(二重バックスラッシュがつらい)
【解決策】正規表現リテラル
【解決策】raw文字列リテラル
言語仕様の変更が必要なく、ライブラリだけで実現可能
【解決策】バックスラッシュをやめる
copyright© 2014 kuwata-lab.com all rights reserved
✓ 正規表現リテラルのないJavaでは、正規表現が書きにくい
✓ 正規表現リテラルのあるPerlやRubyでは、正規表現が書きやすい
✓ だから、プログラミング言語に正規表現リテラルは必要だ!
・問題:解決方法 = 1:N・one of themでしかない方法を、only oneな方法だと勘違いしてる
(*注) もちろん、複数の解決方法の間では優劣が存在する。この場合なら、「正規表現リテラルが必要」と主張するには、それが他の方法より優れていることを説明する必要がある。
(*注)
← わかる
← わかる
← その理屈はおかしい
copyright© 2014 kuwata-lab.com all rights reserved
ここまでのまとめ✓ 正規表現を書きやすくする言語機能は、ひとつではない正規表現リテラル、raw文字列リテラル
✓ 正規表現リテラルよりraw文字列リテラルのほうが望ましい正規表現以外にも利用可能だし、言語仕様も肥大化しない
✓ そもそも、バックスラッシュを使わなければいい言語仕様の拡張が必要ないので、今すぐ使える方法
copyright© 2014 kuwata-lab.com all rights reserved
第1部:正規表現リテラルに関する誤解
✓ 誤解:正規表現リテラルがあったほうが書きやすい✓ 誤解:正規表現リテラルがあったほうが性能がよい✓ 誤解:正規表現のほうが文字列関数より学習コストが低い
copyright© 2014 kuwata-lab.com all rights reserved
## Ruby100.times do filename =~ /\.(png|gif|jpe?g)$/end
正規表現リテラルなら、コンパイルは1度だけ
100回コンパイルされたりはしない(ただし埋め込み式のある場合は別)
copyright© 2014 kuwata-lab.com all rights reserved
## Pythonfor _ in range(100): re.search(r'\.(png|gif|jpe?g)', filename)
正規表現リテラルがない場合はどうなる?
re.search() の中で毎回コンパイルされてそうだから、遅いのでは?
copyright© 2014 kuwata-lab.com all rights reserved
## 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) では、正規表現フラグつきでキャッシュしたり、キャッシュが大きくなりすぎるとパージするなど、もっと複雑である。
キャッシュを活用他の関数は _compile() を呼び出す
copyright© 2014 kuwata-lab.com all rights reserved
## Pythonrexp = re.compile(r'\.(png|gif|jpe?g)')for _ in range(100): re.search(rexp, filename)
キャッシュから取り出すオーバーヘッドがなくなる(通常は気にするほどではない)
どうしても気になる場合は、正規表現オブジェクトを変数で保持すればよい
copyright© 2014 kuwata-lab.com all rights reserved
なおCPythonでは、正規表現ライブラリより文字列関数のほうがかなり速い
startswith()
endswith()
isdigit()
文字列関数 正規表現
文字列関数の速度を100としたときのグラフ
https://gist.github.com/kwatch/f923fb5a71da3f69eccbhttps://gist.github.com/kwatch/0132268e0c38741fe59ahttps://gist.github.com/kwatch/e1bc95fcc6cb75c60c94
copyright© 2014 kuwata-lab.com all rights reserved
// 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
ここまでのまとめ✓ キャッシュを使えば、正規表現が毎回コンパイルされることはない正規表現リテラルがなくても充分な性能は出せる
✓ 正規表現より文字列関数のほうが高速少なくともCPythonではそう
✓ リテラルがなくても正規表現のコンパイル時評価は可能「リテラルの有無」と「処理系の評価戦略」は、基本的に別個の話
copyright© 2014 kuwata-lab.com all rights reserved
第1部:正規表現リテラルに関する誤解
✓ 誤解:正規表現リテラルがあったほうが書きやすい✓ 誤解:正規表現リテラルがあったほうが性能がよい✓ 誤解:正規表現のほうが文字列関数より学習コストが低い
copyright© 2014 kuwata-lab.com all rights reserved
✓ そもそも、「正規表現リテラルは必要か?」と「正規表現と文字列関数はどちらが学習コストが低いか?」は別の話仮に「正規表現のほうが文字列関数よりわかりやすい」という結論になったとしても、それをもって「正規表現リテラルは必要」とはならない
✓ そのうえで、あえて「どちらが学習コストが低いか?」を論じる。
copyright© 2014 kuwata-lab.com all rights reserved
## 正規表現 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
## Rubystring =~ /\.html$/
## Rubystring.end_with?(".html")
また正規表現は落とし穴も多いので、初級者にはつらいことも
初級者でも間違えない!
厳密には間違い(正解は /\.html\z/ )
copyright© 2014 kuwata-lab.com all rights reserved
## 正規表現pat = r"^(\d{4})-(\d\d)-(\d\d)[ T](\d\d):(\d\d):\d\d)$"re.match(pat, input)
とはいえ、上達するにつれ、正規表現を避けることはできない
文字列関数でこれを書くのはつらい
copyright© 2014 kuwata-lab.com all rights reserved
正規表現だけ 正規表現+文字列関数
学習コストの内訳(ソース:個人的印象)
正規表現 正規表現
文字列関数
正規表現の学習コストに比べたら、文字列関数のそれは大したことない(単機能ばかりだから)
copyright© 2014 kuwata-lab.com all rights reserved
“正規表現リテラルがあれば文字列関数を減らせるし、処理系も単純にできるよ!”
コア言語仕様を肥大化させてでも文字列関数を減らすことがそんなに重要なの?
それ正規表現リテラルじゃなくて正規表現のメリットじゃないの?
誰にとってのメリットなの?ときどきしかコードを書かないライトユーザにも嬉しいことなの? (科学者、統計学者、CGデザイナ、etc)
そんな簡単な話ではないはず・・・
「/..../」のパースは単純なの?文字列関数が減るかわりに別の複雑さが増えてない?
copyright© 2014 kuwata-lab.com all rights reserved
ここまでのまとめ✓ 文字列関数だけのほうが学習コストは低い学習コスト: 正規表現 >>> 文字列関数
✓ とはいえ正規表現の勉強はどのみち必要学習コスト: 正規表現 < 文字列関数+正規表現
✓ 正規表現やリテラルの得失は一概には言えないだれにとってのメリット?どのくらいのメリット?
✓ そもそも「正規表現リテラルの得失」と「正規表現の得失」は別の話ちゃんと分けて議論しましょう
copyright© 2014 kuwata-lab.com all rights reserved
第2部:Python正規表現ライブラリの問題点と解決案
copyright© 2014 kuwata-lab.com all rights reserved
第2部:Python正規表現ライブラリの問題点と解決案
✓ re.match()とre.search()の2つがある✓ ライブラリの使い方が2系統ある✓ 正規表現がいつもキャッシュされてしまう✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
第2部:Python正規表現ライブラリの問題点と解決案
✓ re.match()とre.search()の2つがある✓ ライブラリの使い方が2系統ある✓ 正規表現がいつもキャッシュされてしまう✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
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
第2部:Python正規表現ライブラリの問題点と解決案
✓ re.match()とre.search()の2つがある✓ ライブラリの使い方が2系統ある✓ 正規表現がいつもキャッシュされてしまう✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
Pythonの正規表現ライブラリは、使い方が2系統存在する
大抵の正規表現操作が、モジュールレベルの関数と、 コンパイル済み正規表現のメソッドとして提供されることに注意して下さい。関数は正規表現オブジェクトのコンパイルを必要としない近道ですが、いくつかのチューニング変数を失います。
“
”引用元: http://docs.python.jp/3.3/library/re.html
re.xxxx() 系re.compile().xxxx() 系
追加スライド
copyright© 2014 kuwata-lab.com all rights reserved
しかも、両者は似ているようで微妙に違う ;(これは困る
## 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では正規表現フラグが指定できなかった
追加スライド
開始位置と終了位置が、re.compile().match() では指定できるが re.match() ではできない
copyright© 2014 kuwata-lab.com all rights reserved
ところで 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
だったら、全部 re.compile().xxxx() を使うようにすれば、re.xxxx() をなくして一本化できるよね?
rx = re._compile
## マッチングm = re.match(r"(\d+)", "123abc") # beforem = rx(r"(\d+)").match("123abc") # after
## 文字列置換re.sub(r"\.gif$", ".png", filename) # beforerx(r"\.gif$").sub(".png", filename) # after
re.compile()へのショートカット
copyright© 2014 kuwata-lab.com all rights reserved
一本化できれば、「似てるけど微妙に違う2系統」が共存しなくてすむ## re.compile().xxxx() 系re.compile(pat, flags).match(string, pos, endpos)re.compile(pat, flags).sub(repl, string, count)
## rx().xxxx() 系rx = re._compilerx(pat, flags).match(string, pos, endpos) rx(pat, flags).sub(repl, string, count)
等価 (当然)
追加スライド
copyright© 2014 kuwata-lab.com all rights reserved
また1つの関数に5~6個も引数があるくらいなら、2~3個の関数2つに分けたほうがわかりやすい
## beforere.sub(pattern, repl, string, count=0, flags=0)
## afterrx(pattern, flags=0).sub(repl, string, count=0)
引数が5個!
引数2個と 引数3個
copyright© 2014 kuwata-lab.com all rights reserved
第2部:Python正規表現ライブラリの問題点と解決案
✓ re.match()とre.search()の2つがある✓ ライブラリの使い方が2系統ある✓ 正規表現がいつもキャッシュされてしまう✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
re.compile() は正規表現を必ずキャッシュする、けどキャッシュする必要がないときもあるclass HTMLHelper(object): _ESCAPE = re.compile(r"[&<>\"']")
クラス変数に保持しているので、ライブラリ側でキャッシュする必要はない(けど強制的にキャッシュされるので、 キャッシュが必要以上に肥大化する)
copyright© 2014 kuwata-lab.com all rights reserved
特に、たくさんの正規表現がデータとして与えられると、キャッシュが無駄に肥大化してしまう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
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
キャッシュせずにコンパイルする機能が、公式に用意されるとうれしいclass HTMLHelper(object): _ESCAPE = re.sre_compile.compile(r"[&<>\"']")
これならキャッシュしないので、キャッシュの無駄な肥大化を防げる(しかしunofficialなので使用には注意すること)
copyright© 2014 kuwata-lab.com all rights reserved
第2部:Python正規表現ライブラリの問題点と解決案
✓ re.match()とre.search()の2つがある✓ ライブラリの使い方が2系統ある✓ 正規表現がいつもキャッシュされてしまう✓ 連続したマッチングとif文との相性が悪い
copyright© 2014 kuwata-lab.com all rights reserved
複数の正規表現にマッチさせるとき、こう書きたい
## ほんとはこう書きたい
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
でもPythonではこう書くしかない
m = re.match(pat1, text)if m: x, y = m.groups()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文のネストが深くなる
copyright© 2014 kuwata-lab.com all rights reserved
関数+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 break
if文のネストは減ったけど、トリッキーで間違えやすい
copyright© 2014 kuwata-lab.com all rights reserved
そこで、こういう機能はどうでしょう?
m = re.matching(text)
if m.match(pat1): x, y = m.groups()elif m.match(pat2): y, z = m.groups()elif m.match(pat3): z, x = m.groups()
マッチングの対象文字列とマッチング結果を保持するようなオブジェクトを用意すれば、
連続したマッチングが素直に書けるはず
copyright© 2014 kuwata-lab.com all rights reserved
実装はこちら
class matching(object): def __init__(self, string): self.string = string self.matched = None
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)
http://bit.ly/matching_py
copyright© 2014 kuwata-lab.com all rights reserved
Questions?
copyright© 2014 kuwata-lab.com all rights reserved
おまけ: benry.rexpfrom 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'^(\d\d\d\d)-(\d\d)-(\d\d)$'): Y, M, D = m.groups()else m.match(r'(\d\d)/(\d\d)/(\d\d\d\d)$'): M, D, Y = m.groups()
https://pypi.python.org/pypi/benry
copyright© 2014 kuwata-lab.com all rights reserved
おしまい