Hatena::ブログ(Diary)

エネルギー吸収と発散 このページをアンテナに追加 RSSフィード

08/31(Wed), 2011

iPhone+Dropboxを利用したアニメ感想管理システム

論文みたいなタイトルですが、久々に感想サイトの作り方シリーズ(?)です。

関連エントリー:

はじめに

感想を書いてると、どの作品の何話まで感想を書いてて、何話から未視聴なのか分からなくなることがしょっちゅうあります。その度に公式サイト、あるいはしょぼいカレンダーなどから最新サブタイトルを確認して、書き終えた感想を確認して……という手順を実施するのはすごくめんどくさい!

ということで、かなり以前から感想を書いていない回をリストアップ するツール(自作)を使って ました。具体的には、こんな感じで上の手順を自動化するだけです。

  1. 視聴リストから作品名と対応するしょぼいカレンダーへのリンクを取得
  2. エントリー一覧から書いた分の 感想のリストを取得
  3. しょぼいカレンダーとサブタイトルリストとの差分を出力

シンプルな ツールですがものすごく便利で、2時間くらいで作ったわりには役に立つ……と自画自賛していたものです。

環境の解説

さて本題。去年の年末にiPhoneを買ってみたらこれが超便利で、あまりにも便利なので感想もiPhoneで書きたい!と思うようになってきたのでした。だってiPhoneならいつでもどこでも、視聴中ですらササッと感想が書けてしまうじゃないですか!

とはいえ全てを iPhone に移譲するわけにもいかなくて、PCで書きたい場合も依然としてある。細かい誤字脱字の修正や固有名詞の入力はPCの方が圧倒的に便利だし。

そんな訳で、うまいこと iPhoneとPCを同期させる方法としてDropboxを使ってみることにしました。

今までのようにテキストで出力する代わりに、Dropboxの共有フォルダにファイルを作ります。83個ファイルがあるということは、83話分の感想が未完了ということです*1

f:id:kkobayashi:20110831233058p:image

iPhoneから見るとこんな感じ。ちなみに「Nebulous Notes」というアプリを使ってます。

f:id:kkobayashi:20110831233059p:image

iPhoneではファイル名の後ろの方が見えないので、分かりやすいprefixを付けておくと便利です。フォーマットは以下の通り。

XX-MMDD-YYY_タイトル 第ZZ話「サブタイトル」
 |  |    |
 |  |    +--話数
 |  +--日付
 +--通し番号
  • XX: 視聴リストの上から順に付けた通し番号。タイトルでソートされるとiPhoneから探しにくいんですよね。ジュエルペットサンシャインの次はプリティーリズム、という順番の方が直感的でしょう?
  • MMDD:日付。 話数だけだと今何話を見てるのか分からなくなるので、日付情報とリンクさせるといい感じです。
  • YYY:話数。

作品のサブタイトルをファイル名に使うと当然使えない文字があるので 、こんな感じのフィルタで全角変換します。

tr{\"<>|:;*?\/}{”<>|:;*?¥/};

本当はセミコロン";" をファイル名に使ってもいいはずなのですが、iPhone側ではファイルは開けても保存する時に自動でアンダースコア"_"に変換されてしまいます*2。iOSの仕様かアプリの仕様かは分かりませんが、何にしてもセミコロンも全角にしといた方がよさそうです。

ファイルの中身はこんな感じ。

f:id:kkobayashi:20110831233100p:image

はてな記法のタイトルだけが書いてあるので、2行目から感想を書いていきます。

で、最後はこれらをマージすればあら不思議、そのままはてなダイアリーに貼れる感想の出来上がり!という寸法です。

感想をアップロードし終わったら何も考えずにフォルダ内のファイルを全部消すだけでOK。上述のファイル作成&マージするツールを1時間おきに実行しているので、しばらくしたら勝手に 更新されます。

おわりに

環境を構築 してから(おそらく6月初頭)数ヶ月使っていますが、中々よい感じ。やはりiPhoneで感想書けると便利さが段違い。自分用にカスタマイズしすぎているので、参考になるか?と言われると微妙なところかもしれませんね。まあ、ひとつのアイデアとして見ていただければ。

それにしても、以前の自分なら「携帯片手にアニメ見るなんて!」 と憤慨していただろうなあ。今でも正直、こういう怠惰なシステムは積極的に勧めたいとは思ってないです。サブタイトルは毎週公式サイトで確認するべきだし、携帯片手に見ながら書くような文章はどうしても脳のフィルターをスルーした浅いものになりがち。

とはいえ、アニメに対するスタンスに応じて感想の書き方も変わってくるはずで、今の自分にはこのくらいの緩い距離感がちょうどいい。 そんなだらしなさをある程度許容してくれる今のシステムは、我ながら結構気に入ってたりします。

おまけ

参考になるかわかりませんが、別に隠すものでもないのでスクリプトを貼っておきます。

#!/usr/bin/perl

use strict;
use Web::Scraper;
use URI;
use Encode;
use List::Util qw/max/;
use Path::Class;
use utf8;

my $title_list   = 'http://d.hatena.ne.jp/kkobayashi/archive';
my $program_list = shift || 'http://d.hatena.ne.jp/kkobayashi/20110705/p1';
my $disp_limit   = shift || 5;
my $list_dir     = Path::Class::Dir->new('/cygdrive/d/home/work/Dropbox/review');

## main
my $titles   = get_title_list($title_list);      sleep 1;
my $programs = get_program_list($program_list);  sleep 1;
my %reviewed = ();

## check if title in archive contains program name
foreach my $t (@$titles){
  foreach my $p (@$programs){
    my $name = $p->{name};
    if($t->{title} =~ /$name.*(\d+)/){
      ### get episode number
#      print encode("sjis", $name), " - $1\n";
      # put tags to be used
      $reviewed{$name}->{tags} = $t->{tags} unless exists($reviewed{$name});
      
      # set the maximum episode number
      $reviewed{$name}->{seen} = $1 if($reviewed{$name}->{seen} < $1);
    }
  }
}

# print all unreviewed titles

for(my $i=0; $i<@$programs; $i++){
  my $p          = $programs->[$i];
  my $name       = $p->{name};
  my $subtitles  = get_calendar($p->{calendar}); sleep 1;
  my $disp_th    = max(keys %$subtitles) - $disp_limit;

  ### $subtitles
  foreach my $number (sort {$a <=> $b} keys %$subtitles){
    if($number > $reviewed{$name}->{seen} and $number > $disp_th){

      # setup filename
      my $basename = sprintf("%s 第%02d話「%s」", "@{$p->{kw}}", $number, $subtitles->{$number}->{title});

      # ";" can be used for filename, but somehow it is changed into '-' by iphone
      $basename =~ tr{\"<>|:;*?\/}{”<>|:;*?¥/};

      # add prefix so that we can easily find the file from iPhone
      $basename = substr(sprintf("%02d_%s_%03d_%s", $i+1, join('', (split(/-/, $subtitles->{$number}->{date}))[1,2]), $number, $basename), 0, 100). '.txt';

      # turn off UTF-8 flag
      Encode::_utf8_off($basename);

      my $file = $list_dir->file(encode('cp932', decode('utf8', $basename)));
      ### $file : "$file"
      
      unless(-e $file){
        ### create a new file
        my $output = '*' . join('', map { "[$_]" } @{$reviewed{$name}->{tags}}) . ' ';
        $output   .= sprintf("%s 第%02d話「%s」", join(' ', map { "[[$_]]" } @{$p->{kw}}), $number, $subtitles->{$number}->{title});
        $output   .= "\n";
        
        my $ofh = $file->openw;
        print $ofh encode('utf8', $output);
        close $ofh;
      }
    }
  }
}


## subroutine

# get titles from 'archives' page
#
# $t: {
#       tags => [
#                 "\x{30a2}\x{30cb}\x{30e1}",
#                 'YMPT'
#               ],
#       title => "xxx"
#     }
sub get_title_list{
  my $uri = URI->new(shift);
  print STDERR "scraping $uri ...\n";
  my $scraper = scraper {
    process 'li.archive-section', 'r[]' => {
      title => 'TEXT', 'tags' => scraper {
        process 'a.sectioncategory', 't[]' => 'TEXT';
        result 't';
      }
    };
    result 'r';
  };
  return $scraper->scrape($uri);
}

## return names and table 
# {
#   calendar => bless( do{\(my $o = 'http://cal.syoboi.jp/tid/2160/time?Filter=3')}, 'URI::http' ),
#   kw => [
#           "\x{300c}C\x{300d}",
#           'THE MONEY OF SOUL AND POSSIBILITY CONTROL'
#         ],
#   name => "\x{300c}C\x{300d} THE MONEY OF SOUL AND POSSIBILITY CONTROL"
# },

# "name" is used for matching with "title" of get_title_list(). Don't change encoding.
# For kw, it is used for output. It should be encoded properly.

sub get_program_list{
  my $uri = URI->new(shift);
  print STDERR "scraping $uri ...\n";
  my $scraper = scraper {
    process '//div[@class="section"]/table/tr', 't[]' => {
      'name'    => scraper{
        process '//td[3]', 'n' => 'TEXT';
        result 'n';
      },
      'calendar' => scraper{
        process '//td[4]/a', 'p' => '@href';
        result 'p';
      },
      'kw' => scraper{
        # solve wide-tilde problem (euc -> utf-8)
        process '//td[3]/a[@class="keyword"]','n[]' => [ 'TEXT', sub { tr/\x{301c}\x{2212}/\x{ff5e}\x{ff0d}/; $_; } ];
        result 'n';
      },
    };
    result 't';
  };
  return $scraper->scrape($uri);
}

## scrape syoboi calendar
sub get_calendar{
  my $uri = shift;
  print STDERR "scraping $uri ...\n";
  my $scraper = scraper {
    process 'tr.past', 'p[]' => scraper {
      process '//td[4]', 'number' => 'TEXT';
      process '//td[2]', 'date'   => ['TEXT', sub { /(\d{4}-\d{2}-\d{2})/ ? "$1" : "" }];
      process '//td[5]/text()', 'title' => 'TEXT';
    };
    result 'p';
  };
  my $result = $scraper->scrape($uri);

  # copy result to table
  my $table = {};
  $table->{$_->{number}} = $_ foreach (@$result);
  return $table;
 }
#!/usr/bin/bash -x
LANG=ja_JP.cp932 perl /cygdrive/d/home/local/bin/checkreview.pl

cd /cygdrive/d/home/work/Dropbox/review
echo -n > 00_merged.txt

#ls ??_????_???_* | while read f; do
ls | perl -nle 'print $1 if /(\d{2}_\d{4}_\d{3})/' | while read f; do
	[ `wc -l ${f}* | awk '{print $1}'` -gt 1 ] && (cat ${f}* ; echo) >> 00_merged.txt
done

1行目は必ずタイトルが入るので、2行以上あるファイルをマージしています。で、このシェルスクリプトをcronに登録します。

0 * * * * /cygdrive/d/home/local/bin/checkreview.sh  2>&1

*1:これはひどい

*2:"Steins;Gate" → "Steins_Gate" のような