正規表現リテラルは本当に必要なのか?

  • 1,743 views
Uploaded on

PyConJP2014発表資料。 …

PyConJP2014発表資料。
・正規表現リテラルは、**あれば便利だけどなくても困らない**(ライブラリでカバーできる)ことを説明する。
・Pythonの正規表現が抱える問題点とその解決案を紹介する。

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
1,743
On Slideshare
0
From Embeds
0
Number of Embeds
7

Actions

Shares
Downloads
4
Comments
0
Likes
7

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 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
  • 3. え|マジ 正規表現 リテラルないの? あれなしで許されるキモ|イ のはPHPまでだよね! キャハハハハハハ Pythonの評判
  • 4. 発表の目的 ✓ 正規表現リテラルは、あれば便利だけどなくても 困らない(ライブラリでカバーできる)ことを説 明する ✓ Pythonの正規表現ライブラリが抱える問題点と その解決案を紹介する copyright© 2014 kuwata-lab.com all rights reserved
  • 5. 発表の対象者 ✓ Pythonのことをよく知らないPerlerやRubyist やJavaScripter  ✓ 他言語との違いが気になるPythonista ✓ 二重バックスラッシュにイライラしてるJavaユー ザ copyright© 2014 kuwata-lab.com all rights reserved
  • 6. 第1部: 正規表現リテラルに関する誤解 copyright© 2014 kuwata-lab.com all rights reserved
  • 7. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い copyright© 2014 kuwata-lab.com all rights reserved
  • 8. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い 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
  • 11. 二重バックスラッシュ問題を発生させないためには 正規表現リテラルが必要? ## 正規表現リテラルあり /^dd:dd$/ # JavaScript ## 正規表現リテラルなし new RegExp("^dd:dd$") # JavaScript 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
  • 14. 【問題点】 Javaで正規表現が書きにくい (二重バックスラッシュがつらい) 【解決策】 正規表現リテラル 【解決策】 raw文字列リテラル 導入すると、言語仕様が 肥大化してしまう 正規表現以外にも利用可能、 かつ言語仕様が肥大化しない 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 「」のかわりに 「`」を使ってる (変更も可能)
  • 18. 【問題点】 Javaで正規表現が書きにくい (二重バックスラッシュがつらい) 【解決策】 バックスラッシュを やめる copyright© 2014 kuwata-lab.com all rights reserved 【解決策】 正規表現リテラル 【解決策】 raw文字列リテラル 言語仕様の変更が必要なく、 ライブラリだけで実現可能
  • 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
  • 21. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い 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
  • 29. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い copyright© 2014 kuwata-lab.com all rights reserved
  • 30. ✓ そもそも、「正規表現リテラルは必要か?」と 「正規表現と文字列関数はどちらが学習コストが 低いか?」は別の話 仮に「正規表現のほうが文字列関数よりわかりやすい」という結 論になったとしても、それをもって「正規表現リテラルは必要」 とはならない ✓ そのうえで、あえて「どちらが学習コストが低い か?」を論じる。 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
  • 34. 学習コストの内訳(ソース:個人的印象) 文字列関数 正規表現の学習コストに比べたら、 文字列関数のそれは大したことない (単機能ばかりだから) 正規表現正規表現 正規表現だけ正規表現+文字列関数 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
  • 37. 第2部: Python正規表現ライブラリの 問題点と解決案 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 追加スライド
  • 47. また1つの関数に5~6個も引数があるくらいなら、 2~3個の関数2つに分けたほうがわかりやすい 引数が5個! ## before re.sub(pattern, repl, string, count=0, flags=0) ## after rx(pattern, flags=0).sub(repl, string, count=0) 引数2個と引数3個 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
  • 49. re.compile() は正規表現を必ずキャッシュする、 けどキャッシュする必要がないときもある class HTMLHelper(object): _ESCAPE = re.compile(r"[&<>"']") クラス変数に保持しているので、ライブ ラリ側でキャッシュする必要はない (けど強制的にキャッシュされるので、 キャッシュが必要以上に肥大化する) 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文のネストは減ったけど、 トリッキーで間違えやすい
  • 57. そこで、こういう機能はどうでしょう? マッチングの対象文字列とマッチング結果を 保持するようなオブジェクトを用意すれば、 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
  • 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)
  • 59. Questions? copyright© 2014 kuwata-lab.com all rights reserved
  • 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
  • 61. copyright© 2014 kuwata-lab.com all rights reserved おしまい