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は初めて使用しましたが、他の領収書にも流用できそうなので優秀ですね。
それでは、よい事業主ライフを!