Ruby ไม่ได้เป็นเพียงภาษาที่สนุกสนานเท่านั้น แต่ยังมาพร้อมกับไลบรารีมาตรฐานที่ยอดเยี่ยมอีกด้วย ซึ่งบางอันไม่เป็นที่รู้จักและเกือบจะเป็นอัญมณีที่ซ่อนอยู่ วันนี้ Michael Kohl นักเขียนรับเชิญไฮไลท์รายการโปรด:Stringscanner
อัญมณีที่ซ่อนอยู่ของ Ruby:StringScanner
สามารถทำได้ไกลโดยไม่ต้องติดตั้ง gem ของบุคคลที่สาม ตั้งแต่โครงสร้างข้อมูล เช่น OpenStruct และ Set over CSV parsing ไปจนถึงการเปรียบเทียบ อย่างไรก็ตาม มีไลบรารี่ที่ไม่ค่อยมีใครรู้จักในการติดตั้งมาตรฐานของ Ruby ซึ่งมีประโยชน์มาก ซึ่งหนึ่งในนั้นคือ StringScanner
ซึ่งตามเอกสาร "ให้การดำเนินการสแกนคำศัพท์บนสตริง" .
การสแกนและการแยกวิเคราะห์
ดังนั้น "การสแกนคำศัพท์" หมายถึงอะไรกันแน่? โดยพื้นฐานแล้วจะอธิบายกระบวนการรับสตริงอินพุตและดึงข้อมูลที่มีความหมายออกมาตามกฎบางอย่าง ตัวอย่างเช่น สามารถเห็นได้ในขั้นตอนแรกของคอมไพเลอร์ซึ่งใช้นิพจน์เช่น 2 + 1
เป็นอินพุตและเปลี่ยนเป็นลำดับโทเค็นต่อไปนี้:
[{ number: "1" }, {operator: "+"}, { number: "1"}]
โดยปกติแล้ว เครื่องสแกนคำศัพท์มักจะใช้เป็นออโตมาตาที่มีสถานะจำกัด และมีเครื่องมือที่รู้จักกันดีหลายอย่างที่สามารถสร้างมันขึ้นมาให้เราได้ (เช่น ANTLR หรือ Ragel)
อย่างไรก็ตาม บางครั้งความต้องการการแยกวิเคราะห์ของเราไม่ได้ซับซ้อนขนาดนั้น และไลบรารีที่ง่ายกว่า เช่น StringScanner
ที่ใช้นิพจน์ทั่วไป มีประโยชน์มากในสถานการณ์เช่นนี้ ทำงานโดยจำตำแหน่งของสิ่งที่เรียกว่า ตัวชี้สแกน ซึ่งไม่มีอะไรมากไปกว่าดัชนีในสตริง กระบวนการสแกนจะพยายามจับคู่รหัสหลังจากตัวชี้การสแกนกับนิพจน์ที่ให้ไว้ นอกเหนือจากการดำเนินการจับคู่ StringScanner
ยังมีวิธีการย้ายตัวชี้สแกน (เลื่อนไปข้างหน้าหรือข้างหลังผ่านสตริง) มองไปข้างหน้า (ดูว่ามีอะไรต่อไปโดยไม่ได้แก้ไขตัวชี้การสแกน) รวมทั้งการค้นหาว่าตอนนี้เราอยู่ที่ไหนในสตริง (เป็นจุดเริ่มต้นหรือ ท้ายบรรทัด/ทั้งสตริง ฯลฯ)
การแยกวิเคราะห์บันทึกของ Rails
ทฤษฎีพอแล้ว มาดู StringScanner
ในการดำเนินการ ตัวอย่างต่อไปนี้จะใช้รายการบันทึกของ Rails ดังตัวอย่างด้านล่าง
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
และแยกวิเคราะห์เป็นแฮชต่อไปนี้:
{
method: "GET",
path: "/"
ip: "127.0.0.1",
timestamp: "2017-08-20 20:53:10 +0900",
success: true,
response_code: "200",
duration: "79ms",
}
หมายเหตุ:ขณะนี้เป็นตัวอย่างที่ดีสำหรับ StringScanner
แอปพลิเคชันจริงจะดีกว่าถ้าใช้ Lograge และตัวจัดรูปแบบบันทึก JSON
เพื่อใช้งาน StringScanner
เราต้องขอมันก่อน:
require 'strscan'
หลังจากนี้ เราสามารถเริ่มต้นอินสแตนซ์ใหม่โดยส่งรายการบันทึกเป็นอาร์กิวเมนต์ไปยังตัวสร้าง ในขณะเดียวกัน เราจะกำหนดแฮชที่ว่างเปล่าเพื่อเก็บผลลัพธ์ของความพยายามแยกวิเคราะห์:
scanner = StringScanner.new(log_entry)
log = {}
ตอนนี้เราสามารถใช้วิธี pos ของสแกนเนอร์เพื่อรับตำแหน่งปัจจุบันของตัวชี้การสแกนของเรา ตามคาด ผลลัพธ์คือ 0
, อักขระตัวแรกของสตริง:
scanner.pos #=> 0
ลองนึกภาพสิ่งนี้เพื่อให้กระบวนการทำได้ง่ายขึ้น:
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)
สำหรับการพิจารณาสถานะของสแกนเนอร์เพิ่มเติม เราสามารถใช้ beginning_of_line?
และ eos?
เพื่อยืนยันว่าขณะนี้ตัวชี้การสแกนอยู่ที่จุดเริ่มต้นของบรรทัด และเรายังไม่ได้ใช้ข้อมูลของเราจนหมด:
scanner.beginning_of_line? #=> true
scanner.eos? #=> false
ข้อมูลบิตแรกที่เราต้องการแยกคือวิธีการขอ HTTP ซึ่งสามารถพบได้หลังจากคำว่า "เริ่มต้น" ตามด้วยช่องว่าง เราสามารถใช้วิธีข้ามชื่อที่เหมาะสมของสแกนเนอร์เพื่อเลื่อนตัวชี้การสแกนซึ่งจะส่งคืนจำนวนอักขระที่ถูกละเว้น ซึ่งในกรณีของเราคือ 8 นอกจากนี้ เราสามารถใช้การจับคู่? เพื่อยืนยันว่าทุกอย่างทำงานตามที่คาดไว้:
scanner.skip(/Started /) #=> 8
scanner.matched? #=> true
ตัวชี้การสแกนอยู่ก่อนวิธีการขอ:
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)
ตอนนี้เราสามารถใช้ scan_until เพื่อดึงค่าจริง ซึ่งคืนค่าการจับคู่นิพจน์ทั่วไปทั้งหมด เนื่องจากวิธีการร้องขอเป็นตัวพิมพ์ใหญ่ทั้งหมด เราจึงสามารถใช้คลาสอักขระธรรมดาและ +
โอเปอเรเตอร์ที่ตรงกับหนึ่งหรืออักขระ:
log[:method] = scanner.scan_until(/[A-Z]+/) #=> "GET"
หลังจากการดำเนินการนี้ ตัวชี้การสแกนจะอยู่ที่ "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)
ในการแยกเส้นทางที่ร้องขอ เราจะต้องข้ามหนึ่งช่องว่างแล้วแยกทุกอย่างที่อยู่ในเครื่องหมายคำพูดคู่ มีหลายวิธีในการบรรลุเป้าหมาย หนึ่งในนั้นคือผ่านแคปเจอร์กรุ๊ป (ส่วนของนิพจน์ทั่วไปรวมอยู่ในวงเล็บ เช่น (.+)
) ซึ่งตรงกับอักขระใดๆ อย่างน้อยหนึ่งตัว:
scanner.scan(/\s"(.+)"/) #=> " \"/\""
อย่างไรก็ตาม เราจะไม่ใช้ค่าส่งคืนของ scan
นี้ ดำเนินการโดยตรง แต่ใช้การจับภาพเพื่อรับค่าของแคปเจอร์กรุ๊ปแรกแทน:
log[:path] = scanner.captures.first #=> "/"
เราแยกเส้นทางสำเร็จแล้วและตัวชี้การสแกนอยู่ที่เครื่องหมายคำพูดคู่ปิด:
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)
เพื่อแยกวิเคราะห์ที่อยู่ IP จากบันทึก เราใช้ skip
. อีกครั้ง เพื่อละเว้นสตริง "for" ที่ล้อมรอบด้วยช่องว่างแล้วใช้ scan_until
เพื่อจับคู่อักขระที่ไม่ใช่ช่องว่าง (\s
เป็นคลาสอักขระที่แสดงช่องว่างและ [^\s]
เป็นการปฏิเสธ):
scanner.skip(/ for /) #=> 5
log[:ip] = scanner.scan_until(/[^\s]+/) #=> "127.0.0.1"
คุณบอกได้ไหมว่าตัวชี้การสแกนจะอยู่ที่ไหนในตอนนี้ คิดสักครู่แล้วเปรียบเทียบคำตอบกับวิธีแก้ปัญหา:
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)
การแยกวิเคราะห์การประทับเวลาน่าจะคุ้นเคยกันดีอยู่แล้ว ขั้นแรกเราใช้ skip
. เก่าที่เชื่อถือได้ เพื่อละเว้นสตริงตัวอักษร " at "
แล้วใช้ scan_until
เพื่ออ่านจนจบบรรทัดปัจจุบัน ซึ่งแสดงโดย $
ในนิพจน์ทั่วไป:
scanner.skip(/ at /) #=> 4
log[:timestamp] = scanner.scan_until(/$/) #=> "2017-08-20 20:53:10 +0900"
ข้อมูลต่อไปที่เราสนใจคือรหัสสถานะ HTTP ในบรรทัดสุดท้าย ดังนั้นเราจะใช้ skip_until เพื่อนำเราไปจนสุดที่ช่องว่างหลังคำว่า "เสร็จสิ้น"
scanner.skip_until(/Completed /) #=> 296
ตามชื่อของมัน มันทำงานคล้ายกับ scan_until
แต่แทนที่จะส่งคืนสตริงที่ตรงกัน จะส่งคืนจำนวนอักขระที่ข้ามไป ซึ่งจะทำให้ตัวชี้การสแกนอยู่ด้านหน้ารหัสสถานะ HTTP ที่เราสนใจ
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)
^
ก่อนที่เราจะสแกนโค้ดตอบกลับ HTTP จริง จะดีกว่าไหมถ้าเราสามารถบอกได้ว่าโค้ดตอบกลับ HTTP แสดงถึงความสำเร็จ (เพื่อเป็นตัวอย่างนี้ โค้ดใดๆ ในช่วง 2xx) หรือความล้มเหลว (ช่วงอื่นๆ ทั้งหมด) เพื่อให้บรรลุสิ่งนี้ เราจะใช้การแอบดูเพื่อดูอักขระตัวถัดไป โดยไม่ต้องย้ายตัวชี้การสแกน
log[:success] = scanner.peek(1) == "2" #=> true
ตอนนี้เราสามารถใช้ scan เพื่ออ่านอักขระสามตัวถัดไป ซึ่งแสดงโดยนิพจน์ทั่วไป /\d{3}/
:
log[:response_code] = scanner.scan(/\d{3}/) #=> "200"
อีกครั้งที่ตัวชี้การสแกนจะอยู่ที่ส่วนท้ายของนิพจน์ทั่วไปที่ตรงกันก่อนหน้านี้:
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)
^
ข้อมูลส่วนสุดท้ายที่เราต้องการดึงออกจากรายการบันทึกคือเวลาดำเนินการในหน่วยมิลลิวินาที ซึ่งทำได้โดย skip
ping เหนือสตริง " OK in "
แล้วอ่านทุกอย่างจนถึงสตริงตามตัวอักษร "ms"
.
scanner.skip(/ OK in /) #=> 7
log[:duration] = scanner.scan_until(/ms/) #=> "79ms"
และด้วยบิตสุดท้ายในนั้น เรามีแฮชที่เราต้องการ
{
method: "GET",
path: "/"
ip: "127.0.0.1",
timestamp: "2017-08-20 20:53:10 +0900",
success: true,
response_code: "200",
duration: "79ms",
}
สรุป
StringScanner
ของ Ruby ใช้พื้นที่ตรงกลางที่ดีระหว่างนิพจน์ทั่วไปอย่างง่ายกับ lexer ที่สมบูรณ์ ไม่ใช่ตัวเลือกที่ดีที่สุดสำหรับการสแกนและการแยกวิเคราะห์ที่ซับซ้อน แต่ลักษณะตรงไปตรงมาทำให้ทุกคนที่มีความรู้พื้นฐานเกี่ยวกับนิพจน์ทั่วไปสามารถดึงข้อมูลจากสตริงอินพุตได้ง่าย และฉันเคยใช้สิ่งเหล่านี้สำเร็จในโค้ดการผลิตในอดีต เราหวังว่าคุณจะค้นพบอัญมณีที่ซ่อนอยู่นี้
PS:แจ้งให้เราทราบว่าคุณคิดว่าเป็นอัญมณีที่ซ่อนอยู่ที่เราควรจะเน้นต่อไป!