Computer >> Máy Tính >  >> Lập trình >> Ruby

Giới thiệu về Rubyists về Mã hóa ký tự, Unicode và UTF-8

Rất có thể bạn đã thấy một ngoại lệ Ruby như UndefinedConversionError hoặc IncompatibleCharacterEncodings . Ít có khả năng bạn hiểu ngoại lệ nghĩa là gì. Bài viết này sẽ giúp ích. Bạn sẽ học cách mã hóa ký tự hoạt động và cách chúng được triển khai trong Ruby. Cuối cùng, bạn sẽ có thể hiểu và sửa những lỗi này dễ dàng hơn nhiều.

Vậy "mã hóa ký tự" là gì?

Trong mọi ngôn ngữ lập trình, bạn làm việc với chuỗi. Đôi khi bạn xử lý chúng dưới dạng đầu vào, đôi khi bạn hiển thị chúng dưới dạng đầu ra. Nhưng máy tính của bạn không hiểu "chuỗi". Nó chỉ hiểu các bit:1s và 0s. Quá trình chuyển đổi chuỗi thành bit được gọi là mã hóa ký tự.

Nhưng mã hóa ký tự không chỉ thuộc về thời đại của máy tính. Chúng ta có thể học từ một quy trình đơn giản hơn trước khi có máy tính:Mã Morse.

Mã Morse

Mã Morse rất đơn giản trong định nghĩa của nó. Bạn có hai biểu tượng hoặc cách để tạo ra một tín hiệu (ngắn và dài). Với hai ký hiệu đó, bạn đại diện cho một bảng chữ cái tiếng Anh đơn giản. Ví dụ:

  • A là .- (một dấu ngắn và một dấu dài)
  • E là. (một dấu ngắn)
  • O là --- (ba dấu dài)

Hệ thống này được phát minh vào khoảng năm 1837 và nó cho phép, chỉ với hai ký hiệu hoặc tín hiệu, toàn bộ bảng chữ cái được mã hóa.

Bạn có thể chơi với một người dịch trực tuyến tại đây.

Giới thiệu về Rubyists về Mã hóa ký tự, Unicode và UTF-8

Trong hình ảnh, bạn có thể thấy một "encoder", một người chịu trách nhiệm mã hóa và giải mã các thông điệp. Điều này sẽ sớm thay đổi khi có sự xuất hiện của máy tính.

Từ mã hóa thủ công sang tự động

Để mã hóa một tin nhắn, bạn cần một người dịch thủ công các ký tự thành các ký hiệu theo thuật toán của mã Morse.

Tương tự như mã Morse, máy tính chỉ sử dụng hai "ký hiệu":1 và 0. Bạn chỉ có thể lưu trữ một chuỗi ký tự này trong máy tính và khi chúng được đọc, chúng cần được giải thích theo cách có ý nghĩa đối với người dùng.

Quy trình hoạt động như thế này trong cả hai trường hợp:

Message -> Encoding -> Store/Send -> Decoding -> Message

SOS trong mã Morse nó sẽ là:

SOS -> Encode('SOS') -> ...---... -> Decode('...---...') -> SOS
-----------------------              --------------------------
       Sender                                 Receiver

Một thay đổi lớn với máy tính và công nghệ khác là quá trình mã hóa và giải mã đã được tự động hóa, do đó chúng tôi không cần người dịch thông tin nữa.

Khi máy tính được phát minh, một trong những tiêu chuẩn ban đầu được tạo ra để tự động chuyển đổi các ký tự thành 1 và 0 (mặc dù không phải là tiêu chuẩn đầu tiên) là ASCII.

ASCII là viết tắt của American Standard Code for Information Interchange. Phần "Mỹ" đóng một vai trò quan trọng trong cách máy tính làm việc với thông tin trong một thời gian; chúng ta sẽ xem lý do tại sao trong phần tiếp theo.

ASCII (1963)

Dựa trên kiến ​​thức về các mã điện tín như mã Morse và máy tính từ rất sớm, một tiêu chuẩn để mã hóa và giải mã các ký tự trong máy tính đã được tạo ra vào khoảng năm 1963. Hệ thống này tương đối đơn giản vì ban đầu nó chỉ bao gồm 127 ký tự, bảng chữ cái tiếng Anh cộng với các ký hiệu phụ.

ASCII hoạt động bằng cách liên kết từng ký tự với một số thập phân có thể được dịch sang mã nhị phân. Hãy xem một ví dụ:

"A" là 65 trong ASCII, vì vậy chúng ta cần dịch 65 thành mã nhị phân.

Nếu bạn không biết cách hoạt động, đây là một cách nhanh chóng :Chúng tôi bắt đầu chia 65 cho 2 và tiếp tục cho đến khi nhận được 0. Nếu phép chia không chính xác, chúng tôi thêm 1 làm phần dư:

65 / 2 = 32 + 1
32 / 2 = 16 + 0
16 / 2 = 8 + 0
8 / 2  = 4 + 0
4 / 2  = 2 + 0
2 / 2  = 1 + 0
1 / 2  = 0 + 1

Bây giờ, chúng tôi lấy phần còn lại và đặt chúng theo thứ tự ngược lại:

1000001

Vì vậy, chúng tôi sẽ lưu trữ "A" là "1000001" với mã hóa ASCII ban đầu, hiện được gọi là US-ASCII. Ngày nay, với máy tính 8 bit phổ biến, nó sẽ là 01000001 (8 bit =1 byte).

Chúng tôi thực hiện theo cùng một quy trình cho mỗi ký tự, vì vậy với 7 bit, chúng tôi có thể lưu trữ tối đa 2 ^ 7 ký tự =127.

Đây là bảng đầy đủ:

Giới thiệu về Rubyists về Mã hóa ký tự, Unicode và UTF-8 (Từ https://www.plcdev.com/ascii_chart)

Sự cố với ASCII

Điều gì sẽ xảy ra nếu chúng ta muốn thêm một ký tự khác, như ký tự ç trong tiếng Pháp hoặc ký tự tiếng Nhật 大?

Có, chúng tôi sẽ gặp sự cố.

Sau ASCII, mọi người đã cố gắng giải quyết vấn đề này bằng cách tạo ra các hệ thống mã hóa của riêng họ. Họ đã sử dụng nhiều bit hơn, nhưng điều này cuối cùng lại gây ra một vấn đề khác.

Vấn đề chính là khi đọc một tệp, bạn không biết liệu mình có một hệ thống mã hóa nhất định hay không. Việc cố gắng diễn giải nó bằng một mã hóa không chính xác dẫn đến sai ngữ pháp như "���" hoặc "Ã, ÂÃ⠀ šÃ‚Â".

Sự phát triển của các hệ thống mã hóa đó rất lớn và rộng khắp. Tùy thuộc vào ngôn ngữ, bạn đã có các hệ thống khác nhau. Các ngôn ngữ có nhiều ký tự hơn, như tiếng Trung, đã phải phát triển các hệ thống phức tạp hơn để mã hóa bảng chữ cái của chúng.

Sau nhiều năm vật lộn với điều này, một tiêu chuẩn mới đã được tạo ra:Unicode. Tiêu chuẩn này xác định cách máy tính hiện đại mã hóa và giải mã thông tin.

Unicode (1988)

Mục tiêu của Unicode rất đơn giản. Theo trang web chính thức của nó:"Để cung cấp một số duy nhất cho mọi ký tự, bất kể nền tảng, chương trình hoặc ngôn ngữ."

Vì vậy, mỗi ký tự trong một ngôn ngữ có một mã duy nhất được gán, còn được gọi là điểm mã. Hiện có hơn 137.000 ký tự.

Là một phần của tiêu chuẩn Unicode, chúng tôi có nhiều cách khác nhau để mã hóa các giá trị hoặc điểm mã đó, nhưng UTF-8 là cách mở rộng nhất.

Cũng chính những người đã tạo ra ngôn ngữ lập trình cờ vây, Rob Pike và Ken Thompson, cũng đã tạo ra UTF-8. Nó đã thành công bởi vì nó hiệu quả và thông minh trong cách nó mã hóa những con số đó. Hãy xem lý do chính xác.

UTF-8:Định dạng Chuyển đổi Unicode (1993)

UTF-8 hiện là mã hóa thực tế cho các trang web (hơn 94% các trang web sử dụng mã hóa đó). Nó cũng là mã hóa mặc định cho nhiều ngôn ngữ lập trình và tệp. Vậy tại sao nó lại thành công như vậy và nó hoạt động như thế nào?

UTF-8, giống như các hệ thống mã hóa khác, chuyển đổi các số được xác định trong Unicode thành nhị phân để lưu trữ chúng trong máy tính.

Có hai khía cạnh rất quan trọng của UTF-8:- Hiệu quả khi lưu trữ các bit, vì một ký tự có thể chiếm từ 1 đến 4 byte. - Bằng cách sử dụng Unicode và một lượng byte động, nó tương thích với mã hóa ASCII vì 127 đầu tiên ký tự chiếm 1 byte. Điều này có nghĩa là bạn có thể mở tệp ASCII dưới dạng UTF-8.

Hãy phân tích cách UTF-8 hoạt động.

UTF-8 với 1 byte

Tùy thuộc vào giá trị trong bảng Unicode, UTF-8 sử dụng số lượng ký tự khác nhau.

Với 127 đầu tiên, nó sử dụng mẫu sau:Rust1 0_______

Vì vậy, số 0 sẽ luôn ở đó, theo sau là số nhị phân đại diện cho giá trị trong Unicode (cũng sẽ là ASCII). Ví dụ:A =65 =1000001.

Hãy kiểm tra điều này với Ruby bằng cách sử dụng phương thức giải nén trong String:

'A'.unpack('B*').first

# 01000001

Chữ B có nghĩa là chúng ta muốn định dạng nhị phân có bit quan trọng nhất trước tiên. Trong bối cảnh này, điều đó có nghĩa là bit có giá trị cao nhất. Dấu hoa thị cho Ruby biết tiếp tục cho đến khi không còn bit nào nữa. Nếu chúng tôi sử dụng một số thay thế, chúng tôi sẽ chỉ nhận được các bit lên đến số đó:

'A'.unpack('B4').first

# 01000

UTF-8 với 2 byte

Nếu chúng ta có một ký tự có giá trị hoặc điểm mã trong Unicode vượt quá 127, lên đến 2047, chúng ta sử dụng hai byte với mẫu sau:

110_____ 10______

Vì vậy, chúng tôi có 11 bit trống cho giá trị trong Unicode. Hãy xem một ví dụ:

À là 192 trong Unicode, vì vậy trong hệ nhị phân, nó là 11000000, chiếm 8 bit. Nó không phù hợp với mẫu đầu tiên, vì vậy chúng tôi sử dụng mẫu thứ hai:

110_____ 10______

Chúng tôi bắt đầu điền vào các khoảng trống từ phải sang trái:

110___11 10000000

Điều gì xảy ra với các bit trống ở đó? Chúng tôi chỉ đặt các số 0, vì vậy kết quả cuối cùng là:11000011 10000000.

Chúng ta có thể bắt đầu thấy một mô hình ở đây. Nếu chúng ta bắt đầu đọc từ trái sang phải, nhóm 8 bit đầu tiên có hai số 1 ở đầu. Điều này ngụ ý rằng ký tự sẽ mất 2 byte:

11000011 10000000
--       

Một lần nữa, chúng ta có thể kiểm tra điều này bằng Ruby:

'À'.unpack('B*').first

# 1100001110000000

Một mẹo nhỏ ở đây là chúng ta có thể định dạng đầu ra tốt hơn với:

'À'.unpack('B8 B8').join(' ')

# 11000011 10000000

Chúng tôi nhận được một mảng từ 'À'.unpack('B8 B8') và sau đó chúng tôi nối các phần tử với một khoảng trắng để có được một chuỗi. Số 8 trong tham số giải nén yêu cầu Ruby nhận 8 bit trong 2 nhóm.

UTF-8 với 3 byte

Nếu giá trị trong Unicode cho một ký tự không phù hợp với 11 bit có sẵn trong mẫu trước đó, chúng tôi cần thêm một byte:

1110____  10______  10______

Một lần nữa, ba số 1 ở đầu mẫu cho chúng ta biết rằng chúng ta sắp đọc một ký tự 3 byte.

Quy trình tương tự sẽ được áp dụng cho mẫu này; chuyển đổi giá trị Unicode thành nhị phân và bắt đầu lấp đầy các vị trí từ phải sang trái. Nếu chúng ta có một số khoảng trống sau đó, hãy điền chúng bằng 0.

UTF-8 với 4 byte

Một số giá trị thậm chí còn chiếm nhiều hơn 11 bit trống mà chúng ta đã có trong mẫu trước. Hãy xem một ví dụ với biểu tượng cảm xúc 🙂, biểu tượng này đối với Unicode cũng có thể được xem như một ký tự như "a" hoặc "大".

Giá trị hoặc điểm mã của "🙂" trong Unicode là 128578. Con số đó trong hệ nhị phân là:11111011001000010, 17 bit. Điều này có nghĩa là nó không phù hợp với mẫu 3 byte vì chúng tôi chỉ có 16 vị trí trống, vì vậy chúng tôi cần sử dụng một mẫu mới có 4 byte trong bộ nhớ:

11110___  10______ 10______  10______

Chúng tôi bắt đầu lại bằng cách điền nó với số ở dạng nhị phân:Rust1 11110___ 10_11111 10011001 10000010

Và bây giờ, chúng tôi điền phần còn lại bằng các số 0:Rust1 1111000 10011111 10011001 10000010

Hãy xem điều này trông như thế nào trong Ruby.

Vì chúng tôi đã biết rằng điều này sẽ mất 4 byte, chúng tôi có thể tối ưu hóa để có khả năng đọc tốt hơn trong đầu ra:

'🙂'.unpack('B8 B8 B8 B8').join(' ')

# 11110000 10011111 10011001 10000010

Nhưng nếu không, chúng ta chỉ có thể sử dụng:

'🙂'.unpack('B*')

Chúng tôi cũng có thể sử dụng phương thức chuỗi "byte" để trích xuất các byte vào một mảng:

"🙂".bytes

# [240, 159, 153, 130]

Và sau đó, chúng tôi có thể ánh xạ các phần tử thành hệ nhị phân với:

"🙂".bytes.map {|e| e.to_s 2}

# ["11110000", "10011111", "10011001", "10000010"]

Và nếu chúng ta muốn một chuỗi, chúng ta có thể sử dụng join:

"🙂".bytes.map {|e| e.to_s 2}.join(' ')

# 11110000 10011111 10011001 10000010

UTF-8 có nhiều dung lượng hơn mức cần thiết cho Unicode

Một khía cạnh quan trọng khác của UTF-8 là nó có thể bao gồm tất cả các giá trị Unicode (hoặc điểm mã) - và không chỉ những giá trị tồn tại ngày nay mà còn cả những giá trị sẽ tồn tại trong tương lai.

Điều này là do trong UTF-8, với mẫu 4 byte, chúng ta có 21 vị trí cần điền. Điều đó có nghĩa là chúng tôi có thể lưu trữ tối đa 2 ^ 21 (=2.097.152) giá trị, nhiều hơn số lượng giá trị Unicode lớn nhất mà chúng tôi từng có với tiêu chuẩn, khoảng 1,1 triệu.

Điều này có nghĩa là chúng tôi có thể sử dụng UTF-8 với sự tự tin rằng chúng tôi sẽ không cần chuyển sang hệ thống mã hóa khác trong tương lai để phân bổ các ký tự hoặc ngôn ngữ mới.

Làm việc với các mã hóa khác nhau trong Ruby

Trong Ruby, chúng ta có thể thấy ngay mã hóa của một chuỗi nhất định bằng cách thực hiện điều này:

'Hello'.encoding.name

# "UTF-8"

Chúng tôi cũng có thể mã hóa một chuỗi bằng một hệ thống mã hóa khác. Ví dụ:

encoded_string = 'hello, how are you?'.encode("ISO-8859-1", "UTF-8")

encoded_string.encoding.name

# ISO-8859-1

Nếu quá trình chuyển đổi không tương thích, chúng tôi sẽ gặp lỗi theo mặc định. Giả sử chúng tôi muốn chuyển đổi "hello 🙂" từ UTF-8 thành ASCII. Vì biểu tượng cảm xúc "🙂" không phù hợp với ASCII, chúng tôi không thể. Ruby tạo ra một lỗi trong trường hợp đó:

"hello 🙂".encode("ASCII", "UTF-8")

# Encoding::UndefinedConversionError (U+1F642 from UTF-8 to US-ASCII)

Nhưng Ruby cho phép chúng ta có những ngoại lệ, trong đó, nếu một ký tự không thể được mã hóa, chúng ta có thể thay thế nó bằng "?".

"hello 🙂".encode("ASCII", "UTF-8", undef: :replace)

# hello ?

Chúng tôi cũng có tùy chọn thay thế các ký tự nhất định bằng một ký tự hợp lệ trong bảng mã mới:

"hello 🙂".encode("ASCII", "UTF-8", fallback: {"🙂" => ":)"})

# hello :)

Kiểm tra mã hóa của một script trong Ruby

Để xem mã hóa của tệp script bạn đang làm việc, tệp ".rb", bạn có thể thực hiện như sau:

__ENCODING__

# This will show "#<Encoding:UTF-8>" in my case.

Từ Ruby 2.0 trở đi, mã hóa mặc định cho các tập lệnh Ruby là UTF-8, nhưng bạn có thể thay đổi mã đó bằng một nhận xét ở dòng đầu tiên:

# encoding: ASCII

__ENCODING__
# #<Encoding:US-ASCII>

Nhưng tốt hơn hết bạn nên tuân theo tiêu chuẩn UTF-8 trừ khi bạn có lý do chính đáng để thay đổi nó.

Một số mẹo để làm việc với các bảng mã trong Ruby

Bạn có thể xem toàn bộ danh sách các bảng mã được hỗ trợ trong Ruby với Encoding.name_list . Điều này sẽ trả về một mảng lớn:

["ASCII-8BIT", "UTF-8", "US-ASCII", "UTF-16BE", "UTF-16LE", "UTF-32BE", "UTF-32LE", "UTF-16", "UTF-32", "UTF8-MAC"...

Một khía cạnh quan trọng khác khi làm việc với các ký tự bên ngoài ngôn ngữ tiếng Anh là trước Ruby 2.4, một số phương thức như upcase hoặc reverse không hoạt động như mong đợi. Ví dụ, trong Ruby 2.3, chữ viết hoa không hoạt động như bạn nghĩ:

# Ruby 2.3
'öıüëâñà'.upcase

# 'öıüëâñà'

Giải pháp thay thế là sử dụng ActiveSupport, từ Rails hoặc một viên ngọc bên ngoài khác, nhưng kể từ Ruby 2.4, chúng tôi có đầy đủ ánh xạ trường hợp Unicode:

# From Ruby 2.4 and up
'öıüëâñà'.upcase

# 'ÖIÜËÂÑÀ'

Thú vị với biểu tượng cảm xúc

Hãy xem biểu tượng cảm xúc hoạt động như thế nào trong Unicode và Ruby:

'🖖'.chars

# ["🖖"]

Đây là "Bàn tay giơ lên ​​với phần giữa ngón tay giữa và ngón tay nhẫn", còn được gọi là biểu tượng cảm xúc "Vulcan Salute". Nếu chúng ta có cùng một biểu tượng cảm xúc nhưng ở tông màu da khác không phải là màu mặc định, điều thú vị sẽ xảy ra:

'🖖🏾'.chars

# ["🖖", "🏾"]

Vì vậy, thay vì chỉ là một ký tự, chúng tôi có hai cho một biểu tượng cảm xúc duy nhất.

Điều gì đã xảy ra ở đó?

Vâng, một số ký tự trong Unicode được định nghĩa là sự kết hợp của một số ký tự. Trong trường hợp này, nếu máy tính nhìn thấy hai ký tự này cùng nhau, nó sẽ chỉ hiển thị một ký tự với tông màu da được áp dụng.

Có một ví dụ thú vị khác mà chúng ta có thể thấy với cờ.

'🇦🇺'.chars

# ["🇦", "🇺"]

Trong Unicode, biểu tượng cảm xúc cờ được đại diện bên trong bằng một số ký tự Unicode trừu tượng được gọi là "Biểu tượng chỉ báo khu vực" như 🇦 hoặc 🇿. Chúng thường không được sử dụng bên ngoài cờ và khi máy tính nhìn thấy hai biểu tượng cùng nhau, nó sẽ hiển thị cờ nếu có một biểu tượng cho sự kết hợp đó.

Để tự mình xem, hãy thử sao chép phần này và xóa dấu phẩy trong bất kỳ trường hoặc trình soạn thảo văn bản nào:

🇦,🇺

Kết luận

Tôi hy vọng bài đánh giá này về cách Unicode và UTF-8 hoạt động cũng như cách chúng liên quan đến Ruby và các lỗi tiềm ẩn sẽ hữu ích cho bạn.

Bài học quan trọng nhất cần rút ra là hãy nhớ rằng khi bạn đang làm việc với bất kỳ loại văn bản nào, bạn có một hệ thống mã hóa được liên kết và điều quan trọng là phải giữ cho nó hiện diện khi lưu trữ hoặc thay đổi nó. Nếu có thể, hãy sử dụng hệ thống mã hóa hiện đại như UTF-8 để bạn không cần thay đổi hệ thống này trong tương lai.

Lưu ý về các bản phát hành Ruby

Tôi đã sử dụng Ruby 2.6.5 cho tất cả các ví dụ trong bài viết này. Bạn có thể thử chúng trong REPL trực tuyến hoặc cục bộ bằng cách đi đến thiết bị đầu cuối của bạn và thực hiện irb nếu bạn đã cài đặt Ruby.

Vì hỗ trợ Unicode đã được cải thiện trong các phiên bản gần đây nhất, tôi đã chọn sử dụng phiên bản mới nhất để bài viết này sẽ vẫn có liên quan. Trong mọi trường hợp, với Ruby 2.4 trở lên, tất cả các ví dụ sẽ hoạt động như được hiển thị ở đây.