トラック
/
jq
jq
/
演習
/
正規表現チャットボット
正規表現チャットボット

正規表現チャットボット

学習演習

はじめに

正規表現

正規表現(regex)とは、テキスト内で検索パターンを指定する文字の並びです。

正規表現の構文を覚えることは、このトピックの範囲を超えています。 ここでは、正規表現を利用するためにjqが提供している式に焦点を当てます。

正規表現の方言

ツールによって、実装されている正規表現のバージョンが異なります。 jqはOnigurumaという正規表現ライブラリを組み込んでおり、これはPerl v5.8の正規表現とほぼ互換性があります。

jqが使う具体的な構文は、OnigurumaのGitHubリポジトリーで確認できます。

Caution

jqには、正規表現のための特別な構文はありません。 正規表現は、単に文字列として表されます。 つまり、正規表現内のバックスラッシュは、文字列の中でエスケープする必要があります。

たとえば、数字の文字クラス(\d)は"\\d"と書く必要があります。

正規表現の関数

jqの正規表現は、一連のフィルターに限られています。

単純なマッチング

文字列がパターンに一致するかどうかを知りたいときは、testフィルターを使います。

STRING | test(REGEX)
STRING | test(REGEX; FLAGS)
STRING | test([REGEX, FLAGS])

このフィルターは、_真偽値_を出力します。

"Hello World!" | test("W")    # => true
"Goodbye Mars" | test("W")    # => false
マッチに関する情報

実際にパターンに一致した部分文字列を取り出したいときは、matchフィルターを使います。

STRING | match(REGEX)
STRING | match(REGEX; FLAGS)
STRING | match([REGEX, FLAGS])

このフィルターは、次のいずれかを出力します。

  • 一致しなかった場合は、何も出力しません。
  • 一致した場合は、さまざまなプロパティを含むオブジェクトを出力します。

この例では、後方参照の構文\1を使って、連続する2つの同じ母音を探します。

"Hello World!" | match("([aeiou])\\1")
# => empty

"Goodbye Mars" | match("([aeiou])\\1")
# => {
#      "offset": 1,
#      "length": 2,
#      "string": "oo",
#      "captures": [
#        {
#          "offset": 1,
#          "length": 1,
#          "string": "o",
#          "name": null
#        }
#      ]
#    }

matchフィルターは、一致する_たびに_オブジェクトを返します。 次の例では、"g"フラグを使ってすべての母音を見つけています。

"Goodbye Mars" | match("[aeiou]"; "g")
# => { "offset": 1, "length": 1, "string": "o", "captures": [] }
#    { "offset": 2, "length": 1, "string": "o", "captures": [] }
#    { "offset": 6, "length": 1, "string": "e", "captures": [] }
#    { "offset": 9, "length": 1, "string": "a", "captures": [] }
キャプチャした部分文字列

matchフィルターと同様に、captureフィルターも一致した場合はオブジェクトを返します。

STRING | capture(REGEX)
STRING | capture(REGEX; FLAGS)
STRING | capture([REGEX, FLAGS])

返されるオブジェクトは、名前付きキャプチャのマッピングです。

"JIRAISSUE-1234" | capture("(?<project>\\w+)-(?<issue_num>\\d+)")
# => {
#      "project": "JIRAISSUE",
#      "issue_num": "1234"
#    }
部分文字列だけを取り出す

scanフィルターは、"g"フラグを付けたmatchに似ています。

STRING | scan(REGEX)
STRING | scan(REGEX; FLAGS)

scanは部分文字列の_ストリーム_を出力します。

"Goodbye Mars" | scan("[aeiou]")
# => "o"
#    "o"
#    "e"
#    "a"

部分文字列をまとめて取り出すには、[...]という配列コンストラクターを使います。

"Goodbye Mars" | [ scan("[aeiou]") ]
# => ["o", "o", "e", "a"]
文字列の分割

文字列のうち残したい部分がわかっているときは、matchまたはscanを使います。 捨てたい部分がわかっているときは、splitを使います。

STRING | split(REGEX; FLAGS)
Caution

引数を1つだけ取るsplitフィルターは、その引数を固定文字列として扱います。

splitで正規表現を使うには、必ず2番目の引数を指定する必要があります。空文字列を指定してもかまいません。

任意の空白文字で文字列を分割する例です。

"first   second           third fourth" | split("\\s+"; "")
# => ["first", "second", "third", "fourth"]
置換

subとgsubフィルターは、入力文字列を変換し、一致した部分を置換文字列に置き換えることができます。

最初に一致した部分だけを置き換えるには、subを使います。 一致したすべての部分を置き換えるには、gsubを使います。

STRING | sub(REGEX; REPLACEMENT)
STRING | sub(REGEX; REPLACEMENT; FLAGS)
STRING | gsub(REGEX; REPLACEMENT)
STRING | gsub(REGEX; REPLACEMENT; FLAGS)
"Goodnight kittens. Goodnight mittens." | sub("night"; " morning")
# => "Good morning kittens. Goodnight mittens."

"Goodnight kittens. Goodnight mittens." | gsub("night"; " morning")
# => "Good morning kittens. Good morning mittens."

置換テキストでは、一致した部分文字列を参照できます。名前付きキャプチャと文字列補間を使います。

"Some 3-letter acronyms: gnu, csv, png"
| gsub( "\\b(?<tla>[[:alpha:]]{3})\\b";     # find words 3 letters long
        "\(.tla | ascii_upcase)" )          # upper-case the match
# => "Some 3-letter acronyms: GNU, CSV, PNG"

フラグ

上記のすべてのフィルターで、FLAGSは、対応しているフラグを0個以上並べた文字列です。

  • g - グローバル検索(最初の1つだけでなく、すべての一致を探します)
  • i - 大文字と小文字を区別しない検索
  • m - 複数行モード('.'が改行に一致します)
  • n - 空の一致を無視します
  • p - sモードとmモードの両方が有効になります
  • s - 単一行モード('^' -> '\A'、'$' -> '\Z')
  • l - 最も長い一致を探します
  • x - 拡張正規表現フォーマット(空白とコメントを無視します)

たとえば

"JIRAISSUE-1234" | capture("(?<project>\\w+)-(?<issue_num>\\d+)")

# or with Extended formatting

"JIRAISSUE-1234" | capture("
                     (?<project>   \\w+ )  # the Jira project
                     -                     # followed by a hyphen
                     (?<issue_num> \\d+ )  # followed by digits
                   "; "x")

説明

あなたは、チャットボットを開発している会社で、正規表現のスペシャリストとして働くことになりました。

開発はまだごく初期の段階です。 ミッションは、正規表現を使って、チャットボットが自然言語を理解し、生成する能力を高めることです。

1. 有効なコマンドかどうかを確認する

チャットボットは賢いだけでなく、忠実なアシスタントでもあります。 チャットボットに何かを尋ねるには、ユーザーはコマンドの先頭で**"Chatbot"**という単語を言わなければなりません。 キーワードが大文字でも小文字でもかまいません。 重要なのは、その単語の位置です。

ユーザーがコマンドを出しているときにチャットボットがそれを認識できるようにする、is_valid_command関数を実装しましょう。

"Chatbot, play a song from the 80's." | is_valid_command
# => true
"Hey Chatbot, where is the closest pharmacy?" | is_valid_command
# => false
"CHATBOT, do you have a solution for this challenge?" | is_valid_command
# => true

2. 暗号化された絵文字を削除する

チャットボットは、人間が感情を表すために絵文字をどのように使うのかを理解するのが苦手です。 チャットボットがユーザーのメッセージを受け取ると、それぞれの絵文字は"emoji"という文字列のあとに_id番号_が続く形で表されます。

文字列を受け取り、メッセージ全体からすべての絵文字を削除するremove_emojiメソッドを実装しましょう。

絵文字を含まない行は、そのままの形で返します。 絵文字の文字列だけを削除します。 空白は調整しないでください。

"I love playing videogames emoji3465 it's one of my hobbies" | remove_emoji
# => "I love playing videogames  it's one of my hobbies"

3. 有効な電話番号かどうかを確認する

チャットボットとのやり取りの中で、ユーザーが電話番号を伝えることがあります。 チャットボットは、特定の形式の番号にしか発信できません。

check_phone_number関数を実装しましょう。

番号が有効であれば、チャットボットはユーザーに感謝し、番号を確認するメッセージで応答します。 番号が無効であれば、チャットボットはその電話番号が有効でないことをユーザーに伝えます。

期待される形式は(+NN) NNN-NNN-NNNで、Nは数字です。

"chatbot my phone number is (+34) 659-771-594" | check_phone_number
# => "Thanks! Your phone number is OK."
"chatbot, call me at 659-771-594" | check_phone_number
# => "Oops, it seems like I can't reach out to 659-771-594."

4. ウェブサイトのリンクを取得する

チャットボットはとても好奇心旺盛なソフトウェアです。 特定の話題についてインターネットで検索できるにもかかわらず、関連する情報を見つけるために訪れるのにぴったりな素敵なウェブサイトを、ユーザーに尋ねるのが好きです。

会話の例:

チャットボット: ねえユーザー、JavaScriptでコードを書く方法を学びたいんです。どこか学べる素敵なウェブサイトを知りませんか?

ユーザー: exercism.orgでたくさん学びました。あそこには素晴らしいコンテンツがたくさんあります。

ウェブサイトのドメインの配列を返すget_domains関数を実装しましょう。

"I learned a lot from exercism.org and google.com" | get_domains
# => ["exercism.org", "google.com"]

5. ユーザーにあいさつする

礼儀正しいチャットボットは、ユーザーを名前で呼びかけます。 ユーザーが自己紹介をすると、チャットボットはその名前を検出して、親しみやすいあいさつで応えます。

nice_to_meet_you関数を書きましょう。

入力文字列に"My name is Someone."が含まれている場合は、名前を取り出して、"Nice to meet you, Someone."という文字列を返します。

"My name is Jean-Luc" | nice_to_meet_you
# => "Nice to meet you, Jean-Luc"

6. とてもシンプルなCSV解析

「どんどん機能を足したくなる病」に屈して、チャットボットにCSV解析関数を追加することにします。

文字列を受け取り、解析結果のフィールドの配列を返すparse_csv関数を実装しましょう。 フィールドの区切りは「カンマと、それに続く任意の空白」とします。

CSV形式のエッジケース(カンマを含むフィールドなど)は気にしません。

"first, second,third,   fourth" | parse_csv
# => ["first", "second", "third", "fourth"]
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
jq Exercism

正規表現チャットボットを始める準備はできましたか?

Exercismに登録すれば、12個のコンセプト75個の演習、そして本物の人間によるメンタリングとともに、jqを学んでマスターできます。すべて無料です。