12/08/2018, 13:15
Ba bước để khắc phục vấn đề về encoding trong Ruby
Mở đầu Với Ruby (hay là với bất kỳ một ngôn ngữ nào khác) thì bạn cũng sẽ rất hay làm việc với chuỗi. Máy tính thì chỉ hiểu được chuỗi thông qua các byte của chuỗi đó sau khi mã hóa Hiện tại thì có rất nhiều chuẩn được dùng để mã hóa các ký tự có trong chuỗi. Cho nên đôi khi bạn sẽ gặp vấn đề ...
Mở đầu
Với Ruby (hay là với bất kỳ một ngôn ngữ nào khác) thì bạn cũng sẽ rất hay làm việc với chuỗi. Máy tính thì chỉ hiểu được chuỗi thông qua các byte của chuỗi đó sau khi mã hóa
Hiện tại thì có rất nhiều chuẩn được dùng để mã hóa các ký tự có trong chuỗi. Cho nên đôi khi bạn sẽ gặp vấn đề với các chuẩn mã hóa mà bạn đang sử dụng. Đó là vấn đề về encoding
Sau đây mình xin trình bày bài viết 3 Steps to Fix Encoding Problems in Ruby của tác giả Justin Weiss và một ví dụ nhỏ của mình để mọi người hiểu thêm vấn đề bày
Phần 1 : Dịch bài viết
Bạn chỉ thực sự nghĩ về encoding của một chuỗi khi mà đã có vấn đề xảy ra. Khi bạn kiểm tra theo dõi ngoại lệ của bạn và thấy những dòng như bên dưới
Encoding::InvalidByteSequenceError: "xFE" on UTF-8
Do đó, với một encoding không tốt thì làm thế nào để bạn có thể tìm ra vấn đề và cách khắc phục nó?
Thế nào là encoding ?
Nếu bạn có thể hình dung ra được những gì mà encoding đã làm với chuỗi của bạn thì những vấn đề này sẽ dễ dàng để khắc phục hơn
Bạn có thể coi string như là một mảng của các byte hoặc là các số nhỏ
irb(main):001:0> "hello!".bytes => [104, 101, 108, 108, 111, 33]
Trong encoding này thì 104 tương đương với h,..., 33 tương ứng với !
Và như vậy, nó sẽ trở nên phức tạp hơn khi bạn sử dụng những ký tự ít phổ biến trong tiếng Anh
irb(main):002:0> "hellṏ!".bytes => [104, 101, 108, 108, 225, 185, 143, 33]
Trong ví dụ trên thì thật khó để biết được số nào sẽ tương ứng với ký tự nào. Thay vì một byte, ṏ sẽ tương ứng với tập hợp nhiều byte [225, 185, 143]. Nhưng vẫn có mối quan hệ giữa các byte và các ký tự. Và encoding của một chuỗi sẽ đi định nghĩa quan hệ đó
Hãy thử xem tập các byte đơn với 2 encoding (ở đây tác giả đã kiểm tra với encoding ISO-8859-1 và ISO-8859-5) khác nhau như thế nào
# 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]
Tập hợp các byte ở 2 encoding là không có gì thay đổi. Tuy nhiên không phải tất cả đều thực sự giống nhau. Thay đổi encoding đã thay đổi chuỗi được in ra mà các byte thì vẫn như nhau
Và cũng không phải là tất cả các chuỗi đề có thể biểu diễn được với tất cả các encoding
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>'
Hầu hết các encoding đều có giới hạn và không thể xử lý được với mọi ký tự. Bạn có thể sẽ thấy lỗi của 1 ký tự trong encoding này nhưng lại không có trong encoding khác, hay là Ruby không thể tìm ra cách dịch một ký tự từ encoding này sang encoding khác
Bạn có thể làm việc xung quanh lỗi này nếu bạn truyền các tùy chọn bổ sung vào encode
irb(main):064:0> "hi∑".encode("Windows-1252", invalid: :replace, undef: :replace) => "hi?"
Tùy chọn invalid và undef ở đây có nghĩa là sẽ thay thế các ký tự không thể dịch với 1 ký tự khác. Mặc định thì ký tự thay thế là ? (nếu như là Unicode thì đó là ký tự �)
Thật không may, khi bạn thay thế các ký tự với encode, bạn có thể sẽ đánh mất một số thông tin. Bạn không thể làm gì với những byte đã bị thay thế bởi ?. Nhưng nếu bạn muốn dữ liệu sẽ có trong encoding mới thì việc đánh mất dữ liệu vẫn còn hơn là bị phá vỡ
Đến đây, bạn có thể thấy 3 method chính về chuỗi để giúp bạn hiểu được encoding như sau
-
encode : dịch một chuỗi sang encoding khác (chuyển đổi những ký tự sang những ký tự tương đương với nó nhưng ở trong một encoding mới).
-
bytes : sẽ cho các ban thấy rằng các byte tạo nên một chuỗi.
-
force_encoding : cho bạn thấy những byte này sẽ được giải mã như thế nào bởi một encoding khác.
Điểm khác biệt cơ bản giữa encode và force_encoding là encode có thể thay đổi byte còn force_encoding thì không
3 bước để xử lý lỗi encoding
Bạn có thể xử lý hầu hết các vấn đề về encoding với 3 bước sau
1. Khám phá ra encoding thực sự của một chuỗi
Điều này nghe có vẻ đơn giản. Nhưng chỉ vì 1 chuỗi nói là nó thuộc về encoding nào đó thì cũng chưa chắc đó đã là encoding thực sự của chuỗi
irb(main):078:0> "hix99!".encoding => #<Encoding:UTF-8>
Nếu nói chuỗi trên thuộc UTF-8 là không chính xác, vì có cả ký tự lạ trong đó. Vậy, làm thế nào để bạn tìm ra được encoding thực sự của 1 chuỗi?
Rất nhiều các phần mềm cũ sẽ mặc định quy về 1 encoding duy nhất, bạn có thể tìm hiểu xem đầu vào của chuỗi là từ đâu. Ví dụ, nếu một ai đó đã gián chuỗi từ Word thì nó có thể là Windows-1252. Hoặc nếu nó đến từ 1 file hoặc là được kéo về từ một website cũ hơn thì nó có lẽ là ISO-8859-1
Tôi thấy cũng hữu ích để tìm các bảng encoding, giống như là một trong những trang Wikipedia được liên kết. Trong các bảng đó, bạn có thể thấy các ký tự được tham chiếu bởi các con số, và có thể xem nếu như nó có ý nghĩa
Trong ví dụ trên, biểu đồ Windows-1252 cho thấy byte 99 tương ứng với ký tự ™. Byte 99 lại không tồn tại trong ISO-8859-1. Nếu ™ có ý nghĩa ở đây, bạn có thể giả định rằng đầu vào trong Windows-1252 và đưa sang. Nếu không thì bạn có thể giữ cho đến khi bạn tìm ra một ký tự hợp lý hơn
2. Quyết định encoding mà bạn muốn cho chuỗi của bạn
Thật dễ dàng, trừ khi bạn có một lý do thực mạnh mẽ, bạn muốn chuỗi của bạn là UTF-8
Có một encoding thông dụng khác mà bạn có thể sử dụng trong Ruby là ASCII-8BIT. Trong ASCII-8BIT, mỗi ký tự sẽ được biểu diễn bởi một byte duy nhất. Điều đó tức là bạn luôn có str.chars.length == str.bytes.length. Do vậy, nếu bạn muốn kiểm soát triệt để các byte cụ thể trong chuỗi thì ASCII-8BIT sẽ là lựa chọn tốt cho bạn
3. re-encode chuỗi của bạn từ encoding trong bước 1 đến encoding trong bước 2
Bạn có thể thực hiện với hàm encode. Trong ví dụ dưới đây, encoding chuỗi của chúng ta đang là Windows-1252 và chúng ta muốn đưa nó trở thành UTF-8
irb(main):088:0> "hix99!".encode("UTF-8", "Windows-1252") => "hi™!"
Chuỗi trên trong có vẻ đã dễ đọc hơn nhiều
Hãy mở một giao diện điều khiển irb và thực hành với các phương thức encode, bytes và force_encoding. Xem hàm encode làm hay đổi các byte tạo nên chuỗi như thế nào
Xem sự khác nhau giữa các encoding, và khi bạn đã quen các encoding với các bước nêu trên, bạn có thể khắc phục vấn đề trong ít phút
Phần 2 : Ví dụ
Để giúp các bạn hiểu hơn về bài viết, mình xin trình bày một ví dụ. Có rất nhiều ký tự sẽ gây ra vấn đề cho encoding. Ở đây mình xin trình bày cách để đưa chuỗi they’re về they’re
Trước tiên ta có thể kiểm tra
irb(main):001:0> "they’re".bytes => [116, 104, 101, 121, 226, 128, 153, 114, 101]
Ta có str.chars.length = 7 nhưng str.bytes.length = 9. Hãy kiểm tra ký tự ’
irb(main):002:0> "’".bytes => [226, 128, 153]
Chuỗi trên có ký tự ’ được biểu diễn bởi 3 bytes và các ký tự còn lại mỗi ký tự tương ứng với 1 byte. Nếu bạn tìm trong các encoding thông dụng thì có thể thấy đây là Windows-1252
irb(main):003:0> "they’re".force_encoding("Windows-1252").encode("UTF-8") => "they’re"
Như vậy là đã có sự khác biệt giữa 2 encoding
Vậy vấn đề ở đây là
1. Chúng ta có một chuỗi UTF-8 là they’re
2. Được chuyển đồi từ 1 chuỗi Windows-1252 (they’re)
3. Các byte cần được đọc như là UTF-8 (they’re)
Chúng ta cần
1. Sử dụng encode để chuyển đổi chuỗi UTF-8 sang chuỗi Windows-1252
2. Sau đó, sử dụng force_encoding để ép những ký tự lạ trong chuỗi Windows-1252 để đọc ra như là UTF-8
irb(main):003:0> "they’re".force_encoding("Windows-1252").encode("UTF-8") => "they’re" irb(main):006:0> "they’re".encode("Windows-1252").force_encoding("UTF-8") => "they’re"
Vấn đề đã được giải quyết
Tham khảo
-
3 Steps to Fix Encoding Problems in Ruby