Ruby : Nokogiriのパースでエンコーディングをnilに設定したとき

2020/05/22　追記
この投稿の結論は誤っています。詳しくは[こちら](https://qiita.com/tetoralynx/items/ae7bb1d5fd39657fd123)。

#はじめに
`Nokogiri::HTML.parse`メソッドは、第三引数にエンコーディングを指定することができます。（UTF-8、Shift_JISなど）
しかし実はこのエンコーディングの引数を`nil`に設定しても、エラーとなることなくhtmlはパースされます。
このときnokogiriはどの文字コードでエンコーディングしているのか気になったのでまとめました。

#エンコーディングをnilに設定したとき
エンコーディングをnilに設定したときの文字コード判別について、公式含めいくつかのサイトに書かれていました。
>charsetを指定しない場合、文字コードはlibxml2の自動判別に頼ることになります（Nokogiriのドキュメント）。「勝手にUTF-8として解釈される」ということではありません。
[Nokogiri利用時のcharset=nilについて](https://teratail.com/questions/111664)

>Strings are always stored as UTF-8 internally. Methods that return text values will always return UTF-8 encoded strings. Methods that return XML (like to_xml, to_html and inner_html) will return a string encoded like the source document.
[Nokogiri公式ドキュメント](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html)

Nokogiriでは、基本的にメソッドの文字コードは「UTF-8」で返され、XMLを返すようなメソッドは、ページソースの文字コードを参照すると書かれています。
このときのページソースの文字コード判別は、「libxml2」というライブラリが担っているようです。

#検証その1
本当に文字コードが判別できているか、実際のページのタイトルを取得して出力する簡単なスクレイピングを行って検証してみます。
「無印良品」と「zozotown」の2つのページで行ってみました。

- 無印良品トップページの場合

無印良品トップページは「UTF-8」で書かれています。
![Screenshot from 2020-05-19 18-04-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636023/c594986e-afd5-d5bc-2ad1-ee16b9ee55bd.png)


まずは文字コードを「UTF-8」で明示して出力してみます。

```mujirushi1.rb
require 'nokogiri'
require 'open-uri'

url = 'https://www.muji.com/jp/'
html = open(url).read

page = Nokogiri::HTML.parse(html, nil, 'utf-8')

puts page.title
```

```mujirushi1.rbの結果
$ ruby mujirushi1.rb
無印良品
```
きちんとタイトルが表示されています。
もちろん指定する文字コードを「Shift_JIS」などにすると文字化けします。

ここで文字コードを`nil`としてみます。

```mujirushi2.rb
require 'nokogiri'
require 'open-uri'

url = 'https://www.muji.com/jp/'
html = open(url).read

page = Nokogiri::HTML.parse(html, nil, nil)

puts page.title
```

```mujirushi2.rbの結果
$ ruby mujirushi2.rb
無印良品
```
どうやら`nil`とした場合でも正常にタイトルが表示できているようです。

- zozotownトップページの場合

zozotownトップページは「Shift_JIS」で書かれています。
![Screenshot from 2020-05-18 19-00-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636023/cff70319-53b7-66f0-3397-e76c0b15ca2c.png)

文字コードを「Shift_JIS」で明示的に指定するときちんとタイトルが表示されます。（無印良品と同様なので割愛）

文字コードを`nil`としてみると

```zozotown.rb
require 'nokogiri'
require 'open-uri'

url = 'https://zozo.jp/'
html = open(url).read

page = Nokogiri::HTML.parse(html, nil, nil)

puts page.title
```

```zozotown.rbの結果
$ ruby zozotown.rb
ファッション通販ZOZOTOWN
```
zozotownのページでも正常にタイトルが出力されていることが確認できました。

#検証その2
今度は実際のページではなく、自前のhtmlでパースはどうなるのか調べてみました。

最初は「UTF-8」で書かれたhtmlです。

```hello_utf8.html
<!DOCTYPE html>

<html>
  <head>
    <title>こんにちわ</title>
    <meta charset="UTF-8">
  </head>
  <body>
  </body>
</html>
```
文字コードを「UTF-8」と明示してやれば、「こんにちわ」と表示されます。

文字コードを`nil`にして取得してみると、

```utf8.rb
require 'nokogiri'

html = open('hello_utf8.html').read
page = Nokogiri::HTML.parse(html, nil, nil)

puts page.title
```

```utf8.rb結果
$ ruby utf8.rb
こんにちわ
```
正常に取得できています。

次は「Shift_JIS」で書かれたhtmlです。

```sjis.html
<!DOCTYPE html>

<html>
  <head>
    <title>こんにちわ</title>
    <meta charset="Shift_JIS">
  </head>
  <body>
  </body>
</html>
```
こちらは「Shift_JIS」で文字コードを明示すれば「こんにちわ」と表示されます。

それではエンコーディング`nil`で取得してみます。

```sjis.rb
require 'nokogiri'

html = open('hello_sjis.html').read
page = Nokogiri::HTML.parse(html, nil, nil)

puts page.title
```

```sjis.rbの結果
$ ruby sjis.rb
±��ɂ߂�
```
文字化けしました。
さらにこのときNokogiriはmetaタグのcharsetを参照しているというようにも見えません。

確認のため、「UTF-8」でエンコーディングを指定してタイトルを取得してみます。

```sjis2.rb
require 'nokogiri'

html = open('hello_sjis.html').read
page = Nokogiri::HTML.parse(html, nil, 'utf-8')

puts page.title
```

```sjis2.rbの結果
$ ruby sjis2.rb
±��ɂ߂�
```

文字化けしました。
文字化けした文字で判別するのでアレですが、エンコーディングを`nil`にすると「UTF-8」でエンコーディングして取得した結果と同じになるように見えます。



#ここまでのまとめ

- 無印良品（utf-8）で`nil` -> 正常
- zozotown（shift_jis）で`nil` -> 正常
- 自前html（utf-8）で`nil` -> 正常
- 自前html（shift_jis）で`nil` -> 文字化け

#さらに検証
上の検証結果を見るとわかりますが、「zozotown」と「自前html(shift_jis）」では、同じ「Shit_JIS」なのに結果が違っています。
そこで最後の検証として「zozotown」のhtmlを全コピーして、ローカルのファイルに保存し、Nokogiriでエンコーディング`nil`でタイトルを取得してみます。

「zozotown」のソースコード（ctrl + u）を全コピーして「zozo.html」という名前でローカルに保存しています。

```zozo.rb
require 'nokogiri'

html = open('zozo.html').read
page = Nokogiri::HTML.parse(html, nil, nil)

puts page.title
```

```zozo.rbの結果
$ ruby zozo.rb
�t�@�b�V�����ʔ�ZOZOTOWN
```

文字化けしました。
ちなみに「UTF-8」で明示的にエンコーディングすると、同じような文字化けを起こしました。

#総まとめ
- web上のurlからのhtmlを、エンコーディング`nil`でパースすると、Nokogiriが*良しなに*文字コードを判別してエンコーディングしている（ように見える）
- このときにNokogiriはmetaタグのcharsetを参照しているわけでもなさそう
- エンコーディング`nil`でパースすると、Nokogiriは基本的に「UTF-8」でエンコーディングしているっぽい

#さいごに
公式サイトにも書かれていますが、エンコーディング`nil`のときにソースから文字コードを自動判別する「libxml2」は、100%文字コードを判別できるわけではないようです。
> Remember that data is just a stream of bytes. Only us humans add meaning to that stream. Any particular set of bytes could be valid characters in multiple encodings, so detecting encoding with 100% accuracy is not possible. libxml2 does its best, but it can't be right 100% of the time.

> If you want Nokogiri to handle the document encoding properly, your best bet is to explicitly set the encoding. Here is an example of explicitly setting the encoding to EUC-JP on the parser:

ソースの文字コードがわかっているのであれば、きちんと明示してパースするのが無難かと思われます。

「libxml2」がどのように文字コードを判別しているのかは要調査です。