Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

อัญมณีที่ซ่อนอยู่ของทับทิม StringScanner

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:แจ้งให้เราทราบว่าคุณคิดว่าเป็นอัญมณีที่ซ่อนอยู่ที่เราควรจะเน้นต่อไป!