Maeiee系统搭建迭代1

转眼《Maeiee的成长周报》已经来到第 5 周。在第3、4周中,我坚持啃完《「Let's build GPT:from scratch, in code, spelled out.」课程笔记》,发到网上,引起了一些关注与阅读,很有成就感。在本周中,我放慢脚步,对自己的个人系统进行迭代完善,磨刀不误砍柴工。

『我的个人系统』指什么?为了方便自学与知识获取,我给自己开发了一套软件。比如对 qutebrowser 进行二次开发,使之成为可编程浏览器,又如使用 Flutter 开发了一套跨平台的 App,具备资讯、个人管理等功能。个人系统并不意味着,所有软件都要自己重头开发,一些优秀的软件也可以直接纳入,比如 Obsidian

在本周中,我围绕这些软件进行维护、迭代。首先,这套系统还处于早期探索阶段,谈产品化或开源还太早。那作为读者,能从这篇文章中获得什么?如果你具备编程能力,我希望能向你传递“为自己开发软件,工欲善其事,必先利其器”的信念,这将让个人受益终身。另外,这些瞎折腾本身也是很有意思的,我从中也获取到一些感悟,将他们分享出来。

最后,就像在《Maeiee的自我介绍》中所说:

交友,对我来说,不仅仅是增长见识,更是一种精神上的慰藉。在这个信息爆炸的时代,真正的理解和深入的交流变得尤为珍贵。我相信,每一次的对话和分享,都有可能成为我们认识世界和自我更深一层的契机。

请不要犹豫,如果您对我的任一兴趣点有自己的见解或是有任何问题,都可以通过我的社交媒体账号联系我,或是在我的数字花园留言。我期待着从您那里学习新知识,同时也愿意分享我所了解的信息。在知识的海洋中,我们可以一起航行,探索未知的领域。

您可在《Maeiee首页》中查看我的联系方式。


1 新的浏览器自动化脚本

我将 qutebrowser 视作一个可编程浏览器,与 puppeteer 这种受控浏览器有什么区别呢?区别在于,前者我自己也能日常使用,并且手动与自动可以融合。总体来说,qutebrowser 就像浏览器版本的(低配)Emacs,可定制性超高!

qutebrowser 底层基于 PySide/PyQt,它们在对 WebView 的操作上,是基于异步回调。这种代码写起来乱,读起来也乱,打破了从上到下的先后顺序。写了几个自动化脚本之后,我就受不了了。于是,我我封装了一套流式调用,要好得多,示例代码如下:

OperationLoadUrlAndWait(url=QUrl(f"https://www.example.com"))
.then(OperationScrollTo(times=4, perc=100, timeout=3000))
.then(OperationFetchHtml())
.then(OperationExtractInfos())
.then(OperationScrollTo(times=1, perc=0, timeout=3000))

这段代码,模仿了 Promise 的 then 流式调用。相较于 Puppeteer 的 await...async 还方便性上还是差一些。后者在复杂逻辑上更加边界,比如条件、循环。而我这里,未来只能靠加算子(thenRepeat, thenIf)来实现,非常繁琐。不过,我已经满足了,比之前回调满天飞要好太多

脑洞

未来还可以添加录制功能,我在 qutebrowser 中演示一遍,然后自动生成上述脚本。
这让我想到 qutebrowser 本身就带有 Macro Mode!可以录制宏,回头研究下这部分功能!


2 自动化脚本应用:自动化搜索

自动化脚本的用途有很多,概括来说,把重复的事情交给机器去做。举一个例子:基于搜索引擎,实现自动化搜索功能。

有什么用呢?列举两例:

  1. 突然想查一个东西,通过一行命令 search something 即可,节省大量时间
  2. 对关注的东西,可以定期自动搜索,实现进展跟踪

除了自动化搜索外,有很多应用场景,与 RPA 的理念不谋而合。


3 资讯浏览页卡顿优化

我用 Flutter 开发了一个资讯阅读器(跟 RSS 阅读器差不多)。随着信息的增多,启动时越来越卡。在本节中进行优化。

同步阻塞优化

有一处逻辑:首先拉取所有站点,然后一个一个查询站点未读资讯的数量。(我在 Flutter 中直接访问 MongoDB,省去了后端 API 这一层)。

查询数据库本身是一个异步操作,同时,mongo_dart 也是支持一定的并发量的。但是,看下面这段 Dart 代码:

loadData(List<String> crawlers) async {
  for (final crawler in crawlers) {
    final count =
        await getIt.get<RayInfoApp>().getCrawlerUnreadCount(crawler);
    _unreadCountMap[crawler] = count;
    notifyListeners();
  }
}

有什么问题?把所有源的异步转成了顺序同步,导致没有并发了。假设一个请求 1s,200 个源就是 200s,所以打开耗时才会这么慢。

更好的实现是,利用 async...await 的阻塞释放控制权的机制,允许 N 个并行操作。代码优化如下:

Future<void> loadData(List<String> crawlers, {int concurrency = 10}) async {
  // 分批处理爬虫列表
  for (var i = 0; i < crawlers.length; i += concurrency) {
    // 获取当前批次的爬虫列表
    final batch = crawlers.skip(i).take(concurrency);

    // 使用 Future.wait 并行处理当前批次
    final results = await Future.wait(batch.map(
        (crawler) => getIt.get<RayInfoApp>().getCrawlerUnreadCount(crawler)));

    // 处理每个结果
    int j = 0;
    for (final count in results) {
      _unreadCountMap[batch.elementAt(j)] = count;
      j++;
    }
  }

  // 更新未读消息总数等其他操作
  notifyListeners();
}

同时,我在 MongoDB 中添加索引进行加速。这样,在“拉取所有站点,查询站点未读资讯数量”这一步上实现了秒开。

拉取所有未读资讯优化

目前,我有 7w 多条未读资讯。在拉取这个列表时,请求需要好几秒才能返回数据。经过优化后,也实现了秒开。其中,找到最大的性能瓶颈是联表操作没有放到最后

在拉取未读资讯时,包含两个操作:一个是过滤 isNew=True,第二个是联表,为资讯拼接上一些信息。这两个操作的先后顺序,耗时差异是巨大的:

一开始我采用前者,后来改为了后者。同时通过在 MongoDB 中添加索引进行加速。

经过一顿优化之后,最终实现了资讯 App 的完全秒开!


4 Flutter Fluent UI 组件库

今天要坐火车返回北京,路上四、五个小时,得找点事干。我打算研究 fluent_ui | Flutter package,因为我的自用 App 界面实在太丑了。一直想弄却也没时间,趁这个机会,把这个组件库换上。

这个库我之前也有研究过,它的难点主要有两个:

  1. 仅有一个 Example,缺少入门教程
  2. Example 中的最佳实践非常出色,但涉及到大量的库,必须掌握这些库,才能全面掌握 fluent_ui

总结来说,问题不出在 fluent_ui,而是自己太菜。正好,趁这个机会,掌握 Flutter 最佳实践。

有两个自学路径,一是把 Example 跑起来,每个展示用例都提供了单一组件如何使用,相对简单。另一条路径是,看 Example 自身实现源码,这就是我说的最佳实践之所在,相对难,但更有价值,我选择这一条。我的目标是:把 Example 中的架构搬运到我的自用 App 中去。

对 fluent_ui 的自学体会,我记录在《Flutter fluent_ui》这篇文章中。


5 在长列表中持久化阅读状态

在资讯列表中,我使用了一个 ListView,列表中每个元素是一个信息卡片,组件为 InfoCard,是一个 StatefulWidget。我在 InfoCard 内存储了一些状态(isNew、isMark、isHub)。

这里的问题是,当我在 InfoCard 内修改状态,比如点击收藏,此时我在 InfoCard 内通过 setState 更新了状态,这样卡片上的 UI 会变化。(但是资讯列表中的 Model 数据并没有变化)。

这样,当这个卡片离开屏幕后(InfoCard 组件会被其它资讯复用),再切回来,StatefulWidget 中本地修改的状态会丢失。

一种成本比较低的方法是,以 mutable 的方式,直接修改 Model。本次我尝试采用这一低成本方案。


6 收藏资讯关联 Obsidian

我在 App 中收藏的资讯,会在 MongoDB 的资讯 Model 中添加一个 mark 标识。但是,我的知识管理系统是 Obsidian,“收藏资讯”在这两个系统间造成割裂。这种割裂,需要我通过手动操作,在 Obsidian 进行添加,操作成本太高,严重影响效率。

于是,我想的是,点击收藏,自动到 Obsidian 中去创建一篇笔记。我在 Obsidian 中已经搭建了一个网页书签库,因此我的资讯软件只需要按照已有的模板、约定,进行添加即可。

自用的资讯系统升级,成功与 Obsidian 打通,在资讯 App 中点击收藏后,将自动在 Obsidian 中创建对应书签!

理想中的资讯目录:

脑中构想点击收藏(未收藏 -> 收藏):

脑中构想点击收藏(收藏 -> 未收藏):

下面开始正式实现,首先创建一个 ObsidianProvider,统一提供与 Obsidian 相关的所有功能。之后内部提供两个 Manager:ObsidianSiteManager 和 ObsidianInfoManager。还需要 ObsidianVaultManager、ObsidianTemplateManager 管理与路径相关的内容。

ObsidianSiteManager 功能:首先是对 site host 名称的清理,如果是 www. 开头的,去掉之。接下来是返回站点首字母。获取站点目录。

ObsidianInfoManager 功能:对外 API 是 addInfo,通过参数传入所有需要的内容(url、description、host、created)。


7 Feed 流卡片布局优化

基于 Flutter fluent_ui 做了一些简单适配。整体跟 Flutter fluent_ui 比较和谐了,甚至还支持了夜间模式。至此,这样简单的适配也足够我用的了,等以后有更多需求,再升级。


本文作者:Maeiee

本文链接:Maeiee系统搭建迭代1

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!