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

เชี่ยวชาญ Unicode Normalization ใน Ruby: หลีกเลี่ยงพฤติกรรมสตริงที่ไม่คาดคิด

ผมเผยแพร่บทความทดสอบเมธอดสตริงของ Ruby กับอักขระ Unicode บางตัว เพื่อดูว่าพฤติกรรมแปลกหรือไม่ หลายเมธอดทำงานผิดปกติ

ข้อติชมบางอย่างว่าผมใช้สตริงที่ยังไม่ normalized ผมเองก็งงเรื่อง Unicode normalization เหมือนกัน สันนิษฐานว่า Rubyists หลายคนก็เช่นกัน

ด้วย normalization สามารถแปลงสตริง Unicode หลายตัวที่พฤติกรรมแปลก ให้ทำงานร่วมกับเมธอดสตริงของ Ruby ได้ดีขึ้น แต่:

  1. การแปลงไม่สมบูรณ์เสมอไป บางลำดับ Unicode จะทำให้เมธอดสตริงของ Ruby ทำงานผิดตลอด
  2. ต้องทำเอง ไม่ว่า Ruby, Rails หรือ DB จะ normalize ให้อัตโนมัติโดย default

บทความนี้เป็นบทนำสั้นเกี่ยวกับ Unicode normalization ใน Ruby หวังให้เป็นจุดเริ่มต้นสำหรับการสำรวจต่อ

มาลอง normalize สตริงกัน

เมธอด String#unicode_normalize เพิ่มใน Ruby 2.2 เขียนด้วย Ruby เอง จึงไม่เร็วเท่าไลบรารี normalization อย่าง utf8_proc และ unicode gems ที่ใช้ C

เหตุที่ต้อง normalize: ใน Unicode มีหลายวิธีเขียนอักขระเดียวกัน ตัวอักษร "Å" แทนได้ด้วย code point "\u00c5" หรือการรวมตัวอักษร "A" และ accent: "A\u030A"

เชี่ยวชาญ Unicode Normalization ใน Ruby: หลีกเลี่ยงพฤติกรรมสตริงที่ไม่คาดคิด

Normalization แปลงรูปแบบหนึ่งเป็นอีกแบบ:

"A\u030A".unicode_normalize #=> 'Å' (เหมือน "\u00C5")

แต่ไม่ได้มีแค่หนึ่งวิธี normalize นั่นจะง่ายเกินไป! มี 4 วิธี เรียกว่า "normalization forms" ย่อหน้าอักษรอาณัติ: NFD, NFC, NFKD, NFKC

String#unicode_normalize ใช้ NFC โดย default แต่ระบุ form อื่นได้:

"a\u0300".unicode_normalize(:nfkc) #=> 'à' (เหมือน "\u00E0")

แต่ความจริง 4 forms ทำอะไร? มาดูกัน

Normalization Forms

มี 2 ประเภทของการ normalize:

  • Composition (การรวม): แปลงอักขระหลาย code points เป็น single code point เช่น "a\u0300" กลายเป็น "\u00E0" ทั้งคู่ encode อักขระ à
  • Decomposition (การแยก): ทำตรงข้าม composition แปลง single-code-point เป็น multiple code points เช่น "\u00E0" กลายเป็น "a\u0300"

Composition และ decomposition แต่ละอย่างทำได้ 2 แบบ:

  • Canonical: รักษา glyphs ตัวอย่าง: "2⁵" คงเป็น "2⁵" แม้บางระบบไม่รองรับ superscript-five
  • Compatibility: แทนที่ glyphs ด้วย compatibility characters เช่น "2⁵" จะกลายเป็น "2 5"

สอง operation และสอง option รวมกันเป็น 4 "normalization forms" ตารางด้านล่างรายการทั้งหมด พร้อมคำอธิบายและตัวอย่าง input/output:

Name Description Input Output
NFD Canonical Decomposition Å "\u00c5" Å "A\u030A"
NFC Canonical Decomposition Followed by Canonical Composition Å "A\u030A" Å "\u00c5"
NFKD Compatibility Decomposition ẛ̣ "\u1e9b\u0323" ṩ "\u0073\u0323\u0307"
NFKC Compatibility Decomposition Followed by Canonical Composition ẛ̣ "\u1e9b\u0323" ṩ "\u1e69"

ดูตารางนานๆ จะเห็น acronyms มีความหมาย:

  • "NF" = "normalization form"
  • "D" = "decomposition"
  • "C" = "composition"
  • "K" = "kompatibility" :)

รายละเอียดเพิ่มเติมดูที่ Unicode Standard Annex #15

เลือก normalization form

form ที่ควรใช้ขึ้นกับงาน ผมแนะนำตาม Unicode Normalization FAQ

ใช้ NFC สำหรับความเข้ากันได้ของสตริง

ถ้าต้องการให้เมธอดสตริงของ Ruby ทำงานกับ Unicode ส่วนใหญ่ ส่วนใหญ่ควรใช้ NFC มันเป็น default ของ String#unicode_normalize ไม่ใช่เรื่องบังเอิญ

  • รวม multi-code-point characters เป็น single code points ได้เท่าที่ทำได้ Multi-code-point characters เป็นต้นเหตุหลักของปัญหาเมธอดสตริง
  • ไม่เปลี่ยน glyphs ผู้ใช้ปลายทางไม่เห็นการเปลี่ยนแปลงข้อความที่พิมพ์

แต่ไม่ใช่ multi-code-point characters ทุกตัวรวมเป็น single code point ได้ ในกรณีเหล่านั้น เมธอดสตริงของ Ruby จะทำงานแย่:

s = "\u01B5\u0327\u0308" # => "Ƶ̧̈", อักขระที่รวมไม่ได้
s.unicode_normalize(:nfc).size # => 3, แม้มีแค่อักขระเดียว

ใช้ NFKC สำหรับความปลอดภัยและความเข้ากันได้กับ DB

หากทำงานกับข้อความด้านความปลอดภัย เช่น usernames หรือสนใจให้ข้อความเข้ากันได้กับ database NFKC น่าจะดี

  • แปลงอักขระที่อาจก่อปัญหาเป็น compatibility characters
  • จากนั้นรวมอักขระทั้งหมดเป็น single code points

ทำไมดีต่อความปลอดภัย: ทưởng像 user ชื่อ "HenryIV" ผู้บุกรุกอาจสมัคร username "HenryⅣ" ดูเหมือนกันแตกต่างจริง ตัวแรกใช้ ASCII "IV" ตัวหลังใช้ Unicode Roman numeral 4: "Ⅳ"

ป้องกันได้โดย normalize ด้วย NFKC ก่อนตรวจสอบ uniqueness NFKC แปลง "\u2163" เป็น ASCII "IV"

a = "Henry\u2163"
b = "HenryIV"
a.unicode_normalize(:nfc) == b.unicode_normalize(:nfc) # => false เพราะ NFC รักษา glyphs
a.unicode_normalize(:nfkc) == b.unicode_normalize(:nfkc) # => true เพราะ NFKC แปลงทั้งคู่เป็น ASCII "IV"

คำส่งท้าย

หลังศึกษา ผมน่าประหลาดใจว่า Unicode normalization ไม่ใช่หัวข้อบูรณะในชุมชน Ruby/Rails คาดว่า Rails ทำให้ แต่ดูเหมือนไม่ได้ และไม่ normalize ข้อมูลจากผู้ใช้ ทำให้เมธอดสตริงของ Ruby ไม่เชื่อถือได้

หากอ่านคิดว่ามีข้อมูลผมไม่รู้ ติดต่อได้ที่ twitter @StarrHorne หรือ email starr@honeybadger.io Unicode กว้างมาก ผมพิสูจน์แล้วว่าไม่รู้หมด :)