Telegram 机器人用于个性化选择 Habr 的文章

对于诸如“为什么?”之类的问题有一篇较旧的文章 - 自然极客时代——让空间更清洁.

文章很多,出于主观原因,有些我不喜欢,有些则相反,跳过很可惜。 我想优化这个过程并节省时间。

上面的文章建议了一种浏览器内脚本方法,但我不太喜欢它(尽管我以前使用过它),原因如下:

  • 对于计算机/手机上的不同浏览器,如果可能的话,您必须重新配置。
  • 按作者严格过滤并不总是很方便。
  • 那些你不想错过的作者的文章,即使他们每年发表一次,这个问题还没有得到解决。

基于文章评级的网站内置过滤并不总是很方便,因为高度专业化的文章尽管有价值,但可能会获得相当温和的评级。

最初,我想生成一个 RSS 提要(甚至几个),只留下有趣的内容。 但最终发现,阅读RSS似乎并不是很方便:无论如何,要对一篇文章进行评论/投票/将其添加到收藏夹,都必须通过浏览器。 这就是为什么我编写了一个电报机器人,它通过个人消息向我发送有趣的文章。 Telegram 本身制作了精美的预览,与有关作者/评级/观点的信息相结合,看起来信息量很大。

Telegram 机器人用于个性化选择 Habr 的文章

剪辑下方是作品特点、写作过程、技术方案等细节。

简单介绍一下机器人

存储库: https://github.com/Kright/habrahabr_reader

电报中的机器人: https://t.me/HabraFilterBot

用户为标签和作者设置附加评级。 之后,对文章应用过滤器 - 将文章在 Habré 上的评分、作者的用户评分以及按标签划分的用户评分平均值相加。 如果金额大于用户指定的阈值,则文章通过过滤器。

编写机器人的另一个目标是获得乐趣和经验。 此外,我还经常提醒自己, 我不是谷歌,因此很多事情都是尽可能简单甚至原始地完成的。 然而,这并没有阻止编写机器人的过程花费了三个月的时间。

外面正值夏天

七月即将结束,我决定编写一个机器人。 而且不是一个人,而是和一个正在掌握 scala 并想在上面写点东西的朋友一起。 一开始看起来很有希望——代码将由一个团队来削减,任务似乎很容易,我认为在几周或一个月内机器人就会准备好。

尽管我自己在过去几年里时不时地在岩石上编写代码,但通常没有人看到或查看这些代码:宠物项目,测试一些想法,预处理数据,掌握 FP 的一些概念。 我对在团队中编写代码的样子非常感兴趣,因为摇滚代码可以用非常不同的方式编写。

什么本来可以消失 所以? 不过,我们不要着急。
发生的所有事情都可以使用提交历史记录进行跟踪。

27 月 XNUMX 日,一位熟人创建了一个存储库,但没有做其他任何事情,所以我开始编写代码。

七月30

简而言之:我写了 Habr 的 rss feed 的解析。

  • com.github.pureconfig 用于将类型安全配置直接读取到案例类中(事实证明非常方便)
  • scala-xml 用于读取 xml:因为最初我想为 rss feed 编写自己的实现,并且 rss feed 是 xml 格式,所以我使用这个库进行解析。 其实RSS解析也出现了。
  • scalatest 用于测试。 即使对于小型项目,编写测试也可以节省时间 - 例如,在调试 xml 解析时,将其下载到文件、编写测试和纠正错误要容易得多。 当后来在解析一些带有无效 utf-8 字符的奇怪 html 时出现错误时,事实证明将其放入文件并添加测试会更方便。
  • 来自阿卡的演员。 客观地说,它们根本不需要,但该项目是为了好玩而编写的,我想尝试一下。 结果,我准备好说我喜欢它。 OOP的思想可以从另一面来看——有交换消息的参与者。 更有趣的是,您可以(并且应该)以这样的方式编写代码:消息可能不会到达或可能不会被处理(一般来说,当帐户在一台计算机上运行时,消息不应该丢失)。 起初我很困惑,代码中存在垃圾,演员互相订阅,但最终我设法想出了一个相当简单而优雅的架构。 每个 Actor 内部的代码可以被认为是单线程的;当 Actor 崩溃时,acca 会重新启动它 - 结果是一个相当容错的系统。

八月9

我添加到项目中 scala-scrapper 用于解析 Habr 的 html 页面(提取文章评级、书签数量等信息)。

还有猫。 那些在岩石里的。

Telegram 机器人用于个性化选择 Habr 的文章

然后我读了一本关于分布式数据库的书,我喜欢CRDT(无冲突复制数据类型, https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type, 哈布尔),所以我发布了一个交换半群的类型类,以获取有关哈布雷文章的信息。

事实上,这个想法非常简单——我们有单调变化的计数器。 促销的数量正在逐渐增加,优点(以及缺点)的数量也在逐渐增加。 如果我有一篇文章的两个版本的信息,那么我可以“将它们合并为一个” - 较大的计数器状态被认为更相关。

半群意味着具有一篇文章信息的两个对象可以合并为一个。 可交换意味着你可以将A+B和B+A都合并,结果不依赖于顺序,最终保留最新版本。 顺便说一句,这里也有关联性。

例如,按照计划,解析后的 rss 提供的有关文章的信息略有减弱 - 没有浏览量等指标。 然后,一名特殊演员获取有关文章的信息,并跑到 html 页面进行更新并将其与旧版本合并。

一般来说,就像在 akka 中一样,没有必要这样做,你可以简单地存储文章的 updateDate 并获取更新的文章,而不需要任何合并,但冒险之路引导了我。

八月12

我开始感到更加自由,只是为了好玩,我让每次聊天都成为一个单独的演员。 理论上,一个 Actor 本身的重量约为 300 字节,并且可以数百万个字节来创建,因此这是一种完全正常的方法。 在我看来,这个解决方案非常有趣:

一个 Actor 是 Akka 中电报服务器和消息系统之间的桥梁。 他只是接收消息并将其发送给所需的聊天演员。 聊天演员可以发回一些东西作为回应——并且它会被发回电报。 非常方便的是,这个 actor 尽可能简单,只包含响应消息的逻辑。 顺便说一下,每次聊天都会出现有关新文章的信息,但我再次认为这没有任何问题。

一般来说,机器人已经开始工作,响应消息,存储发送给用户的文章列表,我已经认为机器人几乎准备好了。 我慢慢地添加了一些小功能,例如标准化作者姓名和标签(用“s_d_f”替换“sd f”)。

只剩下一件事了 小但是 ——国家在任何地方都没有得到拯救。

一切都错了

您可能已经注意到,这个机器人大部分是我独自编写的。 于是,第二个参与者参与了开发,代码中出现了如下变化:

  • MongoDB 似乎可以存储状态。 与此同时,项目中的日志被破坏了,因为出于某种原因 Monga 开始向它们发送垃圾邮件,而有些人干脆在全球范围内将它们关闭。
  • 《电报》中的桥牌演员变得面目全非,并开始自己解析消息。
  • 聊天的演员被无情地砍掉了,取而代之的是一个一次性隐藏了所有聊天信息的演员。 每打一个喷嚏,这位演员就会陷入麻烦。 嗯,是的,就像更新一篇文章的信息时,将其发送给所有聊天参与者是很困难的(我们就像谷歌,数百万用户正在等待每个聊天中的一百万篇文章),但是每次更新聊天时,进入蒙加很正常。 正如我后来意识到的那样,聊天的工作逻辑也被完全删除了,取而代之的是一些不起作用的东西。
  • 类型类没有留下任何痕迹。
  • 参与者之间的订阅中出现了一些不健康的逻辑,导致了竞争状况。
  • 具有类型字段的数据结构 Option[Int] 转换为具有神奇默认值(如 -1)的 Int。 后来我意识到mongoDB存储的是json,存储在那里并没有什么问题 Option 好吧,或者至少将 -1 解析为 None,但当时我不知道这一点,并相信我的话“应该是这样的”。 那段代码不是我写的,我暂时也懒得去修改它。
  • 我发现我的公共IP地址经常发生变化,每次我都必须将其添加到Mongo的白名单中。 我在本地启动了该机器人,Monga 位于 Monga 公司服务器上的某个位置。
  • 突然间,电报标签和消息格式的标准化消失了。 (嗯,为什么会这样呢?)
  • 我喜欢机器人的状态存储在外部数据库中,当重新启动时,它会继续工作,就像什么都没发生一样。 然而,这是唯一的优点。

第二个人并不着急,所有这些变化在九月初就已经集中出现了。 我没有立即意识到所造成的破坏的规模并开始了解数据库的工作,因为...... 我以前从未与他们打过交道。 直到后来我才意识到有多少工作代码被削减了,并且在其位置上添加了多少错误。

九月

起初我认为掌握 Monga 并做好它会很有用。 然后我慢慢开始明白,组织与数据库的通信也是一门艺术,你可以参加很多比赛,但也会犯错误。 例如,如果用户收到两条消息,例如 /subscribe - 为了响应每一条消息,我们将在表中创建一个条目,因为在处理这些消息时,用户尚未订阅。 我怀疑目前与 Monga 的交流形式并不是以最好的方式编写的。 例如,用户设置是在他注册时创建的。 如果他试图在订阅之前更改它们......机器人不会做出任何反应,因为参与者中的代码进入数据库进行设置,没有找到它并崩溃了。 当被问到为什么不根据需要创建设置时,我了解到,如果用户没有订阅,则无需更改它们...消息过滤系统的制作方式并不明显,即使仔细查看代码后我也可以不明白是不是原意,有错误。

聊天中没有提交的文章列表;相反,建议我自己写。 这让我感到惊讶——总的来说,我并不反对把各种各样的东西拖到项目中,但对于那些把这些东西带进来并搞砸的人来说,这是合乎逻辑的。 但不,第二个参与者似乎放弃了一切,但表示聊天中的列表据说是一个糟糕的解决方案,有必要用诸如“文章 y 已发送给用户 x”之类的事件进行标记。 然后,如果用户请求发送新文章,就需要向数据库发送请求,数据库会从事件中选择与用户相关的事件,同时获取新文章列表,过滤后发送给用户并将有关此事件的事件扔回到数据库中。

第二个参与者被带到了抽象的地方,此时机器人不仅会收到来自 Habr 的文章,而且不仅会发送到电报。

我以某种方式在九月下旬以单独标志的形式实施了事件。 这不是最优的,但至少机器人开始工作并开始再次向我发送文章,我慢慢地弄清楚代码中发生了什么。

现在您可以回到开头并记住该存储库最初不是我创建的。 什么事会变成这样呢? 我的拉取请求被拒绝了。 事实证明,我有乡巴佬代码,我不知道如何在团队中工作,我必须修复当前实现曲线中的错误,而不是将其细化到可用状态。

我很沮丧,查看了提交历史记录和编写的代码量。 我看了一些原本写得很好的片段,后来又被破坏了……

去他妈的

我记得那篇文章 你不是谷歌.

我认为没有人真正需要一个没有实施的想法。 我想我想要一个工作机器人,它将作为一个简单的 java 程序在一台计算机上以一个副本的形式工作。 我知道我的机器人可以工作几个月而无需重新启动,因为我过去已经编写过这样的机器人。 如果它突然掉下来并且不向用户发送另一篇文章,天就不会塌下来,也不会发生什么灾难性的事情。

如果代码根本无法工作或工作不正常,为什么我需要 Docker、mongoDB 和其他“严肃”软件?

我分叉了这个项目并按照我的意愿做了一切。

Telegram 机器人用于个性化选择 Habr 的文章

大约在同一时间,我换了工作,空闲时间变得非常缺乏。 早上我在火车上醒来,晚上我回来很晚,不再想做任何事。 我有一段时间什么也没做,然后完成机器人的愿望压倒了我,我开始在早上开车上班的路上慢慢重写代码。 我不会说它很有成效:坐在摇晃的火车上,腿上放着笔记本电脑,通过手机查看堆栈溢出并不是很方便。 然而,写代码的时间在不知不觉中就过去了,项目开始慢慢走向工作状态。

在我内心深处有一个疑问想要使用 mongoDB,但我认为除了“可靠”状态存储的优点之外,还有明显的缺点:

  • 数据库成为另一个故障点。
  • 代码变得越来越复杂,我会花更长的时间来编写它。
  • 代码变得缓慢且低效;而不是更改内存中的对象,而是将更改发送到数据库并在必要时撤回。
  • 单独表中的事件存储类型存在限制,这与数据库的特性相关。
  • Monga 的试用版有一些限制,如果遇到这些限制,您将必须在某些内容上启动和配置 Monga。

我剪掉了 monga,现在机器人的状态只是存储在程序的内存中,并且时不时地以 json 形式保存到文件中。 也许在评论中他们会写到我错了,这是应该使用数据库的地方,等等。 但这是我的项目,文件的处理方法尽可能简单,并且以透明的方式工作。

扔掉像-1这样的魔法值并返回正常值 Option,添加了哈希表的存储,其中包含将文章发送回带有聊天信息的对象。 添加删除五天以上文章的信息,以免存储所有内容。 我将日志记录带入工作状态 - 日志以合理的数量写入文件和控制台。 添加了一些管理命令,例如保存状态或获取统计信息(例如用户和文章的数量)。

修复了一些小问题:例如,对于文章,现在会显示通过用户过滤器时的浏览量、喜欢、不喜欢和评论的数量。 总的来说,令人惊讶的是有多少小事情需要纠正。 我保留了一份清单,记下了其中所有的“违规行为”,并尽可能地纠正它们。

例如,我添加了直接在一条消息中设置所有设置的功能:

/subscribe
/rating +20
/author a -30
/author s -20
/author p +9000
/tag scala 20
/tag akka 50

还有另一支球队 /settings 以这种形式准确显示它们,您可以从中获取文本并将所有设置发送给朋友。
这看似一件小事,但类似的细微差别却有几十个。

以简单线性模型的形式实现文章过滤 - 用户可以为作者和标签设置附加评级以及阈值。 如果作者的评分、标签的平均评分以及文章的实际评分之和大于阈值,则向用户展示该文章。 您可以使用命令 /new 向机器人索取文章,或者订阅机器人,它将在一天中的任何时间以个人消息的形式发送文章。

一般来说,我的想法是让每篇文章都拉出更多的功能(中心、评论数量、书签、评级变化动态、文章中的文字、图片和代码量、关键词),并向用户展示一个好的/不好在每篇文章下投票并为每个用户训练一个模型,但我太懒了。

另外,作品的逻辑也不会那么明显。 现在我可以手动为 PatientZero 设置 +9000 的评级,并且阈值评级为 +20,我将保证收到他的所有文章(当然,除非我为某些标签设置 -100500)。

最终的架构非常简单:

  1. 存储所有聊天和文章状态的参与者。 它从磁盘上的文件加载其状态,并时不时地将其保存回一个新文件。
  2. 不时访问 RSS 提要的参与者了解新文章、查看链接、解析并将这些文章发送给第一个参与者。 另外,它有时会请求第一个演员的文章列表,选择那些不超过三天、但很长时间没有更新的文章,并更新它们。
  3. 通过电报进行交流的演员。 我这里还是把消息解析完整了。 以友好的方式,我想将其分为两部分 - 一个解析传入的消息,第二个处理传输问题,例如重新发送未发送的消息。 现在没有重新发送,并且由于错误而未到达的消息将简单地丢失(除非在日志中注明),但到目前为止这还没有造成任何问题。 如果一群人订阅了机器人并且我达到了发送消息的限制,也许会出现问题)。

我喜欢的是,多亏了 akka,演员 2 和 3 的跌倒通常不会影响机器人的性能。 也许有些文章没有按时更新,或者有些消息没有到达电报,但帐户重新启动演员,一切都继续进行。 仅当电报参与者响应他已成功传递消息时,我才会保存向用户显示文章的信息。 威胁我的最糟糕的事情是多次发送消息(如果已发送,但确认信息不知何故丢失了)。 原则上,如果第一个演员不将状态存储在自己体内,而是与某个数据库进行通信,那么他也可以在不知不觉中坠落并复活。 我也可以尝试 akka 持久性来恢复 Actor 的状态,但当前的实现因其简单性而适合我。 这并不是说我的代码经常崩溃——相反,我付出了相当大的努力来让它变得不可能。 但糟糕的事情还是发生了,将程序分解成独立的部分(演员)的能力对我来说似乎非常方便和实用。

我添加了circle-ci,这样如果代码损坏,你会立即发现它。 至少,这意味着代码已停止编译。 最初我想添加 travis,但它只显示我的项目,没有分叉。 一般来说,这两个东西都可以在开放存储库中自由使用。

结果

已经十一月了。 该机器人已编写完毕,过去两周我一直在使用它,我喜欢它。 如果您有改进的想法,请写下来。 我不认为将其货币化有什么意义——让它发挥作用并发送有趣的文章。

机器人链接: https://t.me/HabraFilterBot
GitHub: https://github.com/Kright/habrahabr_reader

小结论:

  • 即使是一个小项目也可能需要很多时间。
  • 你不是谷歌。 用大炮射麻雀是没有意义的。 一个简单的解决方案也可能有效。
  • 宠物项目非常适合尝试新技术。
  • Telegram 机器人的编写非常简单。 如果没有“团队合作”和技术实验,这个机器人可能会在一两周内编写完成。
  • Actor 模型是一个有趣的东西,它与多线程和容错代码配合得很好。
  • 我想我已经体会到为什么开源社区喜欢分叉了。
  • 数据库很好,因为应用程序状态不再依赖于应用程序崩溃/重新启动,但使用数据库会使代码复杂化并对数据结构施加限制。

来源: habr.com

添加评论