search
LoginSignup
25

More than 1 year has passed since last update.

posted at

updated at

Organization

Rubyで文字コードを扱うコードを書くときに便利なメソッド集

はじめに

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

ただし、 sizelength では結合された文字に対して想定と異なる結果を返すことがあります。

これは主に結合文字で起こる事象です。見た目には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 の多言語化対応について知りたい場合はこちらの記事に詳しく書いてあります。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
25