作ったもの
「デーモン小暮」「ジミー大西」「ギャル曽根」「ヘルシェイク矢野」...
カタカナ語+名字の芸名やハンドルネームって尖ってて憧れますよね。
今回は、そんなちょっと尖ったハンドルネームを自動で生成する**「†ちょっと尖ったハンネメーカー†」**を作成しました。
目的
先日始めてVue.jsに触れ、フロントエンドについて少しだけわかったところで今回は引き続きAWSの学習の意味を込めて、**S3の静的ホスティング + CloudFrontで公開したページ(Vue.js)**と、Lambda + API Gateway + DynamoDB という構成で作成したREST APIとの連携 でWEBアプリを作成してみることにしました。
使用技術など
Route53
でドメイン(今回は togattahanne.hamabe.info
)を割当て、CloudFront
で配信の安定化およびSSL対応、S3
に静的ファイルを配置しホスティングを行います。また、APIGateway
でREST API
を定義しリクエストがあるとLambda
が発火、DynamoDB
へアクセスし取得した情報をレスポンスとして返しています。
また、DynamoDB
に格納している名字とカタカナ語のデータはPython(BeautifulSoup4)
でWEBスクレイピングを行いました。
多分かなり安い
AWSの料金体系はなかなか分かりにくいですが、この構成はおそらくかなり安いです。
参考:https://qiita.com/teradonburi/items/a382a17e1e0245b7d831
ちゃんと計算はしていないので感覚的な話になってしまいますが、同じようなWEBアプリを例えばEC2+RDSで作る場合と比べると、おそらく何十分の一にも安くなるような気がします。素晴らしい!
仕様
DB設計
DynamoDB
上に作成するテーブルは以下の2つです。
カタカナ語は創作に使えるかもしれない用語集様、名字は須﨑のホームページ様からスクレイピングさせていただきました。
- 名字テーブル(myoji)
カラム名 | 項目名 | 型 | プライマリパーティションキー |
---|---|---|---|
id | 名字ID | Number | ○ |
name | 名字 | String | |
name_read | 名字(読み) | String | |
rank | 世帯数順位 | String |
- カタカナ語テーブル(second_name)
カラム名 | 項目名 | 型 | プライマリパーティションキー |
---|---|---|---|
id | カタカナ語ID | Number | ○ |
second_name | カタカナ語 | String | |
second_name_original | カタカナ語(元言語) | String | |
caption | 意味 | String |
API設計
上記のDB内容を踏まえて、実装するAPIの仕様は以下の通りとなります。
パラメータを指定しない場合ランダムに、指定した場合該当レコードを返します。
- リクエストパラメータ
パラメータ名 | 型 | 説明 |
---|---|---|
name_id | Number | 名字テーブルのID。指定しないor該当レコードが無い場合ランダムに取得する。 |
second_id | Number | カタカナ語テーブルのID。指定しないor該当レコードが無い場合ランダムに取得する。 |
name | String | 名字をIDでなく文字列で指定することもできる。 |
- レスポンス例
パラメータ無し
ランダムなレコードが帰ってくる。
{
name: {
name_read: "おの",
id: 2539,
name: "尾野",
rank: "2145"
},
second_name: {
second_name: "ピュグマリオン",
id: 497,
caption: "ギリシャ神話に登場するキプロス島の王。自らが作った彫刻に恋し人間になることを願う。それを見た女神アフロディーテは彫刻に命を与え、王はその女性と結婚した。",
second_name_original: "Pygmalion"
}
}
パラメータ{ name_id : 12, second_id : 542 }
指定したIDに対応するデータが帰ってくる。
{
name: {
name_read: "やまだ",
id: 12,
name: "山田",
rank: "12"
},
second_name: {
second_name: "ヘクサメトロス",
id: 542,
caption: "古代ギリシャの叙事詩などで用いられた詩の形式で、1行が6つの韻脚となっている詩を指す。ホメロスの「イリアス」「オデュッセイア」がこの形式で書かれている。",
second_name_original: "hexameter"
}
}
パラメータ{ name : "壇ノ浦" }
該当レコードの無いname
を指定すると以下のようなデータが帰ってくる。
{
name: {
name_read: "",
id: 99999,
name: "壇ノ浦",
rank: ""
},
second_name: {
second_name: "ヴァンダリズム",
id: 80,
caption: "建築物や公共の施設などに対して行われる落書きや破壊などの行為。景観破壊。",
second_name_original: "vandalism"
}
}
実装
スクレイピング
前述のとおり今回用いたデータについて、カタカナ語は創作に使えるかもしれない用語集様、名字は須﨑のホームページからスクレイピングさせていただきました。
今回BeautifulSoup
(Pythonのスクレイピング用ライブラリ)について詳しく解説はしません。ソースは以下のような感じです。
from bs4 import BeautifulSoup
import urllib.request
import json
import re
def gen_katakana_json(urls):
result = []
for url in urls:
katakana_soup = get_soup(url)
katakana_json = katakanawords_to_json(katakana_soup)
result.extend(katakana_json)
return result
def gen_myoji_json(file):
myoji_soup = BeautifulSoup(open(file), "lxml")
myoji_json = myoji_to_json(myoji_soup)
return myoji_json
def get_soup(url):
print(url+" 取得中")
req = urllib.request.Request(url)
response = urllib.request.urlopen(req)
html = response.read()
soup = BeautifulSoup(html, "lxml")
return soup
def katakanawords_to_json(soup):
words = soup.find_all("p")[1] # 2個目のpがワードリスト
words_parsed = words.text.replace(
"\u3000", "").split("\n") # u3000を除去、改行で名前・キャプションをパース
words_dictlist = []
for (i, word) in enumerate(words_parsed):
# 名前をカタカナと英語に整理
if i % 2 == 0:
kana_name = re.split("[((]", word[1:])[0]
en_name = re.split("[))]", re.split("[((]", word[1:])[1])[0]
# 辞書型に格納しリストに追加
elif i % 2 == 1:
words_dict = {"second_name": kana_name,
"second_name_original": en_name,
"caption": word}
words_dictlist.append(words_dict)
return words_dictlist
def myoji_to_json(soup):
rows = soup.find_all("tr")
names_dictlist = []
name_dict = {}
for row in rows:
try:
rank = row.find_all("td")[0].text
myoji = row.find_all("td")[1].text
yomi = re.sub("[★☆]", "", row.find_all("td")[4].text)
except:
continue
name_dict = {"name": myoji, "name_read": yomi, "rank": rank}
names_dictlist.append(name_dict)
return names_dictlist
if __name__ == "__main__":
# カタカナ語スクレイピング
katakana_urls = [
"https://kakkoii-yougosyuu.com/archives/1035333396.html",
"https://kakkoii-yougosyuu.com/archives/katakanakagyou.html",
"https://kakkoii-yougosyuu.com/archives/1036149936.html",
"https://kakkoii-yougosyuu.com/archives/katakanatagyou.html",
"https://kakkoii-yougosyuu.com/archives/katakananagyou.html",
"https://kakkoii-yougosyuu.com/archives/1036170595.html",
"https://kakkoii-yougosyuu.com/archives/katakanamagyou.html",
"https://kakkoii-yougosyuu.com/archives/katakanayarawa.html",
]
katakana_words = gen_katakana_json(katakana_urls)
# カタカナ語jsonをファイル出力
with open("./json/katakana.json", 'w') as outfile:
json.dump(katakana_words, outfile, ensure_ascii=False)
# 名字スクレイピング(htmlファイルから)
myoji_file = "./myoji.html"
myoji_words = gen_myoji_json(myoji_file)
# 名字jsonをファイル出力
with open("./json/myoji.json", 'w') as outfile:
json.dump(myoji_words, outfile, ensure_ascii=False)
スクレイピングはその時々html側の事情に対応していく感じになるのでコードを見てもいまいち何やってるか分かりにくいと思いますが、なんだかんだで以下のようなjsonが取得できました。
[
:
{ "name": "林", "name_read": "りん", "rank": "19" },
{ "name": "清水", "name_read": "きよみず", "rank": "20" },
{ "name": "清水", "name_read": "しみず", "rank": "20" },
{ "name": "山崎", "name_read": "やまさき", "rank": "21" },
{ "name": "池田", "name_read": "いけだ", "rank": "22" },
:
]
[
:
{
"second_name": "モロトフ・カクテル",
"second_name_original": "molotov cocktail",
"caption": "英語で「火炎瓶」。"
},
{
"second_name": "ヤハウェ",
"second_name_original": "Yahweh",
"caption": "ユダヤ教・キリスト教の唯一神の名前。ヤーヴェ、エホバとも。"
},
{
"second_name": "ヤルダバオト",
"second_name_original": "Jaldabaoth",
"caption": "グノーシス主義において、この世を作った偽の神の名前。「混沌の子」の意味とされる。"
},
:
]
DB格納
次にスクレイピングした内容をDynamoDBに格納していきます。
今回boto3
というライブラリを用いてPythonを介してのAWS操作を行ってみます。
- boto3のインストール
$ pip install boto3
AWS CLIにて認証情報ファイルの設定をしていれば以下のようにプロファイル名からセッションを作成し、簡単にAWSリソースにアクセスすることができます。
session = Session(profile_name='PROFILE_NAME')
dynamodb = session.resource('dynamodb')
テーブルの作成もPython(boto3)から行うこともできますが、今回はAWSコンソールにて予めテーブルの作成を行っておきます。テーブル名とプライマリーキーを指定するだけです。画像は名字テーブルですが、同様の設定でカタカナ語テーブルも作成します。
boto3
の使用準備とテーブルの作成が完了したので、次はデータのインポートを行っていきます。
boto3
のDynamoDB
クラスのput_item
メソッドによりテーブルに要素を追加できるので、先程のjsonファイルを読み込み、1件ずつ連番(id)を付与してからテーブルへput_item
しています。ソースは以下のとおりです。
import boto3
from boto3.session import Session
import json
profile = 'PROFILE_NAME'
session = Session(profile_name=profile)
dynamodb = session.resource('dynamodb')
table = dynamodb.Table('myoji')
with open("../json/myoji.json", 'r') as f:
myojis = json.load(f)
for (i, myoji) in enumerate(myojis):
insert_item = {
"id": i,
"name": myoji["name"],
"name_read": myoji["name_read"],
"rank": myoji["rank"]
}
table.put_item(Item=insert_item)
put_item
はあまり早くない?ようで、6000件弱の追加に十数分かかってしまいました。ここの処理については改善の余地がありそうです。DynamoDBについてもう少し勉強しないとな…。そもそもDynamoDBの良さ(NoSQLの利点など)を正直まだなにもわかっていません。勉強します…!
なにはともあれ、カタカナ語テーブルにも同様の処理を行い無事にDynamoDBにスクレイピングしたデータを格納することができました。
Lambda関数作成
Lambdaコンソールから関数の作成を行います。
Lambda関数もPythonで記述しており、ここでもboto3
を用いてDynamoDBへのアクセスを行っています。
Lambda関数が呼び出された時最初に実行されるのがlambda_handler
関数です。ここに渡されたパラメータに応じた処理を記述します。
ここで注意すべき点として、下記のコードではevent['name_id'] != ""
という書き方でパラメータが渡されたかのチェックを行っていますが、この記述だとLambdaコンソールで関数のテストをする際、パラメータを渡さなかったらKeyError
が発生してしまいます。
後述のAPIGateway
から呼び出される場合は、例えばパラメータname_id
が渡されない場合でもevent['name_id']
には""
が格納されるため問題無いのですが、Lambda単体で動作させる場合はパラメータname_id
が渡されない場合はevent['name_id']
自体が定義されません。
僕はこれに気づかずLambda単体でのテストでエラーが出まくって軽くハマったので皆さんは気をつけましょう!
import boto3
import random
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
myoji_table = dynamodb.Table('myoji')
secondname_table = dynamodb.Table('second_name')
def get_myoji_by_id(id):
response = myoji_table.get_item(
Key={
'id': id
}
)
try:
return response['Item']
except KeyError:
return get_myoji_random()
def get_secondname_by_id(id):
response = secondname_table.get_item(
Key={
'id': id
}
)
try:
return response['Item']
except KeyError:
return get_secondname_random()
def get_myoji_count():
response = myoji_table.scan(Select='COUNT')
return response['Count']
def get_secondname_count():
response = secondname_table.scan(Select='COUNT')
return response['Count']
def get_myoji_random():
count = get_myoji_count()
random_id = random.randrange(count)
response = myoji_table.get_item(
Key={
'id': random_id
}
)
return response['Item']
def get_secondname_random():
count = get_secondname_count()
random_id = random.randrange(count)
response = secondname_table.get_item(
Key={
'id': random_id
}
)
return response['Item']
def lambda_handler(event, context):
if event['name_id'] != "":
name = get_myoji_by_id(int(event['name_id']))
elif event['name'] != "":
response = myoji_table.scan(
FilterExpression=Key('name').eq(event['name'])
)
try:
name = response["Items"][0]
except (KeyError, IndexError):
name = {
"name_read": "",
"id": 99999,
"name": event["name"],
"rank": ""
}
else:
name = get_myoji_random()
if event['second_id'] != "":
secondname = get_secondname_by_id(int(event['second_id']))
else:
secondname = get_secondname_random()
result = {
"name": name,
"second_name": secondname,
}
return result
API Gateway設定
求めたレスポンスを返してくれるLambda関数が作成できたので、次はAPIGateway
にてhttpリクエストをトリガーにLambda関数が実行されるよう設定を行います。また、GETパラメータがLambdaへ渡されるようマッピングも行います。
ここはAWSコンソールでの操作が大半になるので、画像がたくさん貼ってあるわかりやすい記事を参考にすることをおすすめします。
参考:API Gateway + Lambda + DynamoDB https://qiita.com/leomaro7/items/314a80b6d91f9e6b4060
フロントエンド(Vue.js)
今回はVue.jsの話はしませんが、やっていることは上記で作成したAPIを叩き、その結果をいい感じに表示にしているだけです。その辺のWEBアプリっぽい感じにはなっているのではないでしょうか。
工夫した点
ちょっとした工夫として、結果をシェアする用のリンクはGETパラメータで結果を保持するようになっているので結果をDBなどに保持しておく必要が無く、カジュアルに(?)シェアして楽しめるようになっています。(そのためのAPI設計でした。)
また、リンクを踏めばカタカナ語の意味を知ることができるということを強調することでいい具合にユーザの興味を引けるのではないかと思います。
†ちょっと尖ったハンネメーカー†で新しい名前を生成しました。
— はま (@ybybybmh) September 3, 2020
今日から私は「アンストッパブル濱本」です!
アンストッパブルってどんな意味?→
https://t.co/yWttq7Zzj2 #ちょっと尖ったハンネメーカー
雑感
API Gateway + Lambda + DynamoDBという構成でAPIの作成、及びそれを利用したSPAアプリということで、初めての構成でしたがそれなりにいい感じのものができたので良かったです。フロントエンドが少しできるようになるとぱっと見がいい感じになるので嬉しいですね。皆さんもぜひ使ってみて、ツイートなどしていただけるととても喜びます。
ここまでお読みいただきありがとうございました!今後も色々勉強して、なにか楽しいものを作っていければと思います。
参考
AWS料金早見表
API Gateway + Lambda + DynamoDB
創作に使えるかもしれない用語集
須﨑のホームページ