PythonのSeleniumでnanacoギフトコードを自動登録

# 追記 2020/6/6
nanacoギフトコードを直接指定できるように変更しました。
登録失敗時に再登録を試みるよう変更しました。

# 追記 2020/5/15
nanaco番号などがスクリプトに入力されていない場合、実行時に尋ねるように変更しました。
合わせて本記事に書かれている手順も見直しました。

# 概要
PythonでSeleniumを使用し、nanacoギフトコードの取得から登録までを自動化します。
nanacoギフトコードの取得をせずに、予めストックしておいたギフトコードの登録も可能です。
入力支援ではなく、自動化です。
※福利厚生倶楽部のギフトコードでテストしました。その他の場所で購入したギフトコードはテストしていませんのでご注意ください。
※下記ツイート内の動画は早送りしておらず等倍再生です。また、コンソール画面には全てMISSEDと表示されていますが、2回目の実行のため重複登録扱いになっているためです。実際に1回目の実行時は正しく登録されてOKと表示されますのでご安心ください。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">seleniumで便利なもの作りたいと思ったので、nanacoギフトコードを自動で取得、登録するスクリプト作りました。<a href="https://t.co/7efggT42D5">https://t.co/7efggT42D5</a> <a href="https://t.co/HFm6o2NTzw">pic.twitter.com/HFm6o2NTzw</a></p>&mdash; 🅽.🆈. \ｵﾝｼｬ!!/ (@nog_yo) <a href="https://twitter.com/nog_yo/status/1261348388370386946?ref_src=twsrc%5Etfw">May 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# スクリプト実行までの準備(初回限定)
1. [最新のPython3](https://www.python.org/downloads/)をインストールします。すでにインストールされている場合は次の手順に進んでください。
上記サイトよりインストールする場合、「パスに追加する」旨のチェックボックスをオンにしてください。
![](https://www.javadrive.jp/python/install/img/p3-2.png)
※画像はhttps://www.javadrive.jp/python/install/index3.html より引用

1. [最新のChrome](https://www.google.co.jp/chrome/)をインストールします。すでにインストール済みの場合は次の手順に進んでください。

1. 追加ライブラリをインストールします。Windowsならコマンドプロンプト、Macならターミナルで下記コマンドを実行してください。
※環境に応じて`pip`または`pip3`で実行してください。

    ```
    pip3 install selenium
    pip3 install chromedriver-binary
    ```

1. 下記スクリプトをコピーし、メモ帳などのテキストエディタに貼り付け、`nanaco_transiter.py`というファイル名で保存してください。
この時、福利厚生倶楽部のIDやnanaco番号などをスクリプト内に記載すると、実行がスムースになります。記載する欄は8行目から10行目です。

```py:nanaco_transiter.py
#!/usr/bin/env python3
import time
import re
from sys import argv
import selenium
from selenium import webdriver

userName = "" # <- 購入した福利厚生倶楽部のIDを6桁と4桁で入力してください(例: 012345-6789)
nanacoNo = "" # <- nanaco番号16桁を入力してください(例: 0123456789012345)
securityCode = "" # <- カード記載の番号を7桁で入力してください(例: 0123456)

def getGiftCodes(driver, url):
    driver.get(url.strip())
    driver.find_element_by_name("kainno").send_keys(userName)
    driver.find_element_by_class_name("next").click()
    elements = driver.find_elements_by_css_selector(".table_gift tbody tr")
    giftCodes = []
    for element in elements:
        texts = element.text.split(" ")
        if re.match("^[a-zA-Z\d]{16}$", texts[-3]):
            giftCodes.append(texts[-3])
    return giftCodes

# nanacoサイトのセッションを張る
def loginNanaco(driver):
    driver.get('https://www.nanaco-net.jp/pc/emServlet')
    driver.find_element_by_name("XCID").send_keys(nanacoNo)
    driver.find_element_by_name("SECURITY_CD").send_keys(securityCode)
    driver.find_element_by_name("ACT_ACBS_do_LOGIN2").click() # ここでログイン
    time.sleep(1)
    driver.find_element_by_link_text("nanacoギフト登録").click()
    time.sleep(1)

# セッション内でギフトコードを登録する
def registGiftCode(driver, code, continueCount = 0):
    driver.find_element_by_xpath("//input[@alt=\"ご利用約款に同意の上、登録\"]").click() # ここで新規ウィンドウが開く
    time.sleep(1)
    driver.switch_to.window(driver.window_handles[1]) # 直前に開いた新規ウィンドウをカレントにする

    try:
        driver.find_element_by_id("gift01").send_keys(code[:4])
        driver.find_element_by_id("gift02").send_keys(code[4:8])
        driver.find_element_by_id("gift03").send_keys(code[8:12])
        driver.find_element_by_id("gift04").send_keys(code[12:])

        driver.find_element_by_id("submit-button").click()
        time.sleep(0.1)
        result = "Succeeded"
        driver.find_element_by_xpath("//input[@alt=\"登録する\"]").click()
    except selenium.common.exceptions.NoSuchElementException:
        result = "Duplicated" if "このギフトIDは、すでに下記の通り登録済です。" in driver.page_source else "Missed"
    time.sleep(0.1)
    driver.close() # カレントウィンドウ(さっき開いた新規ウィンドウ)を閉じる
    driver.switch_to.window(driver.window_handles[0]) # nanacoページのウィンドウをカレントにする
    return result if result != "Missed" or continueCount >= 5 else registGiftCode(driver, code, continueCount + 1)

# 入力内容を整理して正しくスクリプトが動作するようにする
# 必要変数が空の場合、対話形式で補完していく
def checkEnviroment(arg):
    global userName, nanacoNo, securityCode
    if not userName:
        print("購入した福利厚生倶楽部のIDを6桁と4桁で入力してください(例: 012345-6789)")
        userName = input()
    if not nanacoNo:
        print("nanaco番号16桁を入力してください(例: 0123456789012345)")
        nanacoNo = input()
    if not securityCode:
        print("カード記載の番号を7桁で入力してください(例: 0123456)")
        securityCode = input()
    urlsOrGiftCodes = []
    if len(arg) < 2:
        print("nanacoギフトコード、またはnanacoギフトコードを表示するためのURLを1行ずつ入力してください。")
        print("全て入力し終わったら、何も入力せずにEnterを押してください。")
        while True:
            code = input()
            if urlsOrGiftCodes and not bool(code):
                break
            if code.strip() != "":
                urlsOrGiftCodes.append(code.strip())
    else:
        for code in arg[1:]:
            urlsOrGiftCodes.append(code.strip())
    return urlsOrGiftCodes

if __name__ == "__main__":
    urlsOrGiftCodes = checkEnviroment(argv)
    print("Preparing...")
    driver = selenium.webdriver.Chrome()
    driver.set_page_load_timeout(30)
    print("Getting gift codes ...")
    giftCodes = []
    for code in urlsOrGiftCodes:
        # 入力内容がURLの場合、コードを取得しにいく
        if code[:8] == "https://":
            giftCodes += getGiftCodes(driver, code)
        # 入力内容がURLでない場合、それがギフトコードとして扱う
        else:
            giftCodes.append(code)
    print("Logging in nanaco website ...")
    loginNanaco(driver)

    progress = {}
    print("Registing codes ...")
    for giftCode in giftCodes:
        print(giftCode, "...", end = "\r")
        result = registGiftCode(driver, giftCode)
        if not result in progress:
            progress[result] = 0
        progress[result] += 1
        print(giftCode, "(%d/%d)" % (sum(progress.values()), len(giftCodes)), result)

    for key in progress:
        print(key + ":", progress[key])
    print("Total:", sum(progress.values()))
    driver.find_element_by_id("memberInfoInner").click()
    driver.quit()
```  
[Github](https://github.com/card1127/nanaco_transiter.git)にも掲載しています。  


# 実行方法
Windowsならコマンドプロンプト、Macならターミナルで下記コマンドを実行してください。
※環境に応じて`python`または`python3`で実行してください。

```
python3 nanaco_transiter.py "ギフトコードのURL"|"ギフトコード" ["ギフトコードのURL"|"ギフトコード" ...]
```

# 実行例
```
python3 nanaco_transiter.py "https://www.fukuri.net/main/www/gift/login.jsp?jujuno=xxxxxxxxxx&cococd=xx" da1Ddf2fffsd4589 fffsd4589da1Ddf2 "https://www.fukuri.net/main/www/gift/login.jsp?jujuno=yyyyyyyyyy&cococd=yy"
```
上記の場合、URL指定した2つのページよりnanacoギフトコードを取得し、ギフトコード指定した2つのギフトコードと合わせて登録を行います。

# 出力結果
スクリプトの進捗状況と、各ギフトコードの登録結果を表示します。
各ギフトコードの出力結果「ギフトコード 進捗度合い 登録結果」で表示されます。
表示例は次のとおりです。

```
abcdefgHIJ123456 (13/20) Succeeded
```

登録結果は以下の3種類で表示されます。

|登録結果|説明|
|:--|:--|
|Succeeded|登録に成功しました|
|Duplicated|すでに登録されています|
|Missed|5回試行しましたが、5回とも登録に失敗しました。正しいギフトコードかどうかとネットワーク環境を確認してください|

# その他
- 引数に何も入力せずに実行した場合、実行時に入力を求められます。ギフトコードが記載されてるURLか、ギフトコードを1行ずつ入力してください。
- 引数のURLは""で括る必要があります。括らない場合、文字列中の&をシェル命令の&と解釈されてしまい正しく動作しません。ギフトコードの場合は括らなくても問題ありません。
- 引数の場合も実行中に入力の場合も、ギフトコードとURLは混在して入力できます。
- ギフトコードの登録に失敗した場合、成功するまで最大5回繰り返します。全て失敗した場合、Missedと表示されます。
- 福利厚生倶楽部のIDの入力は必須です。全てギフトコードを入力した場合はテキトーな値を入力してください。
- 細かなエラー処理は実装していません。本来の使い方でご使用ください。
- ギフトコードの登録中に落ちてしまっても、再実行すれば問題なく登録できます。
- 不具合などありましたらご連絡いただけると助かります。

# 免責
- このスクリプトの実行は全て自己責任でお願いします。
- このスクリプトに悪意ある動作は含めていませんが、万一不具合によって実行者が被害を被ったとしても、作者は一切の責任を負いませんのでご注意ください。