Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの隠された宝石、StringScanner

Rubyは楽しい言語であるだけでなく、優れた標準ライブラリも付属しています。そのうちのいくつかはそれほど知られておらず、ほとんど隠された宝石です。今日、ゲストライターのマイケルコールがお気に入りのStringscannerを紹介します。

ルビーの隠された宝石:StringScanner

OpenStructやSetoverCSV解析などのデータ構造からベンチマークまで、サードパーティのgemのインストールに頼ることなくかなり遠くまで行くことができます。ただし、Rubyの標準インストールで利用できるあまり知られていないライブラリがいくつかあり、非常に便利です。その1つが StringScannerです。 ドキュメントによると、「文字列に対する字句スキャン操作を提供します」

スキャンと解析

では、「字句スキャン」とは正確にはどういう意味ですか?基本的に、特定のルールに従って、入力文字列を取得し、そこから意味のある情報を抽出するプロセスについて説明します。たとえば、これは 2 + 1のような式をとるコンパイラの最初の段階で見ることができます。 入力として、それを次のトークンのシーケンスに変換します:

[{ number: "1" }, {operator: "+"}, { number: "1"}]

字句スキャナーは通常、有限状態オートマトンとして実装されており、それらを生成できる有名なツールがいくつかあります(ANTLRやRagelなど)。

ただし、解析のニーズがそれほど複雑ではなく、正規表現ベースの StringScannerのような単純なライブラリである場合もあります。 このような状況で非常に便利です。いわゆるスキャンポインタの場所を記憶することで機能します これは文字列へのインデックスにすぎません。次に、スキャンプロセスは、スキャンポインタの直後のコードを指定された式と照合しようとします。マッチング操作とは別に、 StringScanner また、スキャンポインタを移動する(文字列を前後に移動する)、先を見据える(スキャンポインタをまだ変更せずに次の項目を確認する)、および文字列の現在の場所(先頭または先頭)を確認する方法も提供します。行の終わり/文字列全体など)

Railsログの解析

十分な理論ですが、 StringScannerを見てみましょう。 アクションで。次の例では、以下のようなRailsのログエントリを取得します。

log_entry = <<EOS
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
Processing by HomeController#index as HTML
  Rendered text template within layouts/application (0.0ms)
  Rendered layouts/_assets.html.erb (2.0ms)
  Rendered layouts/_top.html.erb (2.6ms)
  Rendered layouts/_about.html.erb (0.3ms)
  Rendered layouts/_google_analytics.html.erb (0.4ms)
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
EOS

そしてそれを次のハッシュに解析します:

{
  method: "GET",
  path: "/"
  ip: "127.0.0.1",
  timestamp: "2017-08-20 20:53:10 +0900",
  success: true,
  response_code: "200",
  duration: "79ms",
}

NB:これは StringScannerの良い例になりますが 実際のアプリケーションでは、LogrageとそのJSONログフォーマッターを使用する方がよいでしょう。

StringScannerを使用するには 最初にそれを要求する必要があります:

require 'strscan'

この後、ログエントリを引数としてコンストラクタに渡すことで、新しいインスタンスを初期化できます。同時に、解析作業の結果を保持するために空のハッシュも定義します。

scanner = StringScanner.new(log_entry)
log = {}

これで、スキャナーのposメソッドを使用して、スキャンポインターの現在の場所を取得できます。予想どおり、結果は 0です。 、文字列の最初の文字:

scanner.pos #=> 0

これを視覚化して、プロセスを簡単に実行できるようにします。

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

スキャナーの状態をさらに詳しく調べるには、 beginning_of_line?を使用できます。 およびeos? スキャンポインタが現在行の先頭にあり、入力をまだ完全に消費していないことを確認するには:

scanner.beginning_of_line? #=> true
scanner.eos? #=> false

抽出したい情報の最初のビットはHTTPリクエストメソッドです。これは、「Started」という単語の直後にスペースが続きます。スキャナーの適切な名前のskipメソッドを使用して、スキャンポインターを進めることができます。これにより、無視された文字の数(この場合は8)が返されます。さらに、matched?を使用できます。すべてが期待どおりに機能したことを確認するには:

scanner.skip(/Started /) #=> 8
scanner.matched? #=> true

スキャンポインタがリクエストメソッドの直前になりました:

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

これで、scan_untilを使用して実際の値を抽出できます。これにより、正規表現の一致全体が返されます。 requestメソッドはすべて大文字であるため、単純な文字クラスと +を使用できます。 1つまたは複数の文字に一致する演算子:

log[:method] = scanner.scan_until(/[A-Z]+/) #=> "GET"

この操作の後、スキャンポインタは単語「GET」の最後の「T」になります。

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

したがって、要求されたパスを抽出するには、1つのスペースをスキップしてから、二重引用符で囲まれたすべてを抽出する必要があります。これを実現する方法はいくつかありますが、そのうちの1つは、キャプチャグループ(括弧内に含まれる正規表現の一部、つまり(。+)を使用する方法です。 )任意の文字の1つ以上に一致する:

scanner.scan(/\s"(.+)"/) #=> " \"/\""

ただし、この scanの戻り値は使用しません。 直接操作しますが、代わりにキャプチャを使用して、代わりに最初のキャプチャグループの値を取得します。

log[:path] =  scanner.captures.first #=> "/"

パスが正常に抽出され、スキャンポインタが二重引用符で囲まれています:

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

ログからIPアドレスを解析するには、もう一度 skipを使用します。 スペースで囲まれた文字列「for」を無視してから、 scan_untilを使用します 1つ以上の空白以外の文字に一致させる( \ s 空白と[^\ s]を表す文字クラスです その否定です):

scanner.skip(/ for /) #=> 5
log[:ip] = scanner.scan_until(/[^\s]+/) #=> "127.0.0.1"

スキャンポインタが今どこにあるかわかりますか?少し考えてから、答えを解決策と比較してください。

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

タイムスタンプの解析は、今では非常に馴染みがあるはずです。まず、信頼できる古い skipを使用します "のリテラル文字列"を無視します 次に、 scan_untilを使用します $で表される現在の行の終わりまで読み取る 正規表現の場合:

scanner.skip(/ at /) #=> 4
log[:timestamp] = scanner.scan_until(/$/) #=> "2017-08-20 20:53:10 +0900"

次に関心のある情報は、最後の行のHTTPステータスコードです。そのため、skip_untilを使用して、「Completed」という単語の後のスペースまで移動します。

scanner.skip_until(/Completed /) #=> 296

名前が示すように、これは scan_untilと同様に機能します ただし、一致した文字列を返す代わりに、スキップされた文字の数を返します。これにより、スキャンポインタが目的のHTTPステータスコードの直前に配置されます。

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
         ^

実際のHTTP応答コードをスキャンする前に、HTTP応答コードが成功(この例では2xx範囲のコード)または失敗(他のすべての範囲)を示しているかどうかを判断できたら素晴らしいと思いませんか?これを実現するために、実際にスキャンポインターを移動せずに、ピークを使用して次の文字を確認します。

log[:success] = scanner.peek(1) == "2" #=> true

これで、スキャンを使用して、正規表現 / \ d {3} /で表される次の3文字を読み取ることができます。 :

log[:response_code] = scanner.scan(/\d{3}/) #=> "200"

もう一度、スキャンポインタは以前に一致した正規表現の最後にあります:

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
         ^

ログエントリから抽出する最後の情報は、ミリ秒単位の実行時間です。これは、 skipによって実現できます。 文字列"OKin"にpingを実行します 次に、リテラル文字列 "ms"までのすべてを読み取ります。 。

scanner.skip(/ OK in /) #=> 7
log[:duration] = scanner.scan_until(/ms/) #=> "79ms"

そして最後のビットがそこにあるので、必要なハッシュがあります。

{
  method: "GET",
  path: "/"
  ip: "127.0.0.1",
  timestamp: "2017-08-20 20:53:10 +0900",
  success: true,
  response_code: "200",
  duration: "79ms",
}

概要

RubyのStringScanner 単純な正規表現と本格的なレクサーの中間に位置します。複雑なスキャンと解析のニーズには最適ではありません。しかし、その単純な性質により、基本的な正規表現の知識を持っている人なら誰でも入力文字列から情報を簡単に抽出できます。私は過去にそれらを本番コードでうまく使用しました。この隠された宝石を発見していただければ幸いです。

PS:次に強調すべき隠された宝石についてあなたが思うことを教えてください!


  1. ほとんどの人が聞いたことのない7つの素晴らしいRubyGems

    Railsプロジェクトで使用できる最高のRubygemは何ですか? それがこの記事でわかることです! 私はあなたに7つの宝石を与えるつもりですが、あなたが百万回見たのと同じ古い宝石ではありません 、非常に役立つがあまり知られていない宝石をいくつか紹介します。 しかし、それを行う前に… 警告。 ほぼすべてのことのために宝石を引き込む開発者を見てきました。 リモートで役立つと思われる場合。 その宝石が彼らが抱えている問題を解決するかどうか、それが最良の選択肢であるかどうか、よく維持され、文書化されているかどうかなどを考える時間をとらずに。 それは間違いです。 なぜですか?

  2. Ruby2.6の9つの新機能

    Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,