kzfm’s trial and error

本腰いれてRubyやPHPを覚えていくブログ。

ツイートをリアルタイムに検索し取得する (Ruby)

OAuth対応だとか、数年間でAPIの様式がコロコロ変わった印象のTwitter
Rubyだとどう処理できるのか気になった。

Gem"Twitter" を使う

「特定のキーワードにマッチするツイートをリアルタイム取得する」方法。Twitterの検索は、データをリアルタイムに取得するAPIと、リアルタイムじゃないけど過去を含めて検索したりできるAPIの2通りがあって、今回はリアルタイム側を使う。

このモジュール1つで完結するのね…すげえ。

require "twitter"

client = Twitter::Streaming::Client.new do |config|
  config.consumer_key        = ""
  config.consumer_secret     = ""
  config.access_token        = ""
  config.access_token_secret = ""
end

keywords = "眠い,おやすみ"
options  = {
  :language => 'ja',
  :track    => keywords
}

client.filter(options) do |object|

  if object.is_a?(Twitter::Tweet)
    printf "%s / %s\n  %s\n\n",
      object.user.screen_name,
      object.user.name,
      object.text.gsub("\n", "\n  ")
  end

end

config部分はTwitterの開発者ページ*1の「Manage My Apps」からプログラム登録して設定する。

trackに指定しているキーワードは、カンマ区切りだとor検索、スペース区切りだとAND検索になる様子。その他の詳しいフィルタリングオプションは公式のAPI説明*2参照。

XMLをParseする (Ruby)

Radikoの番組表データを取得する

東京エリアの放送内容をXMLで取得し、色んなモジュールでパースしてみる。
APIの仕様はこちらのページ*1を参考にさせて頂いた。

APIと応答されるXML内容

地域別番組表API
http://radiko.jp/v2/api/program/today?area_id=[area_id]
エリアコードを指定してURLにアクセスすると、その地域の全放送局の放送内容がXMLで返される。

東京エリア(JP13)を指定すると以下のXMLが応答された。

<?xml version="1.0" encoding="UTF-8"?>
<radiko>
  <ttl>1800</ttl>
  <srvtime>1433606138</srvtime>
  <stations>
    <station id="TBS">
      <name>TBSラジオ</name>
      <scd>
        <progs>
          <date>20150606</date>
          <prog ft="20150606050000" to="20150606050500" ftl="0500" tol="0505" dur="300">
            <title>ニュース・天気予報</title>
            <sub_title />  <pfm></pfm>
            <desc />  <info>&lt;img src=&apos;http://www.tbs.co.jp/radio/todays954/photo/noimage.gif&apos;&gt;&lt;br /&gt;&lt;br /&gt;</info>
            <metas>
              <meta name="twitter" value="#radiko" />
              <meta name="facebook-fanpage" value="http://www.facebook.com/radiko.jp" />
              <meta name="twitter-hash" value="#radiko" />
            </metas>
            <url>http://www.tbs.co.jp/radio/</url>
          </prog>
          <prog ft="20150606050500" to="20150606060000" ftl="0505" tol="0600" dur="3300">
            <title>生島ヒロシのサタデー・一直線</title>
            <sub_title />  <pfm>生島ヒロシ/寺田理恵子ゲスト:舟木一夫/森山良子</pfm>
            <desc />  <info>&lt;img src=&apos;http://www.tbs.co.jp/radio/todays954/photo/sat-1.jpg&apos;&gt;&lt;br /&gt;&lt;br /&gt;先週に引き続き、生島ヒロシさんが憧れている舟木一夫さんが登場。&lt;br/&gt;「サタイチ週末大人クラブ」では、森山良子さんとの対談をお届け。&lt;br/&gt;&lt;br/&gt;メール:&lt;a href=&quot;mailto:sat-1@tbs.co.jp&quot;&gt;sat-1@tbs.co.jp&lt;/a&gt;&lt;br/&gt;</info>
            <metas>
              <meta name="twitter" value="#radiko" />
              <meta name="twitter-hash" value="#radiko" />
              <meta name="facebook-fanpage" value="http://www.facebook.com/radiko.jp" />
            </metas>
            <url>http://www.tbs.co.jp/radio/sat-1/</url>
          </prog>
        </progs>
      </scd>
    </station>
    </stations>
</radiko>

(抜粋。本当はstationノードもprogノードもたくさんある)


"REXML"を使ってパース

Ruby標準?のXMLパーサ。

  • XPathでノードを抽出できる
  • elements.each()でXPathにマッチするノードをイテレーション
  • elements[]で子ノードを取得
  • attributes[]で自身の属性を取得
require 'open-uri'
require 'kconv'
require 'rexml/document'

url  = 'http://radiko.jp/v2/api/program/today?area_id=JP13'
xml  = open( url ).read.toutf8

doc = REXML::Document.new(xml)

doc.elements.each('radiko/stations/station') do |station|
    printf "---%s (%s)---\n",
      station.attributes['id'],
      station.elements['name'].text

    station.elements.each('scd/progs/prog') do |prog|
        printf "%d-%d(%2d) : %s(%s)\n",
          prog.attributes['ft'],
          prog.attributes['to'],
          prog.attributes['dur'].to_i/60,
          prog.elements['title'].text,
          prog.elements['pfm'].text
    end
end

凄くstrictな感じ。見やすい。

"ActiveSupport::XMLConverter"を使ってパース

RubyOnRailsに付属してるライブラリ。

  • XMLを解析してハッシュに格納してくれる
  • 複数存在するノードはArrayで格納される
require 'open-uri'
require 'kconv'
require 'active_support/core_ext/hash/conversions'

url  = 'http://radiko.jp/v2/api/program/today?area_id=JP13'
xml  = open( url ).read.toutf8
hash = Hash.from_xml(xml)

hash['radiko']['stations']['station'].each do |station|
  printf "---%s (%s)---\n",
     station['id'],
     station['name']

  station['scd']['progs']['prog'].each do |prog|
    printf "%d-%d(%2d) : %s(%s)\n",
      prog['ft'],
      prog['to'],
      prog['dur'].to_i/60,
      prog['title'], prog['pfm']
  end
end

特定ノードの「属性値」と、そのノードの「子ノード」の区別が無くなる。まあこれは大した問題じゃない。

問題なのは、対象ノードが「1つの場合」と「複数の場合」とで格納内容が変わってしまう点。例えば上記に貼った「抜粋版」のXMLだと、stationノードは1つだけなので配列になっておらず、このコードでは動作しない。

こういった点を考慮するとコードが膨らむので、「要素数が変化する外部サービスのXML」を処理する場合は使わないほうが良さそう。"REXML"のeachは対象ノードが1つの場合も期待通りに動作する。

HTTP通信 (Ruby)

Rubyスクレイピングをやりたくて、HTTP通信の方法から試行錯誤。

"net/http"を使う

最初に辿り着いた、ローレベルな実装方法。
PerlでいうところのLWP::UserAgentを使っている感じで凄く馴染み深い。
こちら*1を参考にさせて頂いて、少し手を加えた。

  • GET/POST対応
  • パラメータ渡せる
  • 取得した文字列はバイナリ扱い(ASCII-8BIT)

Yahooのトップページを取得してみる

require 'net/http'
require 'kconv'

def http_request(method, uri, query_hash={})
    query = query_hash.map{|k, v| "#{k}=#{v}"}.join('&')
    query_escaped = URI.escape(query)

    uri_parsed = URI.parse(URI.escape(uri)) # 
    http = Net::HTTP.new(uri_parsed.host)

    case method.downcase!
    when 'get'
        return http.get(uri_parsed.path + '?' + query_escaped).body
    when 'post'
        return http.post(uri_parsed.path, query_escaped).body
    end
end

url = 'http://www.yahoo.co.jp'
body = http_request('GET', url)

puts body.toutf8

もっと簡潔に書く方法*2もあった。

文字コードはmetaタグのcharset属性値で判定するのが「行儀がいい」はずだが、判定した文字コードを元にUTF8に変換する適当な方法が見つけられなかったので、kconvでざっくり自動判定させた。


"open-uri"を使う

"net/http"のwrapperで、Kernel.openを拡張して直接URLがオープン出来るようになるGem。
引数がファイルパスでも開けるので注意が必要。*3
丁寧に使えばキャッシュ処理が透過的に実装できるから便利かもしれない。

  • GETのみ?
  • 取得した文字列はバイナリ扱い(ASCII-8BIT)

Yahooのトップページを取得してみる

require 'open-uri'
require 'kconv'

url = "http://www.yahoo.co.jp"
body = open( URI.escape(url) ).read

puts body.toutf8

リソースを取得するだけならこれが最短ぽい。


"mechanize"を使う*4

スクレイピングするなら"mechanize"が定番みたい。もの凄い高機能なGem。

  • 勿論GET/POST対応
  • パラメータも渡せる
  • 受信したページは"nokogiri"がオブジェクト化してくれる
  • ページオブジェクトは、Formに値をセットしてSubmit出来る
  • ページオブジェクトは、XPathを使って解析できる

Yahooのトップからキーワード検索を行う

require 'mechanize'

agent = Mechanize.new
agent.user_agent_alias = 'Windows Mozilla'

url = 'http://www.yahoo.co.jp'
page1 = agent.get(url)

form = page1.form_with(:name => 'sf1')
form.field_with(:name => 'p').value = "デミオ"
page2 = form.submit

page2.search('//div[@class="w"]').each do |node|
    puts node.css('div > h3 > a').inner_text     # ざっくり
end

一覧ページから詳細ページURLを抽出、各ページに遷移して、その内容を解析…みたいな事がmechanizeだけで完結する。よく出来てるなあ。

Mechanizeは受信したHTMLをUTF8に変換し、以後UTF8で処理する。もしサーバ側にSJIS等でPOSTする必要がある場合はひと工夫必要、らしい。*5

Rubyの勉強に読ませて頂いた書籍

Perl野郎の自分がRuby習得の第一歩として、スムーズに読めた。
帯に書いてあった『本書は初心者の気持ちを裏切らない出来』というのは納得。
Rubyのコーディングで使う「基本用具一式」が把握できる本。


メタプログラミングRuby

メタプログラミングRuby

RubyにおいてClassはオブジェクトだ!」なんだってー!
という所から、

などなど。
他言語に対するRubyのアドバンテージが見えてくる本。


プログラミング言語 Ruby

プログラミング言語 Ruby

やっぱりオライリー本だよね!という事で、最初に買って挫折した本。
個々に詳細なので、Rubyのアウトラインを理解してから読もうと思った。