owenyoung 08月18日
New Buzzing 已发布!
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者分享了使用Deno重写Buzzing项目的技术细节和制作历程。Buzzing旨在抓取英文世界的热门讨论内容,如Reddit、Twitter和Hacker News等,并将其标题翻译成中文,按主题汇总到不同子网站。作者详细介绍了项目的技术架构,包括数据抓取、格式化、翻译、归档及部署流程,强调了精简页面、提高构建速度和优化用户体验的策略。项目迁移至Cloudflare Pages,并利用Deno Deploy实现动态页面部署,通过Github Actions自动化更新发布,最终实现了高效、健壮的聚合信息服务。

🌟 Buzzing项目重写旨在解决中文用户获取英文世界热门讨论的痛点,通过抓取Reddit、Twitter、Hacker News等平台内容,将其标题翻译成中文并按主题聚合,为用户提供便捷的信息获取渠道。该项目对英文新闻/杂志网站及聚合类网站的内容进行了系统性梳理和引用。

🚀 项目技术架构升级,采用Deno重写,实现了高度的精简和效率提升。每个子网站核心只有一个index.html页面,大大减小了网站体积和构建时间。静态页面部署在Cloudflare Pages,动态页面部署在Deno Deploy,数据存储在Cloudflare R2,并通过Github Actions实现自动化更新发布,整体流程高效健壮。

📦 数据处理流程清晰,包括原始数据抓取(1-raw)、格式化(2-formated)以兼容JSON Feed标准并扩展自有属性、以及多语言翻译(3-translated)。翻译后数据进行归档(current和archive目录),方便静态页面生成和动态页面实时读取。

⚡️ 在用户体验方面,Buzzing提供了默认版和Lite版,Lite版无图,支持快速浏览大量信息。所有站点均支持四种语言,并提供JSON Feed和Atom Feed,方便用户使用RSS阅读器订阅。视频内容采用懒加载技术,仅加载预览图,点击后才加载完整视频,优化了页面加载速度。

💡 Deno的开箱即用TypeScript支持和Deno Deploy的无缝部署,以及不依赖过多第三方库的生态,使得项目开发过程流畅且最终成品非常健壮。作者对此次应用结构设计表示高度满意,并认为未来可以基于feed.json开发体验良好的APP。

在最近两周里,我用 Deno重写了Buzzing,这是一个把英文世界热门的讨论,尤其是用户产生的内容,比如reddit推特,以及我最喜欢的hackernews,把这些优质内容标题翻译成中文,然后根据不同的主题汇总到不同的子网站。我将在本文分享 Buzzing 的一些技术细节和制作历程。

描述完之后,怎么感觉那么像一个标准的垃圾站啊,内容搬运农场!

但没办法,我对这样的“垃圾站“有真实的需求,我好奇世界上那些奇怪的人,固执的人,以及有趣的人在做什么,在读什么,在思考什么问题,但很难在中文世界中找到这样的家园。而在英文世界中,不仅有许多专业的新闻/杂志网站,比如:

还有很多网址聚合类的网站,比如 Hacker News,Lobster, Reddit

这些网址聚合类网站的内容由用户提交(大多数是链接),然后其他用户投票,最终决定哪些内容会被展示在首页,为不同兴趣的人提供了一个绝佳的互联网冲浪入口,所以Reddit 的 slogan 就是:“The Front Page of the Internet” , (刚去查证了一下,发现 Reddit 已经改了自己的Slogan,旧的 reddit的标题依然是这个,新的已经变成了:“Diveinto anything”)

鉴于目前中文网络环境的言论管控,自我审查等,我觉得我的生活已经离不开这些英文信息网站了。但是由于语言墙,快速浏览大量的非母语信息,并找出自己真正感兴趣的内容对我来说真的很难。所以我非常感谢现代翻译技术的发展,尤其是deepl的翻译质量。在此之前,各种翻译工具对技术文章的翻译经常令人啼笑皆非,比如各种stackoverflow 垃圾站,我在测试各种工具之后,发现 deepl的翻译的确是最好的(虽然对于小部分技术名词,也有一些问题)。可以看下hn 站点下 Deepl和微软的翻译对比效果:

所以基于以上的需求,我在 2020 年末制作了 Buzzing 的第一个版本,大概长这样:

各个主题的子网站长这样:

推出了一年,我觉得自己应该是 Buzzing里最重度的用户吧,以至于我自己最常逛的主题hn.buzzing.cc的内容根本不够我看,所以我甚至专门为自己写了Feed for Owen:

这个网站抓取 Hacker News 上 Points 大于 1 的提交,基本上就是 HN上所有的提交了,同时过滤掉最低质量的提交。但由于可以用中文快速浏览标题,所以这样的信息量对我来说就是小菜一碟。

我在使用这个专门为我自己写的网站半年之后得出的结论就是,光看热门的 HN 提交,真的会让你错过超多有意思的人,有意思的事。这其中大多数都不会进入到 HackerNews 首页。

再来看看我最终的成果,也就是New Buzzing

默认版:

Lite 版:

每个主题都有一个 lite 的版本,无图,可以无干扰的快速浏览大量信息。同时每个站点都提供了JSON FeedAtom Feed,方便使用 RSS 阅读器订阅。站点像之前一样,同时支持 4种语言,中文简体,中文繁体,英文,日文,并且每条内容均会显示原文,方便对比参照。同时新增了 4个子网站,纽约客, Lobste,下饭视频Linux以及我最爱的HN 最新

可以说这个版本的 Buzzing 完全是受 Hacker News 社区影响的产物,纯静态 HTML,无 JS,无额外的 CSS 文件, 总共只有 100 多行CSS 样式,内嵌在 HTML 页面里面,所以如果你访问https://hn.buzzing.cc/lite/的话,你只会加载一个 HTML页面,没有多余的请求(icon 是浏览器为了渲染 favicon 请求的):

30KB 的页面里其实包含了多达 200 条的内容。而于此同时,当我打开百度的首页(一个我们用来测网络是否正常的网站,页面看起来如此简洁!):

你会发现他加载了多达 60 个请求,数据量多达 2.53M。吓人。

接下来分享一些 New Buzzing 的技术细节:)

技术细节🔗

之前的 Buzzing 部署在 Github Pages 里,New Buzzing 迁移到了 Cloudflare 的Pages 服务

(其实为啥要搞 New Buzzing,就是因为 Github 的客服说我的 Buzzing 占的空间太大了,其中最大的一个站点占用了 60 多G!因为之前设计的不合理,太依赖静态网站,每个 Tag,每月的 Archive 页面,甚至每个文章的详情页面都生成了HTML,一开始还好,后来整个网站就变得越来越大,由于网站太大,用 force push 到 Github 很容易失败,所以只能正常 push,导致 repo越积越大,直到 Github 都受不了了联系我。。而我对维护工作也变得越来越害怕)

所以新版的 Buzzing,痛定思痛,决定网页全面精简化,那就是每个子网站只有 1 个页面! index.html(有点吹牛,但是核心就是只有 1个页面),以下:

.├── 404.html├── _redirects├── en   ├── feed.json   ├── feed.xml   ├── index.html   └── lite       ├── feed.json       ├── feed.xml       └── index.html├── favicon.ico├── feed.json├── feed.xml├── icon.png├── index.html├── ja   ├── feed.json   ├── feed.xml   ├── index.html   └── lite       ├── feed.json       ├── feed.xml       └── index.html├── lite   ├── feed.json   ├── feed.xml   └── index.html├── robots.txt└── zh-Hant    ├── feed.json    ├── feed.xml    ├── index.html    └── lite        ├── feed.json        ├── feed.xml        └── index.html

这样 Build 速度都不到 1s,目前 20 多个站点的 build 时间 30s 以内搞定(用 Deno),同时用一个动态站点i.buzzing.cc来提供不常被访问的 Tag 页面,Archive页面,以及详情页面,这些页面都属于低频访问,如果每次都生成静态页面的话,会用掉大量的时间。

同时,新增了www.buzzing.cc作为所有子站点的聚合页面,在每次子站点生成之后生成。

具体的生成过程如下,以 HN 站点为例:

cache├── 1-raw   └── 2022       └── 09           └── 03               └── hn                   ├── en_hn_2022_09_03_21_13_18_954_44__32700019.json                   └── en_hn_2022_09_03_21_13_18_954_45__32700066.json├── 2-formated   └── 2022       └── 09           └── 03               └── hn                   ├── en_hn_2022_09_03__32696241.json                   └── en_hn_2022_09_03__32700066.json└── 3-translated    └── 2022        └── 09            └── 03                └── hn                    ├── en_hn_2022_09_03__32696241.json                    └── en_hn_2022_09_03__32700066.jsoncurrent└── items    └── hn        └── items.jsonarchive└── hn    ├── archive       └── 2022           └── 35               └── items.json    ├── issues       └── 2022           └── 32               └── items.json    └── tags        └── show-hn            └── items.json

1-raw 用来存放从Hacker News或其他站点抓取的原始数据

使用以下 adapter,把原始数据格式化到2-formated目录下:

adapters├── devto.ts├── googlenews.ts├── hn.ts├── hn_test.ts├── lobste.ts├── lobste_test.ts├── mod.ts├── newyorker.ts├── ph.ts├── reddit.ts├── rss.ts├── source.ts├── twitter.ts└── twitter_test.ts

格式化后的数据长这样:

{  "id": "en_hn_2022_09_02__32677727",  "url": "https://www.commonsense.news/p/will-i-ever-see-the-36-million-oberlin",  "date_published": "2022-09-02T01:35:39.646Z",  "date_modified": "2022-09-02T01:35:39.646Z",  "_original_published": "2022-09-01T15:25:05.000Z",  "_original_language": "en",  "_translations": {    "en": {      "title": "Will I Ever See the $36MM Oberlin College Owes Me?"    }  },  "authors": [    {      "name": "fortran77",      "url": "https://news.ycombinator.com/user?id=fortran77"    }  ],  "_score": 200,  "_num_comments": 194,  "external_url": "https://news.ycombinator.com/item?id=32677727",  "image": "https://substackcdn.com/image/fetch/w_1200,h_600,c_limit,f_jpg,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb00ec25-b252-44f2-a06a-87cea18ea630_1440x1440.jpeg"}

我让它的格式尽量和jsonfeed保持一致,同时扩展少量的自有属性。格式化后,会删除1-raw的原始文件。

全部格式化完成之后,进入翻译环节,翻译程序只需要读取标准的 formated 文件,然后把_translations下的字段翻译成多个 target语言,翻译后文件存放在3-translated下,变成如下模样:

{  "id": "en_reddit_2022_09_02__r--Bogleheads--comments--x3bi2g--what_to_invest_in_mega_backdoor",  "url": "https://www.reddit.com/r/Bogleheads/comments/x3bi2g/what_to_invest_in_mega_backdoor/",  "date_published": "2022-09-02T01:35:40.828Z",  "date_modified": "2022-09-02T01:35:40.828Z",  "_original_published": "2022-09-01T16:07:45.000Z",  "_original_language": "en",  "_translations": {    "en": {      "title": "What to invest in mega backdoor"    },    "zh-Hans": {      "title": "投资于巨型后门的内容"    },    "zh-Hant": {      "title": "投資於巨型後門的內容"    },    "ja": {      "title": "メガ・バックドアに何を投資するか"    }  },  "tags": ["Bogleheads"],  "authors": [    {      "name": "oswestrywalesmate",      "url": "https://old.reddit.com/user/oswestrywalesmate"    }  ],  "_score": 3,  "_num_comments": 6,  "external_url": "https://old.reddit.com/r/Bogleheads/comments/x3bi2g/what_to_invest_in_mega_backdoor/"}

接下来进入存档环节,存档程序读取3-translated下的文件,根据文件的内容,把 items分别存放到:currentarchive目录下,同时删除3-translated下的文件:

比如current/hn/items.json下的文件,大概结构是这样:

{  "items": {    "en_hn_2022_09_02__32678664": {      "...": "..."    },    "en_hn_2022_09_02__32678665": {      "...": "..."    }  }}

这个文件会被永久存储,这就是当前网站的显示的所有条目,只存放最近 200 条数据。

同时每条数据都会在这个时候归档,归档,目前是按周存放的。比如archive/hn/2022/35/items.json存放第 35周的数据,同时archive/hn/tags/show-hn/items.json会存放Show HN标签下最近 200条的内容。数据格式和current/hn/items.json一致。

当 build hn 站点的静态网页时,只需要读取current/hn/items.json下的数据,然后按照模板渲染出对应的index.html,feed.json,feed.xml页面,然后上传至 cloudflare pages 即可。

对于动态页面,比如https://i.buzzing.cc/hn/tags/show-hn/,会实时读取archive/hn/tags/show-hn/items.json下的数据,然后按照模板渲染。

目前动态页面的程序部署在Deno Deploy上,current,archive,cache文件存放在Cloudflare R2上,静态网页部署在Cloudflare Pages上。同时使用Github Actions 来运行定时任务,每半个小时运行一次,20 多个站点,目前每次仅需要 3分多钟就能完成全部的更新,翻译,发布静态网页。对比以前,抓取数据的时间差不多,但是构建和发布一个静态网站的时间就要多达 30 分钟。

具体的 Github Actions 配置如下:

steps:  - name: Check out repository code    uses: actions/checkout@v3  - uses: actions/setup-node@v3    with:      node-version: 16      cache: "npm"  - name: install wrangler    run: npm install -g wrangler  - uses: denoland/setup-deno@v1    with:      deno-version: v1.x  - uses: actions/cache@v3    with:      path: |        ~/.deno        ~/.cache/deno      key: ${{ runner.os }}-deno-${{ hashFiles('**/*deps.ts') }}  - run: make prod-load    env:      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}      AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}      AWS_ENDPOINT: ${{secrets.AWS_ENDPOINT}}  - run: make install  - run: make prod-source    id: source    continue-on-error: true    env:      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}      AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}      AWS_ENDPOINT: ${{secrets.AWS_ENDPOINT}}      TWITTER_BEARER_TOKEN: ${{secrets.TWITTER_BEARER_TOKEN}}      TWITTER_ACCESS_TOKEN: ${{secrets.TWITTER_ACCESS_TOKEN}}      TWITTER_ACCESS_TOKEN_SECRET: ${{secrets.TWITTER_ACCESS_TOKEN_SECRET}}      TWITTER_CONSUMER_SECRET: ${{secrets.TWITTER_CONSUMER_SECRET}}      PRODUCTHUNT_TOKEN: ${{secrets.PRODUCTHUNT_TOKEN}}      TRANSLATE_TOKEN: ${{secrets.TRANSLATE_TOKEN}}  - name: upload files    run: make prod-upload    env:      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}      AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}      AWS_ENDPOINT: ${{secrets.AWS_ENDPOINT}}      DUFS_URL: ${{secrets.DUFS_URL}}      DUFS_SECRETS: ${{secrets.DUFS_SECRETS}}  - name: throw if source failed    if: steps.source.outcome == 'failure'    run: |      echo "::error::Source build failed"      exit 1  - name: Publish pages    run: make prod-publishall    env:      CLOUDFLARE_ACCOUNT_ID: ${{secrets.CLOUDFLARE_ACCOUNT_ID}}      CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_API_TOKEN}}

最后,使用Pipedream,每一个小时拉取 feedjson,然后分别发布到Twitter:@HackerNewsZh,@NewsBotZh,@AskRedditZh.

小细节🔗

Youtube 视频的懒加载🔗

如果你引入 Youtube 视频的全部 Embed 代码,那么你的网页每次会加载 Youtube内部超多的资源,这很不道德,所以我们可以用iframesrcdoc属性来实现只加载一个预览页面,而不实际加载整个iframe。当用户主动点击播放的时候,这个时候才去真正的 loading,效果如下:

具体代码如下:

<iframe  class="embed-video"  loading="lazy"  src="https://www.youtube.com/embed/${embedUrlVideoId}&autoplay=1"  srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/${embedUrlVideoId}?autoplay=1><img src=https://img.youtube.com/vi/${embedUrlVideoId}/hqdefault.jpg loading='lazy' alt='Youtube Preview Image'><span>▶</span></a>"  frameborder="0"  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"  allowfullscreen></iframe>

总结🔗

用 Deno 做完这一切之后,只能用神清气爽来形容(可以说这是我最满意的一次应用结构设计)。再也不怕新增站点了! Deno 开箱即用的 Typescript支持,以及 Deno Deploy 的无缝部署都让我非常享受,同时 Deno生态的不完整性,也让我更少的依赖别人的东西,整个网站做下来感觉非常的健壮。也许谁有时间可以利用站点提供的feed.json来生成一个体验良好的 APP.

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Deno Buzzing 信息聚合 内容翻译 技术分享
相关文章