AWS Lambdaで自作RSSリーダー開発

# はじめに
こちらは

- [Alexa×BedrockでAWS最新ニュースキャッチアップアプリ コンセプト・設計](https://qiita.com/SawaShuya/private/a22645e54d75667c5148)
- **AWS Lambdaで自作RSSリーダー開発 (本記事)**
- [Self hosted AWS LambdaによるAlexaスキル開発](https://qiita.com/SawaShuya/private/2395a08f57c8988f3163)
- [AlexaでBedrockに質問をするスキル開発](https://qiita.com/SawaShuya/private/46c47068dd2573359c8b)

の連載記事です。本記事では以下のアーキテクチャの作成を目指して解説をしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/bca51a92-e2aa-7dc0-f93f-f5daed47ba18.png)

内容としては以下になりますので、部分的に活用することも可能です。
- RSS (Rich Site Summary)
- RSSリーダーの実装 (Python)
- Amazon Translateを使用した翻訳
- Amazon DynamoDBへの格納
- DynamoDBでのTTL (Time To Live)設定


# RRSとは
RSSとはwebサイトの新着・更新情報を配信する技術を指します。Webページ内の見出しや本文の要約、更新情報等をXMLベースのフォーマットに従って配信されており、ユーザーは効率的にサイトの情報を取得できるようになります。

https://www.rssboard.org/rss-specification

AWSが公式に提供しているブログやニュースなどもRSSに対応しています。
例えば[AWS NEWS](https://aws.amazon.com/new/)の場合、トップページにはRSSへのリンクが添付されており、``<item>...</item>``に記事の内容が記載されます。こちらが随時更新されるので、定期的に取得をすることで最新情報を取得することが可能になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/49664a2c-e74d-a316-4c62-62ec7a6d9ae2.png)

```xml:AWS NEWS RSS (抜粋)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<rss version="2.0">
  <channel>
      :
      :
    <item>
      <link>https://aws.amazon.com/about-aws/whats-new/2023/11/aws-iot-sitewise-supports-asset-model-components/</link>
      <guid isPermaLink="false">72911903d948cd8c25e6dd58be997b727e7b1c20</guid>
      <title>AWS IoT SiteWise now supports Asset model components</title> 
      <description>&lt;p&gt;AWS IoT SiteWise now supports Asset model components to help industrial customers create reusable components to derive new asset models and express variations easily. &lt;/p&gt;</description>
      <pubDate>Wed, 22 Nov 2023 19:06:00 +0000</pubDate>
      <category>general:products/aws-iot-sitewise,marketing:marchitecture/internet-of-things,general:products/aws-govcloud-us</category>
      <author>aws@amazon.com</author>
    </item>
    <item>
    :
    </item>
    :
  </channel>
</rss>
```

AWS関連のRSSは以下の記事にわかりやすくまとまっていたので、ご参考ください。
https://dev.classmethod.jp/articles/aws-rss-feeds/

# 開発
## AWS Lambda構築
**Lambda レイヤー**
- [feedparser](https://pypi.org/project/feedparser/)
    - RSSフィードを扱いやすくするために使用

**Lambdaコードのデプロイ**
<details><summary>コード全行</summary><div>

```python:lambda_function.py
import re
import feedparser
import boto3
from datetime import datetime
from dateutil import tz
from dateutil import parser

# DynamoDB
# boto3.resourceは非推奨のためclientを使用
dynamodb = boto3.client('dynamodb')
TABLE_NAME = 'rss_feeds'
# 何日後にDynamoDBから削除するのか設定
day = 3
TTL = 60 * 60 * 24 * day

# Amazon translate
translate = boto3.client('translate')
SRC_LANG = 'en'
TRG_LANG = 'ja'

# timezone
timezone = tz.gettz('Asia/Tokyo')

# RSS URL
RSS_URL = 'https://aws.amazon.com/jp/blogs/aws/feed/'

def lambda_handler(event, context):
    # RSS取得
    data = feedparser.parse(RSS_URL)

    # 1件ずつitemを取得
    for entry in data.entries:
        title = entry.title
        link = entry.link
        # descriptionにはHTMLタグが含まれるので削除
        description = remove_http_tag(entry.description)
        # DynamoDBのTTLで使用するため、unixtimeも算出
        published, unixtime =  get_time_info(entry.published)
        limit_unixtime = unixtime + TTL
        category = entry.category
        
        # 既存判断。既存の場合はスキップ
        if not is_item_exist(link):
            # タイトルと要約はAmazon Translateで翻訳
            ja_title = get_translate_text(title)
            ja_description = get_translate_text(description)
            # DynamoDBへ格納
            put_item(title, link, description, published, category, ja_title, ja_description, limit_unixtime)
            

def remove_http_tag(str):
    return re.sub(re.compile('<.*?>'), '', str)
    

def is_item_exist(link) -> bool:
    options = {
        'TableName': TABLE_NAME,
        'Key': {
            'link': {'S': link},
        }
    }
    
    try:
        response = dynamodb.get_item(**options)
        response['Item']
        print("The Item Already Exist")
        return True
    except KeyError:
        print("New Item : " + link)
        return False

def put_item(title, link, description, published, category, ja_title, ja_description, limit_unixtime):
    options = {
        'TableName': TABLE_NAME,
        'Item': {
            'title': {'S': title},
            'link': {'S': link},
            'description': {'S': description},
            'published': {'S': published},
            'category': {'S': category},
            'ja_title': {'S': ja_title},
            'ja_description': {'S': ja_description},
            'limit_unixtime': {'N': str(limit_unixtime)}
        }
    }
    # DynamoDB格納
    dynamodb.put_item(**options)
    
def get_translate_text(text):
    # 英語→日本語訳
    response = translate.translate_text(
        Text=text,
        SourceLanguageCode=SRC_LANG,
        TargetLanguageCode=TRG_LANG
    )

    return response.get('TranslatedText')
    
def get_time_info(rss_pub_date):
    # datetimeに変換
    datetime = parser.parse(rss_pub_date).astimezone(timezone)
    
    # iso8601に変換
    iso_datetime = datetime.isoformat()
    
    # unixtime取得
    unixtime = datetime.timestamp()
    
    return iso_datetime, unixtime
    
```
</div></details>

**タイムアウト設定**
Lambdaのデフォルトは3秒であり、今回の処理は3秒を上回ることが予想されるため1分程度にします
> cf. 検証ではRSS100件-20秒程度で実行された


**ロール設定**
以下のポリシーを設定する。
- Lambda作成時にデフォルトで作成されるポリシー
- TranslateReadOnly (マネージドポリシー)
- DynamoDB用のポリシー
```json:dynamodb-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:GetItem"
            ],
            "Resource": "arn:aws:dynamodb:<YOUR REGION>:<ACCOUNT ID>:table/rss_feeds"
        }
    ]
}
```



## EventBridge設定
日次で実行できるようにLambdaのトリガ設定から、EventBridgeを設定します。毎日実行する場合にはcron式 ``(0 15 * * ? *)`` を設定します。(UTCであることに注意)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/e79ffab9-8a23-e436-a129-00538731b70a.png)

## DynamoDB設定
**テーブルの準備**
Lambdaのコードに合わせて、``rss_feeds``テーブルを準備しておきます。
パーティションキーはlinkと設定しました。また、日次でしか動かないのでオンデマンドキャパシティモードを選択しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/cfdec47e-7174-4240-6568-04d8585894c3.png)


**TTL設定**
TTLとはDynamoDBで古くなったデータを自動で削除する機能です。
TTLはunixtimeの属性に対して有効にすることができ、DynamoDBはその属性値を監視し続けます。DynamoDBは現在時刻のunixtimeを常に持っており、その値と比較して対象カラムのunixtimeが小さい時、そのデータを削除対象として削除します。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/howitworks-ttl.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/c9310136-7024-0ed1-9d85-b7ead463ebb7.png)

Lambdaのコードに合わせて``limit_unixtime``項目に対してTTLを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/9549be55-249e-11f0-7976-792df6e73d55.png)


設定が完了すると以下のようなDynamoDBテーブルが出来上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918398/430ee250-0024-f83f-85eb-70a70588caf4.png)



## Amazon translate 設定
特に必要がないです。

# おわりに
Eventbridgeで設定した時刻でDynamoDBに最新情報が格納されました。
本記事ではAWS最新ニュースを取得するアプリケーションを構築しましたが、RSSに対応する記事であればどの記事でも対応可能です。また、部分的な技術も他に活用できますので、ぜひご参考ください。