トップ 最新 追記

Public Diary


2012-01-03

[MBA] MBA出願エッセイ

2012年になりました。本年もどうぞよろしくお願いします。

アメリカの大学の冬休みは本当に長く、多くの学生は実家に帰ったり旅行に出かけたりするのですが、私の場合は諸事情で北米から出られないことになっています(いざというときは出られるんですけどね)。アメリカ国内も学生時代や転職の狭間でそれなりに旅行してますし、この冬休みはのんびりと自宅周辺で過ごしております。

そんな自分が昨年の今頃何をしていたのかなぁと思い返すと、締め切り直前のMBA出願用エッセイを正月返上で書き上げていました。それもこれも自分のタイムマネジメント不足が原因とは言えるのですが、しかしそれにしてもなかなか仕事をしながらのエッセイ書きというのも大変なものです。特に年の瀬はいわゆる年末進行な企業もあるでしょうし、そんな中、たとえば夜1時・2時に終電やタクシーで帰宅してからエッセイを書くのもなかなか現実的ではなく、結局週末や年末年始を使わざるを得ないわけですよね。

そんなわけで季節柄、エッセイの書き方について、渡米前に(というか1年前の出願時に)知っていればなぁということを書いてみます。

エッセイといえば、こちらに来て一番に学んだのはイントロダクションの書き方を含めたエッセイ全体の構造です。特に、イントロダクションは、エッセイの全体的な方向性を左右するので本来はとても重要なパートです。イントロダクションで特に重要なのは"thesis statement"と呼ばれる文です。これがエッセイや論文では非常に大きな意味を持ちますが、日本では聞いたことがありませんでした。僕は卒論を英語で書かされましたが、この論文を参考に同じように書けと言われただけで、論文の構造についてのちゃんとした説明は受けなかった。TOEFLの予備校のようなものにも通って「テンプレート」を教えてもらったけど、thesis statement の書き方のようなものは聞かなかった。でも、アメリカでは高校の英語の授業で教えてるんですよね。つまり thesis statement を知らない日本人の英語エッセイは、構造だけで言うと、アメリカ人の高校生の宿題以下ってことなんです。

Thesis statement は、エッセイの「結論」を一文にまとめたようなもので、通常、イントロダクションの最後に置かれます。ライティングのオンライン講座には定評のあるパデュー大学の資料(http://owl.english.purdue.edu/owl/resource/545/1/)あたりを読んでいただくのが一番わかりやすいと思いますが、議論可能である(arguable)ことと、ピンポイントであること(specific)ことが重要です。そして、広いバックグラウンドの導入から入ってこのピンポイントなthesis statementにつなげていくことが重要です。こうして、イントロダクションをしっかり作るだけで、相当印象が変わってきます。

Thesis statement は通常、「(1)及び(2)の観点から、(A)である」という構造を持ちます。たとえば、パデュー大学のサイトでは

High school graduates should be required to take a year off to pursue community service projects before entering college in order to increase their maturity and global awareness.

という例文を出していますが、「(A)高校を卒業して大学に入るまでの間に、1年間のコミュニティサービスプロジェクトに参加すべき」という主文に対して「(1)成熟するため、および(2)グローバルな知見を高めるため」という目的部分が続いています。そして、エッセイの本文の部分は、(1)及び(2)について議論します。thesis statementでは、"in order to" や "in view of" などの接続句を用いてこれらの部分をつなげるとともに、大まかな本文部分の構造を示唆するのが良いとされています。

Thesis statementそのものはとても簡単です。単なるテンプレートじゃないかと思われるかも知れませんが、アメリカの大学では(特にアカデミックな雰囲気を重んじる大学では)この作法を守っているかどうかというのが非常に重要です。卒論や修論を書くときに、指導教官から文献の引用スタイルについて細かく直された方は多いと思いますが、あれと同じようなものです。もちろん作法を守れば合格ということではありませんが、このような作法に従うのは一種の前提となっており、こうした作法を守らないエッセイはよほどインパクトが強くなければ本文に入る前に門前払いされてしまう可能性すら潜んでいます。よく日本のMBA予備校で一般人にはあり得ないような体験に基づくエッセイを書くことが求められますが、それは構造を無視しているから内容で勝負せざるを得ないということなんだと思います。アメリカ人のMBA受験はもっとおおらかで、マクドナルドのバイトでこんなことを学んだとか、その程度のリーダーシップだったりします。MBAの受験では、本来、世界をひっくり返すような体験は求められていないのです。むしろ、他の人が気づかないような毎日の出来事にきっかけを見いだせる人が求められているのではないかと思うのです。

友人に「テンプレートは嫌いなのでそういうのは使いたくない」という人もいましたが、これはテンプレートというよりも作法とも言うべきものです。論文のリファレンスにA.P.A.スタイルを用いるときに「著者(年)」を使うのが一般常識であるのと同じように、第1段落はthesis statementで終わるもの。thesis statementのない論文でもよほどイントロがしっかりしていれば読まれるでしょうけど、よほど英語での文才がない限りは、thesis statementを利用するのが無難です。自分でもthesis statementを意識するようになってくると、これが無いだけで随分と印象が変わってくるのが面白いです。

参考文献

シカゴ近郊の日系書店で英文エッセイの書き方について日本語で解説してある書籍を探してみましたが、ないようです。以下のリンクが参考になるでしょう。

追記

Thesis statementを用いた構造をしっかり意識すれば、英文エッセイはそれほど難しく考えなくても、ある程度は論理的に作ることができます。有名なStanford大学のMBAエッセイ課題 "What matters to you most?" については、以下のような考え方もできるでしょう。

  1. 過去の印象深かった出来事を思い出す
    • 「たとえば○○への旅行かなぁ」(*)
  2. その理由を3つほど思い浮かべる
    • 「普段の生活では知り合うことの無かった友人ができた、とか」(A)
    • 「新しい土地での習慣を知ることができた、とか」(B)
    • 「体力作りになった、とか」(C)
  3. Thesis statement を作る
    • 「○○への旅行(*)は、友人を増やし(A)、日本の地方についての新しい知識を与え(B)、さらに体力的な基礎を築く(C)という点で、非常に有益だった」
  4. Thesis statement へとつながるイントロダクションを作る
    • 一生の中で様々な出会いがあるが~云々。
    • いや !"#$% の方が重要だという人もいるだろう云々。
    • しかし、私にとっては、○○への旅行(*)こそが、友人を増やし(A)、日本の地方についての新しい知識を与え(B)、さらに体力的な基礎を築く(C)という点で、非常に有益だった。
  5. 本論よりも先に結論部分を書いてみる
    • 「以上で見てきたように、○○への旅行は~云々」
    • 「これからグローバルな視座が求められていく中で、この体験は~云々」
    • 「日本人すら知らない○○であるが、外国人にはぜひ○○を~云々」
  6. 本論部分を作る。(各1パラグラフ)
    • 「友人を増やすということについて~」
    • 「日本の地域を知ることは~」
    • 「体力的な基礎は~」

もちろん、論理的な流れであれば合格というものでもないでしょうから、上記のように考えれば必ず合格ということでもないでしょう。ただ、こういう構造を知らないがゆえにエッセイの印象を下げているということは多々あるものと思います。


2012-01-07

[アメリカ生活] クレジットカード

年末にスノーボードをしにウィスコンシンのGranite Peak Ski Areaまで出かけてきました。このとき、持参したクレジットカード(外貨建て)が使えなくて、フロントから呼び出されることになりました。認証エラーになるとのことでしたが、限度額までは余裕もあって原因がわからずじまいでしたが、結局念のために持っていた日本で発行されたクレジットカード(円建て)で決済しました。

よく、アメリカへ赴任・留学する人へは外貨建てのクレジットカードを持つことを勧められることがあります。これは、アメリカでは不正利用防止のために住所確認が行われますが、日本発行のクレジットカードには有効なアメリカ国内の住所が登録されていないことによるものです(したがって、決済通貨の問題ではなく、登録住所の問題です)。一般的にはJALカードかANAカードの外貨建てカードを作るものだと思います。これらは、アメリカ国内の銀行が発行するカードで、JAL/ANAと提携しているものです。普段使っている航空会社の提携カードを利用するのが良いでしょう。

ただ、住所確認は、登録住所から離れたところで決済されるものをマークしますので、たとえば東海岸に住む方がサンフランシスコベースの企業のウェブサイトでクレジットカード決済しようとすると、時々認証エラーになります。こうやって住所確認に引っかかったカードは、しばらくの間すべての取引で認証エラーになることがあります。今回の場合、発行会社に電話してみたところ、出発直前にネット通販で使ったのが「怪しい」とマークされ、カードに注意コードがかかったようでした。

こういったことは比較的頻繁に起こりうるので、アメリカで新しく発行したカードのほかに、念のため、日本発行のクレジットカードを用意しておくのが無難です。日本発行のクレジットカードは、ガソリンスタンドなど一部の業態のお店で利用できませんが、それでも飲食店やホテルなど一般的に旅行で利用しそうな業態のお店では問題なく利用できますので、安心のためにも、日本発行のクレジットカードとその引き落とし口座にはある程度の余裕をもって渡米するのが無難です。


2012-01-13

[鯖管] さくらのVPSにCentOS 6.2をインストールする

さくらのVPSでは、CentOS 5.5 (Final) (執筆時点) がインストールされていますが、FUSEなどの新しいものを使おうとすると、CentOS 6 系を使いたくなります。

CentOS 5.5 (Final)

CentOS のリリースノートは、「前のメジャー リリース(現在の CentOS 5 や CentOS 4)のインストールを『アップグレード』する手順をサポートしません」としており、アップグレードではなく、クリーンインストールが推奨されています。VPSの場合、手元にCD-Rを用意して…という手順が使えませんから、ネットワークインストールをすることになります。自分用のメモ代わりに、やり方を記録しておきます。

1. root でログインし、情報収集する

ブラウザ上で動くリモートコンソールでもいいですが、URL の入力などにコピペが使えないので、手元の端末からのリモートログインか、WindowsならTera Termなどを使ってログインした方が楽でしょう。

ログインできたら、ネットワークの設定に必要な情報をメモしておきます。

  # cat /etc/sysconfig/network-scripts/ifcfg-eth0
  DEVICE=eth0
  IPADDR=???.???.???.???     <= IPアドレス
  NETMASK=???.???.???.???    <= ネットマスク
  GATEWAY=???.???.???.???    <= ゲートウェイ
  ONBOOT=yes

  # /etc/resolv.conf
  nameserver 210.224.163.4   <= ネームサーバー
  nameserver 210.224.163.3
  search sakura.ne.jp

2. ブート用イメージをダウンロードする

手元の端末で、CentOS のミラーリストから最寄りのFTPミラーを選びます。バージョン (6.2) → os → アーキテクチャ (x86_64) → isolinux とリンクをたどっていった URL (ftp://ftp.kddilabs.jp/Linux/packages/CentOS/6.2/os/x86_64/isolinux/)からダウンロードできるファイルを使います。

1でログインしたサーバー上で、/boot/centos6 ディレクトリを作り、そこに移動します。

  # mkdir /boot/centos6
  # cd /boot/centos6

ダウンロードするファイルが複数(6.2は9個)あるので、最後にワイルドカード (*) を付け、シングルクオートで囲んで wget します。

  # wget 'ftp://ftp-srv2.kddilabs.jp/Linux/packages/CentOS/6.2/os/x86_64/isolinux/*'

これでネットワークインストールに必要なファイルがダウンロードできました。 ここからは、ブラウザ上からコンソールにログインできる「リモートコンソール」(とそのグラフィック版であるVNCコンソール)を使います。

3. ネットワークインストール用カーネルでの起動

VPSコントロールパネルからVNCコンソール(リモートコントロールにあります)を起動し、ログインしたらいったん再起動します。

  # reboot

リブートに成功すると、"Press any key to continue" の後すぐに

  Booting CentOS (2.6.18-194.26.1.el5) in 3 seconds...

とカウントダウンが始まりますので、カウントダウンしている間に何かキーを押して、旧カーネルでの起動を止め、GRUB メニューに切り替えます。

GRUB menu

キー c を押してコマンドモードにし、以下のように入力します。

  grub> kernel /centos6/vmlinuz
  grub> initrd /centos6/initrd.img
  grub> boot

ブートに起動すれば、インストーラーの画面が表示されます。

CentOS 6 installer

4. インストール

インストール中に利用する言語(Japaneseを選択)、キーボード(jp106を選択)、インストール方法(URLを選択)を指定します。続いてネットワークの設定になるので、

  [*] Enable IPv4 support
         ( ) Dynamic IP configuration (DHCP)
         (*) Manual configuration
  [ ] Enable IPv6 support

と選択します。(IPv4の設定はマニュアル指定する必要があります。IPv6は現時点では無効にしておいた方が無難なのでチェックを外します)IPv4の設定には、1.でメモした内容を使います。

続いてURLを入力する画面が表示されるので、2. でインストール用カーネルをダウンロードしたURLの一つ上の階層(CentOS 6.2 の場合だと ftp://ftp-srv2.kddilabs.jp/Linux/packages/CentOS/6.2/os/x86_64/)を入力します。OKを押すと、ファイルのダウンロードとチェックの後、"Welcome to CentOS!" という画面が表示されます。

Welcome to CentOS!

5. インストール続き

タイムゾーン(UTCを利用・Asia/Tokyoを選択)、rootのパスワードを入力。ディスクのパーティション設定は、クリーンインストールの場合 "Use entire drive" でいいでしょう。パーティションの設定をディスクに書き込んだら、しばらく自動的に処理が進み、インストールの終了です。

Completed

再起動が終わったら、すでにサーバーは通常のサーバーとして利用可能な状態になっています。SSHでログインできるので、コンソールなりTera Termなりでログインしましょう。ログインしたら、yum updateでパッケージを更新します。

追記

なお、TCP Segmentation Offload (TSO) のため、さくらVPSと一部プロバイダの間での通信に遅延が発生するらしいです。(参考:さくらのVPSで「CentOS」を利用していますが、回線速度が遅くアクセスに時間がかかります)説明を読んでも、プロバイダによって通信速度が劣化するのか理解しづらいですが、事実 TSO を off にするとアクセスが改善されたとの報告があったので、まぁそういうものなのでしょう。

以下の手順で TSO を off にします。

  1. /etc/sysconfig/network に NETWORKING_IPV6=no を追加
  2. /etc/modprobe.d/ipv6.conf を作成し、以下の設定を追加
    • alias ipv6 off
    • alias net-pf-10 off
  3. コマンド /sbin/chkconfig ip6tables off を実行
  4. サーバーを再起動

2012-01-20

[鯖管] AmazonとGoogleのクラウドストレージ

2011年10月、それまでの Google Storage for Developers が正式サービスとなり Google Cloud Storage としてリリースされました。リリース時にはそれなりに業界ニュースで報じられましたが(例:ITmediaニュース)、その後の続報があまりありません。クラウドWatchなどは、同サービスを「S3対抗」(2011/5/19)などとしていますが、どちらを使うべきなのでしょうか。

Amazon S3は、クラウドストレージとしては古参で、2006年にサービスを開始しました。歴史のある分、日本語のサポートもしっかりしており、また、東京にもリージョン(データが物理的に保存される地域)があるのが魅力です。一方のGoogle Cloud Storageは、2011年にリリースされたもののまだ「実験的な段階」にあるとされ、日本語サポートもありませんし、北米と欧州にしかリージョンがないのもネックです。

両者ともほぼ同等のAPIを利用できるので、Google側にドキュメントがないこと自体はそれほど問題になりませんが、やはり大きな差が出てくるのはリージョンです。以下は、東京に置かれたサーバーから両者のバケットへ ping を打ってみた結果です。RTT で実に10倍の差があります。もちろん、RTTが10倍だからスループットも10倍というわけではありませんが、やはり一つ一つの動作について、レスポンスの違いを体感できる程度になってきます。

** Amazon S3 Tokyo region
--- s3-ap-northeast-1-w.amazonaws.com ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9018ms
rtt min/avg/max/mdev = 3.361/3.441/3.542/0.095 ms

** Google Cloud Storage US region
--- sandbox.l.google.com ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9047ms
rtt min/avg/max/mdev = 33.249/34.234/35.038/0.590 ms

なお、S3QLに付属のベンチマークスクリプトを使ってスループットを計測したところ、Amazon S3の東京バケットは4MB/s、同北米バケットが1MB/s、Google Cloud Storageの北米バケットが1MB/sでしたので、サービス提供者の違いではなく、単にリージョンの違いと言えそうです。

一方で、Google の管理インターフェイスはS3よりも使いやすそうな可能性を持っています。格納オブジェクト数(ページ数)を正しく表示できないなどのバグもあり、現時点ではちょっと使いづらいと言わざるを得ませんが、今後少しずつ改善されていくでしょう。また、Google自らコマンドライン管理ツール GSUtilを提供しているのは望ましいことでしょう。

したがって、現時点では、Googleのクラウドソリューションは日本向けではなく、当分の間、Google Cloud Storage は見送って Amazon S3 を利用するのが良いが、Google には一定の可能性も見いだせる、とまとめられましょうか。


2012-01-27

[プログラミング] TwitterのツイートをtDiaryにポストするスクリプト

(1/29追記) 手っ取り早くスクリプトをダウンロードしたいという方は、1/29の記事の最後の段落を参照してください。

Twitterは短いツイートを簡単に投稿できる点が便利な反面、あっという間にタイムラインが流れ、あとでまとめて読んだりするのに向いていません。そのため、ブログを運営している人であれば一日分のツイートをまとめてブログに掲載したり、メーリングリストを運営している人であればメーリングリストに転載するなどの工夫が行われてきました。

すでにtw2tdiaryなどのプログラムも公開されていますが、過去可能な限りさかのぼって全部のツイートを日別にまとめてtdiaryに投稿するようなプログラムは見つからなかったので、Rubyの勉強もかねて、自分で一から作ってしまうことにします。

環境 (Disclaimer)

想定する環境は CentOS 6.2 で動作している Ruby 1.8.7 です。

また、私はC系プログラム(C/C++/C#/Java)やレガシーインタープリター(Perl/bash)でのプログラミング経験はありますが、Ruby はまったくの初心者ですので、プログラムの作法がRuby的でなかったり、一部の用語を誤って使っている可能性があります。

(1) ライブラリの準備

まず、一部のライブラリを利用するため、gems と irb をインストールします。

 # yum -y install ruby-irb rubygems

(でもなんでgemsは ruby-gems ではなく rubygems (ハイフンが入らない)なんですかねぇ)

Ruby には Twitter なる便利そうなライブラリがあるので、これをインストールします。

 # gem install twitter

(これまた、なんでパッケージ名は rubygems なのにコマンド名は gem (単数形)なんですかねぇ)

とりあえずこれで必要最低限のライブラリはそろったようです。

(2) 過去のツイートを取得するテスト

で、とりあえず過去のツイートを数件取得するには、こんなプログラムでいいそうです。

require 'rubygems'
require 'twitter'

tester = Twitter.user_timeline('wassy')
tester.each do |t|
  puts "=" * 70
  puts "* created_at: " + t.created_at
  puts "* id: "         + t.id.to_str
  puts t.text
end

なるほど、確かに簡単です。

このTwitterライブラリは、ある程度TwitterのREST APIに忠実に作られているようで、オプションの引数は、user_timeline(SCREEN_NAME, {:count=>COUNT, :max_id=>MAX_ID, ...}) といった形で与えてあげれば良いようです。

ただ、このライブラリのドキュメントを読んでも(初学者には)こういったことはさっぱりわからないわけで、もうちょっとドキュメント周りがしっかりしててくれればなぁと思わないわけでもありません。

(3) 「昨日一日分」のツイートを取得する

(2)で作った簡易プログラムは、過去20件分のツイートだけを取得します。これに、さらにツイートが「昨日」に含まれるかどうかを判定しながら表示する処理を加えます。

Twitterライブラリの時刻の表現は、Timeオブジェクトを用いて行われています。Time オブジェクトはUNIXタイム(UTCでの1970年1月1日0時0分0秒)経過秒なので、ローカルタイムにおける「昨日」を求めるのはちょっとトリッキーです。ツイッターブログで教えていただき、

 today     = Time.local(*(Time.now).to_a[3..5].reverse)
 yesterday = today - 86400

あたりが相当するだろう、ということになりました。(86400は一日あたりの秒数です)

この値を使い、ツイートの created_at が yesterday <= created_at < today を満たすかどうか調べながら、繰り返しツイートを取得します。

require 'rubygems'
require 'twitter'

today     = Time.local(*(Time.now).to_a[3..5].reverse)
yesterday = today - 86400
lasttime  = today

begin
  tester = Twitter.user_timeline('wassy')
  if tester then
    tester.each do |t|
      lasttime = t.created_at
      if yesterday <= t.created_at and t.created_at < today then
        puts "=" * 70
        puts "* created_at: " + t.created_at.to_s
        puts "* id: "         + t.id.to_s
        puts t.text
      end
    end
  else
    lasttime = 0
  end
end while (yesterday <= lasttime)

(4) ツイートを処理する部分

Rubyでツイートを扱うのは結構簡単そうだということがわかったので、今度はツイートをファイルに書き出す処理を考えます。とりあえず当座の処理として、カレントディレクトリに YYYY-mm-dd という形式のファイル名で書き出します。

def write_to_file(day, tweets)
  if tweets then
    filename = day.strftime("%Y-%m-%d.txt")
    fh = open(filename, "w")
    tweets.each do |t|
      fh.puts "=" * 70
      fh.puts "* created_at: " + t.created_at.to_s
      fh.puts "* id: "         + t.id.to_s
      fh.puts t.text
    end
    fh.close
  else
    return 0
  end
end

なんだかRubyのTimeクラスって暗黙にローカルタイムを利用してるのがちょっと気持ち悪いですかねぇ…。あと、fh.puts にカッコを付けようとするとインタープリターに怒られるのですが、結合順位というかオペレーターの優先順位が見た目にはっきりせず、なんだか落ち着きませんねぇ。

(5) 過去数日分のツイートをまとめて取得

というわけで、「昨日」だけでなく、過去数日分をまとめて取得するプログラムにします。

$screen_name = 'wassy'
$max_days    = 10

dayend   = Time.local(*(Time.now).to_a[3..5].reverse)
daystart = dayend - 86400
data = []
cont = true
lastid = 0
days = 0

def write_to_file(day, tweets)
  # 省略
end
begin
  if lastid == 0 then
    tester = Twitter.user_timeline($screen_name)
  else
    tester = Twitter.user_timeline($screen_name, {:max_id => lastid-1})
  end

  if tester then
    tester.each do |t|
      lastid = t.id
      if dayend <= t.created_at then
        # 今日分のツイート: 何もしない
      elsif daystart <= t.created_at then
        # 対象とするツイート
        data << t
      else
        # さらに昔のツイート
        write_to_file(daystart, data)
        days = days + 1
        if $max_days <= 0 or days < $max_days then
          data     = [t]
          dayend   = dayend - 86400
          daystart = daystart - 86400
        else
          data = []
          cont = false
          break
        end
      end
    end
  else
    # ツイート取得エラー
    cont = false
  end
end while (cont)

if data.size > 0 then
  write_to_file(daystart, data)
  days = days + 1
end

puts "Acquired tweets for " + days.to_s . " days."

最初、days = days + 1 の部分を、C系言語風に days++ としてはまってしまいました。Ruby にはインクリメント演算子はないようで、((days).+).+ と解釈され左辺値がないことになってしまうから、だとか。うぅむ。

とりあえず今日はここまで。今日の成果物は、tweet2file_20120127 として置いておきます。明日以降の日記に続きます。


2012-01-28

[プログラミング] TwitterのツイートをtDiaryにポストするスクリプト (続き)

昨日tweet2file_20120127をベースに、今日は細々とした処理を追加していきます。

ツイートの装飾

昨日の段階では、ファイルへのツイートの書き込み時、以下のような書き込みを行うようにしています。

tweets.each do |t|
  fh.puts "=" * 70
  fh.puts "* created_at: " + t.created_at.to_s
  fh.puts "* id: "         + t.id.to_s
  fh.puts t.text
end

これを、もう少しHTML的な出力にします。とりあえず、

<li><a href="パーマリンク">時刻</a> ツイート内容 <img src="rt.png" width="24" height="24" alt="発言者への返信" /></li>

という表記にし、加えてツイート本文中に @ があれば、そのユーザーのホーム画面へのリンクを張るくらいのことはしてみましょうか。

Twitterでは、各ツイートに「パーマリンク」と呼ばれるリンクが付けられていて、https://twitter.com/$screen_name/status/$id といったものになっています(一般的なシェル系の表記を用いています)。従って、<a>~</a> の部分については、

 line = "<a href=\"https://twitter.com/#{$screen_name}/status/#{t.id}\">#{t.created_at.strftime("%H:%m")}</a>"

でいけそうです。

続いて、Ruby の正規表現を用いて、@??? という文字列を <a href="https://twitter.com/???">@???</a> に置き換えます。ツイッターのアカウント名は、半角英数字+アンダースコアのみ利用可能で、文字数は1文字~15文字ということなので、否定の先読みを使いつつ、

text = t.text
line += text.gsub(/@([0-9A-Za-z_]{1,15})(?![0-9A-Za-z_])/) do
  url = "https://twitter.com/#{$1}"
  "<a href=\"#{url}\">#{$1}</a>"
end

といったところでしょうか。なんだか text.gsub() をしたあと text 自身が置換前のテキストを持っているのはちょっと気持ち悪いですかね。…と思ったら、gsub!() なんてメソッドがあるですか…うーむ。なお、最近は https://twitter.com/wassy といったように #! が入ることも多いと思いますが、Twitterのプロフィール表示では #! のないURLが表示されますので、こちらを使うことにします。

最後に、このツイートが過去のツイートへのリプライ等の場合に、参考リンクを付ける処理を行います。

if t.in_reply_to_status_id > 0 then
  link = "https://twitter.com/#{t.in_reply_to_screen_name}/status/#{t.in_reply_to_status_id_str}"
  line += "<a href=\"#{link}\"><img src=\"icons/rt.png\" alt=\"#{t.in_reply_to_screen_name}へのリプライ\" border=\"0\" /></a>
}

line += "</li>"

ところで、ツイート中に含まれるURLには自動的にリンクを張りたいものです。 これを処理するのに、URIライブラリが使えそうです。

require 'uri'

uri_reg = URI.regexp(%w[http https])
text.gsub!(uri_reg) { %Q{<a href="#{$&}">#{$&}</a>} }

ツイート中に含まれるリンクは、最近は t.co を使って省略されていることがあります。さらに、URLを展開するとそれはツイートに含まれる画像ファイルだったりすることもあります。できればこれらを単なるリンクとは区別して処理したいものです。

ツイートの装飾(クラスの拡張)

実は Twitter API では、user_timeline メソッドに対して include_entities オプションを渡すと entities として短縮URLや画像の情報が返されます。実験してみると、

require 'rubygems'
require 'twitter'
require 'pp'

ts = Twitter.user_timeline("wassy", {:include_entities=>true})
pp ts.first

という簡単なプログラムで

#<Twitter::Status:0x7f62bfa99998
 @attrs=
  { ...
   "entities"=>
    {"urls"=>
      [{"expanded_url"=>"http://bit.ly/...",
        "url"=>"http://t.co/Vx.....",
        "indices"=>[30, 50],
        "display_url"=>"bit.ly/..."}],
     "hashtags"=>[],
 ...

と、確かに entities が得られています。が、どうやらentitiesに対してはアクセサが用意されていないようで、クラスの外部からこの情報にアクセスできそうにないという問題点が出てきました。

こういうとき、Ruby ではさくっと既存クラスを拡張できるようで、今回は(やや禁じ手ですが)インスタンス変数に対する読み込み専用のアクセサを用意し、そこから情報を引き出すことにします。

module Twitter
  class Status
    attr_reader :attrs
  end
end

この5行を追加するだけで、内部の変数であった attrs にアクセスすることができるようになります。試しに、

 twts = Twitter.user_timeline("wassy", {:include_entities=>true})
 pp twts.first.attrs["entities"]["urls"]

とすると、本来アクセスできなかった entities => urls => expanded_url などにアクセスできるようになっています。あぁこういうライブラリの拡張のしやすさはRubyならでは、かも知れませんね。

以上を踏まえ、各ツイートの entities に対して

  • media に含まれるURLについてはサムネイルを取り出し
  • urls に含まれるURLは展開

を行い、その上で上記2件に該当しないURLについては通常のリンクを張るという処理を行うことにしました。現段階で、

といったマークアップができるようになってきました。もう一息です。これに若干修正を加えたものを tweet2file_20120128 として保存してあります。明日に続きます。


2012-01-29

[プログラミング] TwitterのツイートをtDiaryにポストするスクリプト (とりあえず最後)

昨日の続きです。

(1) tDiaryへの投稿

まず、実際に update.rb へツイートを投稿する部分を作ります。今までファイルに書き込んでいた write_to_file というメソッドを少し改造します。

参考にするのは、tDiary の contrib に含まれている posttdiary.rb というファイルで、これは電子メールで送付されてきた日記をウェブ経由で tDiary にポストするプログラムです。なぜファイルの入出力を使わないのかとも思いましたが、おそらく前方互換性なりファイルシステムの変更なりに対応するためということでしょう。ファイルI/Oが使えないなら最初からRubyで作る必要もなかったか…という気もしますが。

このあたりはあまり悩む必要もなく純粋にPOSTでデータを渡すだけなので、ほぼコピー&ぺーストで作っていきます。なお、update.rb の場所、Basic認証のユーザ名・パスワード等は、今のところグローバル変数で定義しています。

$tdiary_url   = 'http://.../update.rb'   # update.rb のURL
$tdiary_uname = '.....'   # update.rb にアクセスするのに必要な Basic 認証用ユーザ名
$tdiary_pass  = '.....'   # update.rb にアクセスするのに必要な Basic 認証用パスワード
$diary_heading = '![Twitter] 本日のツイート'  # 日記のサブタイトル
def post_tweets(day, tweets)
  if tweets.size > 0 then
    html_data = $diary_heading + "\n"
    tweets.sort {|x,y| x.created_at <=> y.created_at }.reverse.each do |t|
      html_data += tweet2html(t) + "\n"
    end

    data = "year=#{day.year}&month=#{day.month}&day=#{day.day}"
    data << "&body=#{CGI::escape html_data}"
    data << "&append=true"
    data << "&makerss_update=false"

    uri = URI.parse($tdiary_url)
    inre = /<input type="hidden" name="csrf_protection_key" value="([^"]+)">/

    Net::HTTP.start(uri.host, uri.port) do |http|
      auth = ["#{$tdiary_uname}:#{$tdiary_pass}"].pack('m').strip
      res, = http.get(uri.path,
                      "Authorization" => "Basic #{auth}",
                      "Referer" => $tdiary_url)

      if inre =~ res.body then
        data << "&csrf_protection_key=#{CGI::escape(CGI::unescapeHTML($1))}"
      end

      res, = http.post(uri.path, data,
                       "Authorization" => "Basic #{auth}",
                       "Referer" => $tdiary_url)

    end
    puts day.strftime("Posted tweets for %Y/%m/%d. Sleeping for 1 sec.")
    sleep 1
  end
end

(2) コマンドラインオプションに対応する

この時点で概ねやりたいことには対応していますが、動作検証しやすいよう、コマンドラインオプションを付け加えていきます。

以前は Ruby も他の言語と同様 getopts を使ってコマンドラインオプションをパースしていましたが、最近(Ruby 1.8.2以降)は OptionParser というものを使うそうです。

parser = OptionParser.new
cmd_opts = Hash.new("#{__FILE__}: post tweets to tdiary.\nUsage: #{__FILE__} [options]")
parser.on("-d", "--debug", "enter debug mode")      { |v| cmd_opts[:debug]   = true }
parser.on("-v", "--verbose", "enter verbose mode")  { |v| cmd_opts[:verbose] = true }
parser.on("--max-id ID", Integer,
          "retrieve tweets with id smaller (older) than ID") { |v|
           cmd_opts[:max_id]  = v }
parser.on("--days MANDATORY", Integer,
          "set maximum days to retrieve tweets")   { |v|
          cmd_opts[:days] = v }
parser.on("--cont", "continue from the previous run (read max_id from cache)") { |v|
          cmd_opts[:cont] = true
          cmd_opts[:max_id] = read_lastid_from_cache
}

begin
  parser.parse!(ARGV)
rescue OptionParser::ParseError
  $stderr.puts "#{__FILE__}: error #{$!}"
  exit 1
end

def debug_puts(txt, level=0)
  puts " " * level + txt if (cmd_opts[:debug] || false)
end

(3) できあがり

その他、リツイートされたツイートには RT マークを付けるなど細かいところを修正し、最終的にできあがったのが tw2td-20120129.tar.gz です。


1980|03|
1986|04|
1998|04|
2002|01|11|
2003|03|04|05|07|08|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|02|03|04|06|07|08|11|12|
2008|01|02|03|04|06|07|08|09|10|
2009|01|12|
2011|05|10|11|
2012|01|02|10|