<style> /* basic design */ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6, .reveal section, .reveal table, .reveal li, .reveal blockquote, .reveal th, .reveal td, .reveal p { font-family: 'Meiryo UI', 'Source Sans Pro', Helvetica, sans-serif, 'Helvetica Neue', 'Helvetica', 'Arial', 'Hiragino Sans', 'ヒラギノ角ゴシック', YuGothic, 'Yu Gothic'; text-align: left; line-height: 1.6; letter-spacing: normal; text-shadow: none; word-wrap: break-word; color: #444; } .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 {font-weight: bold;} .reveal h1, .reveal h2, .reveal h3 {color: #2980b9;} .reveal th {background: #DDD;} .reveal section img {background:none; border:none; box-shadow:none; max-width: 95%; max-height: 95%;} .reveal blockquote {width: 90%; padding: 0.5vw 3.0vw;} .reveal table {margin: 1.0vw auto;} .reveal code {line-height: 1.2; background:#DDDDDD} .reveal pre code{background:#202020} .reveal p, .reveal li {padding: 0vw; margin: 0vw;} .reveal .box {margin: -0.5vw 1.5vw 2.0vw -1.5vw; padding: 0.5vw 1.5vw 0.5vw 1.5vw; background: #EEE; border-radius: 1.5vw;} /* table design */ .reveal table {background: #f5f5f5;} .reveal th {background: #444; color: #fff;} .reveal td {position: relative; transition: all 300ms;} .reveal tbody:hover td { color: transparent; text-shadow: 0 0 3px #aaa;} .reveal tbody:hover tr:hover td {color: #444; text-shadow: 0 1px 0 #fff;} /* blockquote design */ .reveal blockquote { width: 90%; padding: 0.5vw 0 0.5vw 6.0vw; font-style: italic; background: #f5f5f5; } .reveal blockquote:before{ position: absolute; top: 0.1vw; left: 1vw; content: "\f10d"; font-family: FontAwesome; color: #2980b9; font-size: 3.0vw; } /* font size */ .reveal h1 {font-size: 4.0vw;} .reveal h2 {font-size: 3.0vw;} .reveal h3 {font-size: 2.6vw;} .reveal h4 {font-size: 2.4vw; color: #EE6557;} .reveal h5 {font-size: 2.2vw;} .reveal h6 {font-size: 2.0vw;} .reveal section, .reveal table, .reveal li, .reveal blockquote, .reveal th, .reveal td, .reveal p {font-size: 1.6vw;} .reveal code {font-size: 1.3vw;} /* new color */ .red {color: #EE6557;} .blue {color: #16A6B6;} /* split slide */ #right {left: -18.33%; text-align: left; float: left; width: 50%; z-index: -10;} #left {left: 31.25%; text-align: left; float: left; width: 50%; z-index: -10;} </style> <style> /* specific design */ .reveal h2 { padding: 0 0.75vw; border-left: solid 0.3vw #2980b9; border-bottom: solid 0.2vw #d7d7d7; } </style> # KT#015-正規表現 正規表現 瀬村雄一 --- ## 自己紹介 瀬村です。5ヶ月ぶりです。現職は楽なのでまだまだ居座る気でいます。 2020年度の仕事はこんな感じでした。色々出来て楽しいです。 * JUnitテストの CI 環境構築 * 単体テスト仕様書作成、レビュアー * Excel芸人 * ASP.NET系プロジェクトのビルドデプロイ自動化 * Jenkins の構築 * 自動化スクリプト * ASP.NET系技術 * Windows Server、IIS の構築をちょびっと * WPFアプリの画面テストの自動化支援 * Selenium API * WinAppDriver * アジャイル開発版社内標準の執筆 * スクラム --- ### とまぁ、振り返ってみればテスト系が多いです。 多分社内のヤバそうな案件は、開発段階でスケジュールがヤバイことに気づいて、テスト段階で火消しに入るんだと思います。そういう手伝いばっかりしております。 それとは全く関係ないですが、大学院生の時に結構漁ってた正規表現について共有します。 <br> ### 本日のゴール * 正規表現が読めるようになる。 * 「文字列操作するなら、正規表現使って実装してみるか」的な発想を得る。 * 基本的な文法を知ってもらえる。 * 応用例についても少しだけ紹介。 <br><br><br> ※今回のスタイルシートはここからパクって色々改造しました。 https://hackmd.io/mJ6dr_haQWmAchJEq4truw?both --- ## これは何にマッチする正規表現? ```python= /\*[\s\S]*?\*/|//.*(?=(?:\r|\r?\n)) ``` ### <font color="#FFFFFF">そうだね、C言語のコメントにマッチする正規表現だね。</font> このKTを終えた頃には、上の正規表現が読めるようになっています。 --- ## 正規表現とはなにか? ##### <center>「文字列の集合を一つの文字列で表現する方法の一つだと思う。。。」</center> * エンジニアだったら、何らかの文字列操作をしないと行けないときが年に数回は訪れる。 * 例えば、こんなケース。 * 大きな文書の中から、条件にマッチする文字列を検索・置換しなければならない。 * API が返してくる色んな文字列を、いい感じにフィルタリングしたい。(?) * その処理が複雑すぎると、しばしば **「手動でやるよりスクリプトを書いたほうが早いのでは?」** とか **「文字列操作をもっとスマートに実装出来ないか?」** とか思ったりもする。 --- ## 君はもうその力を持っている #### プログラミング言語には標準搭載 高級なプログラミング言語には、間違いなく正規表現を用いた文字列操作が含まれている。 例: Python では、正規表現を用いた文字列抽出を以下のように書ける。 ```python= for filename in os.listdir(directory): if not filename.endswith(".html"): continue with open(os.path.join(directory, filename)) as f: contents = f.read() # HTML file から ~~.html へのリンクを取得する。 # <a href="~~"> の"~~"の部分をかき集める。 links = re.findall(r"<a\s+(?:[^>]*?)href=\"([^\"]*)\">", contents) pages[filename] = set(links) - {filename} ``` `<a\s+(?:[^>]*?)href=\"([^\"]*)\"` が HTMLテキストの中の`<a href="hoge.html">`にマッチする正規表現である。 --- #### エディタ、IDE にも標準搭載 VSCodeにも正規表現を用いた文字列検索と置換が実装されている。 例: 美人またはかわいいと書かれたミワさんを、パチンカスミワに置換する。 <center><img src="https://i.imgur.com/dLldiXr.png" height="280"></center> 置換後 <center><img src="https://i.imgur.com/mpS2yzY.png" height="280"></center> --- #### かつてチョムスキー階層を完全に理解した私達 にとっては、全く難しい話ではない。 正規表現は、正則表現だったりオートマトンだったりしますからねぇ。ありがたいですねぇ。 :chomsky: <img src="https://i.imgur.com/cevAZ76.jpg" height="64px"><img src="https://i.imgur.com/cevAZ76.jpg" height="64px"><img src="https://i.imgur.com/cevAZ76.jpg" height="64px"><img src="https://i.imgur.com/cevAZ76.jpg" height="64px"><img src="https://i.imgur.com/cevAZ76.jpg" height="64px"> <br><br> ### 正規表現をたくさん活用出来たら嬉しいね~。 --- ## 正規表現で何が出来るか? 先述の通り、正規表現を使うと、 #### <center>文字列表現において、検索や置換の条件をいい感じに書ける。</center> 具体例としては、以下のようなことを叶える事ができる。 * フォーム画面の実装で、メールアドレスをいい感じにバリデーションしたい。 * html のソースコードから、特定のタグだけをいい感じに抜き出したい。 * マークダウンのタグだけいい感じに抜き出したい。でもタグは取りたくない。 * タイプ1のコードクローンをいい感じに検出したい。 * もとい、ソースコードのコメントをいい感じに除去したい。 --- ## 正規表現を書いてみよう。 今回は Python を例に進めていきましょう。雰囲気で進めていきます。 とりあえず、どこかで使えるような正規表現を4ページほど紹介します。 ### 記法1(見たら雰囲気わかる) * 0回以上続く、1回以上続く、回数指定 * 量指定子と呼ばれる。 * 数字を表す文字列。 * メタ文字と呼ばれる。 **Googleのoが2個以上続く文字列にマッチする表現**(Goooooo....oogle) ```python= Goo+gle Go{2,}gle ``` **郵便番号にマッチする正規表現** ```python= \d\d\d-\d\d\d\d \d{3}-\d{4} ``` --- ### 記法2(メタ文字) よく使えるところはこれくらい。メタ文字を使うことで数字空白の表現が楽になる。 | メタ文字 | 意味 | | -------- | -------------------------------------------------- | | \n | 改行文字 | | \s | 空白文字(半角スペース、\t、\n、\r、\f)すべて | | \S | 空白文字(半角スペース、\t、\n、\r、\f)以外すべて | | \d | 半角数字 | | \w | すべての半角英数字とアンダースコア | **「Yuichi Semura」という文字列の間に、任意個数の空白を認めた場合** ```python= Yuichi\s*Semura ``` **価格(円マークから始まって、3桁区切り)にマッチする正規表現** ```python= \\\d{1,3}(,\d{3})* ``` --- ### 記法3(文字クラス) 「`[]`で囲まれた文字のどれかにマッチする表現」を文字クラスと呼ぶ。 * DNA の塩基配列 を表すなら、`[AGCT]+`とか? 生物未履修勢だから適当。 * AGCTのどれかが永遠に続く文字列にマッチする正規表現。 * 数値のメタ文字 `\d` は、 `[0-9]` と一緒である。 * 横線を入れることで、そのあいだの文字も良いよ、という意味になる。 * 変数名に使える文字は、`[a-zA-Z0-9_]` と書ける。 **16進数** ```python= [0-9A-F]* ``` **時刻** 08:25:41 とか 21:06:18 にマッチする正規表現 ```python= ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] ``` **Pythonの変数名** 最初の一文字だけ数値が禁止されている。ちなみに日本語も使えるけどね!!!! ```python= [a-zA-Z_][a-zA-Z0-9_]* ``` --- ### 記法4(その他) **改行コード1(\nか\r\n)** `?`:直前の文字の0個または1個にマッチする。 ```python= \r?\n ``` **改行コード2(\rか\n)** `|`:直前か直後のどちらかにマッチする。 ```python= \r|\n ``` **改行コード3(\rか\nか\r\n)** そしてあわせ技 ```python= \r|\r?\n ``` **ぺろぺろ...** `()`: グループ化出来る。演算の順序を先にする的なイメージ。 ```python= (ぺろ)+ ``` --- ## 正規表現を使って、文字列操作をしてみよう。 読めるようになったら、プログラミングで使ってみよう。 ちなみに、「~~にマッチする(正規)?表現」みたいな日本語をよく使います。(僕の中で) Python で正規表現を使う。 * Python では re モジュールを使って、正規表現系の操作が使えます。 * re は (Regular Expression)のことっす。 * re.search(pattern, string):1つ検索 * re.findall(pattern, string):全部検索 * re.fullmatch(pattern, string):全体でマッチ * re.sub(pattern, repl, string):置換 --- ### re.search(pattern, string) 正規表現にマッチするものを検索する。 ```python= import re pattern = r'tki' # 正規表現を書くときは、raw 文字列を使うと良き。 string = 'smr tki yki smr ski' # 検索対象文字列 # 一つ検索し、マッチオブジェクトを返す。 result = re.search(pattern, string) if result: # none 以外の場合 print(result.span()) # output:(4, 7) print(result.group()) # output:tki ``` --- ### re.findall(pattern, string) 正規表現にマッチするものを、全て検索する。 ```python= pattern = r's\S*' string = 'smr tki yki smr ski' # 検索対象文字列 # 全て検索し、文字列リストを返す。 result = re.findall(pattern, string) print(result) # ['smr', 'smr', 'ski'] ``` --- ### re.fullmatch(pattern, string) 文字列全体が正規表現にマッチしているかどうかを調べる。 ```python= pattern = r'<p>.*?\(.*?\)</p>' string = '<p>Yuichi Semura(Full-Stack Engineer)</p>' # 検索対象文字列 # 全て検索し、文字列リストを返す result = re.fullmatch(pattern, string) if result: print(result.group()) # <p>Yuichi Semura(Full-Stack Engineer)</p> ``` * `.`: 改行文字以外の、任意の文字にマッチする。 * `\`: エスケープ文字 * `(` か `)` など、普通にしてると意味を持ってしまう文字に必要 * [【保存版】正規表現でエスケープが必要な文字一覧表](https://qiita.com/katsukii/items/1c1550f064b4686c04d4) * `.*?`: 任意の文字に最短マッチする。 --- ### re.sub(pattern, repl, string) 正規表現にマッチする部分を、指定の文字列に置換して、全体を返す。 ```python= pattern = r'tk' repl = 'Teku' string = 'tktktktk' # 検索対象文字列 # 検索し、置換する。 result = re.sub(pattern, repl, string) print(result) # TekuTekuTekuTeku ``` 置換に関しては、こっちの書き方が一般的。 ```python= print(string.replace(repl, 'Teku')) # TekuTekuTekuTeku ``` --- ### 「ところで r"hoge" って何?」 * Python には raw 文字列というものがある。 * 正規表現を表す文字列を作りたい場合は、raw文字列を使うことが多い。 * raw文字列では、エスケープシーケンス`\`が、特殊な意味を持たない。 普通の文字列で正規表現を表した場合、以下のようなことになる。 1. 正規表現で`\r\n`を表したい。 2. Pythonの文字列の中に`"\r\n"`を書くと、改行になってしまう。 3. `\`は純粋な`\`として使いたいので、エスケープする。結果、`"\\r\\n"`となる。 それがちょっと解決されるのが、こいつ。 ```python= "\\r|\\r?\\n" r"\r|\r?\n" ``` --- ## 解答編 最後に、冒頭に紹介した正規表現について解説して終わりにしたいと思います。 * C言語のソースコードのコメントを除去したい。 * HTML からリンク先にマッチする文字列を抽出する。 --- ### 「正規表現可視化ツール」 書いた正規表現が合ってるかどうかテストできます。構造もわかるので、めちゃくちゃ便利です。 https://www.debuggex.com/ 例:改行コードにマッチする正規表現 ![Debuggex.com](https://i.imgur.com/ZFnxj7J.png) --- ### C言語のソースコードのコメントを除去したい。(コード) C言語のコメントにマッチする正規表現を用いて、置換を行う。 ```python= pattern = r"/\*[\s\S]*?\*/|//.*(?=(?:\r|\r?\n))" string = """ //line comment hoge /* * multiline comment */ """ result = re.findall(pattern, string) print(result) # ['//line comment', '/* \n * multiline comment\n */'] ``` * `[\s\S]`: 全ての文字にマッチする。 * `.*?`: 改行以外の文字の最短マッチ。 * `(?:)`: キャプチャを行わないグループ化 * `(?=)`: 肯定先読み --- ### C言語のソースコードのコメントを除去したい。(debuggex) debuggexを使うとこんな感じ。 ![c_comment](https://i.imgur.com/n8P1muB.png) --- ### HTML からリンク先にマッチする文字列を抽出する。(コード) ```python= pattern = r"<a\s+(?:[^>]*?)href=\"([^\"]*)\"" string = """ <a href="4513.html">4513</a> <a target="_blank" href="myojin.png">koko</a> <a href="kanejun.tex">erotech</a> """ result = re.findall(pattern, string) # 検索 print(result) # ['4513.html', 'myojin.png', 'kanejun.tex'] ``` * `()`: キャプチャを行うグループ化 * `[^"]`: `"`以外の文字(文字クラスの否定形) --- ### HTML からリンク先にマッチする文字列を抽出する。(debuggex) ```python= r"<a\s+(?:[^>]*?)href=\"([^\"]*)\"" ``` ![AHREF](https://i.imgur.com/wbVRxtG.png) グループ化というテクを使うことで、""で囲まれた部分を取り出せるようにしている。 --- ## ありがとうございました。 どうも、オナ○ーでした。 紹介した正規表現の用法は、まだまだ一部です。 そういえば僕も正規表現の文法を完璧に覚えているわけではないです。 正規表現使って何かしようと思う度に、使いたい文法をネットで調べて使っています。 多分「これは正規表現なら簡単に出来るやろ」という発想が一番重要でしょう。 実装方法や文法なんて調べたら、ちょちょっと出てきますからね。 <br><br> #### <center>正規表現でイキリエンジニアになりましょう!</center> <br><br> 今回紹介したコード→ https://github.com/YuichiSemura/kt-regex --- ## 付録 興味がある人へ * 正規表現アンチパターン * 紹介していない文法について * 後方参照 * (肯|否)定(先|後)読み * 参考書、リンク --- ## 付録1: 正規表現アンチパターン1 正規表現は万能ではない。XMLの構造など、文脈自由言語になってしまうとうまく取れなくなってしまう。 対象:`<fuga><fuga></fuga></fuga><fuga></fuga>` `<fuga>.*</fuga>`という正規表現でマッチさせようとすると、`<fuga><fuga></fuga></fuga><fuga></fuga>`が出てきてしまう。 `<fuga>.*?</fuga>`という正規表現でマッチさせようとすると、`<fuga><fuga></fuga>`が出てきてしまう。 どちらもなんかイケてない。(そもそも何がしたいんだって話だが) --- ## 付録1: 正規表現アンチパターン2 先程紹介した、「Cのコメント」ための正規表現は、検索をすり抜けたり、間違って引っかかったりするようなものを作ることも出来てしまいます。かゆいところに手が届かねえ感じがします。 参考文献: [多様なプログラミング言語に対応可能なコードクローン検出ツール CCFinderSW](https://sel.ist.osaka-u.ac.jp/lab-db/Mthesis/archive/146/146.pdf) ```java= public static void main(){ //line comment System.out.println("foo/*bar*/"); /* * multiline comment */ } ``` 正規表現はあくまで、ある程度いい感じに使える道具だと思っています。 --- ## 付録2: 紹介していない文法について 「先読み後読み」「後方参照」あたりの、単純な正規言語を逸脱した機能が使えるようになると、割と熱いので是非調べてみてください。 * 貪欲マッチ、非貪欲マッチ * 正規表現をやる上でかなり重要事項である。 * リンク:[正規表現の最短マッチ - Qiita](https://qiita.com/ha_g1/items/d41febac011df4601544) * グループ化、後方参照 * リンク1:[Pythonの正規表現マッチオブジェクトでマッチした文字列や位置を取得](https://note.nkmk.me/python-re-match-object-span-group/) * リンク2:[Pythonで個人的によく使う正規表現モジュールの機能](https://qiita.com/HigashinoSola/items/ba96165a04e827cbd377) * 先読みと後読み * 「(肯|否)定(先|後)読み」と呼ばれる * リンク1:[正規表現あれこれ - Qiita](https://qiita.com/ikmiyabi/items/12d1127056cdf4f0eea5#%E5%85%88%E8%AA%AD%E3%81%BF%E3%81%A8%E5%BE%8C%E8%AA%AD%E3%81%BF) * リンク2:[正規表現の先読み・後読みを極める! - あらびき日記](https://abicky.net/2010/05/30/135112/ ) --- ## 付録3: 後方参照 グループ化を使用して、先にマッチしたグループと同じ文字列をマッチさせることが出来る方法。 例:先にマッチしたdivをあとでもう一度`\1`と書くことでマッチさせている。 ```python= pattern = r"<(.*?)\s.*?>*?</\1>" string = """ <div class="token"><p>4513</p></div> """ result = re.search(pattern, string) # 検索 if result: print(result.group()) # <div class="token"><p>4513</p></div> ``` --- ## 付録4: (肯|否)定(先|後)読み① | | 書き方 | 意味 | | ---------- | ------------ | --------------------------- | | 肯定先読み | (?=pattern) | 直後にpatternが来るなら、 | | 否定先読み | (?!pattern) | 直後にpatternが来ないなら、 | | 肯定後読み | (?<=pattern) | 直前にpatternが来るなら、 | | 否定後読み | (?<!pattern) | 直前にpatternが来ないなら、 | * 例えば、肯定先読みなら * 「後に特定の文字列が続くんだけど、その文字列はマッチさせたくない場合」に使える。 --- ## 付録4: (肯|否)定(先|後)読み② 例:バージョンファイルの書き換えで、他を変えずに一部分だけ書き換える。 ```python= import re pattern = r"(?<=AssemblyFinalVersion=\")\d\.\d\.\d\.\d" repl = "2.2.2.2" string = """\ AssemblyVersion="1.0.0.0" AssemblyFinalVersion="1.0.0.0" FixVersion="1.0.0.0"\ """ result = re.sub(pattern, repl, string) # 置換 print(result) # AssemblyFinalVersionのみ書き換わる ``` * 肯定後読み:`(?<=pattern)` <center><img src="https://i.imgur.com/cbhnvzL.png" width="1100"></center> --- ## 付録5: 参考書、リンク * [詳説 正規表現 第3版](https://www.amazon.co.jp/dp/4873113598/) * 研究室時代に読んだ本。書かれている内容は、少し情報は古い。 * C言語のコメント除去についてめちゃくちゃ熱く語られている。 * [RegExr: Learn, Build, & Test RegEx](https://regexr.com/) * JavaScript で使える、正規表現の可視化 * ちなみに僕の修論は、色んな言語の色んな文法をコードクローン検出のために正規化するんだけど、そのために正規表現をいい感じに自動生成していこうぜって話でした。しょーもな。
{"metaMigratedAt":"2023-06-15T20:16:23.059Z","metaMigratedFrom":"YAML","title":"KT#015-正規表現","breaks":true,"slideOptions":"{\"theme\":\"white\",\"slideNumber\":\"c/t\",\"center\":false,\"transition\":\"linear\",\"keyboard\":true,\"width\":\"75%\",\"height\":\"100%\"}","description":"正規表現","contributors":"[{\"id\":\"f19d136b-5434-44b8-b7a4-24df52387c7c\",\"add\":26602,\"del\":12559}]"}
    906 views
   owned this note