Mechanize(2.5.1) のエンコーディング周りに関するメモ
Mechanize#get などによって取得されたリソースは、Mechanize::Page のインスタンスとして返される。その際の Mechanize::Page の初期化プロセスの中で、取得したリソースの文字エンコーディングを推定している。
エンコーディング候補
以下で得られるエンコーディングを、エンコーディング候補として @encodings に格納する。
ボディのデータから推定する
- Mechanize::Util.detect_charset(body)
NKF.guess を利用して文字エンコーディングを推定する。
推定できた場合は ISO-2022-JP, EUC-JP, SHIFT_JIS, UTF-8, UTF-16, UTF-32 の様なエンコーディング文字列が得られる。
推定できなかった場合はデフォルト値として ISO-8859-1 を得る。
レスポンスヘッダから推定する
- Mechanize::Page.response_header_charset(response)
Content-Type ヘッダの値に charset という文字列が含まれている場合に、その値部分を取り出す。取り出した値が none という文字列であった場合は、取り出せなかったものとして扱う。
force_encoding
String#force_encoding が使える場合、ボディに対して ASCII-8BIT で force_encoding しておく。
body.force_encoding 'ASCII-8BIT' if body.respond_to? :force_encoding
meta 要素(1)
- Mechanize::Page.meta_charset body
charset 属性を持つ meta 要素があれば、その属性値をエンコーディングとして取り出す。
charset 属性を持つ meta 要素がなければ、http-equiv 属性値が content-type である meta 要素を探す。見つかればその content 属性値を取り出し、その中から charset= に続く部分をエンコーディングとして取り出す。
class << self def charset content_type charset = content_type[/;(?:\s*,)?\s*charset\s*=\s*([^()<>@,;:\\\"\/\[\]?={}\s]+)/i, 1] return nil if charset == 'none' charset end alias charset_from_content_type charset end def self.meta_charset body # HACK use .map body.scan(/<meta .*?>/i).map do |meta| if meta =~ /charset\s*=\s*(["'])?\s*(.+)\s*\1/i then $2 elsif meta =~ /http-equiv\s*=\s*(["'])?content-type\1/i then meta =~ /content\s*=\s*(["'])?(.*?)\1/i m_charset = charset $2 if $2 m_charset if m_charset end end.compact end
default_encoding
Mechanize#default_encoding がセットされていれば、それもエンコーディング候補としておく。
エンコード(パース)
以下は Mechanize::Page#parser のソース。
def parser return @parser if @parser return nil unless @body if @encoding then @parser = @mech.html_parser.parse html_body, nil, @encoding elsif mech.force_default_encoding then @parser = @mech.html_parser.parse html_body, nil, @mech.default_encoding else @encodings.reverse_each do |encoding| @parser = @mech.html_parser.parse html_body, nil, encoding break unless encoding_error? @parser end end @parser end
Mechanize::Page#encoding は内部で Mechanize::Page#parser.encoding を実行している。つまり、エンコーディングは最終的にHTMLパーサによって決められる。
ケース(1)
Mechanize::Page の @encoding は、Mechanize::Page#encoding= で指定する事が出来る(標準ではnil)。これで指定していると、そのエンコーディングを使用する事になる。
@parser = @mech.html_parser.parse html_body, nil, @encoding
ケース(2)
Mechanize#force_default_encoding を真にしている場合、Mechanize#default_encoding をエンコーディングとして強制する。
@parser = @mech.html_parser.parse html_body, nil, @mech.default_encoding
ケース(3)
ケース1,2に該当しない場合、先に推定しておいたエンコーディング候補で順にパースを試し、エンコーディングによるエラーが発生しなければそれが正しいエンコーディングであるとする。
@encodings.reverse_each do |encoding| @parser = @mech.html_parser.parse html_body, nil, encoding break unless encoding_error? @parser end
エンコーディングによるエラーが発生しているかは以下で判断する。
def encoding_error?(parser=nil) parser = self.parser unless parser return false if parser.errors.empty? parser.errors.any? do |error| error.message =~ /(indicate\ encoding)| (Invalid\ char)| (input\ conversion\ failed)/x end end
まとめ
Mechanize のエンコーディング周りについてざっくりと見てみました。特にまとめられる事もありませんが、あえてあげるとすれば「Nokogiriによるパースが文字コード関係で失敗すると、別のエンコーディングとして処理される事がある」という事でしょうか。
UTF-8のHTMLがあるとして、その中でUTF-8ではない文字が埋め込まれている場合、そのHTMLをMechanizeに処理させると ISO-8859-1 として扱われたりします。
他にも、内部的に iconv に依存しているため、iconv で扱えないエンコーディングは扱えない等といった問題もあった気がしますが、そこは整理できていません。
(OSX 10.6 の iconv が Windows-31J に対応していなくて例外終了するなど)
リーダブルコード
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- 作者: Dustin Boswell,Trevor Foucher,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/06/23
- メディア: 単行本(ソフトカバー)
- 購入: 3人 クリック: 550回
- この商品を含むブログ (17件) を見る
コードに関する名著を咀嚼できている人は読まなくても大丈夫かもしれませんが、自分のように人に読ませられるコードを書く自身が無い人は読んで損は無いと思います。
デザインパターンやアルゴリズムといったテクニカルな話では無く、作文の作法的な話がほとんどだと思います。読みやすいコードとは、読みやすいコードを書くためにどうすれば良いのか。
230ページ程度で内容も軽いのでさくっと読めて良い感じです。
Rubyでデバッグモードの時だけ実行されるメソッドってあるんだっけ?
ruby -d で実行して $DEBUG が真になった時にだけブロックを実行するメソッド、的なものはあるんでしたっけ?
if $DEBUG def debug yield if block_given? end else def debug end end debug { puts 'x' }
こんな感じのやつ。
※ ついでに、こういうやつにふさわしい名前ってなんですかね?
追記
trace という名前はありなのかな?tracer と混同するかな。
if $DEBUG def trace if block_given? result = yield unless $stdout.closed? $stdout.write "%s\n" % result.inspect end end end else def trace end end trace { puts 'x' }
世間のやり方を調べようにも、どう検索したものか分からない…。
MySQLの全文検索を全く理解してないのでさわりだけでも調べておく
FULLTEXT INDEX として作成したインデックスを利用した検索で、WHERE 句の MATCH(カラム名) AGAINST('フレーズ') を使って検索条件を指定する。
- http://dev.mysql.com/doc/refman/5.1/en/fulltext-search.html
- http://dev.mysql.com/doc/refman/5.1/ja/fulltext-search.html
よく分からなかったのは、AGAINSTに与える検索フレーズを使ってどのように検索処理が実行されるのか。
ドキュメントから、以下の検索修飾子(search_modifier)で検索方法を指定出来る事はすぐに分かりました。
- IN BOOLEAN MODE
- IN NATURAL LANGUAGE MODE
- IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
- WITH QUERY EXPANSION
悩んだのは、search_modifier を省略した場合(デフォルト)の挙動。
答えを先に書いてしまうと、"IN NATURAL LANGUAGE MODE"がデフォルトなわけですが、普通にドキュメントに書いてあります。なんで見落としたんでしょうかね。
Full-text searches are natural language searches if the IN NATURAL LANGUAGE MODE modifier is given or if no modifier is given. For more information, see Section 12.9.1, “Natural Language Full-Text Searches”.
http://dev.mysql.com/doc/refman/5.1/en/fulltext-search.html
全文検索は、IN NATURAL LANGUAGE MODE 修飾子が与えられている、または修飾子がまったくない場合は、自然言語検索になります。
http://dev.mysql.com/doc/refman/5.1/ja/fulltext-search.html
自然言語モードのドキュメントは以下(日本語訳だと専用ページがない)。
- http://dev.mysql.com/doc/refman/5.1/en/fulltext-natural-language.html
- http://dev.mysql.com/doc/refman/5.1/ja/fulltext-search.html
自然言語モードでは、検索フレーズとの関連性に応じて結果を返す(関連性が高い方が上位に来る)とのこと。デフォルトではcase-insensitiveで、binary collationを利用すればcase-sensitiveにできる。
「関連性」について詳しく説明出来るほどの理解はないけど、単純な単語のAND/ORではないという程度は把握できました。
RubyのCSVパーサを追試
- FasterCSVは改行を含まない一行のレコードは十分高速に解析できる
- FasterCSVはレコード区切りがCRLFで、カラムに含む改行がLFであれば十分高速に解析できる
- LightCsvが安定して高速(速度のみ確認)
- CSVScanも安定して高速(速度のみ確認)
Ruby の CSV.parse_line が遅いケース
改行が多いとパースに時間がかかるらしい。
どこで時間がかかっているかなど、ソースは見てない。
旧CSVは安定している様子。
1.8.7-p358, FasterCSV 1.5.5
# -*- coding: utf-8 -*- require 'benchmark' require 'rubygems' require 'fastercsv' require 'csv' flat = (1..100).to_a.map{|n| %Q|"#{Array.new(100, 'あ').join(' ')}"| }.join(',') multi = (1..100).to_a.map{|n| %Q|"#{Array.new(100, 'あ').join("\n")}"| }.join(',') Benchmark.bm do |b| b.report { 10.times{ FasterCSV.parse_line flat } } b.report { 10.times{ FasterCSV.parse_line multi } } b.report { 10.times{ CSV.parse_line flat } } b.report { 10.times{ CSV.parse_line multi } } end __END__ user system total real 0.040000 0.000000 0.040000 ( 0.035945) 171.650000 1.240000 172.890000 (172.914305) 0.460000 0.000000 0.460000 ( 0.464624) 0.470000 0.000000 0.470000 ( 0.464264)
1.9.3-p194
# -*- coding: utf-8 -*- require 'benchmark' require 'csv' flat = (1..100).map{|n| %Q|"#{Array.new(1000, 'あ').join(' ')}"| }.join(',') multi = (1..100).map{|n| %Q|"#{Array.new(1000, 'あ').join("\n")}"| }.join(',') Benchmark.bm do |b| b.report { 10.times{ CSV.parse_line flat } } b.report { 10.times{ CSV.parse_line multi } } end __END__ user system total real 0.160000 0.000000 0.160000 ( 0.164214) 20.790000 0.020000 20.810000 ( 20.813151)
OSX Lion で iconv を使って WINDOWS-31J のデータを扱いたい
とある用途で Ruby の Mechanize を使う事になったわけですが、Mechanize は内部で Iconv を使用してエンコーディング変換を行っています。
なので、WINDOWS-31J なページを扱おうとした場合に、iconv が WINDOWS-31J に対応していないと困ってしまうわけです(例外終了してしまう)。
そして、OSX Lion 組み込みの iconv は WINDOWS-31J という名前のエンコーディング(エイリアス)はサポートしていない様です。
どう対応するのが良いのか分からなかったので、とりあえず rvm のパスにパッチを当ててコンパイルした iconv をインストールしてみました。
- http://www.gnu.org/software/libiconv/
- http://www2d.biglobe.ne.jp/~msyk/software/libiconv-1.13-ja-patch.html
$ curl -s http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.13.tar.gz | gzip -dc | tar xvf - $ cd libiconv-1.13 $ curl -s https://trac.macports.org/export/89276/trunk/dports/textproc/libiconv/files/patch-Makefile.devel | patch -p1 $ curl -s https://trac.macports.org/export/89276/trunk/dports/textproc/libiconv/files/patch-utf8mac.diff | patch -p1 $ cat <<EOF | patch -p1 diff --git a/lib/flags.h b/lib/flags.h index d7cda21..4cabcac 100644 --- a/lib/flags.h +++ b/lib/flags.h @@ -14,6 +14,7 @@ #define ei_ascii_oflags (0) #define ei_utf8_oflags (HAVE_ACCENTS | HAVE_QUOTATION_MARKS | HAVE_HANGUL_JAMO) +#define ei_utf8mac_oflags (HAVE_ACCENTS | HAVE_QUOTATION_MARKS | HAVE_HANGUL_JAMO) #define ei_ucs2_oflags (HAVE_ACCENTS | HAVE_QUOTATION_MARKS | HAVE_HANGUL_JAMO) #define ei_ucs2be_oflags (HAVE_ACCENTS | HAVE_QUOTATION_MARKS | HAVE_HANGUL_JAMO) #define ei_ucs2le_oflags (HAVE_ACCENTS | HAVE_QUOTATION_MARKS | HAVE_HANGUL_JAMO) EOF $ curl -s http://www2d.biglobe.ne.jp/~msyk/software/libiconv/libiconv-1.13-ja-1.patch.gz | gzip -dc | patch -p1 $ ./configure --enable-extra-encodings --prefix=$rvm_path/usr $ make -f Makefile.devel $ make check $ make install $ rvm reinstall 1.8.7-p174 --with-iconv-dir=$rvm_path/usr $ $rvm_path/usr/bin/iconv -l | grep -i windows-31j CP932 MS932 SHIFT_JIS-MS SJIS-MS SJIS-OPEN SJIS-WIN WINDOWS-31J WINDOWS-932 CSWINDOWS31J $ ruby -riconv -e 'puts Iconv.conv("Windows-31J", "UTF-8", [ 0x301C ].pack( "U*" ))' | nkf -Sw 〜
使えているように見えなくもない。