Python と selenium でAmazonの購入履歴を取得する

# この記事の内容
Amazonの購入履歴から、領収書をすべてpdf化し、ファイル名を注文番号にして指定したフォルダに保存するという作業をPythonとseleniumによって実現します。
プログラミングをやり始めて3か月なので上手くないところもあるかもしれませんが、備忘を兼ねて記事を残します。

### 目次
1.この記事を書くに至った経緯
2.実装内容の概要
3.実装
　3.0.環境
　3.1.コード全体
　3.2.Selnium のインストール
　3.3.パッケージインストール
　3.4.初期設定
　3.5.ログイン・注文履歴画面へ
　3.6.1ページ内の領収書リンクを取得、領収書内へ
　3.7.領収書をpdf保存し指定のフォルダに名前を変更して保存
　3.8.次のページへ
4.追加の機能追加について
5.saleceforceでの固有の論点
　5.1.二段階認証の突破
　5.2.画面には確かに存在する要素がdriver.find_elementsで取得できない場合->iframeを疑う
　5.3.無限スクロールを読み込む
6.最後に

# 1. この記事を書くに至った背景
先日同僚から、「saleceforceの商談一覧から各商談の内容pdfを保存するという定型業務を毎日行っているが、商談が増えてきてとても手作業では追い付かない」と相談を受けました。

聞いてみると、一日に20~50商談について、それぞれ商談画面に保存されている3~5件のpdfを保存、印刷、ファイリングしているとのこと。この業務自体いらないのではないか？、という論点はさておき、最近Pythonを学び始めた僕には格好の練習場であることから勝手に自動化に向けた学習を始めることとしました。

いきなりsaleceforceで自動化作業を試すのは危険なので、まずは自分のプライベートAmazonアカウントで練習を行うこととしました。また、自社のsaleceforceはカスタマイズがかなり入っておりあまり汎用性がないため、全体像は公開せずに末尾にポイントだけまとめておくこととしました。


# 2. 実装内容の概要
下記、自動化の流れを画像にて記載します。

#### ブラウザを立ち上げAamazon.co.jpにアクセス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/5e7c384b-7e87-d7bd-2dfd-263e798ac805.png)

#### ログイン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/531f1fcb-c6ce-e3ba-f178-d6127a9b1a3c.png)

#### 注文履歴一覧へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/a65a4728-2a4b-f972-4d45-ea71d1392eb7.png)

#### 個別の領収書/購入明細書へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/923c67c9-7419-ef5d-305a-c60d72dd28c6.png)

#### 領収書を保存
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/de84c355-dcf1-e9b9-8538-0a83e48bde37.png)

#### 領収書はファイル名を注文番号にして指定のフォルダに保存。これを１ページすべての注文で行う
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/6261aa8d-45bc-2d13-0be5-c38f47048c60.png)

#### 注文履歴一覧の最後のページまで同じ操作を繰り返す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668985/df5d1b15-109e-fd3d-9ebb-3d3d6dcc279a.png)


#3. 実装
###3.0.環境
Windows 10
Python 3.7.6
Selenium 3.141.0
エディタ: JupyterLab
ブラウザ: Chrome 83.0.4103.116

###3.1.コード全体
```python

# パッケージインストール
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait

import datetime
import json
import os
import time


# ユーザー情報
ID = 'XXXXXXXXXX_XXXXXX@gmail.com'
PASS = 'password'
PROFILE_PATH = 'Users/username/AppData/Local/Google/Chrome/User Data'
# アクセスアドレス
ADRESS = 'https://www.amazon.co.jp/'
# ファイルの保存先
DOWNLOAD_PATH = r'C:\Users\username\Downloads' # pdfダウンロードしてデフォルトで保存される先のパス
SAVE_PATH = r'C:\Users\username\Desktop\amazon_pdfs' # ファイルの保存先のパス 

# pdf保存のための初期設定
chrome_options = webdriver.ChromeOptions()
settings = {"recentDestinations": [{"id": "Save as PDF",
                                    "origin": "local",
                                    "account": ""}],
            "selectedDestinationId": "Save as PDF", 
            "version": 2}
prefs = {'printing.print_preview_sticky_settings.appState': json.dumps(settings)}
chrome_options.add_experimental_option('prefs', prefs)
chrome_options.add_argument('--kiosk-printing')
driver = webdriver.Chrome(r"chromedriver.exe", options=chrome_options)

# クッキー
options = webdriver.chrome.options.Options()
options.add_argument('--user-data-dir=' + PROFILE_PATH)
# ブラウザ立ち上げ
driver.get(ADRESS)
time.sleep(2)

#ログイン
login_btn = driver.find_element_by_id('nav-link-accountList')
login_btn.click()
time.sleep(1)

login_email = driver.find_element_by_id('ap_email')
login_email.send_keys(ID)
next_btn = driver.find_element_by_id('continue')
next_btn.click()
time.sleep(1)

login_pass = driver.find_element_by_id('ap_password')
login_pass.send_keys(PASS)
login_btn = driver.find_element_by_id('signInSubmit')
login_btn.click()

# 携帯電話登録のページがでてくる場合は後でとする
try:
    mobile_skip = driver.find_element_by_id('ap-account-fixup-phone-skip-link')
    mobile_skip.click()
except:
    pass

time.sleep(1)

# 注文履歴画面へ
order_history = driver.find_element_by_id('nav-orders')
order_history.click()
time.sleep(2)
#すべてのページで行う
while True:

    main_handle = driver.current_window_handle
    receipt_links = driver.find_elements_by_link_text('領収書等')
    # 1ページないのすべての領収書で行う
    for receipt_link in receipt_links:

        receipt_link.click()
        time.sleep(1)
        receipt_purchase_link = driver.find_element_by_link_text('領収書／購入明細書')

        # クリック前のハンドルリスト
        handles_before = driver.window_handles
        # 新しいタブで開く
        actions = ActionChains(driver)
        actions.key_down(Keys.CONTROL)
        actions.click(receipt_purchase_link)
        actions.perform()
        # 新しいタブが開くまで最大30秒待機
        WebDriverWait(driver, 30).until(lambda a: len(driver.window_handles) > len(handles_before))
        # クリック後のハンドルリスト
        handles_after = driver.window_handles

        # ハンドルリストの差分
        handle_new = list(set(handles_after) - set(handles_before))
        # 新しいタブに移動
        driver.switch_to.window(handle_new[0])
        # pdf化
        driver.execute_script('window.print();')
        # pdf化したものをダウンロードフォルダから指定フォルダに名前を変更して保存する

        new_filename = driver.find_element_by_class_name('h1').text + '.pdf'# 新しいファイル名
        timestamp_now = time.time() # 現在時刻
        # ダウンロードフォルダを走査
        for (dirpath, dirnames, filenames) in os.walk(DOWNLOAD_PATH):
            for filename in filenames:
                if filename.lower().endswith(('.pdf')):
                    full_path = os.path.join(DOWNLOAD_PATH, filename)
                    timestamp_file = os.path.getmtime(full_path) # ファイルの時間
                    # 3秒以内に生成されたpdfを移動する
                    if (timestamp_now - timestamp_file) < 3: 
                        full_new_path = os.path.join(SAVE_PATH, new_filename)
                        os.rename(full_path, full_new_path)
                        print(full_path+' is moved to '+full_new_path)  

        time.sleep(1)
        driver.close()
        driver.switch_to.window(main_handle)
    # 次へのボタンが押せなくなった時点で終了
    try:
        driver.find_element_by_class_name('a-last').find_element_by_tag_name('a')
        driver.find_element_by_class_name('a-last').click()
        driver.switch_to.window(driver.window_handles[-1])
    except:
        break 

driver.quit()

```

###3.2.Selenium のインストール
まずは、SeleniumのインストールをしようとAnacondaで
```conda install selenium```
をしましたがうまくいかず、Anacondaではあまり推奨されていないようですがpipでインストールしました。
```pip install selenium```
で上手くいきました。

Seleniumはこれだけではうまく動かず、別途使用するブラウザ専用のDriverをダウンロードする必要があるようです。
ダウンロードは SeleniumのHP(https://www.selenium.dev/) からダウンロードができます。
僕が使用しているChromeは83.0.4103.116だったのでここ「chromedriver_win32.zip」をダウンロードしました。
これを解凍し、exeファイルをPythonのexeファイルと同じフォルダに保存しておきました。
僕の場合は「C:\Users\username\AppData\Local\Microsoft\WindowsApps」です。
ドライバーはどこにおいても呼び出せるようですが、調べた限りここに保存するのが一番簡単そうでした。

###3.3.パッケージインストール
必要なPythonパッケージをインストールしておきます。
特に説明ないです。

```python

# パッケージインストール
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait

import datetime
import json
import os
import time
```

###3.4.初期設定
初期値を設定します。ここを変えれば他のアカウント、PCでも使えるはずです。
PC2台で実験しました。

```python

# ユーザー情報
ID = 'XXXXXXXXXX_XXXXXX@gmail.com'
PASS = 'password'
PROFILE_PATH = 'Users/username/AppData/Local/Google/Chrome/User Data'
# アクセスアドレス
ADRESS = 'https://www.amazon.co.jp/'
# ファイルの保存先
DOWNLOAD_PATH = r'C:\Users\username\Downloads' # pdfダウンロードしてデフォルトで保存される先のパス
SAVE_PATH = r'C:\Users\username\Desktop\amazon_pdfs' # ファイルの保存先のパス 

# pdf保存のための初期設定
chrome_options = webdriver.ChromeOptions()
settings = {"recentDestinations": [{"id": "Save as PDF",
                                    "origin": "local",
                                    "account": ""}],
            "selectedDestinationId": "Save as PDF", 
            "version": 2}
prefs = {'printing.print_preview_sticky_settings.appState': json.dumps(settings)}
chrome_options.add_experimental_option('prefs', prefs)
chrome_options.add_argument('--kiosk-printing')
# ドライバー作成
driver = webdriver.Chrome(r"chromedriver.exe", options=chrome_options)

# クッキー
options = webdriver.chrome.options.Options()
options.add_argument('--user-data-dir=' + PROFILE_PATH)
```
ユーザー情報やアクセスするアドレスは変数に入れておきます。
また、ブラウザ画面をpdfとして保存するためには、どうやらこの設定が必要のようです。中身はあまり理解できていないですがとりあえずこれで上手くいきます。

本当はここでファイルの保存先も指定できれば良いのですが、どうやってもデフォルトの保存先（通常はダウンロードフォルダ）にpdfが保存されてしまい、指定した保存先にファイルを保存することができませんでした。
そこで、苦肉の策として、一度デフォルトの保存先に保存されたファイルを指定したフォルダに移動することとしました。該当コードは「3.8.領収書をpdf保存し指定のフォルダに名前を変更して保存」で記載しています。

seleniumで一番重要なdriverを作成します。このあとdriverがたくさん出てきます。

クッキーについては、よくわからないんですが、多くのページで設定するほうがよいとの記載でしたのでとりあえず設定しました。これはなくても動きます。

###3.5.ログインの実装

```python
#ログイン
login_btn = driver.find_element_by_id('nav-link-accountList')
login_btn.click()
time.sleep(1)

login_email = driver.find_element_by_id('ap_email')
login_email.send_keys(ID)
next_btn = driver.find_element_by_id('continue')
next_btn.click()
time.sleep(1)

login_pass = driver.find_element_by_id('ap_password')
login_pass.send_keys(PASS)
login_btn = driver.find_element_by_id('signInSubmit')
login_btn.click()

# 携帯電話登録のページがでてくる場合は後でとする
try:
    mobile_skip = driver.find_element_by_id('ap-account-fixup-phone-skip-link')
    mobile_skip.click()
except:
    pass

time.sleep(1)

# 注文履歴画面へ
order_history = driver.find_element_by_id('nav-orders')
order_history.click()
time.sleep(2)
```
Amazonのページはid名が明確でやりやすいです。ログイン、注文履歴画面まではfind_element_by_idで迷うことなく進めます。

何度かログインを繰り返す中で、「携帯電話登録」のページが出てくる場合がありました。このページをスキップするために try exceptを入れています。

###3.6.1ページ内の領収書リンクを取得、領収書内へ
ここからやや汚いコードとなります。クラスや関数できれいなコードを書ければよいのですが、力及ばず。
作業の流れを淡々と書き下していきます。

```python
# すべてのページで行う
while True:

    main_handle = driver.current_window_handle
    receipt_links = driver.find_elements_by_link_text('領収書等')
    # 1ページないのすべての領収書で行う
    for receipt_link in receipt_links:

        receipt_link.click()
        time.sleep(1)
        receipt_purchase_link = driver.find_element_by_link_text('領収書／購入明細書')

        # クリック前のハンドルリスト
        handles_before = driver.window_handles
        # 新しいタブで開く
        actions = ActionChains(driver)
        actions.key_down(Keys.CONTROL)
        actions.click(receipt_purchase_link)
        actions.perform()
        # 新しいタブが開くまで最大30秒待機
        WebDriverWait(driver, 30).until(lambda a: len(driver.window_handles) > len(handles_before))
        # クリック後のハンドルリスト
        handles_after = driver.window_handles

        # ハンドルリストの差分
        handle_new = list(set(handles_after) - set(handles_before))
        # 新しいタブに移動
        driver.switch_to.window(handle_new[0])
```
まずは、```main_handle``` を定義しておきます。操作するウィンドウが多くなると名前を付けておかないと、「今どのウィンドウの操作を行っているか」がわからなくなります。なのでメインとなるウィンドウについて```main_handle``` という名前を付けておきます。

1ページには10くらいの注文履歴があるので、そのすべての注文履歴の「領収書等」のリンク先をlistに格納しておきます。
「領収書等」のリンクを取得するためにうまい具合のclass name 等の要素が見つからなかったため、直接「領収書等」のテキストを探しに行きました。

「領収書等」のリストをforループで回します。
各「領収書等」をクリックした後に「領収書／購入明細書」をクリックします。これもまた、直接「領収書／購入明細書」テキストを探しに行きクリックしています。

「領収書／購入明細書」を普通にクリックすると、現在のウィンドウが遷移してしまいます。なので「Ctrl + クリック」をするために下記の設定を行います。

```python
# 新しいタブで開く
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL)
actions.click(receipt_purchase_link)
actions.perform()
```
ActionChainsのインスタンス化して、actionsとします。そのあとに、行いたい動作を記述して、perform()で実行します。ここでは「コントロールキーを押す」という動作と「領収書／購入明細書をクリックする」とう動作を記述しています。

この記述方法で、「PAGE_DOWN」を使用すればウインドウを下に移動したりできます。ほかにもいろいろできるようです。詳しくは検索すれば他のページで解説されていますので、そちらで。

新しいウィンドウを開いた後は、新しいウィンドウを選択する必要があります。ここでは最初に見つけたやり方（ハンドルリストの差分をとる）を記載していますが、この記述はやや煩雑です。もっと完結に書くのであれば```handl_new = driver.window_handles[-1] ```として、```window_handles```リストの一番最後を取得することで、今開いたウィンドウを選択したほうがよいです。



###3.7.領収書をpdf保存し指定のフォルダに名前を変更して保存

```python

        # pdf化
        driver.execute_script('window.print();')
        # pdf化したものをダウンロードフォルダから指定フォルダに名前を変更して保存する

        new_filename = driver.find_element_by_class_name('h1').text + '.pdf'# 新しいファイル名
        timestamp_now = time.time() # 現在時刻
        # ダウンロードフォルダを走査
        for (dirpath, dirnames, filenames) in os.walk(DOWNLOAD_PATH):
            for filename in filenames:
                if filename.lower().endswith(('.pdf')):
                    full_path = os.path.join(DOWNLOAD_PATH, filename)
                    timestamp_file = os.path.getmtime(full_path) # ファイルの時間
                    # 3秒以内に生成されたpdfを移動する
                    if (timestamp_now - timestamp_file) < 3: 
                        full_new_path = os.path.join(SAVE_PATH, new_filename)
                        os.rename(full_path, full_new_path)
                        print(full_path+' is moved to '+full_new_path)  

        time.sleep(1)
        driver.close()
        driver.switch_to.window(main_handle)
```
```driver.execute_script('window.print();')``` でプリントを実行します。初期設定でpdf化を行っているのでpdf保存されます。

pdfは初期設定をどう頑張ってもデフォルトの保存先に保存されてしまうので、一度デフォルトに保存しておき、デフォルトフォルダ内で3秒以内に生成されたpdfを指定フォルダに名前を変えて保存するというコードを採用しました。
ファイル名はh1タグの注文番号を取得しています。

pdfの保存が終わった段階で領収書ウィンドウを閉じ、main_handleを選択しなおします。

###3.8.次のページへ

ページ内の領収書の保存が完了した後に、「次へ」ボタンを押して次のページに移ります。

```python

    # 次へのボタンが押せなくなった時点で終了
    try:
        driver.find_element_by_class_name('a-last').find_element_by_tag_name('a')
        driver.find_element_by_class_name('a-last').click()
        driver.switch_to.window(driver.window_handles[-1])
    except:
        break 

driver.quit()
```
「次へ」ボタンは```driver.find_element_by_class_name('a-last').find_element_by_tag_name('a')``` で選択できます。
try exceptで「次へ」ボタンが押せなくなった段階でbreakし、```driver.quit()```` で作業を完了します。

#4.追加の機能追加について
以上が、Amazonの領収書をすべて保存するコードの説明になります。

当初の構想では商品名、購入日、金額等の情報をpandaのDataFrameにappendしていき、最後にcsvに保存しようかと考えたのですが、ややめんどくさいところがあり実装はやめました。気が向いたら実装してみます。

#5.saleceforceでの固有の論点
本来の目的であったsaleceforceの自動化についてはなんとか問題なく作成することができましたが、カスタマイズしたsaleceforceのブラウザ操作をすべて載せたとしても横展が難しいと思います。

ここでは、saleceforceの自動化でややつまった箇所について備忘として記載しようと思います。

###5.1.二段階認証の突破
Seleniumでsaleceforceにログインすると、必ず毎回二段階認証が行われます。二段階認証のメールを取得し、認証コードを入力するというコードを書いてもよかったのですが、saleceforceのセキュリティ設定で「組織の信頼済みIP範囲の設定」で対応が可能です。

詳細は下記をご確認ください。
(https://help.salesforce.com/articleView?id=security_networkaccess.htm&type=5)


###5.2.画面には確かに映っている要素がdriver.find_elementsで取得できない場合->iframeを疑う
今回一番てこずった箇所がここです。
ブラウザの画面に存在する要素を取得できないということがおきます。

いろいろ調べた結果```<iframe>```タグが原因ということがわかりました。
要は、```<iframe>```タグ内の要素については、まず```driver.switch_to_frame(iframe)``` で選択してから、その中の要素を```find_element``` しなければならないようです。

ここだけで数時間悩みました。
わかってみれば簡単です。

###5.3.無限スクロールを読み込む
無限スクロールの表を読み込むという実装がまだうまくできていないです。
PAGE_DOWNでウィンドウを下に移動できますが、表の読み込みとウィンドウの下の移動のタイミング？をうまく制御できていないです。
ここは現時点の課題です。

#6.最後に
長くなりましたが、以上となります。
思ったよりも簡単にできましたが、まだまだ課題があるので少しずつ勉強を続けたいと思います。