欧洲旅游VLOG | 巴塞罗那 罗马 梵蒂冈 威尼斯 米兰 苏黎世 因特拉肯 巴黎
2022年12月
2022年12月
我有一个维护了六年的开源项目—— RSSHub,它正在面临崩溃
表面上,它有接近 30k Stars、900 多 Contributors、每月 3 亿多次请求和数不清的用户、每月几十刀的赞助、有源源不断的 issue 和 pr、代码几乎每天更新,非常健康和充满活力,但在不可见的地方,持续数年高昂的维护时间成本、每月一千多刀的服务器费用、每天重复繁琐且逐渐积累的维护工作,都让它在崩溃的边缘反复横跳
项目是六年前开发的,不少当时以 Next Generation 为口号的时髦 Node.js 技术栈和依赖库已经成为时代眼泪,现在看非常陈旧,很多现在流行的新技术没法应用,比如 JSX、TypeScript、Serverless 等;它的架构也非常不合理,每个路由的信息散落在多个地方,开发或者变更一个路由需要多处修改,一个地方去注册路由,一个地方去编写路由脚本,一个地方去编写 Radar 规则,一个地方去编写文档…这增加了很多工作量,也很容易出错,之前路由少的时候并不是个问题,但现在已经变得难以忍受
在如此糟糕的基础架构下能保持现状已经是竭尽全力,开发新功能更是无本之木,只会增加以后更新的难度,所以我有时候脑子蹦出的新奇想法也很难实现
要解决这些问题,唯一的办法是使用现代化的框架和新设计的架构来重写内核,但随着路由越来越多,改造成本也越来越高,每个基础改动可能都需要多达数月的工作量,所以虽然问题越来越严重,但秉承着又不是不能用的原则一拖再拖
但这又是不得不做的事情,所以我抽空花了几个月的时间重新设计和重写了它
第一步也是最基础和难度最大的是换掉之前使用的 Web 框架 koa,作为六年前流行的下一代 Web 框架,作者早就弃坑了,调研之后决定换用对 JSX、TypeScript、Serverless 支持最好的 Hono
它们的 API 差异很大,需要重写所有中间件和替换所有路由中使用的 koa API
主要改动: https://github.com/DIYgod/RSSHub/pull/14295
![]()
Hono 作者也很喜欢这个改造
https://twitter.com/yusukebe/status/1762801106340782222
改用 TypeScript 可以避免很多类型问题和低级错误,最重要的是可以保证数百名贡献者保持一致难以出错和后续贡献的路由代码质量不至于太糟糕
主要改动:
![]()
https://twitter.com/DIYgod/status/1764360942035312879
ESM 是几年前一些 Node.js 核心开发者强推的规范,它有一些优点,但最多的是与之前 CommonJS 不兼容带来的生态割裂和功能简化带来的诟病
经过这几年的发展,现在可以说大部分场景勉强可用了,tsx 也为 CommonJS 和 ESM 混用的场景提供了支持
虽然已经尽了最大努力,但还是有一些 CommonJS 代码暂时难以迁移,导致现在只能使用 tsx 运行,与一些 Serverless 比如 Vercel 没法兼容,但也有机会后续慢慢解决
主要改动:
![]()
![]()
art-template 是一个支持 koa 的模板引擎,记得六年前还有一个更流行的模板引擎,但是不记得名字了,选用 art-template 是因为那个更流行的我当时没看懂,这个很简单
Hono 自带了 JSX 支持,JSX 就不用多介绍了,根正苗红的 JavaScript 的语法扩展,等同于用 React
主要改动:
Jest 是曾经流行的测试框架,但是在 ESM 时代到来之后就越来越不行了,对 ESM 的支持一直是实现性「experimental support」,现在更流行的是 Vitest 了
主要改动: https://github.com/DIYgod/RSSHub/commit/38e42156a0622a2cd09f328d2d60623813b8df28
目前使用的 Got 也已经是不积极维护的状态了,也没有找到好的替代品,后续也许会换成原生 Fetch 或者自封装的 Fetch,还没有动手
我自己能力还是不够的,在与社区开发者们讨论的过程中学习和改进了很多,过程很有意思:https://github.com/DIYgod/RSSHub/issues/14685
主要改动: https://github.com/DIYgod/RSSHub/pull/14718
![]()
新标准主要为了解决路由信息过于分散的问题,这次应该算第三版
第一版来自 RSSHub 开发阶段,当时没有预见到路由数量会有这么多,所以几乎没什么规划,所有路由在同一个文件中注册,然后再去增加路由脚本和文档,后来这个文件越来越大,很容易冲突,另外所有路由脚本都会在启动阶段被加载,程序性能越来越差
第二版来自 NeverBehave 维护的时期,引入了命名空间,切割了 router.js、radar.js,同命名空间的路由集中在了一个同文件夹中和一个或多个 Markdown 文档中,还实现了懒加载,极大提升了可维护性和性能,但还是会分散在多个文件中,不同文件的信息也容易出现不一致导致错误
本次把路由文件分为了两类,namespace.ts 和任意名字的路由文件
namespace.ts 会通过导出名为 namespace 的对象来定义命名空间的信息
import type { Namespace } from "@/types";
export const namespace: Namespace = {
// ...
};
namespace 包含的字段通过 TypeScript 限制为
interface Namespace {
name: string;
url?: string;
categories?: string[];
description?: string;
}
这些信息会经过编译后被文档和 RSSHub Radar 利用
路由文件会通过导出名为 route 的对象来定义路由的信息
import { Route } from "@/types";
export const route: Route = {
// ...
};
route 包含的字段通过 TypeScript 限制为
interface Route {
path: string | string[];
name: string;
url?: string;
maintainers: string[];
handler: (ctx: Context) => Promise<Data> | Data;
example: string;
parameters?: Record<string, string>;
description?: string;
categories?: string[];
features: {
requireConfig?: string[] | false;
requirePuppeteer?: boolean;
antiCrawler?: boolean;
supportRadar?: boolean;
supportBT?: boolean;
supportPodcast?: boolean;
supportScihub?: boolean;
};
radar?: {
source: string[];
target?: string;
};
}
之前 route.js mantainer.js radar.js 和文档的信息都被集中在这一个文件中,减少了多处定义也减少了出错的可能
实现逻辑就是开发环境通过遍历整个 route 文件夹,找到所有 namespace.ts 和路由文件,读取信息,加载路由,在生成环境使用提前编译好的路径列表来避免遍历和不必要的加载过程,代码在:https://github.com/DIYgod/RSSHub/blob/master/lib/registry.ts
文档也是通过遍历 route 文件夹,找到所有需要的信息然后合成一系列的 Markdown 文件,不再需要手动维护,代码在:https://github.com/DIYgod/RSSHub/blob/master/scripts/workflow/build-routes.ts
当然使用之前路由标准开发的路由都需要迁移到新标准而不是直接放弃掉,已经通过脚本批量抓取整理信息后做了替换,但特别是文档比较混乱也有很多错误,所以抓取的信息也有很多错误,只能在后续逐渐人工修改了
通过这一系列改进,RSSHub 终于能够扔掉历史包袱,安心开发新功能了,这里列出我积累的一些想法抛砖引玉:
最后,开源是一件很昂贵的事情,RSSHub 能活到现在离不开这些开发者的帮助
以及这些赞助的好心人
如果 RSSHub 正在帮助你,也希望你可以积极参与进来,为信息自由的未来贡献一份自己的微小力量
Information freedom does not naturally evolve, it degrades.
—— Open Information Manifesto
Twitter 在 8 月决定了全面限制公开访问和 API 接口,导致第三方集成均无法再正常工作。开放用户数据被绑架成私人敛财工具,曾经的 Open Web 标杆 Twitter 竟沦落到这种境地,数字奴隶制在最不应该的地方出现,令人唏嘘。这也致使许多用户流向 Fediverse,但社交关系和习惯一旦形成,要让其迅速改变并不易,更多人还是选择了忍受,Musk 也是看穿了这一点才有恃无恐
然而,我们也不能武断地说 Twitter 封闭,毕竟它仍然开放了一个起步价为每月 4 万美元,上限不设的企业 API
![]()
什么?你说用不起?
那么你可以像我一样,通过创建一万个账号以绕开封锁
尽管 Twitter 限制了所有公开访问,但我们发现新下载的 Twitter 移动客户端仍可以正常查看用户动态。这为我们提供了潜在的利用方法,通过抓包,我们可以看到客户端是通过请求一系列特殊接口来创建一个权限较低、频率限制严格的临时账号。我们可以用这个账号获取我们需要的大部分数据。然而,这种账号对请求频率的限制非常严格,因此需要大量的这样的账号才能满足基本的使用需求。同时,每个 IP 在一段时间内只能获取一个临时账号,因此我们也需要大量的 IP 代理
具体拆包和抓包过程可以参考 BANKA 的《怎么爬 Twitter(Android)》。站在 BANKA 肩膀上,我们可以写出一个这样的注册脚本(来自 Nitter - Guest Account Branch Deployment):
#!/bin/bash
guest_token=$(curl -s -XPOST https://api.twitter.com/1.1/guest/activate.json -H 'Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F' | jq -r '.guest_token')
flow_token=$(curl -s -XPOST 'https://api.twitter.com/1.1/onboarding/task.json?flow_name=welcome' \
-H 'Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F' \
-H 'Content-Type: application/json' \
-H "User-Agent: TwitterAndroid/10.10.0" \
-H "X-Guest-Token: ${guest_token}" \
-d '{"flow_token":null,"input_flow_data":{"flow_context":{"start_location":{"location":"splash_screen"}}}}' | jq -r .flow_token)
curl -s -XPOST 'https://api.twitter.com/1.1/onboarding/task.json' \
-H 'Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F' \
-H 'Content-Type: application/json' \
-H "User-Agent: TwitterAndroid/10.10.0" \
-H "X-Guest-Token: ${guest_token}" \
-d "{\"flow_token\":\"${flow_token}\",\"subtask_inputs\":[{\"open_link\":{\"link\":\"next_link\"},\"subtask_id\":\"NextTaskOpenLink\"}]}" | jq -c -r '.subtasks[0]|if(.open_account) then {oauth_token: .open_account.oauth_token, oauth_token_secret: .open_account.oauth_token_secret} else empty end'
以及这样的批量注册脚本(来自我自己):
const got = require('got');
const { HttpsProxyAgent } = require('https-proxy-agent');
const fs = require('fs');
const path = require('path');
const concurrency = 5; // Please do not set it too large to avoid Twitter discovering our little secret
const proxyUrl = ''; // Add your proxy here
const baseURL = 'https://api.twitter.com/1.1/';
const headers = {
Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F',
'User-Agent': 'TwitterAndroid/10.10.0',
};
const accounts = [];
function generateOne() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const timeout = setTimeout(() => {
// eslint-disable-next-line no-console
console.log(`Failed to generate account, continue... timeout`);
resolve();
}, 30000);
const agent = {
https: proxyUrl && new HttpsProxyAgent(proxyUrl),
};
try {
const response = await got.post(`${baseURL}guest/activate.json`, {
headers: {
Authorization: headers.Authorization,
},
agent,
timeout: {
request: 20000,
},
});
const guestToken = JSON.parse(response.body).guest_token;
const flowResponse = await got.post(`${baseURL}onboarding/task.json?flow_name=welcome`, {
json: {
flow_token: null,
input_flow_data: {
flow_context: {
start_location: {
location: 'splash_screen',
},
},
},
},
headers: {
...headers,
'X-Guest-Token': guestToken,
},
agent,
timeout: {
request: 20000,
},
});
const flowToken = JSON.parse(flowResponse.body).flow_token;
const finalResponse = await got.post(`${baseURL}onboarding/task.json`, {
json: {
flow_token: flowToken,
subtask_inputs: [
{
open_link: {
link: 'next_link',
},
subtask_id: 'NextTaskOpenLink',
},
],
},
headers: {
...headers,
'X-Guest-Token': guestToken,
},
agent,
timeout: {
request: 20000,
},
});
const account = JSON.parse(finalResponse.body).subtasks[0].open_account;
if (account) {
accounts.push({
t: account.oauth_token,
s: account.oauth_token_secret,
});
} else {
// eslint-disable-next-line no-console
console.log(`Failed to generate account, continue... no account`);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`Failed to generate account, continue... ${error}`);
}
clearTimeout(timeout);
resolve();
});
}
(async () => {
const oldAccounts = fs.readFileSync(path.join(__dirname, 'accounts.txt'));
const tokens = oldAccounts.toString().split('\n')[0].split('=')[1].split(',');
const secrets = oldAccounts.toString().split('\n')[1].split('=')[1].split(',');
for (let i = 0; i < tokens.length; i++) {
accounts.push({
t: tokens[i],
s: secrets[i],
});
}
for (let i = 0; i < 1000; i++) {
// eslint-disable-next-line no-console
console.log(`Generating accounts ${i * concurrency}-${(i + 1) * concurrency - 1}, total ${accounts.length}`);
// eslint-disable-next-line no-await-in-loop
await Promise.all(Array.from({ length: concurrency }, () => generateOne()));
fs.writeFileSync(path.join(__dirname, 'accounts.txt'), [`TWITTER_OAUTH_TOKEN=${accounts.map((account) => account.t).join(',')}`, `TWITTER_OAUTH_TOKEN_SECRET=${accounts.map((account) => account.s).join(',')}`].join('\n'));
}
})();
这些脚本已放到了 RSSHub 仓库: https://github.com/DIYgod/RSSHub/blob/master/scripts/twitter-token/generate.js
在使用前,你需要填入你购买的 IP 代理服务地址。脚本会自动处理超时、请求等错误,并且以 5 并发来自动获取临时账号,当获取到 1000 个账号后将会停止。需注意并发不要设置得过高,我从观察发现,当 Twitter 发现大量请求时会暂停接口一段时间
我购买了 5 家代理服务以进行测试,感觉效果相差无几,选择一个最便宜的服务就可以。通常,最低价的 1G 套餐就足够获取大约几万到十几万个账号了。我目前找到的最便宜的服务是 proxy-cheap,如果你有更好的选择请告知我
这种方法已经在 Nitter 上稳定运行了一段时间,现在也已实装到了 RSSHub 及其官方示例上,我们可以宣布与邪恶 Twitter 奴隶主的战争已经阶段性胜利
🕊️ 本文送给更开放的互联网
起因是看到 @geekbb 介绍 Warp 的推文。尽管 Warp 已经发布了很长时间,就保护 IP 隐私而言,它并没有 iCloud Private Relay 好用,我也没有魔法上网的需求。但是我突然意识到,我还是有隐藏 IP 的需求。
在开发 RSSHub 的几年中,我发现提供公共 API 的站点非常少,许多站点还会采取严格的反爬控制来限制其平台内容的获取。有些站点会屏蔽同一 IP 发出过多请求,而还有一些站点则会全面屏蔽常见云服务器厂商的 IP 地址。因此,仅仅为了获取最新几条内容更新却变得非常困难。
![]()
这种情况需要使用代理,但是专门的爬虫代理通常价格昂贵,性价比极低,如果 Cloudflare WARP 的无限流量和丰富的 IP 资源能被 RSSHub 利用就太棒了。RSSHub 已经支持了通用的代理协议,只要能将 WARP 包装为通用的 proxy 就可以。
![]()
虽然无法直接在命令行环境中方便地使用官方客户端,但这么容易想到的点子肯定已经被别人实现过了。我在 GitHub 上找到了一个封装的 Docker。
然后只需要在 RSSHub 的 docker-compose.yml 中再添加这样一个 service 来启用代理服务
warp-socks:
image: monius/docker-warp-socks:latest
privileged: true
volumes:
- /lib/modules:/lib/modules
cap_add:
- NET_ADMIN
- SYS_ADMIN
sysctls:
net.ipv6.conf.all.disable_ipv6: 0
net.ipv4.conf.all.src_valid_mark: 1
healthcheck:
test: ["CMD", "curl", "-f", "https://www.cloudflare.com/cdn-cgi/trace"]
interval: 30s
timeout: 10s
retries: 5
最后给 RSSHub 加一个 PROXY_URI 环境变量来使用代理
PROXY_URI: 'socks5h://warp-socks:9091'
我选取了一个我经常使用的 hotukdeals 路由(英国版的什么值得买)进行测试。该站点会屏蔽所有 DigitalOcean 的 IP,因此一直处于 403 状态。
![]()
加上 WARP 后可顺利访问
![]()
此外,我发现每次重启 WARP 时,都会输出新的 IP。尽管我没有时间验证,但我感觉 IP 应该会经常自动更改,这对解决反爬是一个好消息。
![]()
还可以进一步自定义 WireGuard 的配置,包括使用付费版 WARP+ 和自定义 endpoint,以获取可能更好的结果。
生成 WireGuard 配置文件可以使用
刷 WARP+ 流量和筛选 endpoint 可以使用
有说法是 WARP+ 的速度并无明显差异(《WARP、WARP + 速度对比,以及 WARP 速度上限》),但是是否影响反爬效果还需要进一步验证。
如果一切顺利,RSSHub 官方实例中许多严格反爬的路由应该能重新使用。我将在几天内进行验证并在此更新。
![]()
我一直将个人博客视为一个理想的展示个人 IP 的 “个人网站”,而不仅仅是发布文章的平台。我曾在 2014 年初学编程时使用 WordPress 建站 《世界,你好!》;入了前端坑后,在 2017 年我转向了 Hexo 《做了一点微小的改动》;Web3 飞升后 2022 年我换成了 xLog 《第一个开源链上博客系统 xLog》。然而,无论我使用什么博客系统,一直都存在一个问题,那就是如何优雅地汇集和展示我在其他平台发布的作品,最好还能直接显示外站的数据。我之前通常以文章形式发布作品,并在文章中附上链接,然而这样做显然不够优雅,读者还需要额外点击链接进行跳转。
我在学习达芬奇剪辑时,发现了影视飓风的网站,它通过外链方式列出了他们在 B 站发布的视频,其中包括标题、封面图、发布时间、播放量等信息。这个发现给了我启发,我完全可以在 xLog 上制作一个装载了我在各个平台作品的作品集,这里面可以有我发布在 B 站的视频、我在 GitHub 上维护的仓库、我参与的小宇宙播客甚至是我在 pixiv 上创作的画作。这样,当人们访问我的博客时,将不只是看到文章,而是会看到更丰富多元的我,这让我的博客更接近一个真正意义上的 “个人网站”。
想法萌发后,实现就简单了。
![]()
![]()
og:image og:title og:description og:date![]()
![]()
![]()
如今,这个简单实用的小功能已经落地实现了,可以看看我的作品集页,你是否也想要尝试在 xLog 建立属于自己的个人作品集呢?