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 và 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ẻ.
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.