最初のエントリでも書いたように、このブログはEvernoteで書いてる(この文章自体も)。
APIキーの取得については以前書いたのでそちらを参照ください。
Evernoteからブログへのデータ取り込み部分のソースを出しておく。クラス構造とか設定ファイルとかは端折って、実際に動くところのみ。Evernote公式のrubyライブラリはいまいち使いにくい。もうちょっとrubyっぽいサードパーティのwrapperとかあってもよさそうだけど、今のところなさそうだし作るのもだるい。
処理の流れとしては、
- 自分のアカウントで認証
- "blog"というタグが含まれている全ノートを抽出
- ノートを一つずつ舐めて、自分のWebアプリにデータ取り込み
認証
とりあえず、関連するライブラリをrequireしてから認証処理。この辺はサンプルのまま。
require "thrift"
require "Evernote/EDAM/user_store"
require "Evernote/EDAM/user_store_constants.rb"
require "Evernote/EDAM/note_store"
require "Evernote/EDAM/limits_constants.rb"
userStoreTransport = Thrift::HTTPClientTransport.new("https://www.evernote.com/edam/user")
userStoreProtocol = Thrift::BinaryProtocol.new(userStoreTransport)
userStore = Evernote::EDAM::UserStore::UserStore::Client.new(userStoreProtocol)
authResult = userStore.authenticate("YOUR_USER_NAME", "YOUR_PASSWORD",
"CONSUMER_KEY", "CONUMER_SECRET")
evernote_user = authResult.user
authToken = authResult.authenticationToken
ここで作ったauthTokenを持ち回してAPIで使う。
ノートにアクセスするための準備
noteStoreUrl = "http://www.evernote.com/edam/note/" + evernote_user.shardId
noteStoreTransport = Thrift::HTTPClientTransport.new(noteStoreUrl)
noteStoreProtocol = Thrift::BinaryProtocol.new(noteStoreTransport)
noteStore = Evernote::EDAM::NoteStore::NoteStore::Client.new(noteStoreProtocol)
ここで作ったnoteStoreでノートにアクセスする。
ノートを検索
filter_tag = "blog"
filter = Evernote::EDAM::NoteStore::NoteFilter.new
filter.words = "tag:#{filter_tag}"
res = noteStore.findNotes(authToken, filter, 0, 100)
このfilter.wordsにはEvernoteクライアントでも使える検索式が書ける。たとえば、tag:で始まるのはタグだし、単に文字列を入れるとその文字列で検索される。
後は、上の検索で引っかかったノートの情報をmongoDBにコピーする処理。同時に、添付されている画像ファイルもローカルに落としてる。mongoDBへのアクセスはいまのところMongoMapperを使っている。見てわかるようにActiveRecordとほぼ同じインターフェース
XSLTを使ってるのは、Evernoteのノートは独自のXMLで保管されているので、それをhtml変換するため。XSLTの中身は以前紹介したやつ。
note2blog = Nokogiri::XSLT(File.read('xslt/note2blog.xslt'))
res.notes.each do |note|
entry = Entry.find_or_initialize_by_evernote_key(note.guid)
next if entry.updated_timestamp.to_i == Time.at(note.updated / 1000).to_i
if note.resources
note.resources.each do |resource|
data = noteStore.getResource(authToken, resource.guid, true, true, true, true)
hex = data.data.bodyHash.unpack('H*').first
ext = case data.mime
when 'image/png'
'png'
when 'image/jpeg'
'jpg'
else
raise "Unknown mime type: #{data.mime}"
end
File.open("publichttp://blog.madoro.org/mn/images/#{hex}.#{ext}", 'w') {|f| f.write(data.data.body) }
end
end
entry.user = user
entry.title = note.title
tags = noteStore.getNoteTagNames(authToken, note.guid)
tags.delete_if {|a| a == filter_tag}
entry.tags = tags
content = noteStore.getNoteContent(authToken, note.guid)
entry.text = note2blog.transform(Nokogiri::XML(content)).to_s
}
entry.updated_timestamp = Time.at(note.updated / 1000)
entry.published_timestamp ||= Time.at(note.updated / 1000)
entry.status = Entry::Published
entry.save!
end
しばらくEevrnoteを使っているのに、いまいち理解しないままでいたコンフリクト時の処理について確認した(コンフリクト=複数のEvernoteクライントで同一のファイルを更新)。
どこまでがクライアント側の処理かまで調べてないのでクライント依存のところもあるかも。Mac版クライアントでは以下のようになる。
ルール:
- サーバ上にあるノートが常に勝つ。
- データのマージは自動では行われない。
- 更新されたノートがサーバにある場合、それをダウンロードする前にアップロードはできない。
- 負けたノートはコンフリクト用のNotebookに移される。コンフリクト用のNotebookはコンフリクトする度に作成される。
例:
- 前提: ノートNが、サーバとクライアントAとクライアントBに存在する。
- クライアントA(例: iPhone)でノートNを編集。このノートを以下N-Aと呼ぶ。
- 同時に、クライアントB(例: Macbook)でノートNを編集。このノートを以下N-Bと呼ぶ。
- クライアントAから、EvernoteサーバへN-Aをアップロード(Sync)。
- その後、BでN-Aをダウンロード(Sync)。
- この時点でクライアントB上でノートN-BとN-Aのコンフリクトが発生。
- クライアントBのメインのNotebookにはN-Aが入る(N-Bは見えなくなる)。同時に、コンフリクト用のNotebookが作成され、そこN-Bは飛ばされる。
- 必要であれば、ユーザはN-AとN-Bを比較し、手でマージする。
SubversionとかGitに慣れていると、テキストなんだし、ある程度勝手にマージしてよ、とか少し思うけど、文章だとそううまくはいかないのかな。
コンフリクトしたときに作られるNotebook
いつものようにEvernoteを検索してて気づいた。Evernoteって画像内の文字認識をするみたい。公式の説明どこかにあるんだろうけど見逃してた。PDFが検索できるのは知ってたんだけど。
これは、とあるミーティング後のホワイトボードをiPhoneで撮ってEvernoteに突っ込んであったもの。calendarという(汚い)字を認識して検索してる。赤枠とちょっとモザイクかけた以外の加工はしてない。検索してヒットした文字の部分が強調して表示される。
さらに、APIで画像の中に入っている単語とその場所の一覧、も取れることを確認した。なんか遊べそうだなー。
rubyでのコード例
filter = Evernote::EDAM::NoteStore::NoteFilter.new
# calendar で検索
filter.words = "calendar"
res = noteStore.findNotes(authToken, filter, 0, 100)
res.notes.each do |note|
note.resources.each do |resource|
# これが画像内の文字の情報のXML
p noteStore.getResourceRecognition(authToken, resource.guid)
end
end
環境は、iMac (early 2009)ちゃんと、Macbook 初代のアルミちゃん。マカー過ぎて恥ずかしい。
自分は、Evernote上にはほぼテキストファイルしか置いてないので、とりあえず、単純に5KBくらいのファイルを1000個作ってみた(API便利!)。結果は良好。たとえば、検索速度は余裕の1秒以内。次に5000個同じ条件でつくってみた。これも特に問題なし。この時点でアプリケーションのメモリ使用量は200MBくらい。200個ぐらいでメモリ使用量40MBぐらいだったので、なんで、ノート数によってお結構リッチに使ってくれちゃうみたい。コンテンツ全部メモリに乗っけてるのかな。(それにしても計算あわないけど)
色々な条件によって遅くなることはあると思うけど、当面の自分の用途的には十分かな。
それと、Viewによって、体感速度が結構違う。普段List Viewなんだけど、それは早い。でも、Mixed Viewとか、Thumbnail Viewだと遅い。描画量がどうしても多くなってしまうからだろう。