はじめに
Ruby Advent Calendar 2020 の11日目の記事です。
昨日は、@universato さんの Ruby競プロTips(基本・罠・高速化108 2.7x2.7) でした。
最近文字コードに依存したコードを書く機会があり、 String
の便利なメソッドをいろいろと知ったので紹介します。
動作確認環境
- Ruby 2.7.2
- UTF-8
前提情報
- Ruby は他の多くの言語と異なり、
String
オブジェクト自体がエンコーディング情報を持っています。 - 現在のスクリプトエンコーディングは疑似変数
__ENCODING__
で確認できます。 - Unicodeでは人間が認識する自然な1文字と、Unicodeのデータ上の1文字が異なることがあります。人間が認識する自然な1文字は「書記素クラスタ(grapheme_cluster)」という単位でカウントされます。
- 特に断りがない場合、
String
のメソッドです。 - 特に断りがない場合、UTF-8を前提に記述しています。
文字コードを知りたい
ord
, each_codepoint
, unpack("U*")
ord
文字列の最初の文字の文字コードを10進数の値( Integer
)で返します。
値を16進数にするには to_s(16)
を使います。
p "a".ord
# => 97
p "a".ord.to_s(16)
# => "61"
16進数の数値をU+nnnnの形式で検索すると、文字コードから文字が分かります。
例えば a
は16進数で 61
という値でした。 U+0061
で検索すると、 a
という文字であることが分かります。
Unicode Utilities: Character Properties
each_codepoint
文字列の各文字の文字コードを知りたい場合は each_codepoint
を使うと便利です。
"abcあいう漢字".each_codepoint { |s| p s.to_s(16) }
# "61" # a
# "62" # b
# "63" # c
# "3042" # あ
# "3044" # い
# "3046" # う
# "6f22" # 漢
# "5b57" # 字
unpack("U*")
unpack("U*")
でも文字コードを調べることが出来ます。返り値は Array
です。
引数の詳細は Ruby リファレンスマニュアルを参照してください。String#unpack (Ruby 2.7.0 リファレンスマニュアル)
p "a".unpack("U*")
# => [97]
"abc".unpack("U*").each { |s| p s.to_s(16) }
# "61"
# "62"
# "63"
# => [97, 98, 99]
返り値のエンコーディング情報は UTF-8 とは限りません。必要に応じ後述のメソッドでエンコーディング情報を変換してください。
a = "a".unpack("U*")
# => [97]
a.each { |s| p s.to_s.encoding }
# #<Encoding:US-ASCII>
i = "い".unpack("U*")
# => [12356]
i.each { |s| p s.to_s.encoding }
# #<Encoding:US-ASCII>
p "👨👩👦👦".unpack("H*")
# => ["f09f91a8e2808df09f91a9e2808df09f91a6e2808df09f91a6"]
p ["f09f91a8e2808df09f91a9e2808df09f91a6e2808df09f91a6"].pack("H*")
# => "\xF0\x9F\x91\xA8\xE2\x80\x8D\xF0\x9F\x91\xA9\xE2\x80\x8D\xF0\x9F\x91\xA6\xE2\x80\x8D\xF0\x9F\x91\xA6"
p ["f09f91a8e2808df09f91a9e2808df09f91a6e2808df09f91a6"].pack("H*").encoding
# => #<Encoding:ASCII-8BIT>
文字コードから文字にしたい
\unnnn
, \u{nnnnnn}
, Array#pack
, Integer#chr
\unnnn, \u{nnnnnn}
文字コードが4桁までの場合は \unnnn(nは16進数の数値)を使えます。5桁〜6桁の場合は {}
でくくることで入力できます。また、 {}
の中には連続して文字コードを入れることができます。
"\u0061"
# => "a"
"\u{1F97A}"
# => "🥺"
"\u{1F31F 1F3B6}"
# => "🌟🎶"
Array#pack
配列の内容を引数で指定された形でパックし、文字列を返します。16進数の文字列を指定するには 0x
を前につけます。
ary = "abc".unpack("U*")
# => [97, 98, 99]
p ary.pack("U*")
# => "abc"
p [0x3042].pack("U*")
# => "あ"
Integer#chr
self
を文字コードと見たときに対応する文字を返します。引数にエンコーディングを渡すことができます。
引数を渡さなかった場合はエンコーディングを US-ASCII
, ASCII-8BIT
, Encoding.default_internal
の順で解釈します。
p 33440.chr(Encoding::SJIS)
# => "\x{82A0}"
p 0x3042.chr(Encoding::UTF_8)
# => "あ"
バイト列から文字にしたい
pack("H*")
を使ってバイナリにしたあと、encode
します。
pack("H*")
の返り値の encoding は変換したい文字コードとは限らないため、 encode
の第2引数に変換したい文字コードを指定します。
この方法を取る場合、 encode
で変換する文字コードを決め打ちするか、なんらかの手段でバイト列から文字コードを推測する必要があります。
ary = 'あいう'.bytes
# => [227, 129, 130, 227, 129, 132, 227, 129, 134]
[ ary.map { _1.to_s(16) }.join ].pack("H*").encode('UTF-8', 'UTF-8')
# => "あいう"
文字数を知りたい
size
, length
や、それに grapheme_clusters
を組み合わせて使うことができます。
size, length
p "a".size
# => 1
p "ああ".size
# => 2
p "ああ".length
# => 2
ただし、 size
や length
では結合された文字に対して想定と異なる結果を返すことがあります。
これは主に結合文字で起こる事象です。見た目には1文字に見えますが、内部的には複数の文字が組み合わさっています。
p "👨👩👧👦".length # U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
# => 7
p "0︀".length # U+0030 U+FE00
# => 2
p "が".length # U+304B U+3099 「か」と濁点を組み合わせた結合文字の「が」
# => 2
p "が".length # U+304C 普段入力する「が」
# => 1
grapheme_clusters
このような結合文字を1文字としてカウントしたいときには、 grapheme_clusters
を使います。これはUnicodeの書記素クラスタ単位でカウントしてくれます。
p "👨👩👧👦".grapheme_clusters.length
# => 1
p "0︀".grapheme_clusters.length
# => 1
p "が".grapheme_clusters.length
# => 1
文字のバイト長を知りたい
bytesize
文字列が何バイトあるか分かります。
p "a".bytesize
# => 1
p "あ".bytesize
# => 3
p "aあ".bytesize
# => 4
p "👨👩👧👦".bytesize
# => 25
文字列の内容が壊れてないか知りたい
valid_encoding?
受け取った文字列が現在のエンコーディングで妥当であれば true
を返します。いわゆる文字化けするような文字でないか確かめられます。
p __ENCODING__
# => #<Encoding:UTF-8>
p "\x61".valid_encoding? # a
# => true
p "\x61\xff".valid_encoding? # a に ff を追加して存在しない文字をつくった
# => false
エンコーディングを変更したい
encode
encode
でエンコーディングを変更・変換できます。エンコーディングの一覧は class Encoding (Ruby 2.7.0 リファレンスマニュアル) で確認できます。self のエンコーディングは変更されません。
細かいエンコーディングの操作には class Encoding::Converter (Ruby 2.7.0 リファレンスマニュアル) もあります。
s = "あいう".encode("Windows-31J")
# => "\x{82A0}\x{82A2}\x{82A4}"
p s.encoding
# => #<Encoding:Windows-31J>
hoge = "ほげ"
p hoge.encode("WINDOWS-31J")
# => "\x{82D9}\x{82B0}" # 返り値はエンコーディングが変換されている
p hoge.encoding # hogeのエンコーディングは変更されない
# => #<Encoding:UTF-8>
force_encoding
self
のエンコーディング情報を破壊的に変更します。このとき、実際のエンコーディングは変換されません。また、 valid_encoding?
のようなチェックも行われません。
piyo = "piyo"
p piyo.encoding
# => #<Encoding:UTF-8>
piyo.force_encoding("EUC-JP")
# => "piyo"
piyo.encoding # piyo のエンコーディングが変更されたことを確認
# => #<Encoding:EUC-JP>
エンコーディング情報を知りたい
encoding
encoding
で self のエンコーディング情報が分かります。
e = "言語".encode("EUC-JP")
# => "\x{B8C0}\x{B8EC}"
u = "言語".encode("UTF-8")
# => "言語"
p e.encoding
# => #<Encoding:EUC-JP>
p u.encoding
# => #<Encoding:UTF-8>
見た目は同じだが文字コードが異なる文字を同じ文字として扱いたい
unicode_normalize
Unicode正規化を行うことで、異なる文字を同じ文字に変換することができます。Unicode正規化についてはここでは割愛します。
ga1 = "が"
ga1.each_codepoint { |s| p s.to_s(16) }
# "304b"
# "3099"
ga2 = "が"
ga2.each_codepoint { |s| p s.to_s(16) }
# "304c"
p(ga1 == ga2)
# => false
p(ga1.unicode_normalize == ga2.unicode_normalize)
# => true
Rubyで扱えるエンコーディングを知りたい
Encoding
クラスの定数に格納されています。
class Encoding (Ruby 3.0.0 リファレンスマニュアル)
pp Encoding.constants
# =>
# [:CompatibilityError,
# :BINARY,
# :ASCII_8BIT,
# :UTF_8,
# :US_ASCII,
# 以下略
参考資料
Ruby の多言語化対応について知りたい場合はこちらの記事に詳しく書いてあります。