読者です 読者をやめる 読者になる 読者になる

ハイパーニートプログラマーへの道

頑張ったり頑張らなかったり

【Ruby】【Rails】【Mechanize】kindle.amazon.co.jpで自分がフォローしている人たちのハイライトを取得

自分のハイライトを取得する方法はちらほら見当たるんですけど、自分がフォローしている人たちのハイライトを取得するのはないなーと。
AmazonはそのためのAPIを提供していないようなので? 愚直にMechanizeでやろうかと。

コード

require "mechanize"

mech = Mechanize.new
mech.user_agent_alias = 'Mac Mozilla'
# サインイン用のページ
url = 'https://www.amazon.co.jp/ap/signin?openid.return_to=https%3A%2F%2Fkindle.amazon.co.jp%3A443%2Fauthenticate%2Flogin_callback%3Fwctx%3D%252F&pageId=amzn_kindle&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.assoc_handle=amzn_kindle_jp&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.pape.max_auth_age=0'
page = mech.get(url)
login_form = page.forms_with(:name => 'signIn').first
login_form.fields_with(:name => 'email').first.value = "YOUR EMAIL"
login_form.fields_with(:name => 'password').first.value = "YOUR PASSWORD"
page2 = login_form.click_button
# スクレイピングするページ数を入力
puts "How many pages?"
pages = gets.chomp.to_i
# 一応遷移先(ログイン後)のページタイトルとurl表示
puts page2.title
puts page2.uri.to_s
# あ、ここからー
pages.times do |count|
  puts "Page: #{count+1}"
  puts "----------"

  kindle_page = mech.get(page2.uri.to_s)
  content = kindle_page.search('div.content')
  content.each do |c|
    puts "Authors:"
    authors = c.text.slice(/(by\s).+?\n/).gsub(/by\s|&/, "").chomp
    authors.split(",").each do |a|
      puts a.strip
    end

    c.css('a').each do |link|
      if link.attribute('href').value.start_with?("/work/")
        puts "Title:"+link.text.gsub(/\(J\w*|E\w*\)/, "").strip
        puts "ASIN:"+link.attribute('href').value.split("/").last
      end
    end
    # (1/14)下記のコードではうまくいかない場合があるので修正しました。
    # puts "Quote:"+c.css('span.sampleCloseQuote').text.strip
    puts "Quote:"+c.css('div.sampleHighlight > div:first-child').text.strip
    puts "----------"
  end
  see_more = page2.link_with(:class => 'seeMore expand')
  page2 = see_more.click
end

ちょっと解説を

サインイン後のページをget

サインインページで

page2 = login_form.click_button

送信ボタンをクリックして遷移先(自分のホーム)の情報をpage2に突っ込むのはいいんですけど、

Mechanizeでgetするときにページのuriをさらに文字列にして渡さないといけないです。

kindle_page = mech.get(page2.uri.to_s)

必要なコンテンツはdiv.content内にありますのでsearchで取得し、それをeachで回してあげて処理、という流れになります。

content = kindle_page.search('div.content')
content.each do |c|
# 以下処理

著者名取得

authors = c.text.slice(/(by\s).+?\n/).gsub(/by\s|&/, "").chomp

著者名はbyから始まるので、.slice(/(by\s).+?\n/)で取り出す・・・のですが、それだとbyと改行が前後にくっつくので、さらに
.gsub(/by\s|&/, "")で置換させます。たまに著者名が複数あって最後が&で終わってるものもあるので・・・これとか

静かなる革命へのブループリント この国の未来をつくる7つの対話

なので``&```も置換対象にします。んーここら辺もうちょっとスマートにできないかなと。正規表現難しいなあ・・・。

で、著者名が複数の場合もあるので

    authors.split(",").each do |a|
      puts a.strip
    end

カンマ区切りでsplitしてeachで回して表示。

書籍のタイトル取得

まず<a>タグのhrefの値はいっぱい取れてしまいますので、その中から/work/で始まるものだけを取得。start_with?()を使います。
引数に"/work/"を渡してあげれば良いと。含まれていればtrueが返ってきます。

    c.css('a').each do |link|
      if link.attribute('href').value.start_with?("/work/")
        puts "Title:"+link.text.gsub(/\(J\w*|E\w*\)/, "").strip
        puts "ASIN:"+link.attribute('href').value.split("/").last
      end
    end

取得できるタイトルには必ずと言っていいほど(Japanese Edition)と余計なものが付いてますのでgsubで置換・・・なのですが、
たまに(Japanで途切れているものもあります。これとか(またか)

静かなる革命へのブループリント この国の未来をつくる7つの対話

f:id:noriyo_tcp:20150105111429p:plain

なので

link.text.gsub(/\(J\w*|E\w*\)/, "").strip

(Jで始まる単語またはEで始まり)で終わる単語を置換対象にしています。(Japanese Edition)なら両方対象ですので間の空白を残して置換できます。
Jでなくて(なんたら Edition)だったらどうしようかと思いますが、とりあえずこれで用は足せてます。

ASIN取得

今回はASINを取得したいので、単純に/で分割して、最後の要素(これがASIN)を取り出しています。

link.attribute('href').value.split("/").last

引用の取得

引用はクラスがsampleCloseQuote<span>タグのテキストにあるので

・・・とは限らない場合がありましたので修正しました。

- c.css('span.sampleCloseQuote').text.strip
+ c.css('div.sampleHighlight > div:first-child').text.strip

これで取得。

See Moreリンクのクリック

デフォルトでは引用は3つしか表示されていないので、その左下のSee Moreリンクをクリックする必要があります(さらに10個表示される) なので最後に

  see_more = page2.link_with(:class => 'seeMore expand')
  page2 = see_more.click

クラスがseeMore expandのリンクをクリック。それをpage2(自分のkindle.amazon.co.jpのホーム)に格納し直し。そしてまたループと。

もうなんかぐちゃぐちゃですが、一応取得できてます。

実行結果

Page: 1
----------
Authors:
司馬遼太郎
Title:街道をゆく 10 羽州街道、佐渡のみち
ASIN:B00M3V4974
Quote:江戸幕府では、勘定畑(長官は勘定奉行)が、もっとも優秀な人材をあつめるしきたりになっている。それが明治政府の大蔵省にひきつがれ、こんにちなお、各省を超越して人材をあつめる優先権をこの省が持っているというのはおもしろい。
----------
Authors:
吉行 淳之介
Title:贋食物誌
ASIN:B00GQQTXU0
Quote:山本五十六だったか、「いまどきの若い者などと申すまじくそうろう」とか言った
----------
Authors:
吉行 淳之介
Title:贋食物誌
ASIN:B00GQQTXU0
Quote:戦前には「キツネうどん」はあったが、「タヌキうどん」というものは存在しなかった。
----------
Page: 2
----------
Authors:
吉行 淳之介
Title:贋食物誌
ASIN:B00GQQTXU0
Quote:意外なことにイクラというのはロシア語
----------
Authors:
岸見 一郎
古賀 史健
Title:嫌われる勇気
ASIN:B00H7RACY8
Quote:いかなる経験も、それ自体では成功の原因でも失敗の原因でもない。われわれは自分の経験によるショック——いわゆるトラウマ——に苦しむのではなく、経験の中から目的にかなうものを見つけ出す。自分の経験によって決定されるのではなく、経験に与える意味によって自らを決定するのである
----------
Authors:
阿川 弘之
Title:私記キスカ撤退 (文春文庫)
ASIN:B009HO4IYO
Quote:支百勇一静可以制百動
----------
Authors:
阿川 弘之
Title:私記キスカ撤退 (文春文庫)
ASIN:B009HO4IYO
Quote:「人ノマサニ死ナントスルヤ其ノ言ヤヨシ」
----------
Authors:
ピーター・ティール
ブレイク・マスターズ
Title:ゼロ・トゥ・ワン 君はゼロから何を生み出せるか
ASIN:B00NQ3QONK
Quote:ベンチャーキャピタルにとっての何よりも大きな隠れた真実は、ファンド中最も成功した投資案件のリターンが、その他すべての案件の合計リターンに匹敵するか、それを超えることだ。
----------
Authors:
ジャレド ダイアモンド
Title:銃・病原菌・鉄 下巻
ASIN:B00DNMG8QC
Quote:つまり、「平等」という形容詞は、小規模血縁集団では、個人の人柄、強靭さ、知性、戦いの技量などにもとづき、リーダーが非公式に決まるということを意味している。
----------
Authors:
ジャレド ダイアモンド
Title:銃・病原菌・鉄 下巻
ASIN:B00DNMG8QC
Quote:小規模血縁集団は、「平等」という形容詞で表現されるが、それは、すべてのメンバーがわけへだてなく平等な権限を持つという意味ではなく、階級が未分化でメンバーが社会的に区別されていない、という意味である。
----------
Authors:
ジャレド ダイアモンド
Title:銃・病原菌・鉄 下巻
ASIN:B00DNMG8QC
Quote:人間社会の多様性は、音楽様式や生活様式などと同様、時間の経過とともに連続的に変化するものであり、段階的に線引きする分類はどうしても不完全なものにならざるをえない。
----------
Authors:
ジャレド ダイアモンド
Title:銃・病原菌・鉄 下巻
ASIN:B00DNMG8QC
Quote:人類の科学技術史は、こうした大陸ごとの面積や、人口や、伝播の容易さや、食料生産の開始タイミングのちがいが、技術自体の自己触媒作用によって時間の経過とともに増幅された結果である。
----------
Authors:
宇野常寛
門脇耕三
落合陽一
尾原和啓
猪子寿之
駒崎弘樹
根津孝
Title:静かなる革命へのブループリント この国の未来をつくる7つの対話
ASIN:B00L8EMLGS
Quote:東京に新しいネットワーク構造をつくるのは、道路や鉄道のような物理空間におけるインフラだけではなく、もしかしたら情報空間におけるインフラなのかもしれません。
----------

参考記事:

amazonの購入履歴を取得する。 - mirandora.commirandora.com

ruby - mechanize how to get current url - Stack Overflow


修正点

たいてい<span class="sampleCloseQuote">内に引用が格納されているのですが、そうでない場合もありました。

<div class="sampleHighlight">
  
    <div>
      
      文学なんて必要ないという人もいますが、とんでもない話です。統計データだけで理解できるほど、世の中は単純ではありません。 <span class="sampleCloseQuote">───ストーリーがないと、普通の人がどうなのかなんてわからないと。</span>
    </div>
  

  
</div>

このように、<span class="sampleCloseQuote">の外に出ちゃってる場合もあるので、<div class="sampleHighlight">の最初の子要素のテキストを取得せねばなりません。

Note:が付いている場合

<div class="sampleHighlight">
  
    <div>
      
      <span class="sampleCloseQuote">相変わらず作戦目的はあいまいで、米軍の本土上陸を引き延ばすための戦略持久か航空決戦かの間を揺れ動いた。とくに注目されるのは、大本営と沖縄の現地軍にみられた認識のズレや意思の不統一であった。</span>
    </div>
  

  
    <div class="sampleNote">
      <div>
  <span class="note">Note:</span>
  <span class="noteText">*****</span>
</div>
    </div>
  
</div>

読者がメモを付けている場合は、Note:として表示されるのですが、それは取得しない。
なので前述した通り<div class="sampleHighlight">の最初の子要素を取ってくる。今の所、NoteはQuoteの下にくる構造になっているのでそれでなんとかいけます。

    # puts "Quote:"+c.css('span.sampleCloseQuote').text.strip
    puts "Quote:"+c.css('div.sampleHighlight > div:first-child').text.strip

さらに修正(1/26)

すいませんほんとにすいません

なんとQuoteがない!?場合もある。いやマジで。
引用をせずにNote、つまりメモだけ、とか。テストとして投稿するパティーンが多いようで。

例えばこんなのとか。

<div class="eventHighlight">
  
  
<div class="sampleHighlight noQuote">
  

  
    <div class="sampleNote">
      <div>
  <span class="note">Note:</span>
  <span class="noteText">テスト</span>
</div>
    </div>
  
</div>

</div>

引用がない場合、sampleHighlightにさらにnoQuoteというクラスが付加されるようなので、

# noQuoteが存在しない(つまり引用があるとみなす)限りは
unless c.css('div.noQuote').text.present?
(以下処理)
else
(引用がないよう、と)
end

というような条件分岐でなんとか回避すると。present?を使っているのでRailsでの話になりますね。
Rubyだとどうか分からんです。nil?やらempty?やらでどうにかするのかなと。