Home

週刊プレカリアート

PayPalのSandboxで”Please login to use the PayPal Sandbox features.”になる問題

クリスマスイブだというのに寂しくコードを書いています(;;)

さて、本題ですがPayPalの支払いのリダイレクトページについて、あちこちにサンプルコードがあります。たとえば

$fp = fsockopen (‘www.sandbox.paypal.com’, 80, $errno, $errstr, 30);

のようになっているサンプルがあるのですが、このままでは動作しません(少なくともこちらでは動作しませんでした)。”Please login to use the PayPal Sandbox features.”が出てきます。これについての日本語ページは探したところ1ページしかなく、期待する解決策はなかったためメモ書きを残すことにしました。英語だと7,500件出てきますが、そのうち何件が役に立つかは定かではありません。

ともあれ、実行するとこのような画面になってしまいログインを促されます。

Sandbox

この原因は

  • https://developer.paypal.com/にログインしないと使えない
  • クッキーにsequre属性が付いているからhttpsにしないとクッキーが無効になる

ことなので、解決策はsandboxにはhttpsでアクセスすることです。

いま、Google App EngineをPayPalから通知を受けるサーバとしてプログラムを書いています。Pythonは不慣れだから色々と大変です。

[tmkm-amazon]4873114756[/tmkm-amazon]
[tmkm-amazon]4797357606[/tmkm-amazon]

ウェブサイトごとに別々の強力なパスワードを作ることは難しいのか

紙だけでサイトごとに異なるパスワードを生成する暗号、米研究者が考案」に関連して。

前から感じているのだけど、パスワードを使い回す人が多く、その理由はパスワードを覚えきれないからという。しかし、実のところ強力でサイトごとにバラバラなパスワードを生成し、それを忘れない方法はあると思う。簡単にいうとパスワードを覚えるのではなく、パスワードを生成するルールを覚えておくのだ。

仮に http://www.example.comというサイトがあるとしよう。ここで仮にパスワード生成のルールに「ドメイン名にsaltを加えてMD5する」と決めておく。saltはNEETにしようか。そうすればあとは必要なときにコンソールを叩くだけだ。

md5 -s "NEETwww.example.com"
MD5 ("NEETwww.example.com") = 3bf864e8b76269f05cb5aa0405f02d5b

こうして得られたパスワード3bf864e8b76269f05cb5aa0405f02d5bは十分に強いし、これをブルートフォース攻撃でクラックしようとしてもまず無理だろう。生年月日や電話番号ではないので想像も難しい。その割にMD5を計算できるパソコンがあればいつでもすぐにパスワードを復元できる。

この方法を使うときはsaltの内容とMD5を使ってパスワードを作っていることを他人に悟られないこと。別にMD5でなくてもいいんだけど。SHA1して先頭の24文字を採用するとかなんでもいい。

AmazonのランキングをPHPで取得する

サイドバーに設置するランキングをPHP, PEARのServices_Amazonで作ってみた。今のアクセス数だとその都度取得してもたぶんいいけど、キャッシュしておくとか機能強化もしたほうがいいかも知れない。

まず適切にPEARをインストールして、pear install -a Services_Amazon-betaなどしてAmazonのサービスを使えるようにしておく。

サンプルコード

そののちにこんな感じのPHPを書いてみた。

< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
< html lang="ja">
< head>
< meta http-equiv="Content-Type" 
        content="text/html; charset=UTF-8">
< title>PHP入門< /title>
< /head>

< body>

< ?php
require_once('Services/Amazon.php');
$access_key_id = 'XXXXXXXXXXXXXXX';
$secret_access_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
$associate_id = 'XXXXXXX-22';

$amazon = new Services_Amazon($access_key_id,$secret_access_key,$associate_id);
$amazon->setLocale('JP');
$options = array();
$options["Keywords"] = 'プログラミングコンテスト';
$options['Sort'] = 'salesrank';
$options['ResponseGroup'] = 'ItemIds,ItemAttributes,Images';
$result = $amazon->ItemSearch('Books', $options);

if (!PEAR::isError($result))
{
	for( $i = 0; $i <  5; $i++ )
	{
		$link = $result['Item'][$i]['DetailPageURL'];

		echo "< a href='".$link."'>< img src='".$result['Item'][$i]['MediumImage']['URL']."'>< /a>< br />";
		echo $result['Item'][$i]['ItemAttributes']['Title'].'< br />';
	}
}
?>

< /body>
< /html>

結果

こんな感じになった。

BBEdit 10.0

最近他のブログばかり更新しているけど、こういう話題はあまり読む人がいないだろうからこっちに書く。

BBEditが39.9ドル=3,192円(1ドル80円換算)

BBEdit 10.0が出ている。BBEditは昔からMacの定番エディタとして有名だがあまりにも高いのでレジストしようとは思わなかった。いま主に使っているエディタはCotEditorとmiである。フリーだし十分高機能だからこれでいいと思っていた。しかし、いまBBEditはそう高いとは言えない。

unidic-mecabのソースコードを見てみるとVerb.csvが90.8MBもある。これをmiとかCotEditorとかで開くと非常に重くて作業が困難である。ところがBBEditはいともあっさり開いて編集ができた。やはりフリーのエディタと定番のエディタは違う、高いだけのことはある。

ただ、いまのところBBEditを買おうと思う動機は巨大ファイルの編集も楽々くらいしかない。Lionのフルスクリーンにもいち早く対応しているし、フリーのエディタよりも開発は精力的なのも魅力だけど、これはそのうちフリーのエディタも対応するだろう。

SubversionやCVS対応は最近のgitより古い感じもする。Lionにバージョン機能があるし個人でバージョン管理をする用途ならサーバは要りそうもない。チーム開発ではCVSってどうなのか。

FTP/SFTP機能だけどscpには対応していないのだろうか。miもFTP機能はあるけど最近はあまり使っていない。FTPを使う機会が減っている。

エディタにいま求める物はソースコードを書く機能が優れているかではないかなと分析している。そのときメニューを見渡してみるとHTML関連は豊富なのだけど、RubyとかC++はそれほど魅力を感じない。HTMLを書くエディタならCodaを買うべきだろう。C++のコードを書いてみたけど、Xcodeのほうがよさそうな感じ。ソースコード関連だとVicoとかSublime Text 2のほうが期待できそう。ただし、こいつらは巨大ファイルの取り扱いでBBEditに劣る感じがする。TextMateも放置されて久しいし、これが定番というテキストエディタがないのが残念。

ミルカさんボット

ベクトルってつぶやきに反応してこっそり「…ヴェクタ」とつぶやくbotとかいないかな

という要望があったので作ってみた。

基本

ほとんどの手順は株ニュースと同じ。今回はTwitter::Searchを使う。結果をHashie::Meshで受け取るので、必要なものだけ取り出す。一度RTしたものには反応しないことにする。

以下ソース。

#!/opt/local/bin/ruby -Ku
require 'rubygems'
require 'mechanize'
require 'nokogiri'
require 'kconv'
require 'logger'

require 'oauth'
require 'twitter'

#---------------------------------------------------------------------------
# 定数
#---------------------------------------------------------------------------

CONSUMER_KEY = "きー"
CONSUMER_SECRET = "しーくれっと"
ACCESS_TOKEN = "とーくん"
ACCESS_TOKEN_SECRET = "とーくんしーくれっと"

#---------------------------------------------------------------------------
# Retweet
#---------------------------------------------------------------------------
def retweet( r )

	user = r["from_user"]
	text = r["text"]
	id = r["id"].to_s

	# もともと「ヴェクタ」を喋るものには反応しない
	return if text.include?("ヴェクタ")

	# 手抜き
	f = open("recent_tweet.txt", "a")
	f.close

	# 過去にRTしているか調べる
	f = open("recent_tweet.txt", "r")
	while recent_id = f.gets
		if recent_id.include?(id) then
			f.close
			return
		end
	end

	# RTする
	f = open("recent_tweet.txt", "a")
	f.puts(id)
	consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
	oauth = Twitter::OAuth.new(CONSUMER_KEY, CONSUMER_SECRET)
	oauth.authorize_from_access(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
	twitter_client = Twitter::Base.new(oauth)

	tweet = "...ヴェクタ QT @#{user} #{text}"
	twitter_client.update(tweet, {:in_reply_to_status_id => id})
#	twitter_client.update(tweet)
	$log.info("Twitterに投稿しました:" + tweet)
	f.close

end

#---------------------------------------------------------------------------
# main
#---------------------------------------------------------------------------
def main

	Twitter::Search.new('ベクトル').each do |r|

		if r.class == Hashie::Mash then
			retweet(r)
		end
	end
end

#---------------------------------------------------------------------------
# エントリポイント
#---------------------------------------------------------------------------

#$log = Logger.new("log.txt")
$log = Logger.new(STDOUT)
$log.level = Logger::INFO

main

Rubyでmixiの日記をバックアップする

  • 2010-05-05 (水)
  • Ruby

mixiを退会すると日記は消えてしまいます。その日記を手っ取り早く手元に残しておきたいとき、1つずつ手作業でやっていると大変です。ここではそれを自動化します。

準備

まず普通にWebブラウザでmixiにログインして日記を見ます。FirefoxだとFirebugが便利です。

FirebugでDOMツリーを見ていくと該当の部分が青く反転するので、必要な場所を絞り込んでいきます。最後に、右クリックをして「XPathをコピー」を選んでやると、XPathを入手できます。

xpathを入手したら、これを利用して解析していきます。ただし、Firefoxではtbodyタグを勝手に挟むため、これを除去します。

コード

#!/usr/local/bin/ruby -Ku
require 'rubygems'
require 'mechanize'
require 'nokogiri'
require 'kconv'

# 簡易待ち付きget
def get(url)
	sleep(10)
	$agent.get(url);
	$agent.page.encoding = "UTF-8"
end

#変数
owner_id = mixiのID
diary_url = "http://mixi.jp/list_diary.pl?id=#{owner_id}"

# mixiのトップページにアクセス
$agent = Mechanize.new
$agent.get("http://mixi.jp/")
$agent.page.encoding = "UTF-8"

# ログイン
$agent.page.form_with(:name => "login_form" ) do |f|
	f.field_with(:name => 'email').value = "めーるあどれす"
	f.field_with(:name => 'password').value = "ぱすわーど"
	f.click_button
end

puts "ログインしました"

# 日記トップを取得
get(diary_url)
parser = Nokogiri::HTML.parse($agent.page.body, nil)
diary = parser.xpath("/html/body/div/div[2]/div/div[3]/div[2]/div[2]").to_html.toutf8
diaries = diary.scan(/<li>.*?"(.*?)".*?"(.*?)".*?li>/)

diaries.each do |d|
	each_diary = "http://mixi.jp/#{d[0]}"
	puts "#{d[1]}を処理"
	Dir::mkdir(d[1])
	Dir::chdir(d[1])

	get(each_diary)

	parser = Nokogiri::HTML.parse($agent.page.body, nil)
	diary_list = parser.xpath('//*[@id="bodyMainAreaMain"]').to_html.toutf8
	entrirs = diary_list.scan(/<dt>.*?"(view_diary.*?)">(.*?)</)

	entrirs.each do |e|
		entry_url = "http://mixi.jp/#{e[0]}"
		get(entry_url)
		parser = Nokogiri::HTML.parse($agent.page.body, nil)
		contents = parser.xpath('//*[@id="bodyMainAreaMain"]').to_html.toutf8
		name = e[1].gsub("/", "/")
		f = open(name + ".html",'w')
		f.puts "<html>"
		f.puts "<head>"
		f.puts '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'
		f.puts "</head>"
		f.puts "<body>"
		f.puts contents
		f.puts "</body>"
		f.puts "</html>"
		f.close
	end

	Dir::chdir("../")
end

RubyでSBIランキングの売り越し額を調べる

  • 2010-05-05 (水)
  • Ruby

SBI証券は個人投資家が多く使っている証券会社です。通常は個人投資家は機関投資家に搾取されるようにできています。株価が暴落すると個人投資家は飛びつき、下げ止まらないとどんどんナンピン買いをして損を膨らませていきます。投資のスタイルは人それぞれですが、個人が買い越している間は手を出さずに売り越しに転じた辺りで買うというのもよいと思っています。

これを毎日、前場と後場にランキングを見て、買い代金と売り代金を比較してソートするのは面倒ですので、これを自動でやらせます。

方針

いつものようにMechanizeで該当のページを読み、xpathで必要な部分だけ取り出して解析します。

売りと買いを配列に入れて1つずつ照合、同じコードを持つもので買い代金と売り代金の差を取って新しい配列に入れます。これをソートして出力すればOK。

実装例

#!/usr/bin/ruby -Ku
require 'rubygems'
require 'nokogiri'
require 'mechanize'
require 'logger'
require 'kconv'

SBI_RANKING = "https://trading1.sbisec.co.jp/ETGate/?_ControlID=WPLETmgR001Control&_DataStoreID=DSWPLETmgR001Control&burl=search_market&dir=ranking%2F&file=et_trading_value.html&cat1=market&cat2=ranking&getFlg=on"

BUY_XPATH = "/html/body/div[@id='middleArea']/div[@id='middleAreaM']/table/tr/td[1]/div[@id='main']/table[2]/tr/td[2]/table[4]/tr/td[1]/table"
SELL_XPATH = "/html/body/div[@id='middleArea']/div[@id='middleAreaM']/table/tr/td[1]/div[@id='main']/table[2]/tr[1]/td[2]/table[4]/tr/td[3]/table"
DATE_XPATH = "/html/body/div[@id='middleArea']/div[@id='middleAreaM']/table/tr/td[1]/div[@id='main']/table[2]/tr[1]/td[2]/table[3]/tr/td[1]/b"
TIME_XPATH = "/html/body/div[@id='middleArea']/div[@id='middleAreaM']/table/tr/td[1]/div[@id='main']/table[2]/tr[1]/td[2]/table[3]/tr/td[2]"

#---------------------------------------------------------------------------
# main
#---------------------------------------------------------------------------
def main

	agent = Mechanize.new
	log = Logger.new(STDOUT)
#	log = Logger.new("sbi_ranking.log")
	log.level = Logger::INFO
	agent.get(SBI_RANKING)
	agent.page.encoding = "Shift_JIS"

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

	# 時刻
	ranking_time = parser.xpath(TIME_XPATH)
	/(d{2,2}):(d{2,2})</td>$/ =~ ranking_time.to_html.toutf8
	sbi_hour = $1
	sbi_min = $2

	# 日付
	ranking_date = parser.xpath(DATE_XPATH)
	/.*?(d{1,2})/(d{1,2})/ =~ ranking_date.to_html.toutf8
	sbi_month = $1
	sbi_day = $2

	buy_code = []
	buy_name = []
	buy_trading_value = []

	sell_code = []
	sell_name = []
	sell_trading_value = []

	for i in 3..22 do
		buy = parser.xpath(BUY_XPATH + "/tr[#{i}]")
		e = buy.to_html.toutf8.gsub("n", "")
		/stock_sec_code_mul=(d{4,4}).*?exchange_code.*?>(.*?)<.*?([d,]+?)</div>/ =~ e
		buy_code << $1
		buy_name << $2
		buy_trading_value << $3.gsub(",", "")
	end

	for i in 3..22 do
		sell = parser.xpath(SELL_XPATH + "/tr[#{i}]")
		e = sell.to_html.toutf8.gsub("n", "")
		/stock_sec_code_mul=(d{4,4}).*?exchange_code.*?>(.*?)<.*?([d,]+?)</div>/ =~ e
		sell_code << $1
		sell_name << $2
		sell_trading_value << $3.gsub(",", "")
	end

	result = []
	for i in 0..buy_code.size - 1 do
		for j in 0..sell_code.size - 1 do
			if buy_code[i] == sell_code[j] then
				result.push( [buy_code[i], buy_name[i], ( buy_trading_value[i].to_i - sell_trading_value[j].to_i )] )
			end
		end
	end

	result = result.sort{ |a, b| a[2] <=> b[2] }

	for i in 0..result.size - 1 do
		puts "#{result[i][0]}, #{result[i][1]}, #{result[i][2]}"
	end
end

#---------------------------------------------------------------------------
# エントリポイント
# 単にmainを呼ぶだけ
#---------------------------------------------------------------------------
main

RubyでNHKラジオ講座のストリーミングを保存する

  • 2010-05-05 (水)
  • Ruby

NHKのラジオ講座は安価に語学を学ぶことができる。ラジオを持っていない人でもストリーミングで聴くことができる。ただしロシア語などいくつかの講座は提供されていない。

ストリーミングは電車の中でiPodに入れて聴きたいと思ったときには不便である。ここではストリーミングファイルを自動的にダウンロードしてmp3に変換する。

ツールを手に入れる

ストリーミングファイルをダウンロードするにはRTMPDumpを使う。ビルド版もあるし、portやyumなどでインストールしてもいい。

flvをmp3に変換するにはFFmpegを使う。これは多種のコーデックをサポートするツールだが、そのぶんビルドが面倒臭い。既にビルドされているものを使うか、やはりportやyumを使って入手するとよい。

準備

まず語学講座のURLを連想配列に入れる。また、flvの情報が記録されているXMLファイルlistdataflv.xmlやストリーミングファイル置き場のURLも定数に入れておく。

#!/usr/local/bin/ruby -Ku
require 'rubygems'
require 'mechanize'
require 'nokogiri'
require 'kconv'
require 'logger'
require "fileutils"

RTMPDUMP = "rtmpdump "
FFMPEG   = "ffmpeg "
STREAMING_BASE = "rtmp://flv9.nhk.or.jp/flv9/_definst_/gogaku/streaming/flv/"
COURSE_XML = "listdataflv.xml"

COURSE = {
	"基礎英語1" => "http://www.nhk.or.jp/gogaku/english/basic1/",
	"基礎英語2" => "http://www.nhk.or.jp/gogaku/english/basic2/",
	"基礎英語3" => "http://www.nhk.or.jp/gogaku/english/basic3/",
	"英語5分間トレーニング" => "http://www.nhk.or.jp/gogaku/english/training/",
	"ラジオ英会話"  => "http://www.nhk.or.jp/gogaku/english/kaiwa/",
	"入門ビジネス英語" => "http://www.nhk.or.jp/gogaku/english/business1/",
	"実践ビジネス英語" => "http://www.nhk.or.jp/gogaku/english/business2/",
	"まいにち中国語" => "http://www.nhk.or.jp/gogaku/chinese/kouza/",
	"まいにちフランス語" => "http://www.nhk.or.jp/gogaku/french/kouza/",
	"まいにちイタリア語" => "http://www.nhk.or.jp/gogaku/italian/kouza/",
	"まいにちハングル講座" => "http://www.nhk.or.jp/gogaku/hangeul/kouza/",
	"まいにちドイツ語" => "http://www.nhk.or.jp/gogaku/german/kouza/",
	"まいにちスペイン語" => "http://www.nhk.or.jp/gogaku/spanish/kouza/"
}

ダウンロードメソッドを作る

まず連想配列の名前を与えてdownloadメソッドを呼ぶ。存在しない講座ならreturnしておく。

ファイルが乱雑に置かれると面倒なので講座ごとにフォルダを作る。まだフォルダがなければ作成しておく。

MechanizeでXMLファイルをgetして、NokogiriのXMLパーサで解析する。解析にはxpathを使う。ファイル名はfile属性にある。講座名や日付もあるので利用する。

まだファイルがないときには、RTMPDumpを用いてflvファイルをダウンロードする。mp3ファイルがまだないときは続けてFFmpegを使って変換する。

最後に連続してアクセスしないように待ちを入れる。

#---------------------------------------------------------------------------
# ラジオ講座のダウンロード
#---------------------------------------------------------------------------

def download(course)

	if COURSE[course] == nil then
		return
	end

	unless( FileTest.exist?(course) ) then
		FileUtils.mkdir_p course
	end

	agent = Mechanize.new
	url = COURSE[course] + COURSE_XML
	agent.get(url)
	parser = Nokogiri::XML.parse(agent.page.body, nil)
	parser.xpath("musicdata/music").each do |element|
		flvname = element.attr("file")
		filename = course + "/" + element.attr("kouza") + "_" + element.attr("hdate")

		unless( FileTest.exist?("#{filename}.flv") ) then
			command = RTMPDUMP + "-o #{filename}.flv -r #{STREAMING_BASE+flvname}"
			system(command)
		end

		unless( FileTest.exist?("#{filename}.mp3") ) then
			command = FFMPEG + "-i #{filename}.flv #{filename}.mp3"
			system(command) 
		end
	end
	sleep(1)
end

聴きたい講座をダウンロードする

#---------------------------------------------------------------------------
# エントリポイント
#---------------------------------------------------------------------------

download("基礎英語1")

聴きたい講座を指定してdownloadメソッドを呼ぶ。

実行結果

RTMPDump v2.2
(c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team; license: GPL
Connecting ...
Starting download at: 0.000 kB
Metadata:
  duration              900.76
  audiodatarate         64.00
  audiodelay            0.04
  audiocodecid          2.00
  canSeekToEnd          TRUE
7577.419 kB / 900.81 sec (100.0%)
Download complete
FFmpeg version 0.5.1, Copyright (c) 2000-2009 Fabrice Bellard, et al.
  configuration: --prefix=/opt/local --disable-vhook --enable-gpl --enable-postproc --enable-swscale --enable-avfilter --enable-avfilter-lavf --enable-libmp3lame --enable-libvorbis --enable-libtheora --enable-libdirac --enable-libschroedinger --enable-libfaac --enable-libfaad --enable-libxvid --enable-libx264 --enable-nonfree --mandir=/opt/local/share/man --enable-shared --enable-pthreads --cc=/usr/bin/gcc-4.2 --arch=x86_64
  libavutil     49.15. 0 / 49.15. 0
  libavcodec    52.20. 1 / 52.20. 1
  libavformat   52.31. 0 / 52.31. 0
  libavdevice   52. 1. 0 / 52. 1. 0
  libavfilter    1. 4. 0 /  1. 4. 0
  libswscale     1. 7. 1 /  1. 7. 1
  libpostproc   51. 2. 0 / 51. 2. 0
  built on Apr 11 2010 12:49:33, gcc: 4.2.1 (Apple Inc. build 5646) (dot 1)
Input #0, flv, from '基礎英語1/基礎英語14月30日放送分.flv':
  Duration: 00:15:00.75, start: 0.000000, bitrate: 64 kb/s
    Stream #0.0: Audio: mp3, 44100 Hz, mono, s16, 64 kb/s
Output #0, mp3, to '基礎英語1/基礎英語14月30日放送分.mp3':
    Stream #0.0: Audio: libmp3lame, 44100 Hz, mono, s16, 64 kb/s
Stream mapping:
  Stream #0.0 -> #0.0
Press [q] to stop encoding
[libmp3lame @ 0x101812800]lame: output buffer too small (buffer index: 9404, free bytes: 388)
Audio encoding failed

FFmpegのバージョンによってはAudio encoding failedになる。解決策がここにある。

[libmp3lame @ 0x101812800]lame: output buffer too small (buffer index: 9404, free bytes: 388)
Audio encoding failed

Rubyで2chの市況1板のニュースを集めTweetする

  • 2010-05-05 (水)
  • Ruby

2chの市況1板には多くの人(個人投資家)が集まって、株価に一喜一憂している。そのため、何かよい材料がないか常に探し求めている状況にある。もしよいニュースを見つけたら「キタ━━━━━━(゜∀゜)━━━━━━」とすぐに書き込んでくれる。これは場合によると自分の見ているニュースサイトに掲載されるより早く情報を手に入れられるかも知れない。

しかし、スレッドは多く全部見ているのは大変である。雑談も多いし、必要な情報だけいち早く報告したらトレードに有利なのではないかと考えた。信頼性の高い情報はソースを載せることが多いので、ニュースサイトのURL付きの書き込みを有用度が高いと判断することにする。

スレッド一覧を取得する

スレッドの一覧は前回書いたようにsubject.txtを見れば手に入る。これをmechanizeでgetする。

BOARD = "http://anchorage.2ch.net/livemarket1/"
DAT = "dat/"
SUBJECT = "subject.txt"

このように定数を用意しておく。

株価コードで絞り込む

市況1板では慣例である銘柄についてのスレッドに株価コードを入れることになっている。自分にとって関心のある銘柄のコードで探せば上手く見つけられる。これを事前に用意して配列に入れておく。

STOCK_CODE = [
"2497",
"5401",
"5405",
"5411",
"5713",
"6301",
"6752",
"6758",
"7203",
"7974",
"8002",
"8031",
"8058",
"8306",
"8316",
"8411",
"8802",
"9132",
"9433",
"9984",
"日経"
]

ここでは出来高の大きそうな銘柄と「日経」に関するスレッドを読むことにする。1行ずつSTOCK_CODEの配列にある文字列を含むか見ていき、該当するものだけをdatsという配列に格納する。

#---------------------------------------------------------------------------
# getThreads
#---------------------------------------------------------------------------
def getThreads(list)

	# 読むべきdatリスト
	dats = []

	# 1行ずつ判定
	list.each_line do |line|
		d = line.split( /<>/ )
		STOCK_CODE.each do |code|
			if d[1].include?(code) then
				dats << d[0]
			end
		end
	end

	#重複があれば除去
	dats.uniq!

	return dats

end

不要なDATファイルを削除する

過去に取得したものの、既に板から消えてしまったDATファイルは不要だから削除する。

#---------------------------------------------------------------------------
# deleteThreads
#---------------------------------------------------------------------------
def deleteThreads(dats)

	# カレントディレクトリのファイル一覧を消去候補リストに入れる
	fileList = Dir::entries(Dir::pwd)

	# datファイル以外は消去リストから取り除く
	fileList.delete_if do |x|
		/.dat$/ !~ x
	end

	# カレントディレクトリの.datファイルでdatsにないものを消す
	fileList.each do |name|

		# flagがtrueのものは消す
		flag = true

		dats.each do |dat|
			if name == dat then
				# 取得したいdatファイルは消さないでおく
				flag = false 
			end
		end

		# datファイルで不要なものは削除する
		if flag == true then
			File.delete(name)
			$log.info(name + "を消去しました")
		end

	end
end

必要なスレッドを読む

dats配列に格納されたスレッドを読む。ただし、丸ごと読むとサーバに負担をかけるので差分ダウンロードをする。差分ダウンロードはwgetでもできるが、差分だけ解析する処理が面倒臭いので自力でやることにする。

差分ダウンロードはHTTPのリクエストヘッダにRangeヘッダを追加してリクエストすればよい。まずローカルに保存されたDATファイルがあるなら容量を計算して、それをRangeにセットすればよい。転送量を減らすためにaccept-encodingにgzipを入れておく。

これでリクエストしたものは差分になる。簡易あぼーん対策として、最初に改行コードが来ていなければ、あぼーんによってスレッドの構成が変わっていると判断する。市況1板ではそれほど個人情報さらしなどの悪質な書き込みがないので、簡易対策だけに留めるが、必要なら丸ごと再取得する。

連続してリクエストするとサーバに負荷をかけて迷惑になるのでsleepを適当に入れる。

#---------------------------------------------------------------------------
# 必要なスレッドをクロールする
#---------------------------------------------------------------------------
def crawl( name )

	daturl = BOARD + DAT + name
	$log.info( daturl + "を取得中")

	header = {}
	header['User-Agent']= 'Monazilla/1.00 (super-hikky/1.0)'

	length = 0
	if File.exist?( name )
		length = File.stat( name ).size
		datfile = open( name, 'a' )
	else
		datfile = open(  name, 'w' )
	end

	if length > 0
		header['Range'] = "bytes=#{length-1}-"
		header['accept-encoding'] = "gzip"
	end

	url = URI.parse(daturl)
	res = Net::HTTP.start(url.host, url.port) do |http|
		http.get(url.path,header)
	end

	#簡易あぼーん対策
	if res.body[0] != 'n' then
		datfile.print res.body
   		analyze(res.body)
	end

	datfile.close
	sleep(10)
end

ニュースを取り出す

書き込まれたURLが全てニュースとは限らない。そこで、ニュースサイトのドメインを用意しておく。

NEWS_SITE =
[
"www.bloomberg.co.jp",
"jp.reuters.com/article",
"www.nikkei.co.jp/news",
"www3.nhk.or.jp/news",
"www.yomiuri.co.jp",
"www.asahi.com",
"www.business-i.jp",
"sankei.jp.msn.com",
"news.livedoor.com/article",
"headlines.yahoo.co.jp",
"www.tokyo-np.co.jp/article",
"news.searchina.ne.jp",
"www.jiji.com",
"www.worldtimes.co.jp",
]

これに該当するものだけをニュースサイトであると判断する。過不足があればこの配列に足してもよい。2chではhttpをttpと書くこともあるので、ドメイン名のみにしておく。これを用いて判断する。

重複したニュースを読むのはコストの無駄なので、recent_news.txtに既に読んだニュースのURLを記録しておく。

新しく取得した差分から1行ずつURLが含まれているかscanする。URLが含まれるときはrecent_news.txtに含まれていないのを確認して、news配列に入れていく。また、recent_news.txtにも記録する。もしかするとrecent_news.txtに記録するタイミングはTwitterにつぶやいた後の方がいいかもしれない。

#------------------------------------------------------------------------------------------
# ニュースサイトを抽出
#------------------------------------------------------------------------------------------
def analyze( body )
	body.each_line do |each_res|

		# URLのみ取り出す
		r = each_res.scan(/ttp://([w+$;?.%,!#~*/:@&\=_-]+)/)

		# URLが入っている場合
		if r.size > 0 then

			# 全てのURLを精査
			r.each do |url|

				NEWS_SITE.each do |news|

					if url[0].include?( news ) then

						# 過去に読んでいるならtrue
						flag = false
						news_url = "http://" + url[0]

						recent_news = open("recent_news.txt", "a+")
						recent_news.each do |line|
							if line.include?(news_url) then
								flag = true
								break
							end
						end

						if flag == false then
							$news << news_url
							recent_news.puts(news_url)
						end

						recent_news.close

					end
				end # each
			end # each
		end # if
	end #each
end

ニュースを読む

ニュースを読み、Twitterにつぶやく。まず、前回取得したOAuthのキー類を用意する。

CONSUMER_KEY = "GDzTag(略)"
CONSUMER_SECRET = "7TbeOj9waUkqVCOiP(略)"
ACCESS_TOKEN = "91938679-(略)"
ACCESS_TOKEN_SECRET = "hvwhVudtK8QOD(略)"

これを用いて

#------------------------------------------------------------------------------------------
# ニュースサイトを抽出
#------------------------------------------------------------------------------------------
def readNews

	$news.each do |url|

		begin
			$agent.get(url)
			puts "getting " + url

			title = $agent.page.title.toutf8.chomp
			$log.info(title)

			text = title + " " + url

			#OAuth
			# http://d.hatena.ne.jp/shibason/20090802/1249204953を参考にしました
			consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
			oauth = Twitter::OAuth.new(CONSUMER_KEY, CONSUMER_SECRET)
			oauth.authorize_from_access(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
			twitter_client = Twitter::Base.new(oauth)
			twitter_client.update(text)

		rescue
		rescue Timeout::Error
		ensure
		end

	end
end

実行する

#---------------------------------------------------------------------------
# main
#---------------------------------------------------------------------------
def main

	# 板一覧を取得
	$agent.get(BOARD + SUBJECT)
	board = $agent.page.body.toutf8

	puts board

	# 読むべきスレッド名を取得
	dats = getThreads(board)

	# 不要なDATファイルを消去する
	deleteThreads(dats)

	# 必要なスレッドを読む
	dats.each do |name|
		crawl(name)
	end

	# ニュースをげっと
	if $news.size > 0 then
		$log.info("ニュースあり")
		readNews
	else
		$log.info("ニュースがありません><")
	end
end

#---------------------------------------------------------------------------
# エントリポイント
#---------------------------------------------------------------------------

#$log = Logger.new("log.txt")
$log = Logger.new(STDOUT)
$log.level = Logger::INFO
$agent = Mechanize.new

#かき集めたニュースURLが入る配列
$news = []

#mainを呼ぶ
main

実行結果

1272986450.dat<>■■■明日の日経平均を予想するスレ14685■■■ (54)
1272984242.dat<>日経225先物オプション実況スレ6264 (907)
9241004012.dat<>【2ちゃんねる十周年】携帯からお試し●を使ってみよう! 【規制?】 (30)
(中略)
I, [2010-05-05T03:10:00.196231 #858]  INFO -- : 1272744747.datを消去しました
(中略)
I, [2010-05-05T03:10:00.198558 #858]  INFO -- : 1272876392.datを消去しました
I, [2010-05-05T03:10:00.198603 #858]  INFO -- : http://anchorage.2ch.net/livemarket1/dat/1272986450.datを取得中
I, [2010-05-05T03:10:01.709770 #858]  INFO -- : http://anchorage.2ch.net/livemarket1/dat/1272984242.datを取得中
(中略)
I, [2010-05-05T03:11:03.133612 #858]  INFO -- : http://anchorage.2ch.net/livemarket1/dat/1266856385.datを取得中
I, [2010-05-05T03:11:04.344469 #858]  INFO -- : http://anchorage.2ch.net/livemarket1/dat/1270186921.datを取得中
I, [2010-05-05T03:33:28.767688 #1070]  INFO -- : ニュースあり
getting http://www.asahi.com/photonews/images/TKY201004290227.jpg
getting http://jp.reuters.com/article/topNews/idJPJAPAN-15081720100430
I, [2010-05-05T03:33:30.066649 #1070]  INFO -- : 
    ギリシャは厳しい緊縮財政措置の構え、市場は安心感で回復
| Reuters

getting http://www.bloomberg.co.jp/apps/news?pid=90920008&sid=au._wm7SnJDQ
I, [2010-05-05T03:33:33.845826 #1070]  INFO -- :   
		       ギリシャでデモ活動が激化、遺跡アクロポリス占拠も−緊縮財政に抗議  - Bloomberg.co.jp

なお、ここでつぶやいたニュースはボットをフォローすれば読むことができる。

参考になる本

[tmkm-amazon]4873113644[/tmkm-amazon]

どちらかというと機械学習の本で、サポートベクターマシン(SVM)とか面白い話題にも踏み込んである。

[tmkm-amazon]4798010618[/tmkm-amazon]

好きな本だけど新品で手に入れることは難しいようだ。

[tmkm-amazon]4873111870[/tmkm-amazon]

やや古いけどいい本。Perlで書いてあるけど内容は問題ない。マナーについてなど単に技術的な話だけに留まらないので是非読むべき。オライリーなのでGoogle Booksで読むことができる。

RubyでTwitter

  • 2010-05-03 (月)
  • Ruby

最初に書いたoauth.rbでトラブルに遭って困った。

#!/usr/local/bin/ruby -Ku
require 'rubygems'
require 'oauth'
require 'twitter'

すると

./oauth.rb:13: uninitialized constant OAuth (NameError)
	from /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require'
	from /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `require'
	from oauth.rb:3

となってしまう。原因は簡単で、ファイル名がoauth.rbだったからである。requireはカレントディレクトリを優先して読みに行くので、自分自身をrequireしていた。解決策もシンプルで別の名前にすればいい。

OAuth認証でTwitterを使う

まず

[root@PC twitter]# gem install twitter
When you HTTParty, you must party hard!
Building native extensions.  This could take a while...
Successfully installed oauth-0.4.0
Successfully installed hashie-0.2.0
Successfully installed crack-0.1.6
Successfully installed httparty-0.5.2
Successfully installed yajl-ruby-0.7.6
Successfully installed twitter-0.9.5
6 gems installed
Installing ri documentation for oauth-0.4.0...
Installing ri documentation for hashie-0.2.0...
Installing ri documentation for crack-0.1.6...
Installing ri documentation for httparty-0.5.2...
Installing ri documentation for yajl-ruby-0.7.6...
Installing ri documentation for twitter-0.9.5...
Installing RDoc documentation for oauth-0.4.0...
Installing RDoc documentation for hashie-0.2.0...
Installing RDoc documentation for crack-0.1.6...
Installing RDoc documentation for httparty-0.5.2...
Installing RDoc documentation for yajl-ruby-0.7.6...
Installing RDoc documentation for twitter-0.9.5...

一緒にoauth-0.4.0もインストールされる。

アプリケーションを登録する

  1. Twitterのアカウントがなければ取得する
  2. Twitterにログインする
  3. ログインした状態でWebブラウザで「Twitter / アプリケーション」にアクセスする
  4. 「新しいアプリケーションを追加」をする
  5. 必要事項を記入する

アプリ名などは自由に記入すればいい。わかりにくいのは「あなたの招待状」は「送信」を選ぶとよい。そうすると出てくる「Default Access type」は「Read & Write」にしておいた方がいいだろう。

必要事項を記入するとConsumer keyとConsumer secretなどが表示されるので記録しておく。

Access token, Access token secretを取得する

#!/usr/local/bin/ruby -Ku
require 'rubygems'
require 'oauth'

# http://d.hatena.ne.jp/shibason/20090802/1249204953を参考にしました

#OAuth
CONSUMER_KEY = "きー"
CONSUMER_SECRET = "しーくれっと"

consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
request_token = consumer.get_request_token

puts "Access this URL and approve => #{request_token.authorize_url}"
print "Input OAuth Verifier: "
oauth_verifier = gets.chomp.strip

access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)

puts "Access token: #{access_token.token}"
puts "Access token secret: #{access_token.secret}"

これを適切な名前を付けて保存する。oauth.rbにしたらひどいトラブルで苦労したので気をつける。実行すると

Access this URL and approve => http://twitter.com/oauth/authorize?oauth_token=とーくん
Input OAuth Verifier: 

と出てくるので、URLにアクセスして出てきた数値を入力すると、Access tokenとAccess token secretが得られる。これを記録しておく。

Twitterに何か書いてみる

#!/usr/local/bin/ruby -Ku
require 'rubygems'
require 'oauth'
require 'twitter'

CONSUMER_KEY = "きー"
CONSUMER_SECRET = "しーくれっと"
ACCESS_TOKEN = "とーくん"
ACCESS_TOKEN_SECRET = "とーくんしーくれっと"

consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
oauth = Twitter::OAuth.new(CONSUMER_KEY, CONSUMER_SECRET)
oauth.authorize_from_access(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
twitter_client = Twitter::Base.new(oauth)
twitter_client.update("こけこっこ")

実行すると「こけこっこ」とつぶやきが投稿される。

Home

Search
Feeds
Meta

Page Top