[ruby] webdriver を使って amazon.co.jp での購入履歴情報を取得する

# 課題

amazon.co.jp での今年に購入した電子書籍の電子領収書を一括で取得する。(MacOSX, firefox の環境で)

電子書籍を大量に購入していて、その購入領収書を提出する必要がある場合に利用する。


# 方法

1. ruby で selenium-webdriver をつかって firefox を自動操作して、
amazon の購入履歴ページ (2014 年分) の全画面の html ソースを取得する。
amazon のページにアクセスするための情報はコマンドラインの引数として渡す。
2. 各購入品の領収書のページにアクセスして、スクリーンショットを取得する。

~~~console
$ ruby amazon.rb 登録メールアドレス パスワード
~~~
プログラムコードはこの記事の末尾に示す。

プログラムを実行すると、firefox が自動起動する。
自動で  amazon へのログイン、購入履歴ページの閲覧、ログアアウト、firefox の終了が行われる。
実行後は、./screeshots 以下に (page-\*.html：一覧ページ、 order-*.png:領収書) が保存される。
./scrennshots フォルダはあらかじめ作成しておく。

# 後処理の例

上のプログラム実行で情報は取得できるが、これを加工する例を２つ示す。

1. 電子領収書を１つの PDF ファイルにまとめる。

~~~sh
convert -resize 575x823 -gravity north -background white -extent 595x842 *.png 1.pdf
~~~

生成した PDF の例：
![amazon-orders.png](https://qiita-image-store.s3.amazonaws.com/0/14958/e97ab7e8-85ba-8eaa-8d40-4c31e6ba4006.png)


2. 表計算ソフトで管理できるように [購入日、価格、値引き額、タイトル、商品情報 URL] を
csv 形式で得る。

~~~console
$ ruby make-index.rb > 1.csv
$ cat head.csv 1.csv > 2.csv
~~~
プログラムコードはこの記事の末尾に示す。

生成した csv を元に列名、価格合計セルを追加した例：

![amazon-order-index.png](https://qiita-image-store.s3.amazonaws.com/0/14958/14873400-7b61-77bd-ccda-d3adfd63e2b2.png)


# ソースコード

## Gemfile

~~~rb
source "https://rubygems.org"

gem 'selenium-webdriver'
gem 'nokogiri'
~~~

## amazon.rb

~~~amazon.rb
# -*- coding: utf-8 -*-

# 1. amazon の購入履歴を取得する。(scrennshots/* に保存される)
#   $ ruby amazon.rb email password
#
# 2. 取得した情報から、明細書(*.png) を１つの PDF にまとめたものを作成する。
#    (imagemagic の convert コマンドを使う)
#   $ convert -resize 575x823 -gravity north -background white -extent 595x842 screenshots/ord*.png 1.pdf
#
# 3. 取得した情報から、csv 形式で購入物一覧表を作成する。
#   $ ruby make-index.rb > 1.csv

require 'rubygems'
require 'selenium-webdriver'

SCREENSHOTS_DIR = './screenshots'

module Amazon
  class Driver
    # 新しいタブで 指定された URL を開き、制御をそのタブに移す。
    def open_new_window(wd, url)
      a = wd.execute_script("var d=document,a=d.createElement('a');a.target='_blank';a.href=arguments[0];a.innerHTML='.';d.body.appendChild(a);return a", url)
      a.click
      wd.switch_to.window(wd.window_handles.last)

      wd.find_element(:link_text, '利用規約')
      yield
      wd.close
      wd.switch_to.window(wd.window_handles.last)
    end

    # 現在の画面からリンクが張られている購入明細を全て保存する。
    def save_order(wd)
      wd.find_element(:link_text, '利用規約')
      orders = wd.find_elements(:link_text, '領収書／購入明細書')
      orders.each do |ord|

        open_new_window(wd, ord.attribute('href')) do
          @order_seq += 1
          wd.save_screenshot("#{SCREENSHOTS_DIR}/order_#{format('%03d', @order_seq)}.png")
        end
      end
    end

    def save_order_history(wd, auth)
      @page_seq = 0
      @order_seq = 0

      # 購入履歴ページへ
      wd.get 'https://www.amazon.co.jp/gp/css/order-history'

      # ログイン処理
      wd.find_element(:id, 'ap_email').click
      wd.find_element(:id, 'ap_email').clear
      wd.find_element(:id, 'ap_email').send_keys auth[:email]

      wd.find_element(:id, 'ap_password').click
      wd.find_element(:id, 'ap_password').clear
      wd.find_element(:id, 'ap_password').send_keys auth[:password]

      wd.find_element(:id, 'signInSubmit-input').click

      unless wd.find_element(:xpath, "//form[@id='order-dropdown-form']/select//option[4]").selected?
        wd.find_element(:xpath, "//form[@id='order-dropdown-form']/select//option[4]").click  # 今年の注文
      end
      wd.find_element(:css, "#order-dropdown-form > span.in-amzn-btn.btn-prim-med > span > input[type=\"submit\"]").click

      # [次] ページをめくっていく
      loop do
        wd.find_element(:link_text, '利用規約')
        @page_seq += 1
        wd.save_screenshot("#{SCREENSHOTS_DIR}/page_#{format('%03d', @page_seq)}.png")
        open("#{SCREENSHOTS_DIR}/page_#{format('%03d', @page_seq)}.html", 'w') {|f|
          f.write wd.page_source
        }

        # ページ中の個々の注文を閲覧する。
        save_order(wd)

        elems = wd.find_elements(:link_text, '次へ »')
        break if elems.size == 0
        elems[0].click
      end

      # サインアウト
      wd.get 'http://www.amazon.co.jp/gp/flex/sign-out.html/ref=gno_signout'
    end
  end
end

include Amazon

if ARGV.size != 2
  puts "usage: ruby #{$PROGRAM_NAME} account password"
  exit 1
end

wd = nil
begin
  ad = Amazon::Driver.new
  wd = Selenium::WebDriver.for :firefox
  wd.manage.timeouts.implicit_wait = 20 # 秒
  ad.save_order_history(wd, email: ARGV[0], password: ARGV[1])
ensure
  wd.quit if wd
end
~~~

### ポイント

一覧ページの領収書のリンクを  click すると、ページ遷移して領収書画面が表示される。
１つの一覧ページは　10 個の領収書のリンクがある。
"領収書リンクの click", "スクリーンショット取得"、"前ページに戻る" の繰り返し操作では一覧ページを何度もロードすることになる。
そこで、領収書画面は別ウィンドウで開くようにし、スクリーンショットを取得したら、そのウィンドウを閉じる という操作をプログラムで行わせることにした。一覧ページを何度もロードすることを避けている。

webdriver では、
  1. javascript  をつかって別 window を開く
  2. 開いた window に制御を移す。そして 領収書画面を load する。
  3. 処理が終わったら、window を close する。
  4. 一覧ページの window に制御を戻す。
という操作を行わせている。

## make-index.rb

~~~make-index.rb
# -*- coding: utf-8 -*-

require 'open-uri'
require 'nokogiri'
require 'csv'

# 値引き額を得る
# @param url 書籍情報ページの IRL
# @return 値引き額
def saving(url)
  off = 0
  begin
    charset = nil
    html = open(url) do |f|
      charset = f.charset
      f.read # htmlを読み込んで変数 htmlに渡す
    end
    doc = Nokogiri::HTML.parse(html, nil, charset)
    # 値引きデータを抜き出す。
    price = doc.css('.savingsRow .price')
    if price && price.size > 0
      off = price.text.scan(/.+/)[0].scan(/\d+/).join('')
    end
  rescue => ex
    STDERR.puts url
    STDERR.puts ex
  end
  off.to_i
end

# 購入物の一覧を csv 形式で得る。（購入日、価格、値引額、タイトル）
# @return csv 形式の文字列
def generate_csv
  csv_string = CSV.generate do |csv|
    Dir::glob("screenshots/**/*.html").each do |path|
      f = File.open path

      page = Nokogiri::XML f
      orders = page.css('.action-box')
      orders.each do |order|
        date = order.css('.order-level > h2').text
        price = order.css('.price').text.scan(/\d+/).join('')
        title =  order.css('.item-title').text.strip
        url = order.css('.shipment a').attribute('href').value
        off = saving(url)
        csv <<  [date, "#{format('%8d', price)}", off, title, url]
        end
      f.close
    end
  end
  csv_string
end

puts generate_csv
~~~

## head.csv

~~~head.csv
日付,価格,OFF,書名,URL
,=SUM(B3:B300),=SUM(C3:C300),
~~~

### ポイント

値引き額は、購入一覧ページ、領収書ページには記載されていない。
商品ページを訪れ、その中の値引き額情報を検索するようにしている。
購入一覧先にある商品ページは存在していないことがある。（価格や情報が更新された？）
その場合はプログラムを中断せずに、STDERR にメッセージを出して、最後の商品まで処理を行うようにしている。

# 更新履歴
- 2019-03-09
png から pdf を作る ruby スクリプトを追加した。 
1 ページに２件、１０ページの pdf を生成することができる。

- 2014-01-04
amazon.co.jp からの購入履歴 (領収書ページ) を得るスクリプトを更新した。  
(amazon の購入履歴ページも構造が半年前から変化したため)  
[https://github.com/katoy/amazon-orders/blob/master/amazon.rb](https://github.com/katoy/amazon-orders/blob/master/amazon.rb)