การแยกวิเคราะห์เป็นศิลปะในการทำความเข้าใจกลุ่มสตริงและแปลงเป็นบางสิ่งที่เราเข้าใจได้ คุณสามารถใช้นิพจน์ทั่วไปได้ แต่ไม่เหมาะกับงานเสมอไป
ตัวอย่างเช่น เป็นความรู้ทั่วไปที่การแยกวิเคราะห์ HTML ด้วยนิพจน์ทั่วไปอาจไม่ใช่ความคิดที่ดี
ใน Ruby เรามี nokogiri ที่สามารถทำงานนี้ให้เราได้ แต่คุณสามารถเรียนรู้ได้มากมายด้วยการสร้าง parser ของคุณเอง เริ่มกันเลย!
การแยกวิเคราะห์ด้วย Ruby
แกนหลักของ parser ของเราคือ StringScanner ชั้นเรียน
คลาสนี้มีสำเนาของสตริงและตัวชี้ตำแหน่ง ตัวชี้จะช่วยให้เราข้ามสตริงเพื่อค้นหาโทเค็นบางตัวได้
วิธีการที่เราจะใช้คือ:
- .peek
- .scan_until
- .getch
อีกวิธีหนึ่งที่มีประโยชน์คือ .scan (โดยไม่ต้องจนกว่า)
หมายเหตุ :
หากไม่มี StringScanner ให้คุณลองเพิ่ม require 'strscan'
ฉันเขียนการทดสอบสองครั้งเป็นเอกสารเพื่อให้เราเข้าใจว่าคลาสนี้ควรทำงานอย่างไร:
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
สิ่งสำคัญอย่างหนึ่งที่ควรสังเกตเกี่ยวกับคลาสนี้คือ เมธอดบางอย่างเลื่อนตัวชี้ตำแหน่ง (ดึง สแกน ) ในขณะที่คนอื่นไม่ (แอบดู ). คุณสามารถตรวจสอบสแกนเนอร์ของคุณได้ทุกเมื่อ (โดยใช้ .inspect หรือ p ) เพื่อดูว่าอยู่ที่ไหน
คลาส parser
คลาส parser เป็นที่ที่งานส่วนใหญ่เกิดขึ้น เราจะเริ่มต้นด้วยตัวอย่างข้อความที่เราต้องการแยกวิเคราะห์ และจะสร้าง StringScanner สำหรับสิ่งนั้นและเรียกวิธีการแยกวิเคราะห์:
def initialize(str) @buffer = StringScanner.new(str) @tags = [] parse end
ในการทดสอบเราให้คำจำกัดความดังนี้:
let(:parser) { Parser.new "<body>testing</body> <title>parsing with ruby</title>" }
เราจะเจาะลึกว่าชั้นเรียนนี้ทำงานอย่างไรในระยะเวลาสั้นๆ แต่ก่อนอื่น มาดูส่วนสุดท้ายของโปรแกรมกันก่อน
คลาสแท็ก
คลาสนี้เรียบง่ายมาก ส่วนใหญ่ทำหน้าที่เป็นคอนเทนเนอร์และคลาสข้อมูลสำหรับผลการแยกวิเคราะห์
class Tag attr_reader :name attr_accessor :content def initialize(name) @name = name end end
มาวิเคราะห์กัน!
ในการแยกวิเคราะห์บางอย่าง เราจะต้องดูที่ข้อความที่ป้อนเพื่อค้นหารูปแบบ ตัวอย่างเช่น เรารู้ว่าโค้ด HTML มีรูปแบบดังนี้:
<tag>contents</tag>
เห็นได้ชัดว่ามีสององค์ประกอบที่แตกต่างกันที่เราสามารถระบุได้ที่นี่ ชื่อแท็กและข้อความภายในแท็ก หากเราจะกำหนดไวยากรณ์ที่เป็นทางการโดยใช้รูปแบบ BNF จะมีลักษณะดังนี้:
tag = <opening_tag> <contents> <closing_tag> opening_tag = "<" <tag_name> ">" closing_tag = "</" <tag_name> ">"
เราจะใช้แอบดู .ของ StringScanners เพื่อดูว่าสัญลักษณ์ถัดไปในบัฟเฟอร์อินพุตของเราเป็นแท็กเปิดหรือไม่ หากเป็นกรณีนี้ เราจะเรียก find_tag และ find_content วิธีการในคลาส Parser ของเรา:
def parse_element if @buffer.peek(1) == '<' @tags << find_tag last_tag.content = find_content end end
find_tag วิธีการจะ:
- 'ใช้' อักขระแท็กเปิด
- สแกนจนกว่าจะพบสัญลักษณ์ปิด (“>”)
- สร้างและส่งคืนออบเจกต์แท็กใหม่ที่มีชื่อแท็ก
นี่คือรหัส สังเกตว่าเราต้องสับ .อย่างไร อักขระสุดท้าย ทั้งนี้เป็นเพราะ scan_until รวม '>' ไว้ในผลลัพธ์ และเราไม่ต้องการสิ่งนั้น
def find_tag @buffer.getch tag = @buffer.scan_until />/ Tag.new(tag.chop) end
ขั้นตอนต่อไปคือการค้นหาเนื้อหาภายในแท็ก ซึ่งไม่น่าจะยากเกินไป เนื่องจากวิธี scan_until จะทำให้ตัวชี้ตำแหน่งเลื่อนไปยังตำแหน่งที่ถูกต้อง เราจะใช้ scan_until อีกครั้งเพื่อค้นหาแท็กปิดและส่งคืนเนื้อหาแท็ก
def find_content tag = last_tag.name content = @buffer.scan_until /<\/#{tag}>/ content.sub("</#{tag}>", "") end
ตอนนี้ :
สิ่งที่เราต้องทำคือเรียก parse_element
วนซ้ำจนกว่าเราจะไม่พบแท็กเพิ่มเติมในบัฟเฟอร์อินพุตของเรา
def parse until @buffer.eos? skip_spaces parse_element end end
คุณสามารถค้นหาโค้ดทั้งหมดได้ที่นี่: https://github.com/matugm/simple-parser คุณยังสามารถดูที่สาขา 'nested_tags' สำหรับเวอร์ชันขยายที่สามารถจัดการกับแท็กภายในแท็กอื่นได้
บทสรุป
การเขียน parser เป็นหัวข้อที่น่าสนใจและบางครั้งก็อาจซับซ้อนได้เช่นกัน
หากคุณไม่ต้องการสร้าง parser ของคุณเองตั้งแต่เริ่มต้น คุณสามารถใช้หนึ่งใน 'ตัวสร้าง parser' ที่เรียกว่า ใน Ruby เรามียอดไม้และผักชีฝรั่ง