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

Thay thế một Cụm từ Thông dụng Phức tạp bằng một Trình phân tích cú pháp Đơn giản

Thời gian thú nhận:Tôi đặc biệt không thích làm việc với các biểu thức chính quy. Mặc dù tôi sử dụng chúng mọi lúc, mọi thứ phức tạp hơn một /^foo.*$/ yêu cầu tôi dừng lại và suy nghĩ. Mặc dù tôi chắc chắn rằng có những người có thể giải mã các biểu thức như \A(?=\w{6,10}\z)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3}) trong nháy mắt, nhưng tôi phải mất vài phút trên googling và khiến tôi không hài lòng. Nó khá khác so với đọc Ruby.

Nếu bạn tò mò, ví dụ trên được lấy từ bài viết này trên trang đầu tìm kiếm regex.

Tình huống

Tại Honeybadger, tôi hiện đang làm việc để cải thiện giao diện người dùng tìm kiếm của chúng tôi. Giống như nhiều hệ thống tìm kiếm, của chúng tôi sử dụng một ngôn ngữ truy vấn đơn giản. Trước những thay đổi của tôi, nếu bạn muốn tìm kiếm phạm vi ngày tùy chỉnh, bạn phải nhập thủ công một truy vấn như sau:

occurred:[2017-06-12T16:10:00Z TO 2017-06-12T17:10:00Z]

Rất tiếc!

Trong giao diện người dùng tìm kiếm mới, chúng tôi muốn phát hiện khi bạn bắt đầu nhập một truy vấn liên quan đến ngày tháng và bật lên một trình chọn ngày hữu ích. Và tất nhiên, người chọn ngày mới chỉ là bước khởi đầu. Cuối cùng, chúng tôi sẽ mở rộng gợi ý phân biệt ngữ cảnh để bao gồm nhiều loại cụm từ tìm kiếm hơn. Dưới đây là một số ví dụ:

assigned:jane@email.com context.user.id=100
resolved:false ignored:false occurred:[
params.article.title:"Starr's parser post"       foo:'ba

Tôi cần mã hóa các chuỗi này theo cách:

  • Khoảng trắng phân tách các mã thông báo, ngoại trừ khi được bao quanh bởi '', "" hoặc []
  • Khoảng trắng chưa được trích dẫn là mã thông báo của chính nó
  • Tôi có thể chạy tokens.join("") để tạo lại chính xác chuỗi đầu vào

Ví dụ:

tokenize(%[params.article.title:"Starr's parser post"       foo:'ba])
=> ["params.article.title:\"Starr's parser post\"", "       ", "foo:'ba"]

Sử dụng Biểu thức Chính quy

Suy nghĩ đầu tiên của tôi là sử dụng một biểu thức chính quy để xác định mã thông báo hợp lệ sẽ trông như thế nào, sau đó sử dụng String#split để chia chuỗi thành các mã thông báo. Thực ra đó là một thủ thuật khá hay:

# The parens in the regexp mean that the separator is added to the array
"foo  bar  baz".split(/(foo|bar|baz)/)
=> ["", "foo", "  ", "bar", "  ", "baz"]

Điều này ban đầu trông có vẻ hứa hẹn, mặc dù các chuỗi trống kỳ lạ. Nhưng biểu thức chính quy trong thế giới thực của tôi phức tạp hơn nhiều. Bản nháp đầu tiên của tôi trông như thế này:

/
  (                          # Capture group is so split will include matching and non-matching strings
    (?:                      # The first character of the key, which is
      (?!\s)[^:\s"'\[]{1}    # ..any valid "key" char not preceeded by whitespace
      |^[^:\s"'\[]{0,1}      # ..or any valid "key" char at beginning of line
    )
    [^:\s"'\[]*              # The rest of the "key" chars
    :                        # a colon
    (?:                      # The "value" chars, which are
      '[^']+'                # ..anything surrounded by single quotes
      | "[^"]+"              # ..or anything surrounded by double quotes
      | \[\S+\sTO\s\S+\]     # ..or anything like [x TO y]
      | [^\s"'\[]+           # ..or any string not containing whitespace or special chars
    )
  )
/xi 

Làm việc với điều này cho tôi cảm giác chìm đắm. Mỗi khi tôi tìm thấy một trường hợp phức tạp, tôi sẽ phải sửa đổi biểu thức chính quy, làm cho nó thậm chí còn phức tạp hơn. Ngoài ra, nó cần phải hoạt động trong JavaScript cũng như Ruby, vì vậy một số tính năng nhất định như giao diện phủ định không khả dụng.

... Đó là khoảng thời gian mà sự vô lý của tất cả những điều này xảy ra với tôi. Cách tiếp cận biểu thức chính quy mà tôi đang sử dụng phức tạp hơn nhiều so với việc viết một trình phân tích cú pháp đơn giản từ đầu.

Giải phẫu trình phân tích cú pháp

Tôi không phải là chuyên gia, nhưng trình phân tích cú pháp đơn giản rất đơn giản. Tất cả những gì họ làm là:

  • Bước qua một chuỗi, từng ký tự
  • Nối mỗi ký tự vào một vùng đệm
  • Khi gặp phải điều kiện phân tách mã thông báo, hãy lưu bộ đệm vào một mảng và làm trống nó.

Biết được điều này, chúng ta có thể thiết lập một trình phân tích cú pháp đơn giản để chia các chuỗi theo khoảng trắng. Nó gần tương đương với "foo bar".split(/(\s+)/) .

class Parser

  WHITESPACE = /\s/
  NON_WHITESPACE = /\S/

  def initialize
    @buffer = []
    @output = []
  end

  def parse(text) 
    text.each_char do |c|
      case c
      when WHITESPACE
        flush if previous.match(NON_WHITESPACE)
        @buffer << c
      else
        flush if previous.match(WHITESPACE)
        @buffer << c
      end
    end

    flush
    @output
  end

  protected

  def flush
    if @buffer.any?
      @output << @buffer.join("")
      @buffer = []
    end
  end

  def previous
    @buffer.last || ""
  end

end


puts Parser.new().parse("foo bar baz").inspect

# Outputs ["foo", " ", "bar", " ", "baz"]

Đây là một bước đi theo hướng những gì tôi muốn, nhưng nó thiếu hỗ trợ cho dấu ngoặc kép và dấu ngoặc kép. May mắn thay, việc thêm điều đó chỉ mất một vài dòng mã:

  def parse(text) 

    surround = nil

    text.each_char do |c|
      case c
      when WHITESPACE
        flush if previous.match(NON_WHITESPACE) && !surround
        @buffer << c
      when '"', "'"
        @buffer << c
        if !surround
          surround = c
        elsif surround == c
          flush
          surround = nil
        end
      when "["
        @buffer << c
        surround = c if !surround
      when "]"
        @buffer << c
        if surround == "["
          flush
          surround = nil
        end
      else
        flush() if previous().match(WHITESPACE) && !surround
        @buffer << c
      end
    end

    flush
    @output
  end

Mã này chỉ dài hơn một chút so với cách tiếp cận dựa trên biểu thức chính quy của tôi nhưng đơn giản hơn nhiều.

Suy nghĩ chia tay

Có lẽ có một biểu thức chính quy sẽ hoạt động tốt với trường hợp sử dụng của tôi. Nếu lịch sử là bất kỳ hướng dẫn nào, nó có lẽ đủ đơn giản để khiến tôi giống như một kẻ ngốc. :)

Nhưng tôi thực sự rất thích cơ hội viết trình phân tích cú pháp nhỏ này. Nó khiến tôi thoát ra khỏi guồng quay của tôi với cách tiếp cận regex. Như một phần thưởng tuyệt vời, tôi tin tưởng vào mã kết quả hơn bao giờ hết với mã dựa trên các biểu thức chính quy phức tạp.