Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ค้นพบ Ruby's Hidden Gem: StringScanner สำหรับ Text Parsing อันทรงพลัง

Ruby ไม่ได้เป็นภาษาที่สนุกอย่างเดียว มันยังมาพร้อมกับ excellent standard library บางส่วนไม่ค่อยรู้จัก และเกือบจะเป็น hidden Gems วันนี้ guest writer Michael Kohl ไฮไลท์ favorite ของเขา: StringScanner

Ruby's hidden Gems: StringScanner

คนสามารถไปไกลได้พอสมควรโดยไม่ต้อง resort ไปที่การติดตั้ง third party gems ตั้งแต่ data structures อย่าง OpenStruct และ Set ไปจนถึง CSV parsing และ benchmarking อย่างไรก็ตาม มี less well-known libraries บางส่วนใน standard installation ของ Ruby ที่สามารถมีประโยชน์มาก หนึ่งในนั้นคือ StringScanner ซึ่งตาม documentation "provides lexical scanning operations on a string"

Scanning and parsing

那麼 "lexical scanning" หมายถึงอะไร exactly? 基本上它描述了根據某些規則從輸入字符串中提取有意義信息片段的過程 例如 這可以在編譯器的第一階段看到 它將像 2 + 1 這樣的表達式作為輸入並將其轉換為以下 token 序列:

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

Lexical scanners 通常被實現為 finite-state automata 並且有幾個知名工具可以為我們生成它們 (例如 ANTLR 或 Ragel)

然而 有時我們的 parsing 需求沒有那麼複雜 一個基於 regular expression 的簡單庫如 StringScanner 可以在這種情況下派上大用場 它通過記住所謂 scan pointer 的位置來工作 這無非是字符串中的一個索引 掃描過程然後嘗試將掃描指針右側的代碼與提供的表達式匹配 除了匹配操作 StringScanner 還提供移動掃描指針的方法 (向前或向後移動字符串)、looking ahead (查看接下來是什麼而不修改掃描指針) 以及找出我們目前在字符串中的位置 (是行首/行尾/整個字符串的開頭/結尾等)

Parsing Rails Logs

夠了理論 讓我們看看 StringScanner 實際運作 下面的例子將獲取像下面這樣的 Rails log entry

log_entry = <

並將其解析為以下 hash:

{
  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 log formatter

為了使用 StringScanner 我們首先需要 require 它:

require 'strscan'

在此之後 我們可以通過將 log entry 作為參數傳遞給構造函數來初始化一個新實例 同時 我們也將定義一個空 hash 來保存我們解析工作的結果:

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

我們現在可以使用 scanner 的 pos 方法獲取 scan pointer 的當前位置 正如預期 結果是 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)

為了進一步內省 scanner 的狀態 我們可以使用 beginning_of_line? 和 eos? 來確認掃描指針當前位於行首且我們尚未完全消耗輸入:

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

我們想提取的第一條信息是 HTTP request method 它位於 "Started" 後面跟著一個空格 我們可以使用 scanner 適當命名的 skip 方法來推進掃描指針 它將返回被忽略的字符數 在我們的情況下是 8 此外 我們可以使用 matched? 確認一切按預期工作:

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

掃描指針現在位於 request method 之前:

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 method 全部是大寫 我們可以使用簡單的字符類和 + 操作符匹配一個或多個字符:

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)

為了提取請求路徑 我們將需要跳過一個空格然後提取雙引號內的所有內容 有幾種方法可以實現 其中一種是通過捕獲組 (正則表達式中括號內的部分 即 (.+)) 匹配一個或多個任意字符:

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

然而 我們不會直接使用這個 scan 操作的返回值 而是使用 captures 獲取第一個捕獲組的值:

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 匹配一個或多個非空白字符 (\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)

解析 timestamp 應該現在感覺很熟悉了 首先我們使用可靠的 skip 忽略字面字符串 " at " 然後使用 scan_until 讀取直到當前行結束 這在正則表達式中用 $ 表示:

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

我們感興趣的下一條信息是最後一行的 HTTP status code 所以我們將使用 skip_until 帶我們一路到 "Completed" 一詞後面的空格

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

正如名稱所示 這與 scan_until 類似 但不返回匹配的字符串 而是返回跳過的字符數 這將掃描指針放在我們感興趣的 HTTP status code 前面

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 response code 之前 如果我們能知道 HTTP response code 表示成功 (為了這個例子 2xx 範圍內的任何代碼) 還是失敗 (所有其他範圍) 不是很好嗎? 為了實現這一點 我們將使用 peek 查看下一個字符 而不實際移動掃描指針

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

現在我們可以使用 scan 讀取接下來的三個字符 用正則表達式 /\d{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 過字符串 " OK in " 然後讀取直到並包括字面字符串 "ms" 來實現

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

有了最後一條信息 我們得到了我們想要的 hash

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

Summary

Ruby 的 StringScanner 佔據了一個不錯的中間地帶 在簡單的正則表達式和功能齊全的 lexer 之間 它不是複雜掃描和解析需求的最佳選擇 但它的 straightforward 自然使得每個擁有基本正則表達式知識的人都能從輸入字符串中提取信息 我過去在生產代碼中成功使用過它們 我們希望你會發現這個 hidden Gem

PS: 告訴我們你認為還有哪些 hidden Gems 我們應該 highlight!