【開発メモ】英字ニュースから頻出単語を抽出したい
作りたいもの
英字ニュースサイトから頻出する順に単語を抽出したい
ざっくりとした仕様
英字ニュースサイトから情報を持ってくる
RSSから記事URLを引っ張ってきて、その後スクレイピング
HTML解析
スクレイピング状態だと不要な情報が多いため、本文のみを抽出する
(ここが難しそうな感じする)
単語頻度分析
単語をカウントし、下記情報をDBに登録
- 記事日時
- 単語
- 単語数
- 記事元ID
いつかは日本語訳や使用されることが多いニュース記事ジャンル等も登録してみたい
使用記事
使用する記事元は一旦BBCとする
RSS : http://feeds.bbci.co.uk/news/rss.xml#
英字ニュースサイトから情報を持ってくる
Pythonで実施、サイトからの取得はrequestsパッケージ、html解析にはPython Beautiful Soup4を使用
詰まったところ
パーサーがない
$ python3 webscraping.py
/Users/nui/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:34: NotOpenSSLWarning: urllib3 v2.0 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
warnings.warn(
Traceback (most recent call last):
File "/Users/nui/Workspaces/other_app/English-frequency/webscraping.py", line 40, in <module>
GetArticleUrlFromRSS("http://feeds.bbci.co.uk/news/rss.xml#")
File "/Users/nui/Workspaces/other_app/English-frequency/webscraping.py", line 9, in GetArticleUrlFromRSS
soup = BeautifulSoup(req.text,"xml")
File "/Users/nui/Library/Python/3.9/lib/python/site-packages/bs4/__init__.py", line 250, in __init__
raise FeatureNotFound(
bs4.FeatureNotFound: Couldn't find a tree builder with the features you requested: xml. Do you need to install a parser library?
別途インストールが必要だった
参考: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser
$ pip3 install lxml
1ヶ月ほど毎日手動実行してみたが、問題なさそうなのでLambda+EventBridgeで自動化してみる
ローカル実行時はファイルに出力していたが、S3に吐くように修正する
1. Lambda関数を作成
アクセス権限でAmazonS3FullAccessを与える
IAM > Roles > 該当Lambdaルール
ライブラリを含める
$ pip3 install --target ./package bs4
$ pip3 install --target ./package boto3
....
ここでもlxmlのインストール忘れず!
つまりポイント
関数ハンドラー名とソース名は一致させる
つまりポイント2
lambdaでbeautifulsoup4使う時、parser周りで詰まる
[ERROR] FeatureNotFound: Couldn’t find a tree builder with the features you requested: lxml. Do you need to install a parser library?
ここの対応は全てダメだった
解決案
先にxml -> html bs4を使用しないで変換できるか試してみる
xmltodictを使用することで解決。記事を書いた。
実行できるようになったら、configuration > general configuration > timeout を3秒から3分に引き上げる。
<値段>
1ms = $0.0000000021なので、3分だと $0.000378 となる。
現在の為替レート($1 = ¥146.276)を使用すると、3分で0.06円程度。
SignatureDoesNotMatchが発生
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
拡張子を指定していないことが原因。".txt"をつけると開けるようになった。
2.EventBridgeの作成
1日に1回クロールしてもらいたいので、cron-based scheduleを作成
cron(00 00 * * ? *)
すぐできた。Eventbridgeは便利だなあ。
5.自動化の設定により不要になった。
3. データベース
3.1 作成
RDS > Create Databaseより、mysqlサーバーを作成。
無料枠で収まるようにした。
無料利用枠は、1 か月あたり 750 インスタンス時間までとなっています。
https://aws.amazon.com/jp/rds/free/faqs/
3.2 アクセスしてみる
EC2からアクセスしようとするも、応答がない
$ mysql -u admin -p -h <hostname>
Enter password:
********
# 返答がない...
インバウンドルールを設定していなかった。。
3.3 テーブル定義を作成する
まず必要なのは、どんな単語があったのかを格納する単語テーブル。
日本語の訳も格納できると良い。
ただし、英語の単語と日本語の訳は1:1ではない。テーブルを分けた方が良いか。
wordテーブル
英単語テーブル
name | type | comment |
---|---|---|
id | int | PK |
word | string | 英単語 |
word_type | int | FK, 品詞。いい英訳がなかった |
translationテーブル
英単語の訳を格納するテーブル
name | type | comment |
---|---|---|
id | int | PK |
word_id | int | FK, word.id |
word_jp | string | 日本語の意味 |
word_type_id | int | FK, 品詞。いい英訳がなかった |
ここで出てきたword_typeは決めうちで良いか。マスタを作成する。
wordtypeマスタ
品詞マスタ
name | type | comment |
---|---|---|
word_type_id | int | PK |
word_type_name | string | 品詞名 |
次に、単語の頻度を測定する。
この際にソース元毎の頻度も測定したいため、ニュース提供元テーブルも作成する必要がある。
providerマスタ
ニュース提供元テーブル
name | type | comment |
---|---|---|
id | string | PK, S3で格納しているproviderid |
site_name | string | サイト名 |
url | string | サイトへのurl、トップページ。ドメイン名でもいいか? |
frequencyテーブル
単語が出現した数を格納するテーブル
name | type | comment |
---|---|---|
id | int | PK |
provider_id | string | FK, provider.id |
word_id | int | FK, word.id |
count | int | 1日に単語が出てきた数 |
date | date | YYYY-MM-DD |
4. データ格納
S3に格納したファイルから、英語の頻度を抽出してDBに保存するものを作成する。
Python + Lambdaでとりあえず作成。
データ量が多いのでpandasとかでデータ操作した方がいいかも。
やることメモ:
- S3 -> Lambda 1日分の英語ファイルを取得する
- 1ファイルをスペース、カンマ、コロン等で区切る
- 大文字を小文字に変換
- pandasで各単語ごとにぶち込む
- pandas.countで統計とる
- データベースに入れる
3番の大文字を小文字に変換をするときに気になった。
「全ての配列要素に対して小文字化させる」「配列要素が大文字を持っているかを確認し、大文字のものだけ小文字化」であれば、どちらが早いのだろうか...?
↑調べて記事書いた
LambdaからRDSの通信はどうやる問題
pymysqlを使用して、rdsインバウンドルールをいじればいけた
S3が取れなくなった問題
vpcに入れたせいでタイムアウト
PandasがLambdaで動かない
DockerでLambda用のビルドイメージをpullして、そこでライブラリを入れる
Python3.11用のビルドイメージがなく、3.9までしかない
Lambdaもそれに合わせて3.9にする
%d動かない問題
%dでintを指定しているのにうごかない。
uncategory_id = int(cur.fetchall()[0][0])
result_count = cur.execute("INSERT INTO word(word,word_type) VALUES ('%s',%s)",(word,uncategory_id))
{
"errorMessage": "%d format: a number is required, not str",
"errorType": "TypeError"
}
intもstrもまとめて%sでいいらしい
5. 自動化
必ずウェブスクレイピングが終わった後にDB格納バッチが走るようにしたいため、StepFunctionで作成する。また、毎日定期実行して欲しいのでEventBridgeを設定する。
Lambdaへの値の渡し方
ウェブスクレイピングは必ず最新の記事をクロールする為、特に値は不要。
DB格納バッチはproviderとdateの2つの値が必要なので、設定する。
まずEventBridgeのPayloadに必要な引数を設定
{ "datetime" : "<aws.scheduler.scheduled-time>", "provider" : "provider" }
これでStepfunctionのInputに上記のJSONが渡る。
次に、ウェブスクレイピングLambdaは上記のJSONを無視し、DB格納バッチLambdaに格納されるようにする。
つまりポイント:
ウェブスクレイピングのResultpathを下記のように書いたら、元々のinputがLambdaの結果に上書きされた
"ResultPath": "$"
下記のように書くことで、元々のJSONにresultという構造が追加された。
"ResultPath": "$.result"
つまり、次のDB格納バッチLambdaに渡される値は下記の通りとなる。
DB格納バッチはdatetime
,provider
しか参照しないため問題ない。
{
"datetime": "2023-09-18T00:00:00Z",
"provider": "provider",
"result": {
"ExecutedVersion": "$LATEST",
"Payload": null,
....
}
}
この記事が非常にわかりやすい
権限不足
実行してみたらこんなエラーが出た
(Service: AWSLambda; Status Code: 403; Error Code: AccessDeniedException; Request ID: xxxx)
StepFunctionのIAMを確認し、LambdaInvokeに対象のLambdaが入っているか確認する。追加したところ無事動いた。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:xxxxxxxx:*",
"arn:aws:lambda:yyyyyyyy:*"
]
}
]
}
これで一旦データ収集部分の実装は完了。
次にやりたいこと
- 収集したデータを取得できるAPI作成
- 収集APIを用いたサイト作成
- 現在BCCのデータしか収集していないため、他サイトからのデータ収集ができるようにする
- バッチがこけたときのアラート
- translationテーブルやwordtypeテーブルの利用。翻訳機能や品詞収集機能の実施