Closed8

【開発メモ】英字ニュースから頻出単語を抽出したい

niinii

作りたいもの

英字ニュースサイトから頻出する順に単語を抽出したい

ざっくりとした仕様

英字ニュースサイトから情報を持ってくる

RSSから記事URLを引っ張ってきて、その後スクレイピング

HTML解析

スクレイピング状態だと不要な情報が多いため、本文のみを抽出する
(ここが難しそうな感じする)

単語頻度分析

単語をカウントし、下記情報をDBに登録

  • 記事日時
  • 単語
  • 単語数
  • 記事元ID

いつかは日本語訳や使用されることが多いニュース記事ジャンル等も登録してみたい

niinii

使用記事

使用する記事元は一旦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
niinii

1ヶ月ほど毎日手動実行してみたが、問題なさそうなのでLambda+EventBridgeで自動化してみる
ローカル実行時はファイルに出力していたが、S3に吐くように修正する

1. Lambda関数を作成

アクセス権限でAmazonS3FullAccessを与える
IAM > Roles > 該当Lambdaルール

ライブラリを含める

$ pip3 install --target ./package bs4 
$ pip3 install --target ./package boto3
....

ここでもlxmlのインストール忘れず!

参考: https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html#python-package-create-no-dependencies

つまりポイント
関数ハンドラー名とソース名は一致させる
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html

つまりポイント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?

ここの対応は全てダメだった
https://qiita.com/PND/items/06e1053eeed69ec4f418

解決案
先にxml -> html bs4を使用しないで変換できるか試してみる
xmltodictを使用することで解決。記事を書いた。

https://zenn.dev/nii/articles/bs4-xml-parser-cannot-use-in-lambda

実行できるようになったら、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"をつけると開けるようになった。

niinii

2.EventBridgeの作成

1日に1回クロールしてもらいたいので、cron-based scheduleを作成
cron(00 00 * * ? *)
すぐできた。Eventbridgeは便利だなあ。

5.自動化の設定により不要になった。

niinii

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: 
********

# 返答がない...

インバウンドルールを設定していなかった。。
https://qiita.com/Yado_Tarou/items/553f60e11b5535050468

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
niinii

4. データ格納

S3に格納したファイルから、英語の頻度を抽出してDBに保存するものを作成する。
Python + Lambdaでとりあえず作成。

データ量が多いのでpandasとかでデータ操作した方がいいかも。

やることメモ:

  1. S3 -> Lambda 1日分の英語ファイルを取得する
  2. 1ファイルをスペース、カンマ、コロン等で区切る
  3. 大文字を小文字に変換
  4. pandasで各単語ごとにぶち込む
  5. pandas.countで統計とる
  6. データベースに入れる

3番の大文字を小文字に変換をするときに気になった。
「全ての配列要素に対して小文字化させる」「配列要素が大文字を持っているかを確認し、大文字のものだけ小文字化」であれば、どちらが早いのだろうか...?

↑調べて記事書いた
https://zenn.dev/nii/articles/lower-upper-comparison

LambdaからRDSの通信はどうやる問題

pymysqlを使用して、rdsインバウンドルールをいじればいけた

S3が取れなくなった問題

vpcに入れたせいでタイムアウト
https://valmore.work/aws-lambda-s3/

PandasがLambdaで動かない

DockerでLambda用のビルドイメージをpullして、そこでライブラリを入れる
https://pomblue.hatenablog.com/entry/2021/06/08/230146

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でいいらしい
https://github.com/PyMySQL/PyMySQL/issues/329

niinii

5. 自動化

必ずウェブスクレイピングが終わった後にDB格納バッチが走るようにしたいため、StepFunctionで作成する。また、毎日定期実行して欲しいのでEventBridgeを設定する。

Lambdaへの値の渡し方

ウェブスクレイピングは必ず最新の記事をクロールする為、特に値は不要。
DB格納バッチはproviderとdateの2つの値が必要なので、設定する。

まずEventBridgeのPayloadに必要な引数を設定

eventbridge
{ "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しか参照しないため問題ない。

DB格納バッチinput
{
  "datetime": "2023-09-18T00:00:00Z",
  "provider": "provider",
  "result": {
    "ExecutedVersion": "$LATEST",
    "Payload": null,
        ....
  }
}

この記事が非常にわかりやすい
https://dev.classmethod.jp/articles/stepfunctions-parameters-inter-states/

権限不足

実行してみたらこんなエラーが出た

 (Service: AWSLambda; Status Code: 403; Error Code: AccessDeniedException; Request ID: xxxx)

StepFunctionのIAMを確認し、LambdaInvokeに対象のLambdaが入っているか確認する。追加したところ無事動いた。

iam
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": [
                "arn:aws:lambda:xxxxxxxx:*",
                "arn:aws:lambda:yyyyyyyy:*"
            ]
        }
  ]
}
niinii

これで一旦データ収集部分の実装は完了。

次にやりたいこと

  • 収集したデータを取得できるAPI作成
  • 収集APIを用いたサイト作成
  • 現在BCCのデータしか収集していないため、他サイトからのデータ収集ができるようにする
  • バッチがこけたときのアラート
  • translationテーブルやwordtypeテーブルの利用。翻訳機能や品詞収集機能の実施
このスクラップは2023/09/18にクローズされました