普通视图
基于 Astro Paper 的个人博客:深度定制和部署实践
一路向南,骑见江南:一人、一车、一旅途
新手跑步第五次:单人挑战不间断半马
记录我的前3次跑步:从陪跑到主动出发
Strava Riding Api 上线
EasyFill 发布了
注册 Chrome Web Store 开发者
利用 Go + COS + GitHub 重构 RSS 爬虫
Blog Function Update 2025 (2)
Blog Function Update 2025 (1)
骑行 桃花峪、黄河文化公园
十月份看完的第一本书
写一个骑行页面(二)
与时间抗衡:笔记本清灰换硅脂记
写一个Chrome表单自动化插件
七月最后一天骑行,有氧100公里
利用Go+Github Actions写个定时RSS爬虫
Tencent CDN 流量被恶意盗刷
公路车管胎被扎,怎么补胎
喜提新车 Wilier Cento 10SL
年味越来越淡,没有感觉
解决Jekyll时区数据源
初一大吉,博客上上新
腾讯云COS文件跨域
Ubuntu Server部署日记
PHP 笔记(长期更新)
爱上Go语言:常量与枚举
爱上Go语言:变量定义与内建变量类型
Manjaro Linux 双显卡切换解决方案
Manjaro Linux 自动禁用触摸板
Docker Compose 快速构建 LNMP 笔记
(转)一个关于if else容易迷惑的问题
使用开源项目免费申请 JetBrains 开源许可证
JetBrains 官方推中文简体包了
ThinkPHP 6.0 下载安装与配置
Manjaro KDE 调教日记
(转)谈 Linux,Windows 和 Mac
(转)为什么犹太人如此优秀?
在 CentOS 下实现 MySQL 数据库定时自动备份
阿川的个人博客 "创建一周年啦!"
说说消失这二十多天发生的事情
ThinkPHP Save 方法引发的错误
爱上Go语言:终端命令
(转)一个程序员眼中的价值
2019-05-22 博客功能更新记录
(转)我眼中的技术高手
ionic 笔记 (环境搭建与命令)
Cmd 笔记(长期更新)
开源分布式管理控制系统 —— Git 笔记
推荐 10 个优秀的技术栈社区
快速定位 Apache 错误
_initialize() 和 __construct() 的区别
Jekyll 环境搭建 (Windows 10)
ThinkPHP 笔记(路由实现与操作方法)
(转)微不足道的坚持
Redis 笔记(安装与数据类型)
Linux 命令笔记(长期更新)
解决ubuntu:E:无法获得锁(11:资源暂时不可用)
(转)论胡编乱造的写作技巧
更换了域名、邮箱并修复了移动端 Bug
面向对象(Object Oriented)学习笔记(一)
致简写作神器 —— Markdown
(转)如何学习开源项目
(转)为什么你写不好一个快速排序? 谈程序员的职业发展
Ubuntu 18.04 下安装 Sublime Text3 并解决它的疑难杂症
一路向南,骑见江南:一人、一车、一旅途
4月24日,递交完辞职报告的那一刻,我心中一阵轻松。我终于可以离开了,离开熟悉的一切,前往下一个未知但让我心跳的地方——义乌。
原本的计划是从郑州骑行到义乌,约一千公里。然而,我的长途骑行装备都还在老家淮阳,不得不先折返一趟,郑州到淮阳≈210公里。
第一站:淮阳
4月26日,天还没亮我就起床了,所有行李早已打包妥当,多亏了顺道的好大哥和双双姐姐,他会把行李直接送到义乌,精准投放到阿丽家门口。所以我只需要背上包、骑车走就行了。
临出门前,我把钥匙存放在小区门口的保安亭,与保安打了招呼,等双姐有空再来取。
05:42 AM 郑州·孙庄北院
在小区南门吃了碗大米红枣粥,这是我在郑州的最后一顿早餐。我吃得很慢,望着小区,心里百感交集。最不舍的,是我的双姐,直到离开,我们也没有一张合影。唉……今朝一别,不知何日再见。
10:49 AM 开封·张市桥
从出发到现在,已骑行近百公里(99.81公里),历时4小时15分。中途几乎没停,一直到开封张市桥,看到一位大爷坐在家门口听戏,我停下来敬了他一根烟,顺势在他门口歇了口气。我实在太饿了,得找点吃的。
骑行至此,除了背包里三根香蕉,还有昨晚和双姐吃剩的半盒牛肉,没有其他补给。吃完香蕉后抽了两根烟,心理上稍有缓解,重新背上包,继续赶路,准备找个地方补点碳水。
11:52 AM 周口·扶沟
点了一碗烩面和一个鸡爪,牛肉是我自带的。说实话,第一次见到餐桌上这么干净的盘子…鸡爪也很入味,咬一口我就扔了,太清真了
骑到汴岗镇时,看到一家超市,顺便买水时偶遇了渤哥。
经过东夏亭镇人民政府旁的一条支流,干涸得厉害,河床都裸露出来了。不知是人为抽干,还是自然干旱所致。在这段行程中,这样的场景我已经见过好几次。
5:39 PM 周口·淮阳
全程逆风,路上每一米都艰难,越过扶沟、西华,路过麦田,虫子爬满全身,骑得越快身上越多,还有像蛆一样的小东西全身都是,包括脸上,这六十多公里真的折磨,全程都是虫。
本次行程装备大致如下:
-
Challenge ELITE 700×25c 管胎 x1
-
Topeak 多功能工具组
-
康比特盐丸 x10包
-
CUKTECH 15 SE 移动电源
-
前后尾灯
-
便携打气筒
-
备用袜子 x4双
-
三合一充电线 若干
左边青蓝色的包是我从郑州背回来的,背负系统不科学,透气性也差,只适合日常出行,不适合长途骑行,这次回来扔家里让他吃灰。这次换上了右边的迪卡侬骑行包,内置水袋设计很实用,咬咬吸嘴就能补水,长途骑行相当方便。
韶音耳机声音太小,周围稍有噪音就听不见。干脆把BOSE音箱扎带绑车上,没有音乐我不能活。
04-28 6:55 AM 淮阳·龙湖新城
奶奶身体最近不太好,高血压住院了,所以骑行晚走了一天。
这是我在淮阳的最后一顿早餐:胡辣汤、鸡蛋饼、两个鸡蛋。早上的蛋尤为重要,它决定了我今天能不能顺利完成骑行
刚吃完早餐,习惯性的捏捏车胎,发现车胎气压不足,我当时就觉得不对劲,很大概率已经爆胎了。当我拿打气筒充气时,气嘴滋滋漏气,我当时就服了。这次行程我就备了一个管胎,这种胎很贵,没那么多资金支持我买一堆备胎,管胎这种轮胎和其他胎不一样,换胎要除胶贴胶费时费力,你要说补胎,我估计一般的小品牌技师都没摸过管胎,手头宽裕了,我一定要把这轮组先换了,它不支持管胎以外的其他胎型。
真是服了,啥好事都让我碰上了
12:12 AM 安徽·阜阳·太和县
骑行105公里到达阜·太和县,途中遇到很多有意思的场景,就是没有拍照,天气实在太热了,骑行的过程中实在懒得下来拍照。
18:02 PM 安徽·淮南·凤台县
骑行202公里到达淮南·凤台,吃个晚饭,点了一晚大肉水饺还有一份鸭腿,这点饭量对骑行时的我是完全不够的,她们家的绿豆汤是免费的,不稀比较稠,而且还是免费的,我连着吃了三碗…
本来想着吃完饭继续骑,奈何自己看见宾馆走不动路,想想两天也到不了,不急这一会儿。
到了屋里第一时间扒个精光,实在太热了,热了干,干了湿,浑身都很黏,
04-29 6:55 AM 安徽·淮南·凤台县
早餐还是老三样,这次的粥我多加了糖,高糖分有助于我长途骑行
11:12 AM 安徽·淮南·田家庵区
路过淮南市区买了5根香蕉,这天实在太热了,隔着手套都能感觉到香蕉是热的
12:32 AM 安徽·合肥·长丰县
这个披萨是我吃过最难吃的,已经不能称为披萨了,速冻的薄饼加热了一下,这顿饭是在蜜雪冰城吃的,就为了只有他们店舍得在荒无人烟的地方开空调
当时看到这个场景真的很惊艳,一抹绿,在这里钓鱼露营一定很爽
太热了,找了阴凉地,坐在国道两波护栏歇会儿…
6:46 AM 安徽·合肥·肥东县
绝了,即出发后第二次爆胎,当时天已经黑了,还好不是在荒郊野外爆的,不然我就要提前体验田野生活了,补救的可能性很小,因为管胎里面的管子炸了,管胎这种东西不用考虑当地施救的可能性,实体店不会有管胎卖,修更别说了,几乎失传的手艺。
也怪我,左转速度太快,正好有个坡度看不到这缝,后轮当场巨响爆了!
爆胎后,我穿着锁鞋推行了两公里左右,找了一家宾馆住了下来,我也懒得检查哪漏气了,碳轮坏了没有,一切都不重要了,现在就想着咋带车去义乌,我没有车包,高铁肯定是上不了。
04-30 8:09 AM 安徽·淮南·瑶海区
经费紧张,去义乌我也有规划着时间,等不起在网上买管胎的代价,由于没有装车包,公路车无法上高铁,托运怕出事。在网上找了一个义乌直达大巴车,可以放下面,车费200,行李房120砍到88,合计费用比高铁还贵。
11:32 AM 江苏·南京·江宁区
到饭店了,客运公司开始收割了,大巴车把车停到了他们据点,周围几公里都没有商店,饭堂和商店的物价堪比上海浦东机场,有些人早有准备自带了泡面,但是人家的热水专供司机,不让旅客用,笑了。
3:16 PM 浙江·杭州·上城区·九堡大桥
看到这里差点掉小珍珠,之前在杭州上班时经常开车路过这里,Hi 杭州 好久不见。
时隔六年,再次为热爱脱皮。痛苦如影随形,却也因此更加坚定
新手跑步第五次:单人挑战不间断半马
“从跑步小白,到不间断半马需要多久?答案是六天,五次!”
Day 4:首次不间断 10 公里
2025年4月20日 · 跑步第四天。早上起床时心情很好,因为我发现大腿已经不疼了,想必是适应了十公里的运动量,我决定今天晚上下班后,开始挑战不间断十公里
晚上刚下班,我直接回家换了运动装,出了门正好看到篮球场在放电影《智取威虎山》,我喜欢露天电影的氛围,有空一定要去看一场
从小区北门起跑,沿着郑上路一路向南,经过郑州市实验小学、第一中学等地标,最后在T字形路口到达西流湖公园北侧门,目前里程为4公里左右,这条路被我骑车压过不知道多少次了,可这次是跑步,带给我的感觉不一样
进了公园右转是上坡,向左则为两条路可以选择:
- 公园外围下坡,路面宽阔、路灯明亮
- 岸边小道,下坡路况一般
这个桥亭设计了很多座位,栏杆也不高,下面是贾鲁河的支流,还做了一个闸口的设计,水流从上面流下来,下面是人工池,形成一个小瀑布的效果,非常适合路亚、溪流钓,台钓佬就省省吧,只能大跑铅
分段成绩
Km | 配速 | 海拔 | 心率 (bpm) |
---|---|---|---|
1 | 5′41″ | −2 m | 167 |
2 | 6′29″ | 0 m | 174 |
3 | 6′57″ | +2 m | 169 |
4 | 7′54″ | 0 m | 161 |
5 | 8′08″ | −12 m | 162 |
6 | 7′35″ | +13 m | 168 |
7 | 6′58″ | −4 m | 170 |
8 | 7′07″ | +1 m | 166 |
9 | 6′47″ | 0 m | 173 |
10 | 6′42″ | +2 m | 170 |
0.1 | 6′05″ | −1 m | 175 |
10 km 综合数据
指标 | 数值 |
---|---|
距离 | 10.14 km |
平均配速 | 7′01″ / km |
最快分段 | 5′41″ / km |
平均经过配速 | 7′05″ / km |
移动时间 | 1 h 11′ 08″ |
全程耗时 | 1 h 11′ 47″ |
平均心率 | 168 bpm |
爬升 | 35 m |
消耗卡路里 | 643 kcal |
Day 6:新手跑步第五次,单人 挑战 不间断 半马!
2025年4月22日 · 跑步第六天 · 第五次
今天我,昨天晚上忙着搬家没有跑步,处于内心的愧疚,我决定今天晚上把昨天的补回来,在跑之前我还不知道半马是什么意思
像往常一样,再次来到西流湖,里程来到了五公里,这段距离的心率有些高,心率区间在165-180,往后的数据都没有这个高
饮水没控制住,已经喝了550ml,当时非常兴奋,因为我即将跑返程了
跑到这里时,体力消耗的差不多了,双腿感觉十分僵硬,停一秒钟感觉都会导致后面跑步下去
到小区门口了,我感到非常兴奋,因为我即将完成我的第一次半马挑战,而且是不间断,除了中途买水,期间几乎没有停过
跑完站在家里,小腿和大腿没有疼痛感,唯一不舒服的就是双腿的膝盖关节处,活动就会有些疼痛
半马·分段成绩
Km | 配速 | 海拔 | 心率 (bpm) |
---|---|---|---|
1 | 5′24″ | −1 m | 170 |
2 | 5′51″ | 0 m | 178 |
3 | 6′23″ | +3 m | 180 |
4 | 6′40″ | −2 m | 177 |
5 | 7′45″ | −12 m | 165 |
6 | 8′28″ | +8 m | 161 |
7 | 7′52″ | 0 m | 162 |
8 | 7′28″ | +1 m | 164 |
9 | 6′52″ | −3 m | 171 |
10 | 7′21″ | −8 m | 166 |
11 | 8′46″ | −3 m | 158 |
12 | 7′04″ | −1 m | 171 |
13 | 8′13″ | +1 m | 164 |
14 | 7′11″ | 0 m | 170 |
15 | 7′17″ | +1 m | 170 |
16 | 7′43″ | +10 m | 167 |
17 | 7′28″ | +2 m | 170 |
18 | 7′26″ | 0 m | 170 |
19 | 7′22″ | −1 m | 170 |
20 | 7′37″ | +2 m | 167 |
21 | 7′37″ | +2 m | 166 |
22 | 7′02″ | 0 m | 172 |
0.9 | 6′34″ | +1 m | 176 |
半马 · 数据一览
指标 | 数值 |
---|---|
距离 | 22.96 km |
平均配速 | 7′17″ / km |
最快分段 | 5′24″ / km |
平均经过配速 | 7′18″ / km |
移动时间 | 2 h 47′ 07″ |
全程耗时 | 2 h 47′ 28″ |
平均心率 | 169 bpm |
爬升 | 69 m |
消耗卡路里 | 1450 kcal |
到家准备脱裤子时才发现,我中午买的第一条跑步用的紧身裤标签还没摘,现在已经被汗水浸湿烂掉了,让我有种破茧的感觉
记录我的前3次跑步:从陪跑到主动出发
“从讨厌到上瘾,原来跑步也能这样有趣”
我一直是个不爱运动的人,尤其讨厌跑步。打小起,我对跑步总是敬而远之
这次之所以开始跑步,完全是被阿坤和阿丽带动的
起初只是想着陪他们减肥,没想到,从第三天开始,我居然有点跑上瘾了
Day 1:人生第一次 5 公里(其实只跑了 3 公里)
第一次跑步是阿坤叫我的,他想减肥,我就陪他出来遛弯。他说目标是 5 公里,结果我们大半时间都在走路,实际上只跑了 3 公里
他有点胖,体力跟不上,但我直到活动结束都没有什么感觉
跑步 Day 2:不间断 5 公里初体验
第二天我刚下晚班(19:00),我打电话问阿坤什么时候出发,他说八点半。我不想等太久,就先回家收拾一下便出门了
第一天穿板鞋和牛仔裤实在太难受,这次吸取了教训,只穿了短裤、速干背心和跑鞋
站在小区门口花两分钟热热身,把软件都打开便开始跑了
刚开始跑到 0.86 公里 时,心率就达到了 183,但呼吸还算平稳
跑到 2 公里时,心率稳定在 168–170,最终顺利完成不间断五公里,一点都不觉得累,只是非常口渴
跑完后在楼下买了瓶水,还给阿坤发了个微信。结果瓶盖还都没拧开,就下起了暴雨,就像是天上开了个花洒一样,很突然…
阿坤因为下雨就没出门,我们在老地方随便吃了点东西聊聊天。准备回家时,我才发现自己腿已经快站不直了,大腿疼得厉害
跑步 Day 3:加码挑战,十公里!
早上起床,大腿肌肉酸痛,走路都不太舒服,走路都一瘸一拐的,就像当初刚学骑自行车一样,这种酸爽的痛感,反倒让我有点兴奋
出门碰头时,阿坤说想骑我的自行车,我说你骑吧,我跑步
相比昨天,今天的心率平稳多了,基本维持在 150–160。跑到 6.59 公里时,心率才到 181,那一刻我只觉得跑步,真的爽!掌握节奏之后,压根不想停下来!
跑着跑着来到奥体,正好赶上徐佳莹的演唱会。场外摆摊的特别多,还有个露天KTV,这种我是第一回次见,他们的声音是真大,我在 2 公里外就听见了,没一会儿,三四个保安冲过来大喊:“里面在开演唱会呢!”结果一个大妈拿着话筒回了一句:“演唱会咋了,演唱会咋了!” 笑死我了
之后我们绕着奥体转了一圈,发现个室外健身区,有很多器械,比如健身单车,还支持联网进行在线竞赛,而且运动数据可同步app,最重要的是全部免费!
返程时演唱会刚结束,整个奥体路被堵得水泄不通,到处是人和出租车
其实,今天的十公里多少有些违心,因为我实际只跑了 8.25 公里,剩下的两公里是骑车,阿坤说骑不动了,让我骑车,他跑着…
Strava Riding Api 上线
该脚本基于 Strava API v3 获取指定用户当年的所有骑行活动数据,并将其保存为JSON格式
功能特性
Strava Riding Api 只实现了 OAuth 2.0 授权流程的部分自动化,由于技术限制,目前无法实现完全自动化:
已实现部分
- 半自动 OAuth 2.0 授权流程,轻松访问您的 Strava 数据
- 自动获取任意年份的所有骑行记录
- 获取每个活动的完整运动数据
- 智能令牌管理:自动保存和刷新过期的访问令牌
- 数据自动转换:公里、时间、速度单位等数据格式化
- 内置多重容错机制,确保数据获取的可靠性
使用前设置
重要: 在使用此脚本前,请确保在Strava开发者平台上正确配置您的应用:
- 访问 Strava开发者设置
- 将以下URL添加到”授权回调域”:
localhost
注意:只需输入
localhost
而不是完整的http://localhost:8000
- 保存设置
使用方法
- 安装依赖:
yarn install
- 获取并处理授权码:
yarn auth
获取授权后,您会收到一个授权码。将其粘贴到命令行中。
- 获取骑行数据:
yarn start
- 查看输出的JSON文件,文件名格式为:
strava_data.json
解决认证问题
如果您遇到API相关错误,请尝试以下解决方案:
- 更新令牌:
yarn auth
重新获取授权并更新令牌
-
检查API状态:
访问 Strava API状态 确认服务是否正常
常见问题解决
- “protocol mismatch”错误:
- 此问题已在最新版本中解决,使用了原生HTTPS模块发送请求
- 确保在Strava开发者设置中添加了
localhost
作为授权回调域
- 无法获取活动数据:
- 确认您的账户中确实有骑行活动
- 检查筛选条件是否正确(默认只获取”Ride”类型活动)
- API错误或限流:
- Strava API有使用限制(每15分钟100次,每天1000次)
- 数据量大时,脚本已添加延迟以避免触发限流
许可证
本项目采用 Mozilla 公共许可证 2.0 版发布
Strava API v3:https://developers.strava.com/docs/reference
Strava Riding Api:https://github.com/achuanya/Strava-Riding-Api
EasyFill 发布了
就在刚刚 EasyFill 终于通过了 Chrome Web Store 的审核,正式发布了!
功能特性
- 智能填充:DOM 加载完后,自动读取表单插入数据。
- 无缝集成:与主流博客平台和评论系统兼容。
- 数据加密:通过 AES-GCM 加密和解密功能,保护用户数据安全。
- 现代化界面:基于 Material-UI 和 React 提供用户友好的界面。
安装
- 打开 Chrome Web Store
- 搜索
EasyFill
。 - 点击 添加到浏览器 按钮完成安装。
- 安装完成后,浏览器工具栏会显示 EasyFill 图标。
更新日志
查看 更新日志 了解最新功能和修复。
问题反馈
如果你在使用过程中遇到问题,请在我的博客留言。
支持作者
感谢您对我的支持,本人非程序员,忙里抽闲,为爱发电。
如果您觉得 EasyFill 对您有帮助,可以通过以下方式支持我继续创作:
许可证
本项目基于 Mozilla Public License Version 2.0。
Github 仓库:https://github.com/achuanya/EasyFill
✨ EasyFill 只为向那些在浮躁时代,依然坚守独立博客精神的你们致敬!
产品被拒
晚上下了班打开电脑刚坐下就看到了一封 Google 邮件,首先看到了发件人 “Chrome Web Store”,当时就心想提交审核一个多星期了,终于看到一点音信了。点开后,还没等我高兴,便看到了:
解决BUG
被拒的原因非常低级,声明了但未使用的 scripting
权限。
scripting 权限是 Manifest V3 中引入的一个重要权限,主要用于动态脚本执行chrome.scripting.executeScript()
和动态样式注入chrome.scripting.insertCSS()
而在EasyFill
中,使用的是静态声明:
content_scripts: [
{
matches: ['<all_urls>'],
js: ['content-scripts/content.js']
}
]
删除scripting
参数后,重新打包并再次向 Chrome Web Store 提交了扩展。
就这么一个小BUG,浪费了我一个星期的审核时间,太耽误事了,当时为了解决 Shadow DOM 才使用 scripting,直到现在这个问题也没有解决,希望下个版本可以解决问题
产品谍照:
注册 Chrome Web Store 开发者
年前曾尝试过 Chrome 扩展开发,《写一个Chrome表单自动化插件》,但是由于没有注册 Chrome Web Store 开发者,无法上传到 Chrome 应用商店。
注册 WildCard
Chrome 注册开发者需要五美元,由于我没有境外信用卡就一直卡在这,2022 年我在杭州办过一张中信的双币卡,年费很高,后来经济紧张时注销了,现在急着用外币还挺麻烦,折腾一圈,最终无脑选择了 WildCard,尽管网上对它负面评论铺天盖地。
WildCard 开卡费用是 10.99 美元/年,实际付款 79.71 人民币,按照今天的市场汇率 7.23,实际多付了 0.24,而且这只是开卡费用,充值另算。
开卡后我充值了 10 美元,支付宝付款 75.07,到账金额 10 美元:
\[\frac{2.77}{75.07} \times 100\% \approx 3.69\%\]四个点我能接受(接不接受都要受着),这个开卡费不便宜,毕竟钱不是大风刮来的,所以注册时,我创建了两个号,推荐注册返现两美金…
注册 Chrome Web Store 开发者
注册账号就很容易了,Google 绑卡付钱就行。但是如果要销售发布就很麻烦:
个人交易者声明
- 您需要提供一个手机号码以验证是您本人在操作
- 您将通过手机接收代码
- 用于证明是您本人的身份证件
- 可接受的文件包括:
- 驾照
- 护照
- 州身份证明
- 绿卡
- 您需要提供一份显示您的姓名和当前地址的文件
- 可接受的文件包括:
- 由政府签发的文件或带照片的身份证件
- 公共事业缴费单或话费账单(日期在过去 60 天内)
- 银行对账单(日期在过去 60 天内)
- 租赁合同或抵押贷款合同
因为 Google 已退出中国市场,不支持交易。而我是美国 Visa 卡,面对这样的要求不容易做到。
日后再说吧,往后这段时间,我打算把博客评论表单自动填充插件重构一下,然后上架 Chrome 应用商店。
空腹骑行75公里
周六
最近郑州天气突然转冷,骑行频率也降了下来,周六正好赶上休息,实在是憋坏了!今天不管刮风下雨,必须出去骑一趟
原计划直接奔开封,结果路过龙湖就停了下来。好久没来了,上次来还是鹅毛大雪天,如今雪没了,只剩下鹅
周六的公园人满为患,没法骑。我推着车沿湖边缓行,遥望着远处炸水的不知名鱼,不由自主的想蹲下摸摸湖水,真的很想钓鱼,自到郑州以来,我连最爱的路亚竿都没摸过
此时正值中午,小孩在沙滩上牵着风筝奔跑,大人排队买小吃,顿时勾起了不少儿时回忆,我也好想光着脚奔跑在沙滩上…
在龙湖公园出来后,我关掉了导航线瞎跑,根本不认识路,不知道自己在哪,扫大街呗
话说现在骑车很少拍照,不是不爱拍,而是懒得下车,即使趴到腰酸,感觉腰快要断了,也不想停下来
周日
拍这张照片时,已经快饿晕了,周六晚上吃得少,周日早上又空腹出门,体力消耗得厉害…
周日早晨睡到自然醒,一看表,我整个人都快立正了,居然八点半了。着急忙慌洗漱后,脱下内衣裤直接换上骑行服,背上包,拿了五块巧克力出发了。因为周一要上班,所以今天必须放纵一下,出发前大致算了算,来回返程再加上逛街的时间,早饭根本来不及吃…哎…
大约骑了25公里,在中石化买了瓶宝矿力水特。又骑行了二三十公里到了贾鲁河桥,饿的没劲,更别说爬坡了,挂上小盘,我慢悠悠到了桥中间,休息了几分钟,把五块巧克力补给全吃了
就这样空腹骑到了开封郊区,此时的里程已经来到了 75.38公里,用时3小时23分钟
到达开封后,心里那股坚持的信念瞬间消失了,又渴又饿,高德帮我找了最近一家名为三不炒(开封总店)的小炒店,我把车子靠着门店随便一放,就去买葡萄糖了
买完水出来发现要排队,人还不少,我是真的饿得快走不动了,但还是懒得换地方,抱着水坐在外面等了半小时。饿得快虚脱了,感觉此时此刻,就算把馒头挂我脖子上都能饿死
排队加吃饭花了一个半小时,吃得太撑,骑上车都趴不下去,推着车穿过老巷子走到了湖边
回到家已经八点半了,这条郑开大道路线真心推荐,毕竟二刷了,虽然沿途风景平平,但对于郑州来说,已经是顶级骑行路线了,一个人骑行在郑开大道,握着下把位,不用担心刹车,不用担心前方有没有人,听着歌,摇着车,也不枉来郑州走一遭
利用 Go + COS + GitHub 重构 RSS 爬虫
之前我写过一篇《利用Go+Github Actions写个定时RSS爬虫》来实现这一目的,主要是用 GitHub Actions + Go 进行持续的 RSS 拉取,再把结果上传到 GitHub Pages 站点
但是遇到一些网络延迟、TLS 超时问题,导致订阅页面访问速度奇慢,抓取的数据也不完整,后来时断时续半个月重构了代码,进一步增强了并发和容错机制
在此感谢 GPT o1 给予的帮助,我已经脱离老本行很多年了,重构的压力真不小,有空就利用下班的时间进行调试,在今天凌晨 03:00 我终于写完了
1. 为什么要重构
旧版本主要基于 GitHub Actions 的定时触发,抓取完后把结果存放进 _data/rss_data.json 然后 Jekyll 就可以直接引用这个文件来展示订阅,但是这个方案有诸多不足:
-
网络不稳定导致的抓取失败
由于原先的重试机制不够完善,GitHub Actions 在国外,RSS 站点大多在国内,一旦连接超时就挂,一些 RSS 无法成功抓取
-
单线程串行,速度偏慢
旧版本一次只能串行抓取 RSS,效率低,数量稍多就拉长整体执行时间,再加上外网到内地的延时,更显迟缓
-
日志不够完善
出错时写到的日志文件只有大概的错误描述,无法区分是解析失败、头像链接失效还是RSS本身问题,排查不便
-
访问速度影响大
这是主要的重构原因!在旧版本里,抓取后的 JSON 数据是要存储到 Github 仓库的,虽然有 CDN 加持,但 GitHub Pages 的定时任务会引起连锁反应,当新内容刷新时容易出现访问延迟,极端情况下网页都挂了
重构后,在此基础上进行了大幅重构,引入了并发抓取 + 指数退避重试 + GitHub/COS 双端存储的能力,抓取稳定性和页面访问速度都得到显著提升
2. 主要思路
2.1 整体流程
先看个简单的流程图
+--------------------------+
| 1. 读取RSS列表(双端可选) |
+------------+-------------+
|
v
+---------------------+
| 2. 并发抓取RSS,限流 |
| (max concurrency) |
+-------+-------------+
|
v
+------------------------------+
| 3. 指数退避算法 (重试解析失败) |
+------------------------------+
|
v
+-------------------+
| 4. 结果整合排序 |
+--------+----------+
|
v
+-------------------------+
| 5. 上传 RSS (双端可选) |
+-------------------------+
|
v
+--------------------+
| 6. 写日志到GitHub |
+--------------------+
-
并发抓取 + 限流
通过 Go 的 goroutine 并发抓取 RSS,同时用一个 channel 来限制最大并发数 -
指数退避重试
每个 RSS 如果第一次抓取失败,则会间隔几秒后再次重试,且间隔呈指数级递增(1s -> 2s -> 4s),最多重试三次,极大提高成功率 -
灵活存储
RSS_SOURCE: 可以决定从 COS 读取一个远程 txt 文件(里面存放 RSS 列表),或直接从 GitHub 的 data/rss.txt 读取
SAVE_TARGET: 可以把抓取结果上传到 GitHub,或者传到腾讯云 COS -
日志自动清理
每次成功写入日志后,会检查 logs/ 目录下的日志文件,若超过 7 天就自动删除,避免日志越积越多
2.2 指数退避
上一次写指数退避,还是在养老院写PHP的时候,时过境迁啊,这段算法我调试了很久,其实不难,也就是说失败一次,就等待更长的时间再重试,配置如下:
- 最大重试次数: 3
- 初始等待: 1秒
- 等待倍数: 2.0
也就是说失败一次就加倍等待,下次若依然失败就再加倍,如果三次都失败则放弃处理
// fetchAllFeeds 并发抓取所有RSS链接,返回抓取结果及统计信息
//
// Description:
//
// 该函数读取传入的所有RSS链接,使用10路并发进行抓取
// 在抓取过程中对解析失败、内容为空等情况进行统计
// 若抓取的RSS头像缺失或无法访问,将替换为默认头像
//
// Parameters:
// - ctx : 上下文,用于控制网络请求的取消或超时
// - rssLinks : RSS链接的字符串切片,每个链接代表一个RSS源
// - defaultAvatar : 备用头像地址,在抓取头像失败或不可用时使用
//
// Returns:
// - []feedResult : 每个RSS链接抓取的结果(包含成功的Feed及其文章或错误信息)
// - map[string][]string : 各种问题的统计记录(解析失败、内容为空、头像缺失、头像不可用)
func fetchAllFeeds(ctx context.Context, rssLinks []string, defaultAvatar string) ([]feedResult, map[string][]string) {
// 设置最大并发量,以信道(channel)信号量的方式控制
maxGoroutines := 10
sem := make(chan struct{}, maxGoroutines)
// 等待组,用来等待所有goroutine执行完毕
var wg sync.WaitGroup
resultChan := make(chan feedResult, len(rssLinks)) // 用于收集抓取结果的通道
fp := gofeed.NewParser() // RSS解析器实例
// 遍历所有RSS链接,为每个RSS链接开启一个goroutine进行抓取
for _, link := range rssLinks {
link = strings.TrimSpace(link)
if link == "" {
continue
}
wg.Add(1) // 每开启一个goroutine,对应Add(1)
sem <- struct{}{} // 向sem发送一个空结构体,表示占用了一个并发槽
// 开启协程
go func(rssLink string) {
defer wg.Done() // 协程结束时Done
defer func() { <-sem }() // 函数结束时释放一个并发槽
var fr feedResult
fr.FeedLink = rssLink
// 抓取RSS Feed, 无法解析时,使用指数退避算法进行重试, 有3次重试, 初始1s, 倍数2.0
feed, err := fetchFeedWithRetry(rssLink, fp, 3, 1*time.Second, 2.0)
if err != nil {
fr.Err = wrapErrorf(err, "解析RSS失败: %s", rssLink)
resultChan <- fr
return
}
if feed == nil || len(feed.Items) == 0 {
fr.Err = wrapErrorf(fmt.Errorf("该订阅没有内容"), "RSS为空: %s", rssLink)
resultChan <- fr
return
}
// 获取RSS的头像信息(若RSS自带头像则用RSS的,否则尝试从博客主页解析)
avatarURL := getFeedAvatarURL(feed)
fr.Article = &Article{
BlogName: feed.Title,
}
// 检查头像可用性
if avatarURL == "" {
// 若头像链接为空,则标记为空字符串
fr.Article.Avatar = ""
} else {
ok, _ := checkURLAvailable(avatarURL)
if !ok {
fr.Article.Avatar = "BROKEN" // 无法访问,暂记为BROKEN
} else {
fr.Article.Avatar = avatarURL // 正常可访问则记录真实URL
}
}
// 只取最新一篇文章作为结果
latest := feed.Items[0]
fr.Article.Title = latest.Title
fr.Article.Link = latest.Link
// 解析发布时间,如果 RSS 解析器本身给出了 PublishedParsed 直接用,否则尝试解析 Published 字符串
pubTime := time.Now()
if latest.PublishedParsed != nil {
pubTime = *latest.PublishedParsed
} else if latest.Published != "" {
if t, e := parseTime(latest.Published); e == nil {
pubTime = t
}
}
fr.ParsedTime = pubTime
fr.Article.Published = pubTime.Format("02 Jan 2006")
resultChan <- fr
}(link)
}
// 开启一个goroutine等待所有抓取任务结束后,关闭resultChan
go func() {
wg.Wait()
close(resultChan)
}()
// 用于统计各种问题
problems := map[string][]string{
"parseFails": {}, // 解析 RSS 失败
"feedEmpties": {}, // 内容 RSS 为空
"noAvatar": {}, // 头像地址为空
"brokenAvatar": {}, // 头像无法访问
}
// 收集抓取结果
var results []feedResult
for r := range resultChan {
if r.Err != nil {
errStr := r.Err.Error()
switch {
case strings.Contains(errStr, "解析RSS失败"):
problems["parseFails"] = append(problems["parseFails"], r.FeedLink)
case strings.Contains(errStr, "RSS为空"):
problems["feedEmpties"] = append(problems["feedEmpties"], r.FeedLink)
}
results = append(results, r)
continue
}
// 对于成功抓取的Feed,如果头像为空或不可用则使用默认头像
if r.Article.Avatar == "" {
problems["noAvatar"] = append(problems["noAvatar"], r.FeedLink)
r.Article.Avatar = defaultAvatar
} else if r.Article.Avatar == "BROKEN" {
problems["brokenAvatar"] = append(problems["brokenAvatar"], r.FeedLink)
r.Article.Avatar = defaultAvatar
}
results = append(results, r)
}
return results, problems
}
2.3 并发抓取 + 限流
为避免一下子开几十上百个协程导致阻塞,可以配合一个带缓存大小的 channel
maxGoroutines := 10
sem := make(chan struct{}, maxGoroutines)
for _, rssLink := range rssLinks {
// 启动 goroutine 前先写入一个空 struct
sem <- struct{}{}
go func(link string) {
// goroutine 执行结束后释放 <-sem
defer func() { <-sem }()
fetchFeedWithRetry(link, parser, 3, 1*time.Second, 2.0)
// ...
}(rssLink)
}
3. 对比旧版本的改进
-
容错率显著提升
遇到网络抖动、超时等问题,能以10路并发限制式自动重试,很少出现直接拿不到数据
-
抓取速度更快
以 10 路并发为例,对于数量多的 RSS,速度提升明显
-
日志分类更细
分清哪条 RSS 是解析失败,哪条头像挂了,哪条本身有问题,后续维护比只给个403 Forbidden方便太多
-
支持 COS
可将最终 data.json 放在 COS 上进行 CDN 加速;也能继续放在 GitHub,视自己需求而定
-
自动清理过期日志
每次抓取后检查 logs/ 目录下 7 天之前的日志并删除,不用手工清理了
4. Go 生成的 JSON 和日志长啥样
4.1 RSS
抓取到的文章信息会按时间降序排列,示例:
{
"items": [
{
"blog_name": "obaby@mars",
"title": "品味江南(三)–虎丘塔 东方明珠",
"published": "10 Mar 2025",
"link": "https://oba.by/2025/03/19714",
"avatar": "https://oba.by/wp-content/uploads/2020/09/icon-500-100x100.png"
},
{
"blog_name": "风雪之隅",
"title": "PHP8.0的Named Parameter",
"published": "10 May 2022",
"link": "https://www.laruence.com/2022/05/10/6192.html",
"avatar": "https://www.laruence.com/logo.jpg"
}
],
"updated": "2025年03月11日 07:15:57"
}
4.2 日志
程序每次运行完毕后,把抓取统计和问题列表写到 GitHub 仓库 logs/YYYY-MM-DD.log:
[2025-03-11 07:15:57] 本次订阅抓取结果统计:
[2025-03-11 07:15:57] 共 25 条RSS, 成功抓取 24 条.
[2025-03-11 07:15:57] ✘ 有 1 条订阅解析失败:
[2025-03-11 07:15:57] - https://tcxx.info/feed
[2025-03-11 07:15:57] ✘ 有 1 条订阅头像无法访问, 已使用默认头像:
[2025-03-11 07:15:57] - https://www.loyhome.com/feed
5. 照葫芦画瓢
如果你也想玩玩 LhasaRSS
-
准备一份 RSS 列表(TXT):
格式:每行一个 URL
如果 RSS_SOURCE = GITHUB,则可以放在项目中的 data/rss.txt
如果 RSS_SOURCE = COS,就把它上传到某个 https://xxx.cos.ap-xxx.myqcloud.com/rss.txt -
配置好环境变量:
默认所有数据保存到 Github,所以 COS API 环境变量不是必要的
env: TOKEN: ${{ secrets.TOKEN }} # GitHub Token NAME: ${{ secrets.NAME }} # GitHub 用户名 REPOSITORY: ${{ secrets.REPOSITORY }} # GitHub 仓库名 TENCENT_CLOUD_SECRET_ID: ${{ secrets.TENCENT_CLOUD_SECRET_ID }} # 腾讯云 COS SecretID TENCENT_CLOUD_SECRET_KEY: ${{ secrets.TENCENT_CLOUD_SECRET_KEY }} # 腾讯云 COS SecretKey RSS: ${{ secrets.RSS }} # RSS 列表文件 DATA: ${{ secrets.DATA }} # 抓取后的数据文件 DEFAULT_AVATAR: ${{ secrets.DEFAULT_AVATAR }} # 默认头像 URL RSS_SOURCE ${{ secrets.RSS_SOURCE }} # 可选参数 GITHUB or COS SAVE_TARGET ${{ secrets.SAVE_TARGET }} # 可选参数 GITHUB or COS
-
部署并运行
只需 go run . 或在 GitHub Actions workflow_dispatch 触发 运行结束后,就会在 data 文件夹更新 data.json,日志则写进 GitHub logs/ 目录,并且自动清理旧日志
注:如果你依旧想完全托管在 COS 上,需要把 RSS_SOURCE 和 SAVE_TARGET 都写为 COS,然后使用 GitHub Actions 去调度
相关文档
- lhasaRSS:https://github.com/achuanya/lhasaRSS
- 腾讯 Go SDK 快速入门: https://cloud.tencent.com/document/product/436/31215
- XML Go SDK 源码: https://github.com/tencentyun/cos-go-sdk-v5
- GitHub REST API: https://docs.github.com/zh/rest
- 轻量级 RSS/Atom 解析库: https://github.com/mmcdole/gofeed
骑行开封
我对于开封的印象,还停留在开封府尹·包拯。处于好奇和无处可去的想法,周六早上吃完饭,说走就走了
这里就到达开封了开封·鼓楼,郑开大道的路上很轻松,室外温度17°+,小风微微的吹着,不冷不热好不痛快
在郑开大道单飞的过程中偶遇骑友,王哥是开封本地的,骑行的路上跟我聊开封哪里好玩,哪里最具性价比,把我领进开封鼓楼后,又带我在景区逛了一圈带我认路,在此感谢大哥
早饭吃的比较仓促,真的很饿,在书店街附近买了些吃的
本来是想在开封呆一天,晚上去清明上河园玩,想到公司有事就提前回去了,怕耽误明天的行程
这次跨市骑行急了一些,时间太紧张了!再过几天休息,我想回一次家,骑行约200KM