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

Làm thế nào để xây dựng một trình phân tích cú pháp với Ruby

Phân tích cú pháp là nghệ thuật tạo ý nghĩa của một loạt các chuỗi và chuyển đổi chúng thành thứ mà chúng ta có thể hiểu được. Bạn có thể sử dụng biểu thức chính quy, nhưng chúng không phải lúc nào cũng phù hợp với công việc.

Ví dụ:người ta thường biết rằng phân tích cú pháp HTML bằng các biểu thức chính quy có lẽ không phải là một ý kiến ​​hay.

Trong Ruby, chúng tôi có nokogiri có thể thực hiện công việc này cho chúng tôi, nhưng bạn có thể học được nhiều điều bằng cách xây dựng trình phân tích cú pháp của riêng mình. Hãy bắt đầu!

Phân tích cú pháp với Ruby

Cốt lõi của trình phân tích cú pháp của chúng tôi là StringScanner lớp học.

Lớp này chứa một bản sao của một chuỗi và một con trỏ vị trí. Con trỏ sẽ cho phép chúng tôi duyệt qua chuỗi để tìm kiếm các mã thông báo nhất định.

Các phương pháp chúng tôi sẽ sử dụng là:

  • .peek
  • .scan_until
  • .getch

Một phương pháp hữu ích khác là .scan (không có cho đến khi).

Lưu ý :

Nếu StringScanner không khả dụng với bạn, hãy thử thêm require 'strscan'

Tôi đã viết hai bài kiểm tra làm tài liệu để chúng tôi có thể hiểu cách lớp này hoạt động:

describe StringScanner do
  let (:buff) { StringScanner.new "testing" }

  it "can peek one step ahead" do
    expect(buff.peek 1).to eq "t"
  end

  it "can read one char and return it" do
    expect(buff.getch).to eq "t"
    expect(buff.getch).to eq "e"
  end
end

Một điều quan trọng cần lưu ý về lớp này là một số phương thức nâng cao con trỏ vị trí ( getch, scan ), trong khi những người khác thì không ( peek ). Tại bất kỳ thời điểm nào, bạn có thể kiểm tra máy quét của mình (sử dụng .inspect hoặc p ) để xem nó ở đâu.

Lớp phân tích cú pháp

Lớp phân tích cú pháp là nơi hầu hết công việc diễn ra, chúng tôi sẽ khởi tạo nó bằng đoạn văn bản mà chúng tôi muốn phân tích cú pháp và nó sẽ tạo một StringScanner cho phần đó và gọi phương thức phân tích cú pháp:

def initialize(str)
  @buffer = StringScanner.new(str)
  @tags   = []
  parse
end

Trong thử nghiệm, chúng tôi xác định nó như thế này:

let(:parser) { Parser.new "<body>testing</body> <title>parsing with ruby</title>" }

Chúng ta sẽ đi sâu vào cách lớp này hoạt động như thế nào, nhưng trước tiên hãy xem phần cuối cùng của chương trình của chúng ta.

Loại thẻ

Lớp này rất đơn giản, nó chủ yếu đóng vai trò là lớp chứa &lớp dữ liệu cho các kết quả phân tích cú pháp.

class Tag
  attr_reader :name
  attr_accessor :content

  def initialize(name)
    @name = name
  end
end

Hãy phân tích cú pháp!

Để phân tích cú pháp một cái gì đó, chúng ta sẽ cần nhìn vào văn bản đầu vào của mình để tìm ra các mẫu. Ví dụ, chúng ta biết mã HTML có dạng sau:

<tag>contents</tag>

Rõ ràng có hai thành phần khác nhau mà chúng tôi có thể xác định ở đây, tên thẻ và văn bản bên trong thẻ. Nếu chúng ta định nghĩa một ngữ pháp chính thức bằng cách sử dụng ký hiệu BNF, nó sẽ trông giống như sau:

tag = <opening_tag> <contents> <closing_tag>
opening_tag = "<" <tag_name> ">"
closing_tag = "</" <tag_name> ">"

Chúng tôi sẽ sử dụng peek của StringScanners để xem liệu ký hiệu tiếp theo trên bộ đệm đầu vào của chúng tôi có phải là thẻ mở hay không. Nếu đúng như vậy thì chúng tôi sẽ gọi find_tag ​​ find_content các phương thức trên lớp Parser của chúng tôi:

def parse_element
  if @buffer.peek(1) == '<'
    @tags << find_tag
    last_tag.content = find_content
  end
end

find_tag ​​ phương thức sẽ:

  • 'Sử dụng' ký tự thẻ mở
  • Quét cho đến khi tìm thấy biểu tượng đóng (“>”)
  • Tạo và trả lại một đối tượng Thẻ mới với tên thẻ

Đây là mã, hãy lưu ý cách chúng ta phải cắt ký tự cuối cùng. Điều này là do scan_until bao gồm dấu ‘>’ trong kết quả và chúng tôi không muốn điều đó.

def find_tag
  @buffer.getch
  tag = @buffer.scan_until />/
  Tag.new(tag.chop)
end

Bước tiếp theo là tìm nội dung bên trong thẻ, điều này không quá khó vì phương thức scan_until đưa con trỏ vị trí đến đúng vị trí. Chúng tôi sẽ sử dụng lại scan_until để tìm thẻ đóng và trả lại nội dung thẻ.

Làm thế nào để xây dựng một trình phân tích cú pháp với Ruby

def find_content
  tag = last_tag.name
  content = @buffer.scan_until /<\/#{tag}>/
  content.sub("</#{tag}>", "")
end

Bây giờ :

Tất cả những gì chúng ta cần làm là gọi parse_element lặp lại cho đến khi chúng tôi không thể tìm thấy thêm thẻ trên bộ đệm đầu vào của mình.

def parse
  until @buffer.eos?
    skip_spaces
    parse_element
  end
end

Bạn có thể tìm thấy mã hoàn chỉnh tại đây:https://github.com/matugm/simple-parser. Bạn cũng có thể xem nhánh ‘nested_tags’ cho phiên bản mở rộng có thể xử lý các thẻ bên trong một thẻ khác.

Kết luận

Viết một trình phân tích cú pháp là một chủ đề thú vị và đôi khi nó cũng có thể trở nên khá phức tạp.

Nếu bạn không muốn tạo trình phân tích cú pháp của riêng mình từ đầu, bạn có thể sử dụng một trong những cái gọi là 'trình tạo trình phân tích cú pháp'. Trong Ruby, chúng ta có treetop và parslet.