Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

11,170 views

Published on

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

Published in: Technology
  • Be the first to comment

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

  1. 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. 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. 3. え|マジ 正規表現 リテラルないの? あれなしで許されるキモ|イ のはPHPまでだよね! キャハハハハハハ Pythonの評判
  4. 4. 発表の目的 ✓ 正規表現リテラルは、あれば便利だけどなくても 困らない(ライブラリでカバーできる)ことを説 明する ✓ Pythonの正規表現ライブラリが抱える問題点と その解決案を紹介する copyright© 2014 kuwata-lab.com all rights reserved
  5. 5. 発表の対象者 ✓ Pythonのことをよく知らないPerlerやRubyist やJavaScripter  ✓ 他言語との違いが気になるPythonista ✓ 二重バックスラッシュにイライラしてるJavaユー ザ copyright© 2014 kuwata-lab.com all rights reserved
  6. 6. 第1部: 正規表現リテラルに関する誤解 copyright© 2014 kuwata-lab.com all rights reserved
  7. 7. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い copyright© 2014 kuwata-lab.com all rights reserved
  8. 8. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い copyright© 2014 kuwata-lab.com all rights reserved
  9. 9. 正規表現リテラルがあると、正規表現は書きやす い? ## 正規表現リテラルあり /[a-z]+/.exec(string) # JavaScript ## 正規表現リテラルなし (new RegExp("[a-z]+")).exec(string) # JavaScript copyright© 2014 kuwata-lab.com all rights reserved
  10. 10. 正規表現リテラルがなくても、正規表現を書きやす くできる ## 正規表現リテラルなし function re(pattern) { # JavaScript return new RegExp(pattern); } re("[a-z]+").exec(string) 関数やライブラリで解決できる copyright© 2014 kuwata-lab.com all rights reserved
  11. 11. 二重バックスラッシュ問題を発生させないためには 正規表現リテラルが必要? ## 正規表現リテラルあり /^dd:dd$/ # JavaScript ## 正規表現リテラルなし new RegExp("^dd:dd$") # JavaScript copyright© 2014 kuwata-lab.com all rights reserved
  12. 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. 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. 14. 【問題点】 Javaで正規表現が書きにくい (二重バックスラッシュがつらい) 【解決策】 正規表現リテラル 【解決策】 raw文字列リテラル 導入すると、言語仕様が 肥大化してしまう 正規表現以外にも利用可能、 かつ言語仕様が肥大化しない copyright© 2014 kuwata-lab.com all rights reserved
  15. 15. とはいえ、raw文字列リテラルを導入するには 言語仕様の拡張が必要 ## Java Pattern pat = Pattern.compile(r"^dddd$"); Matcher m = pat.matcher(input); raw文字列リテラルがほしいけど、 言語仕様を拡張しないと無理じゃん ;( copyright© 2014 kuwata-lab.com all rights reserved
  16. 16. そもそも、バックスラッシュでないとだめなん? ## 先行事例: Cのprintf()やJavaのString.format() String.format("s=%s, n=%d", s, n); メタキャラクタとして「%」を使っている! 二重バックスラッシュのような問題がない! copyright© 2014 kuwata-lab.com all rights reserved
  17. 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. 18. 【問題点】 Javaで正規表現が書きにくい (二重バックスラッシュがつらい) 【解決策】 バックスラッシュを やめる copyright© 2014 kuwata-lab.com all rights reserved 【解決策】 正規表現リテラル 【解決策】 raw文字列リテラル 言語仕様の変更が必要なく、 ライブラリだけで実現可能
  19. 19. ✓ 正規表現リテラルのないJavaでは、 正規表現が書きにくい ✓ 正規表現リテラルのあるPerlやRubyでは、 正規表現が書きやすい ✓ だから、プログラミング言語に 正規表現リテラルは必要だ! ・問題:解決方法 = 1:N (*注) ・one of themでしかない方法を、only oneな方法だと勘違いしてる (*注) もちろん、複数の解決方法の間では優劣が存在する。この場合なら、「正規表現リテ ラルが必要」と主張するには、それが他の方法より優れていることを説明する必要がある。 copyright© 2014 kuwata-lab.com all rights reserved ← わかる ← わかる ← その理屈はおかしい
  20. 20. ここまでのまとめ ✓ 正規表現を書きやすくする言語機能は、ひとつで はない 正規表現リテラル、raw文字列リテラル ✓ 正規表現リテラルよりraw文字列リテラルのほう が望ましい 正規表現以外にも利用可能だし、言語仕様も肥大化しない ✓ そもそも、バックスラッシュを使わなければいい 言語仕様の拡張が必要ないので、今すぐ使える方法 copyright© 2014 kuwata-lab.com all rights reserved
  21. 21. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い copyright© 2014 kuwata-lab.com all rights reserved
  22. 22. 正規表現リテラルなら、コンパイルは1度だけ ## Ruby 100.times do filename =~ /.(png|gif|jpe?g)$/ end 100回コンパイルされたりはしない (ただし埋め込み式のある場合は別) copyright© 2014 kuwata-lab.com all rights reserved
  23. 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. 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. 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. 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. 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. 28. ここまでのまとめ ✓ キャッシュを使えば、正規表現が毎回コンパイル されることはない 正規表現リテラルがなくても充分な性能は出せる ✓ 正規表現より文字列関数のほうが高速 少なくともCPythonではそう ✓ リテラルがなくても正規表現のコンパイル時評価 は可能 「リテラルの有無」と「処理系の評価戦略」は、基本的に別個の話 copyright© 2014 kuwata-lab.com all rights reserved
  29. 29. 第1部:正規表現リテラルに関する誤解 ✓ 誤解:正規表現リテラルがあったほうが書きやすい ✓ 誤解:正規表現リテラルがあったほうが性能がよい ✓ 誤解:正規表現のほうが文字列関数より学習コスト が低い copyright© 2014 kuwata-lab.com all rights reserved
  30. 30. ✓ そもそも、「正規表現リテラルは必要か?」と 「正規表現と文字列関数はどちらが学習コストが 低いか?」は別の話 仮に「正規表現のほうが文字列関数よりわかりやすい」という結 論になったとしても、それをもって「正規表現リテラルは必要」 とはならない ✓ そのうえで、あえて「どちらが学習コストが低い か?」を論じる。 copyright© 2014 kuwata-lab.com all rights reserved
  31. 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. 32. また正規表現は落とし穴も多いので、初級者には つらいことも ## Ruby string =~ /.html$/ 厳密には間違い (正解は /.htmlz/ ) ## Ruby string.end_with?(".html") 初級者でも間違えない! copyright© 2014 kuwata-lab.com all rights reserved
  33. 33. とはいえ、上達するにつれ、正規表現を避けるこ とはできない ## 正規表現 pat = r"^(d{4})-(dd)-(dd)[ T](dd):(dd): dd)$" re.match(pat, input) 文字列関数でこれを書くのは つらい copyright© 2014 kuwata-lab.com all rights reserved
  34. 34. 学習コストの内訳(ソース:個人的印象) 文字列関数 正規表現の学習コストに比べたら、 文字列関数のそれは大したことない (単機能ばかりだから) 正規表現正規表現 正規表現だけ正規表現+文字列関数 copyright© 2014 kuwata-lab.com all rights reserved
  35. 35. コア言語仕様を肥大化させて でも文字列関数を減らすこと がそんなに重要なの? それ正規表現リテラル じゃなくて正規表現の メリットじゃないの? “正規表現リテラルがあれば 文字列関数を減らせるし、 処理系も単純にできるよ!” 誰にとってのメリットなの? ときどきしかコードを書かない ライトユーザにも嬉しいことなの? (科学者、統計学者、CGデザイナ、etc) そんな簡単な話ではないはず・・・ 「/..../」のパースは単純なの? 文字列関数が減るかわりに 別の複雑さが増えてない? copyright© 2014 kuwata-lab.com all rights reserved
  36. 36. ここまでのまとめ ✓ 文字列関数だけのほうが学習コストは低い 学習コスト: 正規表現 >>> 文字列関数 ✓ とはいえ正規表現の勉強はどのみち必要 学習コスト: 正規表現 < 文字列関数+正規表現 ✓ 正規表現やリテラルの得失は一概には言えない だれにとってのメリット?どのくらいのメリット? ✓ そもそも「正規表現リテラルの得失」と「正規表 現の得失」は別の話 ちゃんと分けて議論しましょう copyright© 2014 kuwata-lab.com all rights reserved
  37. 37. 第2部: Python正規表現ライブラリの 問題点と解決案 copyright© 2014 kuwata-lab.com all rights reserved
  38. 38. 第2部:Python正規表現ライブラリの 問題点と解決案 ✓ re.match()とre.search()の2つがある ✓ ライブラリの使い方が2系統ある ✓ 正規表現がいつもキャッシュされてしまう ✓ 連続したマッチングとif文との相性が悪い copyright© 2014 kuwata-lab.com all rights reserved
  39. 39. 第2部:Python正規表現ライブラリの 問題点と解決案 ✓ re.match()とre.search()の2つがある ✓ ライブラリの使い方が2系統ある ✓ 正規表現がいつもキャッシュされてしまう ✓ 連続したマッチングとif文との相性が悪い copyright© 2014 kuwata-lab.com all rights reserved
  40. 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. 41. 第2部:Python正規表現ライブラリの 問題点と解決案 ✓ re.match()とre.search()の2つがある ✓ ライブラリの使い方が2系統ある ✓ 正規表現がいつもキャッシュされてしまう ✓ 連続したマッチングとif文との相性が悪い copyright© 2014 kuwata-lab.com all rights reserved
  42. 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. 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. 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. 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. 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. 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. 48. 第2部:Python正規表現ライブラリの 問題点と解決案 ✓ re.match()とre.search()の2つがある ✓ ライブラリの使い方が2系統ある ✓ 正規表現がいつもキャッシュされてしまう ✓ 連続したマッチングとif文との相性が悪い copyright© 2014 kuwata-lab.com all rights reserved
  49. 49. re.compile() は正規表現を必ずキャッシュする、 けどキャッシュする必要がないときもある class HTMLHelper(object): _ESCAPE = re.compile(r"[&<>"']") クラス変数に保持しているので、ライブ ラリ側でキャッシュする必要はない (けど強制的にキャッシュされるので、 キャッシュが必要以上に肥大化する) copyright© 2014 kuwata-lab.com all rights reserved
  50. 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. 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. 52. キャッシュせずにコンパイルする機能が、公式に用 意されるとうれしい class HTMLHelper(object): _ESCAPE = re.sre_compile.compile(r"[&<>"']") これならキャッシュしないので、 キャッシュの無駄な肥大化を防げる (しかしunofficialなので使用には注意すること) copyright© 2014 kuwata-lab.com all rights reserved
  53. 53. 第2部:Python正規表現ライブラリの 問題点と解決案 ✓ re.match()とre.search()の2つがある ✓ ライブラリの使い方が2系統ある ✓ 正規表現がいつもキャッシュされてしまう ✓ 連続したマッチングとif文との相性が悪い copyright© 2014 kuwata-lab.com all rights reserved
  54. 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. 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. 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. 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. 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. 59. Questions? copyright© 2014 kuwata-lab.com all rights reserved
  60. 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. 61. copyright© 2014 kuwata-lab.com all rights reserved おしまい

×