Google Reader の未読フィード保存

 Google Reader の終了まで 2 週間を切りました。ですが、未読の記事が数えきれないほどたまっています…。Feedly に移行すれば未読・既読状態も引き継いでくれるのかと思っていましたが、そういうわけではないようです。

移行によって履歴データはコピーされないほか、未読カウントも一旦リセットされる。
http://news.mynavi.jp/news/2013/06/18/159/index.html

 そしてもうひとつ知らなかった事実が。

Googleリーダーを利用していますが、1ヶ月以上前の未読フィードが順次既読フィードへと変化してしまいます。
ここを見ると 30日までしか未読を取っておかないようにも読めます。
http://q.hatena.ne.jp/1282905290

 Google Reader は 1ヶ月以上前の未読フィードを既読にしてくれるようです。知りませんでした。なので、もし Feedly 以外の代替サービスが 「未読記事も同期します」 と謳っていても、1ヶ月以上前のデータは既読扱いで同期されてしまうわけですね。困ります。未読フィードを読むのは老後の楽しみにでもしようかと思ってたんですが。

未読と思わしき日付まで遡って、すべてのフィード履歴をテキストで保存する

 既読・未読の状態に関わらず、Google Reader に残っているフィードの履歴をすべて保存することにしました。ということで、まずは手軽にブラウザで試しました。 Chrome のコンソールで利用できるメソッド copy を使って、表示されているエントリをすべてクリップボードに保存します。

var str = ''; Array.prototype.forEach.call(document.querySelectorAll(".entry-original"), function(element) { str += element.parentNode.querySelector('.entry-title').textContent + '\t' + element.parentNode.parentNode.querySelector('.entry-date').textContent + '\n' + element.href + '\n\n'; }); copy(str);

 このスクリプトGoogle Reader を開いたあとの画面で実行しても、ページネーションにひっかかってすべての履歴は取得できません。なので、すべての履歴を画面に表示させるべく、 middleclick および AutoScroll を導入して(トラックパッドユーザーなので)、Google Reader 上でオートスクロール状態に設定し、半日放置しました。
 そして満を持してスクリプトを実行した画面がこれです。

f:id:Ajido:20130619235702p:plain

Google Reader API を利用する

 cURL を使います。認証は以下のように ClientLogin を使って容易にできました。このリクエストのレスポンスに含まれる Auth の値をヘッダーに含めて、Google ReaderAPI にリクエストします。

$ curl "https://www.google.com/accounts/ClientLogin" --data-urlencode Email=<Mail Address> --data-urlencode Passwd=<Password> -d accountType=GOOGLE -d source=Google-cURL-Example -d service=reader

Google Reader の「すべてのアイテム」は reading-list というエントリポイントです。XML は扱いにくいので JSON で取得します。まずは試し打ちでリクエストを飛ばしてクエリやスキーマをチェックします。

$ curl --silent --header "Authorization: GoogleLogin auth=<Auth>" "https://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/reading-list"

指定するクエリ

r=n記事の並び順。ブラウザから利用している並び順と同様の値を指定
n=1000取得する記事数
c=XXXXXXXXXX次の記事リストを取得するためのトークン文字列

フィードの履歴を新しい順に 1000 件単位で取得します。はじめのリクエストでは c を付けませんが、 2 回目以降のリクエストでは c を付けてイテレートさせます。

スキーマ

{
  continuation: 'XXXXXXXXXXX',
  items: [
    {
      title: '記事タイトル',
      published: 1371650750,
      alternate: [ { href: 'http://www.example.com' } ],
      origin: { title: 'プロバイダ' }
    }
  ]
}

重要なプロパティだけに絞っています。 continuation が次の記事一覧を取得するためのトークンで、 items プロパティの中に記事リストが格納されています。

取得できるすべての記事を取得するワンライナー

if [ -f token ]; then rm token; fi; while : ; do URL="https://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/reading-list?r=n&n=100$(if [ -f token ]; then echo "&c=$(cat token)"; fi)"; echo $URL; curl --silent --header "Authorization: GoogleLogin auth=<Auth>" "$URL" | node -e 'var stdin = ""; process.stdin.resume(); process.stdin.setEncoding("utf8"); process.stdin.on("data", function (chunk) { stdin += chunk}); process.stdin.on("end", function () { try { var json = JSON.parse(stdin); if (!json.continuation) { process.exit(1); } console.log(json.continuation); json.items.forEach(function (n) { if (n.title && n.origin.title && n.alternate instanceof Array && n.alternate.length > 0 && n.alternate[0].href) { var d = new Date(n.published * 1000); console.error(d.getFullYear() + ("0"+(d.getMonth()+1)).slice(-2) + ("0"+d.getDate()).slice(-2) + "-" + ("0"+d.getHours()).slice(-2) + ":" + ("0"+d.getMinutes()).slice(-2) + "\t" + n.origin.title + "\t" + n.title + "\t" + n.alternate[0].href); } }); } catch (e) { } });' 1>token.tmp; if [ $? -eq 0 -a -f token.tmp ]; then mv token.tmp token; fi; done 2>feed.tsv

 <Auth> には ClientLogin で取得した Auth の値を指定します。既に読んだ可能性のある記事も含まれるので、最後に未読をゼロにした日でフィルタリングするために、記事の日付も出力しています。
 また無限ループなので、別ターミナルで tail -f feed.tsv などを走らせて、適当なタイミングで(これ以上過去の記事が不要であったり、最後まで到達してトークンが固定された場合など)止める必要があります。これを数分走らせた結果、

$ cat feed.tsv | wc -l
   78898

約 8 万件の記事がサルベージできました。 
 
参考リンク

広告を非表示にする