2017年4月5日水曜日

Chrome extentionを使い、iframe内外でデータ通信

◆ 目的
Webページ(親ページ)とそこからiframeで読み込んだ子ページ間でデータの授受を行う。
ただし、iframe内のコンテンツは親ページのドメイン外に存在する任意のサイトとする。



◆ 課題
解決する課題は2つある。
1. クロスドメイン制約への対応
XMLHttpRequestを投げる際などにも必ず気に掛ける点だろう。
Same Origin Policy(同一生成元ポリシー)を回避しなければならない。

今回は任意の外部サイトをiframeに取り込むことを前提としているため、
CORS(Cross-Origin Resource Sharing)もJSONP(JSON with padding)も今回は使えない。

2. iframe内での読み込み不許可ヘッダへの対応
HTTPのレスポンスヘッダに、iframe内からWebページが読み込まれるのを防止するオプション(X-Frame-Options)が付与されていればiframe内で表示はできない。



◆ 解決策
1. postMessageの利用
クロスドメイン制約への対応として、HTML5で用意されたpostMessageの利用を思いつく。

(親サイト)
<iframe id="ifrm" src="外部サイト"></iframe>

<script type="text/javascript">
window.onload = function() {
  var ifrm = document.getElementById('ifrm').contentWindow
  ifrm.postMessage("hello", '外部サイト')
};
</script>

(外部サイト)
<script type="text/javascript">
window.addEventListener('message', function(event) {
    alert(event.data)
}, false);
</script>

しかし、Chromeが許可しない。
※Safariでは警告なく、実施できた。

(エラー例)
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://親サイト') does not match the recipient window's origin ('http://外部サイト').


2. iframeを使わない
iframeを使う前提を変え、コンテンツをダウンロードし、外部サイトを親側で再現させる。つまり、前提を変える。
が、しかし、JavaScriptが生成する動的ページのリソースの管理は困難があるため、やはり、iframeは使いたい。前提は戻す。


3. Google Chromeの拡張機能の利用
Chromeに限定されるが、Chromeの拡張機能を使えば対応ができそうである。
データの共有は、バックグラウンドで動作させるスクリプト内でセッションストレージを使えばいいだろう。
また、ヘッダの書き換えも実施できる。



◆ 拡張コード概要
chrome拡張を利用することにした。
拡張コードを有効にするには、最低限以下の3つのファイルを用意し、それらを適当なフォルダに入れ、Chromeブラウザの拡張機能からインポートすれば良い。

○ マニフェスト ファイル
拡張機能に関する情報を与える。

○ コンテンツ スクリプト
ブラウザで表示させるページで読み込むjsとは別空間で実行させるjsである。
このファイルは親サイトと、iframe内の外部サイト、両方に読み込まれる。
空間は分かれているため、コンテンツスクリプト内で利用しているjQueryなどのライブラリがサイトで利用しているバージョンと異なっていても問題は起きない。

○ バックグラウンド スクリプト
Chromeのバックエンド側で処理させるjsである。
表示コンテンツには取り込まれないが、コンテント ファイルとの間でメッセージ通信ができる。


簡易図で表すと下記のような感じである。
   parent
+----------+
|          |← contentScripts.js
|  iframe  |                       
|  +----+  |
|  |    |  |
|  |    |←-|-- contentScripts.js
|  +----+  |
|          |
+----------+
background.js

contentScripts.js、background.jsの名称はmanifest.json内で指定する。



◆ 試験
1. iframe内でマウスを操作。マウスオーバしたタグ要素に色がつくようにしている。
  そのタグ要素でクリックすると、タグ名がセッションストレージへ保存される。

2. 親側でiframe外の要素をクリックする。
  iframeで取得した要素がalert表示されれば成功である。



◆ コード例
○ manifest.json


○ contentScripts.js


○ background.js



◆ コード解説
1点、解説を加えておく。
contentScript.jsは親子両方に読み込まれるため、共通処理以外のjsコードは、親子用で条件を加えている。
// iframe用
if (window != parent) {
  ~snip~
} // 親用
else{
  ~snip~
}

2017年3月4日土曜日

ChatHub(LINE上で動作する、Q&Aを中心としたSNSサービス)


LINE上で動作する、お互いに顔までは知らない関係の、身近なコミュニティ内で、匿名のまま情報交換ができるQ&Aを中心としたSNSサービス、ChatHubを作成した。最近はやりのbotの一つである。





























※内部でAI的な要素を使ったおもしろい仕組みを導入している。
  技術詳細に関しては随時公開予定。



2017年2月4日土曜日

AWS EC2にApache、Passenger、Sinatra環境を構築する


◆ 利用するEC2
t2.small
※メモリは2Gは欲しい



◆ rubyのバージョンアップ
$ sudo yum install -y git gcc gcc-c++ openssl-devel readline-devel
$ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l
$ rbenv -v
$ git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install 2.3.3
$ rbenv global 2.3.3
$ ruby -v



◆ apacheとpassengerの連携
$ gem install passenger
$ sudo yum install libcurl-devel httpd httpd-devel apr-devel apr-util-devel
$ passenger-install-apache2-module
※apacheのconfigへ設定する内容をコピー

$ cd /usr/local/src/mydir/
$ vi hello.rb
require 'sinatra'
get '/' do
  'Hello World!'
end

$ mkdir public
$ mkdir tmp
$ vi config.ru
require 'rubygems'
require 'sinatra'
require File.expand_path '../hello.rb', __FILE__

run Sinatra::Application


$ vi /etc/httpd/conf.d/passenger.conf
LoadModule passenger_module /home/ec2-user/.rbenv/versions/2.3.3/lib/ruby/gems/2.3.0/gems/passenger-5.1.2/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /home/ec2-user/.rbenv/versions/2.3.3/lib/ruby/gems/2.3.0/gems/passenger-5.1.2
  PassengerDefaultRuby /home/ec2-user/.rbenv/versions/2.3.3/bin/ruby
</IfModule>

Listen 8080

<VirtualHost *:8080>
  DocumentRoot /usr/local/src/bot/line/public
  <Directory /usr/local/src/bot/line/public>
    Require all granted
    Allow from all
    Options -MultiViews
    #Relax Apache security settings
    #AllowOverride all
    #MultiViews must be turned off
    #Options -MultiViews
  </Directory>
</VirtualHost>

$ sudo /etc/init.d/httpd restart



◆ 確認
ブラウザからアクセス
http://x.x.x.x:8080/
500 Internal Server Error

WEBrickによる切り分け

$ ruby hello.rb
`require': cannot load such file -- sinatra


$ gem install bundle
$ vi Gemfile
source 'https://rubygems.org'
gem "sinatra"

$ bundle install
An error occurred while installing pg (0.19.0), and Bundler cannot continue.

$ sudo yum install postgresql postgresql-devel sqlite sqlite-devel
$ bundle install
$ ruby hello.rb
$ sudo /etc/init.d/httpd restart

ブラウザからアクセス
http://x.x.x.x:8080/
500 Internal Server Error

$ tail -f /var/log/httpd/error_log
configuration error:  couldn't perform authentication. AuthType

$ sudo vi /etc/httpd/conf.d/passenger.conf
#以下をいったん無効にする
#Require all granted

改めて、
$ sudo /etc/init.d/httpd restart

ブラウザからアクセス
http://x.x.x.x:8080/
'Hello World!'


(参考)
http://recipes.sinatrarb.com/p/deployment/apache_with_passenger




2017年1月10日火曜日

Cのselect、epollを使ったI/O多重化


複数のファイルディスクリプタを監視し、その中のいずれが入出力可能な状態であるかを確認するCのシステムコールとしては、select、epoll、kqueue(BSD系)が有名だろう。
ここでは、selectとepollを使い、1プロセス1スレッドでのイベント駆動によるI/O多重化コードを備忘録として残しておく。

(参考)
最近ではselectではなく、epoll、kqueueが使われることが多い。
selectは待ち受けられるファイルディスクリプタの数に上限があり、またパフォーマンス問題も存在するためである。
I/Oからの入力に応じて発生するイベントを処理するライブラリとしてはlibevent, libev, libuvなどがあるがこの内部でもepollやkqueueが利用されている。
※libevはlibeventの速度改善、FDの制限撤廃の対応がされた改良版である。
※libuvはnode.js用のためにlibevをベースに開発されたライブラリである。



(select.c)


(epoll.c)


$ gcc ファイル名

$ ./a.out /dev/tty /dev/input/mouse0 10
hello
/dev/tty: hello

no input after 10 seconds

マウス操作
/dev/input/mouse0: (

2017年1月6日金曜日

Rubyで深さ優先探索(DFS)と幅優先探索(BFS)

グラフ操作の基本、深さ優先探索(DFS: Depth First Search)と幅優先探索(BFS: Breath First Search)のアルゴリズムをRubyで書く。


◆ DFS


◆ BFS

2016年10月18日火曜日

数学の素養がなくてもできるディープラーニング


◆ 目的
ディープラーニング(深層学習)とは、多層構造のニューラルネットワークを用いた機械学習である。ディープラーニングの代表例である、畳み込みニューラルネットワーク、CNN(Convolutional Neural Network)を使い、10種類の手書き数字を認識させる処理フローを追いかけてみる。また、TensorFlowのコードを実際にどう書いて組むかも理解する。数学に対する抵抗があったとしても難しい部分はTensorFlowが対応してくれるため、誰でもディープラーニングを試すことはできる。脱苦手意識!



◆ CNNの全体像
大きな流れは、特徴を抽出(①)し、それを元に分類(②)することである。①→②のフローが基本である。

CNNでは畳み込みフィルタとプーリング層の組み合わせで特徴を引き出す。
またこのセットを何回繰り返すかは目的によりけりである。また目的が違えば抽出方法も異なる。

識別部での分類処理では、隠れ層を例として2段にして書いているが、単層でも目的に叶う場合もあれば、より多層にする必要も出てくる。


                                 ↗︎
       畳み込みフィルタ → プーリング層 →
   ↗︎                            ↘︎
              ・               ・
input         ・               ・
              ・               ・
   ↘︎                            ↗︎
       畳み込みフィルタ → プーリング層 →
                                 ↘︎

|---------------特徴量の抽出部---------------|



      全結合層
  隠れ層   隠れ層
         ↗︎
     ○  →  ○
↗︎       ↘︎    ↘︎
→ 
↘︎                出力
         ↗︎      ↗︎
     ○  →  ○ → □ →
         ↘︎      ↘︎

↗︎       ↗︎    ↗︎
→   ○  →  ○
↘︎       ↘︎

|----------識別部----------|


◆ 識別部
先に②の識別部から覗いてみる。識別部は、入力される特徴量に基づいて分類処理を行う部分であった。識別部の役割を具体的なノード数(縦列)を当て込んで見ていくことにする。

・入力層
入力値として2つの値のペア(x0, x1)を受け取る。
このペアがデータとして複数入力されてくるわけである。
正確に書けば、n番目のデータとして、(x1n, x2n)としたほうがいいだろうか。

・全結合層
隠れ層が2つの多段ニューラルネットワーク。3ノードから成るとする。

・出力層
2次元だった入力データ(x0, x1)が3つの領域へ分けられる(3分割)。

簡易図での示しやすさを優先しているだけだり、それぞれの数に必然性はない。


入力層         結合層     出力層
          隠れ層0 隠れ層1

                 ↗︎
            ○z0 → ○z'0
      ↗︎         ↘︎     ↘︎
  x0  →                       P0
      ↘︎          ↗︎         ↗︎
            ○z1 → ○z'1 →  □ → P1
      ↗︎          ↘︎         ↘︎
  x1  →                       P2
      ↘︎          ↗︎    ↗︎
            ○z2 → ○z'2
                  ↘︎


(入力層から隠れ層0まで)
未知データを予測する数式は、与えられるデータをX、各項の係数をW、定数項をBの行列とし計算すればいいだろう。

Z = h(XW1 + B1)
z0 = h(x0w00 + x1w10 + b1)
z1 = h(x0w01 + x1w11 + b1)
z2 = h(x0w02 + x1w12 + b1)


実計算では行列でまとめる。行と列の縦横を混乱しがちになるが落ち着いて考えれば難しくない。

X =              W1 =                B1 =
[[x0, x1],       [[w00, w01, w02]     [b1, b2]
    ・             [w10, w11, w12]]
    ・
    ・    ]]

|--入力の次元数--| |--隠れ層のノード数--| |--隠れ層のノード数--|

Z =
[[z0, z1, z2],
      ・
      ・
      ・     ]]

X
入力データである。Placeholderと呼ばれる。
トレーニングやテスト用のデータとして(x0, x1)のペアが複数ある。
この入力データが最終的に3つのデータ(z0, z1, z2)に拡張される。

W
最適化するパラメータ(Weight)変数である。Variableと呼ばれる。

B
定数項(Bias)である

・h
x軸の0を境に値が1へ増加する、活性化関数である。
入力信号の変化に応じて出力が活性化するニューロンのような模式である。
以下のようなものがある。
  σ(x): シグモイド関数
  tanhx: 双曲線関数(hyperbolic function)
  relu(x): ReLU(Rectified Linear Unit, Rectifier)

TensorFlowの実コードは後半改めて見る。


(隠れ層1そして出力)
入力層から隠れ層0までと基本同じである。

違いは、隠れ層0の出力が隠れ層1の入力となり、
最後に、ソフトマックス関数を適用させる点である。
ソフトマックス関数は出力をK個に、総和は1となる、ある分類領域に所属する確率を求める関数である。
境界を境にハードに領域が変化するのではなく、ソフトに確率が変化していく。

隠れ層から出力層へはこの式で計算する。
Z' = ZW0 + b
※'はただの別識別の記号として使っているだけである。

P = softmax(Z')
z'0 = z0w00 + z1w10 + z2w20 + b0
z'1 = z0w01 + z1w11 + z2w21 + b0
z'2 = z0w02 + z1w12 + z2w22 + b0

p0 = softmax(z'0)
p1 = softmax(z'1)
p2 = softmax(z'2)


Z =              W0 =              B0 =
[[z0, z1, z2],   [[w00, w01, w02]  [b0, b1, b2]
      ・           [w10, w11, w12]
      ・           [w20, w21, w22]]
      ・    ]]
                |---出力の分割数---|

Z' =
[[z'0, z'1, z'2],
       ・
       ・
       ・       ]]

softmax関数を適用し、
P =
[[p0, p1, p2],
      ・
      ・
      ・     ]]



◆ 特徴量の抽出部
入力データから畳み込みフィルタ、プーリング層を経由させ特徴量の取り出しを行う。

・入力データ
画像ファイルとする。

・畳み込みフィルタ
画像のエッジ抽出などを行うフィルタである。
ディープラーニング専用のファンクションではない。

・プーリング層
解像度を落とす役割を担う。

早速tensolflowのコードを見ながら処理を追いかけたい。
こちらのコードを参考にさせてもらう。

先に書いたフロー図と異なり、畳み込みフィルターとプーリング層の処理セットを2つ並べている。

(1段目の畳み込みフィルターとプーリング層)
# 入力の次元数が784(画像サイズ28x28ピクセル)。
# Noneとしているのは入力数は状況に合わせて動的対応させるため。
x = tf.placeholder(tf.float32, [None, 784])

# 配列内は、[画像枚数, 画像サイズ(縦x横), レイヤ数]。
# 画像枚数はplaceholderに格納したデータ数に任せるよう-1で指示。
# レイヤ数は、複数レイヤを重ね合わせて一つの画像にしている場合に増える。
x_image = tf.reshape(x, [-1,28,28,1])

# フィルタのノード数(縦列)は32とする。
num_filters1 = 32

# 事前に抽出したい特徴が分かっていればそのフィルタを適用すればよい。
# フィルタ配列は、[フィルタサイズ(縦x横), 入力レイヤ数, フィルタ数]である。 
# ただし、手書き文字ではどういった特徴を抽出するか、それ自体が分からないため、
# フィルタ配列をVariableとし、それ自体を勾配降下法による最適化の対象にしてしまう。
# 今回はフィルタサイズを5x5とし、それを当てはめる入力画像はグレースケールの1レイヤとする。
W_conv1 = tf.Variable(tf.truncated_normal([5, 5, 1, num_filters1], stddev=0.1))

# 畳み込みフィルタの計算はconv2d関数を適用するだけである。
h_conv1 = tf.nn.conv2d(x_image, W_conv1, strides=[1,1,1,1], padding='SAME')

# 画像濃度が、ある値b_conv1より小さい場合は0として扱いたい。e.g) b_conv1 = 0.1
# 活性化関数ReLUはx軸が負の値を0にする。この性質を利用する。
# 定数項b_conv1の初期値を0ではなく0.1とするのは、誤差関数の停留値を避けるためである。
# ただし、0.1も最適かどうか分からない。よってこの閾値も最適化対象のパラーメータとするVariableとする。
b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))
h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)

# プーリング層である。
# 28x28ピクッッセルの画像を2x2のピクセルブロックに分解する。
# それぞれを1つのピクセルにすることで14x14ピクセルになる。
h_pool1 =tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')


(2段目の畳み込みフィルターとプーリング層)
1段目と基本変わらない。入力と出力、ノード数が変わっているだけである。

1段目は32このフィルタがあった。2段目は64個のフィルタを使っている。
1段目でフィルタ適用され出力された32個の画像データに2段目のフィルタを当てはめ、それをレイヤ合成して1つにして完成である。2段目フィルタは64個あるため、出力も64個である。

num_filters2 = 64

W_conv2 = tf.Variable(tf.truncated_normal([5, 5, num_filters1, num_filters2], stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2, strides=[1,1,1,1], padding='SAME')

b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)

h_pool2 =tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')



◆ 識別部
説明が前後するが、上の特徴量の抽出ができたので、省略していた識別部のコードを見てみる。

注意点が2つ。
サンプルでは、隠れ層は1層である(最初のフロー図では2段だった)。
また、全結合層から出力時に適合させるソフトマックス関数の間にドロップアウト層を設けている。過学習(オーバーフッティング)を防止するため、一部のノードを切断することが目的である。トレーニングセットの正解率は高いが未知データへの精度の悪さを回避する処理である。


# 入力次元数(7 x 7ピクセル x フィルタ数) ※28⇒14⇒7
num_units1 = 7*7*num_filters2

# h_pool2をnum_units1個のピクセル値を一列に並べた1次元リストへ変換する
h_pool2_flat = tf.reshape(h_pool2, [-1, num_units1])

# 隠れ層のノード数
num_units2 = 1024

# 縦、横が紛らわいい。XWの行列計算思い浮かべること。
#                                     入力の次元数, 層のノード数
w1 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b1 = tf.Variable(tf.constant(0.1, shape=[num_units2]))
hidden1 = tf.nn.relu(tf.matmul(h_pool2_flat, w1) + b1)

# ドロップアウト層である。
keep_prob = tf.placeholder(tf.float32)
hidden1_drop = tf.nn.dropout(hidden1, keep_prob)

# 最後にソフトマックス関数を適用する。
w2 = tf.Variable(tf.zeros([num_units2, 10]))
b2 = tf.Variable(tf.zeros([10]))
p = tf.nn.softmax(tf.matmul(hidden2_drop, w2) + b2)



◆誤差関数、トレーニングアルゴリズム、正解率の定義
定義した数式のパラメータの良し悪しを判断する誤差関数と、それを最小にするパラメータを決める。

コードを見てから説明する。

t = tf.placeholder(tf.float32, [None, 10])

loss = -tf.reduce_sum(t * tf.log(p))

train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)

correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


tは正解部分にビットが立っている10次元のデータである。
下の例の一行目であれば、答えは手書きの2という数字を表す。
t =
[[0, 0, 1, 〜, 0],
       ・
       ・
       ・        ]]

次に誤差を求める。誤差関数として有名かつ直感的に理解が容易なのは最小二乗法だろうか。TensorFlowでももちろん利用できる。
loss = tf.reduce_sum(tf.square(y-t))
しかし最小二乗法では推定した関数から値を予測する確率を最大化する情報を得られない。そこで最尤(さいゆう)推定法を使っている。

誤差を最小にするパラメータを決定する処理には、勾配降下法によるトレーニングアルゴリズムを使う。学習率のディフォルト値は明示的に0.0001を使っている(AdamOptimizer部分)。
トレーニングアルゴリズムにより学習率は動的に調整されるが、ディフォルトの初期値(0.001)がふさわしいとも限らない。学習率が大きいと、パラメータの最適化処理にかかる時間が短くなるが、パラメータが収束せず発散することがある。

argmaxは配列の行、または列内での最大値を取り出す関数である。
1を指定すると横方向、つまり行単位での検索となる。
そして予測と実データの比較をしている。

最終行は、castでbool値を1、0に変換して、reduce_meanによる平均の計算で正解率を求めている。


2016年9月25日日曜日

日本語文字列の自然なソート

手元にあった海外製のJavaで書かれたソフトで日本語の文字列をソートすると、
漢字部分が無秩序、訳わからない並びになった。
Unicodeの並びに沿わせていないからであろうか?

※JavaはUnicodeの文字コードでUTF-16の符号化方式である。
 つまり、StringクラスはUTF-16形式で扱うわけである。
 char型は符号なし16ビット整数である。Characterクラスはこのラッパ。

【参考:一般的な文字コード(文字集合)と符号化方式】
 -文字コード-   -符号化(エンコード)方式-
 JIS X 0201   8bit符号
              7bit符号

 JIS X 0208   EUC-JP

              ISO-2022-JP
              Shift_JIS

 Unicode      UTF-16

              UTF-32
              UTF-8


しかし、ASCII部分、ひらがな、カタカナ、は問題なく、Unicodeに沿っているように見える。なぜ漢字にだけ違和感を感じるのだろうか。
少し調べたところ、Unicodeの漢字は部首順に並べられているようである。規則性を感じなかったのはそのためである。部首単位でまとまるためそれが統一性を感じることも逆にあるだろうが。

国や言語固有の文化を反映した自然だと思われる並びにするにはJavaであればjava.text.Collatorクラスを使うと解決することが分かった。日本語に限らず、他言語にも対応している。英語圏発のソフトウェアはこのあたりへ考え及ばせること難しいかもしれないが考慮入れていただきたい点の一つである。



(脱線1)
文字列比較をする際にはCollator#compare()メソッドを使えばいいが、繰り返しソートする場合は性能面からCollationKeyを利用したビット単位比較をさせた方がいい。

(脱線2)
Javaと文字列の話ついでに、もう一つ。
charの単位は1文字ではない。適切な区切りを得るためにはBreakIteratorクラスあたりを使うこと。

なぜかというと、UTF-16はBMP(Basic Multilingual Plane)以外の面の文字を表すためにサロゲートペア(surrogate pair)や、あと複数のパーツで1文字を表現する結合文字を存在しているからである。濁点、半濁点との結合などである。

(脱線3)
Rubyは少しおもしろい取り組みをしている。

JavaやPythonなどはシステムの内部コードをUnicodeの文字コードに統一するUCS(Universal Code Set)方式を取っている。そのためUnicodeに含まれない文字を扱いたければ、ライブラリを使うか、自力でchar単位で処理するしかない。Unicodeだけで問題あるのかというと、このセットに含まれない文字は実は結構あるらしい。

一方、Rubyは8bitの列に符号化方式の情報をセットにしたものを文字列だと定義する、CSI(Code Set Independent)方式を採用している。文字コードはUnicodeではなく、ただのバイナリでしかない。どのような文字コードでも直接保存できるが、実装、最適化あたりの処理は複雑にならざるを得ないだろうと容易に想像できる。
ちなみに、Ruby2系のディフォルトのエンコーディング方式はUTF-8である。


〜参考〜
http://ruby-doc.org/core-2.3.1/String.html
http://magazine.rubyist.net/?0025-Ruby19_m17
http://d.hatena.ne.jp/nishiohirokazu/20141107/1415286729