目次
モチベーション
僕はFeadlyやはてなブックマークなどで情報取集していて、
気になる記事とかはNotion Web Clipperでクリップして、BookMark用のデータベースに登録する運用をしています。
度々、見逃したり埋もれたりするので、自分が確認したいサイトについては、最初からNotionに自動登録して、
あとから確認できるようにし、登録された記事を確認して、BookMarkとして振り分ける運用にしたいと思って作ってみました。
成果物
- RSSから取得した記事のタイトルと記事URLを下記のようにデータベースに登録されます。
- ページ内には、取得元の記事内容が登録されます。
構成図
- EventBrideで1時間毎に、Lambdaを起動する
- LambdaがAWS Systems ManagerからRSS情報を取得する
- 取得したRSSから前回取得した記事以降に、更新された記事タイトルと記事URLを取得する
- 取得した記事URLにアクセスして、スクレイピングを行う
- AWS Sercrets ManagerからNotionの接続情報を取得する
- 記事タイトル・記事URL・記事内容・記事のタグをNotionに送信
- Notionに記事が登録される
使うための準備
AWS SAM
当機能では、AWS SAMで構築しています。
SAM CLIを使用するには、次のツールをインストールしておく必要があります。
- SAM CLI - Install the SAM CLI
- Python 3 installed
- Docker - Install Docker community edition
Notion integrationを作成
Notion APIでNotionのデータベースに記事を登録するには、インテグレーションを作成する必要があります。
下記リンクにアクセスし、「New Integration」からインテグレーションを作成します。
インテグレーションを作成したらInternal Integration Tokenをコピーして控えてください。
後程、AWS Secrets Managerに登録する際に必要になります。
https://www.notion.so/my-integrations
ワークスペースにインテグレーションを招待
Notionページにインテグレーションを招待する必要があります。
招待することで、Notion APIと連携が可能になります。
データベースIDを取得する
当機能は、データベースに書き込みを行いますので、データベースIDを取得する必要があります。
ページ内に作成したデータベースしたら共有でリンクをコピーしてください。
URLの「?v」の前にある文字列部分がデータベースIDになりますので、控えてください。
https://www.notion.so/{workspace_name}/{database_id}?v={view_id}
後程、AWS Secrets Managerに登録する際に必要になります。
AWS System Managerの設定
下記を作成する
- パラメータ名:RSSURLList
- 利用枠:標準
- タイプ:文字列
- データ型:text
- 値 (下記json参照)
- url:記事の取得先URL
- tag:Notionに記事登録時のタグ名称
{
"rsslist": [
{
"url": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
"tag": "AWS",
"name":"AWSの最新情報"
},
{
"url": "https://aws.amazon.com/jp/blogs/news/feed/",
"tag": "AWS",
"name":"Amazon Web Services ブログ"
},
{
"url": "https://qiita.com/tags/aws/feed",
"tag": "AWS",
"name":"Qiita - AWS"
},
{
"url": "https://qiita.com/tags/docker/feed",
"tag": "Docker",
"name":"Qiita - Docker"
},
{
"url": "https://qiita.com/tags/python/feed",
"tag": "Python",
"name":"Qiita - Python"
},
{
"url": "https://dev.classmethod.jp/feed/",
"tag": "技術ブログ",
"name":"クラスメソッド"
},
{
"url": "https://zenn.dev/feed",
"tag": "技術ブログ",
"name":"Zenn - トレンド"
},
{
"url": "https://zenn.dev/topics/aws/feed",
"tag": "AWS",
"name":"Zenn - AWS"
},
{
"url": "https://zenn.dev/topics/docker/feed",
"tag": "Docker",
"name":"Zenn - Docker"
},
{
"url": "https://zenn.dev/topics/python/feed",
"tag": "Python",
"name":"Zenn - Python"
}
]
}
Secret Manegerの設定
- シークレットのタイプ:その他のシークレットのタイプ
- キー/値のペア
- NOTION_TOKEN:Integration Token
- DATABASE_ID:データベースID
- 暗号化キー:DefaultEncryptionKey
実装の解説
ソースコードはこちら
RSS取得
RSS取得と解析は、下記の記事を参考にしました。
AWS System ManagerからRSS取得先を取得します。
def get_target_url() -> List[str]:
"""
取得対象にするrssのURLを返却する
"""
region_name = "ap-northeast-1"
ssm = boto3.client('ssm',region_name=region_name)
url_param: str = ssm.get_parameter(
Name='RSSURLList'
)['Parameter']['Value']
json_dict = json.loads(url_param)
return json_dict
feedparserを使い記事を解析します。
現在時刻と記事の公開日を比較し、1時間経過した記事を取得対象としています。
def get_rss(endpoint: str, tag: str, interval: int = 60) -> List[RssContent]:
"""
rssのxmlを返すendpoint(url)からrss情報を取得し、必要な情報だけ抜き出す
interval分以内の記事だけを返す。定期実行はinterval分と同じ間隔にすればよい
intervalを負数にすると全記事返す(デバッグ用)
"""
nowtime = datetime.now(timezone(timedelta(hours=+9), 'JST'))
feed = feedparser.parse(endpoint)
rss_list: List[RssContent] = []
for entry in feed.entries:
if not entry.get("link"):
continue
published = convert_time(entry.published_parsed)
if (nowtime - published).total_seconds() // 60 <= interval or interval < 0:
rss_content = RssContent(
title=entry.title,
url=entry.link,
tag=tag,
published_date=published
)
rss_list.append(rss_content)
return rss_list
シークレットの取得
Secret Manegerからの取得ロジックについては、下記の記事を参考にしました。
取得したいシークレットキーを引数に指定することで、シークレットの値を取得しています。
import boto3
import base64
from botocore.exceptions import ClientError
import ast
def get_secrets_manager_key_value(secret_name: str, secret_key: str) -> str:
"""AWS Secrets Managerからシークレットキーの値を取得する."""
value = ''
secrets_dict = get_secrets_manager_dict(secret_name)
if secrets_dict:
if secret_key in secrets_dict:
# secrets_dictが設定されていてsecret_keyがキーとして存在する場合
value = secrets_dict[secret_key]
else:
print('シークレットキーの値取得失敗:シークレットの名前={}、シークレットキー={}'.format(secret_name, secret_key))
return value
def get_secrets_manager_dict(secret_name: str) -> dict:
"""Secrets Managerからシークレットのセットを辞書型で取得する"""
region_name = "ap-northeast-1"
secrets_dict = {}
if not secret_name:
print('シークレットの名前未設定')
else:
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
print('シークレット取得失敗:シークレットの名前={}'.format(secret_name))
print(e.response['Error'])
else:
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
else:
secret = base64.b64decode(get_secret_value_response['SecretBinary'])
secrets_dict = ast.literal_eval(secret)
return secrets_dict
Notion登録
PythonでNotion APIを扱うため、「Notion SDK for Python」と
Notion登録前に記事の内容をスクレイピングするため、「BeautifulSoup」を利用しています。
targetTagsに設定されている値がスクレイピングの対象になります。
対象のタグにあったNotionのリクエストBodyを作り上げ、リクエストしています。
NotionへのリクエストBodyは、公式リファレンスを参照
import os
import traceback
from typing import List
from get_secrets import get_secrets_manager_key_value
from bs4 import BeautifulSoup
import requests
from notion_client import Client, APIResponseError
import urllib.request, urllib.error
def register_notion(rss_list:dict) -> None:
"""
Notionの指定したデータベースに記事のタイトル、タグ、URL、記事内容を登録する
"""
try:
## AWS Secrets Managerに設定しているNotionのシークレットを取得する
notion_token = get_secrets_manager_key_value('notion_rss', 'NOTION_TOKEN')
## AWS Secrets Managerに設定しているNotionのシークレットを取得する
database_id = get_secrets_manager_key_value('notion_rss', 'DATABASE_ID')
## 認証を行う
notion = Client(auth=notion_token)
## 取得したRSS数文登録を行う
for rss in rss_list:
block = []
if not checkURL(rss.url):
continue
soup = BeautifulSoup(requests.get(rss.url).content, 'html.parser')
articleTag = soup.find_all("div", {"class": "content"})
if not articleTag:
articleTag = soup.find_all(['section','article'])
for article in articleTag:
targetTags = article.find_all(['h1','h2','h3','p','span','img','li','pre','blockquote'])
for tag in targetTags:
if tag.name == 'h1':
if tag.get_text(strip=True):
block.append(append_message("heading_1",tag.get_text(strip=True)))
if tag.name == 'h2':
if tag.get_text(strip=True):
block.append(append_message("heading_2",tag.get_text(strip=True)))
if tag.name == 'h3':
if tag.get_text(strip=True):
block.append(append_message("heading_3",tag.get_text(strip=True)))
if tag.name == 'li':
if tag.get_text(strip=True):
block.append(append_message("bulleted_list_item",tag.get_text(strip=True)))
if tag.name == 'p':
if tag.parent.name == 'blockquote':
continue
if tag.get_text(strip=True):
block.append(append_message("paragraph",tag.get_text(strip=True)))
if tag.name == 'img':
if checkURL(tag['src']) and checkExtension(tag['src']):
block.append(append_image(tag['src']))
if tag.name == 'pre':
if tag.get_text(strip=True):
block.append(append_code(tag.get_text(strip=True)))
if tag.name == 'blockquote':
if tag.get_text(strip=True):
block.append(append_message("quote",tag.get_text(strip=True)))
## Notionに登録を行う
notion.pages.create(
parent={'database_id': database_id},
properties=property_data(rss),
children=block
)
Lambda・ポリシー・ロール
起動時間とポリシーとロールは、template.yamlに記述しています。
NotionRegisterFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: "lambda-notion-rss-register"
CodeUri: ./lambda_notion_rss_register
Handler: app.lambda_handler
Runtime: python3.8
Architectures:
- x86_64
Role: !GetAtt NotionRegisterFunctionRole.Arn
Events:
RSSGetSchedule:
Type: Schedule
Properties:
Schedule: rate(1 hour) # 1時間毎
Input: |
{
"region": "tokyo"
}
NotionRegisterFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "sts:AssumeRole"
Principal:
Service: lambda.amazonaws.com
ManagedPolicyArns:
- !Ref NotionRegisterFunctionPolicy
NotionRegisterFunctionPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "logs:CreateLogStream"
- "logs:CreateLogGroup"
- "logs:PutLogEvents"
Resource: "arn:aws:logs:ap-northeast-1:*:*"
- Effect: "Allow"
Action:
- "secretsmanager:GetSecretValue"
- "ssm:GetParameters"
- "ssm:GetParameter"
Resource: "*"
ビルド・実行・デプロイ
コマンド
コマンドは下記
sam build
sam local invoke NotionRegisterFunction --event events/event.json
sam deploy --guided