Computer >> Máy Tính >  >> Lập trình >> Ruby

Ruby's Hidden Gems, StringScanner

Ruby không chỉ là một ngôn ngữ vui nhộn, nó còn đi kèm với một thư viện tiêu chuẩn tuyệt vời. Một số trong số đó không được biết đến, và hầu như là những viên ngọc ẩn. Hôm nay, nhà văn khách mời Michael Kohl nêu bật một tác phẩm yêu thích:Stringscanner.

Đá quý ẩn của Ruby:StringScanner

Người ta có thể tiến khá xa mà không cần phải cài đặt các viên ngọc của bên thứ ba, từ các cấu trúc dữ liệu như OpenStruct và Đặt qua phân tích cú pháp CSV đến đo điểm chuẩn. Tuy nhiên, có một số thư viện ít nổi tiếng hơn có sẵn trong cài đặt tiêu chuẩn của Ruby có thể rất hữu ích, một trong số đó là StringScanner theo tài liệu thì "cung cấp các thao tác quét từ vựng trên một chuỗi" .

Quét và phân tích cú pháp

Vậy chính xác thì "quét từ vựng" có nghĩa là gì? Về cơ bản, nó mô tả quá trình lấy một chuỗi đầu vào và trích xuất các bit thông tin có ý nghĩa từ nó, tuân theo các quy tắc nhất định. Ví dụ:điều này có thể được nhìn thấy ở giai đoạn đầu tiên của trình biên dịch có biểu thức như 2 + 1 làm đầu vào và biến nó thành chuỗi mã thông báo sau:

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

Máy quét Lexical thường được triển khai dưới dạng tự động dữ liệu trạng thái hữu hạn và có một số công cụ nổi tiếng có thể tạo chúng cho chúng tôi (ví dụ:ANTLR hoặc Ragel).

Tuy nhiên, đôi khi nhu cầu phân tích cú pháp của chúng ta không quá phức tạp và một thư viện đơn giản hơn như StringScanner dựa trên biểu thức chính quy có thể rất hữu ích trong những tình huống như vậy. Nó hoạt động bằng cách ghi nhớ vị trí của cái gọi là con trỏ quét mà chỉ là một chỉ mục trong chuỗi. Quá trình quét sau đó sẽ cố gắng khớp mã ngay sau con trỏ quét với biểu thức được cung cấp. Ngoài các hoạt động đối sánh, StringScanner cũng cung cấp các phương thức để di chuyển con trỏ quét (di chuyển tới hoặc lùi qua chuỗi), nhìn về phía trước (xem điều gì tiếp theo mà không cần sửa đổi con trỏ quét) cũng như tìm ra vị trí hiện tại của chuỗi chúng ta đang ở đâu (có phải là đầu hay không cuối dòng / toàn bộ chuỗi, v.v.).

Phân tích cú pháp Nhật ký Rails

Lý thuyết đủ rồi, hãy xem StringScanner đang hoạt động. Ví dụ sau sẽ lấy một mục nhập nhật ký của Rails như bên dưới,

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

và phân tích cú pháp nó thành hàm băm sau:

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

NB:Mặc dù điều này là một ví dụ điển hình cho StringScanner một ứng dụng thực sẽ tốt hơn khi sử dụng Lograge và trình định dạng nhật ký JSON của nó.

Để sử dụng StringScanner trước tiên chúng ta cần yêu cầu nó:

require 'strscan'

Sau đó, chúng ta có thể khởi tạo một thể hiện mới bằng cách chuyển mục nhập nhật ký làm đối số cho hàm tạo. Đồng thời, chúng tôi cũng sẽ xác định một hàm băm trống để lưu giữ kết quả của những nỗ lực phân tích cú pháp của chúng tôi:

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

Bây giờ chúng ta có thể sử dụng phương pháp vị trí của máy quét để lấy vị trí hiện tại của con trỏ quét của chúng ta. Như mong đợi, kết quả là 0 , ký tự đầu tiên của chuỗi:

scanner.pos #=> 0

Hãy hình dung điều này để quá trình theo dõi dễ dàng hơn:

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)

Để tìm hiểu sâu hơn về trạng thái của máy quét, chúng ta có thể sử dụng beginning_of_line?eos? để xác nhận rằng con trỏ quét hiện đang ở đầu dòng và chúng tôi chưa sử dụng hết dữ liệu đầu vào của mình:

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

Bit thông tin đầu tiên chúng tôi muốn trích xuất là phương thức yêu cầu HTTP, có thể được tìm thấy ngay sau từ "Bắt đầu" và theo sau là một khoảng trắng. Chúng ta có thể sử dụng phương thức bỏ qua được đặt tên thích hợp của máy quét để nâng cao con trỏ quét, phương thức này sẽ trả về số ký tự bị bỏ qua, trong trường hợp của chúng ta là 8. Ngoài ra, chúng ta có thể sử dụng đối sánh không? để xác nhận rằng mọi thứ hoạt động như mong đợi:

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

Con trỏ quét hiện nằm ngay trước phương thức yêu cầu:

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)

Bây giờ chúng ta có thể sử dụng scan_until để trích xuất giá trị thực, giá trị này trả về toàn bộ kết quả khớp biểu thức chính quy. Vì phương thức yêu cầu tất cả đều là chữ hoa, chúng ta có thể sử dụng một lớp ký tự đơn giản và + toán tử khớp với một hoặc các ký tự:

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

Sau thao tác này, con trỏ quét sẽ ở chữ "T" cuối cùng của từ "GET".

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)

Để trích xuất đường dẫn được yêu cầu, do đó chúng tôi sẽ cần bỏ qua một khoảng trắng và sau đó trích xuất mọi thứ được đặt trong dấu ngoặc kép. Có một số cách để đạt được điều này, một trong số đó là thông qua nhóm nắm bắt (một phần của Biểu thức chính quy được bao gồm trong dấu ngoặc đơn, tức là (.+) ) khớp với một hoặc nhiều ký tự bất kỳ:

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

Tuy nhiên, chúng tôi sẽ không sử dụng giá trị trả về của scan này hoạt động trực tiếp, nhưng thay vào đó sử dụng các bản chụp để nhận giá trị của nhóm bản chụp đầu tiên thay thế:

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

Chúng tôi đã trích xuất thành công đường dẫn và con trỏ quét hiện đang ở dấu ngoặc kép đóng:

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)

Để phân tích cú pháp địa chỉ IP từ nhật ký, chúng tôi một lần nữa sử dụng skip để bỏ qua chuỗi "for" được bao quanh bởi dấu cách và sau đó sử dụng scan_until để khớp với một hoặc nhiều ký tự không có khoảng trắng (\s là lớp ký tự đại diện cho khoảng trắng và [^\s] là phủ định của nó):

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

Bạn có thể cho biết con trỏ quét sẽ ở đâu bây giờ không? Hãy suy nghĩ về nó một lúc và sau đó so sánh câu trả lời của bạn với giải pháp:

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)

Phân tích cú pháp dấu thời gian bây giờ sẽ cảm thấy rất quen thuộc. Đầu tiên, chúng tôi sử dụng skip cũ đáng tin cậy bỏ qua chuỗi ký tự " at " và sau đó sử dụng scan_until để đọc cho đến cuối dòng hiện tại, được biểu thị bằng $ trong biểu thức chính quy:

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

Phần thông tin tiếp theo mà chúng tôi quan tâm là mã trạng thái HTTP ở dòng cuối cùng, vì vậy chúng tôi sẽ sử dụng ignore_until để đưa chúng tôi đến khoảng trống sau từ "Đã hoàn thành".

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

Như tên cho thấy điều này hoạt động tương tự như scan_until nhưng thay vì trả về chuỗi đã so khớp, nó trả về số ký tự bị bỏ qua. Điều này đặt con trỏ quét ngay trước mã trạng thái HTTP mà chúng tôi quan tâm.

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)
            ^

Bây giờ trước khi chúng ta quét mã phản hồi HTTP thực tế, sẽ không hay nếu chúng ta có thể biết liệu mã phản hồi HTTP biểu thị là thành công (vì ví dụ này là bất kỳ mã nào trong phạm vi 2xx) hay thất bại (tất cả các phạm vi khác)? Để đạt được điều này, chúng tôi sẽ tận dụng khả năng nhìn trộm để xem ký tự tiếp theo mà không thực sự di chuyển con trỏ quét.

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

Giờ đây, chúng ta có thể sử dụng tính năng quét để đọc ba ký tự tiếp theo, được biểu thị bằng biểu thức chính quy /\d{3}/ :

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

Một lần nữa, con trỏ quét sẽ ở ngay cuối biểu thức chính quy đã đối sánh trước đó:

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)
            ^

Chút thông tin cuối cùng mà chúng tôi muốn trích xuất từ ​​mục nhập nhật ký của mình là thời gian thực thi tính bằng mili giây, có thể đạt được bằng cách skip ping qua chuỗi " OK in " và sau đó đọc mọi thứ lên đến và bao gồm cả chuỗi ký tự "ms" .

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

Và với bit cuối cùng trong đó, chúng tôi có hàm băm mà chúng tôi muốn.

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

Tóm tắt

StringScanner của Ruby chiếm một vị trí trung gian tốt đẹp giữa các biểu thức chính quy đơn giản và một lexer đầy đủ. Nó không phải là lựa chọn tốt nhất cho các nhu cầu quét và phân tích cú pháp phức tạp. Nhưng bản chất đơn giản của nó giúp mọi người có kiến ​​thức cơ bản về biểu thức chính quy dễ dàng trích xuất thông tin từ các chuỗi đầu vào và tôi đã sử dụng chúng thành công trong mã sản xuất trước đây. Chúng tôi hy vọng bạn sẽ khám phá ra viên ngọc ẩn này.

Tái bút:Hãy cho chúng tôi biết những gì bạn nghĩ là Đá quý ẩn mà chúng tôi nên làm nổi bật tiếp theo!