はじめに
RSS対応のサイトだと、更新情報追いやすいけど、RSS非対応のページも追いたいよね。って人向けの記事です。
RSS対応しているサイトなら、RSSリーダーを使った方が早いです
また、Discordのチャンネルにも通知がしたかったので、メールとDiscord両方に通知を行っています。
Discord側にWebhook用のURLが必要ですが、本記事では紹介しません
参考サイトのZennの記事が細かく書かれていますので、そちらをご覧ください
なお、この仕組みは更新を検知したいサイトに確認リクエストを送ります。
高頻度で設定してしまうと、サーバーに負荷がかかる為、
高頻度での設定はしないようにお願いします
参考サイト
構成図
コードについて(Lambda)
コードについては、基本的に、クラスメソッドさんの記事を参考にしています
Discordの通知部分については、AmazonBedrock (claude3 sonnet)で、調整しながらコーディングをしています
おかしなところがあったら、教えてください(笑)
また、バージニア指定となっています。
他リージョンでの展開する場合は、SNSのARNとLambdaのlayer部分の調整が必要になりますので、ご注意ください
import json
import hashlib
import requests
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
sns = boto3.client('sns')
TABLE_NAME = 'website-monitor'
SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456781234:website-monitor'
URLS = ["https://xxx","https://yyy","https://zzz",]
def lambda_handler(event, context):
changes_detected = []
for URL in URLS:
# URLからコンテンツを取得
response = requests.get(URL)
content = response.text
# コンテンツをハッシュ化
current_hash = hashlib.sha256(content.encode()).hexdigest()
# DynamoDBから前回のハッシュを取得
table = dynamodb.Table(TABLE_NAME)
try:
response = table.get_item(Key={'URL': URL})
previous_hash = response['Item']['hash'] if 'Item' in response else None
except ClientError as e:
print(e.response['Error']['Message'])
previous_hash = None
# ハッシュを比較
print("Hashed:", current_hash, previous_hash)
if current_hash != previous_hash:
url = "https://discord.com/api/webhooks/1234567890123456789/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # URLを入力する
# response = requests.post(url, data=json.dumps(payload), headers=headers)
MESSAGE_CONTENT = f"ページが更新されています。\n確認しましょう。\n{URL}"
REQUEST_BODY = {"content": MESSAGE_CONTENT}
response = requests.post(url, json=REQUEST_BODY)
if response.status_code == 204:
print("Webhook送信に成功しました。")
else:
print(f"Webhook送信に失敗しました。ステータスコード: {response.status_code}")
# ハッシュが異なる場合は、SNSに通知
message = f'Website content at {URL} has changed.'
sns.publish(TopicArn=SNS_TOPIC_ARN, Message=message)
# 新しいハッシュをDynamoDBに保存
table.put_item(Item={'URL': URL, 'hash': current_hash})
changes_detected.append(URL)
if changes_detected:
return {
'statusCode': 200,
'body': json.dumps(f'Changes detected in: {", ".join(changes_detected)}; notifications sent.')
}
else:
# ハッシュが同じ場合は何もしない
return {
'statusCode': 200,
'body': json.dumps('No change in website content for all URLs.')
}
IaC化してみた
簡単に展開できるよう、AWS CloudFormationに落とし込んでみた
AWSTemplateFormatVersion: '2010-09-09'
Description: Website monitoring Lambda function, DynamoDB table, and SNS topic
Parameters:
NameTag:
Type: String
Description: 'Common resource name'
Default: 'website-monitor'
EmailAddress:
Type: String
Description: 'your email address'
Default: 'xxx@xxx.yyy.zzz'
DiscordWebhookURL:
Type: String
Description: 'Discord Webhook URL'
Default: 'https://discord.com/api/webhooks/1234567890123456789/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
ScheduleInterval:
Type: String
Description: 'Crawl interval'
Default: 'rate(12 hours)'
MonitoringURLs:
Type: String
Description: 'URL to monitor (multiple settings possible)'
Default: '"https://xxx","https://yyy","https://zzz"'
Resources:
WebsiteMonitoringFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${NameTag}-lambda'
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: !Sub |
import json
import hashlib
import requests
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
sns = boto3.client('sns')
TABLE_NAME = '${NameTag}'
SNS_TOPIC_ARN = '${WebsiteMonitorTopic}'
URLS = [${MonitoringURLs},]
def lambda_handler(event, context):
changes_detected = []
for URL in URLS:
# URLからコンテンツを取得
response = requests.get(URL)
content = response.text
# コンテンツをハッシュ化
current_hash = hashlib.sha256(content.encode()).hexdigest()
# DynamoDBから前回のハッシュを取得
table = dynamodb.Table(TABLE_NAME)
try:
response = table.get_item(Key={'URL': URL})
previous_hash = response['Item']['hash'] if 'Item' in response else None
except ClientError as e:
print(e.response['Error']['Message'])
previous_hash = None
# ハッシュを比較
print("Hashed:", current_hash, previous_hash)
if current_hash != previous_hash:
url = "${DiscordWebhookURL}" # URLを入力する
# response = requests.post(url, data=json.dumps(payload), headers=headers)
MESSAGE_CONTENT = f"ページが更新されています。\n確認しましょう。\n{URL}"
REQUEST_BODY = {"content": MESSAGE_CONTENT}
response = requests.post(url, json=REQUEST_BODY)
if response.status_code == 204:
print("Webhook送信に成功しました。")
else:
print(f"Webhook送信に失敗しました。ステータスコード: {response.status_code}")
# ハッシュが異なる場合は、SNSに通知
message = f'Website content at {URL} has changed.'
sns.publish(TopicArn=SNS_TOPIC_ARN, Message=message)
# 新しいハッシュをDynamoDBに保存
table.put_item(Item={'URL': URL, 'hash': current_hash})
changes_detected.append(URL)
if changes_detected:
return {
'statusCode': 200,
'body': json.dumps(f'Changes detected in: {", ".join(changes_detected)}; notifications sent.')
}
else:
# ハッシュが同じ場合は何もしない
return {
'statusCode': 200,
'body': json.dumps('No change in website content for all URLs.')
}
Layers:
- arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p312-requests:3
MemorySize: 512
Timeout: 300
Architectures:
- arm64
Tags:
- Key: Name
Value: !Ref NameTag
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: DynamoDBAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'dynamodb:GetItem'
- 'dynamodb:PutItem'
Resource: !GetAtt WebsiteHashesTable.Arn
- PolicyName: SNSPublishAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'sns:Publish'
Resource: !Ref WebsiteMonitorTopic
Tags:
- Key: Name
Value: !Ref NameTag
WebsiteHashesTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: URL
AttributeType: S
KeySchema:
- AttributeName: URL
KeyType: HASH
BillingMode: PAY_PER_REQUEST
TableName: !Ref NameTag
Tags:
- Key: Name
Value: !Ref NameTag
WebsiteMonitorTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Ref NameTag
DisplayName: !Ref NameTag
Tags:
- Key: Name
Value: !Ref NameTag
EmailSubscription:
Type: AWS::SNS::Subscription
Properties:
Endpoint: !Ref EmailAddress
Protocol: email
TopicArn: !Ref WebsiteMonitorTopic
ScheduledRule:
Type: AWS::Events::Rule
Properties:
Description: Scheduled rule to run every 12 hours
ScheduleExpression: !Ref ScheduleInterval
State: ENABLED
Targets:
- Id: WebsiteMonitoringFunction
Arn: !GetAtt WebsiteMonitoringFunction.Arn
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt WebsiteMonitoringFunction.Arn
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt ScheduledRule.Arn
Outputs:
LambdaFunctionName:
Description: Name of the Lambda function
Value: !Ref WebsiteMonitoringFunction
Export:
Name: !Sub '${NameTag}-lambda-function-name'
DynamoDBTableName:
Description: Name of the DynamoDB table
Value: !Ref WebsiteHashesTable
Export:
Name: !Sub '${NameTag}-dynamodb-table-name'
SNSTopicArn:
Description: ARN of the SNS topic
Value: !Ref WebsiteMonitorTopic
Export:
Name: !Sub '${NameTag}-sns-topic-arn'
パラメータについて
EmailAddress
→更新されたことを通知するメールアドレス
DiscordWebhookURL
→発行したDiscordWebhookURL
ScheduleInterval
→更新確認頻度(デフォルトでは12時間としてます)
MonitoringURLs
→更新情報をキャッチアップしたいURL(複数設定可)
最後に
毎日新しい情報が出る時代です
効率よく情報収集していきましょう!