普通视图

发现新文章,点击刷新页面。
昨天以前游钓四方的博客
  • ✇游钓四方的博客
  • 十月份看完的第一本书游钓四方的博客
    为这本书,我近两天几乎废寝忘食,心中感慨颇多,最深刻的收获便是对人性与形式的深刻理解。许多事情在当时看来似乎是理所当然,但如今回首,才发现那时的自己是多么稚嫩,甚至有些可笑 这是我第一次尝试电子书,有声加文字的双重体验感觉不错,美中不足的是,机械式的发音让情感显得苍白,在这个时代,做个拟人化的语音合成也不难啊,腾讯读书这块业务还是小众,资源太少了,很多我想看的书都找不到,涉及敏感话题的搜都搜不到,哎
     

十月份看完的第一本书

2024年10月3日 20:31

为这本书,我近两天几乎废寝忘食,心中感慨颇多,最深刻的收获便是对人性与形式的深刻理解。许多事情在当时看来似乎是理所当然,但如今回首,才发现那时的自己是多么稚嫩,甚至有些可笑

微信读书 · 沧浪之水

这是我第一次尝试电子书,有声加文字的双重体验感觉不错,美中不足的是,机械式的发音让情感显得苍白,在这个时代,做个拟人化的语音合成也不难啊,腾讯读书这块业务还是小众,资源太少了,很多我想看的书都找不到,涉及敏感话题的搜都搜不到,哎

  • ✇游钓四方的博客
  • 一场说走就走的骑行游钓四方的博客
    凌晨四点半,忽然醒了,辗转反侧,睡意全无,脑海中第一个蹦出的念头竟然是骑车 连衣服都没顾得上穿,立刻下床检查车子胎压,拿好盐丸和刚冲泡的蛋白粉,穿上骑行服,扛着车冲出家门,直奔早餐店 到早餐店的时间是4.50,来的太早了,选择不多,只有小米粥和茶叶蛋,包子还要等五分钟才熟,要了一碗小米粥、两个茶叶蛋、三个牛肉包。吃得有些急促,可以用赵本山的急头白脸吃一顿来表示 坐在店里,我心中还没想好骑去哪儿。随手打开地图,首页推荐了鹿邑的太清宫。还记得小时候常听到同学开玩笑说:老子是鹿邑的! 看了一下路线,沿着322省道,往返148km,这个省道我走过,去年去骑行去上海就是这个,路况烂的没法说,这次过去更是炸裂 路况越来越差了,柏油路被超载车辆压的细碎,我这25c管胎感觉随时都要爆,不禁怀念瓜车山地带来的安全感 8.20到达太清宫,但是车子的存放问题要解决一下,正好门口有卖香火的大妈,我付了她十块钱,让她帮我看着车子 太清宫的门票要60,有些贵了,相比之下,淮阳的太昊陵只要40,且规模和和设计都远胜太清宫 从三清大殿出来后看到这,我瞬间挂上痛苦面具,因为我今天
     

一场说走就走的骑行

2024年9月26日 20:02

凌晨四点半,忽然醒了,辗转反侧,睡意全无,脑海中第一个蹦出的念头竟然是骑车

连衣服都没顾得上穿,立刻下床检查车子胎压,拿好盐丸和刚冲泡的蛋白粉,穿上骑行服,扛着车冲出家门,直奔早餐店

家门口的早餐店

到早餐店的时间是4.50,来的太早了,选择不多,只有小米粥和茶叶蛋,包子还要等五分钟才熟,要了一碗小米粥、两个茶叶蛋、三个牛肉包。吃得有些急促,可以用赵本山的急头白脸吃一顿来表示

坐在店里,我心中还没想好骑去哪儿。随手打开地图,首页推荐了鹿邑的太清宫。还记得小时候常听到同学开玩笑说:老子是鹿邑的!

看了一下路线,沿着322省道,往返148km,这个省道我走过,去年去骑行去上海就是这个,路况烂的没法说,这次过去更是炸裂

椿树王庄的日出

路况越来越差了,柏油路被超载车辆压的细碎,我这25c管胎感觉随时都要爆,不禁怀念瓜车山地带来的安全感

太清宫外的香火小贩

8.20到达太清宫,但是车子的存放问题要解决一下,正好门口有卖香火的大妈,我付了她十块钱,让她帮我看着车子

到达 鹿邑·太清宫

太清宫的门票要60,有些贵了,相比之下,淮阳的太昊陵只要40,且规模和和设计都远胜太清宫

太清宫·三清大殿

三清大殿后的广场

从三清大殿出来后看到这,我瞬间挂上痛苦面具,因为我今天出来穿的是骑行锁鞋,前脚跟高,后脚跟低,走路时一步两响,比高跟鞋都变态

出来后多少有些失望,毕竟这是老子的诞生地,但道教的气息却淡得几乎感觉不到,园林设计更是稀碎。相反,倒是社会主义的标语随处可见。我是感觉现在的寺庙和道观都没有其文化特色,如同各地方美食城的铁板鱿鱼

想必这和上层意识脱不开干系,组织对于宗教信仰很敏感

来奶茶店补个电

一路上导航和听歌消耗了不少电,在奶茶店喝了两杯,顺便给手机充了个电,不过鹿邑中午也太热了,我出发时还穿了棉背心

STRAVA记录

返程78km,全程逆风,出发前还吃多了,当我坐在车上那一刻,再次带上痛苦面具…

下午五点半点到家,回来得有些匆忙,因为这个省道很窄,晚上全是半挂,我不敢贪玩冒这个风险,图片还是有点少了,骑行时不可自拔,除了点烟会顺手会拿起手机拍照,其他时候不想断了骑行的节奏

  • ✇游钓四方的博客
  • 聊聊自行车游钓四方的博客
    洋车子的起源 在河南老家,人们通常称自行车为“洋车子”。确实,是洋鬼子的杰作,早在1791年,法国佬西夫拉克设计出了世界上第一辆自行车,据说他是受溜冰鞋的启发,整车为木制,上管由一根大横梁作为主体车架,下方装两个木制车轮,没有转向结构,拐弯全靠搬。当然,它也没有传动系统,骑多快这就真看腿了 0-100的开始 追溯历史,19世纪六七十年代,自行车由洋鬼子、华人带入中国,数量寥寥 随着时间的推移,20世纪二十年代,上海做自行车销售的同昌车行开始仿制生自行车零部件,再搭配进口件的基础上进行组装贴牌。从此内地第一家仿制、组装整车的自行车企业诞生,据注册登记记载有:飞马、飞鹰、飞人、飞轮等型号。直到新中国1956年国家队将其收编、创新再贴牌。由此诞生:上海凤凰 新中国1955-1957年期间,是内地自行车产业发展的关键时期,由主管自行车工业的工信部,组织上海、天津、沈阳的自行车厂进行标准化设计,确定了28寸自行车的规格和零配件的质量标准,为内地自行车行业制定了第一部行业标准,为后来几十年的自行车普及奠定了基础 到了新中国六七十年代,自行车在全国范围内迅速普及,尽管当时的产业链尚不
     

聊聊自行车

2024年8月16日 03:02

洋车子的起源

在河南老家,人们通常称自行车为“洋车子”。确实,是洋鬼子的杰作,早在1791年,法国佬西夫拉克设计出了世界上第一辆自行车,据说他是受溜冰鞋的启发,整车为木制,上管由一根大横梁作为主体车架,下方装两个木制车轮,没有转向结构,拐弯全靠搬。当然,它也没有传动系统,骑多快这就真看腿了

河北 中国自行车博物馆 仿制藏品

0-100的开始

追溯历史,19世纪六七十年代,自行车由洋鬼子、华人带入中国,数量寥寥

随着时间的推移,20世纪二十年代,上海做自行车销售的同昌车行开始仿制生自行车零部件,再搭配进口件的基础上进行组装贴牌。从此内地第一家仿制、组装整车的自行车企业诞生,据注册登记记载有:飞马、飞鹰、飞人、飞轮等型号。直到新中国1956年国家队将其收编、创新再贴牌。由此诞生:上海凤凰

新中国1955-1957年期间,是内地自行车产业发展的关键时期,由主管自行车工业的工信部,组织上海、天津、沈阳的自行车厂进行标准化设计,确定了28寸自行车的规格和零配件的质量标准,为内地自行车行业制定了第一部行业标准,为后来几十年的自行车普及奠定了基础

到了新中国六七十年代,自行车在全国范围内迅速普及,尽管当时的产业链尚不完善,但许多品牌已逐渐崭露头角,下面有请八十年代国产五虎上将:上海永久、凤凰、天津飞鸽、红旗、江苏金狮

上海永久ZA51-9

富裕的象征

新中国1981年9月,三流日报曾转载了一篇文章:湖北农民杨小运在超额完成国家交售指标后,县里问他想要什么奖励,他说想要一辆永久牌自行车。很多年后,杨小运回忆:“我是壮起胆子提出了自己的想法。须知道那时候能够骑上一辆永久牌自行车是多么得意的事情,因为据说只有凭什么“工业券”才能买到这种稀罕物,比现在坐小轿车还要显摆,简直就是一种身份的象征。在我的印象中,好像只有公社党委书记这样的人物才配有这么一辆自行车

由此可见,自行车不仅是代步工具,更是那个年代富裕与社会地位的象征,俗称小资三件套:缝纫机、手表,自行车

60年代初-80年代末发行的自行车券

短暂的辉煌

然而,好景不长。随着改革开放的推进,外资品牌涌入中国市场,这些闭门仿车国产老品牌被外资的新工业设计所冲击,如:GIANT、Merida、Trek等等

此时的自行车带来最直接的就是视觉感官,功能多样的车架和丰富多彩的颜色,彻底颠覆了内地自行车的固有形象。相比之下,国产品牌的市场份额断崖式下跌,怕不是编制兜底,如今已全部倒闭,至此,沪上小资品牌殊荣不再

在新工业品牌中,最具代表性的是GIANT,尽管价格昂贵,但其时尚的设计和优越的性能激发了人民的购买欲望,迅速取代国产五虎上将,成为新一代的“高级自行车”,GIANT驻扎内地后年年蝉联市场第一,这一变革标志着中国内地自行车市场的一个重要转折点

较受欢迎的 GIANT ATX 760 图为90款

从改革开放到21世纪初的几十年间,中国自行车运动突然就死在了襁褓里,由于汽车的普及和部分国人的观念癌症,自行车逐渐边缘化,成为人们眼中过时的交通工具。就此,内地再也没有形成新的自行车运动

而在此期间,外面的世界发生了天翻地覆的变化。UCI在瑞士洛桑成立了世界自行车中心和场地车训练场,致力于培训精英选手并为赛事提供支持。高卢鸡的环法赛愈演愈烈,成为全球最受瞩目的公路自行车赛事之一。与此同时,小日子的Shimano凭借革命性的STI技术,将刹车和变速融合,逐步通过专利,垄断全球自行车零部件市场

1998年环法路过一片向日葵地

反观中国内地。自行车产业在这一波全球变革中停滞不前,国产品牌没有主动认清提升技术和产品价值的重要性,依然闭门造车,专注于下沉市场。没有技术,没有创新,短期图利坑蒙拐骗,论长期发展无异于自我毁灭

直到今天,大部分内地品牌在观念和行动上依然停留在过去的成功经验 ,靠坑蒙拐骗人民继续吃老本,最近又冒出一个内地品牌——Maserati玛莎拉蒂自行车,一个车架都要使用公模,搭配些工业垃圾套件组装。技术水平确实了不起,至少标贴得还算正。我都感到丢人,是不是人民好糊弄?都当成傻子?《这些,它们够用了》

昙花一现

进入21世纪后,随着资本经济的涌入,共享单车兴起,给中国的自行车运动带来了新的生机,环保意识的提升和健康生活方式的推广,使得越来越多的人将骑行作为健身方式和生活态度,骑行团体的壮大和各种赛事也愈发频繁

疫情期间,由于长时间的封闭管控,许多人渴望户外活动的机会,当管控解除后,户外运动迅速升温,体育及户外运动板块纷纷涨停板,骑行也因此成为了炙手可热的选择之一

然而,在各方面利好的同时,骑行运动也面临诸多挑战,在鄙人看来,部分国人对于新鲜事物的包容度较低,这与千百年来的文化属性密不可分

在这个二十一世纪的现代社会,有些人甚至认为骑行是“文化入侵”,这一观点显然过于狭隘,抱着自由言论的心态,鄙人不敢苟同

中国作为一个发展中国家,许多城市的道路基础设施尚未完善,尤其是在道路规划方面,机动车道与非机动车道的设计往往缺乏科学性和合理性,例如,原本就狭窄的非机动车道常常被汽车占用,加上逆行和鬼探头等行为,骑行安全难以保障,导致了骑行风险的增加。在这种情况下,所谓的“暴骑团”被迫驶入机动车道,进一步加剧了骑行者与机动车之间的矛盾,交通事故频发

此外,骑行者的素质问题也是不可忽视的因素。部分骑行者缺乏基本的交规意识、逆行、闯红灯等行为屡见不鲜。面对这种情况,我只能说,管好自己

非机动车道被抓

关于最近发生的河北亲子骑行事件,引发了社会的广泛关注和讨论,不少网友竟表示同情,在视频中司机被殴打被迫下跪,什么死者为大?呸恶心。鄙人认为这是家长全责!缺乏对骑行的敬畏之心,忽视了安全因素。对于部分圣母指责辱骂司机我只想说

在这个环境内,你可以漠视周围发生的一切不公平,直到这种不公平在某一天以一种你无法预期的方式降临到你身上

  • ✇游钓四方的博客
  • 写一个骑行页面(二)游钓四方的博客
    在前几天写的数据展示页面中,日历与JSON数据的时间处理依赖于本地时区的getDay()和setDate()方法。然而,博客部署在GitHub Pages,时区的不同导致日历出现了显示偏差 本地时间异常 涉及函数:getStartDate 原代码: 这里的getDay()和setDate()方法是基于Github本地时区,不细心 function getStartDate(today, daysOffset) { const currentDayOfWeek = today.getDay(); const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1); const startDate = new Date(today); startDate.setDate(today.getDate() - daysToMonday - daysOffset); startDate.setDate(startDate.getDate() - (startDate
     

写一个骑行页面(二)

2024年8月14日 20:40

在前几天写的数据展示页面中,日历与JSON数据的时间处理依赖于本地时区的getDay()和setDate()方法。然而,博客部署在GitHub Pages,时区的不同导致日历出现了显示偏差

本地时间异常

涉及函数:getStartDate

原代码:

这里的getDay()和setDate()方法是基于Github本地时区,不细心

function getStartDate(today, daysOffset) {
    const currentDayOfWeek = today.getDay();
    const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1); 
    const startDate = new Date(today);
    startDate.setDate(today.getDate() - daysToMonday - daysOffset); 
    startDate.setDate(startDate.getDate() - (startDate.getDay() === 0 ? 6 : startDate.getDay() - 1)); 
    return startDate;
}

改进后:

修改后:采用getUTCDay()和setUTCDate()方法,使用UTC时间来保证时间处理的一致性

function getStartDate(today, daysOffset) {
    const currentDayOfWeek = today.getUTC11Day();
    const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1);
    const startDate = new Date(today);
    startDate.setUTCDate(today.getUTCDate() - daysToMonday - daysOffset);
    return startDate;
}

JSON数据与日历数据两者时区不一致


// 涉及函数:generateCalendar

// 原代码:

// 在与JSON日期数据进行比较时,由于时区问题,日历的显示存在错位

const todayStr = getChinaTime().toISOString().split('T')[0];
let currentDate = new Date(startDate);

// 改进后:

// 将currentDate时间归零,避免由于时区差异导致的日期比较错误

const todayStr = getChinaTime().toISOString().split('T')[0];
let currentDate = new Date(startDate);
currentDate.setUTCHours(0, 0, 0, 0);

日期显示与更新逻辑异常

涉及函数:createDayContainer

同上,本地时间异常

// 原代码:
dateNumber.innerText = date.getDate();

// 改进后
dateNumber.innerText = date.getUTCDate();

异步打字机时区异常

涉及函数:displayCalendar

同上,本地时间异常

// 原代码:
currentDate.setDate(currentDate.getDate() + 1);

// 改进后
currentDate.setUTCDate(currentDate.getUTCDate() + 1);

UPDATE 日历交互动画

1.默认显示当天日期,不显示球体

2.光标悬浮其他日历时,隐藏当天日期,显示球体,反之亦然

3.整体主题色为:rgb(36, 36, 40)

4.日历的所有日期下添加2px厚下划线

function createDayContainer(date, activities) {
    const dayContainer = document.createElement('div');
    dayContainer.className = 'day-container';

    const dateNumber = document.createElement('span');
    dateNumber.className = 'date-number';
    dateNumber.innerText = date.getUTCDate();
    dayContainer.appendChild(dateNumber);

    const activity = activities.find(activity => activity.activity_time === date.toISOString().split('T')[0]);
    // console.log(processedActivities);
    if (activity) processedActivities.push(activity);

    // 根据骑行距离设置球的大小
    const ballSize = activity ? Math.min(parseFloat(activity.riding_distance) / 4, 24) : 2;

    const ball = document.createElement('div');
    ball.className = 'activity-indicator';
    ball.style.width = `${ballSize}px`;
    ball.style.height = `${ballSize}px`;
    if (!activity) ball.classList.add('no-activity');
    ball.style.left = '50%';
    ball.style.top = '50%';
    dayContainer.appendChild(ball);

    dayContainer.addEventListener('mouseenter', () => {
        if (date.toDateString() === new Date().toDateString()) {
            dateNumber.style.opacity = '0';
            ball.style.opacity = '1';
        } else {
            if (todayContainer) {
                todayContainer.querySelector('.date-number').style.opacity = '0';
                todayContainer.querySelector('.activity-indicator').style.opacity = '1';
            }
        }
    });
    dayContainer.addEventListener('mouseleave', () => {
        if (date.toDateString() === new Date().toDateString()) {
            dateNumber.style.opacity = '1';
            ball.style.opacity = '0';
        } else {
            if (todayContainer) {
                todayContainer.querySelector('.date-number').style.opacity = '1';
                todayContainer.querySelector('.activity-indicator').style.opacity = '0';
            }
        }
    });

    if (date.toDateString() === new Date().toDateString()) {
        todayContainer = dayContainer;
        dayContainer.classList.add('today');
        ball.style.backgroundColor = '#242428';
        dateNumber.style.color = '#242428';
        dateNumber.style.opacity = '1';
        ball.style.opacity = '0';
    }
    return dayContainer;
}

  • ✇游钓四方的博客
  • 与时间抗衡:笔记本清灰换硅脂记游钓四方的博客
    下午,笔记本忽然发出了咔哧咔哧的响声,仿佛是临死前的挣扎。我知道,又是哪零件坏了。大半年没清灰,这机器,似乎比人还脆弱 压CPU的螺丝多半滑丝,硅脂也是,抹得太多,溢得像过剩的腐败 风扇拆下来时洒下许多灰尘,这风扇犹如一台无力的革命机器,快要垮掉。 一抹灰尘,手上黑漆漆的 折腾了半个小时,总算洗干净了,接下来是抹硅脂 这硅脂还是一九年买的,小日子的信越X-23-7868-2D,导热效果还不错,不过这种产品很虚,需要时间来验证 涂了不少,想压住它的病症,但心里知道,这些努力也只是苟延残喘。机器,终究是要坏的 螺丝虽然滑丝了,但还勉强拧得上。我想,我该买些新螺丝,以免将来它彻底罢工 一次点亮。这台笔记本跟了我好多年,从上学时就陪着我,现在还能跑动。除了渲染不给力,写代码倒是没问题。 我觉得,电脑这东西,特别是Windows系统,得学会一些优化技巧。不然,即使配置再高,也会卡顿,这和底层设计脱不开关系,和Android有异曲同工之处。 相比之下,我更喜欢Linux。之前用Manjaro Linux做主力机有四年时间。不过,Linux的图
     

与时间抗衡:笔记本清灰换硅脂记

2024年8月11日 20:19

下午,笔记本忽然发出了咔哧咔哧的响声,仿佛是临死前的挣扎。我知道,又是哪零件坏了。大半年没清灰,这机器,似乎比人还脆弱

笔记本内部堆满灰尘

压CPU的螺丝多半滑丝,硅脂也是,抹得太多,溢得像过剩的腐败

过多的硅脂溢出

满是灰尘的风扇

风扇拆下来时洒下许多灰尘,这风扇犹如一台无力的革命机器,快要垮掉。

风满是灰尘的风扇

一抹灰尘,手上黑漆漆的

满是污垢的盖板

折腾了半个小时,总算洗干净了,接下来是抹硅脂

清理干净,准备组装

这硅脂还是一九年买的,小日子的信越X-23-7868-2D,导热效果还不错,不过这种产品很虚,需要时间来验证

假一赔命

涂了不少,想压住它的病症,但心里知道,这些努力也只是苟延残喘。机器,终究是要坏的

硅脂涂抹过多

螺丝虽然滑丝了,但还勉强拧得上。我想,我该买些新螺丝,以免将来它彻底罢工

安装完毕

一次点亮。这台笔记本跟了我好多年,从上学时就陪着我,现在还能跑动。除了渲染不给力,写代码倒是没问题。

我觉得,电脑这东西,特别是Windows系统,得学会一些优化技巧。不然,即使配置再高,也会卡顿,这和底层设计脱不开关系,和Android有异曲同工之处。

相比之下,我更喜欢Linux。之前用Manjaro Linux做主力机有四年时间。不过,Linux的图形界面BUG让我很头疼,几乎每个版本都有各种关于GUI的BUG。刚接触Linux时,最让我害怕的就是系统更新。说到这里,不得不提一下王垠

谈 Linux,Windows 和 Mac

点亮成功

  • ✇游钓四方的博客
  • 写一个骑行页面游钓四方的博客
    作为一个爱好骑行的博主,总觉得博客里少了点什么,骑行骑行的,怎么能没有一个专门的骑行数据展示页呢 在设计这个页面的时候,参考了许多骑行APP,然而,国内的骑行数据页面设计真的是一言难尽… 我骑行看数据用Strava多一些,但是它的PC端交互体验,实在不敢苟同。除了用APP版本,我几乎不会去它的网页。不得不说,国外这些骑行数据端做的确实很到位,我个人觉得数据分析方面Strava比Garmin要好! 项目结构 老规矩,先放目录结构。由于网站的主样式文件main压缩后都超160kb了,为了避免堵塞加载,新开辟一条生产线 说起SCSS,还是受Fooleap的启发才接触到的,我非常喜欢这种方式,它允许嵌套CSS,让代码更加模块化、结构化,还支持变量、继承。比起传统CSS那真是有过之而无不及啊! Blog ├─assets │ cycling.min.css │ cycling.min.js │ ├─pages │ cycling.html │ └─src │ cycling.js │ main.js │ ├─cycli
     

写一个骑行页面

2024年8月11日 16:02

作为一个爱好骑行的博主,总觉得博客里少了点什么,骑行骑行的,怎么能没有一个专门的骑行数据展示页呢

在设计这个页面的时候,参考了许多骑行APP,然而,国内的骑行数据页面设计真的是一言难尽…

我骑行看数据用Strava多一些,但是它的PC端交互体验,实在不敢苟同。除了用APP版本,我几乎不会去它的网页。不得不说,国外这些骑行数据端做的确实很到位,我个人觉得数据分析方面Strava比Garmin要好!

项目结构

老规矩,先放目录结构。由于网站的主样式文件main压缩后都超160kb了,为了避免堵塞加载,新开辟一条生产线

说起SCSS,还是受Fooleap的启发才接触到的,我非常喜欢这种方式,它允许嵌套CSS,让代码更加模块化、结构化,还支持变量、继承。比起传统CSS那真是有过之而无不及啊!

Blog
├─assets
│      cycling.min.css
│      cycling.min.js
│
├─pages
│      cycling.html
│
└─src
    │  cycling.js
    │  main.js
    │
    ├─cycling
    │      cycling.scss
    │      _bar-chart.scss
    │      _base.scss
    │      _calendar.scss
    │      _message-box.scss
    │      _sports.scss
    │
    └─sass

cycling.js

目前所有的逻辑都在这一个文件里完成,现在的功能还是个雏,因为没有打通Strava api,JSON数据是我手搓的..最近一直在搞Strava api,有好大哥懂吗?它们现在限制了每小时的请求次数,我本来就是半吊子水平,现在是雪上加霜

import './cycling/cycling.scss';

// 为了数据的统一性,generateCalendar处理后赋值供全局使用
let processedActivities = [];

// 日历
function generateCalendar(activities, startDate, numWeeks) {
    const calendarElement = document.getElementById('calendar');
    calendarElement.innerHTML = ''; 
    
    const daysOfWeek = ['', '', '', '', '', '', '']; 
    daysOfWeek.forEach(day => {
        const dayElement = document.createElement('div');
        dayElement.className = 'calendar-week-header';
        dayElement.innerText = day;
        calendarElement.appendChild(dayElement);
    });

    const todayStr = getChinaTime().toISOString().split('T')[0];
    // 起始日期
    let currentDate = new Date(startDate);

    processedActivities = [];

    // 创建日历
    function createDayContainer(date, activities) {
        const dayContainer = document.createElement('div');
        dayContainer.className = 'day-container';

        const dateNumber = document.createElement('span');
        dateNumber.className = 'date-number';
        dateNumber.innerText = date.getDate();
        dayContainer.appendChild(dateNumber);

        const activity = activities.find(activity => activity.activity_time === date.toISOString().split('T')[0]);
        if (activity) processedActivities.push(activity);

        // 根据骑行距离设置球的大小
        const ballSize = activity ? Math.min(parseFloat(activity.riding_distance) / 4, 24) : 2;

        const ball = document.createElement('div');
        ball.className = 'activity-indicator';
        ball.style.width = `${ballSize}px`;
        ball.style.height = `${ballSize}px`;
        if (!activity) ball.classList.add('no-activity');
        ball.style.left = '50%';
        ball.style.top = '50%';
        dayContainer.appendChild(ball);

        dayContainer.addEventListener('mouseenter', () => {
            dateNumber.style.opacity = '1';
            ball.style.opacity = '0';
        });
        dayContainer.addEventListener('mouseleave', () => {
            dateNumber.style.opacity = '0';
            ball.style.opacity = '1';
        });

        // 今天日期和球的颜色
        if (date.toDateString() === new Date().toDateString()) {
            dayContainer.classList.add('today');
            ball.style.backgroundColor = '#2ea9df';
            dateNumber.style.color = '#2ea9df';
        }

        return dayContainer;
    }

    // 异步显示,模仿打字机效果
    async function displayCalendar() {
        for (let week = 0; week < numWeeks; week++) {
            for (let day = 0; day < 7; day++) {
                const currentDateStr = currentDate.toISOString().split('T')[0];
                // 不再计算超过今天的日期
                if (currentDateStr > todayStr) return;

                const dayContainer = createDayContainer(currentDate, activities);
                calendarElement.appendChild(dayContainer);

                // 速度
                await new Promise(resolve => setTimeout(resolve, 30));
                currentDate.setDate(currentDate.getDate() + 1);
            }
        }
    }
    displayCalendar().then(() => {
        generateBarChart();
        displayTotalActivities();
    });
}

// 柱形图
function generateBarChart() {
    const barChartElement = document.getElementById('barChart');
    barChartElement.innerHTML = '';

    const today = getChinaTime();
    const startDate = getStartDate(today, 21);

    // 每周数据
    const weeklyData = {};

    // 每周总活动时间
    processedActivities.forEach(activity => {
        const activityDate = new Date(activity.activity_time);
        const weekStart = getWeekStartDate(activityDate);
        const weekEnd = new Date(weekStart);
        weekEnd.setDate(weekStart.getDate() + 6);

        const weekKey = `${weekStart.toISOString().split('T')[0]} - ${weekEnd.toISOString().split('T')[0]}`;
        weeklyData[weekKey] = (weeklyData[weekKey] || 0) + convertToHours(activity.moving_time);
    });

    // 最大时间
    const maxTime = Math.max(...Object.values(weeklyData), 0);

    // 创建柱形图
    Object.keys(weeklyData).forEach(week => {
        const barContainer = document.createElement('div');
        barContainer.className = 'bar-container';

        const bar = document.createElement('div');
        bar.className = 'bar';
        const width = (weeklyData[week] / maxTime) * 190;
        bar.style.setProperty('--bar-width', `${width}px`);

        const durationText = document.createElement('div');
        durationText.className = 'bar-duration';
        durationText.innerText = '0h';

        const messageBox = createMessageBox();
        const clickMessageBox = createMessageBox();

        barContainer.style.position = 'relative'; 
        bar.appendChild(durationText);
        barContainer.appendChild(bar);
        barContainer.appendChild(messageBox);
        barContainer.appendChild(clickMessageBox);
        barChartElement.appendChild(barContainer);

        bar.style.width = '0';
        bar.offsetHeight;
        // 动画效果
        bar.style.transition = 'width 1s ease-out';
        bar.style.width = `${width}px`;

        durationText.style.opacity = '1';
        // 动态文本
        animateText(durationText, 0, weeklyData[week], 1000);
        setupBarInteractions(bar, messageBox, clickMessageBox, weeklyData[week]);
    });
}

// 动态文本显示
function animateText(element, startValue, endValue, duration) {
    const startTime = performance.now();
    function update() {
        const elapsed = performance.now() - startTime;
        const progress = Math.min(elapsed / duration, 1);
        const currentValue = Math.floor(progress * endValue);
        element.innerText = `${currentValue}h`;
        if (progress < 1) {
            requestAnimationFrame(update);
        } else {
            element.innerText = `${endValue.toFixed(1)}h`;
        }
    }
    update();
}

// 计算总公里数
function calculateTotalKilometers(activities) {
    return activities.reduce((total, activity) => total + parseFloat(activity.riding_distance) || 0, 0);
}

// 显示总活动数和总公里数
function displayTotalActivities() {
    const totalCountElement = document.getElementById('totalCount');
    const totalDistanceElement = document.getElementById('totalDistance');

    if (!totalCountElement || !totalDistanceElement) return;

    const totalCountValue = totalCountElement.querySelector('#totalCountValue');
    const totalDistanceValue = totalDistanceElement.querySelector('#totalDistanceValue');

    const totalCountSpinner = totalCountElement.querySelector('.loading-spinner');
    const totalDistanceSpinner = totalDistanceElement.querySelector('.loading-spinner');

    totalCountSpinner.classList.add('active');
    totalDistanceSpinner.classList.add('active');

    const uniqueDays = new Set(processedActivities.map(activity => activity.activity_time));
    const totalCount = uniqueDays.size;
    const totalKilometers = calculateTotalKilometers(processedActivities);

    animateCount(totalCountValue, totalCount, 1000, 50);
    animateCount(totalDistanceValue, totalKilometers, 1000, 50, true);

    setTimeout(() => {
        totalDistanceValue.textContent = `${totalKilometers.toFixed(2)} km`;
        totalCountSpinner.classList.remove('active');
        totalDistanceSpinner.classList.remove('active');
    }, 1000);
}

// 获取一周的开始日期
function getWeekStartDate(date) {
    const day = date.getDay();
    const diff = (day === 0 ? -6 : 1) - day;
    const weekStart = new Date(date);
    weekStart.setDate(weekStart.getDate() + diff);
    return weekStart;
}

// 将JSON的时间数据转换为小时
function convertToHours(moving_time) {
    const [hours, minutes] = moving_time.split(':').map(Number);
    return hours + (minutes / 60);
}

// 博客托管Github Pages需要中国时间
function getChinaTime() {
    const now = new Date();
    const offset = 8 * 60 * 60 * 1000;
    return new Date(now.getTime() + offset);
}

// 手搓JSON
async function loadActivityData() {
    const response = await fetch('XXXXXX');
    return response.json();
}

(async function() {
    const today = getChinaTime();
    const startDate = getStartDate(today, 21);

    const activities = await loadActivityData();
    generateCalendar(activities, startDate, 4);
})();

// 创建消息盒子
function createMessageBox() {
    const messageBox = document.createElement('div');
    messageBox.className = 'message-box';
    return messageBox;
}

// 获取起始时间
function getStartDate(today, daysOffset) {
    const currentDayOfWeek = today.getDay();
    const daysToMonday = (currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1); 
    const startDate = new Date(today);
    startDate.setDate(today.getDate() - daysToMonday - daysOffset); 
    startDate.setDate(startDate.getDate() - (startDate.getDay() === 0 ? 6 : startDate.getDay() - 1)); 
    return startDate;
}

// 动态更新计数器
function animateCount(element, totalValue, duration, intervalDuration, isDistance = false) {
    const step = totalValue / (duration / intervalDuration);
    let count = 0;
    const interval = setInterval(() => {
        count += step;
        if (count >= totalValue) {
            count = totalValue;
            clearInterval(interval);
        }
        element.textContent = isDistance ? count.toFixed(2) : Math.round(count);
    }, intervalDuration);
}

// 骚话集合
function setupBarInteractions(bar, messageBox, clickMessageBox, weeklyData) {
    let mouseLeaveTimeout;
    let autoHideTimeout;

    bar.addEventListener('mouseenter', () => {
        clearTimeout(mouseLeaveTimeout);
        clearTimeout(autoHideTimeout);

        const message = weeklyData > 14 ? '这周干的还不错' : '偷懒了啊';
        messageBox.innerText = message;
        messageBox.classList.add('show');

        autoHideTimeout = setTimeout(() => {
            messageBox.classList.remove('show');
        }, 700);
    });

    bar.addEventListener('mouseleave', () => {
        mouseLeaveTimeout = setTimeout(() => {
            messageBox.classList.remove('show');
        }, 700);
    });

    bar.addEventListener('click', () => {
        clickMessageBox.innerText = '一起来运动吧!';
        clickMessageBox.classList.add('show');
        setTimeout(() => {
            clickMessageBox.classList.remove('show');
        }, 700);

        messageBox.classList.remove('show');
        clearTimeout(mouseLeaveTimeout);
        clearTimeout(autoHideTimeout);
    });
}

cycling.scss

骑行统计页面不会止步于此,接下来还会有很大的延申改动,我提前把变量接口留好了,定义了一些主样式变量,SCSS模块化继承了一些基础样式,二次开发会轻松很多

// 总次数和总距离字体
$primary-color: #2ea9df;
// 柱状图字体
$gray-color: #333;
// 柱状图颜色
$light-gray-color: #EBE6F2;
// 柱状图边框
$light-gray-border-color: #DFD7E9;
// 未活动日历
$no-activity-color: gray;
//------ 分类色
// 公路车
$cycling-color: #EBE6F2;
$cycling-border-color: #DFD7E9; 
// 跑步
$running-color: #D5E5D3;
$running-border-color: #BDD6BA;
// 背景和文本颜色
$background-color: #333;
$text-color: #fff;

@import 'base';
@import 'calendar';
@import 'bar-chart';
@import 'sports';
@import 'message-box';

webpack配置

html和scss没啥好看的,配置一下收工

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        main: path.resolve(__dirname, 'src/main.js'),
        cycling: path.resolve(__dirname, 'src/cycling.js'),
    },
    output: {
        path: path.resolve(__dirname, 'assets'),
        filename: '[name].min.js',
        publicPath: '/'
    },
    stats: {
        entrypoints: false,
        children: false
    },
    module: {
        rules: [
            {
                test: /\.(scss|css)$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.html$/,
                use: ['html-loader']
            }
        ],
    },
    resolve: {
        alias: {
            'iDisqus.css': 'disqus-php-api/dist/iDisqus.css',
        }
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].min.css'
        })
    ],
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                parallel: true
            }),
            new CssMinimizerPlugin()
        ],
    }
};

效果

Fooleap的博客真的是相当不错,我特别喜欢他写的Jekyll主题,还有很大的折腾空间,比如全站PJAX、懒加载等等

这一周,我也着手用JQuery重新了写整站,完事后感觉真傻逼了,属于画蛇添足,多此一举。毕竟小站点,拖着一个磨盘挺累的。不上国内服务器的话,原生这条路死磕到底了,不过PJAX是必须要上的,预计下星期全站PJAX、懒加载上线

初稿

骑行:https://lhasa.icu/cycling.html

  • ✇游钓四方的博客
  • 写一个Chrome表单自动化插件游钓四方的博客
    在刷博客的时候,最麻烦的事情之一就是手动填写各种表单。为了提高我的冲浪体验,诞生了这款表单自动化插件。经过爬虫上百次调教,兼容95%博客,另外5%的网站正常人写不出来,autocomplete小伎俩都上不了台面,各种防止逆向、防调试测试,心累。 项目结构 插件纯绿色,无隐私可言。除images外,全部资源和代码文件都经过Webpack打包,下面是项目的目录结构以及各部分的说明: Form-automation-plugin │ index.html │ LICENSE │ manifest.json │ package-lock.json │ package.json │ README.md │ webpack.config.js │ ├─dist │ 33a80cb13f78b37acb39.woff2 │ 8093dd36a9b1ec918992.ttf │ 8521c461ad88443142d9.woff │ autoFill.min.js │ eventHandler.min.js │ formM
     

写一个Chrome表单自动化插件

2024年8月7日 14:26

在刷博客的时候,最麻烦的事情之一就是手动填写各种表单。为了提高我的冲浪体验,诞生了这款表单自动化插件。经过爬虫上百次调教,兼容95%博客,另外5%的网站正常人写不出来,autocomplete小伎俩都上不了台面,各种防止逆向、防调试测试,心累。

项目结构

插件纯绿色,无隐私可言。除images外,全部资源和代码文件都经过Webpack打包,下面是项目的目录结构以及各部分的说明:

Form-automation-plugin
│  index.html
│  LICENSE
│  manifest.json
│  package-lock.json
│  package.json
│  README.md
│  webpack.config.js
│  
├─dist
│      33a80cb13f78b37acb39.woff2
│      8093dd36a9b1ec918992.ttf
│      8521c461ad88443142d9.woff
│      autoFill.min.js
│      eventHandler.min.js
│      formManager.min.js
│      main.min.css
│
└─src
    │  autoFill.js
    │  eventHandler.js
    │  formManager.js
    │  template.css
    │  template.html
    │
    ├─fonts
    │      iconfont.css
    │      iconfont.ttf
    │      iconfont.woff
    │      iconfont.woff2
    │
    └─images
            Appreciation-code.jpg
            icon128.png
            icon16.png
            icon48.png

webpack.config.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    autoFill: './src/autoFill.js',
    eventHandler: './src/eventHandler.js',
    formManager: './src/formManager.js',
  },
  output: {
    filename: '[name].min.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'main.min.css',
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src', 'template.html'),
      filename: '../index.html',
      inject: 'body',
    }),
  ],
  resolve: {
    extensions: ['.js', '.css'],
  },
};

autoFill.js

// autoFill.js文件是插件的最重要的核心模块,涉及到了插件的主要输出功能
// 遍历当前页面所有input,将autocomplete值设置为on,监听Textarea输入时触发
function handleAutocomplete() {
  const inputs = document.querySelectorAll('input');
  inputs.forEach(input => {
    const autocompleteAttr = input.getAttribute('autocomplete');
    if (autocompleteAttr) {
      input.setAttribute('autocomplete', 'on');
    } else {
      input.setAttribute('autocomplete', 'on');
    }
  });
}

// 这个函数有些臃肿,马上要去骑车,懒得搞了,现在的逻辑已经完善到9成了,大多数意外情况都卷了进去,但是一些防逆向防调试,我暂时无法解决,前端菜鸟,还望大哥指点一二
function fillInputFields() {
  chrome.storage.sync.get(["name", "email", "url"], (data) => {
    // console.log(data);

    const hasValidName = data.name !== undefined && data.name !== "";
    const hasValidEmail = data.email !== undefined && data.email !== "";
    const hasValidUrl = data.url !== undefined && data.url !== "";

    // 关键字
    const nameKeywords = [
      "name", "author", "display_name", "full-name", "username", "nick", "displayname", 
      "first-name", "last-name", "full name", "real-name", "given-name", 
      "family-name", "user-name", "pen-name", "alias", "name-field", "displayname"
    ];
    const emailKeywords = [
      "email", "mail", "contact", "emailaddress", "mailaddress", 
      "email-address", "mail-address", "email-addresses", "mail-addresses", 
      "emailaddresses", "mailaddresses", "contactemail", "useremail", 
      "contact-email", "user-mail"
    ];
    const urlKeywords = [
      "url", "link", "website", "homepage", "site", "web", "address", 
      "siteurl", "webaddress", "homepageurl", "profile", "homepage-link"
    ];

    const inputs = document.querySelectorAll("input, textarea");

    inputs.forEach((input) => {
      const typeAttr = input.getAttribute("type")?.toLowerCase() || "";
      const nameAttr = input.getAttribute("name")?.toLowerCase() || "";
      let valueToSet = "";

      // 处理 URL
      if (urlKeywords.some(keyword => nameAttr.includes(keyword))) {
        if (hasValidUrl) {
          valueToSet = data.url;
        }
      }
      // 处理邮箱
      else if (emailKeywords.some(keyword => nameAttr.includes(keyword))) {
        if (hasValidEmail) {
          valueToSet = data.email;
        }
      }
      // 处理名称
      else if (nameKeywords.some(keyword => nameAttr.includes(keyword))) {
        if (hasValidName) {
          valueToSet = data.name;
        }
      }

      // 处理没有 type 属性或者 type 为 text 的情况
      if ((typeAttr === "" || typeAttr === "text") && valueToSet === "") {
        if (nameAttr && nameKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidName) {
            valueToSet = data.name;
          }
        } else if (nameAttr && urlKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidUrl) {
            valueToSet = data.url;
          }
        }
      }

      // 处理 type 为 email
      if (typeAttr === "email" && valueToSet === "") {
        if (nameAttr && emailKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidEmail) {
            valueToSet = data.email;
          }
        }
      }

      // 处理 type 为 url
      if (typeAttr === "url" && valueToSet === "") {
        if (nameAttr && urlKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidUrl) {
            valueToSet = data.url;
          }
        }
      }

      // 填充输入字段
      if (valueToSet !== "") {
        input.value = valueToSet;
      }
    });
  });
}

function clearInputFields() {
  const inputs = document.querySelectorAll("input");
  inputs.forEach((input) => {
    const typeAttr = input.getAttribute("type")?.toLowerCase();
    if (typeAttr === "text" || typeAttr === "email") {
      input.value = "";
    }
  });
}

// 监听 textarea 标签的输入事件
document.addEventListener("input", (event) => {
  if (event.target.tagName.toLowerCase() === "textarea") {
    handleAutocomplete();
    fillInputFields();
  }
});

formManager.js

该文件负责向Chrome本地存储和修改,就CURD,没啥含量

import './fonts/iconfont.css';
import './template.css';

document.getElementById("save").addEventListener("click", () => {
  const saveButton = document.getElementById("save");
  if (saveButton.textContent === "更改") {
    unlockInputFields();
    changeButtonText("保存");
    return;
  }

  const name = document.getElementById("name").value.trim();
  const email = document.getElementById("email").value.trim();
  const url = document.getElementById("url").value.trim();

  // 验证邮箱格式的正则表达式
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (name === "" || email === "") {
    alert("请填写必填字段:姓名和邮箱!");
    return;
  }

  if (!emailPattern.test(email)) {
    alert("请输入有效的邮箱地址!");
    return;
  }

  // 从 Chrome 存储中读取当前的值
  chrome.storage.sync.get(["name", "email", "url"], (data) => {
    const isNameAndEmailChanged = name !== data.name || email !== data.email;
    const isUrlChanged = url !== data.url;

    if (isNameAndEmailChanged || isUrlChanged) {
      chrome.storage.sync.set({ name, email, url }, () => {
        lockInputFields();
        changeButtonText("更改");
      });
    } else {
      lockInputFields();
      changeButtonText("更改");
    }
  });
});

// 页面加载完成时执行
document.addEventListener("DOMContentLoaded", () => {
  chrome.storage.sync.get(["name", "email", "url"], (data) => {
    document.getElementById("name").value = data.name || "";
    document.getElementById("email").value = data.email || "";
    document.getElementById("url").value = data.url || "";

    if (data.name || data.email || data.url) {
      lockInputFields();
      changeButtonText("更改");
    }
  });

  const menuItems = document.querySelectorAll('.dl-menu li a');
  const tabContents = document.querySelectorAll('.tab-content');

  menuItems.forEach(menuItem => {
    menuItem.addEventListener('click', (event) => {
      event.preventDefault();
      tabContents.forEach(tab => tab.classList.remove('active'));
      const targetId = menuItem.getAttribute('href').substring(1);
      document.getElementById(targetId).classList.add('active');
      menuItems.forEach(item => item.parentElement.classList.remove('active'));
      menuItem.parentElement.classList.add('active');
    });
  });
});

// 锁定输入框
function lockInputFields() {
  document.getElementById("name").setAttribute("disabled", "true");
  document.getElementById("email").setAttribute("disabled", "true");
  document.getElementById("url").setAttribute("disabled", "true");
}

// 解锁输入框
function unlockInputFields() {
  document.getElementById("name").removeAttribute("disabled");
  document.getElementById("email").removeAttribute("disabled");
  document.getElementById("url").removeAttribute("disabled");
}

// 更改按钮文本
function changeButtonText(text) {
  document.getElementById("save").textContent = text;
}

使用方法

git clone到本地,浏览器打开:chrome://extensions/,加载已解压的扩展程序

由于我没有注册Chrome应用商店开发者,目前只能本地运行,过几天上线应用商店,Tampermonkey等骑车回来再做

Github

Form-automation-plugin:https://github.com/achuanya/Form-automation-plugin

  • ✇游钓四方的博客
  • 七月最后一天骑行,有氧100公里游钓四方的博客
    今天是七月的最后一天,晚上必须来一次有氧小长途骑行,目标暂且定为100公里 出发前,先泡两瓶蛋白粉放进冰箱,一瓶550ML,一瓶750ML,我还是觉得不够用。骑车过程中不想下来,容易打断节奏。我打算坐尾再安装一个支架放一瓶750ML,只要室外温度不是特变态,百里油耗三瓶水没有问题。 检查一下前变、后拨、夹气和胎压。前胎由于自补液在气嘴处凝固,导致无法打气,不过目前胎压足够,不影响骑行,估计再骑一周胎压就不行了,由于管胎的特殊结构,我还没有合适的解决方案,除了换胎。 下午喝了两碗绿豆粥,吃了些核桃饼,开始做最后的准备,带了两件便携式螺丝刀,一小包纸巾,一包干湿巾,一包电解质盐丸,这些正好放进后尾包,占了大概60%空间,剩余空间还能放盒烟。 衣服就没啥好挑的,穿条ASSOS背带裤和速白干背心就行了,如果不是为了注意个人形象,我直接背带裤光膀子。在上海生活的时候也特热,多次骑行都是背带裤光膀子,有两次路过外滩被交警抓了,说外滩都是游客,我个人形象影响市容。 到地方了,这骑行路段是挺不错的,我有七年没来这里了,今天来到这里发现大变样了,还可以租皮划艇,改天一定玩玩。在这里绕圈骑了
     

七月最后一天骑行,有氧100公里

2024年8月1日 01:37

今天是七月的最后一天,晚上必须来一次有氧小长途骑行,目标暂且定为100公里

出发前,先泡两瓶蛋白粉放进冰箱,一瓶550ML,一瓶750ML,我还是觉得不够用。骑车过程中不想下来,容易打断节奏。我打算坐尾再安装一个支架放一瓶750ML,只要室外温度不是特变态,百里油耗三瓶水没有问题。

奥普帝蒙 黑标分离乳清蛋白粉

检查一下前变、后拨、夹气和胎压。前胎由于自补液在气嘴处凝固,导致无法打气,不过目前胎压足够,不影响骑行,估计再骑一周胎压就不行了,由于管胎的特殊结构,我还没有合适的解决方案,除了换胎。

下午喝了两碗绿豆粥,吃了些核桃饼,开始做最后的准备,带了两件便携式螺丝刀,一小包纸巾,一包干湿巾,一包电解质盐丸,这些正好放进后尾包,占了大概60%空间,剩余空间还能放盒烟。

衣服就没啥好挑的,穿条ASSOS背带裤和速白干背心就行了,如果不是为了注意个人形象,我直接背带裤光膀子。在上海生活的时候也特热,多次骑行都是背带裤光膀子,有两次路过外滩被交警抓了,说外滩都是游客,我个人形象影响市容。

到地方了,这骑行路段是挺不错的,我有七年没来这里了,今天来到这里发现大变样了,还可以租皮划艇,改天一定玩玩。在这里绕圈骑了十五公里,又跑去周口公园绕圈,要说这夏天油耗确实高,机动车顶不住,人也顶不住啊!我的水有点不够了,今天才33°,河南的热和江浙沪的热真是不一样,回家后一直不适应,出公园后去蜜雪冰城买了一杯柠檬水,找店里的小妹妹白嫖了两瓶冰水,直奔淮阳区·龙湖

周口植物园

到龙湖后里程已经到了60公里,这时天也暗了,有些许疲惫,主要是颈椎疼,下把位骑多了。吃了两片盐丸,两手握把立边缘慢骑摆烂二十分钟,这个时候一定不能下来歇,推车都不行,下车体力直接归零,不知道别人咋样,我是这样的,与自己较劲,100公里?200公里又算个屁,出来了,就要干

奶奶煮的绿豆粥

在龙湖绕了三圈后达标,真是饿得不行了。到家已经十点,浑身湿透,黏糊糊的,洗了澡,洗了衣服,然后躺下看订阅,期待八月的骑行。

今日有氧100公里完成

  • ✇游钓四方的博客
  • 利用Go+Github Actions写个定时RSS爬虫游钓四方的博客
    说起这事,还是受一位博友的启发“1900”他的左邻右舍页面很棒,决定模仿一下。我平时也用 Inoreader,但我还是喜欢直接打开博客的感觉,心血来潮,搞。 起初,我打算使用 COS 和 GitHub Actions,但在测试过程中发现 GitHub 的延迟非常高,验证和文件写入速度极慢,频频失败。干脆直接上 GitHub 自产自销。 大致思路 main() │ ├── readFeedsFromGitHub() │ ├── GitHub API 调用 │ │ ├── 读取 rss_feeds.txt 文件 │ │ └── 处理文件报错 │ └── Return │ ├── fetchRSS() │ ├── 遍历 RSS │ │ ├── HTTP GET 请求 │ │ └── 处理请求错误 │ ├── 解析 RSS │ │ ├── 清理 XML 内容中的非法字符 │ │ ├── 提取域名 │ │ └── 格式化并排序 │ └── Return │ └── saveToGitHub() ├── Git
     

利用Go+Github Actions写个定时RSS爬虫

2024年7月27日 09:50

说起这事,还是受一位博友的启发“1900”他的左邻右舍页面很棒,决定模仿一下。我平时也用 Inoreader,但我还是喜欢直接打开博客的感觉,心血来潮,搞。

起初,我打算使用 COS 和 GitHub Actions,但在测试过程中发现 GitHub 的延迟非常高,验证和文件写入速度极慢,频频失败。干脆直接上 GitHub 自产自销。

大致思路

main()
│
├── readFeedsFromGitHub()
│   ├── GitHub API 调用
│   │   ├── 读取 rss_feeds.txt 文件
│   │   └── 处理文件报错
│   └── Return
│
├── fetchRSS()
│   ├── 遍历 RSS
│   │   ├── HTTP GET 请求
│   │   └── 处理请求错误
│   ├── 解析 RSS
│   │   ├── 清理 XML 内容中的非法字符
│   │   ├── 提取域名
│   │   └── 格式化并排序
│   └── Return
│
└── saveToGitHub()
    ├── GitHub API 调用
    │   ├── 保存到 _data/rss_data.json 供 Jekyll 调用
    │   └── 处理错误
    └── Return

由于用 Go 搬砖,所有的包、类型和方法均可在 GitHub API 客户端库的第 39 版文档查询

关于 Github API 有一点需要注意,配置好环境变量后,Token 操作仓库需要有一定的权限,务必启用 Read and write permissions 读取和写入权限

go mod init github.com/achuanya/Grab-latest-RSS
// Go-GitHub v39
go get github.com/google/go-github/v39/github
// RSS 和 Atom feeds 解析库
go get github.com/mmcdole/gofeed
// OAuth2 认证和授权
go get golang.org/x/oauth2

Go RSS 爬虫 Code

package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"sort"
	"sync"
	"time"

	"github.com/google/go-github/v39/github"
	"github.com/mmcdole/gofeed"
	"golang.org/x/oauth2"
)

const (
	maxRetries    = 3                // 最大重试次数
	retryInterval = 10 * time.Second // 重试间隔时间
)

type Config struct {
	GithubToken      string // GitHub API 令牌
	GithubName       string // GitHub 用户名
	GithubRepository string // GitHub 仓库名
}

// 用于解析 avatar_data.json 文件的结构
type Avatar struct {
	Name   string `json:"name"`   // 用户名
	Avatar string `json:"avatar"` // 头像 URL
}

// 爬虫抓取的数据结构
type Article struct {
	DomainName string `json:"domainName"` // 域名
	Name       string `json:"name"`       // 博客名称
	Title      string `json:"title"`      // 文章标题
	Link       string `json:"link"`       // 文章链接
	Date       string `json:"date"`       // 格式化后的文章发布时间
	Avatar     string `json:"avatar"`     // 头像 URL
}

// 初始化并返回配置信息
func initConfig() Config {
	return Config{
		GithubToken:      os.Getenv("TOKEN"), // 从环境变量中获取 GitHub API 令牌
		GithubName:       "achuanya",         // GitHub 用户名
		GithubRepository: "lhasa.github.io",  // GitHub 仓库名
	}
}

// 清理 XML 内容中的非法字符
func cleanXMLContent(content string) string {
	re := regexp.MustCompile(`[\x00-\x1F\x7F-\x9F]`)
	return re.ReplaceAllString(content, "")
}

// 尝试解析不同格式的时间字符串
func parseTime(timeStr string) (time.Time, error) {
	formats := []string{
		time.RFC3339,
		time.RFC3339Nano,
		time.RFC1123Z,
		time.RFC1123,
	}

	for _, format := range formats {
		if t, err := time.Parse(format, timeStr); err == nil {
			return t, nil
		}
	}
	return time.Time{}, fmt.Errorf("unable to parse time: %s", timeStr)
}

// 将时间格式化为 "January 2, 2006"
func formatTime(t time.Time) string {
	return t.Format("January 2, 2006")
}

// 从 URL 中提取域名,并添加 https:// 前缀
func extractDomain(urlStr string) (string, error) {
	u, err := url.Parse(urlStr)
	if err != nil {
		return "", err
	}
	domain := u.Hostname()
	protocol := "https://"
	if u.Scheme != "" {
		protocol = u.Scheme + "://"
	}
	fullURL := protocol + domain

	return fullURL, nil
}

// 获取当前的北京时间
func getBeijingTime() time.Time {
	beijingTimeZone := time.FixedZone("CST", 8*3600)
	return time.Now().In(beijingTimeZone)
}

// 记录错误信息到 error.log 文件
func logError(config Config, message string) {
	logMessage(config, message, "error.log")
}

// 记录信息到指定的文件
func logMessage(config Config, message string, fileName string) {
	ctx := context.Background()
	client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: config.GithubToken,
	})))

	filePath := "_data/" + fileName
	fileContent := []byte(message + "\n\n")

	file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
	if err != nil && resp.StatusCode == http.StatusNotFound {
		_, _, err := client.Repositories.CreateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
			Message: github.String("Create " + fileName),
			Content: fileContent,
			Branch:  github.String("master"),
		})
		if err != nil {
			fmt.Printf("error creating %s in GitHub: %v\n", fileName, err)
		}
		return
	} else if err != nil {
		fmt.Printf("error checking %s in GitHub: %v\n", fileName, err)
		return
	}

	decodedContent, err := file.GetContent()
	if err != nil {
		fmt.Printf("error decoding %s content: %v\n", fileName, err)
		return
	}

	updatedContent := append([]byte(decodedContent), fileContent...)

	_, _, err = client.Repositories.UpdateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
		Message: github.String("Update " + fileName),
		Content: updatedContent,
		SHA:     github.String(*file.SHA),
		Branch:  github.String("master"),
	})
	if err != nil {
		fmt.Printf("error updating %s in GitHub: %v\n", fileName, err)
	}
}

// 从 GitHub 仓库中获取 JSON 文件内容
func fetchFileFromGitHub(config Config, filePath string) (string, error) {
	ctx := context.Background()
	client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: config.GithubToken,
	})))

	file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
	if err != nil {
		if resp.StatusCode == http.StatusNotFound {
			return "", fmt.Errorf("file not found: %s", filePath)
		}
		return "", fmt.Errorf("error fetching file %s from GitHub: %v", filePath, err)
	}

	content, err := file.GetContent()
	if err != nil {
		return "", fmt.Errorf("error decoding file %s content: %v", filePath, err)
	}

	return content, nil
}

// 从 GitHub 仓库中读取头像配置
func loadAvatarsFromGitHub(config Config) (map[string]string, error) {
	content, err := fetchFileFromGitHub(config, "_data/avatar_data.json")
	if err != nil {
		return nil, err
	}

	var avatars []Avatar
	if err := json.Unmarshal([]byte(content), &avatars); err != nil {
		return nil, err
	}

	avatarMap := make(map[string]string)
	for _, a := range avatars {
		avatarMap[a.Name] = a.Avatar
	}

	return avatarMap, nil
}

// 从 RSS 列表中抓取最新的文章,并按发布时间排序
func fetchRSS(config Config, feeds []string) ([]Article, error) {
	var articles []Article
	var mu sync.Mutex     // 用于保证并发安全
	var wg sync.WaitGroup // 用于等待所有 goroutine 完成

	avatars, err := loadAvatarsFromGitHub(config)
	if err != nil {
		logError(config, fmt.Sprintf("[%s] [Load avatars error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
		return nil, err
	}

	fp := gofeed.NewParser()
	httpClient := &http.Client{
		Timeout: 10 * time.Second,
	}

	for _, feedURL := range feeds {
		wg.Add(1)
		go func(feedURL string) {
			defer wg.Done()
			var resp *http.Response
			var bodyString string
			var fetchErr error

			for i := 0; i < maxRetries; i++ {
				resp, fetchErr = httpClient.Get(feedURL)
				if fetchErr == nil {
					bodyBytes := new(bytes.Buffer)
					bodyBytes.ReadFrom(resp.Body)
					bodyString = bodyBytes.String()
					resp.Body.Close()
					break
				}
				logError(config, fmt.Sprintf("[%s] [Get RSS error] %s: Attempt %d/%d: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, i+1, maxRetries, fetchErr))
				time.Sleep(retryInterval)
			}

			if fetchErr != nil {
				logError(config, fmt.Sprintf("[%s] [Failed to fetch RSS] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, fetchErr))
				return
			}

			cleanBody := cleanXMLContent(bodyString)

			var feed *gofeed.Feed
			var parseErr error
			for i := 0; i < maxRetries; i++ {
				feed, parseErr = fp.ParseString(cleanBody)
				if parseErr == nil {
					break
				}
				logError(config, fmt.Sprintf("[%s] [Parse RSS error] %s: Attempt %d/%d: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, i+1, maxRetries, parseErr))
				time.Sleep(retryInterval)
			}

			if parseErr != nil {
				logError(config, fmt.Sprintf("[%s] [Failed to parse RSS] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), feedURL, parseErr))
				return
			}

			mainSiteURL := feed.Link
			domainName, err := extractDomain(mainSiteURL)
			if err != nil {
				logError(config, fmt.Sprintf("[%s] [Extract domain error] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), mainSiteURL, err))
				domainName = "unknown"
			}

			name := feed.Title
			avatarURL := avatars[name]
			if avatarURL == "" {
				avatarURL = "https://cos.lhasa.icu/LinksAvatar/default.png"
			}

			if len(feed.Items) > 0 {
				item := feed.Items[0]

				publishedTime, err := parseTime(item.Published)
				if err != nil && item.Updated != "" {
					publishedTime, err = parseTime(item.Updated)
				}

				if err != nil {
					logError(config, fmt.Sprintf("[%s] [Getting article time error] %s: %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), item.Title, err))
					publishedTime = time.Now()
				}

				originalName := feed.Title
				// 该长的地方短,该短的地方长
				nameMapping := map[string]string{
					"obaby@mars": "obaby",
					"青山小站 | 一个在帝都搬砖的新时代农民工":       "青山小站",
					"Homepage on Miao Yu | 于淼":    "于淼",
					"Homepage on Yihui Xie | 谢益辉": "谢益辉",
				}

				validNames := make(map[string]struct{})
				for key := range nameMapping {
					validNames[key] = struct{}{}
				}

				_, valid := validNames[originalName]
				if !valid {
					for key := range validNames {
						if key == originalName {
							logError(config, fmt.Sprintf("[%s] [Name mapping not found] %s", getBeijingTime().Format("Mon Jan 2 15:04:2006"), originalName))
							break
						}
					}
				} else {
					name = nameMapping[originalName]
				}

				mu.Lock()
				articles = append(articles, Article{
					DomainName: domainName,
					Name:       name,
					Title:      item.Title,
					Link:       item.Link,
					Avatar:     avatarURL,
					Date:       formatTime(publishedTime),
				})
				mu.Unlock()
			}
		}(feedURL)
	}

	wg.Wait()
	sort.Slice(articles, func(i, j int) bool {
		date1, _ := time.Parse("January 2, 2006", articles[i].Date)
		date2, _ := time.Parse("January 2, 2006", articles[j].Date)
		return date1.After(date2)
	})

	return articles, nil
}

// 将爬虫抓取的数据保存到 GitHub
func saveToGitHub(config Config, data []Article) error {
	ctx := context.Background()
	client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: config.GithubToken,
	})))

	manualArticles := []Article{
		{
			DomainName: "https://foreverblog.cn",
			Name:       "十年之约",
			Title:      "穿梭虫洞-随机访问十年之约友链博客",
			Link:       "https://foreverblog.cn/go.html",
			Date:       "January 01, 2000",
			Avatar:     "https://cos.lhasa.icu/LinksAvatar/foreverblog.cn.png",
		},
		{
			DomainName: "https://www.travellings.cn",
			Name:       "开往",
			Title:      "开往-友链接力",
			Link:       "https://www.travellings.cn/go.html",
			Date:       "January 01, 2000",
			Avatar:     "https://cos.lhasa.icu/LinksAvatar/www.travellings.png",
		},
	}

	data = append(data, manualArticles...)
	jsonData, err := json.Marshal(data)
	if err != nil {
		return err
	}

	filePath := "_data/rss_data.json"
	file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
	if err != nil && resp.StatusCode == http.StatusNotFound {
		_, _, err := client.Repositories.CreateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
			Message: github.String("Create rss_data.json"),
			Content: jsonData,
			Branch:  github.String("master"),
		})
		if err != nil {
			return fmt.Errorf("error creating rss_data.json in GitHub: %v", err)
		}
		return nil
	} else if err != nil {
		return fmt.Errorf("error checking rss_data.json in GitHub: %v", err)
	}

	_, _, err = client.Repositories.UpdateFile(ctx, config.GithubName, config.GithubRepository, filePath, &github.RepositoryContentFileOptions{
		Message: github.String("Update rss_data.json"),
		Content: jsonData,
		SHA:     github.String(*file.SHA),
		Branch:  github.String("master"),
	})
	if err != nil {
		return fmt.Errorf("error updating rss_data.json in GitHub: %v", err)
	}

	return nil
}

// 从 GitHub 仓库中获取 RSS 文件
func readFeedsFromGitHub(config Config) ([]string, error) {
	ctx := context.Background()
	client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: config.GithubToken,
	})))

	filePath := "_data/rss_feeds.txt"
	file, _, resp, err := client.Repositories.GetContents(ctx, config.GithubName, config.GithubRepository, filePath, nil)
	if err != nil && resp.StatusCode == http.StatusNotFound {
		errMsg := fmt.Sprintf("Error: %s not found in GitHub repository", filePath)
		logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
		return nil, fmt.Errorf(errMsg)
	} else if err != nil {
		errMsg := fmt.Sprintf("Error fetching %s from GitHub: %v", filePath, err)
		logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
		return nil, fmt.Errorf(errMsg)
	}

	content, err := file.GetContent()
	if err != nil {
		errMsg := fmt.Sprintf("Error decoding %s content: %v", filePath, err)
		logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
		return nil, fmt.Errorf(errMsg)
	}

	var feeds []string
	scanner := bufio.NewScanner(bytes.NewReader([]byte(content)))

	for scanner.Scan() {
		feeds = append(feeds, scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		errMsg := fmt.Sprintf("Error reading RSS file content: %v", err)
		logError(config, fmt.Sprintf("[%s] [Read RSS file error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), errMsg))
		return nil, fmt.Errorf(errMsg)
	}

	return feeds, nil
}

func main() {
	config := initConfig()

	// 从 GitHub 仓库中读取 RSS feeds 列表
	rssFeeds, err := readFeedsFromGitHub(config)
	if err != nil {
		logError(config, fmt.Sprintf("[%s] [Read RSS feeds error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
		fmt.Printf("Error reading RSS feeds from GitHub: %v\n", err)
		return
	}

	// 抓取 RSS feeds
	articles, err := fetchRSS(config, rssFeeds)
	if err != nil {
		logError(config, fmt.Sprintf("[%s] [Fetch RSS error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
		fmt.Printf("Error fetching RSS feeds: %v\n", err)
		return
	}

	// 将抓取的数据保存到 GitHub 仓库
	err = saveToGitHub(config, articles)
	if err != nil {
		logError(config, fmt.Sprintf("[%s] [Save data to GitHub error] %v", getBeijingTime().Format("Mon Jan 2 15:04:2006"), err))
		fmt.Printf("Error saving data to GitHub: %v\n", err)
		return
	}
	fmt.Println("Stop writing code and go ride a road bike now!")
}

Go 生成的 json 数据

[
    {
        "domainName": "https://yihui.org",
        "name": "谢益辉",
        "title": "Rd2roxygen",
        "link": "https://yihui.org/rd2roxygen/",
        "date": "April 14, 2024",
        "avatar": "https://cos.lhasa.icu/LinksAvatar/yihui.org.png"
    },
    {
        "domainName": "https://www.laruence.com",
        "name": "风雪之隅",
        "title": "PHP8.0的Named Parameter",
        "link": "https://www.laruence.com/2022/05/10/6192.html",
        "date": "May 10, 2022",
        "avatar": "https://cos.lhasa.icu/LinksAvatar/www.laruence.com.png"
    }
]

Go 生成的日志

[Sat Jul 27 08:42:2024] [Parse RSS error] https://lhasa.icu: Failed to detect feed type

[Sat Jul 27 08:41:2024] [Get RSS error] https://lhasa.icu: Get "https://lhasa.icu": net/http: TLS handshake timeout

Github Actons 1h/次

name: ScheduledRssRawler

on:
  schedule:
    - cron: '0 * * * *'
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: '1.22.5'

    - name: Install dependencies
      run: go mod tidy
      working-directory: ./api

    - name: Build
      run: go build -o main
      working-directory: ./api

    - name: Run Go program
      env:
        TOKEN: $
      run: ./main
      working-directory: ./api

腾讯 COS 也写了一份,Github 有延迟问题就没用,也能用,逻辑上和 Go 是没啥区别

Grab-latest-RSS:https://github.com/achuanya/Grab-latest-RSS

COS Go SDK:https://cloud.tencent.com/document/product/436/31215

效果页:https://lhasa.icu/links.html

  • ✇游钓四方的博客
  • Tencent CDN 流量被恶意盗刷游钓四方的博客
    看到这张图的时候,我很震惊。这个CDN流量包是我昨天凌晨刚买的,直到此刻才发现我的CDN流量被恶意盗刷了。 事情是这样的,前天23号我在写新功能,本地调试调用了很多资源,当时看到消耗了90G的流量,我没有在意,以为是调试的问题。因为那天我写了一天代码,不停地调用Tencent COS,而COS还套了一层Tencent CDN。当时我以为是正常消耗,眼看流量不够,我又充了一个CDN加速包。 然而就在今晚22:45,我骑行回来关闭了免打扰模式,邮箱忽然弹出通知,腾讯云提示我CDN流量不足?我当时非常震惊,因为这是我24号凌晨刚买的流量包啊! 看到这张图时,我火了,在独立博客圈彻底火了,2天内请求数42万?赶超月光博客! 回想过去,我在博客圈认识的人一只手能数过来,更谈不上得罪谁。这事也怪我,之前COS没有任何防护,几乎处于裸奔状态。 由于我的博客托管在Github Pages,主机问题大可不必考虑,我能做的只有设置黑白名单和周期限流。 不再裸奔,已老实。 UPDATE 凌晨 02:32 知道怎么回事了,24年后,大陆境内出现一窝狗,利用PCDN恶意流量攻击!
     

Tencent CDN 流量被恶意盗刷

2024年7月25日 00:44

来自腾讯云的邮件

看到这张图的时候,我很震惊。这个CDN流量包是我昨天凌晨刚买的,直到此刻才发现我的CDN流量被恶意盗刷了。

事情是这样的,前天23号我在写新功能,本地调试调用了很多资源,当时看到消耗了90G的流量,我没有在意,以为是调试的问题。因为那天我写了一天代码,不停地调用Tencent COS,而COS还套了一层Tencent CDN。当时我以为是正常消耗,眼看流量不够,我又充了一个CDN加速包。

然而就在今晚22:45,我骑行回来关闭了免打扰模式,邮箱忽然弹出通知,腾讯云提示我CDN流量不足?我当时非常震惊,因为这是我24号凌晨刚买的流量包啊!

腾讯云 数据分析控制台

看到这张图时,我火了,在独立博客圈彻底火了,2天内请求数42万?赶超月光博客!

腾讯云 访问分布

TOP ONE 60.221.195.144

回想过去,我在博客圈认识的人一只手能数过来,更谈不上得罪谁。这事也怪我,之前COS没有任何防护,几乎处于裸奔状态。

由于我的博客托管在Github Pages,主机问题大可不必考虑,我能做的只有设置黑白名单和周期限流。

不再裸奔,已老实。

UPDATE 凌晨 02:32

知道怎么回事了,24年后,大陆境内出现一窝狗,利用PCDN恶意流量攻击!

  • 攻击的主要IP来源于山西、江苏和安徽联通等地的固定网段

  • 攻击时间非常规律,集中在19:50到23:00之间

  • 攻击者会针对体积较大的静态文件进行持续攻击

自7月初以来,已转头无差别地对大陆中小型网站展开攻击。

建议将山西等地的IP段暂时屏蔽,减少恶意流量的影响。

目前,GitHub上已经有相关项目 ban-pcdn-ip 用于收集这些恶意IP段。

  • ✇游钓四方的博客
  • 公路车管胎被扎,怎么补胎游钓四方的博客
    管胎被扎了,还是后轮,我心如刀绞啊,太贵了,换不起,外面技师都不修管胎的 解刨管胎 玻璃渣子把管胎扎透了 拿砂纸打磨一下,涂完胶等风干 胶风干后贴片,按按揉揉 好多年没做针线活了,没想到今天给管胎缝闭口 线头有些遭了,扯断好多次 闭合管胎,涂胶加固 安装后轮打气 管胎缺点就优点就是轻,比开口胎、真空胎都轻,常用于专业竞赛和环法,就算车胎破了也能继续骑的,而开口胎和真空胎不行。 缺点就是破了就废了,各大车店都是不修管胎的,只能换,这用管胎的成本真是太高了,不是富哥真用不起,这款是意大利产的Challenge Elite Pro 25c,零售价三百多/条,就这还全网缺货,八月初才到货。我的轮组不支持另外两种胎,缝缝补补吧 7-18 晚 测试补充 再次经历三过家门而不入,就是为了凑这个整,今天管胎补的可以经测试一百公里没有问题。现在我心率还不是稳不住,恢复到之前的状态好难啊
     

公路车管胎被扎,怎么补胎

2024年7月19日 14:38

管胎被扎了,还是后轮,我心如刀绞啊,太贵了,换不起,外面技师都不修管胎的

先拆后轮

解刨管胎

解刨管胎

玻璃渣子把管胎扎透了

扎眼

拿砂纸打磨一下,涂完胶等风干

打磨涂胶

胶风干后贴片,按按揉揉

贴片

好多年没做针线活了,没想到今天给管胎缝闭口

穿针引线

管胎外皮真厚

线头有些遭了,扯断好多次

穿针走线

闭合管胎,涂胶加固

闭合管胎,涂胶加固

安装后轮打气

安装后轮打气

管胎缺点就优点就是轻,比开口胎、真空胎都轻,常用于专业竞赛和环法,就算车胎破了也能继续骑的,而开口胎和真空胎不行。

缺点就是破了就废了,各大车店都是不修管胎的,只能换,这用管胎的成本真是太高了,不是富哥真用不起,这款是意大利产的Challenge Elite Pro 25c,零售价三百多/条,就这还全网缺货,八月初才到货。我的轮组不支持另外两种胎,缝缝补补吧

7-18 晚 测试补充

再次经历三过家门而不入,就是为了凑这个整,今天管胎补的可以经测试一百公里没有问题。现在我心率还不是稳不住,恢复到之前的状态好难啊

测试

  • ✇游钓四方的博客
  • 喜提新车 Wilier Cento 10SL游钓四方的博客
    得这辆车纯属缘分,前段时间在网上认识一个宁波的好大哥,没想到去年我们一起参加过同一个比赛,大哥是在宁波鄞州区开自行车店的,聊了许久大哥给我推荐一辆神车Wilier Cento 10SL!这是他朋友的爱车,财富自由润加拿大了,一些不方便带走的东西就卖掉了,这辆车刚到店里第一天,机缘巧合我就赶上了! 配置如下: Wilier Cento 10SL RIM STEMMA SL 把立 BARRA SL 弯把 WIND BREAK 50框 碳辐条 SHIMANO 105 R7000夹气,其他全车Ultegra 8000 算上平踏、水杯架 整车重为7.45KG,一对平踏重0.3KG,换DA夹气分分钟上6! 这台车最吸引我的地方就是,圈刹!SL后最后一代顶级圈刹车,我在网上找了许久都找不到同款,这台车已经停产几年了,太稀有了, 上图
     

喜提新车 Wilier Cento 10SL

2024年7月12日 22:33

得这辆车纯属缘分,前段时间在网上认识一个宁波的好大哥,没想到去年我们一起参加过同一个比赛,大哥是在宁波鄞州区开自行车店的,聊了许久大哥给我推荐一辆神车Wilier Cento 10SL!这是他朋友的爱车,财富自由润加拿大了,一些不方便带走的东西就卖掉了,这辆车刚到店里第一天,机缘巧合我就赶上了!

配置如下:

  • Wilier Cento 10SL RIM
  • STEMMA SL 把立
  • BARRA SL 弯把
  • WIND BREAK 50框 碳辐条
  • SHIMANO 105 R7000夹气,其他全车Ultegra 8000

算上平踏、水杯架 整车重为7.45KG,一对平踏重0.3KG,换DA夹气分分钟上6!

这台车最吸引我的地方就是,圈刹!SL后最后一代顶级圈刹车,我在网上找了许久都找不到同款,这台车已经停产几年了,太稀有了,

上图

车到了

我装车的时候把刹车线芯插坏了

我装好了

寄回来之前,技师称重

骑行照

  • ✇游钓四方的博客
  • 搞个公众号游钓四方的博客
    改名记录: 2024年02月08日 “阿川的博客”改名“游钓四方的博客” 2019年05月28日 “阿川的个人博客”改名“阿川的博客” 2018年10月26日 注册“阿川的个人博客” 今天捡回了18年注册的公众号,数据重新导了一遍,这手动整理几年的文章数据,我多少有些疲惫 这次熬的有点久了,明歇一天,再写个脚本让 Github Pages 文章自动同步到微信公众号,得想个办法 公众号还需要做个人认证,不然内置超链接是个麻烦事 公众号前端页面也需要重新写一个,腾讯自带UI限制文章数量 任重而道远啊!加油。
     

搞个公众号

2024年3月10日 15:51

改名记录:

  • 2024年02月08日 “阿川的博客”改名“游钓四方的博客”
  • 2019年05月28日 “阿川的个人博客”改名“阿川的博客”
  • 2018年10月26日 注册“阿川的个人博客”

今天捡回了18年注册的公众号,数据重新导了一遍,这手动整理几年的文章数据,我多少有些疲惫

这次熬的有点久了,明歇一天,再写个脚本让 Github Pages 文章自动同步到微信公众号,得想个办法

公众号还需要做个人认证,不然内置超链接是个麻烦事

公众号前端页面也需要重新写一个,腾讯自带UI限制文章数量

任重而道远啊!加油。

  • ✇游钓四方的博客
  • 解决Jekyll时区数据源游钓四方的博客
    由于Jekyll默认使用UTC时区,导致博客更新时间不准确。这里需要写入上海时间:timezone: Asia/Shanghai,但是我在本地调试时需要在配置内注释掉,不然就会报错 jekyll 3.9.3 | Error: No source of timezone data could be found. Please refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error. 上传到仓库 Github pages 不会出现这样的问题。老是注释调试挺麻烦的,Google搜出来的解决方案都是瞎扯淡,也不知道都是哪复制粘贴就发出来的。 gem install tzinfo-data Gemfile 直接指定版本 gem 'tzinfo-data', '>= 1.2021a' 写入配置 timezone: Asia/Shanghai,确保调试的电脑时区也正常,开始运行 bundle exec jekyll serve
     

解决Jekyll时区数据源

2024年2月11日 23:07

由于Jekyll默认使用UTC时区,导致博客更新时间不准确。这里需要写入上海时间:timezone: Asia/Shanghai,但是我在本地调试时需要在配置内注释掉,不然就会报错

  • jekyll 3.9.3 | Error: No source of timezone data could be found. Please refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error.

上传到仓库 Github pages 不会出现这样的问题。老是注释调试挺麻烦的,Google搜出来的解决方案都是瞎扯淡,也不知道都是哪复制粘贴就发出来的。

gem install tzinfo-data

Gemfile 直接指定版本

gem 'tzinfo-data', '>= 1.2021a'

写入配置 timezone: Asia/Shanghai,确保调试的电脑时区也正常,开始运行

bundle exec jekyll serve

  • ✇游钓四方的博客
  • 初一大吉,博客上上新游钓四方的博客
    图片预览 ` var preview = document.getElementById("preview"); var previewImage = document.getElementById("previewImage"); var previewImageTitle = <figcaption class="previewImageTitle"> + image.title[i] + </figcaption>; previewImage.setAttribute('src', image.url[i]); preview.style.display = 'flex'; var previousPreviewImageTitle = document.querySelector('.previewImageTitle'); if (previousPreviewImageTitle) { previousPreviewImageTitle.parentNode.removeChild(previousPreviewImageTitle)
     

初一大吉,博客上上新

2024年2月11日 05:35

图片预览

`

var preview = document.getElementById("preview");
var previewImage = document.getElementById("previewImage");

var previewImageTitle = <figcaption class="previewImageTitle"> + image.title[i] + </figcaption>;
previewImage.setAttribute('src', image.url[i]);
preview.style.display = 'flex';

var previousPreviewImageTitle = document.querySelector('.previewImageTitle');
if (previousPreviewImageTitle) {
    previousPreviewImageTitle.parentNode.removeChild(previousPreviewImageTitle);
}
previewImage.insertAdjacentHTML('afterend', previewImageTitle);

preview.addEventListener("click", function() {
    this.style.display = 'none';
});

`

markdown太丧心病狂了,js的代码块在转换的过程中给我生效了,大多方法都不能阻止这段代码不生效,把代码删删减减让我足足花了5分钟去注释这段代码…..

点开试试

前几天Fooleap留言建议我用cn-font-split把字体做一下分包处理,分包后原18M变1M不到,一篇千文的文章才几百KB,加载速度没得说。

cn-font-split 分包后的效果

Haibao@DESKTOP-IB7LLPB MINGW64 /d/lhasa.github.io (master)
$ bundle exec npm run build

> fooleap@1.0.0 build D:\lhasa.github.io
> webpack -p && jekyll b

Hash: 470f4b7c37655d2798f9
Version: webpack 4.47.0
Time: 2347ms
Built at: 2024/02/11 上午5:27:39
       Asset      Size  Chunks             Chunk Names
main.min.css  27.2 KiB       0  [emitted]  main
 main.min.js   156 KiB       0  [emitted]  main
[0] ./src/main.js 25.7 KiB {0} [built]
[3] ./src/sass/main.scss 39 bytes {0} [built]
    + 3 hidden modules
Configuration file: D:/lhasa.github.io/_config.yml
To use retry middleware with Faraday v2.0+, install `faraday-retry` gem
            Source: D:/lhasa.github.io
       Destination: D:/lhasa.github.io/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
                    done in 7.403 seconds.
 Auto-regeneration: disabled. Use --watch to enable.

这些天我把webPack临时学了一手,也是半斤八两,普通打个包是没问题,就是不知道这分包后的字体能不能用webPack打包,我还没试过。

因为网站在Github pages,这加载速度也快到顶了。目前还缺少一个已备案的域名,我想把腾讯云的COS用CDN来加速一下,这样的话静态资源应该会更快点,博客也没啥访问量,按流量计费,也花不了几个钱。

2024/2/14 更新

后来注意到URL包含了腾讯图片处理样式后缀,这里用正则做一下处理

image.url[i] = image.url[i].replace(/\.(jpg|jpeg|png|gif)[^/]*$/, '.$1');

  • ✇游钓四方的博客
  • 腾讯云COS文件跨域游钓四方的博客
    今天换博客主要文字了,”仓耳今楷”,字体更美观更适合阅读。但是过程中遇到点问题 @font-face { font-family: 仓耳今楷01-W04; src: url("https://api.lhasa.icu/assets/font/tsanger01W04.ttf") format("truetype"); } 这段CSS写的是没有问题的,但是不生效,控制台报错跨域 has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. 腾讯云COS跨域访问CORS配置如下: 配置好后又遇到麻烦了,字体太大了,一个字体文件17.9M!网站都脱垮了 这里做一下处理,取子集压缩文字,需要用到 FontSmaller 和 现代汉语常用3500汉字 取子集压缩之后字体文件大小为1.94M
     

腾讯云COS文件跨域

2024年2月5日 17:33

今天换博客主要文字了,”仓耳今楷”,字体更美观更适合阅读。但是过程中遇到点问题

@font-face {
    font-family: 仓耳今楷01-W04;
    src: url("https://api.lhasa.icu/assets/font/tsanger01W04.ttf")  format("truetype");
}

这段CSS写的是没有问题的,但是不生效,控制台报错跨域

  • has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

腾讯云COS跨域访问CORS配置如下:

腾讯云COS跨域访问CORS设置

配置好后又遇到麻烦了,字体太大了,一个字体文件17.9M!网站都脱垮了

网站被拖垮了

这里做一下处理,取子集压缩文字,需要用到 FontSmaller现代汉语常用3500汉字

取子集压缩之后字体文件大小为1.94M

取子集压缩后的效果

  • ✇游钓四方的博客
  • 技术亦福亦是祸游钓四方的博客
    今天一个偶然接触到了Clarity Session Recording,当我看到它在我调试本地网站时,它居然录制了一条长达45分钟的视频,我大为震惊!由此,我怀揣着对技术的敬畏和亢奋的状态写下来这篇文章,虽然我非技术大牛,也非经济哲学家,但丝毫不影响我对技术的一些拙见。 技术,造福与社会的同时也存在着弊端,这是一个漫长的历史过程。在资本主义原始积累时期,技术发展相对缓慢,因此,在人们利用自然资源的同时,对于群众的利益得失,那是既看不出来,也堂而皇之。 在全民炼钢铁的时代,为了追求技术,挨家挨户交出铁器充数,上至灶台的铁锅,下至门板上的钢钉都要拆下来,全部投进了土高炉。先不讲炼铁对环境的污染,毕竟人都吃不饱,光是大食堂的一平二调、三高五风就饿死多少同胞啊! 犹如二零年新冠疫情初期的口罩机,相当受欢迎的低成本高回报疫情红利生产技术,口罩是一片难求,人民苦不堪言。而资本业绩翻一翻。但好景不长,随着口罩机业务跳水,增速不再。口罩凉了,口罩机成了一堆废铁。 要说技术亦福亦是祸,这不堪回首的民族历史足以证明。这种资本式的疯狂扩张给无知的人带来无尽的灾难,亦不可为,而为之,这就是资本的本性。
     

技术亦福亦是祸

2024年2月2日 15:50

今天一个偶然接触到了Clarity Session Recording,当我看到它在我调试本地网站时,它居然录制了一条长达45分钟的视频,我大为震惊!由此,我怀揣着对技术的敬畏和亢奋的状态写下来这篇文章,虽然我非技术大牛,也非经济哲学家,但丝毫不影响我对技术的一些拙见。

技术,造福与社会的同时也存在着弊端,这是一个漫长的历史过程。在资本主义原始积累时期,技术发展相对缓慢,因此,在人们利用自然资源的同时,对于群众的利益得失,那是既看不出来,也堂而皇之。

在全民炼钢铁的时代,为了追求技术,挨家挨户交出铁器充数,上至灶台的铁锅,下至门板上的钢钉都要拆下来,全部投进了土高炉。先不讲炼铁对环境的污染,毕竟人都吃不饱,光是大食堂的一平二调、三高五风就饿死多少同胞啊!

犹如二零年新冠疫情初期的口罩机,相当受欢迎的低成本高回报疫情红利生产技术,口罩是一片难求,人民苦不堪言。而资本业绩翻一翻。但好景不长,随着口罩机业务跳水,增速不再。口罩凉了,口罩机成了一堆废铁。

要说技术亦福亦是祸,这不堪回首的民族历史足以证明。这种资本式的疯狂扩张给无知的人带来无尽的灾难,亦不可为,而为之,这就是资本的本性。

随着技术的迅猛发展,尤其是在信息时代,技术对社会、经济、文化等方方面面的影响愈发显著。曾经由无知带来的寒蝉效应逐渐缓解。相对来说,资本已经完成原始积累,对于技术的态度更为复杂,当WEB市场各种大利好大利空时,毫不犹豫的用PHP进行开发。当PHP已经不能获得更多合理的利润时,就会对PHP嗤之以鼻,甚至会给予判处死缓,阻碍它进入生产领域。

到了新媒体时代,我们在享受数字化便利的同时,也时刻面临着隐私泄露、信息滥用等风险。随着人工智能、大数据分析等技术的日益成熟,我们的个人信息被不断挖掘,商业巨头通过分析我们的行为、偏好来精准投放广告,甚至影响我们的决策过程,因百度竞价逝世的魏则西就是典型!

此外,技术的快速发展也带来了社会的数字鸿沟。那些掌握先进技术的人群能够更好的适应现代社会,而缺乏技术接触的人可能会陷入信息贫困。这种信息差不仅是技术能力的差异,更是社会资源分配的问题。

由此可见,在现代科技的巨大浪潮中,技术进步所带来的便利与其可能引发的负面效应是相对的。技术不仅仅是一种工具,更是对社会和人类价值观的挑战。我们在追逐技术的同时,必须寻找这个平衡点,制定法治和伦理准则,引导技术更加普惠,而非为一己之私,任道重远啊!

  • ✇游钓四方的博客
  • 晒晒年中时的骑行游钓四方的博客
    年前回家了,怀念年中在上海骑车日子 以下素材来自 2023年5月14日 开赛当天,比赛时闷头骑没时间拍,遗憾 年中时iphone坏了换了手机,导致今年六月份 两天半的时间从 河南周口 骑行到 上海 记录没有了…是我目前用时最短,距离最长骑行的记录。还有好多在上海留下的足迹照片都没了,下次证据一定奉上!
     

晒晒年中时的骑行

2024年2月2日 05:24

年前回家了,怀念年中在上海骑车日子

骑行 TCR PRO 在闵行区华漕路附近路亚

骑行 TCR PRO 在黄浦区苏州河喝咖啡

骑行 XTC 800 在邹市明拳击馆附近夜骑

从闵行区虹桥青杉路出发,目的地:余姚市全民健身中心签名报道!

宁波·余姚第六届环四明山比赛·第17届马自骑资格认证赛 公路车·全程租·A0753 张宏海

PAS骑行服

比赛前物品准备·军火展示

比赛前一周训练·在四明山入口处

比赛前一周训练·在四明山不知名山腰

比赛前一周训练·望着后方已经爬过的群山 我站在山腰极度兴奋,大概剩100公里结束训练

以下素材来自 2023年5月14日 开赛当天,比赛时闷头骑没时间拍,遗憾

5月14日 完赛后 主办奖台留念

奖状与奖牌

最终成绩224名

Garmin Edge 1040 码表成绩

年中时iphone坏了换了手机,导致今年六月份 两天半的时间从 河南周口 骑行到 上海 记录没有了…是我目前用时最短,距离最长骑行的记录。还有好多在上海留下的足迹照片都没了,下次证据一定奉上!

❌
❌