SERPREP通知第1部分:搜索引擎结果页面(SERP)
#aws #python #seo #slack

Serply Notifications

什么是Serply通知?

Serply通知是Serply API的开源通知调度程序。它允许您安排和接收Slack帐户上的Google搜索引擎结果页面(SERP)的通知。

该应用程序是通过无AWS服务器服务开发的:API网关,Lambda,DynamoDB和EventBridge。其所有资源都定义为云开发套件堆栈。这是其所有组件的详细演练。首先,让我们从用户的角度看应用程序功能。

作为一个松弛用户,我可以...

  • 计划特定搜索查询的SERP通知。

  • 每天,每周或每月接收通知。

  • 列表通知时间表。

  • 禁用并重新启用计划的通知。

您如何跟踪网站的SERP位置?

要跟踪网站在Google搜索引擎结果页面(SERP)中的位置,您可以使用一些方法:

  1. 手动搜索:您可以手动搜索与您的网站相关的特定关键字,并在SERP中注明网站的位置。

  2. SERP跟踪工具:有多种在线工具可以跟踪您网站在SERP中的位置。一些流行的工具包括Ahrefs,Semrush,Moz和Serpstat。

  3. Google搜索控制台:这是Google提供的免费工具,可让您在Google搜索中跟踪网站的性能。它提供有关您网站的关键字的信息以及每个关键字的网站位置。

无论您使用哪种方法,都必须定期跟踪网站的位置以监视其性能并进行必要的调整以提高其在SERP中的排名。

示例手动搜索

https://www.google.com/search?q=professional+network&num=100&domain=linkedin.com

Search Engine Result Pages

松弛 /serply命令

命令结构真的很简单。它从斜线命令 /serply开始,然后是子命令和特定参数。有两个子命令:SERP和列表。

|命令|子命令|网站/域|查询|间隔|
| /serply | serp | linkedin.com | “专业+网络” |每日|

/serply SERP-安排SERP通知

使用搜索查询“ Professional+Network”,SERP子命令为LinkedIn.com域创建了计划的通知。输入命令后不久,您将看到带有时间表详细信息的确认消息。您将收到指定间隔的通知:每日,每周或每月。

Slack /serply command

/serply列表 - 列出所有计划的通知

列表子命令返回所有时间表的列表。

禁用计划的通知

您可以通过单击给定的松弛消息上的禁用按钮来禁用计划的通知。通知历史记录将保留在数据库中。您也可以通过单击启用按钮来重新启用它。

应用设计

让我们从请求模式开始

由于这是事件驱动的应用程序,因此在编码之前计划所有请求模式将非常有帮助。您不需要代码,因为您只能克隆SERPLEN NOTICATIONS存储库。但是,我确实想介绍设计此应用程序的思考过程。简单地说,这是请求的顺序。

用户输入 /serply命令... < /p>

  1. Slack将通过消息有效载荷执行我们的应用程序Webhook。

  2. Webhook将使用200个状态代码响应Slack,确认收到请求。

  3. 后端将从slash命令字符串中解析有效载荷和参数。

  4. 后端将将有效载荷转换为计划对象并将其发送到事件总线。

  5. 函数将接收该事件并执行2个任务:

  6. 另一个功能将向Slack发送确认消息,让用户知道时间表已创建。

  7. 时间表将调用执行3个任务的另一个功能:

  8. 另一个功能将从事件总线接收SERP数据,并将通知发送回Slack以供用户查看。

DynamoDB:计划访问模式

我们还应该预测如何建模,索引和查询数据。让我们列出DynamoDB访问模式的列表以及我们将用来适应它们的查询条件。

|访问模式|查询条件|
|通过ID获取时间表| pk = schedule _ [属性],sk = schedule_ [属性] |
|通过ID获取通知| pk = schedule _ [属性],sk = notification_ [属性]#[timestamp] |
|按时间表查询通知|查询表pk = schedule_ [属性],sk = begins_with notification_ |
|查询时间表按帐户| QUERY COLLUCTYINDEXEX GSI COLLECTION = [account] #schedule,sk = begins_with schedule_ |

邻接列表设计模式

当应用程序的不同实体之间有多一关系的关系时,可以将关系建模为邻接列表。在此模式中,使用分区密钥表示所有顶级实体(图中的节点的代名词)。与其他实体(图中的边缘)的任何关系通过设置目标实体ID(目标节点)的排序键的值表示为分区中的项目。

此模式的优点包括最小数据复制和简化查询模式,以查找与目标实体相关的所有实体(节点)(对目标节点具有边缘)。

〜Amazon DynamoDB开发人员指南

主钥匙,排序钥匙设计

除了请求和DynamoDB访问模式外,我们还需要考虑其他服务约束。

  • 将Slack命令映射到计划数据库密钥。

  • 计划主键是附表_。

  • 的串联属性
  • 时间表排序键有意与计划主键相同。

  • SERP通知类似于带有Notification_的计划主键,并带有ISO 8601 DateTime String。

  • 将附表数据库键映射到EventBridge计划名称。

  • EventBridge计划名称的最大限制为64个字符。

  • Slack命令字符串和数据库密钥属性可能超过64个字符。

  • 从eventbridge计划名称的数据库密钥中生成确定性哈希,该名称始终为64个字符。

示例键

Slack命令将始终产生相同的键。 EventBridge计划名称哈希是从附表PK生成的。

|命令| /serply serp linkedin.com“专业+网络” |
|计划PK | schedue_serp#linkedin.com#专业+网络#每日|
|计划SK | schedue_serp#linkedin.com#专业+网络#每日|
|计划收集| F492A0AFBEF84B5B8E4FEDEB635A7737#计划|
| SERP通知PK | schedue_serp#linkedin.com#专业+网络#每日|
| SERP Notification SK | notification_serp#linkedin.com#专业+网络#每日#2023-02-04T07:21:04 |
| SERP集合| F492A0AFBEF84B5B8E4FEDEB635A7737#SERP |
| EventBridge计划名称| D8F9C6FE6ED8A3049C7F8900298D981E58EDACFB0C28B5ECA4869F2F2F16BA92C0 |

eventbridge计划API参考:名称长度约束

EventBridge Schedule Name Length Constraint

准备多租户:全球次要索引

在这种情况下,创建全局辅助索引的主要原因是,当我们输入 /serply List命令时,我们需要查询给定Slack帐户的所有时间表。该指数的另一个好处是,如果以后需要,我们可以为此应用程序开发多租户。我们可以为每个Slack帐户范围范围。目前,只有一个默认帐户ID。

Serplystack

所有AWS资源都是在SerplyStack中定义的。当我们运行 CDK部署命令时,CDK将应用程序的基础架构作为一组AWS云形式模板,然后使用这些模板在指定的AWS帐户和区域中创建或更新云形式堆栈。在部署期间,该命令设置了CDK应用程序代码中定义的所需AWS资源,并配置了资源之间的任何必要连接。结果是在目标AWS环境中的功能齐全,部署的应用程序。

# src/cdk/serply_stack.py

class SerplyStack(Stack):

    def __init__ (self, scope: Construct, construct_id: str, config: SerplyConfig, **kwargs) -> None:
        super(). __init__ (scope, construct_id, **kwargs)

        RUNTIME = _lambda.Runtime.PYTHON_3_9

        lambda_layer = _lambda.LayerVersion(
            self, f'{config.STACK_NAME}LambdaLayer{config.STAGE_SUFFIX}',
            code=_lambda.Code.from_asset(config.LAYER_DIR),
            compatible_runtimes=[RUNTIME],
            compatible_architectures=[
                _lambda.Architecture.X86_64,
                _lambda.Architecture.ARM_64,
            ]
        )

        event_bus = events.EventBus(
            self, config.EVENT_BUS_NAME,
            event_bus_name=config.EVENT_BUS_NAME,
        )

        event_bus.apply_removal_policy(RemovalPolicy.DESTROY)

        scheduler_managed_policy = iam.ManagedPolicy.from_aws_managed_policy_name(
            'AmazonEventBridgeSchedulerFullAccess'
        )

        scheduler_role = iam.Role(
            self, 'SerplySchedulerRole',
            assumed_by=iam.ServicePrincipal('scheduler.amazonaws.com'),
        )

        scheduler_role.add_to_policy(
            iam.PolicyStatement(
                effect=iam.Effect.ALLOW,
                actions=['lambda:InvokeFunction'],
                resources=['*'],
            )
        )

        slack_receive_lambda = _lambda.Function(
            self, 'SlackReceiveLambdaFunction',
            runtime=RUNTIME,
            code=_lambda.Code.from_asset(config.SLACK_DIR),
            handler='slack_receive_lambda.handler',
            timeout=Duration.seconds(5),
            layers=[lambda_layer],
            environment={
                'STACK_NAME': config.STACK_NAME,
                'STAGE': config.STAGE,
            },
        )

        event_bus.grant_put_events_to(slack_receive_lambda)

        slack_respond_lambda = _lambda.Function(
            self, 'SlackRespondLambdaFunction',
            runtime=RUNTIME,
            code=_lambda.Code.from_asset(config.SLACK_DIR),
            handler='slack_respond_lambda.handler',
            timeout=Duration.seconds(5),
            layers=[lambda_layer],
            environment={
                'SLACK_BOT_TOKEN': config.SLACK_BOT_TOKEN,
                'STACK_NAME': config.STACK_NAME,
                'STAGE': config.STAGE,
            },
        )

        slack_notify_lambda = _lambda.Function(
            self, 'SlackNotifyLambdaFunction',
            runtime=RUNTIME,
            code=_lambda.Code.from_asset(config.SLACK_DIR),
            handler='slack_notify_lambda.handler',
            timeout=Duration.seconds(5),
            layers=[lambda_layer],
            environment={
                'SLACK_BOT_TOKEN': config.SLACK_BOT_TOKEN,
                'STACK_NAME': config.STACK_NAME,
                'STAGE': config.STAGE,
            },
        )

        slack_notify_lambda.role.add_managed_policy(scheduler_managed_policy)

        # More resources...

云形式资源列表

  • aws :: Apigateway :: Restapi

  • aws :: apigateway :: account

  • aws :: apigateway ::部署

  • aws :: apigateway :: method

  • aws :: apigateway ::资源

  • aws :: apigateway :: stage

  • aws :: dynamodb :: table

  • aws :: events :: eventbus

  • aws :: events :: Eventrule

  • aws :: iam ::政策

  • 是:iam ::角色

  • aws :: lambda :: function

  • aws :: lambda :: laysversion

  • aws :: lambda ::许可

  • aws :: Scheduler :: Schedulegroup

有用的提示

在Google上,所有云形式资源都很好地索引。搜索其中任何一个都会迅速返回相关文档。在文档中,您将能够查看所有可用参数和验证条件。

API网关REST API /SLACK /{代理+}

CDK创建了一个AWS :: Apigateway :: Restapi资源,该资源将充当所有Slack事件的全Webhook。该请求通过lambda_proxy集成请求转发到Slack_receive_lambda函数。

API Gateway Rest API

lambda功能

业务逻辑完全生活在Python开发的Lambda功能中。这些功能由三个来源触发:API网关,EventBridge Events和EventBridge Schedule。

src/slack/

  • slack_notify_lambda

  • slack_receive_lambda

  • slack_respond_lambda

src/schedule/

  • schade_disable_lambda

  • schedue_enable_lambda

  • schade_save_lambda

  • schade_target_lambda

Slack_receive_lambda

此功能有一些重要任务:

  1. 接收并处理由API网关转发的事件。

  2. 如果在请求正文中存在,请检查松弛挑战字符串。

  3. 验证X-Slack-Signature标题以确保它是呼叫我们的REST API的Slack应用程序。

  4. SlackCommand类解析/serply命令字符串。

  5. 将所有数据转发到SerplyEventBus

  6. 在3秒内返回对Slack的确认回应。

必须在发送原始请求的3000毫秒内的Slack收到此确认,否则,将显示“超时”向用户显示。如果您无法验证请求有效负载,则您的应用程序应返回错误并忽略请求。

〜确认收据| api.slack.com

import json
from slack_receive_response import (
    command_response,
    default_response,
    event_response,
    interaction_response,
)

def get_challenge(body):
    if not body.startswith('{') and not body.endswith('}'):
        return False
    return json.loads(body).get('challenge', '')

responses = {
    '/slack/commands': command_response,
    '/slack/events': event_response,
    '/slack/interactions': interaction_response,
}

def handler(event, context):

    challenge = get_challenge(event.get('body'))

    if challenge:
        return {
            'statusCode': 200,
            'body': challenge,
            'headers': {
                'Content-Type': 'text/plain',
            },
        }

    return responses.get(event.get('path'), default_response)(event=event)

Serply活动巴士

事件总线是一个允许不同服务进行交流和交换事件的渠道。 EventBridge使您能够创建自动触发事件反应的规则,例如从SaaS应用程序中发送电子邮件以响应事件。事件总线能够同时触发多个目标。

接收Slack命令

Receiving the Slack command

收到Slack的初始请求后,Slack_receive_lambda函数将事件放入SerplyEventBus。

Event bus triggers 2 functions

事件总线触发2个功能:

  • slack_respond_lambda

  • schade_save_lambda

Slack_Respond_lambda

此功能将SERP通知计划的消息发送到Slack频道。我本可以在Slack_receive_lambda函数中这样做。但是,我需要尽可能保持该功能以在3秒内确认收据。响应消息故意委派给Slack_Respond_lambda函数。

import boto3
from pydash import objects
from slack_api import SlackClient, SlackCommand
from slack_messages import ScheduleMessage, ScheduleListMessage
from serply_config import SERPLY_CONFIG
from serply_database import NotificationsDatabase, schedule_from_dict

notifications = NotificationsDatabase(boto3.resource('dynamodb'))
slack = SlackClient()

def handler(event, context):

    detail_type = event.get('detail-type')
    detail_input = event.get('detail').get('input')
    detail_schedule = event.get('detail').get('schedule')

    if detail_type == SERPLY_CONFIG.EVENT_SCHEDULE_SAVE:
        schedule = schedule_from_dict(detail_schedule)
        message = ScheduleMessage(
            channel=detail_input.get('channel_id'),
            user_id=detail_input.get('user_id'),
            command=schedule.command,
            interval=schedule.interval,
            type=schedule.type,
            domain=schedule.domain,
            domain_or_website=schedule.domain_or_website,
            query=schedule.query,
            website=schedule.website,
            enabled=True,
            replace_original=False,
        )
        slack.respond(
            response_url=detail_input.get('response_url'),
            message=message,
        )

    elif detail_type == SERPLY_CONFIG.EVENT_SCHEDULE_LIST:
        schedules = notifications.schedules()
        message = ScheduleListMessage(
            channel=detail_input.get('channel_id'),
            schedules=schedules,
        )
        slack.notify(message)

    elif detail_type in [
        SERPLY_CONFIG.EVENT_SCHEDULE_DISABLE_FROM_LIST,
        SERPLY_CONFIG.EVENT_SCHEDULE_ENABLE_FROM_LIST,
    ]:
        schedules = notifications.schedules()
        message = ScheduleListMessage(
            schedules=schedules,
            replace_original=True,
        )
        slack.respond(
            response_url=detail_input.get('response_url'),
            message=message,
        )

    elif detail_type == SERPLY_CONFIG.EVENT_SCHEDULE_DISABLE:
        schedule = SlackCommand(
            command=objects.get(detail_input, 'actions[0].value'),
        )
        message = ScheduleMessage(
            user_id=detail_input.get('user').get('id'),
            command=schedule.command,
            interval=schedule.interval,
            type=schedule.type,
            domain=schedule.domain,
            domain_or_website=schedule.domain_or_website,
            query=schedule.query,
            website=schedule.website,
            enabled=False,
            replace_original=True,
        )
        slack.respond(
            response_url=detail_input.get('response_url'),
            message=message,
        )

    elif detail_type == SERPLY_CONFIG.EVENT_SCHEDULE_ENABLE:
        schedule = SlackCommand(
            command=objects.get(detail_input, 'actions[0].value'),
        )
        message = ScheduleMessage(
            user_id=detail_input.get('user').get('id'),
            command=schedule.command,
            interval=schedule.interval,
            type=schedule.type,
            domain=schedule.domain,
            domain_or_website=schedule.domain_or_website,
            query=schedule.query,
            website=schedule.website,
            enabled=True,
            replace_original=True,
        )
        slack.respond(
            response_url=detail_input.get('response_url'),
            message=message,
        )

    return {'ok': True}

supch_save_lambda

此功能将计划数据保存到DynamoDB通知表。

EventBridge计划未通过CDK提供。相反,每个/serply serp命令都有一个特定的时间表,该命令是由Schedue_save_lambda函数通过 boto3.client('scheduler')创建的。 client。

eventbridge时间表

EventBridge Schedule

import boto3
import json
from serply_database import NotificationsDatabase, schedule_from_dict
from serply_scheduler import NotificationScheduler

notifications = NotificationsDatabase(boto3.resource('dynamodb'))
scheduler = NotificationScheduler(boto3.client('scheduler'))

def handler(event, context):

    schedule = schedule_fro_dict(event.get('detail').get('schedule'))

    notifications.save(schedule)

    scheduler.save_schedule(
        schedule=schedule,
        event=event,
    )

    return {'ok': True}

Schedule_target_lambda

此功能由其相应的时间表触发,并且具有3个任务。

  1. 从Serply Api https://api.serply.io/v1/serp端点获取SERP数据。

  2. 将事件和响应数据保存到数据库中作为Serpnotification。

  3. 将所有数据转发到SerplyEventBus。

import boto3
import json
from dataclasses import asdict
from serply_api import SerplyClient
from serply_config import SERPLY_CONFIG
from serply_database import NotificationsDatabase, SerpNotification, schedule_from_dict
from serply_events import EventBus

event_bus = EventBus(boto3.client('events'))
notifications = NotificationsDatabase(boto3.resource('dynamodb'))
serply = SerplyClient(SERPLY_CONFIG.SERPLY_API_KEY)

def handler(event, context):

    detail_headers = event.get('detail').get('headers')
    detail_schedule = event.get('detail').get('schedule')
    detail_input = event.get('detail').get('input')

    schedule = schedule_from_dict(detail_schedule)

    if schedule.type not in [SERPLY_CONFIG.SCHEDULE_TYPE_SERP]:
        raise Exception(f'Invalid schedule type: {schedule.type}')

    if schedule.type == SERPLY_CONFIG.SCHEDULE_TYPE_SERP:

        response = serply.serp(
            domain=schedule.domain,
            website=schedule.website,
            query=schedule.query,
            mock=schedule.interval == 'mock',
        )

        notification = SerpNotification(
            command=schedule.command,
            domain=schedule.domain,
            domain_or_website=schedule.domain_or_website,
            query=schedule.query,
            interval=schedule.interval,
            serp_position=response.position,
            serp_searched_results=response.searched_results,
        )

    notifications.save(notification)

    notification_input = asdict(notification)

    event_bus.put(
        source=schedule.source,
        detail_type=SERPLY_CONFIG.EVENT_SCHEDULE_NOTIFY,
        schedule=schedule,
        input={
            **detail_input,
            **notification_input,
        },
        headers=detail_headers,
    )

    return {'ok': True}

计划的通知

Scheduled notification

Slack_notify_lambda

此功能构建通知消息并将其发送到Slack。 SerpnotificationMessage数据类是用于使用Slack Message Blocks格式化消息的模板。

import boto3
import json
from serply_config import SERPLY_CONFIG
from slack_api import SlackClient
from slack_messages import SerpNotificationMessage
from serply_database import schedule_from_dict
from serply_scheduler import NotificationScheduler

slack = SlackClient(SERPLY_CONFIG.SLACK_BOT_TOKEN)
scheduler = NotificationScheduler(boto3.client('scheduler'))

def handler(event, context):

    detail_schedule = event.get('detail').get('schedule')
    detail_input = event.get('detail').get('input')

    schedule = schedule_from_dict(detail_schedule)

    if schedule.type not in [SERPLY_CONFIG.SCHEDULE_TYPE_SERP]:
        raise Exception(f'Invalid schedule type: {schedule.type}')

    if schedule.type == SERPLY_CONFIG.SCHEDULE_TYPE_SERP:
        message = SerpNotificationMessage(
            channel=detail_input.get('channel_id'),
            serp_position=detail_input.get('serp_position'),
            serp_searched_results=detail_input.get('serp_searched_results'),
            command=schedule.command,
            domain=schedule.domain,
            domain_or_website=schedule.domain_or_website,
            interval=schedule.interval,
            query=schedule.query,
            website=schedule.website,
        )
        slack.notify(message)

    if schedule.interval in SERPLY_CONFIG.ONE_TIME_INTERVALS:
        scheduler.delete_schedule(schedule)

    return {'ok': True}

serpnotificationMessage松弛消息块

@dataclass
class SerpNotificationMessage:

    blocks: list[dict] = field(init=False)
    domain: str
    domain_or_website: str
    command: str
    interval: str
    query: str
    serp_position: int
    serp_searched_results: str
    website: str
    channel: str = None
    num: int = 100
    replace_original: bool = False

    def __post_init__ (self):

        TEXT_ONE_TIME = f'This is a *one-time* notification.'
        TEXT_YOU_RECEIVE = f'You receive this notification *{self.interval}*. <!here>'

        website = self.domain if self.domain else self.website
        total = int(self.serp_searched_results or 0)
        google_search = f'https://www.google.com/search?q={self.query}&num={self.num}&{self.domain_or_website}={website}'
        results = f'<{google_search}|{total} results>' if total > 0 else f'0 results'

        self.blocks = [
            {
                'type': 'section',
                'text': {
                    'type': 'mrkdwn',
                    'text': f'> `{website}` in position `{self.serp_position or 0}` for `{self.query}` from {results}.'
                },
            },
            {
                'type': 'context',
                'elements': [
                    {
                        'type': 'mrkdwn',
                        'text': f':bell: *SERP Notification* | {TEXT_ONE_TIME if self.interval in SERPLY_CONFIG.ONE_TIME_INTERVALS else TEXT_YOU_RECEIVE}'
                    }
                ]
            },
        ]

Slack notification

蠕动着火

schedule_target_lambda函数使对等效于以下curl请求的SERPLE API进行获取请求。

请求

curl --request GET \
  --url 'https://api.serply.io/v1/serp/q=professional+network&num=100&domain=linkedin.com' \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: API_KEY'

响应

{
    "searched_results": 100,
    "result": {
        "title": "Why professional networking is so important - LinkedIn",
        "link": "https://www.linkedin.com/pulse/why-professional-networking-so-important-jordan-parikh",
        "description": "7 de nov. de 2016 Networking becomes a little clearer if we give it a different name: professional relationship building. It's all about getting out there ...",
        "additional_links": [
            {
                "text": "Why professional networking is so important - LinkedInhttps://www.linkedin.com pulse",
                "href": "https://www.linkedin.com/pulse/why-professional-networking-so-important-jordan-parikh"
            },
            {
                "text": "Traduzir esta pgina",
                "href": "https://translate.google.com/translate?hl=pt-BR&sl=en&u=https://www.linkedin.com/pulse/why-professional-networking-so-important-jordan-parikh&prev=search&pto=aue"
            }
        ],
        "cite": {
            "domain": "https://www.linkedin.com pulse",
            "span": " pulse"
        }
    },
    "position": 8,
    "domain": ".linkedin.com",
    "query": "q=professional+network&num=100"
}

参考

GitHub存储库

您可以找到存储库here。它包括所有安装步骤:设置您的Slack应用程序,Serply帐户和AWS CDK部署。将其部署在您的AWS帐户中,在AWS免费层中。您还可以分支并根据自己的需求进行自定义。

serply-inc/notifications

可能性

serply通知的结构是使所有调度都使用事件总线从消息传递逻辑解耦。可以添加更多集成和通知类型,例如电子邮件通知,其他聊天机器人平台和更多的通知类型。

请继续关注此Serply Notifications系列的第2部分!