☆軽量で操作性の良いTwitterクライアントPino

少し前に見つけたPinoというTwitterクライアントの紹介です。
TwitterのほかにIdenti.ca というのにも対応しているようです。
Valaとう言語で書かれています。
Version 0.21では日本語が入力できなかったのですが、0.24になって日本語入力できるようになりました。
残念ならが日本語化されていません。
本家サイトはこちら
pino-twitter - Project Hosting on Google Code

導入方法は本家サイトにも書いてある通りで、Karmic以上であれば下記のコマンドで導入できます。
Lucid Beta-1でも同じ操作で導入できることを確認しています。

$ sudo add-apt-repository ppa:vala-team/ppa 
$ sudo add-apt-repository ppa:troorl/pino
$ sudo apt-get update
$ sudo apt-get install pino

画面はLucidに入っているGwibberと似ています。検索機能や複数のTLを流す機能はありません。
気にいっている点は、
・軽快な動作
・Ctrl+1,Ctrl+2,Ctrl+3,Ctrl+4でHome、Mentions、Direct Message、User Infoを切替可能
・投稿はAlt+Enter
最後の投稿操作は何処にも書いていなかったので迷いました。
しかし私は漢字変換の確定にEnterを使うので、誤ってPostしてしまうことが少なくてよいと思っています。


メイン画面のスクリーンショット

User InfoはHome TimelineやMentionsでプロフィール画像をダブルクリックして表示する方法と、
ユーザ名を入力する表示する方法の2種類があります。
残念ながらダイレクトメッセージを頂いたことがないのでDirect Messagesはさびしいスクリーンショットになってしまいました。

Home Timeline (Ctrl+1) Mentions (Ctrl+2) Direct Messages (Ctrl+3) User Info (Ctrl+4) [Show User]押下後のUser Info

メニューの中身

これは見たまんまです。特に説明は不要だと思います。
複数のアカウントを登録して切り替えることが出来そうですが、一つしかアカウントを持っていないのでわかりません。

[Pino] [Edit] [View] [Help]

Preferences(設定画面)

デフォルトではフォントサイズが9ポイントなのですが、日本語は9ポイントではちょっときついと思うので大きくすることをお勧めします。
[Enable Spell checking]はオフの方がいいと思います。(これを書いていて気付きました。)

Main Desktop Account Appearance

gnome-sudoku bug evidence

initial state
Symbol definition
A:Cell(1,5)
B:Cell(1,7)
C:Cell(2,9)

Case1 and Case6 are wrong.
Last input Number dose not change to red.

Case1
A->B->C
Case2
A->C->B
Case3
B->A->C
Case4
B->C->A
Case5
C->A->B
Case6
C->B->A

☆KarmicにBlogtk2をいれるとGwibberが起動しなくなる件

KarmicでjayredingさんのPPAを追加してblogtkを導入すると、gwibberが起動しなくなることに気付きました。
blogtk2の起動でも出た"enchant error for language: ja_JP.UTF-8"です。
原因はgwui.pyとblogtk(2)につられて入るpython-gtkspellです。
(/usr/lib/python2.6/dist-packages/gwibber/gwui.py)
問題になるのは最初のほうにある

try:
  import gtkspell
except:
  gtkspell = None
と、後ろのほうにある
    if gtkspell:
      self.spell = gtkspell.Spell(self, None)

gwibberパッケージの依存性からはpython-gtkspellパッケージが導入されません。
したがって冒頭部分でgtkspellがNoneになるので、後ろのほうのコードが実行されません。

ここへpython-gtkspellパッケージが導入されると後ろのほうのコードでエラーが発生します。

もともとgwibberはpython-gtkspellなしで動作しますし、
gwibberがらみのソースをgrep -i -e spell しても使っている形跡がありません。
下記のように変更してしまえばgwibberが動作するようになります。

gwui.pyへのパッチ
@@ -15,10 +15,11 @@
 
 IMG_CACHE_DIR = os.path.join(resources.CACHE_BASE_DIR, "gwibber", "images")
 
-try:
-  import gtkspell
-except:
-  gtkspell = None
+# try:
+#   import gtkspell
+# except:
+#   gtkspell = None
+gtkspell = None
 
 class Color:
   def __init__(self, hex):

ついでにblogtk(2)の方も見てみたのですが、こちらもgtkspell.Spellから取得したものを使っている様子はありません。
なんなのでしょうかgtkspell。なぞです。

この時期なのでLucid Beta1でどうなったか気になったので調べてみました。
blogtkのバージョンは古い系統の1.1-2でpython-gtkspellとは無縁でした。
(依存性の指定が不足しているようで起動エラーになりましたが....)
gwibberは新しくなっていて、依存パッケージにpython-gtkspellが入っています。
さすがに標準アプリになっただけあって起動エラーにはなりません。
調べてみるとgtkspell.Spellの呼び出しがtry:、except: で囲まれてました。
grep -i -e spellしてみたのですが、相変わらず使われてなさそうです....。
なんなのでしょうかgtkspell。なぞです。

☆普段使わないパッケージコマンドやオプションをメモ

普段使わないパッケージ関連のコマンドやオプションをとりあえずメモ
・apt-file
  ファイルの検索
  --ignore-case(-i)
  --regexp(-x)

・apt-cache showsrc

・gep-dctrl,grep-status,grep-available
  sea also "/var/lib/dpkg/status","/var/lib/dpkg/available"

・apt-cache dumpavail
  利用可能なパッケージの表示

・apt-cache showpkg
  パッケージの情報をAPTの内部形式で表示
  sea also apt-show-versions


・apt-get --simulate
  実際には実行しない installと一緒に使う

・apt-get --print-uris
  debファイルの取得元を表示する。installと一緒に使う
  キャッシュにファイルが存在すると表示しない
  この場合はcleanするか--reinstallを指定。

・apt-listchanges
  パッケージの変更履歴を確認する
  同名のパッケージを導入する必要あり
  導入後 sudo dpkg-reconfigure apt-listchanges
  http://ascii.jp/elem/000/000/475/475007/index-3.html
  http://www.debian.org/doc/manuals/apt-howto/ch-search.ja.html

・apt-rdepends
  依存関係の表示。再帰する。
  --reverse,--build-depends とかある
  sea also apt-cache depends,apt-cache rdepends
  http://d.ma-aya.to/?date=20091216

・apt-mark showauto
  依存関係で自動的にインストールされたパッケージを表示
  
・dpkg --audit
  パッケージの状態チェック

・dpkg --configure --pending
  インストールがこけた場合の修復

・dpkg-reconfigure
  設定のやり直し

・dpkg-query
・dpkg --stasus(-s) パッケージ名
・dpkg --info(-i) パッケージファイル
・dpkg --field(-f) パッケージファイル
・dpkg --contents(-c) パッケージファイル

・dpkg-depcheck
  http://www.netfort.gr.jp/~ohura/diary/200501-1.html#07
  dpkg-depcheck(1) は特定のコマンドを strace 上で実行し、 そのコマンドが必要とするパッケージを表示する。 


・dpkg-source -x hoge.desc
  ソースを展開してパッチを適用
  (see also apt-get source)

・debfoster
  同名パッケージを導入する必要あり。

・apt-cache showsrc パッケージ名

・dpkg-buildpackage
・debuild
・dpkg-genbuilddeps
・debsign

・pkg-config
  gcc `pkg-config --cflags --libs` みたいに使う
  http://dolphin2005.blog.so-net.ne.jp/2008-02-17-2

・ディレクトリ、ファイル
/var/lib/dpkg/info

/var/log/installer/initial-status.gz
  インストール時のstatus

・推奨パッケージをインストールしない
sudo apt-get --no-install-recommends install devscripts

・パッチを当てたあとにパッケージのバージョンを上げる
dch -v "VERSION"

changelogの雛形が作成されるので、適当に入力してCtrl+Oで保存
このときファイル名から「.dch」を取り除く

☆Twitterの投稿時刻をPerlでローカル時刻にする

Twitterの投稿時刻は投稿してから24時間以内は"約○○前"のように現在との時間差で表示されます。
ログインしていない場合、1日以上経過しているつぶやき時刻は日本との時差がなぜか17時間(PSTの場合。PDTの時期は16時間)もあります。
"約○○前"の表示はあまり好きではありませんし、自分のつぶやき時刻が17時間もずれて表示されているとびっくりしてしまいます。
そこでPerlで時刻の表示を一律 %Y/%m/%d %H:%M:%S の形式に変更しようと思いました。
使用する環境はUbuntu 9.10 Karmic koalaです。

目をつけたのは、下記の data="{time: の後ろの文字列です。

<span class="published timestamp" data="{time:'Tue Mar 16 12:44:41 +0000 2010'}">約1時間前</span>
この形式を扱えるモジュールを探します。
"perl Datetime"で検索すると、結構たくさんのモジュールが引っかかるのですが、どれを使えばよいのかわかりません。
とりあえず、DateTime::Format::W3CDTF と DateTime::Format::Mail を試したのですが、うまくいきません。
検索条件をいろいろ変えてみて、Twitter のStreaming APIにおける日時表現の解析についてというサイトに行き着きました。
DateTime::Format::DateParse で可能と書いてありました。私が扱おうとしているのはStreaming APIではないのですが、時刻の形式は一緒です。

DateTime::Format::DateParseは "libdatetime-format-dateparse-perl" パッケージに含まれています。
依存関係は

Depends: perl (>= 5.6.0-16), libdatetime-timezone-perl (>= 0.27), libdatetime-perl (>= 0.29), libtimedate-perl (>= 1.16)
となっています。

Manpageをみると

SYNOPSIS
           use DateTime::Format::DateParse;

           my $dt = DateTime::Format::DateParse->parse_datetime( $date );
           my $dt = DateTime::Format::DateParse->parse_datetime( $date, $zone );

となっているのですが、なぜか引数を2つとる形式はうまく動きませんでした。

DateTime::Format::DateParseを先日のtw_rev.plに組み込むと下記のようになります。

tw_rev2.pl
#!/usr/bin/perl
use DateTime;
use DateTime::Format::DateParse;
my $tzhere = DateTime::TimeZone->new( name => 'local' );

sub repTime
{
  my $gmt = shift;
  $dt = DateTime::Format::DateParse->parse_datetime($gmt);
  $dt->set_time_zone($tzhere);
  return $dt->strftime('%Y/%m/%d %H:%M:%S');
}


# search start html header
while(<>){
  print;
  last if m#<head>#;
}

# For relative link. ex list in right side.
print qq(<base href="http://twitter.com/" />\n);

# search start of tweets
while(<>){
  print;
  last if m#<ol id='timeline' class='statuses'>#;
}

# stack each tweets
my(@TWEET);
OUTER:
while(1){
  my(@tweet);
  while(<>){
    last OUTER if m#</ol>#;
    s#(<span class="published timestamp" data="{time:'([^']+)'}">)[^<]+</span>#$1 . repTime($2) . "</span>"#e;
    push @tweet, $_;
    last if m#</li>#;
  }
  unshift @TWEET, \@tweet;
}

# output each tweets
my($tweet);
for $tweet (@TWEET){
  my($line);
  for $line (@$tweet){
    print $line;
  }
}
print qq(</ol>\n);

# output remaining
while(<>){
  print;
}

☆TwitterのHTMLをPerlでDOM的に扱おうとして絶望

これは失敗の記録です。PerlでTwitterのHTMLをDOM的に扱おうとして断念しました。

●やりたかったこと

Twitterのタイムラインを通常の逆順(投稿時刻の昇順)に表示する。
先日作ったperlスクリプトは改行に依存している部分があってTwitterのHTMLが少し変わっただけで影響を受けてしまいます。
この欠点をXHTMLを解析できるモジュールを使って克服したい。

●そもそもなぜTwitterのAPIではなくHTMLを扱おうとしたか

GUIアプリを作るのは大変手間がかかる(LinuxではそもそもGUIアプリを作るスキルがない)ので、ブラウザで表示したい。
APIでとってきたXMLまたはJSONをHTMLに組み立てなおすのは結構手間がかかります。

●何がうまくいかなかったか

Twitterの返すHTMLは & がエンティティ化されておらず、XML::DOM::Parser、XML::LibXMLともにエラーが発生しました。
& をエンティティ化しようとすれば、それがリンクの一部でないかなどの判定が必要で、実質的にHTMLを解析する必要があります。
これではモジュールを使って手間を省く意味がありません。
また蛇足ですが、XML::DOMを使う場合、XML::DOM::DocumentはgetElemetByIDメソッドが存在しないので、
つぶやきの&lt;li&gt;を持っている&lt;ol&gt;ノード"&lt;ol id='timeline' class='statuses'&gt;"を取得するために余計なロジックが必要になります。
XML::LibXMLは&に引っかからない場合、idの重複でエラーが出ます。これはbody要素とひとつのdiv要素に同じid="profile"が存在するためです。
(XML::DOMがエラーを吐かないのが不思議です。)

参考:XML::DOM::Parserのエラーメッセージ(&が原因)
not well-formed (invalid token) at line 577, column 181, byte 27722 at /usr/lib/perl5/XML/Parser.pm line 187

参考:XML::LibXMLのエラーメッセージ(&が原因)
test.html:681: parser error : EntityRef: expecting ';'
参考:XML::LibXMLのエラーメッセージ(id重複が原因)
test.html:933: validity error : ID profile already defined

●その他、雑記

■Perlのモジュールに対応するUbuntuのdebパッケージを探す方法について

apt-cache searchはあてにならない気がします。
http://packages.ubuntu.com/ の"Search the contents of packages"は検索文字列に"/"が入っているとまったくマッチしなくなるようなので使えない。
apt-fileを使うのがベストだと思います。導入と使い方は Ubuntu Weekly Recipe 第16回参照。
XML::DOM::Parserを探したい場合、apt-file XML/DOM/Parser とやればOKです。

■debパッケージのないPerlモジュールはどうする

自分では試していませんが、Ubuntu日本語フォーラムの記事を見てみたらdh-make-perlでdeb化するのがよさげです。

■debパッケージがなかったので試さなかったPerlモジュール
▼HTML::TagParser

http://www.kawa.net/works/perl/html/tagparser.html
これはパースできるだけの模様。ノードを付け加えたり、削ってからHTMLを吐くというのは出来なさそうです。

▼XML::Liberal

http://search.cpan.org/dist/XML-Liberal/lib/XML/Liberal.pm
XML::Liberal - Super liberal XML parser that parses broken XML とあるので多少XMLに反する部分があってもやってくれそうです。
試してないのでわかりません。

●結局

☆Twitterのタイムラインを逆順に表示するで作成した、tw_rev.plは、つぶやきが含まれているol要素の始まりと終わり、各li要素の区切さえうまく捕まえることが出来ればよいように出来ています。
であれば、多少Twitterの吐くHTMLが変わったとしてもそれを補正してやるフィルタをかませてからtw_rev.plに入力すればいいので、大抵の場合、比較的簡単に対応が出来そうです。
直近にあった</li>のあとに<li>や</ol>が来てしまう変更は、tw_rev.plを直そうとすると以外に面倒ですが、sedで事前に改行を入れてやるだけで済みました。

☆Twitter のタイムラインを逆順に表示する(3)

こんどはpython-twitter編です。
Ubuntu(Karmic使ってます)ならapt-getで入ります。
ドキュメントをみてもソースコードをgrepしてみてもリスト機能に対応していないようです。

ならば、継承して足せばよい、というわけでリストのタイムラインを取ってくる機能だけ追加してみました。
テストのために取ってきた情報を端末へ出力するコードも書いてあります。
xfterm4やgnome-terminalであればURLを右クリックしてブラウザで開けるので
これだけで簡易Twitterリーダーになります。
(取ってきた情報でHTMLを組み立てるのが面倒なだけです....)

これをやって気づいたのはアイコンの重要性。
誰が発言したかを見分けるのにとても役に立ってるんだと再認識しました。

引数は二つ、ユーザと取得数です。リストのタイムラインを取得したい場合は user/listのようにして指定します。

tw_list.py ubuntubot 50
tw_list.py MidSpecLowLoad/ubuntu-ittoke-doujou 50
出力の例
--------------------------------------------------------------------------------
ubuntubot : 2010-03-03 10:22:49 
Ubuntuのシステムディスクを交換 - こだわり、ときどき、やけくそ ...: UbuntuのCD-ROMから起動する。ここでは、日本語Remixのデスクトップ版を使う。 dumpと restoreをインストールする. /et... http://bit.ly/8YpApo
--------------------------------------------------------------------------------
ubuntubot : 2010-03-03 10:22:50 
Tue, Mar 02 on Twitter - doronkoのTwitterログ: Ubuntu 10.04はJailbreakなしでiPhoneにフルアクセス可能!? - ユーザー報告が話題 http://bit.ly/c... http://bit.ly/9E1fTD
--------------------------------------------------------------------------------
ubuntubot : 2010-03-03 12:41:55 
【レポート】 インストールしてから5分で動かせる - CUDAをサポートしたLinuxが登場: マイコミジャーナル
FedoraやRHEL、Ubuntuなどと比べ、システム開発などにおいて不要なプロセスやサービスを停止させること... http://bit.ly/cPOHG6
--------------------------------------------------------------------------------
ubuntubot : 2010-03-03 14:35:21 
「Ubuntu 10.04」アルファ第3版で大発見--iPhoneやiPod touchに対応するとのユーザー報告: 現在アルファ第3版の状態にあるUbuntu 10.04(開発コードLucid Lynx)がiPhoneやiPo... http://bit.ly/dvFHJk
--------------------------------------------------------------------------------
ubuntubot : 2010-03-03 16:36:56 
仮想HD(VHD)を使うための準備 ≪ むずかしいことはわかりません: ステップ実行してるとこのまま実行してやりたい衝動にかられるけど、ここで我慢しないと今までの時間が無駄になる 19 hours ago; ものは試しと ubu... http://bit.ly/b5Bmgx

2010/03/07 引数チェック追加
tw_list.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import codecs
import simplejson
import twitter
import time

class ApiSupportsList(twitter.Api):
  def GetUserListTimeline(self, user=None,ulist=None, count=None, since=None, since_id=None):
    try:
      if count:
        int(count)
    except:
      raise twitter.TwitterError("Count must be an integer")
    parameters = {}
    if count:
      parameters['per_page'] = count
    if since:
      parameters['since'] = since
    if since_id:
      parameters['since_id'] = since_id
    if not user and not self._username:
      raise twitter.TwitterError("User must be specified if API is not authenticated.")
    else:
      user = user if user else self._username
      url = 'http://api.twitter.com/1/%s/lists/%s/statuses.json' % (user, ulist)   
    json = self._FetchUrl(url, parameters=parameters)
    data = simplejson.loads(json)
    self._CheckForTwitterError(data)
    return [twitter.Status.NewFromJsonDict(x) for x in data]

sys.stdout = codecs.getwriter('utf_8')(sys.stdout)
api = ApiSupportsList()

argv = sys.argv
argv.pop(0)
user, count = argv

if user.count('/') == 1:
  user, group = user.split('/')
  statuses = api.GetUserListTimeline(user,group,count)
else:
  statuses = api.GetUserTimeline(user,count) 

statuses.reverse()
for status in statuses:
  print '-' * 80
#  print '%s : %s (%s)' % (status.user.screen_name, time.strftime('%F %T',time.localtime(status.created_at_in_seconds)), status.relative_created_at)
  print '%s : %s ' % (status.user.screen_name, time.strftime('%F %T',time.localtime(status.created_at_in_seconds)))
  print status.text
  

☆Twitterのタイムラインを逆順に表示する(2)

昨日の続きです。
TwitterのAPIを参照するとURLの後ろにパラメータが使えるようです。
http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-GET-list-statuses
http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-statuses-user_timeline
昨日のスクリプトのに与える引数に取得したい数する場合は "?per_page=50"
(50は取得したい数、1~200が指定可能)を追加します。シェルが?を展開しようとするのでクオートします。
per_pageはリストのAPIにのっていて、指定ユーザのタイムラインを取得すところにはなかったのですが、
両方動作します。

実行例
tw_rev_browse.sh 'ubuntubot?per_page=50'
tw_rev_browse.sh 'MidSpecLowLoad/ubuntu-ittoke-doujou?per_page=50'

これだけでは何なので、シェルをちょっといじって同一ファイルに一定間隔で取得内容を
書き出して、ブラウザはF5キーを押してリロードする仕組みにしてみます。
(HTMLのヘッダにJavascriptでリロードを仕込もうとしたのですが、うまくやれませんでした。)
終了されるときはCTRL+Cで止めます。
下のスクリプトでは3分にしています。変更したい場合は "GET_INTERVAL"の値をいじってください。

実行例
tw_rev_browse_cont.sh 'MidSpecLowLoad/ubuntu-ittoke-doujou?per_page=50'
2010/03/07 引数チェック追加
tw_rev_browse_cont.sh
#!/bin/bash

GET_INTERVAL=3m

if [ -z $1 ] ;then
  echo "$0 requires a parameter"
  exit 1
fi

HTML=TwitRev-`date '+%F-%H%M%S'`-$$.html

wget -O - http://twitter.com/$1 |sed -e 's#</li>#\n#;'| tw_rev.pl > $HTML
x-www-browser $HTML &

while true;do
  sleep $GET_INTERVAL
  echo "--- Start update $HTML:" `date '+%F %T'` " ---"
  wget -O - http://twitter.com/$1 |sed -e 's#</li>#\n#;'| tw_rev.pl > $HTML
  echo "--- End   update $HTML:" `date '+%F %T'` " ---"
done

APIによれば取得し始めのIDなんかを指定できるようです。
サーバーにかける負荷や自分のマシンリソースを無駄遣いしないように、
新規のつぶやきだけとってくるようにするのが望ましいのですが、
今日はとりあえずここまで。

☆Twitterのタイムラインを逆順に表示する

Twitterのタイムラインを逆順に表示したいのですが、Read All Tweetsは一つのリストしか指定できないみたいです。
複数のリストを見るために設定を変えるのは結構面倒です。
とりあえず手持ちの知識だけでやれることをやってみました。
この方法では一ページ分しか処理できません。

tw_rev_browse.shにブラウザで見た場合のURLから"http://twitter.com/"を除いた部分を指定して使います。
account_nameのアカウントのタイムラインを見たい場合は "tw_rev_brose.sh account_name"、
リストの場合は"tw_rev_brose.sh account_name/list_name"のようにして使います。
結果はブラウザで表示されます。
TwitRev-YYYY-MM-DD-HHMMSS-数字何桁か.html というファイルがカレントディレクトリに出来るので不要になったら削除してください。

実行例
# アカウント指定
$ tw_rev_browse.sh ubn
# リストの指定
$ tw_rev_browse.sh MidSpecLowLoad/ubuntu-ittoke-doujou

以下の二つのファイルに実行権限をつけてをパスの通ったところにおきます。

2010/03/07 引数チェック追加
tw_rev_browse.sh
#!/bin/bash
if [ -z $1 ] ;then
  echo "$0 requires a parameter"
  exit 1
fi

HTML=TwitRev-`date '+%F-%H%M%S'`-$$.html
wget -O - http://twitter.com/$1 |sed -e 's#</li>#\n#;'| tw_rev.pl > $HTML
x-www-browser $HTML &
tw_rev.pl
#!/usr/bin/perl

# search start html header
while(<>){
  print;
  last if m#<head>#;
}

# For relative link. ex list in right side.
print qq(<base href="http://twitter.com/" />\n);

# search start of tweets
while(<>){
  print;
  last if m#<ol id='timeline' class='statuses'>#;
}

# stack each tweets
my(@TWEET);
OUTER:
while(1){
  my(@tweet);
  while(<>){
    last OUTER if m#</ol>#;
    push @tweet, $_;
    last if m#</li>#;
  }
  unshift @TWEET, \@tweet;
}

# output each tweets
my($tweet);
for $tweet (@TWEET){
  my($line);
  for $line (@$tweet){
    print $line;
  }
}
print qq(</ol>\n);

# output remaining
while(<>){
  print;
}

HTMLを見ると1Tweetが<li></li>でまとめられているのでJavaスクリプトでDOMを操作すれば、結構簡単な気がします。
Firefoxのプラグインの作り方を知っていればこれが一番簡単そうです。
Perlを使う場合でもDOMを操作できるモジュールを使えばもっとスマートに出来るとは思うのですが、モジュールはあまり使用したことがないので知りません。
python twetterなどを調べて使ってみるのも良いかもしれません。
何か一つくらいチャレンジしてみるつもりです。