Helium - Cool Python!

Event

はんなりPython #43

Presented

2021/09/17 nikkie

このLTでは

ブラウザ操作自動化ライブラリHeliumを、 書き方を工夫 🆒して使ったことについて共有します

お前、誰よ

  • Python大好き にっきー @ftnext / @ftnext

  • Python歴もうじき4年。株式会社ユーザベースのデータサイエンティスト(NLPer)

  • アニメも大好き。最近好きな挨拶「ういっす✌️」🐙

お前、誰よ:PyCon JP 2021 座長🇨🇭

PyCon JP 2021( 10/15(金), 16(土) )、チケット販売中!🎫🙏

LT:Helium - Cool Python!

まずは導入

先日のLT⚡️

Helium

こんなに簡単です🍰


from helium import *
start_chrome("google.com")  # 1. Chrome立ち上げ、「helium」についてGoogle検索
write("helium selenium github")
press(ENTER)
click("mherrmann/helium")  # 検索結果の中からクリック
go_to("github.com/login")  # 2. 別の例:GitHubにログイン
write("username", into="Username")
write("password", into="Password")
click("Sign in")
kill_browser()  # Chrome終了

https://github.com/mherrmann/selenium-python-helium/blob/master/docs/cheatsheet.md

PyCon JP スタッフでよくやる作業を自動化

  • 先日のLTでは、指定したconnpassイベントのコピー(デモしました)

  • 今日デモします :connpassから参加者情報CSVのダウンロード

Heliumを使う中で感じた😖

  1. 繰り返し書くコードがある

  2. スクリプトで例外が送出されたときにブラウザのウィンドウが残る

それぞれ、詳しい説明 👉 解決策 の順で話します

Heliumを使う中で感じた2点は解決済み🙌

  • Pythonの知識 を使って解決。共有していきます

  • 解決方法=カッコいいPythonの書き方✨

  • おことわり:ブラウザはFirefoxです(Chromeについても途中言及します)

1️⃣ 繰り返し書くコードがある

Heliumを使う中で感じた😖 1点目

例:HeliumのリポジトリをGoogle検索して移動


start_firefox()
go_to("google.com")
write("helium selenium github")
press(ENTER)
click("Selenium-python but lighter: Helium - GitHub")

例:connpassにログイン


start_firefox()
go_to("connpass.com/login")
write(os.getenv("CONNPASS_USERNAME"), into="ユーザー名")
write(os.getenv("CONNPASS_PASSWORD"), into="パスワード")
click("ログインする")

wait_until(Text("あなたのイベント").exists)

繰り返し書く start_firefox


start_firefox()
go_to("connpass.com/login")
write(os.getenv("CONNPASS_USERNAME"), into="ユーザー名")
write(os.getenv("CONNPASS_PASSWORD"), into="パスワード")
click("ログインする")

wait_until(Text("あなたのイベント").exists)

start_firefox を繰り返し書くのをなくしたい

  • start_firefox でFirefoxを立ち上げている

  • 👉 デコレータ

デコレータとは(ざっくり)

  • 関数やクラスの定義の直前に @ で付けるあれ


@decorator
def function():
    ...

デコレータとは

  • 関数を引数に取り、関数を返す 関数用語集

  • 渡された関数の前後にコードを追加できる(ref:『Effective Python 第2版』項目26)

デコレータの実装


def decorator(function):  # 引数に関数
    def wrapper(*args, **kwargs):
        # functionの前に実行する処理
        function(*args, **kwargs)
        # functionの後に実行する処理

    return wrapper  # コードを追加した関数を返す

デコレータの例:関数の開始と終了を示す


def show_start_end(func):
    @functools.wraps(func)  # funcの__name__や__doc__を残すために付ける
    def wrapper(*args, **kwargs):
        print(func.__name__, "Start")
        returned = func(*args, **kwargs)
        print(func.__name__, "End")
        return returned
    return wrapper

関数の開始と終了を示すデコレータを使う


@show_start_end
def calculate_bmi(height_m, weight_kg):
    return weight_kg / height_m / height_m

>>> calculate_bmi(1.58, 46)
calculate_bmi Start
calculate_bmi End
18.426534209261334

✅ 関数の実行前に start_firefox を呼び出すコード


def using_firefox(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_firefox()
        func(*args, **kwargs)

    return wrapper

@using_firefox でデコレートすれば、start_firefox は不要


@using_firefox
def search_helium_repository():
    """HeliumのGitHubリポジトリをGoogle検索して移動する"""
    go_to("google.com")
    write("helium selenium github")
    press(ENTER)
    click("Selenium-python but lighter: Helium - GitHub")

@using_firefox でデコレートすれば、start_firefox は不要(その2)


@using_firefox
def login_connpass():
    """connpassにログインする(ユーザー名とパスワードは環境変数を想定)"""
    go_to("connpass.com/login")
    write(os.getenv("CONNPASS_USERNAME"), into="ユーザー名")
    write(os.getenv("CONNPASS_PASSWORD"), into="パスワード")
    click("ログインする")

    wait_until(Text("あなたのイベント").exists)

デコレータの効能

  • 繰り返しがなくなった!🙌

  • FirefoxだけでなくChromeでも動かしたい場合、 デコレータを書き換えるだけ で済む!!

Chromeで動きます


@using_chrome  # start_chromeを呼び出すデコレータ
def login_connpass():
    go_to("connpass.com/login")
    write(os.getenv("CONNPASS_USERNAME"), into="ユーザー名")
    write(os.getenv("CONNPASS_PASSWORD"), into="パスワード")
    click("ログインする")

    wait_until(Text("あなたのイベント").exists)

閑話休題:販売中はチケット🎫だけでなく

https://pyconjp.connpass.com/event/221241/

2️⃣ スクリプトで例外が送出されたときにブラウザのウィンドウが残る

Heliumを使う中で感じた😖 2点目

ブラウザの終了は kill_browser


start_firefox()
# 退屈なブラウザ操作を代わりにやってくれる処理
kill_browser()

スクリプトの途中でエラーが送出されると、 kill_browser が呼び出されない


start_firefox()
# 退屈なブラウザ操作を代わりにやってくれる処理の途中で
raise Exception
kill_browser()  # 呼び出されない

デモします

残ってしまったブラウザウィンドウ

../_images/202109_remaining_window_in_error.png

例外が送出されても、ブラウザのウィンドウは閉じたい

  • 例外が送出された場合も、 kill_browser を呼び出したい

  • 👉 コンテキストマネージャ

コンテキストマネージャとは

  • with 文で使えるオブジェクト ( 用語集


with コンテキストマネージャ:
    # 処理
    ...

with

with 文はコードのかたまりの前後でコードの初期化と終了処理を実行できるようにします。

https://docs.python.org/ja/3/reference/compound_stmts.html の冒頭より

コンテキストマネージャとは(詳しく)

  • コードの初期化:オブジェクトの __enter__ メソッド

  • 終了処理:オブジェクトの __exit__ メソッド

コンテキストマネージャを使ったコード


with EXPR as VAR:
    BLOCK

https://www.python.org/dev/peps/pep-0343/#specification-the-with-statement

コンテキストマネージャのイメージ

  1. EXPR を評価(コンテキストマネージャを返す)

  2. コンテキストマネージャの __enter__ を呼ぶ(=初期化処理)

  3. BLOCK を実行

  4. コンテキストマネージャの __exit__ を呼ぶ(=終了処理)

コンテキストマネージャの実装

  • クラス

  • 関数

クラスで実装(イメージ)


class MyContextManager:
    def __enter__(self):
        ...

    def __exit__(self, exc_type, exc_value, traceback):
        ...

https://docs.python.org/ja/3/reference/datamodel.html#context-managers

関数で実装もできる!


import contextlib

@contextlib.contextmanager
def my_context_manager():
    # __enter__ に相当する初期化処理

    yield  # (yieldで値を返すと as で受け取れる)

    # __exit__ に相当する終了処理

https://docs.python.org/ja/3/library/contextlib.html#contextlib.contextmanager

✅ 例外が送出された場合も、 kill_browser を呼び出すコード


@contextmanager
def using_firefox():
    start_firefox()
    try:
        yield
    finally:
        kill_browser()

例外が送出された場合も、ブラウザウィンドウは閉じる🙌


with using_firefox():
    # 退屈なブラウザ操作を代わりにやってくれる処理
    raise Exception

まとめ🌯:Helium - Cool Python!

PyCon JP 2021 チケットお願いします🐦🍕🙏

https://pyconjp.connpass.com/event/221241/

🍕は 9/20(月・祝)まで (まだ知らない方に教えてあげてください)

Heliumを使う中で感じた2点😖をカッコいいPythonの書き方で解決🙌

  1. 繰り返し書くコードがある 👉 デコレータ で繰り返し start_firefox を書かない

  2. スクリプトで例外が送出されたときにブラウザのウィンドウが残る 👉 コンテキストマネージャ でどんなときも kill_browser

ご清聴ありがとうございました

紹介したデコレータやコンテキストマネージャはヘルパーとしてPyPIで公開予定です

References & Appendixが続きます(よろしければどうぞ!)

References

  • 説明できなかった wraps

  • コンテキストマネージャ

  • デコレータの最終形

説明できなかった wraps

コンテキストマネージャ

デコレータの最終形

Appendix:LTにいただいた質問

  1. 定期実行

  2. AWS Lambdaで実行

ブラウザ自動化処理の定期実行はできるか?

  • heliumの中にはない(ブラウザ操作自動化だけ)

  • 回答1: スクリプトの自動実行として crontab で設定

  • 回答2: 定期実行のライブラリ(次へ)

ブラウザ自動化処理の定期実行はできるか?(承前)

  • 回答2: 定期実行のライブラリを使う

    • 標準ライブラリのイベントスケジューラ sched

    • 以前はんなりPythonで知った schedule (有力候補)


import schedule
schedule.every().day.at("10:30").do(job)

AWS Lambdaで実行できるか

Appendix:loginもデコレータにする

  • PyCon JP スタッフのconnpass操作自動化をいくつも書いて

  • connpassにログインするコードを繰り返す

connpassにログインするコード


go_to("connpass.com/login")
write(os.getenv("CONNPASS_USERNAME"), into="ユーザー名")
write(os.getenv("CONNPASS_PASSWORD"), into="パスワード")
click("ログインする")

wait_until(Text("あなたのイベント").exists)

ログインするコードをデコレータに


def logged_in(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        go_to("connpass.com/login")
        write(os.getenv("CONNPASS_USERNAME"), into="ユーザー名")
        write(os.getenv("CONNPASS_PASSWORD"), into="パスワード")
        click("ログインする")

        wait_until(Text("あなたのイベント").exists)

        func(*args, **kwargs)

    return wrapper

📣 2つのデコレータは以下の順


@using_firefox
@logged_in
def automate():
    # connpassにログインが必要な退屈なブラウザ操作を代わりにやってくれる処理

2つのデコレータの実行順は 外側から

  1. logged_inautomate の実行前後にコードを追加

  2. logged_in が返した関数の実行前後に using_firefox でコードを追加

これにより、 start_firefox一番最初に呼ばれる

EOF