プログラマでありたい

おっさんになっても、プログラマでありつづけたい

AppleTVのYoutubeにログインできない場合の傾向と対策

 息子がプラレールで遊んでいるうちに、すっかりと鉄ちゃん予備軍になりました。来る日も来る日もプラレールで遊び、またある時はYoutubeで電車の映像を眺めています。そんな訳でいちいち検索するのも面倒くさいので、息子が利用する用のアカウントを作って幾つかのチャンネルを登録しています。おかげ様で、ますます電車漬けの毎日です。


 そんなこんななので、さすがに少し制限したくてiPadの利用を控えさせています。もうちょっとコントロールし易いテレビとAppleTVでYoutubeを見せようとしました。そしたら、Youtubeにログインできないのです。出てきたのは、下記のような無慈悲なメッセージです。

Apple TV ではこのアカウントを使用できません。別のアカウントでもう一度やり直してください。


 調べてみたところ、AppleTVでYoutubeにログインできないケースは、2つの理由があるようです。

  1. Googleアカウントを、2要素認証の設定をしている
  2. Youtubeで自分のチャンネルを作っていない

 一つ目の2要素認証ですが、AppleTVの方が対応していません。その為、AppleTV用のアプリ固有パスワードを発行して、そのパスワードを設定する必要があるようです。何となく納得できますね。


 二つ目のチャンネルの件ですが、ハッキリ言って訳が解りません。ネットで幾つかの事例を見ていると、それで解決したというのが幾つかありました。半信半疑で、PCの方でYoutubeにログインして自分のチャンネルを作成しました。その後、AppleTVでログインすると、何事もなくログインできるようになりました。


 ただのバグだろうと思いますが、Googleのアカウント統合が上手くいっていない一例なんでしょうね。

FireFoxやChromeを使って任意のノードのXPathを簡単に抽出する方法について

 「Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath」というエントリーで、Nokogiriの使い方に絡めてXPathの記述をちょこっと書きました。XPathは、XMLやHTMLのノードを指定する為の構文です。XPathを使うことにより、HTML中のタグも一意に指定することが出来ます。例えば、titleタグをXPathで指し示すのであれば、ルートノードであるhtmlタグの下のheadタグの下のtitleタグという構造から/html/head/titleといった表記になります。複数ある要素の場合、img[1]といった配列で指定することも可能なので、基本的には全てのタグを指定出来ます。XPathは色々なところで使われるようになっているので、スクレイピングに限らず覚えておいたら便利です。
 しかし、問題が一点あります。目的のノードのXPathを、どうやって抽出するのか。XPathは、基本的には上からタグを辿っていくと解ります。ただし、昨今の複雑怪奇なHTMLを根気よく辿っていける人は、違う職業を探した方が良いと思います。FireFoxやChromeを使うと、選択しているノードのXPathを簡単に抽出できます。知っている人にとっては当たり前ですが、知らなかった人には革命的なので紹介しておきます。

ChromeでXPathの抽出する方法



  1. 目的のノード(場所)を選択している状態で、右クリックを押します。
  2. メニューの中から「要素の検証」を選択します。
  3. 選択されているノード(青くなっている部分)の上で右クリックを推して、「Copy XPath」をクリックします。

 この3つの手順だけで、クリップボードにXPathがコピーされます。
f:id:dkfj:20140419130710p:plain


 上記の例は、Amazon.co.jpの個別商品ページから、価格を抜き出している例です。コピーされたXPathは、次のようになっています。

//*[@id="actualPriceValue"]/b


 Chromeで抜き出すXPathは、目的のノードに一番近いID属性からの相対パスです。実際のHTMLは、下記のようなものです。(インデント等を多少修正しています。)

<td id="actualPriceContent">
	<span id="actualPriceValue">
		<b class="priceLarge">¥ 3,024  </b>
	</span>
    <span id="actualPriceExtraMessaging">    
	    <span></span>
		<b>通常配送無料</b>
		<a href="/gp/help/customer/display.html/ref=mk_sss_dp_1?ie=UTF8&amp;nodeId=642982&amp;pop-up=1" target="AmazonHelp" onclick="return amz_js_PopWin('/gp/help/customer/display.html/ref=mk_sss_dp_1?ie=UTF8&amp;nodeId=642982&amp;pop-up=1','AmazonHelp','width=550,height=550,resizable=1,scrollbars=1,toolbar=0,status=0');">詳細</a>
	</span>
</td>

 目的の情報である価格はBタグで囲まれ、その一つ上がSpanタグということが解ります。このSpanタグには、actualPriceValueというID属性がついています。HTMLにおけるID属性は、文章内で要素を一意に識別するための識別子です。取得したXPathは一意の要素であるactualPriceContent属性をもつSpanタグの下のBタグということを示しています。

FireFoxでXPathの抽出する方法



 FireFoxの場合も、ほぼ同様の操作で取得できます。ただし前提として、FireBugをインストールしている必要があります。FireBugはアドオンなので、FireFoxのメニューのツール->アドオンから「アドオンの入手」の検索窓でFireBugを検索すれば簡単にインストールできます。インストール後は下記の手順でXPathを取得できます。

  1. 目的のノード(場所)を選択している状態で、右クリックを押します。
  2. メニューの中から「FireBugで要素を調査」を選択します。
  3. 選択されているノード(青くなっている部分)の上で右クリックを推して、「XPathをコピー」をクリックします。

f:id:dkfj:20140419132730p:plain

FireBugのConsoleの$x関数



 FireFoxでXPathを検証する方法があります。FireBugには、幾つもの組込み関数が用意されています。その中の一つに、$x関数というものがあります。これは、任意のXPath要素を取得するというものです。使い方は、FireBugのコンソールタブから一番下の「>>>」と表示されているところで、$x("//title")といった感じで、XPathを記述します。指定に間違いがなければ、該当のノードが取得されます。

f:id:dkfj:20140419134527p:plain

ちゃんとXPathを指定したつもりなのに上手くいかない場合は、これを使って検証すると良いと思います。またFireBugには、これ以外にも便利な機能は沢山あります。いつかまとめてみようと思います。

まとめ



 考えてみれば、ブラウザはHTMLの構文解析を行っている訳なので、上記のようなことを把握しているのは当たり前なんですね。ということで、XPathに迷ったらブラウザを使いましょう。


See Also:
Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath - プログラマになりたい
Ruby製のクローラー Anemoneの文字化け対策
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた

Rubyによるクローラー開発技法

Rubyによるクローラー開発技法

Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath

f:id:dkfj:20140415021125p:plain

 RubyでHTMLやXMLをパースする構文解析ツールの定番は、Nokogiriです。スクレイピングする際の必需品で、なくてはならないモジュールの1つです。ただ色々なことが出来る反面、どこから取りかかれば良いのか解り難い部分もあります。自習を兼ねて、Nokogiri概要と主要な機能を紹介してみます。

Nokogiriとは何か?



 ReademeによるとNokogiriとは、「HTMLとXMLとSAXとXSLTとReaderのパーサー」で、特徴としては、XPathとCSS3セレクター経由で探索する機能を持つことのようです。他にもHTMLやXMLのビルダーの機能を持っていますが、HTMLとXMLのパーサー(構文解析器)と覚えておけばよいでしょう。

Nokogiriのクラス構造



 Nokogiriは、なかなか巨大なライブラリです。10以上のモジュールと70以上のクラスで構成されていて、yardでダイアグラム図を作ってみると下記のように壮大なものになります。

f:id:dkfj:20140413193256p:plain

 正直、どこから見ていけばよいのか途方に暮れると思います。Nokogoriを使う前に、次のことだけ覚えてください。何となく頭に入りやすくなると思います。

3つのクラスをおさえる

 Nokogiriを理解する上では、Nokogiri::XML::Nodeのメソッドと挙動を覚えるのが大事です。ダイアグラム図を見れば解るように、Nokogiri::XML::Nodeを継承しているオブジェクトが多数あります。また、Nokogiri::HTML::Document < Nokogiri::XML::Documentの関係から解るように、HTMLモジュールもXMLモジュールを継承しているものが多数あります。

Nokogiri::HTML::Document < Nokogiri::XML::Document < Nokogiri::XML::Node

 挙動が解らなければ、Nokogiri::XML::Nodeのソースをじっくり読みとくと良いです。あとは、Nokogiri::XML::DocumentとNokogiri::HTML::Documentの2つです。この3つのクラスをおさえておくと、Nokogiriが出来ることが大体把握できます。

http://nokogiri.org/Nokogiri/XML/Node.html
http://nokogiri.org/Nokogiri/XML/Document.html
http://nokogiri.org/Nokogiri/HTML/Document.html

NokogiriでHTMLを解析する



 一番基本的なHTMLの解析は以下の通りです。open-uriはrubyの組み込みライブラリであるKernel#openを再定義したもので、ファイルと同様の操作でhttp/ftpにアクセスすることができます。Nokogiriと一緒に利用すると便利です

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(open('http://www.yahoo.co.jp'))

Nokogiri::HTMLもしくはNokogiri::HTML#parseを呼び出すと、内部的にNokogiri::HTML::Document#parseメソッドを実行します。返り値としては、Nokogiri::HTML::Documentを返します。下記と同じです。

doc = Nokogiri::HTML.parse(open('http://www.yahoo.co.jp'))

 生成したNokogiri::HTML::Documentから、特定のタグを抽出するには下記のとおりです。

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(open('http://www.yahoo.co.jp'))
title = doc.xpath('/html/head/title')
objects = doc.xpath('//a')

NodeとNodeSet



 XPathやcssの検索結果は、Nokogiri::XML::NodeSetを返します。NodeSetは、Nokogiri::XML::Nodeのリストです。NodeSetのメソッドの1つであるNodeSet#inner_text()は、リスト内の全てのNodeのinner_textを返します。textは、inner_textのエイリアスです。HTMLのTitleタグのようにドキュメント中に1つしかないタグの場合は、下記のように全て同じ結果を返します。また、HTML::Documentには、Titleタグを抜き出す特別のメソッドがあります。

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(open('http://www.yahoo.co.jp'))

puts doc.title # => Yahoo! JAPAN

nodesets = doc.xpath('//title')
puts nodesets.text # => Yahoo! JAPAN 
puts nodesets.inner_text # => Yahoo! JAPAN
puts nodesets.first.inner_text # => Yahoo! JAPAN

nodesets.each{|nodeset|
  puts nodeset.content() # => Yahoo! JAPAN
  puts nodeset.text # => Yahoo! JAPAN
  puts nodeset.inner_text # => Yahoo! JAPAN
}

 複数の要素がヒットする場合、結果は違ってきます

nodesets = doc.xpath('//a')
puts nodesets.inner_text

nodesets.each{|nodeset|
  puts nodeset.inner_text # => Yahoo! JAPAN
}

NodeとNodeSetの検索メソッド



 NodeとNodeSetには、様々な検索方法があります。検索に関しては、同じメソッドが利用可能です。下記の例は、全て同じ結果を返します。

puts doc%'//title'
puts doc/'//title'
puts doc.at('//title') # => 検索にヒットした最初のノードを返す
puts doc.at_xpath('//title') # => xpathの検索にヒットした最初のノードを返す
puts doc.at_css('title') # => cssの検索にヒットした最初のノードを返す
puts doc.css('title') # => cssで検索。NodeSetを返す
puts doc.css('title')[0] # => cssで検索。NodeSetから最初のノードを返す
puts doc.search('title') # => xpathかcssで検索。NodeSetを返す
puts doc.search('title')[0] # => xpathかcssで検索。NodeSetから最初のノードを返す
puts doc.xpath('//title') # => xpathで検索。NodeSetを返す
puts doc.xpath('//title')[0] # => xpathで検索。NodeSetから最初のノードを返す
puts doc.xpath('//title').first # => xpathで検索。NodeSetから最初のノードを返す

NodeとNodeSetの参照メソッド

 NodeとNodeSetは、参照に関してほぼ同じメソッドが使えます。一部NodeSetでは使えないメソッドがあります。また、同一の結果を返すメソッドの多くはエイリアスとして設定されているものです。

#Nodeの参照
#HTMLタグ含む
puts doc.at('//title').to_html
puts doc.at('//title').to_xhtml
puts doc.at('//title').to_xml
puts doc.at('//title').to_s

#HTMLタグで囲まれた文字列
puts doc.at('//title').text
puts doc.at('//title').inner_html
puts doc.at('//title').inner_text
puts doc.at('//title').text
puts doc.at('//title').to_str

#属性値の取得
puts doc.at('//a').[]('href')
puts doc.at('//a').attribute('href')
puts doc.at('//a').get_attribute('href')

#NodeSetの参照
#HTMLタグ含む
puts doc.xpath('//title').to_html
puts doc.xpath('//title').to_xhtml
puts doc.xpath('//title').to_xml
puts doc.xpath('//title').to_s

#HTMLタグで囲まれた文字列
puts doc.xpath('//title').text
puts doc.xpath('//title').inner_html
puts doc.xpath('//title').inner_text
puts doc.xpath('//title').text
#puts doc.xpath('//title').to_str

検索方法いろいろ



 NokogiriというかXPathの検索方法です。idやclassなどの属性値で検索することが多いですが、実は属性値であれば、なんでも使えます。属性値検索の場合は、[]で指定します。@部分が属性値の名前です。

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(open('http://www.hatena.ne.jp/'))

#class指定でh2タグを検索
puts doc.xpath("//h2[@class='title']")

#id指定でdivタグを検索
puts doc.xpath("//div[@id='copyright']")

#カスタムの属性値でdivタグを検索
puts doc.xpath("//div[@data-component-term='tweet']")

#id指定で全てのタグを検索
puts doc.xpath("//*[@id='copyright']")

#絞込検索
puts doc.xpath("//div[@id='copyright']//ul")

#リンク先のURLを抜き出す
doc.xpath('//a').each do |item|
  puts item[:href]
end

NodeSetなのかElementなのか



 検索結果から属性等を参照しようとして、次のようなエラーが出る場合があります。

`[]': no implicit conversion of String into Integer (TypeError)

 殆どの場合は、Elementを取得したと思っていたのに、NodeSetを取得していたというパターンが多いでしょう。よくある間違いとしては、一意の要素をとってきても、その下にタグがあってNodeSetになっている場合です。

 つまったら、取得したデータがNodeSetなのかElementなのかを確認しましょう。classで確認できます。

doc.xpath("//div[@data-component-term='tweet']").each { |tweet|
  puts tweet.class #=> Nokogiri::XML::NodeSet
  puts tweet[0].class #=> Nokogiri::XML::Element

NokogiriでXMLを解析する



 Nokogiriは、HTMLのみならずXMLも解析できます。XMLは構造もシンプルなので、解析はHTMLよりも簡単に出来ると思います。しかし、一点だけ注意が必要です。Nokogiriを使ってXPathで検索する場合、そのXMLが名前空間がある場合は必ず指定する必要があるという点です。指定しないと、全く検索にヒットしません。
 下記の例は、はてなのホットエントリーのRSSフィードを抽出する例です。RSS1.0なので名前空間を持ちます。

require 'nokogiri'
require 'open-uri'

url = 'http://feeds.feedburner.com/hatena/b/hotentry'
xml = open(url).read

doc = Nokogiri::XML(xml)

namespaces = {
  "rss" => "http://purl.org/rss/1.0/", #デフォルト名前空間
  "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
  "content" => "http://purl.org/rss/1.0/modules/content/",
  "dc" => "http://purl.org/dc/elements/1.1/",
  "feedburner" => "http://rssnamespace.org/feedburner/ext/1.0"
}

#channel
channel = doc.xpath('//rss:channel', namespaces)
#Xpathでtitleを検索
puts channel.xpath('rss:title', namespaces)
puts channel.xpath('feedburner:info', namespaces)
lis = channel.xpath('//rdf:li', namespaces)
lis.each {|li|
  puts li.attribute("resource")
}

NokogiriとHpricot



 Ruby製のHTML/XMLパーサーとしては、hpricotも選択肢の1つでした。しかしながら、今では公式に「Hpricot is over.」と宣言され、Nokogiriを使いましょうとなっています。Nokogiriとhpricotは、APIなど共通点が多いのですが、一点だけ大きな違いがあります。Nokogiriは内部の処理をUTF-8で行うに対して、hpricotは与えられた文字コードをそのまま扱い変換をしません。その為、文字コード絡みのトラブルを避ける為に、今でもhpricotを利用している人もいるようです。

XPathとCSS3セレクタのどちらを使うか



 Nokogiriのノードの探索は、主にXPathかCSS3セレクターを使う方法があります。両方共覚える必要はありません。どちらかの使い方を覚えれば充分です。CSSの方が得意であれば、CSS3セレクターを使いましょう。どちらも得意でないのであれば、比較的サンプルが多いXPathを使えば良いのでしょう。デザイナー系であればCSSの方が得意という人も多いでしょう。その場合は、CSSを使えばよいと思います。

Nokogiriの解析の実装



 NokogiriのXML,HTMLのパースは、基本的にはLibXML2に依存しています。内部のソースを見ていると、LibXML2に文句を言いつつ足りない機能を補完しようとしているのが解ります。Nokogiriとは極論すると、LibXML2のRubyのラッパーモジュールです。それを頭に入れておくと、HTMLでもXMLでも同じ考え方で解析しているのが解ります。

Nokogiriの各メソッドのエイリアス



 Nokogiriのちょっと取っ付きにくい理由として、同じ処理を様々なパターンで記述できるところにあります。色々なサンプル見ても、多種多様で初めての人は面食らいます。その一因となっているのが、メソッドのエイリアスの多さ。基本的には、自分が馴染んだ名前で使うのがよいでしょう。

$ grep -R alias *
css/parser_extras.rb:        alias :cache_on? :cache_on
css/parser_extras.rb:        alias :set_cache :cache_on=
css/tokenizer.rb:  alias :scan :scan_str
xml/attr.rb:      alias :value :content
xml/attr.rb:      alias :to_s :content
xml/attr.rb:      alias :content= :value=
xml/document.rb:      alias :to_xml :serialize
xml/document.rb:      alias :clone :dup
xml/document.rb:      alias :<< :add_child
xml/document_fragment.rb:      alias :serialize :to_s
xml/node/save_options.rb:        alias :to_i :options
xml/node.rb:      alias :/ :search
xml/node.rb:      alias :% :at
xml/node.rb:      alias :next           :next_sibling
xml/node.rb:      alias :previous       :previous_sibling
xml/node.rb:      alias :next=          :add_next_sibling
xml/node.rb:      alias :previous=      :add_previous_sibling
xml/node.rb:      alias :remove         :unlink
xml/node.rb:      alias :get_attribute  :[]
xml/node.rb:      alias :attr           :[]
xml/node.rb:      alias :set_attribute  :[]=
xml/node.rb:      alias :text           :content
xml/node.rb:      alias :inner_text     :content
xml/node.rb:      alias :has_attribute? :key?
xml/node.rb:      alias :name           :node_name
xml/node.rb:      alias :name=          :node_name=
xml/node.rb:      alias :type           :node_type
xml/node.rb:      alias :to_str         :text
xml/node.rb:      alias :clone          :dup
xml/node.rb:      alias :elements       :element_children
xml/node.rb:      alias :delete :remove_attribute
xml/node.rb:      alias :elem? :element?
xml/node.rb:      alias :add_namespace :add_namespace_definition
xml/node_set.rb:      alias :<< :push
xml/node_set.rb:      alias :remove :unlink
xml/node_set.rb:      alias :/ :search
xml/node_set.rb:      alias :% :at
xml/node_set.rb:      alias :set :attr
xml/node_set.rb:      alias :attribute :attr
xml/node_set.rb:      alias :text :inner_text
xml/node_set.rb:      alias :size :length
xml/node_set.rb:      alias :to_ary :to_a
xml/node_set.rb:      alias :+ :|
xml/parse_options.rb:      alias :to_i :options
xml/reader.rb:      alias :self_closing? :empty_element?
xml/sax/push_parser.rb:        alias :<< :write

まとめ



 イマイチまとまりませんでしたが、Nokogiriの使い方について自分なりに理解が深まったような気がします。Ruby使うのであれば、使いこなしたいライブラリの上位に入ると思います。少し時間を掛けてドキュメントやソースを読むことで理解は格段にあがるので、ぜひ一度お試しあれ!!


追記:
そんで、どうやってXPathを抽出するのという話を書きました
FireFoxやChromeを使って任意のノードのXPathを簡単に抽出する方法について - プログラマになりたい

追記2:
この辺りの話をまとめた、「Rubyによるクローラー開発技法」という本を出しています。


See Also:
Ruby製のクローラー Anemoneの文字化け対策
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた
takuros/anemone · GitHub


参照:
sparklemotion/nokogiri · GitHub
Tutorials - Nokogiri 鋸


Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

バッドデータハンドブック ―データにまつわる問題への19の処方箋

バッドデータハンドブック ―データにまつわる問題への19の処方箋

Google Adsenseのレスポンシブル広告ユニット(ベータ版)を、スマホで見たらひどかった件

 はてなブログに移行しまたので、スマホの方にGoogle Adsenseの設定をしてみました。幾つか方法を調べていたのですが、JavaScriptを使い自分でサイズに応じて広告の大きさを変える方法と、Googleが提供するレスポンシブル広告ユニット(ベータ版)を使う方法があるそうです。時代はレスポンシブルだということで、後者の方を選んでみました。(本音は、JavaScript書くのが面倒くさかったから。)

 そうしたら、ひどいのです。Googleの広告の配信に最適化されました。記事が見えんです。
f:id:dkfj:20140406214905p:plain 

Google Adsenseのレスポンシブル広告ユニット(ベータ版)の調整



 さすがにひどすぎたので、対策をちょっと調べてみました。Google Adsenseから広告コードを取得の際に、デフォルトの「スマートサイズ(推奨)」ではなく、「アドバンス(コード変更が必要)」を選ぶと調整ができるようです。
f:id:dkfj:20140406215336p:plain

 取得したコードのうち、下記の部分を整形します。
デフォルトでは320×50になっているようです。(どう見ても、300×250だったけど)

<style>
.my_adslot { width: 320px; height: 50px; }
@media(min-width: 500px) { .my_adslot { width: 468px; height: 60px; } }
@media(min-width: 800px) { .my_adslot { width: 728px; height: 90px; } }
</style>

 私は、次のように変えています。広告サイズのところを、Googleが出していないサイズにすると、違反になるそうなのでご注意を。

<style>
.my_adslot { width: 234px; height: 60px; }
@media(min-width: 500px) { .my_adslot { width: 468px; height: 60px; } }
@media(min-width: 800px) { .my_adslot { width: 728px; height: 90px; } }
</style>

 
 結果、このようになりました。まぁウザくならない程度なので、良しとします。カテゴリと広告が近いので、もうちょっと調整する予定です。
f:id:dkfj:20140406214931p:plain

まとめというか雑感



 昔、子供向けアプリの広告について文句を書いたことがありますが、読者や広告主の為にならない広告手法って淘汰されると思うのですね。最近のスマホの広告、誤クリックを増やす方向にいっている気がしてならないです。今回の件をみてると、Googleのようなプラットフォーム側の問題もあるのではと感じました。どうなんでしょうね?
※こんなこと書いていると広告外せやと言われますが、効果測定という意味で中々面白い指標なんですね。


追記:
スマホに気を取られて、PC版がサイズオーバーしてました。これは、完全に私のミス。
f:id:dkfj:20140406220847p:plain


See Also:
絵本作家という無理ゲーと、幼児向けスマフォ・タブレットアプリについて - プログラマになりたい

Ruby製のクローラー Anemoneの文字化け対策

 何度も取り上げていますが、Ruby製のクローラーであるAnemoneについてです。もう2年ほどメンテナンスされていないものの、Rubyの中のクローラー・フレームワークとしては未だに一番の完成度です。しかし、残念ながら幾つかの問題点があります。その中で日本語を扱う我々にとっては一番大きな問題は、文字化けです。

Anemoneの文字化けの原因



 Anemoneの文字化けの原因は、ずばりUTF-8以外の考慮が何もされていないためです。Anemoneが利用するHTMLパーサーであるNokogiriは、もともと内部的な文字コードをUTF-8として扱います。UTF-8以外の文字コードを扱う場合は、文字コードを指定して渡す必要があります。それにもかかわらず、AnemoneがHTMLをパースする時は、次のような実装になっています。

#
# Nokogiri document for the HTML body
#
def doc
  return @doc if @doc
  @doc = Nokogiri::HTML(@body) if @body && html? rescue nil
end

 この実装ではUTF-8やASCII以外の場合は、文字化けが発生します。

小手先のAnemoneの文字化け対策



 対策の1つとしては、AnemoneがパースしたNokogiriドキュメントを使わない方法があります。
取得先のページの文字コードをみて、String#encodeを使って明示的に変換します。

require 'anemone'

urls = []
urls.push("http://www.amazon.co.jp/gp/bestsellers/books/466282/")

Anemone.crawl(urls, :depth_limit => 0) do |anemone|
  anemone.on_every_page do |page|
    doc = Nokogiri::HTML.parse(page.body.encode("utf-8","shift_jis"))
    puts doc.xpath("//title").text
  end
end

 実行すると、この通り文字化けせずに結果を取得できます。

$ ruby anemone-convert.rb 
Amazon.co.jp ベストセラー: ビジネス・経済 の中で最も人気のある商品です

抜本的なAnemoneの文字化け対策



 一方で、折角Anemone内部でNokogiriドキュメントを作っているのに、それを切り捨てて新たなNokogoriドキュメントを作るのは無駄以外の何者でもありません。また、複数のサイトをクローリングする際は、決め打ちの文字コードの対処はできません。抜本的な対策としては、Anemone内部の問題を解決するのが良いでしょう。

 AnemoneがNokogiriドキュメントを生成しているのは、Anemone::Pageクラスのdocメソッドで実体はlib/anemone/paage.rbにあります。問題は、変換の際の文字コードに何を指定すれば良いかという点です。Anemoneが扱うのは、HTMLです。なので、HTML中からcharsetを抜き出し、それをセットすることにしましょう。幸い、Anemoneにはcontent_typeというメソッドがあり、HTMLヘッダーからcontent-typeを抜き出してくれています。ここからcharsetを抜き出せば大丈夫です。

#
# Nokogiri document for the HTML body
#
def doc
  return @doc if @doc
  if @body && html?
if charset == 'utf-8' || charset.nil?
  body = @body
else
  body = @body.encode("UTF-8", charset, :invalid => :replace, :undef => :replace) rescue nil
end
@doc = Nokogiri::HTML(body) if body
  end
end

#
# The content-type returned by the HTTP request for this page
#
def content_type
  headers['content-type'].first
end

#
# The charset returned by the content-type request for this page
#
def charset
  matcher = content_type.match(/charset=[\"]?([a-zA-Z\_\-\d]*)[\"]?/)
  matcher[1].downcase if matcher
end

 実装している途中で気がついたのですが、anemoneのPull Requestの中にほぼ同じ実装がありました。totothinkという方によるForkです。
totothink/anemone · GitHub
 
 私の方では、少し実装を変えています。content_typeのマッチの部分が「_」が含まれていなかった為にShift_JISに対応出来かなったので追加しています。また、もともとのHTMLのbodyが格納されている@body変数も、変換することなく元のままで残しております。その方が、後々使いやすいと考えたからです。

追記 2014/04/07
 content_typeだけで大丈夫なのというご指摘ありました。その通りだと思います。が、必要に応じて、変更していけば良いと思います。(MetaタグとかLangとか優先順位をつけて判定)ちなみに@bodyに入っている文字列をString#encodingで判定しようとしたのですが、必ずUS_ASCIIと判定されるので使えません。

Fork版のGitHubからのインストール



 GitHubにForkして、修正版を置いています。興味あれば使ってください。

git clone https://github.com/takuros/anemone.git
cd anemone
gem build anemone.gemspec
gem install anemone-0.7.2.gem

まとめ



 Pythonには、ScrapyというCrawlerのフレームワークがあります。今でも開発は活発です。RubyのCrawlerフレームワークとして網羅的な機能を持つのはAnemoneしかありません。Anemoneは残念ながら、もう開発されていない模様です。Pull Requestも幾つもあるけど、取り込まれる様子はありません。Forkで何か新しいの作っても良いのかもしれませんね。


See Also:
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた
takuros/anemone · GitHub


参照:
chriskite/anemone · GitHub
anemone RDoc


Rubyによるクローラー開発技法

Rubyによるクローラー開発技法

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Ruby2.0の文字エンコーディングの簡単なまとめ。KconvとM17N

 浦島太郎状態になっていたので、Ruby周りの文字コードの扱いについて改めて勉強しなおしました。簡単にいうとRuby 1.9.1以降の多言語化仕様(M17N)では、文字コードをどう扱えばよいのかという点です。

Rubyの文字コード変換ライブラリ


 Ruby 1.9.1の多言語仕様化以前は、文字コードの変換には文字コード変換ライブラリを使用する方法が一般的でした。主な文字コード変換ライブラリは、次のようなものがあります。

  • Kconv
  • NKF
  • Iconv
  • Uconv

 この中で、KconvはNKFのラッパーであり、NKFはnkf(Network Kanji code conversion Filte)をRubyから使うためのモジュールです。つまり実装としては、KconvとNKFの根の部分は同じです。またIconvは、UNIX95のiconv()関数のラッパーです。なお、IconvはRuby 1.9から非推奨、Ruby 2.0から標準添付ライブラリから削除されています。

Kconvの利用


 Kconvを読み込むと、Stringクラスに次のメソッドが追加されます。

String#iseuc String#isjis String#issjis String#isutf8 String#kconv String#toeuc String#tojis String#tolocale String#tosjis String#toutf16 String#toutf32 String#toutf8

使い方としては、下記のとおりで手軽に使えて便利です。

require 'kconv'

str = '日本語の文字コード'     # 何らかの文字コードの文字列
puts str.toeuc   # => ECUの文字コードに変換
puts str.tosjis  # => Shift_JISの文字コードに変換
puts str.toutf8  # => UTF-8の文字コードに変換

 一方で、Kconvについては幾つかの問題点があります。1つは、先述のとおりnkfライブラリを利用する為に、プラットフォーム依存が強いという点です。2つ目は、nkfライブラリの名前(Network Kanji code conversion Filte)が示すように、多言語化対応ではなく日本語対応のモジュールということです。この辺りの改善のために、Ruby 1.9からはM17Nを盛り込んだ仕様になっています。

追記:2014/04/08
 nkfライブラリのプラットフォーム依存の話は、間違いでした。nkfのソース自体がRubyにインポートされて、ビルドされている模様です。モジュールを確認するとその通りでした。@yugui さんご指摘ありがとうございます。

f:id:dkfj:20140408120732p:plain

$ tree nkf/
nkf/
├── Makefile
├── depend
├── extconf.h
├── extconf.rb
├── lib
│   └── kconv.rb
├── mkmf.log
├── nkf-utf8
│   ├── config.h
│   ├── nkf.c
│   ├── nkf.h
│   ├── utf8tbl.c
│   └── utf8tbl.h
├── nkf.c
└── nkf.o

M17N対応



 Ruby 1.9.1からは、M17N(Multilingualization)で多言語化の対応が根本的に変わっています。 Rubyの多言語化は、CSI(Code Set Independent)という方式を採用しています。これは他の言語の主流であるUCS Normalizationとは全く別の方式です。UCS Normalizationは、内部コードを特定の文字コード(多くの場合は、UTF)とし、入出力の際に別の文字コードが来た場合は変換して利用します。
 これに対して、Rubyが採用したCSI方式は内部コードを持ちません。どの文字コードでも、そのエンコーディングに従い扱います。そのために、プログラム自身にエンコーディングを教える必要があります。Ruby 1.9でコード中にマルチバイト文字が出てきた場合に、Magic Commentが必要になったのはこのためです。

# coding: UTF-8

str = '日本語の文字コード'     # 何らかの文字コードの文字列
puts str.encode("Shift_JIS") # => 暗黙的にUFT8から、Shift_JISに変換する
puts str.encode("Shift_JIS","UTF-8") # => 明示的にUFT8から、Shift_JISに変換する

 このString#encodeメソッドの実装は、class Encodingです。これは内部に文字コードの変換表を持っている訳ではなく、文字コードの取扱ルールを定義しているようです。またNKFのように日本語対応のみでなく、多言語対応です。

まとめ



 ということで互換性の問題がなければ、String#encodeメソッドを利用しましょう。またちゃんと、Magic Commentを書きましょうね。(Ruby 2.0ではデフォルトがUTF-8になっているので、ほぼ不要とも言えますが。)


参照:
module NKF
library kconv
Rubyist Magazine - Ruby M17N の設計と実装
class String
class Encoding::Converter
多言語化
Rubyist Magazine - 標準添付ライブラリ紹介 【第 3 回】 Kconv/NKF/Iconv
404 Blog Not Found:ruby|perl - 文字コードのちょっと高度な判定


プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

新社会人に伝えたい「インプットよりアウトプットが大切」の嘘

 新社会人向けに、おっさんがブログで講釈をたれるのが流行る季節になってきました。おっさんの一人として、私も偉そうなこと言ってみます。それは、

 

「インプットよりアウトプットが大切」

 

 なんて大嘘です。

そんな戯言を真に受けている暇があったら、さっさとインプットしましょう。確かにアウトプット大事です。私も定期的にブログやTwitterでアウトプット大事にしようと呟いています。でも、それには裏事情があります。

 

アウトプット大事と呟く人は、世の中には2種類います。

  • インプットが好きで好きで、意識しないと延々にインプットし続けてしまう人
  • そんな事情を知らずに、アウトプットが大事という事がを真に受けてしまう人

 

 知識や経験の裏付けがないアウトプットは薄っぺらいものです。1つ2つであれば、まぐれ当たりで凄い評価や反響を得ることもあるでしょう。でも、続きません。継続的に質の良いアウトプットを出し続けるには、まずはインプットを重視しましょう。インプットを続けて続けて、もう貯めきれないとなって自ずから吐き出されるようなアウトプット。まずはそれを目指すと良いのではないでしょうか。

 立花隆か誰かのインタビューか忘れましたが、1つのテーマについて書く時はそれに関する本を本棚1つ一杯になるまで読み込むそうです。1冊のアウトプットに対して、数百冊のインプットですね。すごく納得感がありますね。

 

 まぁ人が語る人生訓なんて、その背景となる経験とセットで考えないと全く身につかないものです。面白い読み物程度にとらえて、せっせとインプットに励んでください。そうしたら、自ずとアウトプットも出てきます。

 

See Also:

今まで読んで良かった本 100冊


 

ぼくはこんな本を読んできた―立花式読書論、読書術、書斎論 (文春文庫)

ぼくはこんな本を読んできた―立花式読書論、読書術、書斎論 (文春文庫)