まるっとワーク

データ分析・電子工作など気になることを残していきます

できるだけPythonだけでWEBアプリを作る③(Python Streamlit)

できるだけPythonだけで・・・と進めてきましたが、画期的なフレームワークの存在最近まで知らず・・・
「できるだけPythonだけでWEBアプリを作る第3弾」としてPython ライブラリのStreamlitとStreamlit Cloudを使用した方法をまとめていきます。
この方法は他の方も多くまとめているので、備忘録として簡単なフローだけを示しています。

第1弾, 第2弾記事は以下の通り。
dango-study.hatenablog.jp
dango-study.hatenablog.jp

目次


Streamlitでできること

  • PythonだけでWebアプリが作れる
  • フロントエンド(UI)の開発スキルが無くても簡単にUIを作ることができる
  • 作成したアプリをStreamlit Cloudに(無料)デプロイできる

デプロイ方法は、Streamlit Cloud意外に色々ありますが、Streamlit Cloudではセキュリティーが担保できないので、機密データを処理したい時などは、ローカルサーバーやセキュリティ保護されたサーバーでの運用が望ましいです。

Streamlitを用いたWEBアプリ作成

開発環境

[ローカル]
Microsoft Windows 10 Pro
Python 3.9.0

ライブラリのインストール

以下を実行して必要なライブラリをインストールします。

$ pip install streamlit

アプリ構成

基本的な構成は、「処理等を記載した.pyファイル」「必要ライブラリとバージョン情報を記載したrequirements.txt」の2つの構成になります。

@@@.py
requirements.txt

コード作成

任意の名前の.pyファイルには、最低限streamlitのモジュールインポート記載をしておけば、実行確認ができます。

$ import streamlit as st 

ローカルで実行/確認

コンソールで以下コードを実行して、
エラーが出なければ「http://localhost:8555」にアクセスして実行結果を確認できます。

$ streamlit run @@@.py

→.pyファイルの記載がstreamlitのモジュールインポート記載のみであれば、白紙のページが表示されるだけです。

コードの書き方などは、ありがたいことに・・先人の皆様のページで詳しく説明されているので割愛します。

  • 参考コード(私)

https://github.com/Mya-mori/streamlit_csv/blob/main/test.py

  • 参考ページ①

Streamlit入門+応用 ~ データ分析Webアプリを爆速で開発する - Qiita

  • 参考ページ②

PythonでDX「Streamlit」簡単ダッシュボード(前編) | コードファミリー

  • 参考ページ③

Streamlitで手軽にWebアプリ開発 - アルファテックブログ

アプリをStreamlit Cloudにデプロイ

Streamlit Cloudにサインイン

https://share.streamlit.io/signup
Googleアカウント、Githubアカウント、メールアドレスのいずれかを利用してアカウントを作成します。

Githubアカウント連携

Settingsをクリックし、Connect Github accountをクリックして、Githubと連携する。
(Githubアカウントがない場合は、作成する)

GithubリポジトリにWebアプリのソースコードをアップロード

作成したソースコードGithubにアップロードします
アップロードするファイルは、最低限以下構成のファイル

@@@.py
requirements.txt

requirements.txtは以下コードを実行することで、出力することができる。
必要最低限の情報でないとエラーが出る場合がある為、余計なライブラリ情報は削除する。(ライブラリが多すぎることが原因のエラーが出ました)

$pip freeze > requirements.txt

アプリのデプロイ

Streamlit CloudにStreamlitで作成したWebアプリをデプロイします。
Streamlitにログインして、"New app"と記載されたボタンを押す。

以下の通り設定をして、デプロイボタンを押すことで、デプロイされる。

何か問題があれば、エラーが吐き出されます。

動作確認

デプロイされるアプリURLにアクセスすると、デプロイしたアプリの動作確認ができます。
簡単にデプロイして利用できるのがStereamlit Cloudを用いた利点ですね。
参考までに私が作成したアプリは以下↓

https://webapplicationsample1-rwb1ydqchif.streamlit.app/
システム同定シミュレーターアプリ】
csvデータを入力して、線形システムのシステム同定シミュレーションができます。
Githubアイコンを押すとコードも参照頂けます。

まとめ

今回は、前回とは違う方法でWEBアプリを作ってみました。
正直、セキュリティーとかなんやら何も気にしなくてよいのであれば、この方法が一番簡単だなと思いました。今までの努力は・・笑
次は、こういったフレームワーククラウドを組み合わせてかつ、セキュリティも考慮したアプリを作成していこうと思います。

Amazon API Gatewayを理解する

Amazon API Gatewayは、AWS Lambdaと組み合わせて使用することが一般的で、サーバーレスアーキテクチャを構築するための基盤として利用されています。WEBアプリを色々と作ろうとしている段階で、このAmazon API Gatewayを避けて通ることはないと思うので、この機能を理解して、使う時に引っかかるであろうポイントをまとめようと思います。

目次

Application Programming Interface(API)とは

分かっているようで分かっていない気がするので改めて、確認します。
APIは、アプリケーションやサービスとやり取りをするための標準的なインターフェースのことです。プログラムが他のプログラムやサービスと情報を共有するために使用され、APIを使用することで、異なるプログラムやサービス間でデータを共有することができ、相互運用性や拡張性を向上させることができます。

APIの種類

APIは、異なる種類のアプリケーションやサービスで使用されます。Web APIREST APISOAP API等の多様な種類のAPIがあります。APIは、標準化された規格に従って設計されることが多く、APIの利用者がアクセスしやすいように、ドキュメント化され、公開されることが一般的です。

Web API

APIというとWeb APIを指すことが大部分で、Webブラウザ経由でアクセスするAPIのことを指します。
HTTPプロトコルを使用してリクエストを送信し、JSONXML、またはプレーンテキストの形式で応答を受け取ることができます。

APIというと「アプリケーションやサービスとやり取りをするための標準的なインターフェース」のことなので、ローカル環境でもこのワードに該当するサービスを作れば、APIと呼ぶものになるので、単純なAPIとWeb APIだとワードの範囲が違うんだなと理解しました。

Representational State Transfer(REST API)

Web APIと並列で考えてよいのかが怪しいのですが、RESTアーキテクチャスタイルであるWEB APIを指します。

  • アドレス指定可能なURIで公開されていること
  • インターフェース(HTTPメソッドの利用)の統一がされていること
  • ステートレスであること
  • 処理結果がHTTPステータスコードで通知されること

ステートレスとは、システムが現在の状態を表すデータなどを保持せず、入力の内容によってのみ出力が決定される方式。同じ入力に対する出力は常に同じになる。

これらの原則に則ったWebサービスをRESTfulなサービスといいます。
Web APIと同じように、HTTPプロトコルを使用して通信してデータをやり取りしますが、REST APIは、リソースの操作を表現するためにHTTPメソッド(後述: POST, GET, PUTなど)を使用する点が、Web APIとは異なるようです。

詳細は以下に載っていました。
RESTful API とは? - RESTful API の説明 - AWS

Simple Object Access Protoco(SOAP API)

SOAPは、HTTPプロトコルの代わりに独自のプロトコルを使用してXML形式でデータをやり取りしてAPI操作を表現します。独自のプロトコルを使うという点とXML形式を使うという点で、Web APIREST APIと異なり、SOAPの方が複雑な設定が必要であることから、比較的重いプロトコルであり、通信速度が遅くなります。ただし、セキュリティや信頼性に優れるという利点があり、主に企業間のシステム間の通信に使用されることが多いようです。

Amazon API Gatewayとは

Amazon API GatewayAWS内でのAPIの作成および諸々の管理を行えるサービスのこと。基本これ単体で使うのではなく、AWS Lambda等とつなげて、リクエストに対して結果を返すサービスを作るの目的に使われます。

ちなみに、公式だと以下の説明が載っています。
Amazon API Gateway(規模に応じた API の作成、維持、保護)| AWS

フルマネージド型サービスの Amazon API Gateway を利用すれば、デベロッパーは規模にかかわらず簡単に API の作成、公開、保守、モニタリング、保護を行えます。API は、アプリケーションがバックエンドサービスからのデータ、ビジネスロジック、機能にアクセスするための「フロントドア」として機能します。API Gateway を使用すれば、リアルタイム双方向通信アプリケーションを実現する RESTful API および WebSocket API を作成することができます。API Gateway は、コンテナ化されたサーバーレスのワークロードやウェブアプリケーションをサポートします。

APIのタイプ

APIタイプは以下から選択ができ、それぞれの特徴は以下の通りです。

HTTP API Web Socket API REST API
HTTP APIは、HTTPリクエスト/レスポンスを使用してRESTfulなWebサービスを作成するためのAPI。機能を絞ることで、REST APIよりも低レイテンシかつ低コスト(コスト最適化)を実現できるようです。 WebSocket APIは、リアルタイムなWebアプリケーションを構築するためのAPIです。WebSocket APIは、クライアントとサーバー間で長時間の接続を確立することができ、データの双方向通信を可能にします。WebSocket APIは、リアルタイムな情報の伝達が必要なチャットアプリ、オンラインゲーム、監視システムなどに適しています。 REST APIは、HTTPプロトコルに基づいたAPIで、HTTP APIの違いは色々とあるが、HTTPメソッド(GET、POST、PUT、DELETEなど)を使用してリソースの操作を実行します点が大きな差かなと思っています。

目的に応じて選択が必要です。私の場合、JSON形式でデータ送信したいので、HTTP API/REST APIから選択ができ、HTTPメソッドを用いて簡単に開発したいならばREST APIを選ぶ感じかなと思います。

REST APIのHTTPメソッド

REST APIタイプは以下から選択ができ、それぞれの特徴は以下の通りです。

メソッド 説明
GET Webページや画像などリソースを取得するために使用する
POST リソースを送信するために使用する
PUT リソースの更新や作成をするために使用する
DELETE リソースを削除するために使用する
PATCH リソースの一部を更新するために使用する
HEAD リソースのヘッダ情報のみを取得するために使用する
OPTIONS リソースに対して許可されているHTTPメソッドの一覧を取得するために使用する
ANY 一般的なHTTPメソッドではなくAWS API Gatewayで使われる概念で、すべてのHTTPメソッドを扱うことができる

なんとなくは分かりましたが・・扱えるようになるため、実際に使ってみます。

Amazon API Gateway(REST API)を使ってみる

実際に使う前に、理解した範囲で知識をまとめます。

統合リクエスト設定

統合リクエストは、API Gatewayがバックエンドに送るHTTPリクエストで、クライアントから送信されたリクエストデータを渡して、必要に応じて変更します。今回はAWS Lambdaに渡すことを前提とします。設定方法は、以下にまとめられています。
API Gateway で Lambda プロキシ統合を設定する - Amazon API Gateway



メソッドリクエストから、クエリパラメータを受け取って、Lambdaに渡したい場合は、クエリパラメータを登録する必要がある。
統合リクエストから、マッピングテンプレートを登録する。今回は、JSON形式で受け取りたいので、以下の通り3つのパラメータ(my_param1, my_param2, my_param3)をもらう設定を追加。

クエリパラメータの設定方法

URLにクエリパラメータを付与する場合は、ルールに従って以下のように書く必要がある。

  • パラメータはURLの後に「?」をつけて記載
  • 各パラメータは「名前=値」で記載
  • 複数パラメータを付与する場合は、「&」で分ける

書き方(3つのパラメータmy_param1, my_param2, my_param3を付与するケース)
URL(https://*******)?my_param1=1&my_param2=2&my_param3=3


URLはデプロイしたHTTPメソッドのエンドポイントに設定する。


エンドポイントとは、クライアントとAPIとが通信をするための窓口であり、APIのリクエスト先になる。

プロキシ統合設定について

HTTPメソッドを作成する際に、「Lambdaプロキシ統合の使用」というチェックボックスがあり、この役割を説明する。


詳しくは以下にまとめられていたが、以下のように理解しました。

統合 非統合
API GatewayはLambda関数に直接接続され、API GatewayがHTTPリクエストをLambda関数に直接ルーティングするため、API GatewayとLambda関数の間の通信が最適化されます。Lambdaから返される値のフォーマットが決まっている API GatewayとLambdaは独立して動作します。API GatewayはHTTPリクエストを受け取り、Lambda関数を呼び出すためにLambda関数のARN(Amazonリソース名)を設定します。Lambda関数はリクエストを処理し、結果をAPI Gatewayに返します。API GatewayはLambda関数からのレスポンスを返信し、クライアントに返します。Lambdaから返される値にフォーマットはなくそのままの値が返される

qiita.com

動作を確認

環境設定に関しては、以下の通りで、Lambda関数は「statusCode」「body("Hello World")」「event(リクエスト)」を返す関数であり、API Gateway側ではANYメソッドがリクエストデータをそのままLambda関数に渡す設定になっている。プロキシ統合は非統合の設定。

Lambda関数

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps('Hello World'),
        'event': json.dumps(event)
    }


AWS API Gateway

GET

ローカルにて以下コードを実行して、出力結果を確認した。

import requests
import json

pattern = "GET"
URL = r"エンドポイント"
payload = {'my_param1':"1", 'my_param2':2, 'my_param3':3}

if pattern == "GET":
    print("GET")
    response = requests.get(URL, params=payload)

data = response.json()
print ("json", json.dumps(data, indent=4))
print("status_code",response.status_code)    # ステータスコード取得
print("HTML_text", response.text)    # HTMLを文字列で取得
print("binary", response.content)    # HTMLをバイナリ形式で取得


出力結果は以下の通りで、eventは何もない状態で返されている。クエリとして、3つのパラメータを送付しているが、クエリパラメータを受け取る設定にしていないため、こういった現象が起きている。

GET
json {
    "statusCode": 200,
    "body": "\"Hello World\"",
    "event": "{}"
}
status_code 200
HTML_text {"statusCode": 200, "body": "\"Hello World\"", "event": "{}"}
binary b'{"statusCode": 200, "body": "\\"Hello World\\"", "event": "{}"}'


上記で説明した通り、統合リクエスト設定にてマッピングテンプレートを設定すると、以下の通り、受け取ることができて、Lambda関数に適切に渡されていることが確認できる。

# マッピングテンプレート設定後
GET
json {
    "statusCode": 200,
    "event": "{\"my_param1\": \"1\", \"my_param2\": \"2\", \"my_param3\": \"3\"}"
}
status_code 200
HTML_text {"statusCode": 200, "body": "\"Hello World\"", "event": "{\"my_param1\": \"1\", \"my_param2\": \"2\", \"my_param3\": \"3\"}"}
binary b'{"statusCode": 200, "body": "\\"Hello World\\"", "event": "{\\"my_param1\\": \\"1\\", \\"my_param2\\": \\"2\\", \\"my_param3\\": \\"3\\"}"}'


POST

ローカルにて以下コードを実行して、出力結果を確認した。

import requests
import json

pattern = "POST"
URL = r"エンドポイント"
payload = {'my_param1':"1", 'my_param2':2, 'my_param3':3}

if pattern == "POST":
    print("POST")
    response = requests.post(URL, data=json.dumps(payload))

data = response.json()
print ("json", json.dumps(data, indent=4))
print("status_code",response.status_code)    # ステータスコード取得
print("HTML_text", response.text)    # HTMLを文字列で取得
print("binary", response.content)    # HTMLをバイナリ形式で取得


出力結果は以下の通りで、POSTメソッドの場合はマッピングテンプレートの設定をせずパラメータを送付して、Lambda関数に適切に渡されていることが確認できる。

POST
json {
    "statusCode": 200,
    "body": "\"Hello World\"",
    "event": "{\"my_param1\": \"1\", \"my_param2\": 2, \"my_param3\": 3}"
}
status_code 200
HTML_text {"statusCode": 200, "body": "\"Hello World\"", "event": "{\"my_param1\": \"1\", \"my_param2\": 2, \"my_param3\": 3}"}
binary b'{"statusCode": 200, "body": "\\"Hello World\\"", "event": "{\\"my_param1\\": \\"1\\", \\"my_param2\\": 2, \\"my_param3\\": 3}"}'

ローカルにて以下コードを実行して、出力結果を確認した。

import requests
import json

pattern = "HEAD"
URL = r"エンドポイント"
payload = {'my_param1':"1", 'my_param2':2, 'my_param3':3}

if pattern == "HEAD":
    print("HEAD")
    response = requests.head(URL)

#data = response.json()
#print ("json", json.dumps(data, indent=4))
print(response)
print("status_code",response.status_code)    # ステータスコード取得
print("HTML_text", response.text)    # HTMLを文字列で取得
print("binary", response.content)    # HTMLをバイナリ形式で取得


出力結果は以下の通りで、HEADメソッドでは、レスポンスボディを返さないため、 response.content などでレスポンスボディを取得することはできません。HEADメソッドは、サーバーからのレスポンスボディを取得することがないため、GETメソッドよりも高速にリソースの存在確認を行うことができます。

HEAD
<Response [200]>
status_code 200
HTML_text
binary b''


他のメソッドに関しては、実際にWEBアプリを作った際に色々と確認しようと思う。一旦はここまで。

まとめ

APIに関する言葉の意味からスタートして、色々と勉強になりましたが、まだまだ奥が深く知識を浅いことを痛感しました。
HTTPメソッドは、Webアプリケーション開発において非常に重要な役割を果たしているので、使い方を勉強して、WEBアプリケーション作りに活かしていきたいと思います。

オリジン間リソース共有 (CORS)設定について

WEBアプリ(以下)を作成した時に、ブラウザのCORSというポリシーによるエラーが発生し、エラー解除に苦戦をしたので、COR2の関連知識についてまとめていきます。
dango-study.hatenablog.jp
 
目次


CORSのエラーについて

色々調べていくと、以下のように書かれていました。
Cross-Origin Resource Sharing(CORS: オリジン間リソース共有)はサーバーが同一オリジンポリシーを緩和できる標準で、異なるオリジンからのリクエストに対してサーバー側が許可を与えなかった場合に発生するエラーです。・・ちょっとまだ完全に理解ができていないので、用語などを調べながら理解を進めようと思います。

Origin(オリジン)とは

オリジンとは、ウェブコンテンツにアクセスされるために使われるURLの中のスキーム(プロトコル)ホスト(ドメイン)ポート番号を組みわせたもの

項目 詳細 備考
オリジン https://www.example.com:8080 以下3つを組み合わせたもの
スキーム(プロトコル) https:// かhttp:// 2つの違いは通信が暗号化されているかどうかで、httpsが暗号化されている
ホスト(ドメイン) www.example.com いわゆるインターネット上の住所で自身で好きなドメイン名にしたりする
ポート 8080等 ポートに関しては、httpsが443番でhttpが80番が既定番号となり、既定番号ら省略ができるので、ポート番号が付けられないこともある

同一オリジンポリシーとは

上記の通りプロトコル+ドメイン+ポートが同一の場合は同一オリジンとみなして保護をして、異なるオリジンからのアクセスを制限する仕組みのこと。

主な原則として3つがあります。

  • ドキュメントオブジェクトモデル(Document ObjectModel:DOM)は、同じオリジンのスクリプトにしかアクセスできない。

DOMは、HTMLのように文章の構造を示すものやJavaScriptのように操作を示すもの

  • XMLHttpRequest(XHR)オブジェクトを使用して、異なるオリジンからデータを取得することはできない。

XHRはサーバーとのデータ通信に使い、ウェブページの更新などを行う目的で使用される

  • クッキー(HTTP Cookie)は、同じオリジンのWebページのみに送信されます。

クッキーは、クライアントのWebブラウザとWebサーバとの間で、状態を維持・管理する仕組みで、その通信の際にクライアントのWebブラウザに保存された情報。その後のリクエストとともにサーバーに送信され、ログイン状態の維持などの役割で使用される。

つまり、あるオリジン(Webサーバー)の更新は、同一オリジン(内包したプログラム)やクライアント側でのみ行われ、異なるオリジンからの操作/情報取得等を制限する仕組みと思われます。

CORSの制限対象

同一オリジンポリシーの考え方を踏まえるとCORS(オリジン間リソース共有)の考え方は、なんとなく理解ができ、異なるオリジンからのHTTPリクエストが制限対象となります。

今回のエラーケースでいうと、アーキテクチャが以下の通りで、CORSのエラーが初期段階で出ていました。
恐らく、JavaScriptからXMLHttpRequestAPI Gatewayからデータを取得しようとしていた為、CORSエラーが発生したと思われます。
(追記)アーキテクチャ

CORSエラーの解決方法

全て網羅できていなさそうですが、対処方法を以下にまとめます。

サーバー側で必要なHTTPレスポンスヘッダーを設定する

この方法が、私がWEBアプリを作成時に対応した方法です。サーバー側で、必要なHTTPレスポンスヘッダーを設定することで、異なるオリジンからのリクエストを許可することができます。具体的には、Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-HeadersなどのHTTPレスポンスヘッダーを設定することが必要です。これらのヘッダーは、CORSエラーを回避するために必要な情報を提供するために使用されます。
詳細は、以下に記載されています。
API Gateway からの CORS エラーのトラブルシューティング | AWS re:Post
  

プロキシを使用する

プロキシを使用することで、異なるオリジン間での通信を実現することができます。プロキシとは、クライアントとサーバーの間にある中継サーバーのことで、クライアントからのリクエストを代理でサーバーに送信することができます。プロキシを使用することで、サーバー側でCORSの設定を行わなくても、異なるオリジンからのリクエストを許可することができます。

まとめ

CORSエラーは、異なるオリジン間での通信が必要な場合に発生する問題です。CORSエラーを回避するためには、サーバー側で必要なHTTPレスポンスヘッダーを設定することや、プロキシを使用することができます。開発をしていて障害になるのは、こういった制限ルールなどが多いので、こういった知識をひとつづつ学んでいかないといけないなと思いました。

できるだけPythonだけでWEBアプリを作る②(Python flask/zappa + AWS)

「できるだけPythonだけでWEBアプリを作る第2弾」は、第1弾から少し構成を変えて、拡張性を考慮したやり方で作っていきます。
今回はZappaというフレームワークを使っており、様々なAWSの設定が簡単になる反面、裏で何をやっているのかが完全に理解できなかったので、とりあえずシステムの作成フローをこのページにまとめて、掘り下げて分かったことは追ってまとめようと思います。
第1弾(前回)記事は以下の通り。
dango-study.hatenablog.jp

目次


WEBアプリをクラウド(AWS)で作成するためのアプローチ

前回はとりあえずWEBアプリを作る!という感じだったので、特に気にしていなかったのですが、本来はどんなアプローチ方法がとれるのかを踏まえた上で適切に選択すべきなので、ここで一旦まとめようと思います。調べた限り、(AWS)サービスの組み合わせ方で言うと、かなり色々な方法があったので、主観ですが大きなくくりでまとめていきます。

FaaS(Function as a Service)サービスを使うアプローチ[AWS Lambda + API Gateway等]

FaaSとは「Function as a Service」の略で、サーバーレスでアプリケーション開発ができるクラウドサービスのこと。
AWS Lambda等のインスタンスを返してサーバーを任意のタイミングで利用するパターンで、サーバー管理はサービスベンダーが行うため、その費用などを抑えることができる。

CaaS(Container as a Service)サービスを使うケース[ECS + Fargate等]

CaaSとは「Container as a Service」の略で、コンテナ化をすることで環境に依存しない開発ができるクラウドサービスのこと。
CaaSサービスであるECSはAmazon EC2インスタンスを用いてDockerコンテナ管理することができるが、Amazon EC2のような仮想サーバーの管理は不要なアーキテクチャを構築できる。

IaaS(Infrastructure as a Service

IaaSとは「Infrastructure as a Service」の略で、サーバやストレージなどのインフラを提供するサービスのこと。
仮想サーバーを使用できることから開発自由度は上がるが、サーバー管理が必要であること(作業が増える)、コストが他と比較して高い。

LambdaとECS、EC2を様々な観点で比較結果は以下が詳しくまとめられていました。感謝です
EC2 vs ECS vs Lambda:APIのサービングの観点から | Hakky Handbook

つまり、使い分けは、以下の通りと理解しました。
現時点ではLambdaでできる範囲で問題がなさそうなので、コストを重視してLambdaを使う構成で対応します。

おすすめサービス
開発における制限(実行時間やライブラリ利用可否)を重視する場合 ECSかEC2
コストを重視する場合 Lambda


アーキテクチャについて

(おさらい)前回作ったアプリのアーキテクチャ

以下画像のような構成で、Amazon S3に保存したHTMLファイルにアクセス頂き、HTMLページのボタンを押すと、JavaScriptからAmazon API GatewayにHTTPリクエストが送信され、 AWS Lambdaを動かして、計算した結果をJavaScriptでHTMLに上書きするといった動きをします。

今回作るアプリのアーキテクチャ

ここが完全に理解できていないので、現状記載ができない(後述のZappaが理解できていない)。なんで、前回と同じ構成で作らないのか?という疑問が出てくるが、今回はFlaskというPythonのWebアプリケーションフレームワークで作成したWebアプリケーションをAWSでデプロイしたいから、が理由になります。今後も想定すると、1からすべて作る前回の方法よりもフレームワークを使った方がよさそうだなと・・・また、今回はFlaskのWEBアプリケーションをAWSにデプロイするのに、Zappaというフレームワークも使っています。ZappaはAWS LambdaとAPI Gatewayを使って、FlaskやDjangoなどのPython WEBアプリケーションをデプロイすることができます。AWS上の設定などを自動で設定して、構成してくれるのが便利なようです。このような理由から、こちらを使いました。
AWS マネジメントコンソールのAWS Lambda画面では、以下のような画面になっていますが、全体の構成としては、AWS Lambda/EventBridge/API Gatewayの構成になっている?この辺りから怪しいのですが、アクセスするURL(WEBアプリ)はAPI Gatewayで作成するHTTPエンドポイントで、HTTPリクエストをAWS Lambda関数に転送して実行していると思っています。ただ、EventBridgeでAWS Lambdaを定期実行する必要があるのでしょうか?Zappaを使うと、インターフェースの設定を勝手にやってくれるのが便利なのですが、どう設定をしたのかを勉強したい人にとってはつらいですね・・・

分かっていない部分も多いですが、、じゃんけんゲームのアプリを作っていきます。

ちなみに、その他の同じような構成(FaaSサービスを使うケース)例として、以下を紹介しておきます。
AWS での開始方法 | AWS Lambda、Amazon API Gateway、AWS Amplify、Amazon DynamoDB、および Amazon Cognito を使用してサーバーレスウェブアプリケーションを構築する

開発環境

[ローカル]
Microsoft Windows 10 Pro
Python 3.9.0
[AWS]
AWS Lambda ランタイム python3.9
AWS API Gateway
Amazon S3

開発手順

  1. 前準備
  2. アプリ作成(コード作成)
  3. コード実行(動作確認)


実装

前準備

AWS CLIを使えるようにする(省略)

ZappaではAWS CLIを使用する為、使えるようにしておく必要がある。
こちらは色々なページで記載があるので省略します。
AWS CLIの設定(以下)をするところまで対応します。

$ aws configure

↓参考ページ
AWS CLIを利用するメリットと導入方法 | TOKAIコミュニケーションズ AWSソリューション

仮想環境の導入

Zappaを使うには仮想環境の用意が必須となる為、その手順を説明します。
手順は以下の通りコマンドとともに示します。

1. 仮想環境を作る venvモジュールを使用する。pyenvモジュールを使うケースも多いみたいであるが、Windows OSだと問題がありそうなのでこっちを使う(私はpythonバージョン3.9.0が入っていたので、Python 3.9.0の仮想環境ができる)
$ python -m venv <好きなファイル名>
$ cd <上記コマンドで作成したファイルパス>

2. 仮想環境に入る
$ Scripts\activate

必要モジュールのインストール

Zappaでは仮想環境の用意が必須で、アプリを構成するコード類と合わせて仮想環境のモジュールもzip化してAWS Lambdaにアップロードするので、不要なモジュールを入れすぎても負荷を掛けそうなので、最低限のモジュールをインストールする。
ここからの作業は仮想環境に入った状態で対応します。

$ pip install flask
$ pip install zappa

私の環境では、この後のZappaモジュール実行時にurlib3に関するエラーが出た為(バージョン指定)、エラーに従って以下の通り対応した。
$pip uninstall urllib3
$pip install "urllib3>=1.25.4,<1.27"

Zappaの設定

WEBアプリケーションのディレクトリを作成して、その中で初期化コマンドを実行します。

適当にフォルダを作成する
$ mkdir 適当な名前
$ cd <上記コマンドで作成したファイルパス>

Zappaの初期化を行う
$ zappa init


適当に設定すると(後から変えれるので全てEnterでも良い)、現在のディレクトリにzappa_settings.jsonが作成される


アプリ作成(コード作成)

やっとアプリの作成に移り、じゃんけんゲームのアプリを作っていきます。
htmlは2種類、初期画面/じゃんけん結果画面の2つであり、コードを含めたもろもろの構成は以下の通りです。

<flask_sample> 
 ├ <templates>  
 │  ├ home.html 初期画面 
 │  └ result.html じゃんけん結果画面
 └ __init__.py   
my_app4.py アプリで最初に実行されるファイル
requirements.txt 必要モジュールなどを記載したtxt
zappa_settings.json zappa設定ファイル

WEBページの作成

初期画面/じゃんけん結果画面は以下のコードで構成しています。

home.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>じゃんけんゲーム</title>
	<style>
		body {
			font-family: Arial, sans-serif;
			text-align: center;
		}
		h1 {
			font-size: 32px;
		}
		form {
			margin-top: 32px;
			display: flex;
			flex-direction: column;
			align-items: center;
		}
		form label {
			font-size: 20px;
			margin-bottom: 8px;
		}
		form input[type="radio"] {
			margin-right: 8px;
		}
		form button {
			margin-top: 16px;
			font-size: 24px;
			padding: 8px 16px;
			border: none;
			background-color: #1E90FF;
			color: #FFFFFF;
			border-radius: 8px;
			cursor: pointer;
		}
		form button:hover {
			background-color: #4169E1;
		}
	</style>
</head>
<body>
	<h1>じゃんけんゲーム</h1>
    <form action="{{ url_for('play') }}" method="POST">
		<label><input type="radio" name="choice" value="グー" required>グー</label>
		<label><input type="radio" name="choice" value="チョキ">チョキ</label>
		<label><input type="radio" name="choice" value="パー">パー</label>
		<button type="submit">決定</button>
	</form>
</body>
</html>

ハマったところとして、画面遷移が最初はうまくいきませんでした・・・("forbidden 403"が表示)
該当箇所は、

(修正後です)の個所で、初期はパスを直書きしていたことが問題でした。Flaskのurl_for関数を使って関数とURLを紐づけてアクセスできるように変えています。url_for('play')と書くことで、関数:playが実行されます。詳細は以下参照しました。
【Python】Flask url_forについて | しげっちBlog


result.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>じゃんけんゲーム - 結果</title>
	<style>
		body {
			font-family: Arial, sans-serif;
			text-align: center;
		}
		h1 {
			font-size: 32px;
			margin-top: 48px;
			margin-bottom: 32px;
		}
		.result {
			font-size: 24px;
			margin-bottom: 16px;
		}
	</style>
</head>
<body>
	<h1>結果</h1>
	<div class="result">あなたの選択: {{ user_choice }}</div>
	<div class="result">コンピュータの選択: {{ computer_choice }}</div>
	<div class="result">結果: {{ result }}</div>
	<a href="{{ url_for('home') }}">もう一度プレーをする</a>
</body>
</html>

{{}}でくくられた箇所が変数であり、pythonコードから返された結果が表示されます。

pythonコードについて

pythonコードは2ファイル(__init__.pyだけの1ファイルだけでも良かったが・・)に分かれていて、最初にmy_app4.pyが実行される設定にしています。
コードは以下の通りです

my_app4.py

#----------
#Flaskアプリケーション実行ファイル
#----------

from flask_sample import app

if __name__ == "__main__":
    app.run()

__init__.pyを実行するだけの役割ですね


__init__.py

#----------
#Flaskアプリケーション立ち上げ後に呼び出されるファイル
#my_app4.py→__init__.py
#----------

#Flaskとrender_template(HTMLを表示させるための関数)をインポート
from flask import Flask,render_template, request, url_for
import random

#Flaskオブジェクトの生成
app = Flask(__name__)

#「/」へアクセスがあった場合に、「home.html」を返す
@app.route("/")
def home():
    return render_template("home.html")

@app.route("/result", methods=["POST"])
def play():
    # ユーザーの選択を取得
    print("def_play_start")
    user_choice = request.form["choice"]
    
    # コンピュータの選択をランダムに決定
    choices = ["グー", "チョキ", "パー"]
    computer_choice = random.choice(choices)

    print("chiices", choices)
    
    # 勝敗を判定
    result = ""
    if user_choice == computer_choice:
        result = "引き分け"
    elif (user_choice == "グー" and computer_choice == "チョキ") or (user_choice == "チョキ" and computer_choice == "パー") \
        or (user_choice == "パー" and computer_choice == "グー"):
        result = "勝ち"
    else:
        result = "負け"
    
    # 結果を表示
    return render_template("result.html", user_choice=user_choice, computer_choice=computer_choice, result=result)

コチラのコードがメインとなり、初期アクセスでは関数: homeが実行され、後述のhome.htmlが表示されます。

コード実行(動作確認)

アプリのデプロイ

作ったアプリは、zappa_settings.jsonファイルがあるディレクトに移動して、以下コードでAWSにデプロイします。

デプロイする
$ zappa deploy default ←defaultは自分が設定した値

一度デプロイをして、アップデートをしたい場合は以下を実行
$ zappa update default ←defaultは自分が設定した値

アプリケーションを削除
$ zappa undeploy default ←defaultは自分が設定した値

デプロイ時などのエラーを見たい場合
$ zappa tail

デプロイが完了すると、以下文字が出力され、URLが吐き出される。

Your updated Zappa deployment is live!: https:/*******************


動作確認

吐き出されたURLにアクセスする。


決定をクリックする

どこからでもアクセスができるURLでじゃんけんゲームができるようになりました。

まとめ

今回は、前回とは違う構成でWEBアプリを作ってみました。
フレームワークを使うと、便利なのですが若干ブラックボックスな部分が出てくるので、理解には少し時間がかかりそうです・・・
ただ、Flaskを使ったアプリを展開できるようになったので、もう少し複雑なものを作ってみたいと思います。

できるだけPythonだけでWEBアプリを作る①(Python + AWS + HTML/Java)

最近は生成AIを応用して色々と作ってみたりしていたのですが、著作権問題など色々な問題が話題になっているので、少し様子見で他のことを一度やってみようと思います。
今回は「WEBアプリを作ろう」ということで、作るまでの過程をまとめます。
ちなみに、私はPython以外の言語があまり扱えないので、Pythonを中心としてWEBアプリを作っています。
今回は初回ということで、簡単なものを作っており、次はもう少し複雑なものにもチャレンジしたいです。「作成したアプリ: タイヤサイズからタイヤ外径を計算するアプリ」

WEBアプリを作ろうとしたきっかけ

何か機能を作成した後、その機能をどなたかに使ってもらおうとすると、コードを渡して各々の環境で使ってもらうというやり方になるのかなと思うのですが・・・環境依存の問題があり、うまくいかないことも多いです。また、"不特定多数の人に気軽に使って貰えるものを作る"という目標達成のためにもWEBアプリを作れるようになることが大切かなと思っています。

目次

使用環境

AWS Lambda ランタイム python3.9
AWS API Gateway
Amazon S3
フロント側はHTML/Javaで構築し、バックエンド側はAWSで構築する
→複雑なところはAWS+Pythonでという戦略

(追記)アーキテクチャ

開発手順

  1. 前準備
  2. Amazon S3を使用する環境設定
  3. AWS Lambdaでコード実行(動作確認)

実装

前準備(AWS環境を整える)

AWS Lambda関数の作成

AWSコンソールにログインして、Lambdaの画面を開き、関数->関数作成を押下する。
以下のような画面が出るので、関数名は適当に記入、ランタイムは"Python3.9"を設定する。

関数

API Gatewayの作成/設定

苦戦したので詳しく書きますが、まだ完全に理解できていないため、補足は追記予定。

Lambdaの関数画面で"トリガーを追加"個所をクリックして、ウィンドウを開く。
ウィンドウが開いたのち、"API Gateway"を選択すると以下画像のような設定画面が現れるので、APIタイプ REST API/セキュリティは"開く"を選択して設定する。

詳細設定

上記の通り作成すると、先程の関数ウィンドウのトリガー欄に、API Gatewayが作成される。

API Gateway


API Gatewayの名前をクリックして、詳細を設定する。
以下画像では、"Resources"欄に"/POST"という1つしか存在しないが、当初は"ANY"が存在している。
"ANY"はすべてのHTTPメソッドをサポートしており、色々使えそうであるが・・使い方が理解できていないので、一旦違う方法で対応。"ANY"メソッドは削除して"GET"メソッドを作成する。GETメソッドは、指定したリソースのデータを返すメソッドで、HTML上でJavaを呼び出し、API Gateway経由でAWS Lambdaを呼び出してその値をJavaに返す為に使用する。
作成画面は以下の通りで"Lambda 関数"と記載の個所は、自身の関数名を入力する

作ろうとしているシステムは、HTML/javaからのAWS API GatewayへのGETリクエストをAWS Lambdaに渡して、その結果を返すというもので、HTMLの入力値をLambdaに渡す必要があります。まずは、その渡す部分の設定をしていきます。

統合リクエストを選択して、"マッピングテンプレート(jsonパラメータ)の追加"を行う


json形式でAws lambdaに値を渡す為、Context-Typeにapplication/jsonを追加して、テンプレートを追加する。

{
    "input1": "$input.params('param1')",
    "input2": "$input.params('param2')",
    "input3": "$input.params('param3')"
}


HTML/javaからは3つの変数(param1,param2,param3)が渡され、それらをlambdaに渡します。
lambda側からは、それぞれの変数をevent["param1"], event["param2"] , event["param3"] で取り出すことができます。

  • API Gatewayの設定③(オリジン間リソース共有 (CORS)設定)

これに関しては理解が追いついておらず、詳しい説明は別サイトを参照した方が良い。
今回のプログラムを実行しようとするとブラウザのCORSというポリシーによるエラーが発生する。このエラーを回避するために、Aws lambdaから返ってきた値をHTML/javaに返す際に、アクセス許可をするためのヘッダーを追加する。

developer.mozilla.org

統合レスポンスを選択して、"レスポンスヘッダーの追加"を行う


以下の通り、"*"を入れることで、アクセス許可を付与

  • API のデプロイ

"Resources"の横の"Actions"欄から、Deploy APIを選択することで、APIをデプロイ(設置)することができる。
URLに載っています。
デプロイ時のStage名などは、任意に設定する。

WEBページの作成/Amazon S3への保管

Amazon S3の設定について[省略:別ページで紹介]

dango-study.hatenablog.jp

WEBページ(HTML)の作成

HTMLの作成方法は以下を参考にして作成した。
基本的なフォーム -- ごく簡単なHTMLの説明

以下コードの"script src="の欄に、次で紹介するjavascriptのオブジェクトURL(S3に保管した)を入力する。コードはメモ帳などに入力後、"@@.html"の形式で保存しておく。

<!DOCTYPE html>
<heml lang="ja">
<head>
    <meta charset="utf-8">
    <title>Webアプリケーション sample1</title>
</head>
<body>

<article>
    <h1>Webアプリケーション タイヤ外径計算機</h1>
    <section>
        <p>タイヤサイズ情報を入力すると、タイヤ外径値を返します</p>
        <table>
        <tr>
        <th class="hissu"><label>タイヤサイズ</label></th>
        <td><textarea id="input_textfield1" name="input" rows="2" cols="10"></textarea></td>
        <th>/</th>
        <td><textarea id="input_textfield2" name="input" rows="2" cols="10"></textarea></td>
        <th>R</th>
        <td><textarea id="input_textfield3" name="input" rows="2" cols="10"></textarea></td>
        </tr>
        </table>
        <br>
        <input type="submit" value="計算結果" onclick="send_message();">
        <script src="<javaスクリプトのオブジェクトURLを記入>"></script>
        <h2>出力結果</h2>
        <table>            
        <tr>
        <th id="output_label1">ここに出力されるよ</th>
        <th>[mm]</th>
        </tr>
        </table>
        <p></p>
    </section>
</article>
</body>
</heml>

WEBページ(java)の作成

javaの作成方法は以下を参考にして作成した。
JavaScriptでAPIを呼び出す方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

以下コードの" request_url ="の欄に、AWS lambda 関数のオブジェクトURLを入力する。
コードはメモ帳などに入力後、"@@.js"の形式で保存しておく。
HTML/javaからAWS lambdaに変数を渡すことになるが、HTTPリクエストのパラメータリクエストは以下のように記載する。
↓HTTPリクエスト(パラメータあり)

https://(AWS lambda関数)?param1=値&param2=値&param3=値
#####"値"には実際に渡す引数値が入る。#####


コード

const send_message = () => {
  // URLを作成
  let input_label1 = document.getElementById("input_textfield1");
  let input_label2 = document.getElementById("input_textfield2");
  let input_label3 = document.getElementById("input_textfield3");
  var parameter1 = input_label1.value;
  var parameter2 = input_label2.value;
  var parameter3 = input_label3.value;
  parameter1 = parameter1.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している(TODO:改善)
  parameter2 = parameter2.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している(TODO:改善)
  parameter3 = parameter3.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している(TODO:改善)
  parameter1 = encodeURI(parameter1);
  parameter2 = encodeURI(parameter2);
  parameter3 = encodeURI(parameter3);
  console.log(parameter1);
  console.log(parameter2);
  console.log(parameter3);

  request_url = "<AWS lambda関数のオブジェクトURLを記入>?";
  request_url = request_url + "param1=" + parameter1 + "&" + "param2=" + parameter2 + "&" + "param3=" + parameter3;
  console.log(request_url);

  // リクエストオブジェクトの作成"
  var request = new XMLHttpRequest();
  request.open('GET', request_url, true);

  // リクエストが成功したときに呼ばれる関数
  request.onload = function () {
    var json_data = this.response;
    var return_message1 = JSON.parse(json_data)["body"];

    let output_label1 = document.getElementById("output_label1");
    output_label1.innerText = return_message1;
  };

  request.send(); // URLリクエストを送信する
}


AWS lambdaから返ってきた値は、以下の通りに受け取って、HTMLの更新を行う。

  // リクエストが成功したときに呼ばれる関数
  request.onload = function () {
    var json_data = this.response;
    var return_message1 = JSON.parse(json_data)["body"];

    let output_label1 = document.getElementById("output_label1");
    output_label1.innerText = return_message1;
  };

Amazon S3にウェブページを保管

WEBページHTML/JavaをそれぞれS3に保存する。
HTMLファイルのオブジェクトURLがWEBアプリのアクセスURLとなる
フロントエンド部分をHTMLで構成している為

AWS Lambdaでコード実行(動作確認)

コード作成

Lambda関数では、以下コードを作成/実行する。
(こちらがフルのコードとなります)

import json

def lambda_handler(event, context):
    # TODO implement
    param1 = event['input1']
    param2 = event['input2']
    param3 = event['input3']

    try:
        if str.isnumeric(param1):
            overall_diameter_of_tyre = float(param1)*float(param2)/100*2+float(param3)*25.4
        
        return {
            'statusCode': json.dumps(200),
            'body': json.dumps(overall_diameter_of_tyre)
        }
    except Exception as e:
        raise e
        return {
            'statusCode': json.dumps(400),
            'body': json.dumps("Error")
        }


コード解説

HTML/javaからは3つの変数(param1,param2,param3)が渡され、それらをlambdaに渡します。
lambda側からは、それぞれの変数をevent["param1"], event["param2"] , event["param3"] で取り出すことができます。

param1 = event['input1']
param2 = event['input2']
param3 = event['input3']


lambdaで処理した結果は、関数:lambda_handlerの戻り値でjson形式で返す

        return {
            'statusCode': json.dumps(400),
            'body': json.dumps("Error")
        }

Java側の値の受け取り方は、上の記述の通り

実行結果

値を入力した後に"計算結果"ボタンを押すと、出力結果が表示されました。

まとめ

Python以外が詳しくない私でもWEBアプリを作ることができました。
かなり単純なアプリで、これだったらAWS+Pythonの必要ないのでは・・と思われるかもしれないので、次はもう少し複雑なものを作っていきたいと思います。

LINEの質問(Text)に対して音声で返してもらう(ChatGPT + AWS Polly + LINE)

AWSサービス(Lambda + API Gateway + AWS Polly)とLINE、ChatGPTとを連携させて、LINEで質問した内容の返信を音声で受け取るシステムを作った時のメモを残します。
最終目標として、RVC(Retrieval-based-Voice-Conversion)やbark(以下)といった技術を使用して、流ちょうなしゃべり方、特定のキャラクターのしゃべり方への音声変換して使われることを目標としています。
とりあえずは、その前段階の作成記録として、順々にまとめていきます。
github.com


目次

使用環境

AWS Lambda ランタイム python3.9
AWS API Gateway
Amazon Polly
Amazon S3
・LINE (developers)
・ChatGPT API

開発手順

  1. 前準備(過去ブログに記載されている為 省略)
  2. Amazon Polly + Amazon S3を使用する環境設定
  3. AWS Lambdaでコード実行(動作確認)

実装

前準備[省略:別ページで紹介]

これらの手順は過去のブログ(以下)で説明をしているので、そちらをご参照ください。
具体的な作業は、AWS API Gateway + LINE API + ChatGPT APIを使うための準備になります。
dango-study.hatenablog.jp
dango-study.hatenablog.jp

Amazon Polly + Amazon S3を使用する環境設定

Amazon Polly はAWSのサービスの一つで、深層学習技術を使用し、人間の声のような音声を合成します。Text-to-Speech (TTS)の方法は他にも色々ありますが、他の方法をAWSで実装するよりも既存のものを使った方が安いし早いと思ったので、こちらのサービスを使いました。

今回は、このサービスにChatGPTの返答結果(Text)を入れて、音声ファイルを出力させます。
この音声ファイルをLINEで送る為に、一度どこかに保管が必要であり、その保管場所として、Amazon S3を使用します。

Amazon S3の設定について
  1. AWS コンソールにサインインして、Amazon S3にアクセスをします
  2. S3のバケットを作成します



  • 各種設定についての特記事項


 LINEにデータを渡すための方法として、パブリックアクセスができるような設定にしています。
 S3コンソールを使用したバケット作成時に、パブリックアクセスのブロック設定の無効化に s3:PutBucketPublicAccessBlock 許可が必要となる為、IAMのポリシーに追加する必要がある。

  • パブリックアクセスができるのかどうかの確認


試しに、buffer.mp3というファイルをアップロードして、そのファイルにアクセスできるかどうかを確認する。
ファイルのタブをクリックして"URLをコピー"を押すと、ファイルのアクセスパスを入手できる。
そのパスにブラウザでアクセスをした時に、ファイルをダウンロードをすることができれば、パブリックアクセス設定ができている。

Amazon Pollyの設定について
  1. AWS LambdaからAmazon Pollyにアクセスができるようにロールを追加

ロールを追加したら、あとはコードを書くだけ。
以下を参考にして、コードを作成する。
開始方法 - Amazon Polly | AWS

AWS Lambdaでコード実行(動作確認)

コード作成

Lambda関数では、以下コードを作成/実行する。
(こちらがフルのコードとなります)

import json
import urllib.request
import os
import openai 
from linebot import LineBotApi
from linebot.models import TextSendMessage, AudioSendMessage
import boto3
from botocore.exceptions import ClientError
  
#######LINE Bot関係
# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
# チャネルアクセストークンを使用して、LINE BotのAPIインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

#######ChatGPT
openai.api_key = os.environ["Openai_ACCESS_TOKEN"]
#Token数
MAX_TOKENS = 150
#モデルを指定
MODEL_ENGINE = "gpt-3.5-turbo"

#######Dynamo DB

def get_system_prompts():
    try:
        # Reverse messages
        system_prompts = {"role": "system", "content": "質問を返してください"}
                
        # Join the lists
        return system_prompts

    except Exception as e:
        # Raise the exception
        raise e

def use_chatgpt(messages):
    try:
        response = openai.ChatCompletion.create(
            model=MODEL_ENGINE,
            messages=messages,
            max_tokens=MAX_TOKENS
        )
        answer = response.__dict__["_previous"]["choices"][0]["message"]["content"]
        return answer
    except Exception as e:
        # Raise the exception
        raise e

def reply_message_for_line(reply_token, completed_text, original_content_url):
    #LINEで入力されたテキストは ['events’][0]['message’]['text’] に入っています。
    #また、受信したメッセージに対してリプライを行うには、replyTokenの値を使用します。
    try:
        # Reply the message using the LineBotApi instance
        print(original_content_url)
        #テキストで返す場合は、以下を使用する
        #LINE_BOT_API.reply_message(reply_token, TextSendMessage(text=completed_text))
        #音声で返す場合は、以下を使用する
        LINE_BOT_API.reply_message(reply_token, AudioSendMessage(original_content_url=original_content_url, duration=30000))

    except Exception as e:
        # Raise the exception
        raise e

def create_completed_text(line_user_id, prompt_text):

    # Create the list of a current user prompt
    current_prompts = {"role": "user", "content": prompt_text}
    
    system_prompts = get_system_prompts()
    
    messages = [system_prompts] + [current_prompts]
    
    completed_text = use_chatgpt(messages)
    
    return completed_text

def lambda_handler(event, context):
    try:        
        if 1:
            #S3_Polly
            session = boto3.Session()
            polly = boto3.client("polly")
        
            s3 = session.resource('s3')
            bucket = s3.Bucket("polly.speech")
                        
            # Parse the event body as a JSON object
            #if json.loads(event)["events"][0]["message"]["type"] == "text":# textの場合のみ対応
            if event["events"][0]["message"]["type"] == "text":# textの場合のみ対応
                reply_token = event["events"][0]['replyToken']# リプライ用トークン
                prompt_text = event["events"][0]["message"]["text"]# 受信メッセージ
                line_user_id  = event["events"][0]['source']['userId']# userID

                # Check if the event is a message type and is of text type
                if prompt_text is None or line_user_id is None or reply_token is None:
                    raise Exception('Elements of the event body are not found.')

                # Create the completed text by Chat-GPT 3.5 turbo
                completed_text, history_text, conversation_num = create_completed_text(line_user_id, prompt_text)
                
                print("completed_text", completed_text)

                #Polly output
                if 1:
                    response = polly.synthesize_speech(
                        Text=completed_text,
                        #Engine="neural",
                        OutputFormat="mp3",
                        VoiceId="Mizuki")
                    with closing(response["AudioStream"]) as stream:
                        bucket.put_object(Key="<任意のファイル名>.mp3", Body=stream.read(), ACL="public-read")
                    
                #バケット内の音声
                original_content_url=<ここにS3に保管したmp3ファイルのアクセスパスを入れる>
                
                # Reply the message using the LineBotApi instance
                reply_message_for_line(reply_token, completed_text, original_content_url)

    except Exception as e:
        # Log the error
        
        # Return 200 even when an error occurs as mentioned in Line API documentation
        return {'statusCode': 200, 'body': json.dumps(f'Exception occurred: {e}')}
    
    # Return a success message if the reply was sent successfully
    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}


Amazon Pollyに渡す引数設定や、S3からファイルを取り出すためのコードは、AWSのドキュメントが一番詳しく書いてあるが、簡単に以下説明を追加した。

#Polly output
response = polly.synthesize_speech(
Text=completed_text,
OutputFormat="mp3",#出力ファイル形式設定
VoiceId="Mizuki") #出力ボイスの選択
with closing(response["AudioStream"]) as stream:
bucket.put_object(Key="<任意のファイル名>.mp3", Body=stream.read(), ACL="public-read") #キーとファイルの組み合わせで格納する。ACL="public-read"とすることで、更新したファイルのパブリックアクセスできるようになる
                    
#バケット内の音声
original_content_url=<ここにS3に保管したmp3ファイルのアクセスパスを入れる>#保存したファイルのアクセスパス


実行結果

ブログなので、結果を表現しずらいですが、、あいさつに対して、ChatGPTが適切な挨拶を考えて、その文言が音声として帰ってきます。

実行結果


まとめ

今回は、複数のAWSサービスとLINEを連携させ、LINEに音声で返すところまでを構築しました。
これをベースにして、任意のキャラクターとLINEで会話が楽しめるようなシステムを作っていこうと思います。

Python(BeautifulSoup4):ネットのページから必要情報を取得する(野球の試合状況を取得する)

特定の(最新)情報を入手して、何かをしたい場合、情報入手先にアクセスして、情報を取得する作業が必要となります。その方法をこのページではまとめます。
今回はその例として野球試合情報を入手するコードを紹介します。

目次

使用環境

$wmic os get caption
>Microsoft Windows 10 Pro

$wmic os get osarchitecture
>64ビット

$python -V
Python 3.11.2

手順

  1. 必要モジュールのインストール
  2. コード作成/実行

必要モジュールのインストール

今回は、Requests, BeautifulSoup4モジュールを使用します。
Requestsモジュールは、HTMLデータの取得に使用します。
その後、必要情報のみを抽出する為、HTMLデータの抽出で使用するのが、BeautifulSoup4モジュールになります。
HTMLとは「ハイパーテキストマークアップ・ランゲージ(Hyper Text Markup Language)」のことで、WEBページを作成するための言語で、ほとんどのウェブページは、HTMLで作成されています。
Google Chrome(グーグルクローム)のブラウザを使う場合、F12キーを押すと、デベロッパーツールが起動して、HTMLの内容を確認できますね。必要情報が、HTML内のどこに存在するのかを確かめながら、コードを組むのがよさそうです。

まずはBeautiful Soupをインストールしましょう。pipのコマンドでインストールすることが出来ます。
モジュールの詳細は以下をご参照ください。
www.crummy.com

pip install requests
pip install beautifulsoup4


コード作成/実行

今回の目的(野球の試合状況を取得する)は、以下コードで実現しています。
このコードを短期間で連続使用する場合、サーバーに負荷がかかるため、気を付けた方がよさそうです。

import requests
from datetime import datetime
from time import sleep
from bs4 import BeautifulSoup

#######野球設定
OUR_TEAM = "広島" #情報取得したいチーム名をTEM_LIST内から1チーム選ぶ
TEAM_LIST = ["広島", "ヤクルト", "阪神", "DeNA", "中日", "巨人"]

def live_scores(date) ->"yyyy-mm-dd":

    # URLを指定
    url = "https://baseball.yahoo.co.jp/npb/schedule/?date=" + date

    # URLにアクセスしてHTMLを取得
    try:
        print(url)
        response = requests.get(url)
    except: 
        print('Error')
    html = response.text

    # HTMLを解析してBeautiful Soupオブジェクトを作成
    soup = BeautifulSoup(html, "html.parser")
    #print("soup",soup)

    # スコアのテーブルを取得    
    game_card = soup.find_all("ul", class_="bb-score__card bb-score__card--separate")
    game_card_several_team_name = soup.find_all("div", class_="bb-score__team")
    game_card_several_score = soup.find_all("div", class_="bb-score__detail")

    num = []
    for row in range(len(game_card_several_team_name)):
        # 日付、対戦相手、スコア、勝敗、詳細ページのURLを取得
        if OUR_TEAM in str(game_card_several_team_name[row]):
            num.append(row)

    if num :
        check_dict = {}
        buff = game_card_several_team_name[num[0]].find_all("p")
        for i in range(len(buff)):
            for name in TEAM_LIST:
                if name in str(buff[i]):
                    check_dict[name] = i
        n=0
        for i in list(check_dict.keys()):
            if n == 0:
                home_team = i
            else:
                away_team = i
            n+=1
        if home_team == OUR_TEAM:
            home_team_flg = 1
        else:
            home_team_flg = 0
        
        print(home_team, away_team)

        try: 
            if home_team_flg:
                game_status = "ホーム"
                home_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--left").text.strip()
                away_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--right").text.strip()
            else:
                game_status = "アウェイ"
                away_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--left").text.strip()
                home_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--right").text.strip()
                
            now_game_status = game_card_several_score[num[0]].find("p", class_="bb-score__link").text.strip()        
            if now_game_status == "試合終了":
                if home_score > away_score:
                    status_comment = "勝った"
                elif home_score < away_score:
                    status_comment = "負けた"
                elif home_score == away_score:
                    status_comment = "引き分けた"
            else:
                if home_score > away_score:
                    status_comment = "勝っている"
                elif home_score < away_score:
                    status_comment = "負けている"
                elif home_score == away_score:
                    status_comment = "引き分けている"
            return date + "の" + OUR_TEAM + "の試合は" + now_game_status + "で、スコアは" + home_score + "-" + away_score + "で"+status_comment
        except:
            print("skip")
        
            now_game_status = game_card_several_score[num[0]].find("p", class_="bb-score__link").text.strip()
            return  date + "は" + OUR_TEAM + "の試合は開催前で、午後開催" 
    else:
        return  OUR_TEAM + "は" + "の試合はない"


簡単にコードを解説すると、

  • requests.get(url)にて指定のURLのHTMLを取得する
  • soup = BeautifulSoup(html, "html.parser")にてHTMLを解析する(HTMLパーサー)

 Webページから必要情報を取得したりすることを、スクレイピングと呼びますが、スクレイピングは、“データを収集した上で利用しやすく加工すること"のようです。その後、必要情報を取得するコードは、BeautifulSoupライブラリを使って実行しており、今回は、".find"などを使って、必要情報を抽出しています


参考までに実行結果を紹介すると、

live_scores("2023-04-22")

Output: '2023-04-22の広島の試合は試合終了で、スコアは3-0で勝った'


まとめ

今回は、ネットページから必要情報を取得する例を紹介しました。
定期的に何かの情報を取得して使用するといった処理が必要な場合に重宝しそうですね