Pythonのrequestsでログインが必要なページにアクセスする

タイトルにはログインと書きましたが要するにPythonのrequestsライブラリでセッションを扱う方法を書きます。

PythonでWebアクセスする場合、普通にURLにGETでアクセスして必要な情報が取れる場合だけでなく、事前にログインが必要なページの情報を取りたい場合があります。
requestsライブラリを使えばそれも比較的容易に実現できました。

基本的に、どのサイトでも同じような方法で対応可能ですが、例として今回はSBI証券のバックアップサイトで保有証券の一覧あたりを取って見ましょう。
バックアップサイトを使うのはHTMLがシンプルで見やすいからです。
ログイン画面のURLは https://k.sbisec.co.jp/bsite/visitor/top.do で、
ログイン後に取得したい保有証券一覧ページは、https://k.sbisec.co.jp/bsite/member/acc/holdStockList.do です。

通常、ログイン等のセッションはCookieを使って維持されます。
そのため、素直に考えれば以下の手順でログイン済みのページにアクセスすることができます。

– ログインフォームのPOST先URL(htmlのformタグでaction属性で指定されているURL)に、フォームの入力内容のデータ(ユーザーIDやパスワード)をPOSTする。
– レスポンスのCookieを取得する。
– そのCookie情報をヘッダーに付与した状態でアクセスしたかったページにアクセスする。

あとは、アクセスするたびに、Cookieを付与してGETなりPOSTなりのリクエストを送れば、ブラウザで操作するのと同じように、ログイン後の画面にアクセスできます。

ただ、非常にありがたいことに、requestsライブラリには、Sessionっていう専用のクラスがあって、これを使うと逐一レスポンスからCookieを取り出したり、ヘッダーに付与したりするすることを意識せずにセッションを維持できます。

参考: Advanced Usage – Session Objects

使ってみる前に、POST先の情報や、フォームの各要素のnameを把握しておく必要があるので、まずはログインしたいページのフォームのHTMLを見ておきます。

import requests
from bs4 import BeautifulSoup


response_0 = requests.get("https://k.sbisec.co.jp/bsite/visitor/top.do")
soup_login = BeautifulSoup(response_0.text)
print(soup_login.find("form"))
"""
<form action="https://k.sbisec.co.jp/bsite/visitor/loginUserCheck.do" method="POST" name="form1">
<b>ユーザネーム</b>:
                <div style="margin-top:5px;">
<input istyle="3" maxlength="32" name="username" size="15" style="width:115px" type="text" value=""/>
</div>
<br/>
<b>パスワード</b>:
                <div style="margin-top:5px;">
<input class="bsite_pass" istyle="3" maxlength="30" name="password" size="15" type="password" value=""/>
</div>
<br/>
<div style="margin-top:5px;">
<a href="/bsite/info/policyList.do?list=attention">お取引注意事項</a>に同意の上、ログインして下さい
                </div>
<br/>
<div align="center">
<input name="login" type="submit" value=" ログイン "/>   
                <input name="cancel" onclick="dataclear()" type="button" value="キャンセル"/>
</div>
<br/>
</form>
"""

input タグのnameを見ると、username と password で良いようですね。

Sessionクラスを使う方法

さて、POST先ですが、actionに https://k.sbisec.co.jp/bsite/visitor/loginUserCheck.do と絶対URLで入ってるので、これをそのまま使えば良いでしょう。サイトやフォームによっては相対URLなので注意してください。
ではいよいよやってみましょう。

username と passwordは自分のを使います。

username = "{ユーザーネーム}"
password = "{パスワード}"
login_url = "https://k.sbisec.co.jp/bsite/visitor/loginUserCheck.do"

# セッションのインスタンスを作成する。
session = requests.Session()
response_1 = session.post(
        url=login_url,
        data={
            "username": username,
            "password": password,
        }
    )

print(response_1.text)
# 長いので出力略
# ログイン後に表示される機能メニューのHTMLが得られている。

はい、この段階でログインができました。そして、このsessionオブジェクトを使うと、目当ての保有証券一覧ページにアクセスできます。

response_2 = session.get(
        url="https://k.sbisec.co.jp/bsite/member/acc/holdStockList.do"
    )
print(response_2.text)
# 長いのでこれも出力略

自分が持ってる株を公開するつもりもないので、出力完全に省略させていただきましたが、このページのHTMLをパースすると保有中の証券の一覧が得られます。

やっていることといえば、requests.get や requests.post の代わりに、最初にSessionオブジェクトを作って、それのgetやpostメソッドを使うというだけです。非常に手軽ですね。

おまけ: Sessionクラスを使わない場合どうするか

冒頭でCookieを付与して云々という手順を書いていますが、もちろんそのやり方でログインすることもできます。Sessionクラスを使うデメリットとか得なくて、自分でCookieの操作を実装する必要性無いと思うのですが、実は実験したのでそれ書いておきます。
次のようにすると、Session使わずにログイン後のページにアクセスできます。

# ログインフォームにデータをポストしログインする。
response_3 = requests.post(
        url=login_url,
        data={
            "username": username,
            "password": password,
        }
    )
# cookieを取り出す。
cookies = response_3.cookies

# cookieをヘッダーに付与してgetする。
response_4 = requests.get(
        url="https://k.sbisec.co.jp/bsite/member/acc/holdStockList.do",
        cookies=cookies,
    )

# 保有証券一覧ページのHTMLが得られる
print(response_4.text)

うまくいかなかった場合に検証すること

サイトによっては、ここまでの方法をそのまま適用してもうまくいかないことがあります。
だいたいその原因は何かしらのセキュリティ対策がなされているためです。
ユーザーエージェントの問題の場合もあると思いますし、サイトによっては、ユーザーIDやパスワードを入力する画面の表示時点でCookieを発行していて、それと整合性が取れない場合、不正な画面遷移としてログインさせてくれないこともあります。その場合は、Sessionのインスタンスを作って、そのインスタンスで一度ログイン画面をgetしてCookieを得ておく必要があるでしょう。

また、ログインフォームにhidden属性で非表示のinputタグを作っておき、そこにトークンのようなものを埋めておいてそれが無いとログインできないというケースもあります。
その場合は、HTMLから情報取り出して、それらのトークン情報をデータに加えてPOSTするなどの対応が必要になります。

ハードリンクとシンボリックリンク

新卒で就職したばかりの頃、UNIX/Linuxの研修で習ったような気がするけど忘れてしまっていた話を改めて調べたのでまとめておきます。
ちなみに、この記事はMacで検証しています。

概要説明

Windowsでは特定のファイルやフォルダーを別のフォルダーから開けるようにするショートカットという仕組みがあります。Macを含むUNIX系OSにも同様の仕組みがあり、それがリンクです。(正確には、ディスク上のファイルの実体と、ファイルパスを繋げる仕組みをリンクと言います。)

ここで、Windowsと違うのは、UNIX系のリンクにはハードリンクとシンボリックリンクの2種類がある点です。(実はWindowsでもハードリンクができるという話も聞きますが、あまり一般的ではないと思います。)

この後、実際にファイルを操作しながら具体的な挙動の違いを見ていきますが、ざくっと二つのリンクの違いを説明すると次のようになります。

ハードリンクは、一つの実体を持つファイルを表現するパスを複数作る方法です。
マニュアルにも、ハードリンクでは、リンクと元のファイルの区別はつかないと書かれています。
man ln から抜粋。
> A hard link to a file is indistinguishable from the original directory entry

それに対してシンボリックリンクは、リンク先のファイルへの参照を含みます。Windowsのショートカットに近いのはこちらです。

わかりにくいですね。

例えば、 file01.txt というファイルがあったとします。これは当然、ディスク上にあるデータが存在し、それに対して、file01.txt という名前でアクセスできることを意味します。

これに対して、file01.txt を file02.txtに「ハードリンク」したとします。すると、そのディスク上の同じデータが file02.txt というファイル名でもアクセスできるようになります。

一方で、file01.txt を file03.txt に「シンボリックリンク」したとします。すると、file03.txt は、file01.txt というパスへのリンクになります。その結果、file03.txtにアクセスしようとすると、file01.txtにアクセスすることになり、結局同じ実体のファイルにアクセスできることになります。

2種類のリンクの作成

まだわかりにくいのでやってみましょう。実際にファイルとリンクを作ってみます。
まず、サンプルのデータから。

# 検証用ディレクトリを作って移動
$ mkdir linktest
$ cd linktest
# サンプルデータ作成
$ echo "サンプルファイル1行目" > file01.txt
# 作成したファイルの情報を確認
$ ls -li
total 8
8795955 -rw-r--r--  1 {owner} {group}  34  6 12 17:51 file01.txt

ls コマンドで作ったデータを見るときに、-iオプションをつけてiノード番号も表示させました(先頭の8795955がそれ)。iノードというのは、ファイルの属性が記録されているデータのことで、iノード番号というのはそのデータについている番号のことです。このデータは実体のファイルと対応して存在しているので、実質的にディスク上のファイルの実体と対応している番号だと考えて大丈夫です。iノード番号が同じなら同じファイルを表しています。

さて、ここからリンクを作っていきましょう。リンクは ln コマンドで作ります。文法は次のとおりです。

# ハードリンクを作成する
$ ln リンク先 リンク名
# シンボリックリンクを作成する
$ ln -s リンク先 リンク名

やってみます。

# ハード/シンボリックでそれぞれリンクを作成する
$ ln file01.txt file02_hard.txt
$ ln -s file01.txt file03_symbolic.txt
# 確認
$ ls -li
total 16
8795955 -rw-r--r--  2 {owner} {group}  34  6 12 17:51 file01.txt
8795955 -rw-r--r--  2 {owner} {group}  34  6 12 17:51 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt

はい、できました。

ls の結果を見ていきましょう。まずハードリンクの方(file02_hard.txt)です。
着目するべきは、最初のiノード番号で、元のファイルと全く同じになっています。これはfile02_hard.txtがfile01.txtと全く同じ実体ファイルにリンクしていること意味しており、同じデータに対して2個ファイル名があるような状態にになっています。ファイルサイズも同じ34バイトですね。
権限(-rw-r–r–) の後ろに 2 という数字がありますが、実はこれ、そのファイルへのリンク数です。元々1だったのがハードリンクを作成したことで2になっています。そして、ここからわかるのですが、シンボリックリンクの方はノーカウントです。(カウントされるなら3になるはず。) ちなみに、この数字がリンク数だっていう情報は、lsのマニュアル(man ls)のThe Long Format ってセクションに書かれています。

次は、シンボリックリンクの方(file03_symbolic.txt)を見ていきましょう。 -> で、元ファイルへのパスが書かれており、いかにもリンクって感じの表示になっていますね。
権限の最初にlがついて、lrwxr-xr-x となっていますが、このlはシンボリックリンクであることを示しています。そして、iノード番号は元のファイルと違うものが振られています。また、ファイルサイズも異なっていますね。file01.txtへの参照だけなのでサイズが小さいです。

当然ですが、全部同じデータを見てるので、中身を表示したら一致します。

$ cat file01.txt
サンプルファイル1行目
$ cat file02_hard.txt
サンプルファイル1行目
$ cat file03_symbolic.txt
サンプルファイル1行目

リンク元ファイルに変更が発生した場合の挙動

ここから、元ファイルに変更が発生した時のハードリンクとシンボリックリンクの挙動の違いを見ていきます。

まず、元ファイルに修正が入った場合、これはどちらのリンク方法でもそれぞれ反映されます。同じファイル見てるだけだからですね。1行追記してみてみましょう。

$ echo "サンプルファイル2行目" >> file01.txt
$ ls -li
total 16
8795955 -rw-r--r--  2 {owner} {group}  64  6 12 18:23 file01.txt
8795955 -rw-r--r--  2 {owner} {group}  64  6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt

# 念のため中身も見る
$ cat file01.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file03_symbolic.txt
サンプルファイル1行目
サンプルファイル2行目

はい、 echo で追記したのはfile01.txtだけですが、3ファイルとも2行目が増えましたね。ファイルサイズが増えたのはfile01.txt/ file02_hard.txt だけで、file03_symbolic.txtはそのままなので想定通りです。ここまでは、ハードリンク、シンボリックリンク共に違いはありません。

ここからが差が発生することです。もし、このfile01.txtがリネームされたり削除されたりして、そのパスに存在しなくなったらどうなるでしょうか。
試しに消してみます。

$ rm file01.txt
# 1ファイル消えて2ファイル残っている。
$ ls -li
total 8
8795955 -rw-r--r--  1 {owner} {group}  64  6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt
# ハードリンクの方は元ファイルがなくなっていても開ける
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
# シンボリックリンクの方はリンク先ファイルがなくなると開けない
$ cat file03_symbolic.txt
cat: file03_symbolic.txt: No such file or directory

はい、ここで大きな差が生じました。元ファイルが消えてしまったのですが、ハードリンクのファイルは、元ファイルのパスと関係なくディスク上のファイルの実体にリンクされていたので元ファイルのパスが消えてしまっても問題なく開くことができます。(消したと思ったファイルが残ってる、というのがデメリットになるケースもありそうですが。)
ちなみに、ls -li の結果のリンク数は1個になっていますね。

一方で、シンボリックリンクの方は、元ファイルへの参照しか情報を持っていなかったので、元ファイルのパスがなくなってしまうとデータにアクセスができなくなってしまっています。

さて、次はその逆の操作です。元々存在してたファイルと同じパスで、再度ファイルが作成されたらどうなるでしょうか。この時の挙動もそれぞれ異なります。やってみましょう。

# あたらめてリンク先ファイル作成
$ echo "新規作成したファイル1行目" > file01.txt
$ ls -li
total 16
8798109 -rw-r--r--  1 {owner} {group}  38  6 12 18:37 file01.txt
8795955 -rw-r--r--  1 {owner} {group}  64  6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt

新しい file01.txt は 元のと異なるiノード番号で作成されましたね。これは要するに、ファイル名は同じだけど実データとしては元々存在してたfile01.txtとは異なるファイルであることを意味します。そいて、よく見ていただきたいのは、file01.txt と file02_hard.txt のファイルサイズが違うことです。もはや同じファイルは指し示しておらず、file02_hard.txtは元々のファイルにリンクされていますね。それぞれ開くと明らかです。

$ cat file01.txt
新規作成したファイル1行目
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file03_symbolic.txt
新規作成したファイル1行目

はい、ハードリンクしていたfile02_hard.txtはもう完全にfile01.txtとは別ファイルになってしまいました。
一方で、リンク先がなくなって開けなくなっていたシンボリックリンクの方(file03_symbolic.txt)は、同じパスのファイルができたら自動的にそこにリンクされてfile01.txtと同じデータが参照できるようになりました。

ハードリンク、シンボリックリンクのどちらを選ぶかは、これらの挙動を踏まえて決めるのが良いと思います。

その他の違い

以上で元ファイルの編集に関するリンクごとの挙動の違いを書いて来ましたが、他にも少し違いがありますのでまとめておきます。

まず、ファイルではなくディレクトリに対しては、ハードリンクは作成できず、シンボリックリンクのみ作成できます。

# 実験用ディレクトリ作成
$ mkdir subfolder01

# ハードリンクは作れない
$ ln subfolder01 subfolder02
ln: subfolder01: Is a directory

# シンボリックリンクは作れる
$ ln -s subfolder01 subfolder02
$ ls -li
8799037 drwxr-xr-x  2 {owner} {group}  64  6 12 18:46 subfolder01
8799038 lrwxr-xr-x  1 {owner} {group}  11  6 12 18:46 subfolder02 -> subfolder01

また、別の違いとして、ハードリンクはパーティションなどファイルシステムを跨いで作ることはできないというものもあります。ハードリンクを作れるのは同じパーティション内のみだけです。理由はiノード番号の管理が違うからだそうです。
一方でシンボリックリンクはどこでも作れます。

あとは、ハードリンク(元のファイルパスも含む)を全部消すと、ファイル自体が削除されてしまいますが、シンボリックリンクは消しても元のファイルに影響がないとか細々した違いがあります。

ハードリンクしているファイルを探す方法

ls コマンドでそのファイルへのリンク数が表示されますが、 どこからリンクされているか探したくなることはあると思います。そのファイルを確実に消したい時などは一通り洗い出す必要ありますし。

良い探し方を調べていたのですが、今のところ、find コマンドで iノード番号を調べて検索する以外になさそうです。-inum 引数で指定できます。

今やっているサンプルは同じディレクトリ配下なので楽勝ですが、遠いパスに作っていたらかなり広範囲をfindで探さないといけないですね。

# 実験のためハードリンクを作成する 
$ ln file01.txt file04.txt
# iノード番号を調べる
$ ls -i file01.txt
8798109 file01.txt
# find コマンドで該当のiノード番号を持つファイルを探す
$ find . -inum 8798109
./file04.txt
./file01.txt

余談: 何が発端でこれを調べていたのか

なんで今になってこんなのを調べているのかというと、実は個人的に開発してるプロジェクトがあって、そのコード管理に使いたかったからです。

ほとんどのソースコードはプロジェクトのディレクトリ配下に格納されていてそこでgit管理されているのですが、ごく一部/etc/の配下とか、ホームディレクトリのドット付き隠しフォルダの下とかで作成する必要上がります。

これらをどうやって管理しようかなと思ったときに、プロジェクトのディレクトリ内に実体ファイルを作って、それらの本来の配置場所にリンクを貼れば単一のリポジトリで管理できるじゃないかと思いつきました。で、その時のリンク方法が2種類あったのでどっちがいいのかというのが発端になります。

この用途だと、git管理してるファイルと、稼働してるファイルを確実に同期させたいのでシンボリックリンクの方が良さそうですね。

まぁ、その他、brewで入れたソフトウェアとかMeCabの辞書のファイルたちとか自分の環境内でシンボリックリンクで稼働しているファイルはいろいろ存在し、これらについても理解を深められたのは良かったです。

Lambda の関数URLで送信されたデータを扱う

前回に引き続き、Lambdaの関数URLの話です。URLでLambdaを起動できるのは便利ですが、せっかくならアクセスするときに何かしらの情報を渡して処理を変えたいということは多くあると思います。その場合に、あり得るパターンの数だけLambda関数と個別のURLを用意しておくというのは現実的ではありません。
普通は、世の中のたいていのAPIがやっているように、クエリパラメーターや、POSTされたデータに応じて挙動を変える作りにします。

受け取った値に応じて挙動を変えるっていうのは、ただのPythonのコーディングの話なので、今回の記事では、どうやってクエリパラメーターとかPOSTされたデータを受け取るかって部分を扱います。これまでAPI GatewayでAPI作ってたような人にとっては常識的なことしか書いてないと思いますがご了承ください。(僕はWebエンジニアではなく、これまでLambdaは手動かスケジュール実行で使って来たのでこの辺の挙動に詳しくないのです。)

さて、前置きが長くなって来ましたが、実はタネは非常に簡単で、Lamda関数の定義にデフォルトで入っている引数「event」、これが今回の主役です。

def lambda_handler(event, context):  # この第一引数eventに欲しいデータが渡される
    # 処理の中身

関数URLからLambdaが呼び出されたとき、lambda_handlerの第一引数には、以下のドキュメントのリクエストペイロードと呼ばれているデータが渡され、関数中ではeventという変数名で扱えます。
参考: Lambda 関数 URL の呼び出し – AWS Lambda の リクエストペイロードの形式 の部分

以下のような関数を作って実際に動かしてみるのが一番わかりやすいと思います。
テキストエリア2個と送信ボタンを持つフォームをGET/POSTでそれぞれ作成し、event変数の中身を表示します。

import pprint


def lambda_handler(event, context):
    html = """
<!DOCTYPE html>
<h2>GETのフォーム</h2>
<form action="/" method="get">
    <input type="text" name="text1"><br>
    <input type="text" name="text2"><br>
    <input type="submit">
</form>
<h2>POSTのフォーム</h2>
<form action="/" method="post">
    <input type="text" name="text3"><br>
    <input type="text" name="text4"><br>
    <input type="submit">
</form>
    """

    html +="<h2>eventの内容</h2>\n"
    html += "<pre>" + pprint.pformat(event, compact=True) + "</pre>"

    return {
        "headers": {
            "Content-Type": "text/html;charset=utf-8",
        },
        'statusCode': 200,
        "body": html,
    }

上記のコードで関数を作成し、関数URLからアクセスすると、フォーム二つとpprintで成形されたeventの辞書データが表示されます。また、それぞのフォームに値を入れて送信すると、eventのどこに結果が入るかが確認できます。
(キャプチャとか貼るとわかりやすいと思うのですが、idっぽいものが大量にあるので省略させていただきます。上のコードを試してただくのが一番良いです。)

まず、メソッド(GET/POSTなど)の区別ですが、次の部分にあります。
Pythonで言えば、 event[“requestContext”][“http”][“method”] で取得できますね。

{
    # 略
    'requestContext': {
        # 略
        'http': {
            # 略
            'method': 'GET',
        }
    }
}

それでは、 GET/ POST 順番にフォームを実行して、どのように値が取れるか見て行きましょう。
フォームには以下の値を入れて送信します。
1つ目のテキスト: あ&い=う%え,お
2つ目のテキスト: abc?123

フォームからGETで送信する場合、URLの末尾に入力内容が添付され、次のURLにアクセスされます。特殊文字はパーセントエンコーディングされていますね。

https://{url-id}.lambda-url.{region}.on.aws/?text1=あ%26い%3Dう%25え%2Cお&text2=abc%3F123

eventの中ではevent[“rawQueryString”] と event[“queryStringParameters”]の部分に現れます。
クエリ文字列がないときは、rawQueryStringは空文字””ですが、queryStringParametersの方はキー自体が存在しないので、コードで使うときは注意してください。

{
    # 略
    'queryStringParameters': {'text1': 'あ&い=う%え,お', 'text2': 'abc?123'},
    # 略
    'rawQueryString': 'text1=%E3%81%82%26%E3%81%84%3D%E3%81%86%25%E3%81%88%2C%E3%81%8A&text2=abc%3F123',
    # 略
}

上記の内容で分かる通り、rawQueryStringの方はURIエンコーディングされていますが、queryStringParametersの方は使いやすいように辞書型にパースしてURIエンコードも元に戻してくれています。こちらを使って行きましょう。
フォームに入れた値がURLに載ってしまうというのは大きなデメリットですが、ぶっちゃけると単純にテキスト等を送るフォームならPOSTよりGETの方が渡した値使いやすいな、と感じています。(Web系開発素人の発想ですが。)

続いて、POSTの場合です。個人的にはこの種のフォームは普通はPOSTで使うものだと思っています。
POSTの場合は、GETと違って少しわかりにくく、eventを表示しても送ったデータがそのままでは見つかりません。ではどこにあるのかというと、実は event[“body”]に、base64エンコードされて入ってます。これはフォームからPOSTした場合の挙動です。curl等でテキストのままポストしてあげればそのまま表示されます。
base64の判定は、event[“isBase64Encoded”] で行います。

{
    # 略
    'body': 'dGV4dDM9JUUzJTgxJTgyJTI2JUUzJTgxJTg0JTNEJUUzJTgxJTg2JTI1JUUzJTgxJTg4JTJDJUUzJTgxJThBJnRleHQ0PWFiYyUzRjEyMw==',
    # 略
    'isBase64Encoded': True,
    # 略
}

base64なので自分でこれをデコードする必要があります。
参考: PythonでBase64エンコードとデコード

該当部分のコードだけ書くとこんな感じです。

import base64

base64.b64decode(event["body"]).decode()
# text3=%E3%81%82%26%E3%81%84%3D%E3%81%86%25%E3%81%88%2C%E3%81%8A&text4=abc%3F123

パーセントエンコードされていますね。これを以下の手順で処理する必要があります。
&で区切って、フォームの各要素ごとの値に分割する。
=で区切ってキーと値のペアに変える。
パーセントエンコードをデコードする。

自分でやるのは面倒なので、ライブラリ使いましょう。
参考: PythonでURL文字列を要素に分解する

次のようになります。

import base64
from urllib.parse import parse_qs


parse_qs(base64.b64decode(event["body"]).decode())
# {'text3': ['あ&い=う%え,お'], 'text4': ['abc?123']}

キー対値 の辞書ではなく、 キー対値の配列 の辞書が結果として得られるので注意が必要です。特殊文字たちも元の形に戻っていますね。

さて、以上で関数URLにクエリストリングに付加されたデータや、POSTされて来たデータをLambdaの関数で取り出せるようになりました。

あとはこれを受け取ってそれに応じた処理をする関数を作るだけで、柔軟な処理を行えるようになると思います。

AWS Lambda Function URLs を試してみた

発表されてから少し時間が経ってしまったので、今更感が出ていますが AWS Lambdaの新機能である Function URLs (関数URL) を試してみました。
参考: AWS Lambda Function URLs の提供開始: 単一機能のマイクロサービス向けの組み込み HTTPS エンドポイント

これ何かと言うと、AWSのLambda に API Gateway を使わずに、Lambda内部の機能だけでURLを発行して、そのURLにアクセスするだけで関数を実行できると言うものです。

ドキュメントはこちらかな。
参考: Lambda 関数 URL

ちょっとお試しで動かしてみましょう。関数URLが有効な新しい関数を作る手順は以下の通りです。

1. Lambdaのコンソールにアクセス。
2. 関数の作成をクリック。
3. 関数名/ ランタイム(NodeやPythonのバージョン)/ アーキテクチャ などを入力。
4. アクセス権限でロールの設定。
5. ▼詳細設定のメニューを開く。ここに「関数URLを有効化」が新登場しています。
6. 「関数URLを有効化」にチェックを入れる。
7. 認証タイプ を選択。お試しなので、一旦NONEにしてパブリックにしました。
8. 関数の作成をクリック。

これで関数が出来上がります。デフォルトでは以下のコードが用意されています。

import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

ここで、コードソースのウィンドウの上の、関数の概要ウィンドウの右側に、関数URLというのが作られ、以下の形式のURLが用意されているのがわかります。
この時点でURL用意されているんですね。

https://{url-id}.lambda-url.{region}.on.aws/

ドキュメントによると、url-id 部分からわかる人はその人のアカウントIDが取得できるそうです。そのため、僕のサンプルで作った関数URLがお見せできませんが、たったこれだけの手順で試せるので是非やってみてください。

このURLを開くと、”Hello from Lambda” の文字が表示されます。curlでも良いです。

$ curl https://{url-id}.lambda-url.ap-northeast-1.on.aws/
"Hello from Lambda!"

折角ブラウザで開けるので、HTMLを表示してみましょう。
ここで注意しないといけないのは、単純に”body”にHTMLの文字列を書くだけだと、JSONとして扱われて、ブラウザがHTMLソースをレンダリングせずにそのまま表示してしまいます。

それを防ぐためには、Content-Type を “text/html” に指定してあげる必要があります。
指定は、ドキュメントのこちらのページの、「レスポンスペイロードの形式」というセクションに沿って行います。
参考: Lambda 関数 URL の呼び出し – AWS Lambda

{
   "statusCode": 201,
    "headers": {
        "Content-Type": "application/json",
        "My-Custom-Header": "Custom Value"
    },
    "body": "{ \"message\": \"Hello, world!\" }",
    "cookies": [
        "Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT",
        "Cookie_2=Value2; Max-Age=78000"
    ],
    "isBase64Encoded": false
}

Content-Type の デフォルトは “application/json” ですね。

このブログへのリンクでも表示してみましょうか。
コードを以下の内容に書き換えて、 Deploy をクリックします。

def lambda_handler(event, context):
    html = """<!DOCTYPE html>
<a href="https://analytics-note.xyz/" target="brank_">分析ノート</a>"""

    return {
        "headers": {
            "Content-Type": "text/html;charset=utf-8",
        },
        "statusCode": 200,
        "body": html
    }

これで、関数URLにアクセスすると、このブログへのリンクだけの簡素なページが表示されます。

Content-Type を “text/html” ではなく、 “text/html;charset=utf-8” にしているのは、こうしないと日本語が文字化けするからです。

ここまで、新規の関数について関数URLを設定する方法を書いて来ましたが、既存の関数についても「設定」のところに関数URLの項目が追加されており、非常に簡単に浸かるようになっています。

こうしてURLを簡単に使えると、たとえばEC2の起動/停止などの処理も今までboto3のバッチでやっていましたが、ブラウザでアクセスするだけで起動できるようになります。
jupyter notebook のサーバーを操作するも一層手軽になりそうです。
(端末のコンソール起動する一手間がなくなるだけですが、地味に面倒だったので)

ただ、気をつけないといけないのはグローバルにアクセスできるので、誰でも実行できるという点ですね。もしクリティカルな処理で使う場合は、何かしらの制限をかけた方が良いでしょう。