2月になると個人事業主にとって面倒な確定申告の時期ですね。。
私はめんどくさがりなので、月末に領収書を整理することもなく、
この時期になると慌ただしくなり始めます。

特に面倒なのが楽天市場で購入した商品の領収書取得。。。
私の場合、年間100件近い領収書を1つづつダウンロードしていかなくてはなりません。
それも毎年。。。これは苦行。。。

ということで、
楽天の指定年の領収書を一括でダウンロードするプログラムをSelenium(Ruby)で組んでみました。
Seleniumを選んだ理由は、楽天側でJs処理が走るので、mechanizeでは対応できないと思い、
使用したことのあるSeleniumにしました。

結果としてはできたのですが、ビックカメラや楽天Koboなどの
通常とは異なる領収書発行が必要な決済については、
下記の点から今回はスルー
し、手動での取得にしました。

  • 通常の取得から更にページ複数をまたぐ
  • そもそもの決済数が少ない

ただ、それでも自分の場合は90%以上を自動で賄えましたので、
来年からは作業時間もかなり短縮されるでしょう。

また、領収書のファイル名も「order_receipt_注文番号.pdf」となっており、
自分が期待するものではなかったので、その点もPDFの内容を解析し、
独自のファイル名をつけるように設定しました。

こんな記事を書いて怒られないか心配ですが、とりあえず見ていきましょう。

# この記事で解決すること

  • 楽天の領収書を一括でダウンロードできる(特殊な領収書発行が必要な場合を除く)
  • 領収書のpdfファイル名をpdfの中身を解析して変更できる

前提

今回はWEBには公開せず、ローカルでRubyを使用してCLI内で行います。

かかる時間は、購入一覧までの処理を除き、1件につき5秒程度です。(120件で5分程度)

環境

  • WSL
  • ruby 3.1.2p20

使用したライブラリ

  • selenium-webdriver 4.17.0
  • pdf-reader 2.12.0

処理の順序

  • Ruby側の処理
  • Seleniumの処理
    • ログイン処理
    • 指定年の購入履歴ページへ移動
    • 領収書の取得
  • ファイル名を変更

Ruby側の処理

まずは「ID・パスワード・取得年・宛名」が必要ですので、
CLIで入力するためのコードを挿入します。
※ローカル処理がメインなので、直書きでも良いかも。。。

p 'IDを入力してください'
id = gets.chomp
p 'パスワードを入力してください'
password = gets.chomp
p '取得する年を入力してください'
year = gets.chomp
p '請求書の宛名を入力してください'
$myname = gets.chomp

8行目

$mynameのみ関数内で使用するので、グローバル変数で指定してます。

Seleniumの処理

ログイン処理

options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')
session = Selenium::WebDriver.for :chrome, options: options
session.manage.timeouts.implicit_wait = 10

session.navigate.to 'https://grp02.id.rakuten.co.jp/rms/nid/vc?__event=login&service_id=s08&fidomy=1'
login_form = session.find_element(:name, 'LoginForm')
login_name = session.find_element(:name, 'u')
login_pass = session.find_element(:name, 'p')
login_name.send_keys(id)
login_pass.send_keys(password)
login_form.submit
sleep(1)

1~4行目

Seleniumの初期設定になります。
今回はheadlessで取得します。
4行目は10秒経過しても進まない場合、相手のサーバーにとって迷惑なので、
処理を停止し、エラーを返します。

6~12行目

ログイン画面に移動し、ログインします。

13行目

今回のプログラムに共通して言えることですが、ページ遷移の際は、
相手のサーバーに負荷をかけないために、最低でも1秒は待つという業界の暗黙の了解です。

指定年の購入履歴ページへ移動

session.find_element(:xpath, "//a[@aria-label='購入履歴']").click
sleep(1)

year_value = session.find_element(:name, 'display_span')
year_value.send_keys(year)
sleep(1)

1行目

そのままでも購入履歴ページへたどり着くと思いますが、仕様が変わった際のことも考え、
購入履歴ページ再度遷移するようにしています。

4, 5行目

最初にCLIで入力した「取得する年」を指定年のselectで選択します
これで、取得する年の購入履歴ページへ遷移することができます。

領収書の取得

# 一覧ページ数取得
total_item_count = session.find_element(:xpath, "//div[@class='oDrPager']//*[@class='totalItem']").text.to_i
total_item_pages = total_item_count.quo(25).to_f.ceil

# 注文詳細URLの一覧取得
1.upto(total_item_pages) do |num|

  ## インボイスの取得
  0.upto(24) do |i|
    item_list_url = 'https://order.my.rakuten.co.jp/?page=myorder&act=list&page_num=' + num.to_s
    session.navigate.to item_list_url
    sleep(1)
    return session.find_elements(:xpath, "//a[@data-ratid='ph_pc_orderdetail']")
    # 注文詳細ページでの処理
    if detail_link_list[i]
      href = detail_link_list[i].attribute('href')
      if href.include?('order.my.rakuten.co.jp')
        session.navigate.to href
        sleep(1)
        getInvoice(session)
        sleep(1)
      else
        pp "error: #{href}"
      end
    else
      pp 'complete: all'
      break
    end
  end
  if total_item_pages == 1
    pp 'complete: all'
    break
  end
  pp "complete: #{num}"
end

# ブラウザを終了
session.quit

1~3行目

決済の総数を取得し、そこから一覧のページ数を割り出しています。
楽天は件数変更ができず25件表示固定なので、この方法で問題ないかと。
※paginationを辿っても良さそうなのですが、
そんなに大量に注文しておらず、仕様が不明だったのでこの方法に落ち着きました。

6行目

本当は全てのURLを取得してからループさせたかったのですが、
Sleniumの仕様上、前回のページで取得した情報を引き継げないようですので、
各一覧ページごとに処理することにしました。

9~13行目

1ページの商品件数は25件固定なので、25回ループさせます。
処理としては、num番目の一覧ページにある
注文詳細ページへのリンクURL一覧を取得して返すというものです。
回りくどいことをしていますが、
前回のページで取得した情報を引き継げないというSeleniumの仕様に則ったものです。
これをなくすと、1つ目は取得できても、2つ目からはエラーが返ります。

15~24行目

先程取得したリンクURL一覧から、i番目に該当するもののhrefを取得し遷移します。
注意点は、order.my.rakuten.co.jp以外の注文詳細は特殊な領収書発行に該当します。
そのため、エラーログを返して後で見返せるようにしています。

getInvoice(session)について

def getInvoice(session)
  pp "start: #{session.current_url}"
  if !session.find_elements(:name, 'receipt_name').empty?
    receipt_name = session.find_element(:name, 'receipt_name')
    receipt_disabled = receipt_name.attribute('disabled')
    if !receipt_disabled
      receipt_name.send_keys($myname)
    end
    session.find_element(:class, 'receiptBtn').click
    if !session.find_elements(:id, 'invoiceReceiptDonwloadBtn').empty? && !receipt_disabled
      session.find_element(:id, 'invoiceReceiptDonwloadBtn').click
    end
    pp "complete: #{session.current_url}"
  else
    pp "error: #{session.current_url}"
  end
end

楽天の仕様上、一度発行した領収書の宛名は変更できません。
また、領収書ダウンロードのボタンが、
1段階目はプレーン、2段階目はModalで出現する場合もあるので、
その辺りを加味した内容になっています。
尚、1段階目のプレーンなボタンすらない場合は、特殊な領収書発行に該当しますので、
エラーログを返すようにしています。

26, 27行目

detail_link_list[i]がない場合、仮にiが3だとすると、
3以降は空だと推測できるのでcompleteログを出してループを抜けます。

30~33行目

仮に商品が20件しかない場合、一覧ページが1つだけという解釈になりますので、
completeログを出してループを抜けます。

38行目

ここまでくると領収書は全てダウンロードされているはずですので、seleniumを終了します。

ファイル名を変更

さて、ここからはpdf-readerを使用して、任意のファイル名を変更していきます。
※特殊な領収書発行に該当するものは、pdf内部の形式が異なるので手動です。

通常の領収書はorder_receipt_xxxxxx-xxxxxxxx-xxxxxxxxxx.pdf(xは注文番号)となり、
これでは、いつ・どこで・いくらの決済があった領収書かわからない状態です。

自分の場合は「注文日(決済日)_ショップ名_金額」にしたいので、それに沿って説明します。

invoice_files = Dir.glob('./*.pdf')
invoice_files.each do |path|
  pdf_file = File.basename(path)
  reader = PDF::Reader.new(pdf_file)
  reader.pages.each do |page|
    if page.number == 1
      page_text = page.text
      price = page_text.match(/([0-9|\,]{1,})円領収しました/)[1]
      full_date = page_text.match(/注文日[:|:][\s]??([0-9]{4})年([0-9]{1,2})月([0-9]{1,2})日??/) # 最後の日が別行として読み込まれている場合有
      date = "#{full_date[1]}#{full_date[2]}#{full_date[3]}"
      store = page_text.match(/但し[:|:][\s]??(.+)との取引/)[1]
      new_file_name = "#{date}_#{store}_#{price}.pdf"
      File.rename(pdf_file, new_file_name)
    end
  end
end

1, 2行目

今回はseleniumにheadless指定していますので、
領収書のpdfファイルはrubyのファイルと同階層に保存されているはずです。
ですので、同階層にあるpdfファイル一覧を取得し、ループを回します。

3~6行目

pdfを処理するための下準備をして、ページごとにループを回します。
この時、2ページ目以降も処理してしまうと、無駄な処理が走ってしまうので、
必要な情報が存在する1ページ目だけを処理します。

7~13行目

あとは、ページのテキストを取得して、
そこから正規表現で「金額」「日付」「ショップ名」を取得し、リネームします。
注意点は、9行目のように思いがけないところに改行が入っている場合があるという点です。

 

これで全て完了です!

最後に

久々にSeleniumを使いましたが、思いの外簡単に楽天の領収書を発行することができて大満足です。

ただし、楽天の仕様が変われば、再調整が必要になります。

規模の大きいモールなのでそんな簡単に変わることはないと思いますが。。。

また、pdf-readerは初めて使用しましたが、他の領収書にも流用できそうなので優秀ですね。

それでは、よい事業主ライフを!