07/09/2018, 16:54

Encoding Problem In Ruby

Bạn chỉ nghĩ về encoding khi bạn gặp vấn đề với nó, khi bạn check log và nhìn thấy Encoding:: InvalidByteSequenceError: "xFE" on UTF- 8 đập vào mặt mình. Hay khì "they're" hiện thị thành “they’re”. Chỉ khi đó bạn mới tự hỏi encoding là gì vậy và làm sao để fix lỗi đây ?! ...

Bạn chỉ nghĩ về encoding khi bạn gặp vấn đề với nó, khi bạn check log và nhìn thấy

Encoding::InvalidByteSequenceError: "xFE" on UTF-8

đập vào mặt mình. Hay khì "they're" hiện thị thành “they’re”. Chỉ khi đó bạn mới tự hỏi encoding là gì vậy và làm sao để fix lỗi đây ?!

Nếu bạn có thể hình dung encoding(bộ mã hóa) làm gì với một string, những lỗi trên sẽ dễ dàng được fix.
Hãy nghĩa về string như một mảng các byte, hoặc các số:

irb(main):001:0> "hello!".bytes
=> [104, 101, 108, 108, 111, 33]

Trong mã hóa, 104 tương đương với "h", 33 là "!". Tuy nhiên, vấn đề trở nên phức tạp nếu trong string chứa các ký tự ít phổ biến hơn trong tiếng Anh.

irb(main):002:0> "hellṏ!".bytes
=> [104, 101, 108, 108, 225, 185, 143, 33]

Thật khó để có thể chỉ ra số nào đại diện cho ký tự nào, thay vì một byte, ṏ lại chiếm một nhóm byte [225, 185, 143]. Nhưng giữa byte và ký tự vẫn tồn tại một mối quan hệ được định nghĩa bởi bộ encoding.
Hãy cùng xem ví dụ sau khi ta thử mã hóa một string bằng các encoding khách nhau:

# Try an ISO-8859-1 string with a special character!
irb(main):003:0> str = "hellÔ!".encode("ISO-8859-1"); str.encode("UTF-8")
=> "hellÔ!"

irb(main):004:0> str.bytes
=> [104, 101, 108, 108, 212, 33]

# What would that string look like interpreted as ISO-8859-5 instead?
irb(main):005:0> str.force_encoding("ISO-8859-5"); str.encode("UTF-8")
=> "hellд!"

irb(main):006:0> str.bytes
=> [104, 101, 108, 108, 212, 33]

Các byte đại diện cho string không hề thay đổi trong khi string được in ra khác nhau. Do đó các encoding không hề thay đổi "bản chất" của một string mà chỉ thay đổi "bề ngoài" của chúng mà thôi.
Và không phải mọi string đều có thể được diễn tả bởi bất kỳ encoding nào

irb(main):006:0> "hi∑".encode("Windows-1252")
Encoding::UndefinedConversionError: U+2211 to WINDOWS-1252 in conversion from UTF-8 to WINDOWS-1252
 from (irb):61:in `encode'
 from (irb):61
 from /usr/local/bin/irb:11:in `<main>'

Phần lớn các encoding đều nhỏ và không thể xử lý hết toàn bộ các ký tự đặc biệt được. Chúng ta sẽ thấy lỗi khi một ký tự nằm trong bộ encoding này nhưng không tồn tại ở bộ khác, hoặc khi Ruby không thể tìm ra cách để "dịch" một ký tự nằm giữa 2 bộ encoding.
Trong trường hợp này, cách xử lý là tạm thời ngó lơ những ký tự này đi bằng cách thêm một số option vào hàm encode:

irb(main):064:0> "hi∑".encode("Windows-1252", invalid: :replace, undef: :replace)
=> "hi?"

Hai option invalid và undef sẽ thay thế toàn bộ những ký tự không dịch được bởi một ký tự khác, mặc định sẽ là "?"(trong Unicode là �).
Không may, việc này đồng nghĩa với việc ta sẽ mất thông tin cũng như không thể biết byte đã bị thay thế bơi "?". Tuy nhiên so sánh với việc mọi thứ đổ bể và system crash thì lựa chọn nỗi đau này vẫn còn nhẹ nhàng hơn.

Hiện tại, để hiểu rõ về encoding chúng ta chỉ cần tập trung vào 3 method chính sau:

  • encode, chuyển một string sang một encoding mới (convert các ký tự sang các ký tự tương ứng trong bộ encoding mới)

  • bytes, hiển thị những byte tạo nên string

  • force_encoding, thay đổi encoding của một string nhưng không convert các ký tự

    irb(main):060:0> str = str.force_encoding("UTF-8")
    irb(main):061:0> str.encoding
    => #<Encoding:UTF-8> 
    

Có một cách cơ bản sau để xử lý vấn đề với encoding:

  1. Tìm ra đúng bộ encoding của string

    irb(main):078:0> "hix99!".encoding
    => #<Encoding:UTF-8>
    

    Nghe có vẻ dễ nhưng hãy nhìn ví dụ trên, nếu string trên thực sự là UTF-8, nó sẽ ko có kiểu {number} như vậy. Vậy làm sao để tìm ra đúng encoding cho string bạn đang có ? Cách tốt nhất là lục lọi ở danh sách các encoding trên Internet và xem có ký tự nào phụ hợp cho chuỗi số kia không.
    Ví dụ, trong Window-1252, byte 99 đại diện cho ký tự “™” và nó lại không tồn tại trong ISO-8859. Do đó một giả thuyết hợp lý rằng input nằm trong Window-1252. Mặt khác, có thể tiếp tục tìm kiếm cho đến khi gặp được ký tự matching hợp lý hơn.

  2. Quyết định encoding sẽ mã hóa string
    Hiểu một cách đơn giản, nếu không có lý do gì thực sự đặc biệt, hãy sử dụng UTF-8. Một số trường hợp hiếm gặp, có thể sẽ phải dùng tới ASCII-8BIT trong Ruby. Với bộ mã này, mỗi ký tự được đại diện bởi duy nhất một byte, kể cả những ký tự đặc biệt đi nữa. Do đó, nó sẽ là lựa chọn tốt khi bạn muốn xử lý từng byte một trong string của mình.

  3. Re-encode cho đến khi nào thỏa mãn

    irb(main):088:0> "hix99!".encode("UTF-8", "Windows-1252")
    => "hi™!"
    

    Trong ví dụ này, string đang ở Window-1252 nhưng tôi thích nó thành UTF-8 và chúng ta có thể dễ dàng thực hiện điều đó trong encode

  • http://graysoftinc.com/character-encodings/ruby-19s-string
  • http://ruby-doc.org/core-1.9.3/String.html#method-i-encode
  • http://www.justinweiss.com/articles/3-steps-to-fix-encoding-problems-in-ruby/
0