阅读视图

发现新文章,点击刷新页面。

新手跑步第五次:单人挑战不间断半马

“从跑步小白,到不间断半马需要多久?答案是六天,五次!”


Day 4:首次不间断 10 公里

2025年4月20日 · 跑步第四天。早上起床时心情很好,因为我发现大腿已经不疼了,想必是适应了十公里的运动量,我决定今天晚上下班后,开始挑战不间断十公里

Day 4 · 小区在放电影·智取威虎山

晚上刚下班,我直接回家换了运动装,出了门正好看到篮球场在放电影《智取威虎山》,我喜欢露天电影的氛围,有空一定要去看一场

Day 4 · 中原区·西流湖公园

从小区北门起跑,沿着郑上路一路向南,经过郑州市实验小学、第一中学等地标,最后在T字形路口到达西流湖公园北侧门,目前里程为4公里左右,这条路被我骑车压过不知道多少次了,可这次是跑步,带给我的感觉不一样

Day 4 · 西流湖公园·外围下坡

进了公园右转是上坡,向左则为两条路可以选择:

  1. 公园外围下坡,路面宽阔、路灯明亮
  2. 岸边小道,下坡路况一般

Day 4 · 西流湖公园·不知名桥亭

这个桥亭设计了很多座位,栏杆也不高,下面是贾鲁河的支流,还做了一个闸口的设计,水流从上面流下来,下面是人工池,形成一个小瀑布的效果,非常适合路亚、溪流钓,台钓佬就省省吧,只能大跑铅

Day 4 · 西流湖公园·不知名桥亭

Day 4 · 完成不间断 10 公里

分段成绩

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日 · 跑步第六天 · 第五次

今天我,昨天晚上忙着搬家没有跑步,处于内心的愧疚,我决定今天晚上把昨天的补回来,在跑之前我还不知道半马是什么意思

Day 6 · 半马·五公里记录

像往常一样,再次来到西流湖,里程来到了五公里,这段距离的心率有些高,心率区间在165-180,往后的数据都没有这个高

Day 6 · 半马·十公里记录

饮水没控制住,已经喝了550ml,当时非常兴奋,因为我即将跑返程了

Day 6 · 半马·十五公里记录

跑到这里时,体力消耗的差不多了,双腿感觉十分僵硬,停一秒钟感觉都会导致后面跑步下去

Day 6 · 半马·记录

到小区门口了,我感到非常兴奋,因为我即将完成我的第一次半马挑战,而且是不间断,除了中途买水,期间几乎没有停过

Day 6 · 半马·记录

Day 6 · 魔镜魔镜谁是最持久男人

跑完站在家里,小腿和大腿没有疼痛感,唯一不舒服的就是双腿的膝盖关节处,活动就会有些疼痛

半马·分段成绩

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

Day 6 · 标签

到家准备脱裤子时才发现,我中午买的第一条跑步用的紧身裤标签还没摘,现在已经被汗水浸湿烂掉了,让我有种破茧的感觉

记录我的前3次跑步:从陪跑到主动出发

“从讨厌到上瘾,原来跑步也能这样有趣”

我一直是个不爱运动的人,尤其讨厌跑步。打小起,我对跑步总是敬而远之

这次之所以开始跑步,完全是被阿坤和阿丽带动的

起初只是想着陪他们减肥,没想到,从第三天开始,我居然有点跑上瘾了

Day 1:人生第一次 5 公里(其实只跑了 3 公里)

第一次跑步是阿坤叫我的,他想减肥,我就陪他出来遛弯。他说目标是 5 公里,结果我们大半时间都在走路,实际上只跑了 3 公里

他有点胖,体力跟不上,但我直到活动结束都没有什么感觉

Day 1 · 没有什么感觉


跑步 Day 2:不间断 5 公里初体验

第二天我刚下晚班(19:00),我打电话问阿坤什么时候出发,他说八点半。我不想等太久,就先回家收拾一下便出门了

第一天穿板鞋和牛仔裤实在太难受,这次吸取了教训,只穿了短裤、速干背心和跑鞋

站在小区门口花两分钟热热身,把软件都打开便开始跑了

刚开始跑到 0.86 公里 时,心率就达到了 183,但呼吸还算平稳

跑到 2 公里时,心率稳定在 168–170,最终顺利完成不间断五公里,一点都不觉得累,只是非常口渴

跑完后在楼下买了瓶水,还给阿坤发了个微信。结果瓶盖还都没拧开,就下起了暴雨,就像是天上开了个花洒一样,很突然…

Day 2 · 不间断 5 公里,暴雨突降

Day 2 · 首次不间断 5 公里

阿坤因为下雨就没出门,我们在老地方随便吃了点东西聊聊天。准备回家时,我才发现自己腿已经快站不直了,大腿疼得厉害


跑步 Day 3:加码挑战,十公里!

早上起床,大腿肌肉酸痛,走路都不太舒服,走路都一瘸一拐的,就像当初刚学骑自行车一样,这种酸爽的痛感,反倒让我有点兴奋

出门碰头时,阿坤说想骑我的自行车,我说你骑吧,我跑步

相比昨天,今天的心率平稳多了,基本维持在 150–160。跑到 6.59 公里时,心率才到 181,那一刻我只觉得跑步,真的爽!掌握节奏之后,压根不想停下来!

Day 3 · 徐佳莹在奥体开演唱会

跑着跑着来到奥体,正好赶上徐佳莹的演唱会。场外摆摊的特别多,还有个露天KTV,这种我是第一回次见,他们的声音是真大,我在 2 公里外就听见了,没一会儿,三四个保安冲过来大喊:“里面在开演唱会呢!”结果一个大妈拿着话筒回了一句:“演唱会咋了,演唱会咋了!” 笑死我了

之后我们绕着奥体转了一圈,发现个室外健身区,有很多器械,比如健身单车,还支持联网进行在线竞赛,而且运动数据可同步app,最重要的是全部免费!

Day 3 · 阿坤骑着我的自行车

返程时演唱会刚结束,整个奥体路被堵得水泄不通,到处是人和出租车

Day 3 · 首次十公里

其实,今天的十公里多少有些违心,因为我实际只跑了 8.25 公里,剩下的两公里是骑车,阿坤说骑不动了,让我骑车,他跑着…

Strava Riding Api 上线

该脚本基于 Strava API v3 获取指定用户当年的所有骑行活动数据,并将其保存为JSON格式

功能特性

Strava Riding Api 只实现了 OAuth 2.0 授权流程的部分自动化,由于技术限制,目前无法实现完全自动化:

已实现部分

  • 半自动 OAuth 2.0 授权流程,轻松访问您的 Strava 数据
  • 自动获取任意年份的所有骑行记录
  • 获取每个活动的完整运动数据
  • 智能令牌管理:自动保存和刷新过期的访问令牌
  • 数据自动转换:公里、时间、速度单位等数据格式化
  • 内置多重容错机制,确保数据获取的可靠性

使用前设置

重要: 在使用此脚本前,请确保在Strava开发者平台上正确配置您的应用:

  1. 访问 Strava开发者设置
  2. 将以下URL添加到”授权回调域”:
    localhost
    

    注意:只需输入 localhost 而不是完整的 http://localhost:8000

  3. 保存设置

使用方法

  1. 安装依赖:
    yarn install
    
  2. 获取并处理授权码:
    yarn auth
    

    获取授权后,您会收到一个授权码。将其粘贴到命令行中。

  3. 获取骑行数据:
    yarn start
    
  4. 查看输出的JSON文件,文件名格式为:strava_data.json

解决认证问题

如果您遇到API相关错误,请尝试以下解决方案:

  1. 更新令牌
    yarn auth
    

    重新获取授权并更新令牌

  2. 检查API状态

    访问 Strava API状态 确认服务是否正常

常见问题解决

  1. “protocol mismatch”错误
    • 此问题已在最新版本中解决,使用了原生HTTPS模块发送请求
    • 确保在Strava开发者设置中添加了localhost作为授权回调域

  2. 无法获取活动数据
    • 确认您的账户中确实有骑行活动
    • 检查筛选条件是否正确(默认只获取”Ride”类型活动)

  3. 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 的审核,正式发布了!

Chrome 应用商店:产品已成功发布

功能特性

  • 智能填充:DOM 加载完后,自动读取表单插入数据。
  • 无缝集成:与主流博客平台和评论系统兼容。
  • 数据加密:通过 AES-GCM 加密和解密功能,保护用户数据安全。
  • 现代化界面:基于 Material-UI 和 React 提供用户友好的界面。

安装

  1. 打开 Chrome Web Store
  2. 搜索 EasyFill
  3. 点击 添加到浏览器 按钮完成安装。
  4. 安装完成后,浏览器工具栏会显示 EasyFill 图标。

更新日志

查看 更新日志 了解最新功能和修复。

问题反馈

如果你在使用过程中遇到问题,请在我的博客留言

支持作者

感谢您对我的支持,本人非程序员,忙里抽闲,为爱发电。

如果您觉得 EasyFill 对您有帮助,可以通过以下方式支持我继续创作:

许可证

本项目基于 Mozilla Public License Version 2.0

Github 仓库:https://github.com/achuanya/EasyFill

✨ EasyFill 只为向那些在浮躁时代,依然坚守独立博客精神的你们致敬!

产品被拒

晚上下了班打开电脑刚坐下就看到了一封 Google 邮件,首先看到了发件人 “Chrome Web Store”,当时就心想提交审核一个多星期了,终于看到一点音信了。点开后,还没等我高兴,便看到了:

Chrome 应用商店:"EasyFill"被拒通知

解决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,直到现在这个问题也没有解决,希望下个版本可以解决问题

产品谍照:

EasyFill·我的信息

注册 Chrome Web Store 开发者

年前曾尝试过 Chrome 扩展开发,《写一个Chrome表单自动化插件》,但是由于没有注册 Chrome Web Store 开发者,无法上传到 Chrome 应用商店。

注册 WildCard

Chrome 注册开发者需要五美元,由于我没有境外信用卡就一直卡在这,2022 年我在杭州办过一张中信的双币卡,年费很高,后来经济紧张时注销了,现在急着用外币还挺麻烦,折腾一圈,最终无脑选择了 WildCard,尽管网上对它负面评论铺天盖地。

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 开发者

Chrome Web Store 账单

注册账号就很容易了,Google 绑卡付钱就行。但是如果要销售发布就很麻烦:

个人交易者声明

  • 您需要提供一个手机号码以验证是您本人在操作
    • 您将通过手机接收代码
  • 用于证明是您本人的身份证件
  • 可接受的文件包括:
    • 驾照
    • 护照
    • 州身份证明
    • 绿卡
  • 您需要提供一份显示您的姓名和当前地址的文件
  • 可接受的文件包括:
    • 由政府签发的文件或带照片的身份证件
    • 公共事业缴费单或话费账单(日期在过去 60 天内)
    • 银行对账单(日期在过去 60 天内)
    • 租赁合同或抵押贷款合同

因为 Google 已退出中国市场,不支持交易。而我是美国 Visa 卡,面对这样的要求不容易做到。

日后再说吧,往后这段时间,我打算把博客评论表单自动填充插件重构一下,然后上架 Chrome 应用商店。

空腹骑行75公里

周六

最近郑州天气突然转冷,骑行频率也降了下来,周六正好赶上休息,实在是憋坏了!今天不管刮风下雨,必须出去骑一趟

原计划直接奔开封,结果路过龙湖就停了下来。好久没来了,上次来还是鹅毛大雪天,如今雪没了,只剩下鹅

倒挂白鹅

周六的公园人满为患,没法骑。我推着车沿湖边缓行,遥望着远处炸水的不知名鱼,不由自主的想蹲下摸摸湖水,真的很想钓鱼,自到郑州以来,我连最爱的路亚竿都没摸过

龙湖·北岸

此时正值中午,小孩在沙滩上牵着风筝奔跑,大人排队买小吃,顿时勾起了不少儿时回忆,我也好想光着脚奔跑在沙滩上…

龙湖·人工沙滩

在龙湖公园出来后,我关掉了导航线瞎跑,根本不认识路,不知道自己在哪,扫大街呗

STRAVA 74.6km  爬升313m  时间4h 19m

话说现在骑车很少拍照,不是不爱拍,而是懒得下车,即使趴到腰酸,感觉腰快要断了,也不想停下来

周日

拍这张照片时,已经快饿晕了,周六晚上吃得少,周日早上又空腹出门,体力消耗得厉害…

周日早晨睡到自然醒,一看表,我整个人都快立正了,居然八点半了。着急忙慌洗漱后,脱下内衣裤直接换上骑行服,背上包,拿了五块巧克力出发了。因为周一要上班,所以今天必须放纵一下,出发前大致算了算,来回返程再加上逛街的时间,早饭根本来不及吃…哎…

大约骑了25公里,在中石化买了瓶宝矿力水特。又骑行了二三十公里到了贾鲁河桥,饿的没劲,更别说爬坡了,挂上小盘,我慢悠悠到了桥中间,休息了几分钟,把五块巧克力补给全吃了

郑州·贾鲁河桥

就这样空腹骑到了开封郊区,此时的里程已经来到了 75.38公里,用时3小时23分钟

到达开封后,心里那股坚持的信念瞬间消失了,又渴又饿,高德帮我找了最近一家名为三不炒(开封总店)的小炒店,我把车子靠着门店随便一放,就去买葡萄糖了

就去买葡萄糖了

买完水出来发现要排队,人还不少,我是真的饿得快走不动了,但还是懒得换地方,抱着水坐在外面等了半小时。饿得快虚脱了,感觉此时此刻,就算把馒头挂我脖子上都能饿死

我前面排了八个人

排队加吃饭花了一个半小时,吃得太撑,骑上车都趴不下去,推着车穿过老巷子走到了湖边

对面就是清明上河园

御河桥洞下

御河桥洞下

正在乐钓的五星开封好市民

STRAVA 164.1km  爬升417m  时间8h 5m

回到家已经八点半了,这条郑开大道路线真心推荐,毕竟二刷了,虽然沿途风景平平,但对于郑州来说,已经是顶级骑行路线了,一个人骑行在郑开大道,握着下把位,不用担心刹车,不用担心前方有没有人,听着歌,摇着车,也不枉来郑州走一遭

利用 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 就可以直接引用这个文件来展示订阅,但是这个方案有诸多不足:

  1. 网络不稳定导致的抓取失败

    由于原先的重试机制不够完善,GitHub Actions 在国外,RSS 站点大多在国内,一旦连接超时就挂,一些 RSS 无法成功抓取

  2. 单线程串行,速度偏慢

    旧版本一次只能串行抓取 RSS,效率低,数量稍多就拉长整体执行时间,再加上外网到内地的延时,更显迟缓

  3. 日志不够完善

    出错时写到的日志文件只有大概的错误描述,无法区分是解析失败、头像链接失效还是RSS本身问题,排查不便

  4. 访问速度影响大

    这是主要的重构原因!在旧版本里,抓取后的 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   |
           +--------------------+
  1. 并发抓取 + 限流
    通过 Go 的 goroutine 并发抓取 RSS,同时用一个 channel 来限制最大并发数

  2. 指数退避重试
    每个 RSS 如果第一次抓取失败,则会间隔几秒后再次重试,且间隔呈指数级递增(1s -> 2s -> 4s),最多重试三次,极大提高成功率

  3. 灵活存储
    RSS_SOURCE: 可以决定从 COS 读取一个远程 txt 文件(里面存放 RSS 列表),或直接从 GitHub 的 data/rss.txt 读取
    SAVE_TARGET: 可以把抓取结果上传到 GitHub,或者传到腾讯云 COS

  4. 日志自动清理
    每次成功写入日志后,会检查 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. 对比旧版本的改进

  1. 容错率显著提升

    遇到网络抖动、超时等问题,能以10路并发限制式自动重试,很少出现直接拿不到数据

  2. 抓取速度更快

    以 10 路并发为例,对于数量多的 RSS,速度提升明显

  3. 日志分类更细

    分清哪条 RSS 是解析失败,哪条头像挂了,哪条本身有问题,后续维护比只给个403 Forbidden方便太多

  4. 支持 COS

    可将最终 data.json 放在 COS 上进行 CDN 加速;也能继续放在 GitHub,视自己需求而定

  5. 自动清理过期日志

    每次抓取后检查 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

  1. 准备一份 RSS 列表(TXT):

    格式:每行一个 URL
    如果 RSS_SOURCE = GITHUB,则可以放在项目中的 data/rss.txt
    如果 RSS_SOURCE = COS,就把它上传到某个 https://xxx.cos.ap-xxx.myqcloud.com/rss.txt

  2. 配置好环境变量:

    默认所有数据保存到 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
    


  3. 部署并运行

    只需 go run . 或在 GitHub Actions workflow_dispatch 触发 运行结束后,就会在 data 文件夹更新 data.json,日志则写进 GitHub logs/ 目录,并且自动清理旧日志

注:如果你依旧想完全托管在 COS 上,需要把 RSS_SOURCE 和 SAVE_TARGET 都写为 COS,然后使用 GitHub Actions 去调度

相关文档

骑行开封

我对于开封的印象,还停留在开封府尹·包拯。处于好奇和无处可去的想法,周六早上吃完饭,说走就走了

到达开封鼓楼

这里就到达开封了开封·鼓楼,郑开大道的路上很轻松,室外温度17°+,小风微微的吹着,不冷不热好不痛快

Strava记录

在郑开大道单飞的过程中偶遇骑友,王哥是开封本地的,骑行的路上跟我聊开封哪里好玩,哪里最具性价比,把我领进开封鼓楼后,又带我在景区逛了一圈带我认路,在此感谢大哥

与王哥的合照

早饭吃的比较仓促,真的很饿,在书店街附近买了些吃的

干饭

本来是想在开封呆一天,晚上去清明上河园玩,想到公司有事就提前回去了,怕耽误明天的行程

鼓楼合影

郑开大道

这次跨市骑行急了一些,时间太紧张了!再过几天休息,我想回一次家,骑行约200KM

Strava记录

Blog Function Update 2025 (2)

Update details

  • 移除红灯笼
  • 新增 sitemap.xmlsitemap.txt,自动生成,不再手动更新!

之前我一直使用 xml-sitemaps 手动生成sitemap.xml,但每当 URL 新增或变更都需要手动提交。实在麻烦!所以,今日用 Liquid 实现自动生成,一劳永逸

sitemap.xml 优化策略

  • 首页优先级最高 (1.0),其他页面次之 (0.8)
  • 新文章优先级高(30 天内 0.9,半年内 0.8,一年内 0.6),让新内容更容易被搜索引擎收录
  • 旧文章优先级降低(1 年以上 0.4,2 年以上 0.2),减少搜索引擎对老旧内容的爬取
  • 动态调整 changefreq,确保新内容频繁爬取,而老文章爬取频率降低
---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {% assign now = site.time | date: "%s" | plus: 0 %}
  
  {% for page in site.pages %}
    {% if page.url == "/" %}
    <!-- 首页优先级最高 -->
      {% assign page_priority = "1.0" %}
    {% else %}
      {% assign page_priority = "0.8" %}
    {% endif %}
    
    <url>
      <loc>{{ site.url }}{{ page.url | replace:'index.html','' }}</loc>
      <lastmod>{{ site.time | date_to_xmlschema }}</lastmod>
      <changefreq>weekly</changefreq>
      <priority>{{ page_priority }}</priority>
    </url>
  {% endfor %}
  
  <!-- 根据发布时间动态调整 priority 和 changefreq -->
  {% for post in site.posts %}
    {% assign post_time = post.date | date: "%s" | plus: 0 %}
    {% assign diff = now | minus: post_time %}
    {% assign days_old = diff | divided_by: 86400 %}
    
    {% if days_old < 30 %}
      {% assign priority = "0.9" %}
      {% assign changefreq = "daily" %}
    {% elsif days_old < 180 %}
      {% assign priority = "0.8" %}
      {% assign changefreq = "weekly" %}
    {% elsif days_old < 365 %}
      {% assign priority = "0.6" %}
      {% assign changefreq = "monthly" %}
    {% elsif days_old < 730 %}
      {% assign priority = "0.4" %}
      {% assign changefreq = "yearly" %}
    {% else %}
      {% assign priority = "0.2" %}
      {% assign changefreq = "never" %}
    {% endif %}
    
    <url>
      <loc>{{ site.url }}{{ post.url }}</loc>
      <lastmod>{{ post.date | date_to_xmlschema }}</lastmod>
      <changefreq>{{ changefreq }}</changefreq>
      <priority>{{ priority }}</priority>
    </url>
  {% endfor %}
</urlset>

sitemap.txt 兼容旧版爬虫

sitemap.txt 适用于不支持 XML 的搜索引擎(如某些旧版爬虫)

---
layout: null
permalink: /sitemap.txt
---
{% for page in site.pages %}
{{ site.url }}{{ page.url | replace:'index.html','' }}
{% endfor %}

{% for post in site.posts %}
{{ site.url }}{{ post.url }}
{% endfor %}

在 robots.txt 里声明 Sitemap

确保搜索引擎能找到 Sitemap,需要在 robots.txt 文件中声明 sitemap.xmlsitemap.txt

User-agent: *
Allow: /

User-agent: MJ12bot
Disallow: /
User-agent: AhrefsBot
Disallow: /
User-agent: SemrushBot
Disallow: /
User-agent: dotbot
Disallow: /

Sitemap: https://lhasa.icu/sitemap.xml
Sitemap: https://lhasa.icu/sitemap.txt

Blog Function Update 2025 (1)

由于郑州最近的雨夹雪天气,已经一周没有骑行了,实在憋得不行,给自己找点事做,今天中午下班时更新了一下博客

Update details

  • 修复了柱形图显示错位
  • 移除了骑行页面的活动天数
  • 新增了全年骑行总时长、全年骑行总公里数
  • 柱形图的宽度不再由骑行时长来计算,而是由骑行公里数来计算显示
  • 新增春节快乐红灯笼(移动端不支持)
  • 移除 node-sass 包,由 sass 代替

Fix Bugs:柱形图显示错位

当前的柱形图仅为有骑行数据的周生成柱形图,导致柱形图与日历中的周对齐错位,所以即某周没有骑行数据时,柱形图也要生成一根柱子

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

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

    // 创建所有周的时间范围
    const weeklyData = {};
    let currentWeekStart = new Date(startDate);
    currentWeekStart.setUTCHours(0, 0, 0, 0);

    // 按周计算未来 4 周的日期范围
    for (let i = 0; i < 4; i++) {
        const weekStart = new Date(currentWeekStart);
        const weekEnd = new Date(weekStart);
        // 一周结束日期为开始日期 +6 天
        weekEnd.setUTCDate(weekStart.getUTCDate() + 6);
        const weekKey = `${weekStart.toISOString().split('T')[0]} - ${weekEnd.toISOString().split('T')[0]}`;

        // 初始化每周骑行数据为 0
        weeklyData[weekKey] = 0;
        // 移动到下一周
        currentWeekStart.setUTCDate(currentWeekStart.getUTCDate() + 7);
    }

    // 累加每周的骑行距离
    processedActivities.forEach(activity => {
        const activityDate = new Date(activity.activity_time);
        // 活动所在周的开始日期
        const weekStart = getWeekStartDate(activityDate);
        const weekEnd = new Date(weekStart);
        weekEnd.setUTCDate(weekStart.getUTCDate() + 6);

        const weekKey = `${weekStart.toISOString().split('T')[0]} - ${weekEnd.toISOString().split('T')[0]}`;
        if (weeklyData[weekKey] !== undefined) {
            weeklyData[weekKey] += parseFloat(activity.riding_distance);
        }
    });

    // 获取最大骑行距离(用于柱形图比例)
    const maxDistance = Math.max(...Object.values(weeklyData), 0);

    // 创建并显示每周的柱形图
    Object.keys(weeklyData).forEach(week => {
        // 当前周的骑行距离
        const distance = weeklyData[week];
        const barContainer = document.createElement('div');
        barContainer.className = 'bar-container';

        const bar = document.createElement('div');
        bar.className = 'bar';

        // 计算柱形图的宽度
        const width = maxDistance > 0 ? (distance / maxDistance) * 190 : 0;
        bar.style.setProperty('--bar-width', `${width}px`);

        const distanceText = document.createElement('div');
        distanceText.className = 'cycling-kilometer';
        distanceText.innerText = '0 km';

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

        barContainer.style.position = 'relative';
        bar.appendChild(distanceText);
        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`;

        distanceText.style.opacity = '1';
        // 动态更新柱形图的数值
        animateText(distanceText, 0, distance, 1000, true);
        setupBarInteractions(bar, messageBox, clickMessageBox, distance);
    });
}

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

New:全年骑行总时长、全年骑行总公里数

// 显示总活动数和总公里数
function displayTotalActivities(activities) {
    // 全年骑行时长
    const ridingTimeThisYear = document.getElementById('totalCount');
    // 全年骑行公里数
    const milesRiddenThisYear = document.getElementById('milesRiddenThisYear');
    // 动态年标题《2025 骑行总时长》
    const totalTitleElement = document.getElementById('totalTitle');

    if (!ridingTimeThisYear || !milesRiddenThisYear || !totalTitleElement) return;

    const ridingTimeThisYearValue = ridingTimeThisYear.querySelector('#ridingTimeThisYearValue');
    const milesRiddenThisYearValue = milesRiddenThisYear.querySelector('#milesRiddenThisYearValue');

    const totalCountSpinner = ridingTimeThisYear.querySelector('.loading-spinner');
    const milesRiddenThisYearSpinner = milesRiddenThisYear.querySelector('.loading-spinner');

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

    const currentYear = new Date().getFullYear();
    totalTitleElement.textContent = `${currentYear} 骑行总时长`;

    // 筛选全年活动数据
    const filteredActivities = activities.filter(activity => {
        const activityYear = new Date(activity.activity_time).getFullYear();
        return activityYear === currentYear;
    });

    // 计算全年活动时间的总和(单位:小时)
    const totalMovingTime = filteredActivities.reduce((total, activity) => {
        return total + parseFloat(activity.moving_time) || 0;
    }, 0);

    // 计算全年总公里数
    const totalKilometers = calculateTotalKilometers(filteredActivities);

    // 动画效果
    animateCount(ridingTimeThisYearValue, totalMovingTime, 1000, 50, false);
    animateCount(milesRiddenThisYearValue, totalKilometers, 1000, 50, true);

    setTimeout(() => {
        console.log(totalKilometers.toFixed(2));
        ridingTimeThisYearValue.textContent = `${totalMovingTime.toFixed(2)} h`;
        milesRiddenThisYearValue.textContent = `${totalKilometers.toFixed(2)} km`;
        totalCountSpinner.classList.remove('active');
        milesRiddenThisYearSpinner.classList.remove('active');
    }, 1000);
}

// 加载数据并生成日历
(async function() {
    const today = getChinaTime();
    const startDate = getStartDate(today, 21);

    const activities = await loadActivityData();
    // 显示4周的日历
    generateCalendar(activities, startDate, 4);

    // 显示全年骑行时长和公里数
    displayTotalActivities(activities);
})();

New:春节快乐红灯笼

两年前在冲浪时下载的,已经是第二次用了:

// default.html
include lantern.html

// main.scss
@use 'lantern'

Fix Bugs:移除 node-sass 包

node-sass 是基于 LibSass 库构建的,而 LibSass 从 2019 年就停止了更新。所以,Sass 团队放弃了这个项目,重构了 sass(Dart 编写)

sass 相对 node-sass 的优点

  1. 原生支持 Dart

    sass 是由 Dart 编写,它不再依赖 C++ 编译器,安装和构建速度更快

  2. 不再依赖编译

    node-sass 需要本地编译,会遇到编译问题,尤其是 Windows 系统上。而 sass 是纯 JavaScript 实现,跨平台时不会有编译问题

{
    "devDependencies": {
        "sass": "^1.83.4",
    }
}

Show

测试

# 构建镜像
docker build -t lhasa-jekyll .

# 运行容器,将容器的 8888 Jekyll端口映射到宿主机的 8888 端口
docker run -it -p 8888:8888 lhasa-jekyll


从你提供的信息来看,Docker Desktop 需要你安装并启用 WSL2 发行版(例如 Ubuntu)才能进行文件共享和集成操作。如果没有安装 WSL2 发行版,Docker 就无法与 WSL2 进行集成,这会导致文件同步的问题。

解决步骤:
1. 安装 WSL2 发行版
你可以按照以下步骤来安装一个 WSL2 发行版:

启用 WSL 和虚拟化(如果尚未启用):

打开 PowerShell(以管理员身份运行),输入以下命令:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
然后重启电脑。
安装 WSL2:

打开 PowerShell(以管理员身份运行),输入以下命令来安装 WSL2:
wsl --set-default-version 2
安装一个 WSL2 发行版:



打开 Microsoft Store,搜索并安装你喜欢的 Linux 发行版(比如 Ubuntu)。
安装完后,打开应用并创建一个用户。
2. 配置 Docker Desktop 启用 WSL2 集成
在你安装并启用了 WSL2 发行版后,Docker Desktop 应该能够识别到它并允许你启用集成。

打开 Docker Desktop 设置:

点击 Settings。
在 Resources -> WSL Integration 中,应该能看到你已安装的 WSL2 发行版(例如 Ubuntu)。
启用集成:

勾选你希望 Docker 集成的发行版(例如 Ubuntu),然后点击 Apply。
重启 Docker:完成设置后,重启 Docker Desktop。

3. 验证文件同步
完成上述设置后,你可以通过以下方法检查文件同步是否正常:

使用 WSL2 中的挂载路径:在 WSL2 中打开终端,进入你的项目文件夹,验证是否能正常访问:
cd /mnt/c/Users/<YourUsername>/path/to/your/project
检查 Docker 容器是否能访问文件:通过以下命令进入 Docker 容器,检查文件是否已同步:
docker exec -it <container_id> bash
cd /app
ls
运行 Jekyll:在容器中使用 jekyll serve --watch 命令来查看文件是否实时更新。

4. 确保文件夹共享
如果 Docker 仍然无法正确同步文件,尝试以下操作:

确保文件夹具有正确的权限,以便 Docker 和 WSL2 可以访问。
如果使用的是项目目录的路径,如 C:/Users/...,确保路径没有拼写错误,并且 Docker 能够正确访问。
总结
安装并启用 WSL2 发行版(如 Ubuntu)。
在 Docker Desktop 中启用 WSL2 集成。
验证 WSL2 是否正确配置并且文件夹能被正确访问。

骑行 桃花峪、黄河文化公园

周末假期被骑行占有,那种满足是一切都比不过

刚吃完饭准备出发

公园内设有多条越野骑行路线,坡度和难度多样,我这光胎的公路车进去根本生存不了。总的来说是一个不错的地方,集挑战、风景与文化于一体,是体验黄河沿岸自然与文化魅力的不二之选

到达桃花峪-炎黄越野公园

这条路很窄,一次只能过一辆汽车,有上有下,而且路面陡峭,路非常烂很危险,目测坡度15°

桃花峪最陡最烂的坡路

去窗口交费取票,姓名都不需要留

到达黄河文化公园 检票口

黄河流域地势平坦,水资源丰富,土壤肥沃,非常适宜农业发展。古人利用黄河水系灌溉农田,使中原成为中国古代经济、人口最为集中的地区,为中华民族的形成和壮大提供了物质基础,所以把黄河称呼为母亲河、民族摇篮也不足为过

民族摇篮牌坊

炎黄二帝塑像

日晷是一种古老的计时器,利用太阳的影子来确定时间。它的起源可以追溯到公元前4000年的古埃及,后来广泛传播到世界各地。在中国,日晷被称为“圭表”,早在周朝时期便已使用,用于确定二十四节气和天文观测

日晷

遇到一位好大哥帮我拍的照片

鼎

广场的某个角落

终于到达了心心念念的黄河岸边,此处跨越黄河的建筑就是大名鼎鼎的京广铁路,它是一条纵贯中国南北的交通大动脉,途经黄河流域、长江流域和南岭山脉。旁边的郑太高铁则是中国“八纵八横”高速铁路网的重要主通道之一,大大缩短了晋东南、蒙西等地与中东部地区的时空距离

到达黄河岸边

湖水清澈见底,微风拂过时,水面波光粼粼,宛如点点繁星洒在湖面上,因此得名“星海湖”

星海湖 原相机没有P图 水质水太好了

黄河缆车门口 鸿运当头

这辆车是我穿着锁鞋背上山的,如果没有体验过锁鞋的朋友,可以想象一下穿高跟鞋走山路,不过这里的高跟得反过来 —— 脚掌部分垫高,脚跟贴地

五龙峰 其中一个山峰顶

五龙峰 黄河少年雕像

中午时在超市吃了一碗凉皮,和老板沟通一番后把车子暂存于此,爬楼梯的路上三步一回头

毛主席视察黄河纪念地下方的超市

强烈推荐“浮天阁”,登顶后俯瞰整个黄河流域,东西南北尽收眼底

浮天阁

倒泻银河事有无,掀天浊浪只须臾

人间更有风涛险,翻说黄河是畏途

东

西

南

北

东南

对于喜欢骑行、越野和民族文化的朋友来说,桃花峪和黄河文化公园很适合,两个地方紧挨着,要场地有场地,要难度有难度,完事后紧挨着黄河散散步,傍晚时登顶浮天阁看日落,想想都美哦

结束

骑行郑州三环

上次我打算一天骑完郑州的三四环线,预计160km,没想到下午摔的手梆硬,昨天六号上午一雪前耻了

郑州龙湖公园

路过龙湖公园被吸引了,多了10km+里程

公园放养的白天鹅

这里的天鹅挺不少,小百只了,小鸭子不计其数。岸边挤满了摄影发烧友,到处是大炮

人工沙滩

郑州龙湖公园

骑行郑州三环 结束

到这里,骑行三环算是告一段落了,它比四环线少40km,难度低了几个档,三环红路灯非常多,而骑行四环时刹车都很少碰。三环道路也比较好,不像四环压根就没有路,什么路都没有。只能走菜地、翻墙、最后走高速绿化带,我提着车爬上机场高架,是用手爬,不是骑车爬…最后为了安全从高架上下来,走下水道过去了…

骑行 金融岛

上午去了一次龙湖公园,但是中间的金融岛一直找不到地方上去,下午又去了一次,逛了 Specialized、colnago

骑行 中原福塔

沪上有明珠,中原有福塔

回家

下午回了家,借同事电脑把博客整了下,前俩月博客处于失联状态,证书都过期了,开往都把我踢了,现在OK了,只是骑行页面每天要手动每天上传一下,Strava接口暂时没有精力搞了

郑州骑行记录

骑行西流湖

骑行西流湖

骑行西流湖

骑行西流湖

探金水河

探金水河,溪流钓的天堂,台钓的地狱

骑行桃花峪

郑州的面积比我预计小不少 明天准备绕圈 ,骑行四环(97km)和三环(57km) 在地图上画两个正方形,好事成双

骑行桃花峪

骑行桃花峪

骑行桃花峪

骑行桃花峪

骑行桃花峪 结束

这里是:郑州 四环 非机动车道

14点饭后出发骑三环,没一会儿出事了,太遗憾了 总而言之,郑州的西北南四环路况一般,且由北四环入东四环后非常拉跨,你除了走机场高架外没有选择,当时为了安全避免走机场高架,过草地和下水道硬控了我大半个小时

骑行郑州四环

骑行郑州四环

骑行玩手机的下场

骑行郑州四环 结束

十月份看完的第一本书

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

微信读书 · 沧浪之水

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

一场说走就走的骑行

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

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

家门口的早餐店

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

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

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

椿树王庄的日出

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

太清宫外的香火小贩

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

到达 鹿邑·太清宫

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

太清宫·三清大殿

三清大殿后的广场

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

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

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

来奶茶店补个电

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

STRAVA记录

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

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

聊聊自行车

洋车子的起源

在河南老家,人们通常称自行车为“洋车子”。确实,是洋鬼子的杰作,早在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.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的图形界面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
    │
    ├─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
│      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背带裤和速白干背心就行了,如果不是为了注意个人形象,我直接背带裤光膀子。在上海生活的时候也特热,多次骑行都是背带裤光膀子,有两次路过外滩被交警抓了,说外滩都是游客,我个人形象影响市容。

到地方了,这骑行路段是挺不错的,我有七年没来这里了,今天来到这里发现大变样了,还可以租皮划艇,改天一定玩玩。在这里绕圈骑了十五公里,又跑去周口公园绕圈,要说这夏天油耗确实高,机动车顶不住,人也顶不住啊!我的水有点不够了,今天才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()
    ├── 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万?赶超月光博客!

腾讯云 访问分布

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 晚 测试补充

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

测试

喜提新车 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后最后一代顶级圈刹车,我在网上找了许久都找不到同款,这台车已经停产几年了,太稀有了,

上图

车到了

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

我装好了

寄回来之前,技师称重

骑行照

搞个公众号

改名记录:

  • 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

初一大吉,博客上上新

图片预览

`

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配置如下:

腾讯云COS跨域访问CORS设置

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

网站被拖垮了

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

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

取子集压缩后的效果

技术亦福亦是祸

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

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

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

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

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

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

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

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

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

晒晒年中时的骑行

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

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

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

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

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

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

PAS骑行服

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

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

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

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

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

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

奖状与奖牌

最终成绩224名

Garmin Edge 1040 码表成绩

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

论南北饮食差异

沪上为异客,巴月三成弦。朔风正摇落,行子已归旋。

在家真好!今起了个大早,六点半时天蒙蒙亮,我穿着毛绒睡衣骑电动车赶早怼了碗胡辣汤,五元/碗经济实惠,舀上一勺感受这辛辣酸甜,顿时想起沪漂这些年的心酸往事…

胡辣汤、肉包子、茶叶蛋、鸡蛋饼

回想上海20/碗清汤淡水,那是什么狗八胡辣汤,色香味你是一样都不沾。勾八商家,不要跟我说房租成本,原料物价高,我做原料采购这些年,江浙沪海鲜农产品行情不敢说是专家,多少有点自己的见解,那一两胡椒面值几个钱,一碗水放俩菜叶子净毛利有多少?这些投机倒把的无良商家一点诚信都没有,餐饮届都让你们这些老鼠屎给败坏了。

说多说少我对爱丁堡有些偏见,但并不妨碍我对于淮阳菜、粤菜的喜爱,民以食为天嘛!由于之前就职粤菜杭菜的原料采购经历,今我想聊聊南北的饮食差异。

南甜北咸,东辣西酸

南米北面,若从菜系角度来讲,民间起初有四大菜系:粤菜、淮扬菜、鲁菜、川菜。后来随着生活提高又衍生出了:浙菜、闽菜、湘菜、徽菜。就此入的流的就这八大菜系,此外的就不讲了。

在餐饮界,八大菜系是谁也不服谁。鲁师傅嘲讽粤菜淡不拉几没味,啥都剩着吃,俺鲁菜作为御膳才是八大菜系之首。 听到鲁菜叫嚣,淮扬菜师傅立马就坐不住了便大喊到:老子是国宴,一手白袍虾仁尼克松来了都要舔盘子。闻声,川师傅急忙插一嘴:你们淮阳菜太甜,像吃了蜜蜂屎,不如俺们的麻婆豆腐,那叫一个地道…你说一句我怼一句,一时间硝烟四起…本是同根生,口味差距为何如此之大?

南北气候差异

先从南北所处生态环境来讲,再映射原料。北方气候较为寒冷且干燥,北京为例,坐标蒙古高压的东南边缘,西北方向有广阔的沙漠和草原,所以北京全年降雨量少且集中在夏季。此外,北京的雾霾和沙尘暴也是常态化,春季最为明显。所以结论得出北方的土壤适合种植小麦、玉米、大豆等农作物,即面食,这些原料应对寒冷干燥的环境可以贮藏很长时间。

而南方则相反,小桥流水,细雨芊芊,最属江浙,我的最爱。这里属亚热带季风气候,全年降水充沛,集中在春夏季。即使在降水量低的冬季,受冷暖空气共同作用,也不会像北方寒冷干燥,隐约的记得在杭州采购部的时候,屋里的被子感觉都没有干过。

所以南方以湿润的气候独步天下,富饶的土壤和丰富的水资源在贫困的新手发育阶段显得尤为重要,我认为这也是南北方饮食差异的最大原因:经济。

差异的本质

结上来看,各菜系各有各的特色,鲁菜利用广阔的地理资源吃的有些粗犷,而南方的小资较为精细,要说原因,我看还是穷。从GDP来说,

2019年中国各省GDP总量及增速情况

要么说还是南方人富裕,全面小康喊了几十年的口号,得道的又有几个地方?毕竟地理位置和气候环境决定了起步的高度。图上排名靠前的省份大多是清一色的长江中下游地区,从这里咱们可以从地理位置以农耕的角度来看经济效应。

秦岭到淮河一线以南属亚热带地区,常年降水量大于800毫米,热量充足,降水充沛,得天独厚的条件能一年两熟或一年三熟。

秦岭到淮河一线以北常年旱地,有一年一熟或两年三熟就不错了。所以秦岭到淮河这条线的走向也是决定了南北饮食差异化的原因之一。

既然没钱,裤腰带勒紧过,川菜的辛辣起初也是经济的体现。如中国的旧社会,有势有钱有三吃:细,鲜,甜。就这三个条件在旧社会就不得了,穷人家别说吃甜,鲜,能有粮食就不错了,别管变质与否,所以难吃变质的食物加以调料掩盖其味道得以饱腹。

我奶奶是从五八年走过来的人,我小时候常挑食的时候我奶奶马上就会讲,放到五八年,你们这群人都得饿死,树皮都没得吃。也就是这些历史传承原因导致了现在的南北饮食差异,现在的日子不同以往了,除个别困难地区,大多数家庭还是能温饱的,口味也都传承了下来。

虽然现在的经济形势也很差,但比以前是好多了,人的嘴都吃叼了。我想南北饮食的差异化也会随着小康社会的推动逐渐减少。

置办了一套茶具

我挺喜欢喝茶的,在采购部的时候,凌晨起来上班都会泡壶一级龙井放在车上,都是供应商明着面送采购部让大家喝的,主任睁一只眼闭一只眼也不会说什么。

中午下午闲的时候,我常去江杨市场和江桥的国际餐具市场转悠,一是去询价看看新上市的原料了解一下当下行情,二是去供应商那坐坐,大伙聊聊行情吹吹牛逼,过程自然少不了盖碗茶。

话说对于这茶具我是熟悉又陌生,我的工作有一部分是采购餐厨具,但主要的茶具多由集团向龙泉瓷集中采购,还轮不到我插一嘴。前些天茶喝的时候忽然想到了茶具,抱着对传统文化的好奇心,我就置办了点便宜的德化窑白陶瓷。

茶盘、盖碗、公道杯、斗笠杯、茶夹、茶漏

我喜欢这样的陶瓷器,通透,一抹凝脂如玉的色,把玩着、抚摸着宛如女人的肌肤。

说到陶瓷器,我认为景德镇为上佳,德化和他差的不是一丁半点,从出身、烧制起就有所不同。

景德镇,始于唐,盛于宋,早在元世祖在位时就成立了御窑厂,上呈皇族,下卖显贵。由此以来景德镇的名号都成了陶瓷器的金字招牌,但凡沾上点关系,价格翻一翻。

而德化不同于老大哥的官窑背景,少些贵族气质,多些烟火气息,怎么说德化也是世界陶瓷之都啊!

其次,两者的工艺也有所不同,景德镇的高温烧制工艺讲究还原气氛,使其具有了独特搜刮钱财的光泽,最具代表性则为青白瓷釉,一线大师齐聚一堂,精益求精的工艺赋予了景德镇陶瓷独特的艺术品格。

相关,德化的机械化流水线式生产就不像老大哥那考究,低温烧制,一个字,快!主打一个从人民中来,到人民中去。

两者肉眼还是容易区分的,前者由于高岭土含铁元素较高,高温烧制后骚了点,脸颊上泛着隐约的青涩。小老弟则含钾多些,低温烧制属软性瓷,通透,特白,所以两者在颜色上可以很直观的区分出来。

正在工作的盖碗

正在工作的斗笠杯

打了这么多字,我手都把不住门了,望着盖碗中的铁观音,捏住这斗笠杯,一饮而尽!入口清香,回甘绵长,这看着锅里的,吃着碗里的,神仙来了也上头啊!

我回来了

一晃几年过去了,一切都物是人非,说起来有点哽咽…哎…

最重要的域名也在今年搞丢了,续费保护期也过了,曾经二三百的域名,现在要二万块…先拿github的用用,以后再说吧

一时间,脑子蒙蒙的,纵有千言万语却不知道说些什么。总之,接下来我会把丢掉的博客再捡起来,曾经的朋友你们还在吗?留言系统还没有弄好,可以联系Email

大年初一,徒步西湖

早上八点从滨康路地铁口出发,从候潮门出来就一直在走

徒步中

因为穿的是薄薄的板鞋,走路实属不舒服,走好久到了西湖,下台阶走了两步差点来了一个“新年第一摔”

西湖石板上的苔藓

我写博客的时候才发现照片颜色不对劲,我也不知道咋回事…话说西湖环境管理的挺不错的,湖水没有太多垃圾,岸边都是可以清澈见底的

西湖

西湖

在杭州我最喜欢的就是杭州的绿化,清晨或傍晚走在南山路散步,那感觉真是令人心旷神怡

西湖绿化

我站西湖岸边抽烟一扭头,发现岸边有一条大青! (看体型像鲤鱼) 目测身长70cm-10斤左右!大年初一的真是让人喜出望外,难道这就是传说中的鸿运当头吗?啊哈哈哈!看的我也是手痒痒真想抽几杆,看来西湖也是有大货啊

大青

大青

果然好事连连!我妈给我打电话说今天和我爸们临时调休了,带着我出去玩!对于我这个十几年的留守儿童来到爸妈工作的城市,和爸妈一起出去玩坐一起吃吃饭,真的对我来说是多么奢侈的事情。

我爸提议来河坊街玩

河坊街

我妈看到我喜欢吃的绿豆糕就给我买了一盒,挺好吃的,吃着面面的很细腻,有点绿豆甜那种味道,后来买了千层饼、长沙臭豆腐、糖雪球我都光顾着吃了并没有拍哈哈哈

绿豆冰糕

逛到大概一点钟我们去吃饭了,西湖春天南山店,我最喜欢吃的是糯米藕和乳鸽,乳鸽口感真的很脆,也没有很油腻

吃饭喽

吃完饭,我妈带我去买了两双鞋,我妈说让我把身上穿这双板鞋扔了,我这双回力的板鞋穿三年了,就是穿去户外真的不舒服,底子薄还硬,走的远会很累。但是想想这双鞋跟了我好久,陪我走过好多路真的不是很舍得扔….

回家了

爱上Go语言:常量与枚举

常量

常量,一经定义不可改变的量,当出现不需要被更改的数据时,应该使用常量进行储存,从语法角度看,使用常量可以保证数据,在整个运行期间内,不会被更改

常量的值仅仅支持,基础类型,字符串,字符,整型,浮点,布尔

package main

import (
	"fmt"
	"math"
)

func consts() {
	const (
        // 类型可以通过值推导出来,例如这个 filename 就是一个字符串
		filename = "abc.txt"
		a, b     = 3, 4
	)
	var c int
	// 常量没有定义类型的情况下,其数值可作为各种类型使用
	c = int(math.Sqrt(a*a + b*b))
	fmt.Println(filename, c)
}

func main() {
	consts()
}

枚举

其实Golang并没有enum,但是可以使用 const和iota 来模拟枚举

package main

import (
	"fmt"
)

func enums() {
	const (
		// iota 初始化后会自动递增
		c = iota
		_
		java
		php
		javascript
	)

	const (
		b = 1 << (10 * iota)
		kb
		mb
		gb
		tb
		pb
	)

	// 0 2 4 3
	fmt.Println(c, java, javascript, php)

	// 1 1024 1048576 1073741824 1099511627776 1125899906842624
	fmt.Println(b, kb, mb, gb, tb, pb)
}


func main() {
	enums()
}

爱上Go语言:变量定义与内建变量类型

变量的定义

package main

import "fmt"

func variableZeroValue() {
	// var 关键字表示定义变量,定义变量时,名在前,类型在后
	// int默认值为0,string为空字符串
	var a int
	var s string
	fmt.Printf("%d %q\n", a, s)
}

func variableInitialValue() {
	// Golang语法严格,定义的变量必须被使用,否则报错:declared but not used
	var a, b int = 3, 4
	var s string = "abc"
	fmt.Println(a, b, s)
}
func variableTtypeDeduction() {
	// 定义多个变量的同时,也可以给其定义类型
	var a, b, c, s = 3, 4, true, "def"
	fmt.Println(a, b, c, s)
}

func variableShorter() {
	// : 冒号也表示定义变量,注意定义变量时才用!!!
	a, b, c, s := 3, 4, true, "def"
	// 若变量重复定义则会报错:no new variables on left side of :=
	b = 5
	fmt.Println(a, b, c, s)
}

// 在函数内定义的变量,它作用域只在函数内,但函数外也可以定义变量
// 不管函数内还是函数外都可以使用 var() 集中定义变量
var (
	id   = 1
	user = "achuan"
)

// 需要注意的是,在函数外定义变量只可以用var关键字进行定义,不能使用冒号
// 否则报错:syntax error: non-declaration statement outside function body
//ges := 20

// 注意上面定义的id,user变量,它们的作用域不是"全局变量",而是包内部的变量
// Golang没有全局变量这个说法,只是包内部的

func main() {
	fmt.Println("Hi achuan")
	variableZeroValue()
	variableInitialValue()
	variableTtypeDeduction()
	variableShorter()
	fmt.Println(id, user)
}

内建变量类型

布尔与字符串

  • bool、string

bool再了解不过了吧,它的值只有两种:true、false,if和for语句的条件部分都是布尔的值,并且==<等比较操作也会产生bool值

string是一个不可改变的字节序列,字符串可以包含任意的数据

整数型

  • (u)int、(u)int8、(u)int16、(u)int32、(u)int64、uintptr

加(u)是无符号整数,不加(u)是有符号整数,有符号整数还分两类,一类规定长度的,如int8、int32、int64。还有一类是不规定长度的,不规定长度的是根据操作系统来,在32位操作系统里面它就是32位,在64位系统它就是64位

还要一个uintptr类型,这个ptr就是指针,这个指针的长度也是跟着操作系统来的

uintptr类型是无符号整型,用于存放一个指针,等过几天再详细学一下

字符型

  • byte、rune

Golang字符型有两种,一种是uint8,另一种就是rune

byte就是uint8的别名,即一个字节长度,常用来处理ASCII字符

rune同等于int32,代表一个UTF-8字符,即4个字节吗,常用于处理中文或其他复合字符

浮点型

  • float32、float64

Golang提供了两种浮点型:float32和float64,它们的算术规范是由IEEE754国际标准定义,现代CPU都实现来这个规范。

浮点数能够表示的极限范围可以在math包中获取,math.MaxFloat32表示float32的最大值,约3.4e38,math.MaxFloat64大约是1.8e308,两个类型最小的非负值大约是1.4e-45和4.9e-324

float32大约可以提供小数点后6位的精度,而float64可以提供小数点后15位的精度。

Golang也提供了两种大小的复数:complex64和complex128,分别由float32和float64组成。内置函数complex从指定的实部和虚部构建复数,内置函数realimag用来获取复数的实部和虚部

例如欧拉公式:

package main

import (
	"fmt"
	"math"
	"math/cmplx"
)

func euler() {
	fmt.Printf(cmplx.Exp(1i*math.Pi)+1)
}

func main() {
	euler()
}

强制类型转换

Golang是一种强类型语言,不同类型的数据不能赋值,不能在函数中传参,我个人是很喜欢这种。因很多错误在编译期间就被揪出来来,不像PHP等弱类型语言,很多错误只有运行时才能被发现,如下面这个例子:

用Golang求出直角三角形斜边值5

Alt text

package main

import (
	"fmt"
	"math"
)

func triangle() {
	var a, b int = 3, 4
	var c int
	// 参数类型不一致报错:cannot use a * a + b * b (type int) as type float64 in argument to math.Sqrt
	// c = int(math.Sqrt(a*a + b*b))
	// 因为Sqrt参数要求float64类型,Golang语法严格,必须强制性转换
	c = int(math.Sqrt(float64(a*a + b*b)))
	fmt.Println(c)
}

func main() {
	triangle()
}

// 5

Manjaro Linux 双显卡切换解决方案

1.卸载原有开源驱动

# 查看安装了哪些显卡驱动,全部删除
$ mhwd -li
$ sudo mhwd -r pci video-nvidia

2.安装NVIDIA闭源驱动

具体这个驱动版本可以根据显卡型号去NVIDIA官方查询,我的是GTX1060

$ sudo mhwd -i pci video-nvidia-450xx

3.安装依赖

# 查询Linux内核版本
$ uname -r
5.8.11-1-MANJARO

# inux58-headers这个内核头文件包名‘58’是内核版本缩写
$ sudo pacman -S linux58-headers acpi_call-dkms xorg-xrandr xf86-video-intel git

4.挂载acpi_call模块

$ sudo modprobe acpi_call

如果遇到modprobe: FATAL: Module acpi_call not found in directory报错,需要安装acpi_call

# 安装时注意选择相应的内核版本
$ sudo pacman -S acpi_call

5.清理文件

如果以下目录下有任何定义video/gpu.conf文件,请备份/删除。因为脚本会删除所有的文件。

/etc/X11/
/etc/X11/mhwd.d/
/etc/X11/xorg.conf.d/
/etc/modprobe.d/
/etc/modules-load.d/

6.安装切换脚本

$ git clone git@github.com:dglt1/optimus-switch-sddm.git
$ cd optimus-switch-sddm
$ chmod +x install.sh
$ sudo ./install.sh

7.切换命令

# 切换为Intel
$ sudo set-intel.sh
# 切换为NVIDIA
$ sudo set-nvidia.sh

# 切换后需要重启才能生效
$ reboot

相关文档

脚本英文文档

GUI切换

Manjaro Linux 自动禁用触摸板

安装

安装必要的函数库和驱动程序

$ sudo pacman -S libinput xf86-input-libinput xorg-xinput

编写Shell脚本

$ vim DisableTouchpad.sh

#!/bin/bash

declare -i ID
ID=`xinput list | grep -Eio '(touchpad|glidepoint)\s*id\=[0-9]{1,2}' | grep -Eo '[0-9]{1,2}'`
declare -i STATE
STATE=`xinput list-props $ID|grep 'Device Enabled'|awk '{print $4}'`
if [ $STATE -eq 1 ]
then
    xinput disable $ID
else
    xinput enable $ID
fi

赋予脚本读/写/执行权限

$ sudo chmod 0755 DisableTouchpad.sh

Systemd 自启动

$ cd /usr/lib/systemd/system
# 创建Systemd服务
$ sudo vim touchpad.service

[Unit]
Description=Touchpad control service

[Service]
Type=oneshot
ExecStart=/File/Self-starting/DisableTouchpad.sh

[Install]
WantedBy=multi-user.target

配置touchpad服务自启动

$ systemctl enable touchpad

Docker Compose快速构建LNMP笔记

目录结构

/
├── data                        数据库数据目录
│   ├── esdata                  ElasticSearch 数据目录
│   ├── mongo                   MongoDB 数据目录
│   ├── mysql                   MySQL8 数据目录
│   └── mysql5                  MySQL5 数据目录
├── services                    服务构建文件和配置文件目录
│   ├── elasticsearch           ElasticSearch 配置文件目录
│   ├── mysql                   MySQL8 配置文件目录
│   ├── mysql5                  MySQL5 配置文件目录
│   ├── nginx                   Nginx 配置文件目录
│   ├── php                     PHP5.6 - PHP7.3 配置目录
│   ├── php54                   PHP5.4 配置目录
│   └── redis                   Redis 配置目录
├── logs                        日志目录
├── docker-compose.sample.yml   Docker 服务配置示例文件
├── env.smaple                  环境配置示例文件
└── www                         PHP 代码目录

快速使用

如果当前用户不是root,为了避免频繁输入root密码,需要将当前用户加入docker组

# 创建Docker组  注:安装Docker时就自动创建了,如果没有则手动创建
$ sudo groupadd docker
# 当前用户加入Docker组
$ sudo gpasswd -a ${USER} docker
# 将当前用户的group切换到docker用户组
$ newgrp docker

Clone项目

$ gh repo clone achuanya/dnmp

拷贝文件

$ cd dnmp
# 复制环境变量文件
$ cp env,sample .env
# 复制docker-compose配置文件
$ cp docker-compose.sample.yml docker-compose.yml
# 创建并后台运行
$ docker-compose up -d

PHP与扩展

切换Nginx使用的PHP版本

1.比如,从php切换到php56,那就先在docker-compose.yml文件中查看PHP56有没有被注释掉,删掉注释后启动,再更改Nginx配置文件:

fastcgi_pass   php:9000;
更改为:
fastcgi_pass   php56:9000;

其中phpphp56docker-compose.yml文件中容器的NAME名称

2.让其配置生效还需再重新加载Nginx配置文件

$ docker exec -it nginx nginx -s reload

这里有两个Nginx,第一个是容器NAME名称,第二个是容器中的Nginx程序

在宿主机安装PHP扩展

1.如果要安装更多PHP扩展,在根目录找到.env环境配置文件,如以下PHP扩展配置

# 安装扩展应当使用英文逗号隔开
PHP56_EXTENSIONS=pdo_mysql,mysqli,mbstring,gd,curl,opcache,redis

2.保存完成后,重新构建镜像

$ docker-compose build php

在Docker中安装扩展

$ docker exec -it php /bin/sh
# 安装redis扩展
$ install-php-extensions redis

支持安装扩展列表

此扩展来自Michele Locati,请前往查看最新支持的PHP扩展

  • https://github.com/mlocati/docker-php-extension-installer

在宿主机中使用命令行

PHP CLI

1.参考根目录bash.alias.sample示例文件,将PHP CLI函数拷贝到/etc/profile系统环境变量文件

# 刷新系统环境变量
$ source /etc/profile

2.在宿主机中执行PHP命令了

 ~ [06:24:00]
achuan$ php -v
PHP 7.4.7 (cli) (built: Jun 11 2020 19:07:15) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.7, Copyright (c), by Zend Technologies
    
 ~ [06:24:04]
achuan$ php56 -v
PHP 5.6.40 (cli) (built: Jan 31 2019 01:30:45) 
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies

Composer

1.首先确定Composer缓存目录,Composer配置文件在根目录中的data/composer

2.参考根目录bash.alias.sample示例文件,将PHP CLI函数拷贝到/etc/profile系统环境变量文件

# 刷新系统环境变量
$ source /etc/profile

3.之后就可以在宿主机使用Composer命令了

$ cd /work/dnmp/www
$ composer -V
Composer version 1.10.13 2020-09-09 11:46:34

4.第一次使用Composer后data/composer目录下会生成config.json全局配置文件,可指定镜像,例如中国全量镜像:

{
    "config": {},
    "repositories": {
        "packagist": {
            "type": "composer",
            "url": "https://packagist.phpcomposer.com"
        }
    }
}

或使用命令修改Composer的全局配置文件

$ composer config -g repo.packagist composer https://packagist.phpcomposer.com

管理命令

容器的创建、启动与构建

$ docker-compose
	up # 创建并且启动所有容器
	up -d # 创建并且后台运行所有容器
	up nginx php mysql # 创建并且启动多个容器
	
	start # 启动容器
	stop # 停止容器
	restart # 重启容器
	build # 构建容器
	
	rm # 停止并且删除容器
	down # 停止并且删除容器、网络、图像与挂载卷

快捷命令

1.参考根目录bash.alias.sample示例文件,将Composer函数拷贝到/etc/profile系统环境变量文件

# 刷新系统环境变量
$ source /etc/profile

2.例如,进入php容器

$ dphp

(转)一个关于if else容易迷惑的问题

本文转自Laruence


这个本来是之前在微博上有个同学说他经常用来面试别人,大概是说,对于如下代码,你觉得会输出啥:

$a = true;if ($a) {  echo "true";} else label: {  echo "false";}

当时觉得有点偏,没想写,今天中午又有人问我,我想那就介绍下这个原因吧.

首先,上面的代码输出truefalse, 如果你知道原因,那就不用继续往下看了,如果不知道,那么:

这块让人比较迷惑的原因可能是因为,我们会很直观的认为:

label : {  statement;}

应该是一个整体, 就好比类似:

if ($a) {} else switch($a) {}

或者:

if ($a) {} else do {} while (!$a);

因为在PHP的语法设计中,if else本质上是:

if_stmt: if_stmt_without_else T_ELSE statement

也就是说,else后面可以接一切statement,如果条件不成立,执行流就跳到else后面的statement,而while, switch都可以归约为statement。

但label这块稍微有点特别(可以说是一个设计违反直觉的”缺陷”吧), 在zend_language_parser.y中:

statement:  ...  | T_DO statement T_WHILE '(' expr ')' ';' {...}  | T_SWITCH '(' expr ')' switch_case_list {...}  | T_STRING : { $$ = zend_ast_create(ZEND_AST_LABEL, $1); }

大家可以看到, do while, switch 都会联合他们的body归约为statement(语句),但标签(label)有点不同,”label :”本身会规约为一条statement, 这就导致了这个看起来比较迷惑的问题的出现,他本质上就变成了:

$a = true;if ($a) { echo "true";} else { label: ;  //单独的一条语句}echo "false";

最后多说一句,我忘了之前在那看到的,说是这个世界上本无elseif,有的只不过是else (if statement),本质上其实就跟这个意思是一样的。 就是,else后面可以接语句(statement)。

善用这个结合switch, for, do while等,有的时候可以让我们的代码更精简。 比如,我们要遍历处理一个数组,当数组的长度为零的时候,要做点其他事,那很多人可能会这么写:

if (count($array)) {  for ($i = 0; $i < count($array); $i++) {  }} else {  //数组为空的逻辑}

但你也可以写成:

if (count($array) == 0) {   //数组为空的逻辑} else for ($i = 0; $i < count($array); $i++) {}

至于这俩中写法孰好孰坏, 那就是萝卜白菜了。

最后,大家如果在实际中遇到类似让大家觉得迷惑的问题,可以留言,也许以后也可以单独成文。

使用开源项目免费申请JetBrains开源许可证

JetBrains公司旗下产品都太棒了,我是深陷其中不可自拔,离了这个软件我写啥都不舒服,最近更新一下PHPSTORM之后发现网上买的教育认证老是失效,最终我在网上发现可以用开源项目免费申请到JetBrains全家桶且为Ultimate版本!免费订阅有效期为一年,许可证到期前不久,JetBrains会有电子邮件进行续订提醒!不过申请还是有要求的

  • 你必须是项目的发起人或是活跃的 commiter
  • 你的项目需要积极开发3个月以上
  • 定期发布版本
  • 符合开源的定义,不能包含有关商业性质的内容

如何申请

JetBrains开源许可证申请

填写表单就行了,提交申请后我两天后就收到了JetBrains官方的电子邮件。

成功

今天我去钓鱼了

今早晨9点多醒了,透过窗户看看天气不错,看看手机了解今最低气温23度,最高气温31度,很适合钓鱼鲢鱼。

我正在穿衣服顺丰小哥就来电话了,前几天竿稍被我不小心折断了,虽然换了竿稍绳但是还是感觉不舒服。
在狗东宝飞龙旗舰店花50多补了件竿稍,竿稍拿到换好确定没有问题后 饭都没吃我提着包就走了。

到了地方后大概9点多了,开始干活了 装配钓椅、打窝又支伞…….
大概20分钟东西准备好了。调漂、搓耳抛出了今的第一杆,因为我调的饵料物化较快,所以第一杆很快结束。
收杆后搓耳抛出了第二杆,抛杆后直接放在炮台上,感觉刚打窝应该还没啥鱼,有点热脱掉外套。就特么在这时候,漂上下急跳,我急忙扔下外套直接猛提杆!
中鱼了,劲还不小!溜大概半分钟漏个身 看得出来不是正口,锚的,直觉告诉我大概有3斤多,特么爽啊,上鱼真快今的第一炮,在第二杆打响!
就在我高兴的时候,它脱钩了…..我特么…..艾。不过掉到中午的时候我也习惯了,今一早上我锚鱼 脱钩不下于10次……

大概六七杆左右吊到今第一条鱼———花鲢。大概有一斤多左右。杆子不是很硬37调,抄网都没用稍微使劲鱼直接飞上来。
比较遗憾的是上午脱钩了很多鱼,上午大概上了三四条全正口。看我发窝却上不了鱼,旁边老哥告诉我,我这新关东2号钩无倒刺,如果不是正口,溜鱼的话很容易跑。 我大概坚持两杆锚到一个不过又脱钩…..无奈之下换回前几天用的伊势尼10号钩。不想用倒刺钩原因是我老挂着东西,不好去掉。

到了下午两三点的时候,我左手搓耳搓的手疼,频繁提杆有点累了,正好饵料快用完了,我借旁边老哥拉丝粉又搞了一些饵料。
不一会儿有人来了,问我钓多少,我说大概7条,父女俩说想买,我听口音感觉他不是本地人。他们父女俩我们这旅游游玩,说想买我的鱼拿回去养,养大了再吃。

七条鲢鱼

我当时挺蒙蔽的….第一回遇见这事,没了解过行情价格,想想早上买了饵料和鱼钩花了20多,给个饵料钱30吧。
父女俩说三四十都行,我说三十就行,我这没有袋子,父女俩回车拿了个水桶箱,我把鱼放进去后。
这时候男的犹豫一下,说鱼不是很大,吃了感觉有点残忍…..当时周围一几个人都特么蒙了…..说在家养着还是等会儿放生了。
后来还是要了,微信给我转了30抱着水桶箱走了,旁边几个老哥说咋不多要点,人家外地来的有钱不缺钱,觉得咱龙湖里鱼好,要100都给….

过了会儿,累了躺椅子,没一会我看见直接黑漂了,连忙起身提杆,鱼线直接拉直,鱼毛都没见,它一发力2.5主线加漂直接秒切…..
上午我还说想尝尝爆杆啥感觉,没想到切线了…..旁边老哥都说鱼不小,也有可能说线伤了,因为上午提杆过猛挂树上好几次。

切剩下的线

翻翻渔具包,找不到5.4米的线了,一堆4米的,除了钩子和主线,太空豆还没买。
旁边老哥好心拿他线给我做一套线组。3号主线配1.5子线,钩子伊势尼10号。说线不太好,不过拉个四五斤没问题,在这再谢谢我老哥

老哥在帮我做线组

钓着钓着快6点了,哥几个下午鱼口不太好掉了,钓一天了,大伙聊垂钓、聊新闻还有八卦三四个人说说笑笑,几个人互相认识一下,加个微信创个群,挺开心的。
我大概又钓了三条鲢鱼,老哥几个家离得远准备前走了,太少了不想吃,给他们都不要,我把鱼放湖里了,收拾一下也准备走了。

临走拍张照晒一下哈哈哈。

我的装备

弦歌台垂钓区

龙湖

回到家后洗洗澡,把回来时配的线组再截一下,4号主线配2号子线,老哥说窝子弹簧物化不错,我配上试了试,钩子伊势尼10号
我感觉4号主线和10号钩有点大材小用,毕竟我钓鱼大多在1-5斤左右。不过想想线细了被切了呢,唉,大鱼可遇不可求啊!

新线组

今天钓的鱼还是挺多的,哈哈哈,10条,大概十三四斤还多的样子,很是开心!
锚十几个的如果都上来20斤起步了。唉,我不太会钓,加油把!

JetBrains官方推中文简体包了!

今天我在Github TranslatorX这个IDE汉化项目的Issues下看到有个JetBrains员工说关于中文包的事:

Hi, I’m a JetBrains employee, and we’re working on initial support for the Chinese language pack. If you’re interested in, feel free to contact me: ignatov@jetbrains.com

Cheers, Sergey.

然后前不久1-17号官方中文插件在2020.1的EAP版本已经推出了,相见恨晚啊!

然而朋友说中文IDE没有灵魂,这这这…

消息

2020-05-08更新

在Plugins里面搜索Chinese,安装后重启即可。

ThinkPHP6.0下载安装与配置

  • ThinkPHP 6.0 要求 PHP >= 7.1.0
  • 6.0版本开始,必须通过Composer方式安装和更新,无法使用Git下载安装。

安装Composer

$ curl -sS https://getcomposer.org/installer | php

# 全局调用
$ sudo mv composer.phar /usr/local/bin/composer

 /usr/src [09:22:13]
achuan$ composer
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.10.5 2020-04-10 11:44:22

# 使用阿里云镜像
$ composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

安装

# 稳定版
$ composer create-project topthink/think tp6

# 开发版
$ composer create-project topthink/think=6.0.x-dev tp6

# 更新已安装的旧版本核心框架
$ composer update topthink/framework

配置

# apache配置
$ sudo vim /etc/httpd/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
    ServerName tp6.io
    DocumentRoot /home/achuan/language/php/tp6/public
    <Directory  "/home/achuan/language/php/tp6/">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

$ sudo vim /etc/hosts
127.0.0.1 tp6.io

# 不进行apache配置也可以测试运行 默认端口为:8000
$ php think run -p 1081

# 环境变量定义 可以更名为.env进行修改生效,该文件默认开启调试模式。
$ mv example.env .env

# 对外访问
$ sudo chmod -R 777 public

# 安装多应用模式扩展 think-multi-app
$ composer require topthink/think-multi-app
# 创建一个名为 index 的应用,返回Successed则成功
$ php think build index
# 多应用模式部署后,必须要删除controller目录,因为系统根据该目录作为判断是否为单应用
$ rm -rf controller

目录结构

单应用模式

www  WEB部署目录(或者子目录)
├─app           应用目录
│  ├─controller      控制器目录
│  ├─model           模型目录
│  ├─ ...            更多类库目录
│  │
│  ├─common.php         公共函数文件
│  └─event.php          事件定义文件
│
├─config                配置目录
│  ├─app.php            应用配置
│  ├─cache.php          缓存配置
│  ├─console.php        控制台配置
│  ├─cookie.php         Cookie配置
│  ├─database.php       数据库配置
│  ├─filesystem.php     文件磁盘配置
│  ├─lang.php           多语言配置
│  ├─log.php            日志配置
│  ├─middleware.php     中间件配置
│  ├─route.php          URL和路由配置
│  ├─session.php        Session配置
│  ├─trace.php          Trace配置
│  └─view.php           视图配置
│
├─view            视图目录
├─route                 路由定义目录
│  ├─route.php          路由定义文件
│  └─ ...   
│
├─public                WEB目录(对外访问目录)
│  ├─index.php          入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于apache的重写
│
├─extend                扩展类库目录
├─runtime               应用的运行时目录(可写,可定制)
├─vendor                Composer类库目录
├─.example.env          环境变量示例文件
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件

默认应用文件

├─app           应用目录
│  │
│  ├─BaseController.php    默认基础控制器类
│  ├─ExceptionHandle.php   应用异常定义文件
│  ├─common.php            全局公共函数文件
│  ├─middleware.php        全局中间件定义文件
│  ├─provider.php          服务提供定义文件
│  ├─Request.php           应用请求对象
│  └─event.php             全局事件定义文件

相关文档:

Manjaro KDE 调教日记

更换源

# 更新数据源
$ sudo pacman -Sy
# 选清华源 mirrors.tuna.tsinghua.edu.cn
$ sudo pacman-mirrors -i -c China -m rank
$ sudo pacman -Syu

# 添加Arch源
$ sudo vi /etc/pacman.conf
# 把下面这几行写进去
[archlinuxcn]
SigLevel = Optional TrustedOnly
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch
$ sudo pacman -Syy && sudo pacman -S archlinuxcn-keyring

基础设置

$ sudo pacman -S vim git rpm yay unzip snapd you-get annie
$ sudo systemctl enable --now snapd.socket

# Git代理,需配合Qv2ray使用
$ git config --global https.proxy https://127.0.0.1:8888

# 主目录改为英文
$ sudo pacman -S xdg-user-dirs-gtk
$ export LANG=en_US &&  xdg-user-dirs-gtk-update

# 将时区设置为中国上海
$ timedatectl set-timezone Asia/Shanghai

# 细长的等宽字体
$ yay -S ttf-iosevka

export http_proxy="socks5://127.0.0.1:1080"
export ALL_PROXY=socks5://127.0.0.1:1080



# 禁用封锁
$ sudo vim /etc/security/faillock.conf
deny = 0

# 虚拟终端字体问题
$ sudo pacman -S terminus-font
$ sudo vim /etc/vconsole.conf
FONT=ter-132n

自动挂载NTFS硬盘

# 查看磁盘分区的UUID
$ sudo blkid -o list
# 5016CF88CCD20C21 就是我的UUID,同时要记录一下device和fs_type等会要用
device                   fs_type    label       mount point                  UUID
-------------------run----------------------------------------------------------------------------------------------
/dev/sdb1                ntfs       File        /run/media/achuan/File          5016CF88CCD20C21

# /dev/sdb1挂载点
$ mkdir ~/File
# 打开fastab文件,看到以下文件内容
$ sudo vim /etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a device; this may
# be used with UUID= as a more robust way to name devices that works even if
# disks are added and removed. See fstab(5).
#
# <file system>             <mount point>  <type>  <options>  <dump>  <pass>
UUID=8a9b74b7-c33a-413b-b654-80f3a16b5e12 /home          ext4    defaults,noatime,discard 0 2
UUID=b23b3470-26c1-4b39-9358-43278c73763e /              ext4    defaults,noatime,discard 0 1
UUID=ad103e33-78b7-4b33-8632-03c3fe6364fc /boot          ext4    defaults,noatime,discard 0 2
UUID=b0d5e88d-136a-4fa6-b164-5d70e5073b5d /opt           ext4    defaults,noatime,discard 0 2
tmpfs                                     /tmp           tmpfs   defaults,noatime,mode=1777 0 0

# 从这个文件内容可以看出文件有6列
 - 第一列file system选项是UUID
 - 第二列mount point选项是挂载点
 - 第三列type选项是所要挂载设备的文件系统或者文件系统类型
 - 第四列options选项是挂载选项,常见参数如下
 
|  配置选项    | 选项说明                                            
|-------------|-----------------------------------------------------
| async/sync  | 设置是否为同步方式运行,默认为async
| auto/noauto | 当下载mount -a命令时,此系统是否被主动挂载,默认为auto
| rw/ro       | 是否以只读或读写模式挂载
| exec/noexec | 限制此文件系统内是否能够进行“执行”操作
| user/nouser | 是否允许用户使用mount命令挂载
| suid/nosuid | 是否允许SUID的存在
| userquota   | 启动文件系统支持磁盘配额模式
| grpquota    | 启动文件系统对群组磁盘配额模式的支持
| defaults    | 同时具有rw、suid、suid、dev、exec、auto、nouser、async等默认参数的设置
 deepin-wine-wechat
 - 第五列dump选项是文件系统备份选项。0备份,1备份
 - 第六列pass选项是磁盘检查设置,其值是一个顺序,0不检查,1检查(根目录永远都为1)其它分区从2开始,数字越小越先检查,如果有两个分区的数字相同,同时检查

# 这是挂载/dev/sdb1的挂载配置,插入一行保存退出
UUID=5016CF88CCD20C21                     /home/achuan/File ntfs   defaults 0 0

# 如果/etc/fstab配置不对,会导致系统无法启动!一定要检查一下是否能正确挂载!如果改挂了,找个U盘改回来就行了。
$ sudo mount -a

常用软件

Vim配置

# Lightline
$ git clone https://github.com/itchyny/lightline.vim ~/.vim/pack/plugins/start/lightline

# 更换 PaperColor_dark.vim
$ mv -f ~/.vim/pack/plugins/start/lightline/autoload/lightline/colorscheme/PaperColor_dark.vim ~/.vim/pack/plugins/start/lightline/plugin/lightline.vim

# 编辑全局配置并写入以下配置 # 用户个人配置 ~/.vimrc
$ sudo vim /etc/vimrc

" 语法高亮
syntax on
" 底部状态显示
set showmode
" 使用UTF-8编码
set encoding=utf-8  
" 启用256色
set t_Co=256
" 开启文件类型检查,并且载入与该类型对应的缩进规则
filetype indent on
" 按回车后,下一行缩进自动同上
set autoindent
" 按TAB,Vim显示的空格数量
set tabstop=4
" 在文本上按下>>(增加一级缩进)、<<(取消一级缩进)或者==(取消全部缩进)时,每一级的字符数
set shiftwidth=4
" 由于 TAB 键在不同的编辑器缩进不一致,该设置自动将 TAB 转为空格
set expandtab
" TAB转为多少个空格
set softtabstop=4
" 显示行号
set number
" 光标所在当前行高亮
set cursorline
" 设置行宽,即一行显示多少个字符
set textwidth=80
" 自动折行,即太长的行分成几行显示
set wrap
" 只有遇到指定的符号(比如空格、连词号和其他标点符号),才发生折行。也就是说,不会在单词内部折行
set linebreak
" 是否显示状态栏。0 表示不显示,1 表示只在多窗口时显示,2 表示显示
set laststatus=2
" 在状态栏显示光标的当前位置(位于哪一行哪一列)
set  ruler
" 搜索时,高亮显示匹配结果
set hlsearch
" 输入搜索模式时,每输入一个字符,就自动跳到第一个匹配的结果
set incsearch
" 搜索时忽略大小写
set ignorecase
" 如果有一个大写字母,则切换到大小写敏感查找
set smartcase 
" 保存撤销历史
set undofile
" 出错时,发出视觉提示
set visualbell
" 保存Vim历史操作次数
set history=1000
" 打开文件监视。如果在编辑过程中文件发生外部改变(比如被别的编辑器编辑了),就会发出提示
set autoread
" 如果行尾有多余的空格(包括 Tab 键),该配置将让这些空格显示成可见的小方块
set listchars=tab:»■,trail:■
set list
" 命令模式下,底部操作指令按下 Tab 键自动补全。第一次按下 Tab,会显示所有匹配的操作指令的清单;第二次按下 Tab,会依次选择各个指令
set wildmenu
set wildmode=longest:list,full

Oh-My-Zsh

$ sudo pacman -S zsh
$ sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
# 切换到zsh
$ chsh -s /bin/zsh
# 安装3den主题
$ sudo vim ~/.zshrc
ZSH_THEME="3den"

小狼毫输入法

# 搜狗装了十几回,它那个兼容性真让我抓狂,还是小狼毫香。 
$ sudo  pacman -S ibus ibus-rime
$ yay -S ibus-qt

配置

# 默认是繁体,需要可以改为简体中文
$ vim ~/.config/ibus/rime/luna_pinyin.custom.yaml
# luna_pinyin.custom.yaml
patch:
  switches:                   # 注意缩进
    - name: ascii_mode
      reset: 0                # reset 0 的作用是当从其他输入法切换到本输入法重设为指定状态
      states: [ 中文, 西文 ]   # 选择输入方案后通常需要立即输入中文,故重设 ascii_mode = 0
    - name: full_shape
      states: [ 半角, 全角 ]   # 而全/半角则可沿用之前方案的用法。
    - name: simplification
      reset: 1                # 增加这一行:默认启用「繁→簡」转换。
      states: [ 漢字, 汉字 ]

# 编辑系统环境变量并写入以下配置
$ sudo vim /etc/profile
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus
ibus-daemon -d -x

搜狗输入法

2020-2-11 Update:现在已经有fcitx5包了,体验确实提升不少,ibus没有搜狗香了哈哈哈

$ sudo pacman -Sy fcitx fcitx-configtool 
$ yay -Sy fcitx-sogoupinyin

# 编辑系统环境变量并写入以下配置
$ sudo vim /etc/profile
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"

微信 TIM 完美解决方案

# QQ
$ yay -S deepin-wine-qq
# 自动切换前确保 deepin-wine5 包的支持,完成后就可以启动使用了
$ /opt/apps/com.qq.im.deepin/files/run.sh -d
# 高分辨率屏幕支持
$ env WINEPREFIX="$HOME/.deepinwine/Deepin-QQ" deepin-wine5 winecfg
# 默认使用文泉驿微米黑(wqy-microhei)字体,可以使用其他字体替代,直接将字体文件或字体链接文件放置到字体文件夹就会生效,不会影响系统字体
$ cd ~/.deepinwine/Deepin-QQ/drive_c/windows/Fonts

# 新版TIM
$ yay -S com.qq.tim.spark
# 微信
$ yay -S com.qq.weixin.deepin
# 注意:如果是 N 卡用户,可能需要用安装 lib32-nvidia-libgl 才能使用
# 中文方块乱码:WINE_CMD="LC_ALL=zh_CN.UTF-8 deepin-wine5"
# KDE/Plasma桌面 需要安装 xsettingsd,然后设置到 /usr/bin/xsettingsd 自启动
$ ln -s /usr/bin/xsettingsd ~/.config/plasma-workspace/env/xsettingsd

# KDE字体设置 DPI 120
$ env WINEPREFIX=$HOME/.deepinwine/Deepin-WeChat deepin-wine5 winecfg

# 添加字体到Fonts目录
$ cp MicrosoftYaheiConfig.ttf ~/.deepinwine/Deepin-WeChat/drive_c/windows/Fonts

# 修改系统注册表且修改以下两行
$ vim ~/.deepinwine/Deepin-WeChat/system.reg
"MS Shell Dlg"="MicrosoftYahei"
"MS Shell Dlg 2"="MicrosoftYahei"

# 注册字体并添加一下代码
$ vim MicrosoftYaheiConfig.reg
REGEDIT4
[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink]
"Lucida Sans Unicode"="MicrosoftYahei.ttc"
"Microsoft Sans Serif"="MicrosoftYahei.ttc"
"MS Sans Serif"="MicrosoftYahei.ttc"
"Tahoma"="MicrosoftYahei.ttc"
"Tahoma Bold"="MicrosoftYahei.ttc"
"MicrosoftYahei"="MicrosoftYahei.ttc"
"Arial"="MicrosoftYahei.ttc"
"Arial Black"="MicrosoftYahei.ttc"

# 注册
$ WINEPREFIX=~/.deepinwine/Deepin-WeChat deepin-wine5 regedit MicrosoftYaheiConfig.reg

开发

# 谷歌浏览器
$ sudo pacman -S google-chrome
# 火狐浏览器并汉化
$ sudo pacman -S firefox firefox-i18n-zh-cn
# 在浏览器的地址栏输入
about:config
# 搜索
intl.locale.requested
# 将其值修改为
zh_CN
# 360安全浏览器
$ yay -S browser360
# Opera 自带梯子的浏览器
$ sudo pacman -Sy opera
# 博主感觉最好的Markdown编辑器
$ sudo pacman -S typora
# phpstorm
$ yay -S phpstorm
# GoLand
$ sudo pacman -S goland
# CLion
$ sudo pacman -S clion clion-cmake make clion-lldb
# idea
$ sudo pacman -S intellij-idea-ultimate-edition
# Visual Studio Code
$ sudo pacman -S visual-studio-code-bin
# Postman
$ sudo pacman -S postman-bin
# Mycli 具有自动完成和语法突出显示功能的MySQL / MariaDB / Percona客户端
$ sudo pacman -S mycli
# 具有自动完成功能和语法突出显示功能的Redis客户端
$ yay -S iredis
# 开源图形化的Redis客户端管理软件
$ sudo snap install redis-desktop-manager
# Java JDK
$ sudo pacman -S jdk8-openjdk java-14-openjdk
$ archlinux-java status
$ sudo archlinux-java set java-14-openjdk
# Navicat Premium 150.0.10
# 链接: https://pan.baidu.com/s/1ihWcDY2Vs9igWuDfKh5giA  密码: nnft
$ chmod +x navicat15-premium-cs.AppImage
$ ./navicat15-premium-cs.AppImage
# 后来发现的,对不起Navicat我投入了DataGrip的怀抱
$ yay -S datagrip

# Jekyll
$ sudo pacman -S ruby
# 缺包了就装 bundle add
$ sudo vim /etc/profile
# 把ruby写入到系统环境变量
export PATH="$PATH:/home/achuan/.gem/ruby/3.0.0/bin/"
$ source /etc/profile
$ sudo gem update
$ sudo gem install jekyll bundle bundler

办公

# Thunderbird (邮件收发和RSS订阅,KDE预装)
$ sudo pacman -S thunderbird thunderbird-i18n-zh-cn
# XMind思维导图 (需要JAVA8+)
$ yay -S xmind
# KDE下最好用的PDF阅读器
$ sudo pacman -S okular
# WPS\字体\中文语言包
$ sudo pacman -S wps-office ttf-wps-fonts wps-office-mui-zh-cn
# 如果WPS不能输入中文
$ sudo vim /usr/bin/wps
# 写入以下配置
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus
ibus-daemon -d -x

娱乐

# Spotify
$ sudo pacman -S spotify
# 网易云音乐
$ sudo pacman -S netease-cloud-music
# Telegram
$ sudo pacman -S telegram-desktop
# Asciinema 在云端记录并分享你的终端会话
$ sudo pacman -S asciinema
# 开源的游戏平台(可以打美服LOL)
$ sudo pacman -S lutris

工具

# 查看内核版本
$ uname -r
# 安装时对应注意Linux内核版本
$ sudo pacman -S virtualbox virtualbox-guest-dkms
# vboxusers扩展包
$ yay -S virtualbox-ext-oracle virtualbox-guest-iso
# 添加当前用户到virtualbox用户组
$ sudo gpasswd -a $USER vboxusers
# 激活内核模块
$ sudo modprobe vboxdrv
# 坚果云
$ sudo pacman -S nutstore
# 百度网盘
$ yay -S baidunetdisk
# 新一代网络工具包
$ sudo pacman -S iproute2
# Snap Store
$ sudo snap install snap-store
# 程序启动器
$ sudo pacman -S albert
# 桌面面板
$ sudo pacman -S latte-dock
# 迅雷
$ yay -S deepin-wine-thunderspeed
# Teamviewer
$ sudo pacman -S teamviewer
# 如果无法打开或不能联网执行
$ sudo teamviewer --daemon enable
# 支持快捷键下拉的终端模拟器 (KDE预装默认F12唤醒)
$ sudo pacman -S yakuake
# 深度取色器
$ sudo pacman -S deepin-picker
# 深度录屏
$ sudo pacman -S deepin-screen-recorder
# 截图工具
$ sudo pacman -S flameshot-git
# Motrix (支持下载 HTTP、FTP、BT、磁力链、百度网盘等资源)
$ sudo pacman -S motrix
$ git clone git@github.com:sbwtw/deepin-repair-tools.git
# 全平台多线程下载管理器,恢复断/死下载、安排和转换下载、内置视频转换器、支持各大流行浏览器插件
$ yay -S xdman
# 腾讯播放器
$ yay -S debtap
# 升级 debtap
$ sudo debtap -u
$ wget https://dldir1.qq.com/qqtv/linux/Tenvideo_universal_1.0.10_amd64.deb
# 将deb包转换成pkg包
$ sudo debtap Tenvideo_universal_1.0.10_amd64.deb
# 安装pkg包
$ sudo pacman -U sogoupinyin-2.3.1.0112-1-x86_64.pkg.tar.xz

# xDroid 安卓模拟器
https://www.linzhuotech.com/index.php/home/index/xdroid.html
$ tar xvf xDroidInstall-x86_64-v3.0007.tar.gz
$ cd xDroidInstall-x86_64/
$ sh install.sh

# SwitchHosts(hosts管理)
https://github.com/oldj/SwitchHosts/releases
$ chmod +x SwitchHosts._linux_x86_64_3.5.4.5517.AppImage
$ ./SwitchHosts._linux_x86_64_3.5.4.5517.AppImage

开发环境

Apache

$ sudo pacman -S apache
# 设置Apache开机启动服务
$ sudo systemctl enable httpd
# 启动apache服务
$ sudo systemctl start httpd

配置参数

$ cd /etc/httpd/conf
# 备份源文件
$ sudo cp httpd.conf httpd.conf.backup
$ sudo vim httpd.conf 
# 开启重写
LoadModule rewrite_module modules/mod_rewrite.so

</IfModule>
    ServerAdmin achuan@achuan.io
    ServerName io:80
<Directory />
DocumentRoot "/home/achuan/www"
<Directory />
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>
<Directory "/home/achuan/www">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

$ sudo systemctl restart httpd
# 报错
[Sat Apr 25 00:36:21.725913 2020] [core:error] [pid 106186:tid 140238307444480] (13)Permission denied: [client 127.0.0.1:53420] AH00035: access to / denied (filesystem path '/home/achuan/www') because search permissions are missing on a component of the path

# 很常见的权限问题
$ sudo chmod +x /home/achuan

<IfModule dir_module>
    DirectoryIndex index.php index.html
</IfModule>

$ sudo systemctl restart httpd
# 让apache支持php
Include conf/extra/php7_module.conf
LoadModule php7_module modules/libphp7.so
Include conf/extra/httpd-vhosts.conf

$ sudo systemctl restart httpd
# 重启后直接无法启动了,查看httpd状态看看
$ sudo systemctl startus httpd
[pid 113670:tid 140226396240832] Apache is running a threaded MPM, but your PHP Module is not compiled to be threadsafe.  You need to recompile PHP.

# Arch Wiki上说Apache 2.4.7不支持非线程安全版PHP。php-apache中包含的libphp7.so不支持mod_mpm_event,仅支持mod_mpm_prefork。需要在/etc/httpd/conf/httpd.conf中注释掉。
$ sudo vim httpd.conf
# 取消以下行的注释:
# LoadModule mpm_event_module modules/mod_mpm_event.so
# 取消以下行的注释:
LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
$ sudo systemctl restart httpd

# 配置hosts
$ sudo vim /etc/hosts
# Virtual hosts
127.0.0.1  io
127.0.0.1  phpmyadmin.io
127.0.0.1  acphp.io
127.0.0.1  tp.io

# 配置虚拟主机
$ sudo vim extra/httpd-vhosts.conf
# Virtual Hosts
<VirtualHost _default_:80>
    DocumentRoot "/home/achuan/www/"
    ServerName io
</VirtualHost>

# phpMyAdmin.io
<VirtualHost *:80>
    ServerName phpmyadmin.io
    DocumentRoot /home/achuan/Carry/phpMyAdmin/
    <Directory  "/home/achuan/Carry/phpMyAdmin/">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

# tp.io
<VirtualHost *:80>
    ServerName tp.io
    DocumentRoot /home/achuan/language/php/tp/public
    <Directory  "/home/achuan/language/php/tp/">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

$ sudo systemctl restart httpd

PHP

$ sudo pacman -S php php-apache
$ php -v
PHP 7.4.5 (cli) (built: Apr 15 2020 17:14:40) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

安装Redis并编译扩展

$ sudo pacman -S redis
# 设置Redis开机启动服务
$ sudo systemctl enable redis
$ sudo systemctl start redis

# 编译Redis扩展
$ git clone https://github.com/phpredis/phpredis.git
$ cd phpredis
# 下载下来后默认这develop分支,需要手动切换到主分支
$ git checkout master
$ /usr/bin/phpize
$ sudo ./configure --with-php-config=/usr/bin/php-config
$ sudo make && sudo make install

配置参数

$ cd /etc/php
# 备份源文件
$ sudo cp php.ini php.ini.backup
$ sudo vim php.ini

error_reporting = E_ALL
display_errors = On
short_open_tag = On
display_startup_errors = On
memory_limit = 128M
post_max_size = 32M
date.timezone = Asia/Shanghai

extension=curl
extension=ftp
extension=imap
extension=mysqli
extension=pdo_mysql
extension=sockets
extension=zip
extension=redis

Composer

$ curl -sS https://getcomposer.org/installer | php
# 全局调用
$ sudo mv composer.phar /usr/local/bin/composer
# 使用阿里云镜像
$ composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

MySQL

$ sudo pacman -S mysql
# 初始化
$ sudo mysqld --initialize --user=mysql --basedir=/usr --datadir=/var/lib/mysql
# 我遇到了问题,初始化没有提供密码,不管它直接改。
$ sudo vim /etc/mysql/my.cnf
在[mysqld]中写入
skip-grant-tables
$ sudo systemctl restart mysqld
$ mysql -uroot -p
# 设置MySQL开机启动服务
$ sudo systemctl enable mysqld
# 改密码
# 在这我遇到个问题,如果不先刷新权限,SQL语句就会报错
ERROR 1290 (HY000): The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)

mysql> ALTER user 'root'@'localhost' IDENTIFIED BY 'achuan.io';
Query OK, 0 rows affected (0.02 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)

> QUIT
Bye

GoLand

安装

$ sudo pacman -S go

配置

# 配置环境变量
$ sudo vim /etc/profile
export GOPATH=$HOME/Important/go
$ source /etc/profile

Qv2ray 科学上网

  • 搬瓦工方案库存监控页面
  • 我的VPS配置:CN2 1核 1GB 20GB 1TB 1Gbps 洛杉矶 $49.99
  • 要求:Ubuntu 16+ / Debian 8+ / CentOS 7+ 系统
  • 推荐使用 Debian 9 系统,脚本会自动启用 BBR 优化

v2ray安装

$ yum install curl -y
$ bash <(curl -s -L https://git.io/v2ray.sh)

快速管理

# 查看 V2Ray 配置信息
$ v2ray info
# 修改 V2Ray 配置
$ v2ray config
# 生成 V2Ray 配置文件链接
$ v2ray link
# 生成 V2Ray 配置信息链接
$ v2ray infolink
# 生成 V2Ray 配置二维码链接
$ v2ray qr
# 修改 Shadowsocks 配置
$ v2ray ss
# 查看 Shadowsocks 配置信息
$ v2ray ssinfo
# 生成 Shadowsocks 配置二维码链接
$ v2ray ssqr
# 查看 V2Ray 运行状态
$ v2ray status
# 启动 V2Ray
$ v2ray start
# 停止 V2Ray
$ v2ray stop
# 重启 V2Ray
$ v2ray restart 
# 查看 V2Ray 运行日志
$ v2ray log
# 更新 V2Ray
$ v2ray update
# 更新 V2Ray 管理脚本
$ v2ray update.sh
# 卸载 V2Ray
$ v2ray uninstall

配置文件路径

# V2Ray 配置文件路径
/etc/v2ray/config.json
# Caddy 配置文件路径
/etc/caddy/Caddyfile
# 脚本配置文件路径
/etc/v2ray/233blog_v2ray_backup.conf

v2ray客户端

推荐俩v2ray客户端

  • Qv2ray

    三大平台GUI客户端,使用C++17/Qt5、支持订阅、扫描二维码、自定义路由编辑

  • v2rayL

    linux GUI客户端,支持订阅、vemss、ss等协议,自动更新订阅、透明代理

# AppImage版本
$ wget https://github.com/Qv2ray/Qv2ray/releases/download/v2.5.0/Qv2ray.v2.5.0.linux-x64.AppImage

# 因为政策原因Qv2ray并不自带v2ray核心
$ sudo pacman -S qv2ray v2ray

# 代理扩展
SwitchyOmega
# SwitchyOmega配置
https://raw.githubusercontent.com/wiki/FelisCatus/SwitchyOmega/GFWList.bak

# GoFW
# 境外网站加速器,Github一个项目,利用BootCDN加载境外网站某些静态资源
https://github.com/xmcp/GoFW

Pacman

更新

# 全面更新
$ pacman -Syyu
# 更新所有包
$ pacman -Syu
# 更新包数据源
$ pacman -Sy
# 更新已安装的包
$ pacman -Su

搜索安装

# 安装
$ pacman -S
# 搜索含关键字的包
$ pacman -Ss
# 同步包后再执行安装
$ pacman -Sy
# 安装本地包 (扩展名:pkg.tar.gz)
$ pacman -U
# 搜索已安装的包
$ pacman -Qs
# 升级全部包
$ pacman -Syu
# 只下载,不安装
$ pacman -Sw

显示删除

# 删除包,不会删除其依赖
$ pacman -R
# 删除包,及其所有没有被其它包使用的依赖
$ pacman -Rs
# 删除一个包,包括所有依赖
$ pacman -Rsc
# 清理未安装的包 (包文件目录:/var/cache/pacman/pkg/)
$ pacman -Sc
# 清理所有缓存文件
$ pacman -Scc
# 显示包信息
$ pacman -Si
# 查询本地包的详情信息
$ pacman -Qi
# 列出所有不再作为依赖的包
$ pacman -Qdt
# 列出所有明确安装而且不被其他包依赖的包
$ pacman -Qet

Snap

# 查看版本信息
$ snap --version
# 找出所有snap应用
$ snap find
# 安装应用
$ snap install
# 重启应用
$ snap restart
# 升级应用
$ snap refresh
# 查看安装的应用
$ snap list
# 卸载应用
$ snap remove

you-get

  • Github:https://github.com/soimort/you-get

  • 命令行程序,提供便利的方式来下载网络上的媒体信息

# 下载视频
$ you-get -i
# 使用 --http-proxy/-x为you-get设置HTTP代理:
$ you-get -x 127.0.0.1:8888
# 加载okie,目前支持两种cookie格式:Mozilla cookies.sqlite 和 Netscape cookies.txt.
$ you-get -c
# 获得页面所有可下载URL列表,支持JSON格式
$ you-get -u

annie

  • Github:https://github.com/iawia002/annie
  • 这是个国产的命令行下载器,用GO构建,目前支持的网站有
# 下载,如果URL包含特殊字符,需要用引号引起来
$ annie [URL]
# 显示视频质量等信息
$ annie -i https://www.bilibili.com/video/BV1FV411d7u7
 Site:      哔哩哔哩 bilibili.com
 Title:     bilibili献给新一代的演讲《后浪》 P1 bilibili献给新一代的演讲《后浪》
 Type:      video
 Streams:   # All available quality
     [80]  -------------------
     Quality:         高清 1080P
     Size:            65.55 MiB (68738121 Bytes)
     # download with: annie -f 80 ...
# 下载1080P列表所有视频,若只下载第一个去掉 -p
$ annie -f 80 -p https://www.bilibili.com/video/BV1FV411d7u7

最后再来一张图啊哈哈哈~

我的桌面

后续报错

JetBrains DataGrip的JavaFx报错

  • tried to use preview panel provider (javafx webview), but it is unavailable. reverting to default.

打开Markdown文件就会报错,为在Medium上找到的一个解决方案,重装一下PHPSTORM JRE

$ yay -S phpstorm-jre

Increasing the amount of inotify watchers

  • /home/achuan/.gem/ruby/2.7.0/gems/rb-inotify-0.10.1/lib/rb-inotify/watcher.rb:74:in `initialize’: No space left on device - Failed to watch “/home/achuan/github/achuanya.github.io/.jekyll-cache/Jekyll/Cache/Jekyll–Converters–Markdown/e6”: The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource. (Errno::ENOSPC)

使用$ jekyll server提示被限额了,增加限额永久化:

$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
$ sudo sysctl -p

(转)谈 Linux,Windows 和 Mac

本文转自当然我在扯淡


这段时间受到很多人的来信。他们看了我很早以前写的推崇 Linux 的文章,想知道如何“抛弃 Windows,学习 Linux”。天知道他们在哪里找到那么老的文章,真是好事不出门…… 我觉得我有责任消除我以前的文章对人的误导,洗清我这个“Linux 狂热分子”的恶名。我觉得我已经写过一些澄清的文章了,可是怎么还是有人来信问 Linux 的问题。也许因为感觉到“舆论压力”,我把文章都删了。

简言之,我想对那些觉得 Linux 永远也学不会的“菜鸟”们说:

  1. 1. Linux 和 Unix 里面包含了一些非常糟糕的设计。不要被 Unix 的教条主义者吓倒。学不会有些东西很多时候不是你的错,而是 Linux 的错,是“Unix 思想” 的错。不要浪费时间去学习太多工具的用法,钻研稀奇古怪的命令行。那些貌似难的,复杂的东西,特别要小心分析。

  2. 2. Windows 避免了 Unix,Linux 和 Mac OS X 的很多问题。微软是值得尊敬的公司,是真正在乎程序开发工具的公司。我收回曾经对微软的鄙视态度。请菜鸟们吸收 Windows 设计里面好的东西。另外 Visual Studio 是非常好的工具,会带来编程效率的大幅度提升。请不要歧视 IDE。要正视 Emacs,VIM 等文本编辑器的局限性。当然,这些正面评价不等于说你应该为微软工作。就像我喜欢 iPhone,但是却不一定想给 Apple 工作一样。

  3. 3. 学习操作系统最好的办法是学会(真正的)程序设计思想,而不是去“学习”各种古怪的工具。所有操作系统,数据库,Internet,以至于 WEB 的设计思想(和缺陷),几乎都能用程序语言的思想简单的解释。

先说说我现在对 Linux 和相关工具(比如 TeX)的看法吧。我每天上班都用 Linux,可是回家才不想用它呢。上班的时候,我基本上只是尽我所能的改善它,让它不要给我惹麻烦。Unix 有许许多多的设计错误,却被当成了教条,传给了一代又一代的程序员,恶性循环。Unix 的 shell,命令,配置方式,图形界面,都是相当糟糕的。每一个新版本的 Ubuntu 都会在图形界面的设计上出现新的错误,让你感觉历史怎么会倒退。其实这只是表面现象。Linux 所用的图形界面(X Window)在本质上几乎是没救的。我不想在这里细说 Unix 的缺点,在它出现的早期,已经有人写了一本书,名叫 Unix Hater’s Handbook,里面专门有一章叫做 The X-Windows Disaster。它分析后指出,X Window 貌似高明的 client-server 设计,其实并不像说的那么好。

这本书汇集了 Unix 出现的年代,很多人对它的咒骂。有趣的是,这本书有一个“反序言”,是 Unix 的创造者之一 Dennis Ritchie 写的。我曾经以为这些骂 Unix 的人都是一些菜鸟。他们肯定是智商太低,或者被 Windows 洗脑了,不能理解 Unix 的高明设计才在那里骂街。现在理解了程序语言的设计原理之后,才发现他们说的那些话里面居然大部分是实话!其实他们里面有些人在当年就是世界顶尖的编程高手,自己写过操作系统和编译器,功底不亚于 Unix 的创造者。在当年他们就已经使用过设计更加合理的系统,比如 Multics,Lisp Machine 等。

可惜的是,在现在的操作系统书籍里面,Multics 往往只是被用来衬托 Unix 的“简单”和伟大。Unix 的书籍喜欢在第一章讲述这样的历史:“Multics 由于设计过于复杂,试图包罗万象,而且价格昂贵,最后失败了。” 可是 Multics 失败了吗?Multics,Oberon,IBM System/38, Lisp Machine,…… 在几十年前就拥有了 Linux 现在都还没有的好东西。Unix 里面的东西,什么虚拟内存,文件系统,…… 基本上都是从 Multics 学来的。Multics 的机器,一直到 2000 年都还在运行。Unix 不但“窜改”了历史教科书,而且似乎永远不吸取教训,到现在还没有实现那些早期系统早就有的好东西。Unix 的设计几乎完全没有一致性和原则。各种工具程序功能重复,冗余,没法有效地交换数据。可是最后 Unix 靠着自己的“廉价”,“宗教”和“哲学”,战胜了别的系统在设计上的先进,统治了程序员的世界。

如果你想知道这些“失败的”操作系统里面有哪些我们现在都还没有的先进技术,可以参考这篇文章:Oberon - The Overlooked Jewel。它介绍的是 Niklaus Wirth(也就是 Pascal 语言的设计者)的 Oberon 操作系统。

胜者为王,可是 Unix 其实是一个暴君,它不允许你批评它的错误。它利用其它程序员的舆论压力,让每一个系统设计上的错误,都被说成是用户自己的失误。你不敢说一个工具设计有毛病,因为如果别人听到了,就会以为你自己不够聪明,说你“人笨怪刀钝”。这就像是“皇帝的新装”里的人们,明明知道皇帝没穿衣服,还要说“这衣服这漂亮”!总而言之,“对用户友好”这个概念,在 Unix 的世界里是被歧视,被曲解的。Unix 的狂热分子很多都带有一种变态的“精英主义”。他们以用难用的工具为豪,鄙视那些使用“对用户友好”的工具的人。

我曾经强烈的推崇 FVWM,TeX 等工具,可是现在擦亮眼睛看来,它们给用户的界面,其实也是非常糟糕的设计,跟 Unix 一脉相承。他们把程序设计的许多没必要的细节和自己的设计失误,无情的暴露给用户。让用户感觉有那么多东西要记,仿佛永远也没法掌握它。实话说吧,当年我把 TeXbook 看了两遍,做完了所有的习题(包括最难的“double bend”习题)。几个月之后,几乎全部忘记干净。为什么呢?因为 TeX 的语言是非常糟糕的设计,它没有遵循程序语言设计的基本原则。

这里有一个鲜为人知的小故事。TeX 之所以有一个“扩展语言”,是 Scheme 的发明者 Guy Steele 的建议。那年夏天,Steele 在 Stanford 实习。他听说 Knuth 在设计一个排版系统,就强烈建议他使用一种扩展语言。后来 Knuth 采纳了他的建议。不幸的是 Steele 几个月后就离开了,没能帮助 Knuth 完成语言的设计。Knuth 老爹显然有我所说的那种“精英主义”,他咋总是设计一些难用的东西,写一些难懂的书?

一个好的工具,应该只有少数几条需要记忆的规则,就像象棋一样。而这些源于 Unix 的工具却像是“魔鬼棋”或者“三国杀”,有太多的,无聊的,人造的规则。有些人鄙视图形界面,鄙视 IDE,鄙视含有垃圾回收的语言(比如 Java),鄙视一切“容易”的东西。他们却不知道,把自己沉浸在别人设计的繁复的规则中,是始终无法成为大师的。就像一个人,他有能力学会各种“魔鬼棋”的规则,却始终无法达到象棋大师的高度。所以,容易的东西不一定是坏的,而困难的东西也不一定是好的。学习计算机(或者任何其它工具),应该“只选对的,不选难的”。记忆一堆的命令,乌七八糟的工具用法,最后脑子里什么也不会留下。学习“原理性”的东西,才是永远不会过时的。

Windows 技术设计上的很多细节,也许在早期是同样糟糕的。但是它却向着更加结构化,更加简单的方向发展。Windows 的技术从 OLE,COM,发展到 .NET,再加上 Visual Studio 这样高效的编程工具,这些带来了程序员和用户效率的大幅度提高,避免了 Unix 和 C 语言的很多不必存在的问题。Windows 程序从很早的时候就能比较方便的交换数据。比如,OLE 让你可以把 Excel 表格嵌入到 Word 文档里面。不得不指出,这些是非常好的想法,是超越“Unix 哲学”的。相反,由于受到“Unix 哲学”的误导,Unix 的程序间交换数据一直以来都是用字符串,而且格式得不到统一,以至于很多程序连拷贝粘贴都没法正确进行。Windows 的“配置”,全都记录在一个中央数据库(注册表)里面,这样程序的配置得到大大的简化。虽然在 Win95 的年代,注册表貌似老是惹麻烦,但现在基本上没有什么问题了。相反,Unix 的配置,全都记录在各种稀奇古怪的配置文件里面,分布在系统的各个地方。你搞不清楚哪个配置文件记录了你想要的信息。每个配置文件连语法都不一样!这就是为什么用 Unix 的公司总是需要一个“系统管理员”,因为软件工程师们才懒得记这些麻烦的东西。

再来比较一下 Windows 和 Mac 吧。我认识一个 Adobe 的高级设计师。他告诉我说,当年他们把 Photoshop 移植到 Intel 构架的 Mac,花了两年时间。只不过换了个处理器,移植个应用程序就花了两年时间,为什么呢?因为 Xcode 比起 Visual Studio 真是差太多了。而 Mac OS X 的一些设计原因,让他们的移植很痛苦。不过他很自豪的说,当年很多人等了两年也没有买 Intel 构架的 Mac,就是因为他们在等待 Photoshop。最后他直言不讳的说,微软其实才是真正在乎程序员工具的公司。相比之下,Apple 虽然对用户显得友好,但是对程序员的界面却差很多。Apple 尚且如此,Linux 对程序员就更差了。可是有啥办法呢,有些人就是受虐狂。自己痛过之后,还想让别人也痛苦。就像当年的我。

我当然不是人云亦云。微软在程序语言上的造诣和投入,我看得很清楚。我只是通过别人的经历,来验证我已经早已存在的看法。所以一再宣扬别的系统都是向自己学习的 Apple 受到这样的评价,我也一点不惊讶。Mac OS X 毕竟是从 Unix 改造而来的,还没有到脱胎换骨的地步。我有一个 Macbook Air,一个 iPhone 5,和一个退役的,装着 Windows 7 的 T60。我不得不承认,虽然我很喜欢 Macbook 和 iPhone 的硬件,但我发现 Windows 在软件上的很多设计其实更加合理。

我为什么当年会鄙视微软?这很简单。我就是跟着一群人瞎起哄而已!他们说 Linux 能拯救我们,给我们自由。他们说微软是邪恶的公司…… 到现在我身边还有人无缘无故的鄙视微软,却不知道理由。可是 Unix 是谁制造的呢?是 AT&T。微软和 AT&T 哪个更邪恶呢?我不知道。但是你应该了解一下 Unix 的历史。AT&T 当年发现 Unix 有利可图,找多少人打了多少年官司?说微软搞垄断,其实 AT&T 早就搞过垄断了,还被拆散成了好几个公司。想想世界上还有哪一家公司,独立自主的设计出这从底至上全套家什:程序语言,编译器,IDE,操作系统,数据库,办公软件,游戏机,手机…… 我不得不承认,微软是值得尊敬的公司。

公司还不都一样,都是以利益为本的。我们程序员就不要被他们利用,作为利益斗争的炮灰啦。见到什么好就用什么,就学什么。自己学到的东西,又不属于那些垄断企业。我们都有自由的头脑。

当然我不是在这里打击 Linux 和 Mac 而鼓吹 Windows。这些系统的纷争基本上已经不关我什么事。我只是想告诉新人们,去除头脑里的宗教,偏激,仇恨和鄙视。每次仇恨一个东西,你就失去了向它学习的机会。

后记:“对用户友好”是一个值得研究,却又研究得非常不够的东西。很多 UI 的设计者,把东西设计的很漂亮,但是却不方便,不顺手。如果你想了解我认为怎样的设计才是“对用户友好的”,可以参考这篇博客《什么是“对用户友好”》

(转)为什么犹太人如此优秀?

本文转自有个老外叫马绍飞


虽然我几乎在每一个回复下都说“本人是土生土长的纽约人”,但我同时也是德系(Ashkenazi)犹太人。我选择回答这个问题并不是为了吹嘘或贬低自己的民族,而是想尽可能地消除很多人对德系犹太人的误解。

首先,我们一定要弄清楚的是,那些所谓的“优秀”犹太人到底是谁呢?犹太人分布在全球各地,总人口数最多一千五百万,还不到全世界人口比例的0.2%。不过,犹太人分成了很多不同的民族和宗教派别。爱因斯坦、弗洛伊德、马克思、罗斯柴尔德,还有一些诺贝尔奖得主等著名的犹太人大多属于“世俗德系犹太人”这个群体,“世俗”意味着没有什么明确的宗教信仰。据我观察,亚洲人经常给这一部分犹太人贴上“努力”、”聪明”、“有钱”、“优秀”等标签,而欧美洋人们通常认为我们“小气”、“贪婪”、“狡猾”、“虚伪”等等。

其次,我也希望大家不要盲目地吹捧犹太人,因为并不是所有的德系犹太人都很了不起。我们像其他民族一样有自己优缺点。不过确实有很多人对我们很感兴趣,我非常感谢这些人对我们民族的尊重。

在东亚人民眼中,德系犹太人好像是一群特别能吃苦耐劳、又极具商业头脑的土豪。但在我和我身边人的眼中,我们德系犹太人不过是个有点古怪、无法融入周围社会的游牧民族。说实话,作为犹太人,我心里其实一直都有点自卑感,因为我自己的先辈们移民到美国的时候,真的是一穷二白。我长大之后,才逐渐意识到德系犹太人的成功率竟然是全球最高的。我估计这可能是因为我们的价值观跟大众不太一样吧。

有哪里不一样呢?举个例子,大部分人努力学习是为了获得更好的挣钱机会,或者是给自己更多的职业选择。但犹太人恰恰相反,整体来说,德系犹太人努力挣钱是为了获得更多的学习时间。

Alt text

犹太人追求智慧并不是为了提高自己的经济地位,而更是为了让自己的生活变得更加丰富。上图中可能看出,虔诚(Orthodox)犹太人一天到晚都在读塔木德经,甚至到老年都不放弃学习。其实这个现象已经在以色列产生了严重的经济问题,因为很多虔诚犹太人去钻以色列的福利制度空子,然后拿政府的钱回家读书。

世俗犹太人对智慧的这种忠贞不渝的态度和生活习惯,主要是传承于信仰犹太教的祖辈。但是十九世纪欧洲的“犹太启蒙运动”导致不少德系犹太人逐渐抛弃犹太教。放弃宗教学习后,世俗德系犹太人把自己的学习精力都转向了科学技术、金融、学术、哲学、艺术、音乐、外语等各种领域。

但除了热爱学习之外,犹太人的高成功率还包括一些其他因素,比如:

1、 欧洲中世纪期间,梵蒂冈的教皇宣称任何涉及发放贷款的工作都很不纯洁,所以禁止信仰基督教的人们从事银行工作。只有“肮脏龌龊”的犹太人才有权利发放贷款,这样银行企业自然而然地被犹太人垄断了。梵蒂冈教皇搬了石头砸了自己的脚,不过这个巨大的错误却有利于德系犹太人的长期经济回报。

2、 犹太人的集体主义思想比较强。虽然犹太人对政治、宗教、巴以冲突等方面都持有完全不同的意见,但犹太人整体都互相帮助、互相热爱。我们确实喜欢吵架,但我们依然把其他的犹太人视为自己的亲人。

3、 我们的价值观比大部分西方人稍微保守一点。比如我们大多数人不会花天酒地、吸毒约炮、吃喝嫖赌等等。德系犹太人也非常重视结婚生子、对家庭负责等生活习惯,这一点跟中国人的价值观比较相似。

4、 德系犹太人的历史非常悲惨。近两千年的历史中,德系犹太人在欧洲各地不断被屠杀、驱逐、掠夺、强奸等等。为了能生存下来,犹太人必须努力奋斗,有时甚至是不择手段。基督教徒相信只要向上帝祈祷就能帮你的生活变得更好,但犹太人倒认为上帝对你的日常生活来说连个屁都不算。犹太人觉得只有通过自己的努力,才能改变自己的命运。

还有人说德系犹太人的高成功率跟旧约本身有关系,我倒是觉得犹太教跟基督教或伊斯兰教本质上并没有太大的区别,这三个一神教其实都有良好的一面和残暴的一面,有理性的一面也有反智的一面。德系犹太人的生活方式确实受到犹太教的影响,但比起旧约的内容,他们的高成功率更多是由于历史原因以及德系犹太人的独特生活习惯。最直观的就是,传统德系犹太人和传统非洲犹太人都信仰旧约,但这两个民族的成功率截然不同….

与其他民族和宗教派别的教学方式相比,德系犹太人的教学方式更重视“疑问”,而不怎么重视“回答”,因为很多问题并没有绝对正确的答案。从小长辈们就经常让我们问很多问题,跟同学们一起来进行辩论,从而不断地锻炼自己的独立思考能力。在这种环境下,小朋友们很快就会发现这个世界含有无限的奥秘以及无穷无尽的知识。对我们来说,追求智慧本身才是生命的意义。

德系犹太人热爱学习的主要原因是为了追求智慧,而不是为了考高分、挣钱、竞争、炫耀、装逼等等。实际上,很多世俗德系犹太人倒是更趋向于谦虚低调、勤俭朴素的生活方式,而不怎么爱去购买豪宅豪车或者最新发布的苹果手机,因为这些东西对我们来说毫无意义。在纽约和加州确实有一部分笑贫不笑娼、纸醉金迷的犹太人富豪,但这一部分人毕竟还是少数,因为我们的文化很重视谦虚。大部分德系犹太人把自己的钱都留在银行或者投入孩子的教育。犹太人也会用自己的钱来帮助周围的社区发展、创立非政府组织、建设新的图书馆等,或者把钱捐赠给艺术、教育、脱贫组织,以及慈善机构等等。根据犹太教的传统价值观,挣钱本身并没有什么不好,在这一方面我们的信仰跟基督教徒确实有区别。可是按照犹太教的规则,有钱人一定要用自己的钱来帮助穷人,所以贫穷犹太人从来不会被淘汰。虽然我们世俗犹太人并没有认真地信奉犹太教,但我们依然把这些传统的价值观都传承给了后代。我们对犹太教的态度是“取其精华,去其糟粕”,就像当代中国人对儒家思想以及中国传统社会道德的态度一样吧。

我好像有点跑题了。除了上面提到的原因外,犹太人的高成功率还有另外一个原因,那就是德系犹太人根深蒂固的思维方式。这一点一言难尽,但我会尽量给大家解释一下:

如果你把金钱、面子、社会地位等方面都排在智慧之前,你必然会活得很不开心,你的生活也没什么意义。此外,你在这种思想下也无法挣钱,因为缺乏智慧的人没有独立思考的能力。你的创造力不够全面、特殊技能不够发达、目光太短浅。相反的,如果你把智慧、教育、想象力、激情等方面都放在物质欲望的前面,你不仅可以填满你心里的空虚,也可以凭借丰富的知识和开阔的视野来得到更靠谱的工作机会,并给自己打造更美好的前途。

举个例子,假设你对医学不怎么感兴趣,但你为了挣钱而选择去学医,你将来必定会成为一个没什么工作激情、心情沮丧的三流医生。但如果你是因为想了解人体构造、传染病的症状等去学医,通过自己的努力,你就可以成为一个心满意足、高薪酬的优秀医生。这样的人也有能力给全社会带来巨大的帮助。

爱因斯坦的解释可能会更清楚一点吧:“对知识本身的追求,对正义近乎偏执的热爱,以及对个人独立的渴望,这些都是传统犹太人的特点。而我由衷感恩,我是其中的一员。

Alt text

在CentOS下实现MySQL数据库定时自动备份

前几次系统数据老是出问题,前几天经理让我给写个数据库自动备份,Shell能力有限,周六日再改改..

编写 Shell

# 查看磁盘空间
$ df -h

    Filesystem      Size  Used Avail Use% Mounted on
    /dev/vda1        79G   36G   40G  48% /
    devtmpfs        3.9G     0  3.9G   0% /dev
    tmpfs           3.9G  1.1M  3.9G   1% /dev/shm
    tmpfs           3.9G  592K  3.9G   1% /run
    tmpfs           3.9G     0  3.9G   0% /sys/fs/cgroup
    tmpfs           783M     0  783M   0% /run/user/0

# 进入根目录,创建 DatabaseBackup 文件夹并进入
$ cd /
$ mkdir DatabaseBackup
$ cd DatabaseBackup

# 创建 gupiaocl188.sh 文件并编辑
$ vim gupiaocl188.sh

    #!/bin/bash
    mysqldump -u project gupiaocl188 | gzip > /DatabaseBackup/gupiaocl188__$(date +%Y-%m-%d__%H:%M:%S).sql.gz

    #生成格式
    www_gupiaocl188__2019-09-17__12:00:01.sql.gz

# 赋值可执行权限
$ sudo chmod u+x gupiaocl188.sh

配置 MySQL

# 配置 MySQL [mysqldump]
$ my.ini

    [mysqldump]
    user     = project
    password = HiGirl0921

# 执行 Shell脚本测试
$ ./gupiaocl188.sh

安装 Crontab


# 安装 Crontab
$ yum install -y vixie-cron

# 设置 Crontab 开启启动
$ chkconfig –level 35 crond on

# 启动 Crontab 服务
$ service crond start

# 编辑计时器设置
$ crontab -e

    # 每俩小时自动备份 www_gupiaocl188_ 数据库
    */120 * * * * /DatabaseBackup/www_gupiaocl188_.sh

## Crontab 命令

# 启动服务
$ service crond start

# 关闭服务
$ service crond stop

# 重启服务
$ service crond restart

# 重载配置
$ service crond reload

# 查看状态
$ service crond status

阿川的个人博客"创建一周年啦!"

岁月不居,时节如流。阿川的个人博客至今已经创建一周年了。还记得创建前看过别人的一些博客文章,说什么不推荐小白创建博客,说什么要支出各种域名服务器等费用,博客还挣不到钱,完全没必要创建博客。在我看来我觉这样的说真的误人子弟。在我创建博客这一年中我学会了太多东西,也在因为博客认识了一些志同道合的朋友。这些远比那些钱重要。当然把博客运营好且挣到钱,这也是非常有成就感的。

创建博客这段时间,我也渐渐爱上了写作,它可以让我的知识更加牢固,也正好练习一下文笔,虽然我博客文章质量还不咋样…但是提交前我可究竟了好久,各种改。。。

总之创建这个博客的初心就是记录,学习。希望自己能坚持下去,晚安。

说说消失这二十多天发生的事情

消失这二十多天,出了很多事情:离职,电脑坏了,找工作还有一大堆其他事情…可能这些东西看着也没啥。可是二十多天坏的电脑让我有点感觉人生低谷(;′⌒`),先说第一件事情吧离职。

离职

在公司3个月左右就想离职了,其实我不是很讨厌前后端不分离,只是公司那项目也太乱了。Model不是Model。css+js+php+html一起写。我也想把样式分离出来,但是公司整体项目要统一。我特么服了,一大堆转义符很是烦人。

还有就是工资的拖欠。我觉得公司有困难大家一起可以度,但是公司无公告无邮箱拖欠工资,问财务部根本不说话。我真是对公司这些混日子的很是反感。

电脑坏了

电脑坏了完全是我自己做的…我神舟是大问题没有,小问题不断。那天晚上写项目写完了,我像往常一样把笔记本放在床上折叠桌上并盖上了盖。 不知道是凌晨几点,我一脚把折叠桌踢下了床,大概笔记本离地板有半米。笔记本正朝下摔还挺响…当时意识比较模糊捡起来又睡着了。

第二天起来也没在意,我擦擦电脑开机打开PHPSTORM,大概几分钟电脑开始出现报警声音一长一短不一会儿非正常关机了,不慌不慌反正也不是第一次了。直接拆开主板检查了一遍主板是不是哪摔松了,内存条没有问题,主板什么也是没有问题的。开机后打开Ps打开别的软件,不一会就报警了,报警声音和刚才一样。我直接把主板全拆了重新排线。就这样试了好多次。我有点慌了!!!∑(゚Д゚ノ)ノ

当时准备联系郑州神舟授权售后,然后想想授权售后估计没啥用(真几把没用),打开QQ联系买神舟电脑店长,找老船长问他解决问题。结果是说主板可能出问题了,他搞不了让我神舟苏州总部售后进行返厂,返厂且最低15天。谷歌搞一下返厂攻略。大家好像都是不太推荐。当时先放下返厂,找修电脑的。结果是我感觉他们只会清灰重装、组装电脑…我TM…问了十几家没有一家能修。

找的很烦,只能返厂了,售后查我的保修期在8.31号后就过期了,当时还沾沾自喜赶上时候(现在想想真几把傻屌)。大概2天顺风寄过去了,因为11号周六不上班给拒收了,12号早上签收,下午给我打电话确认电脑。15号检查到问题并报价:“主板5000、键盘300”并监测到主板进水不保修。

当时几把就火了,说劳资笔记本主板进水不保修???劳资键盘是排线断了,换根新的接口就十几块钱。你这什么意思,还想给我换个新键盘???主板坏了她说技术不修,直接换。是直接换!当时忍不住就想骂人,我说明写的清清楚楚的只修不换,我要换主板还要你们售后???主板牵扯到显卡,修不了就换Mac。几把当时就二种选择,修掏钱,不修寄回来。也就是说忙活一星期啥都没干成。

给客服打电话让转接技术,客服说公司没这规定,有什么事情给她说。他妈的当时就想骂人,想要打电话告他们。我要客服工号,客服一个劲说要干什么干什么???我要告你们欺诈消费者???客服说一句:你去告吧!接着就挂了电话。我日尼玛这能忍???好好好不让你们修了,给我寄回来寄回来好不好。客服说包装一下给我寄回来,货到付款。

我尼玛当初修的时候说我付一半,你们付一半。这时候给我说货到付款…客服:“公司有规定,没有修的一律货到付款!”。
心累…感觉自己太年轻了…我好难啊(;′⌒`)

后来历经周转找到在郑州百脑汇找到一家硬件芯片维修,开元世纪电脑维修大哥技术炒鸡帮!主板显卡接触不良,没多久就搞好了花了240

找工作

在郑州顶着太阳40°找工作真是是舒服…上午跑下午跑,一般一天面试俩,最多三家。我商城这块太弱,一直在找商城这块公司。有几个公司真是挺难受的,事之前联系好好的,到了公司给经理打电话,给我说招满了…明二十六号去试岗三天祝我好运吧。

ThinkPHP Save 方法引发的错误

事情是这样的,今下午在我在完善修改老人档案表单验证的时候遇到点问题。明明 Save 方法执行成功却报错?

if ( !empty( $ChildrenDate[ 'name' ] ) OR !empty( $ChildrenDate[ 'idcard_no' ] OR !empty( $ChildrenDate[ 'tel' ] ) OR !empty( $ChildrenDate[ 'relation' ] ) ) ) {
    if ( !empty( $ChildrenDate[ 'name' ] ) ) {
        if ( $ChildrenDate[ 'sex' ] AND $ChildrenDate[ 'idcard_no' ] AND $ChildrenDate[ 'tel' ] AND $ChildrenDate[ 'relation' ] == TRUE ) {
            $AddChildren = D( 'OlderFamily' ) -> where( array( 'id' => I( 'id' ) ) ) -> save( $ChildrenDate );
            if ( $AddChildren == FALSE ) {
                print_r( D( 'OlderFamily' ) -> getLastSql() );die;
                // $this -> error( "数据操作失败,请与管理员联系!" );
            }
        }
        else {
            $this -> error( '请补充完整家属信息!' );
        }
    }
    else {
        $this -> error( '请输入家属姓名!' );
    }
}

就不能惯它!要么成功,要么就用getLastSql()打它

UPDATE `glbt_older_family` SET `older_id` = 31649,`name` = '阿川',`sex` = 2,`idcard_no` = '412727200001160414',`tel` = '18623947428',`relation` = '家人' WHERE( `id` = 4000 );
> Affected rows: 0
> 时间: 0.001s

执行了一下UPDATE并返回 0,也就是说语法并没有错只是没有修改数据。然后我又改变 sex 值为 1,然后返回了Affected rows: 1修改成功了!
这个UPDATE语句我输出输入几十遍,一点错都没有。我脑子笨,找了好长时间找不到,然后把这个逻辑重写了两遍。然而还是无用…一度感到难受…

好了事情的转折点来了!出于个人癖好,总是喜欢闲着没事敲键盘点鼠标,按着按着鼠标乱点到了save方法,跳到Model模型类源码…
出于好奇,反正来都来了,也瞅瞅Save的源码(这里感谢PHPSTORM的强大)

// Model.class.php
public function save ( $data = '', $options = array() )
{
    if ( empty( $data ) ) {
        if ( !empty( $this -> data ) ) {
            $data = $this -> data;
        }
        else {
            $this -> error = L( '_DATA_TYPE_INVALID_' );
            return FALSE;
        }
    }
    $data = $this -> _facade( $data );
    
    $options = $this -> _parseOptions( $options );
    if ( !isset( $options[ 'where' ] ) ) {
        if ( isset( $data[ $this -> getPk() ] ) ) {
            $pk                 = $this -> getPk();
            $where[ $pk ]       = $data[ $pk ];
            $options[ 'where' ] = $where;
            unset( $data[ $pk ] );
        }
        else {
            $this -> error = L( '_OPERATION_WRONG_' );
            return FALSE;
        }
    }
    $result = $this -> db -> update( $data, $options );
    return $result;
}


// Db.calss.php
public function update ( $data, $options )
{
    $sql   = 'UPDATE '
        . $this -> parseTable( $options[ 'table' ] )
        . $this -> parseSet( $data )
        . $this -> parseWhere( isset( $options[ 'where' ] ) ? $options[ 'where' ] : '' )
        . $this -> parseOrder( isset( $options[ 'order' ] ) ? $options[ 'order' ] : '' )
        . $this -> parseLimit( isset( $options[ 'limit' ] ) ? $options[ 'limit' ] : '' )
        . $this -> parseLock( isset( $options[ 'lock' ] ) ? $options[ 'lock' ] : false );
    return $this -> execute( $sql );
}


// DbPdo.class.php
public function execute ( $str )
{
    $this -> initConnect( TRUE );
    if ( !$this -> _linkID )
        return FALSE;
    $this -> queryStr = $str;
    $flag             = FALSE;
    if ( $this -> dbType == 'OCI' ) {
        if ( preg_match( "/^\s*(INSERT\s+INTO)\s+(\w+)\s+/i", $this -> queryStr, $match ) ) {
            $this -> table = C( "DB_SEQUENCE_PREFIX" ) . str_ireplace( C( "DB_PREFIX" ), "", $match[ 2 ] );
            $flag          = (boolean) $this -> query( "SELECT * FROM user_sequences WHERE sequence_name='" . strtoupper( $this -> table ) . "'" );
        }
    }

    if ( !empty( $this -> PDOStatement ) )
        $this -> free();
    N( 'db_write', 1 );

    G( 'queryStartTime' );
    $this -> PDOStatement = $this -> _linkID -> prepare( $str );
    if ( FALSE === $this -> PDOStatement ) {
        throw_exception( $this -> error() );
    }
    $result = $this -> PDOStatement -> execute();
    $this -> debug();
    if ( FALSE === $result ) {
        $this -> error();
        return FALSE;
    }
    else {
        $this -> numRows = $this -> PDOStatement -> rowCount();
        if ( $flag || preg_match( "/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $str ) ) {
            $this -> lastInsID = $this -> getLastInsertId();
        }
        return $this -> numRows;
    }
}

ThinkPHP内置抽象数据库访问层,它是基于PDO封装实现的,项目使用数据库是MySQL,下面开始找UPDATE方法的返回参数。
通过$result = $this -> db -> update( $data, $options );找到UPDATE更新语句方法。
再通过return $this -> execute( $sql );找到execute执行语句方法。

$this -> numRows = $this -> PDOStatement -> rowCount();
if ( $flag || preg_match( "/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $str ) ) {
    $this -> lastInsID = $this -> getLastInsertId();
}
return $this -> numRows;

execute方法里有段代码返回个rowCount()这是什么玩意…它认识我,我不认识它。翻一下手册了解到是返回SQL语句影响的行数(真是对不起楚老师当初还手把手教我们封装PDO…)

也就是说它返回的 SQL影响行数就是我判断的参数,根据SQL输出来说返回个0,是的,是个0。我的判断$AddChildren == FALSE
我瞬间就特么服了Save这个方法…,返回给我0,我是用等于号对比值0 == FALSE这玩意貌似没啥意义…

因为在使用==比较的时候,0会进行布尔转换,在PHP中0即FALSE。我是要你是用来判断错误的,你他妈一对比不是相等了…
所以说全等===进行比较值,拒绝类型转换直接比较值和类型。结果就是整型不等于布尔,解决问题。

爱上Go语言:终端命令

Hello Golang!

在前段时间我学习Docker的过程中和Go进行了我们的第一次约会,超级简单的环境配置搭建、简洁而清晰的语法,近期我已经完全沦陷于Go,已经到了如痴如醉的地步。

作为一个phper,我发现学习Go也是有优势的!PHP和Go都是C语系,上手难度不大,我在微服务、区块链专栏中也经常看到PHP\Go的需要。

至于发展前景也是一片光明。Go诞生于Google且开源,打造的王牌项目都是没的说:Docker、Kubernetes…

build、run、clean

// 这是测试编译命令,和其他静态类型语言一样,要执行 Go程序,需要先进行编译,然后在执行产生的可执行 .exe 文件,
// 如果我们在执行 go build 命令时不后跟任何代码包,那么命令将编译当前目录下的代码包。但不是所有的 Go 程序都可以编译成可执行文件的,它需要满足两个条件:
$ go build
    // 例如以下代码且取名 main.go 在第一行 package main 表示是一个可执行的程序,当然每个 Go应用程序都应当包含一个名为 main 的包。
    // 看到 import ("fmt") 这是指将 fmt 这个包导入 main.go,表示在 main.go 中可以使用 fmt 包中可见所有方法、类型等。

    package main
    import (
        "fmt"
    )
    func main() {
        fmt.Println("Hello Go!")
    }

// run 可以把编译源文件和运行对应的可执行文件,这两步化为一步(需要注意的是 run 不会产生文件)
$ go run

// 删除当前目录下的所有可执行文件(无参数)
$ go clean [alrtAFR]
    // 会删除对应的可执行文件
    sourcefile.go

install、test

# install 用于编译并安装指定的代码包及依赖包。该命令分成两步操作,第一步是生成结果文件(.exe或.a),第二部把编译结果移到 $GOPATH/pkg 或 $GOPATH/bin
    # .exe:带 main 函数的Go文件产生的,有函数入口,所有可以直接运行
    # .a:应用包,不包含main函数的Go文件产生的,没有函数入口,只能被调用
$ go install

# 用来运行测试,这种测试是以包为单位的。需要遵循三个规则
    # 测试源码文件是名称以“_test.go”为后缀
    # 内涵若干测试函数的源文件
    # 测试函数一般是以“Test”为名称前缀并有一个类型为“testing.T”的参数声明的函数
$ go test

get、doc

# 获取代码进行自动编译和安装
$ go get [-alrtAFR]
    # 显示操作流程日志及信息
    -v
    # 下载丢失的包,但不更新已经存在的包
    -u
    # 只下载,不自动安装
    -d
    # 允许使用 HTTP 方式进行下载操作
    -insecure

# 支持 package、func const、var这些代码生成文档,且只对 Public 变量自动生成,而 Private 变量不会
$ go doc

其他命令工具

# 查看Go当前的版本
$ go version

# 用来修复以前老版本的代码到新版本,列入go1之前老版本的代码转化到go1
$ go fix

# 查看当前go的环境变量
$ go env

# 列出当前全部安装的package
$ go list

# 格式化Go代码文件
$ go fmt

(转)一个程序员眼中的价值

本文转自Laruence


前天看了TimYang的<一个技术从业人员眼中的2014> , 有些观点我很是赞同, 于是我也有了想写点什么的冲动…

在2015年的第二天, 我终于好像有了一些跳跃的想法, 可以说给大家听听. 也许不够体系, 不够完整, 但或许能得到一些共鸣.

我先给我大家讲讲我的故事, 我在2007年的时候去了雅虎实习. 当时应聘实习的时候, 我记得我和面试官应该表达过, 我不在乎赚多钱(实习生工资), 只是希望学习.

后来, 2008年的时候要毕业了, 因为在雅虎的氛围等让我觉得非常好, 我几乎没有看什么其他机会就签了雅虎中国, 当时的工资是7000一个月..

就这样相安无事的过了2个月, 突然第三个月要补交社保什么的, 一个月的工资发到手只有不到1000多块(也许我记得不是很精确, 不过只记得很少)..

然后, 我就意识到, 不行, 我得先养好自己…… 于是选择了跳槽…….

到今天, 我已经不会给刚毕业的孩子们说: 刚毕业的时候不要注意工资了..

这是我想说的程序员的一个”价值”方面…

2011在百度呆了3年的时候, 我的技术职级到了T7, 太多的赞誉和给面子的称赞, 让我误以为自己技术已经是”登峰造极”了, 觉得自己特别特别牛逼, 公司对我的职级是不公平的. 我觉得我得到了不公平的评价, 我认为评价我的其他领域的架构师都是不懂我的牛逼.我觉得我的薪水实在是不衬我的能力..等等等….

3年来, 我又进步了很多, 也在不少新的领域有了进一步的提升, 我认识到, 当时的我是多么的狭隘, 技术缺陷是多么的明显, 和楼外楼的人们又有多大的差别.

现在的我, 更喜欢用自己的不足去和别人的优点做比较, 追赶他们的优点, 而不是把别人拉到自己的领域比较别人, 进一步蒙蔽自己的缺点…

这是我想说的, 程序员自己眼中的一个”价值”方面…..

在PHP开发组做了一段时间的时候, 帮助不少人解决了PHP的bug, 有一次很快速的解决ifix提出的一个bug, ifix的人发邮件想我表达谢意, 并且希望我给他们我的邮寄地址, 他们想要寄点礼物感谢我..

我感觉好像不是那么好, 于是我就问Rasmus, 是否可以接受这个礼物, 他说: “我们不是政客, 我们做开源, 为不知道多少人创造了多大的价值, 接受点别人真心感谢的礼物, 没什么不好”.

最后我收到了一个ifix套件… 上面还有一条便签, “Thanks for your help”.

这是我想说的程序员的另外一个”价值”方面…..

2014年, 应该是做贡献做的最大的一年, 在微博内, 我们把无线LAMP的性能提高了2.6倍, 响应时间下降一半多, 节省服务器60%(每年几百万的成本).

在PHP社区, 我们经过去年到今年的各种尝试, 终于得到了PHP7, 性能相比PHP5.6有了成倍的提升, 每一个PHP项目都可以透明的得到巨大的性能提升, 也许整个社区也许都会为此而受益.

我也发现有了很多支持我的人, 也有了不少的”粉丝”, 会收到很多或明显,或不明显的尊敬.

这是我想说的, 我最看重的”价值”….

好吧, 说了这么多, 总结下吧:

一个程序员的价值, 在于你真正产生了多大的贡献, 为公司, 为他人..

如果你真正做出了真正有价值的贡献, 那么你就可以得到于价值相当的肯定. 或多点, 或少点, 或早点, 或晚点.

也许你会自认为得到的不是你期望的, 或者得到的少于付出得, 但, 那应该不会差太远.

而如果只追求你得到的价值, 并且要追求各种价值, 而忽略了你付出的价值, 那会活的很幸苦..

这些散落的点滴, 分享一下.

2015, 祝福大家, 都能创造更大的价值, 追求到自己想要的价值, 新年快乐~

2019-05-22 博客功能更新记录

 这段时间我居然有个危险的想法,更换博客板式…确实也下了狠心去找。弄了一些理想的Jekyll主题。可是那些主题代码都很乱。我很受不了…想一想还是自己动手改版吧。我博客主要问题就是移动端功能支持不太好,没有PC端一样的博文分类导航,这我怎么受得了?必须整它。平时上班也忙,大概整了俩星期。增加了:“移动端博文分类、移动端文章上滑”,经过这次给移动端改版我好喜欢Jekyll这个静态模板引擎很好用。摄影分类也需要改版,它跟技术这类文章不一样,在看摄影分类文章系列时,只有文字显的太古板了。我想做一个卡片格式的。友联板块也要改一改。好了太晚了好饿的,要回去了…公司六点下班,我呆到九点半…公司人七点就走光了,附图一张,白!

阿川的移动端博客图片

(转)我眼中的技术高手

本文转自玉伯的博客


今天发了一条微博

会原生 JavaScript 不代表什么,懂 jQuery、YUI 等才真正好。怎么这么多人有原生主义情结呢?走出那点小天地,海阔天高。
很多人已经猜出,这是为了晚上的文章而发,提前收集大家的想法。这个话题,很早就想谈,肯定会引起口水仗,但有些事不辩不明,不理不清,与其和谐社会,不如辛亥革命。

奇怪的现象

平时工作,时不时能听到一些困惑、感慨:

jQuery 虽好,但只会 jQuery,不会原生 JS 是不被大公司认可的。
最近半年对原生 JS 有些生疏,得补一补。
得好好看看 ECMAScript 规范,把 JS 语言学透彻。

和原生情结对应的,是国内程序员特别喜欢研读源码,比如:

jQuery 源码分析系列
YUI 源码分析
Backbone 及 Underscore 源码解析

国内程序员对源码的热衷,可以拿 SeaJS 的数据来看:

SeaJS数据图片

fork 数高达 500 多,但 watch 数只有 1000 多,这其实是不正常的。相比而言,RequireJS 的数据正常很多:

RequireJS数据图片

简言之,国内与国外相比,有比较明显的两个特点:

  1. 对原生 JS 的学习心更强。
  2. 对类库、框架的源码更感兴趣。

这两点看起来很好,可是:

  1. 研究原生 JS 的优秀文章,大都出自国外程序员。
  2. 类库、框架,国人好像一直在研究,鲜有产出。

这肯定跟我们是社会主义国家有关,但我越来越怀疑这一点。

语言高手们

真正的语言高手不多,我不是,正在看这篇文章的你,很可能也不是,而且这一辈子可能都和我一样成为不了语言高手。

JavaScript(纯语言,不含 DOM 等)高手,在国内屈指可数。周爱民、白露飞、老赵、winter、月影、hax 等等等等,还有一些非常低调的隐士,这些人读 ECMAScript 规范像磕瓜子一样轻松,甚至能花几个晚上就像 BE 大神一样造出一门新语言来。你我等闲之辈,除了佩服之外,只能去谈恋爱。

工作中,我们需要语言高手吗?肯定的说,需要!可是,我们需要大量语言高手吗?除了特殊岗位,我相信很多公司都不需要!

题外话:目前为人知的 JS 语言高手里,除了周爱民,我最看好白露飞。这是一个有能力也有潜力造就 SuperScript 的人,最佩服的是他有实际行动,虽然方向未必正确。

我们的价值在哪

除了重新投胎,我们大部分人这辈子都不大可能有兴趣、有能力、有机缘去成为 BE 大神了。这是个残酷的现实,之所以残酷,只因视野太狭窄。

跳出来,天大地大。妹子多着呢,而且更漂亮。

Douglas Crockford 的 JS 能力很可能不及 winter,但 Douglas 规范并布道了 JSON 格式,天下留名,惠泽全球。

Jeremy Ashkenas 的 JS 能力可能还不如老赵,但 Jeremy 用很裸的代码写就了 Backbone,至少影响了一万人,给各个公司创造的价值总额很可能过千万美刀。

更不用说 Isaac Z. Schlueter,这小伙的 JS 功力很可能还不如我,但 Isaac 打造了 npm 生态圈,而我至今只有精力玩玩 Ant 和 Grunt。

有幸还看过 Google Docs 的前端源码,那代码和 Java 一样中规中矩。但在 RTE 领域,Google Docs 是王者,里面的专利都一堆一堆的……

特别想提及的还有开发 Evernote Clearly 的前端工程师,这小伙子的代码,我眼睁睁看着其从很生涩的 JS 代码,逐步演化成上万行牛逼代码还保持了相当好的可维护性。这份代码就像 Clearly 产品一样奕奕生光。

以及把 jQuery 用得出神入化的 Amazon!前不久那个秒杀国内互联网公司的悬浮菜单,可不是研究原生 JS 能想出来的。

还有 Facebook 的工程师们,Twitter 的工程师们…… 这些故事大家并不陌生。

不是总结的总结

不贬低语言高手们,也不反对去研究编译原理、ECMAScript 规范等。作为技术人员,我们需要这种精神。但是,这仅仅是很小很小很小很小很小的一个领域。并且在这个领域里,永远有比你更聪明的人。

具体对 JavaScript 语言来说,会用就好。搞清楚数据类型、作用域、闭包、原型链等基本概念,足矣。再深入进去,对绝大部分人来说,除了能满足下心理上的优越感,对实际工作不会有任何实质性帮助。

语言的本质和互联网一样,只是工具,是剪刀、石头、布。让张小泉去研究怎么做剪刀就好,我们用好剪头,去剪出各种窗花,更有意思。还有一个有趣的事实是,张小泉会造剪头,但剪不好窗花。

跳出很小很小很小很小很小的语言领域之外,天大地大。永远不要妄自菲薄,每个人身上都背负着独特的使命。去努力寻找自己的,不要老盯着别人的,否则就会成为观众。

好像跑题了。前面那个奇怪的现象,还有很多想吐槽的点。比如

  1. 源码只是很小很小的一部分。直接读源码往往无法领会类库框架的精髓。不读源码,用心去用,用时间去体味,偶尔针对性看看源码,往往更能掌握一个类库框架的真谛。 对社区的贡献可以有很多很多。你的使用经验、用心的 bug 提交、入乡随俗的 pull request、一个认真的评论等等,这些都比去研究什么狗屁源码更有价值。
  2. 一个 Java 高手如果说他会原生 Java,那一定会遭来很多人的围观。我还会谭浩强教我的 C 呢,那几个 if else 还有结构体、指针等谁不会。语言之外的领域知识,才真正造就了高手。对于前端来说,会原生 JS 只能打 20 分,另外 40 分需要你深入使用 CSS、DOM、HTML5 等领域知识,还有 20 分需要你对业务需求、架构设计等有真正的运用,这已经 80 分了,不要太贪心。剩下 20 分,只有两个字:勤奋。

郭靖和黄蓉数据图片

题图:我的网名射雕的来历,喜欢郭靖,无底线。

(完)

ionic笔记(环境搭建与命令)

 昨天下午项目经理让我学习 ionic,我就折腾了一下,经过一番折腾我挺喜欢 ionic 和 node.js,不过我也是在折腾的道路上越走越远,别看这偏博客上就那么点东西,可我这小白操作,真的非常难受…中途遇到各种问题…哎…

在项目中用到的环境依赖:

  • Node.js
  • Android SDK
  • Java JDK
  • Gradle
  • ionic

下载安装Node.js

安装的时候把环境变量默认勾选所以不用我们手动配置

# 查看的当前的Node版本
$ node -v 
v10.15.3 

更换源

# 临时
$ npm --registry https://registry.npm.taobao.org install express

# 永久
$ npm config set registry https://registry.npm.taobao.org

# 验证是否更换成功
$ npm config get registry
https://registry.npm.taobao.org/

第一个Node.js程序!

achuan@DESKTOP-CL4MMTQ MINGW64 /h/Node.js
$ vi HiNodeJs.js

    console.log("Hello World!");

achuan@DESKTOP-CL4MMTQ MINGW64 /h/Node.js
$ node HiNodeJs.js
Hello World!

交互模式

用终端 node 进入交互模式,可以直接执行遇见并显示结果

achuan@DESKTOP-CL4MMTQ MINGW64 /h/Node.js  
$ node  
> console.log("Hello World!");  
Hello World!  
undefined

下载安装 Android SDK

配置环境变量

ANDROID_HOME
ANDROID_SDK_ROOT
H:\environment\Android\android-sdk

我也不是很理解,明明一样还要声明俩?但是少一个就报错,说 ANDROID_HOME 已经弃用......

# 在用户变量找"PATH"并新建环境变量
%ANDROID_HOME%\tools

# android 命令帮助,出现内容即环境配置成功
$ android -h 

配置 Android SDK Manager

Android SDK Manager找到下面这些勾选上,然后点击Install packages按钮进行下载

Tools
    # 工具包自带的Tools
    Aondroid SDK Tools

    # Android公用平台工具
    Aondroid SDK Plaform-tools
    
    # Android构建工具
    Aondroid SDK BUild-tools

# 必须安装,至少需要安装一个版本的API工具包!
Android 9 (API 28)
    # 开发app平台依赖和调试工具
    SDK Platform

    # 模拟器镜像
    Intel x86 Atom_64 System Image

    # API源码
    Sources for Android SDK

Extras
    # 安卓兼容库
    Android Support Repository

    # 加速 Intel 体系的安卓模拟器
    Intel x86 Emulator Accelerator (HAXM install)

创建、配置并运行 avd 模拟器

# 创建 avd 模拟器,执行后会有个对话框,右侧有创建按钮根据需要去配置模拟器配置
$ android avd

# 运行 avd 模拟器
$ emulator -avd [avdName]

下载安装 Java JDK

在官网下载真是死慢,我下载后放在微云了

配置环境变量

在安装 Java JDK 点被环境变量出现问题,需要自己手动配置一下

JAVA_HOME
H:\environment\Java\jdk1.8.0_211

# 在用户变量找"PATH"并新建环境变量
%JAVA_HOME%\bin

在系统环境变量”Path”里找下面这条删掉它
C:\Program Files (x86)\Common Files\Oracle\Java\javapath

下载安装 Gradle

安装完毕后查看一下版本,成功忽略下一步,我点背。

$ gradle -v

配置环境变量

GRADLE_HOME
H:\environment\gradle-4.10.3

# 在用户变量找"PATH"并新建环境变量
%GRADLE_HOME%\bin

安装ionic

这下轮到我们的猪脚”ionic”登场!npm install -g cordova ionic

achuan@DESKTOP-CL4MMTQ MINGW64 /h/Node.js
$ npm install -g cordova ionic
C:\Users\achuan\AppData\Roaming\npm\cordova -> C:\Users\achuan\AppData\Roaming\npm\node_modules\cordov
a\bin\cordova
C:\Users\achuan\AppData\Roaming\npm\ionic -> C:\Users\achuan\AppData\Roaming\npm\node_modules\ionic\bi
n\ionic
+ ionic@4.12.0
+ cordova@9.0.0
added 718 packages from 390 contributors in 658.274s

第一个ionic应用!

# 创建一个空应用 myApp
$ ionic start myApp tabs

# 添加对 android 平台的支持 
$ ionic cordova platform add android

    # 参数可选
    [android] or [ios]

# 编译项目 apk
$ ionic cordova build android [可填选参数]

    # 压缩编译项目 apk
    --prod

    # 编译发行版 apk
    --release

    # 编译压缩发行版 apk
    --release --prod

生成的 apk 文件在  `[项目名]\platforms\android\app\build\outputs\apk\debug\`

运行项目进行调试

# 用默认浏览器运行调试,默认端口`8100`,`127.0.0.1:8100`。
$ ionic serve

# 在 AVD 模拟器中运行调试 [android] or [ios]
$ ionic cordova emulate android

遇到的问题

 看着终端依赖疯狂报错……心好累……忽然觉得我这辈子最让我难受的不是电脑卡、网速慢这都是死问题能搞好。而这软件框架在win运行真是各种奇葩环境依赖Bug……它能把我逼疯……

  • 缺少 Android SDK
You have been opted out of telemetry. To change this, run: cordova telemetry on.
Failed to find 'ANDROID_HOME' environment variable. Try setting it manually.
Failed to find 'android' command in your 'PATH'. Try update your 'PATH' to include path to valid SDK directory.
[ERROR] An error occurred while running subprocess cordova.

        cordova build android exited with exit code 1.

        Re-running this command with the --verbose flag may provide more information.
  • Java JDK 版本过低
    这玩意有个对 Java JDK 版本要求,不能低于1.7正好被我踩个准正好1.7…..
Checking Java JDK and Android SDK versions
Requirements check failed for JDK 8 ('1.8.*')! Detected version: 1.7.0
Check your ANDROID_SDK_ROOT / JAVA_HOME / PATH environment variables.
ANDROID_SDK_ROOT=undefined (recommended setting)
ANDROID_HOME=H:\environment\Android\android-sdk (DEPRECATED)
[ERROR] An error occurred while running subprocess cordova.

    cordova build android exited with exit code 1.

    Re-running this command with the --verbose flag may provide more information.
  • Gradle 版本过高导致不兼容
    这玩意跟 Java JDK “搞双簧?”版本过高导致不兼容。 以前是我单独装的Gradle 5.4.1 all报错后不知道怎么了,自动修复给下载了Gradle 4.10.3 all
Using cordova-fetch for andrid
Failed to fetch platform andrid
Probably this is either a connection problem, or platform spec is incorrect.
Check your connection and platform name/version/URL.
Error: npm: Command failed with exit code 1 Error output:
npm ERR! code E404
npm ERR! 404 Not Found: andrid@latest

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\achuan\AppData\Roaming\npm-cache\_logs\2019-05-07T05_31_11_192Z-debug.log
[ERROR] An error occurred while running subprocess cordova.

    cordova platform add andrid --save exited with exit code 1.

    Re-running this command with the --verbose flag may provide more information.

常用命令

# 查看 Node.js 版本
$ npm -v

# 安装 ionic
$ nmp install -g ionic

# 安装 Cordova 
$ nmp install -g cordova

# 更新 ionic cordova
$ nmp update -g ionic cordova

# 查看 ionic 版本
$ ionic -v

# 查看 ionic 环境信息
$ ionic info

# 查看 ionic 信息
$ ionic -info

# 移除 [android] or [ios] 平台的支持
$ ionic cordova platform remove android

# 用浏览器进行本地调试
$ ionic serve

# 在模拟器中运行调试 [android] or [ios]
$ ionic cordova emulate android

# 显示系统中全部 android 平台
$ android list targets

# 显示系统中全部 avd 模拟器
$ android list avd

# 删除 avd 模拟器
$ android delete avd -n [avdName]

我的第一部单反相机

 咱老百姓,今儿真啊么真高兴!嘿嘿,我的第一部单反相机”佳能EOS 200D 银色单反套机”,今早9:18快递到了!纸箱镇楼! 受苦了宝贝…发货之前忘了指定快递服务,默认发了圆通。看这箱子样子,想起以前我去快递分拣兼职,活太累了管它什么物品都是暴力扔…

Alt text

 大大小小拆完了就是这样样子了(32G SD卡、新秀丽蓝色双肩包、限量20年礼盒、Luce波点内胆包、EOS 200D机身、电池充电器\线 LC-E17C、锂电池LP-E17、相机宽背带 EW-400D、EF-s 18-55m f/4-5.6 IS STM)

Alt text

 看到这个狗头挺烦的,也怪自己吧…在天猫买件肯高UV镜55mm。买回来发现装不上…一看狗头58mm。

Alt text

 为了业余摄影,还特地在博客开了个”摄影”分栏,感觉自己这次博客要大改了一下,需要做个几个相册,就那种卡片视图效果,不过我博客逻辑有点特殊,估计需要点时间,让我头疼的是图片,以后摄影照片一张5MB-10MB,就Github在国内这个接入速度有点吃不消,还是要想办法无备案DNS加速。

Alt text

 这里有点尴尬…本想来拍几张预热看看,没电来了,晚上准备骑行去广场玩玩

Alt text
Alt text

推荐10个优秀的技术栈社区

 作为一个技术人,经验积累对于我们来说无比重要,我们只有不断的学习提升自己才能不被这个圈子淘汰。另外也要不断的总结自己的经验和教训。既然要学习那总要有途径吧,现在网络已经很发达了,社区\论坛随处可见,可高质量并不多。今天拿出来给大家分享一下,这些论坛\社区也是业界公认的优秀…

Github

网址:https://github.com/
 Github是最活跃的开源代码库和版本控制平台,同时也是全球最大的男性交友平台,因此也有网友叫做:“Gayhub”(Gayhub是真实存在的网站就像“知乎”和“逼乎”…不过这个Gayhub真是Gay…)
 Github几乎是在业界知名度最高的技术社区,各个领域的开源类库、代码应有尽有,当然也不缺乏大牛入住,比如我的偶像Linus。

Stack Overflow

网址: https://stackoverflow.com/
 Stack Overflow是最受程序猿欢迎的IT技术问答社区,而且内容也是最丰富的社区之一。百度我不知道,就说google搜索问题时,基本每次都有这小子。后来我在技术问题上遇到了难点直接上在Stack Overflow搜。搜不到满意的答案时再自己发起提问。不过这个网站是英文…扯淡是有人把Stack Overflow给汉化了…协慌网

InfoQ

网址: https://www.infoq.com/
 InfoQ是一个面向IT领域的技术媒体社区。通过提供高质量新闻、文章、演讲,以促进IT领域知识的传播和分享。InfoQ还专设的中文、英文、日文等海外版站点。

FreeBuf

网址: https://www.freebuf.com/
 FreeBuf是国内活跃度较高的垂直类网络安全网站。网站主要内容就是国内外一些技术文章。网站人群以极客、黑客、从事安全类人员居多。

  • 官宣: “FreeBuf希望为国内网络安全领域带来一抹正能量,传递真正的黑客与极客精神。”

ChinaUnix

网址: https://www.chinaunix.net/
 我想作为一个IT开发者人员,怎么也避免不了跟Linux系统打交道。而ChinaUnix就是以Linux\Unix、服务器、数据库、内核源码等技术为主的自由、开放的技术社区。运维人员居多。

看雪安全论坛

网址: https://bbs.pediy.com/
 看雪的历史比较悠久,创始于2000年(那年我刚出生…)看雪是国内首个讨论加密解密技术的论坛,看雪见证了中国互联网的发展及国内安全领域大环境的发展。社区大牛居多,氛围很棒,在国内安全圈具有很强的号召力。

吾爱破解

网址: https://www.52pojie.cn/
 吾爱破解成立于08年,与看雪一样是国内氛围很好的论坛,主要板块是逆向、脱壳。。站内帖子基本没有灌水评论,人群我感觉论坛学生不少,因为小时候喜欢软件破解,看雪和吾爱论坛我也是来回逛。

比格张

网址: https://bigezhang.com/
 比格张是一个发现和分享互联网优质资源的平台,网站资源比较广:技术、开发者、产品、设计运营、素材等,平台还设有新闻热榜,如知乎、微博等各种热榜话题。

黑客派

网址: https://hacpai.com/
 黑客派是一个活跃的小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。社区名称寓意:HacPai 分别取 Hacker / Painter 的头三个字母组成,源自《黑客与画家》。

图灵社区

网址: https://www.ituring.com.cn/
 技术改变世界,阅读塑造人生。社区逛够了,我们坐下来安静的看会儿书吧!图灵社区是一个计算机书籍类社区。图灵社区电子书做的很帮,因为图灵自己就是出版社,电子书籍的排版真的是超赞!同时也提供 mobi 推送,可以在 Kindle 等设备阅读。而且图灵电子书全部是 DRM-Free(未加密),技术上来说我们可以任意拷贝。

快速定位Apache错误

 今天给项目配置权限的时候又遇到了Apache错误,这种事情早习惯了,排错了…要说启动吧,您看这让人难受错误提示…

Windows 不能在 本地计算机 启动 apache24。有关更多信息,查阅系统事件日志。如果这是非 Microsoft 服务,请与服务厂商联系,并参考特定服务错误代码 1

这是啥???错误报告这么官方真的没意思。。。一般配置遇到问题我都是看日志,不过日志也是经常不靠谱,他描述的也是很扯淡…这个时候就是还是启动apache启动程序好使,文件名叫“httpd.exe”位置在apache根目录“bin”目录下,通过命令行启动就好。

achuan@DESKTOP-CL4MMTQ MINGW64 /c/AppServ
$ cd Apache24/bin

achuan@DESKTOP-CL4MMTQ MINGW64 /c/AppServ/Apache24/bin
$ ./httpd.exe
AH00526: Syntax error on line 113 of C:/AppServ/Apache24/conf/extra/httpd-vhosts.conf:
<Directory "H:\\tp5\\public""> path is invalid.

这个错误大概说的意思是 C:/AppServ/Apache24/conf/extra/httpd-vhosts.conf 文件第113行语法错误,
<Directory "H:\tp5\public"">路径无效。 打开httpd-vhosts.conf文件后,定位到113行发现路径多个“/”…. 来回找目录可能麻烦一些,不过创建一个软连接就可以了。
不过若真的找不到错误或者错误找到解决不了也有办法,请点这个万能链接

好了开个玩笑,如果您有什么解决不了问题您可以在下方留言大家可以讨论一下。

_initialize()和__construct()的区别

 今天项目经理让我做个一个功能页面,在查阅往常相同项目源码时遇到一个没有见过的函数 _initialize()Google一下知道了它是Thinkphp内置函数,查阅了一番资料写了几遍总结一下。

 _initialize()函数是在任何函数之前,都要执行的,当然也包括__construct()也就是构造函数。也就是说如果_initialize()函数存在,调用对象的任何方法都会导致_initialize()函数的自动调用,而_construct()函数仅仅在创建一次,跟其它方法调用没有关系。

 __construct()函数它是双下划线,而_initialize()函数是单下划线

 如果父子类均有_initialize()函数,则子类覆盖父类,如果子类没有而父类有,则子类继承父类。

 默认情况下,子类的构造函数不会自动调用父类的构造函数。在调用子类对象的_initialize()函数时,也不会导致自动调用父类的_initialize()函数。 实际编写子类的构造函数时,一般都要加上父类构造函数的主动调用parent::__construct(),否则会导致子类对象空指针的异常, 如 Call to a menmber function assign() on anon-obiect

Jekyll环境搭建(Windows 10)

 最近想把想博客改版需要在本地用到Jekyll,不过搭建的过程真的心累…过程中真的是各种问题谷歌百度各种乱七八糟的文章一点都不靠谱,折腾一天给重要给搭建成功了…
整个搭建过程需要用到的软件/安装依赖我先列下

  • Rubyinstaller-2.3.3-x64
  • DevKit-mingw64-64-4.7.2
  • Gem
  • Bundler

安装Ruby

 因为Jekyll是基于ruby开发的,首先下载安装ruby,我不推荐下载Ruby2.3以上的版本,不知道怎么回事2.4、2.5、2.6我都试过感觉他们跟自带BUG似的,总是出现一堆莫名其妙的问题。
ruby官网下载地址(下载速度奇慢)

  • 官网链接:https://rubyinstaller.org/downloads?achuan.io

我用的ruby 2.3.3这里给大家发出来微云的链接

  • 微云链接:https://share.weiyun.com/5vv4fjt?achuan.io
  • 微云密码:achuan

再次声明一下,这里我用的是ruby 2.3.3,其他版本我并不保证。

  1. 双击ruby应用程序,进行安装
    注意:默认如果有已勾选项,它会自动帮你配置环境变量,但是环境变量如果不立即生效可以用cmd命令刷新环境变量,如果还不行就重启。
> echo %PATH%
  1. 安装完成后测试一下是否安装成功,若输出版本信息即安装成功。
> ruby -v

更换源

通过gem sources查看当前源

> gem sources -l

删除源默认源,我这个里默认有一个源,官方源,因为它太慢了,我们删掉它

> gem sources -r https://rubygems.org/

以前还有淘宝源可以用,不过现在也就已经凉了,下面是我们要替换成国内RubyGems镜像站点

> gem sources -a https://gems.ruby-china.com

DevKit的安装与配置

 这里我用的是DevKit 64-4.7.2,这里依然提供微云存储

  • 微云链接:https://share.weiyun.com/5sROg49?achuan.io
  • 微云密码:achuan
    1. 双击DevKit应用程序,运行它解压到你想要的位置,尽量url短一点因为等会要用到比如;C:DevKit
  1. 初始化与配置
 # 进入DevKit目录并执行命令,初始化生成 config.yml
> ruby dk.rb init

进入DevKit目录并编辑config.yml

# 用cmd命令记事本来编辑config.yml
> notepad config.yml

      # Example:
      #
      # ---
      # - C:/ruby19trunk
      # - C:/ruby192dev
      #
      ---
      - H:/environment/Ruby23-x64


注意:“- H:/environment/Ruby23-x64”,其中的“-”也是符号!配置config.yml这一步非常重要,还请您仔细检查!
  1. 安装
# 安装
> ruby dk.rb install

Bundler的安装

 Bundler是管理Gem一个依赖工具 注意:minima是我在创建Jekyll项目的时候出现的问题,当我使用 jekyll new achuan.io 创建Jjekyll项目时,命令卡死了,随后Ctrl + C 强行停止后 cd achuan.io 使用 bundler show 查看缺少依赖文件

Could not find gem 'minima (~> 2.0) x64-mingw32' in any of the gem sources
listed in your Gemfile.

# 它说我在gem源中找不到 “minima”, 然后我就安装了,不过装好后又出现这样的tzinfo-data和wdm...提前列出以防大家走弯路。

> gem install bundler

# gem 依赖文件
> gem install minima
> gem install tzinfo-data
> gem install wdm

下面是gem依赖列表

  • addressable (2.6.0)
  • bundler (2.0.1)
  • colorator (1.1.0)
  • concurrent-ruby (1.1.5)
  • em-websocket (0.5.1)
  • eventmachine (1.2.7)
  • ffi (1.10.0)
  • forwardable-extended (2.6.0)
  • http_parser.rb (0.6.0)
  • i18n (0.9.5)
  • jekyll (3.8.5)
  • jekyll-feed (0.12.1)
  • jekyll-sass-converter (1.5.2)
  • jekyll-seo-tag (2.6.0)
  • jekyll-watch (2.2.1)
  • kramdown (1.17.0)
  • liquid (4.0.3)
  • listen (3.1.5)
  • mercenary (0.3.6)
  • minima (2.5.0)
  • pathutil (0.16.2)
  • public_suffix (3.0.3)
  • rb-fsevent (0.10.3)
  • rb-inotify (0.10.0)
  • rouge (3.3.0)
  • ruby_dep (1.5.0)
  • safe_yaml (1.0.5)
  • sass (3.7.4)
  • sass-listen (4.0.0)
  • tzinfo (2.0.0)
  • tzinfo-data (1.2019.1)
  • wdm (0.1.1)

安装Jekyll

 通过gem install安装jekyll

> gem install jekyll

# 分页
> gem install jekyll-paginate

# 查看版本号。出现就ok
> jekyll -v
jekyll 3.8.6

启动Jekyll

 写到这里真的不容易,以前在Linux搭建Jekyll没有这么心累,今windows搭建真的…几十个bug疯狂涌出…算了…留着幸福的泪,码着命令…

# 生成一个Jekyll模板并进入它
> jekyll new achuan.io && achuan.io

# 启动Jeykll,默认端口4000
> jekyll serve

# 成功后显示如下
H:\git\achuanya.github.io>jekyll serve
Configuration file: H:/git/achuanya.github.io/_config.yml
            Source: H:/git/achuanya.github.io
       Destination: H:/git/achuanya.github.io/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
                    done in 1.196 seconds.
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
 Auto-regeneration: enabled for 'H:/git/achuanya.github.io'
    Server address: https://127.0.0.1:4001/
  Server running... press ctrl-c to stop.

打开浏览器输入:127.0.0.1:4000
本地ip:127.0.0.1,端口:4000。感觉IP+端口麻烦的话,去VirtualHost配置。
如果不太熟悉VirtualHost配置,可以看我以前的文章:Ubuntu Server部署日记

今日随笔

 到今天为止我已经工作了第十六天,我热爱我的工作也非常喜欢我司的文化与业务,公司也非常人性化,我们开发部老总人高马大平易近人,同事们很幽默经常说说笑笑,别的部门不知道怎么样,但是我们开发部氛围很棒,完全没有入职前感觉的紧张与压抑。美中不足的是小区楼下早餐店比较少,但是小区周围的绿化却很棒,出门左右拐都有一条小路,小路两旁种植了很多我认不出的花草和树木,三月的初春,小路两旁的花草和树木都长得格外养眼,它们挨挨挤挤,散发着清香。出了小区向左拐这个小路直通我公司,每次上下班我都喜欢从那里骑共享单车回家,耳机播着她喜欢的歌曲,欣赏着小路两旁的花草和树木,时不时吹来一阵微风把花草散发出清香吹入我的鼻子里,这味道能使我每一天都开心……日子过的可能太安逸了。

第一次校招复试总结

 上个月28号的时候校招拿到了一家公司offer,今天十号了也到了约定的时间,我早上8点30左右出发点去应聘,那会儿在公交车上晕车感觉真是生不如死….大概是9点到了目的地。进入他们公司后,环境跟自己想的差距有这么一点,我看他们应该都在忙着工作,不知道是是不是因为这个让我感觉气氛有些压抑。不过他们的技术主管给我印象还是很不错的,挺阳光的,我和他在对话的时候他也微笑的对我说,我很喜欢这人的人。
 这次复试大概的流程去了后填一下个人信息表,然后同把简历给了一个姐姐,提交了资料后大概十几分钟吧,我被第一个叫过去了。我这人内向第一次复试三个面试官盯着我一个我,看似我坐那抱着书包不慌,其实内心已经波涛澎湃…刚开始做个自我介绍,然后技术主管问了一些我是如何学习的,我举例说了一些Github,开源中国等等社区论坛…然后问了我项目制作的一个流程,还有MySQL的优化,MVC框架运作的原理等等…比较难受的两点是那个高并发之类的技术,因为我没有接触过这些前沿技术直接回答不懂,不过有一点真是扯淡…当时我非常紧张…他问我PHP数据类型有哪些,我直接懵逼了…
 差不多也就上述那些事,问完后就让我出去坐等一会儿,陆续了半个小时左右,我的朋友也都回来等最重结果。后来就是那样我们几个过了,谈论了一下公司的文化、项目与期望薪资,谈妥了经理说让我们回去等候电话,慎重考虑一下。
 基本是说完了,过程就是那么个过程,想想这次面试中犯的错真是幼稚,太紧张把PHP数据类型给忘了…自己面向对象这块抽象类和接口知识不太牢固,还是写的少。还有就是ThinkPHP的开发和CentOS部署,我没有玩过CentOS晚会儿装一下玩玩,正好再来一篇关于CentOS的博客。就到这里吧,第一次校招复试总结也就那么多内容。

第一次校招面试总结

 今天上午校招面试,也是我人生中对自己喜欢的工作的一次技能检验。面试提到几个问题,其中还有个关于与ajax异步请求,我记得不是太清没答上来。现在刚吃完饭就来总结一下。

Ajax的异步与同步

 今天校招面试就这个没有答上来,想想前段时间刚学过,没及时复习真是不应该…  ajax里面有个ES7的异步函数:async。ajax默认是异步请求,如果把async默认参数改城true,这标识着在请求开始后,其他代码依然能够执行,但是这样的话如果ajax代码不通,会导致下面的代码停止加载,会导致页面加载崩溃。
 举一个栗子:

console.log('1');
$.ajax({
    url:'admin/ajax_add',
    async:true,
    success:function(e)
    {
        console.log('2');
    }
});
console.log('3');

 也就是说async设置为true时,这时ajax是异步的,就算ajax出现问题,其他的代码还可以继续执行。如果当async是同步既(false)这样的话ajax请求不到数据就会停止下面的代码。

Mvc设计模式与Mvc框架的区别

 这里之前先简单的说一下Mvc,它是为了代码分离而产出的一个程序模式。

  • M(Model,‘模型’)专门处理应用程序数据逻辑,通常用于数据库中存取数据。
  • V(View,‘视图’)指数据呈现后的状态,简单的说也就是用户看到的UI画面。
  • C(Contrller,‘控制器’)它是控制视图的输出,并向模型发送数据。

 我是这样理解的,Mvc设计模式是一群大佬经过长久的实践后的总结。也就是用于解决一系列问题的解决方法。

  • Mvc设计模式可以说是设计师设计的图纸,而Mvc框架则是工程师以设计师的图纸而建造的产品。

面向对象知识点

接口和抽象类的区别

抽象类是一种不能被实例化的类,只能作为其他类的父类来使用。抽象类是通过关键字abstrat来声明。
抽象类与普通类相似,都包含成员变量和成员方法,两者的区别在于,抽象类中至少要包含一个抽象方法,抽象方法没有方法体,该方法天生就是被子类重写的。

接口是通过interface关键词来声明的,接口中的成员常量和方法都是public,方法可以不写关键词public,接口中的方法也是没有方法体。接口中的方法也是天生就是要被子类实现的。

抽象类和接口实现的功能十分相似,最大的不同是接口能实现多继承。在应用中选择抽象类还是接口要看具体实现。

__autoload()函数是如何运作的?

 这个魔术函数的基本条件是类文件的文件名要和类名字保持一致,当程序执行到实例化某个类的时候,如果在实例化之前没有引入这个类文件,那么就会自动执行“__autoload()”函数。

总结

 人生中第一次正式的技术面试,一向内向的我比较紧张,不过二次面试的机会还是拿到了,还有就是这次简历做的不是很理想,太简约了,我以为简约能让hr更好的阅读…好吧..虽然我本人不是很喜欢花里胡哨的东西,楚老师也说这是个看脸的时代…好了不写了,再写的话手机话费要超负额了….

ThinkPHP笔记(路由实现与操作方法)

 前段时间一直在学习ThinkPHP,不过比较忙没有同步博客,今天发表一下。

路由文件

  • H:\tp5\thinkphp\library\think

生成一个控制器文件

控制器文件命名规范,大写字母开头的驼峰命名。

  • php think make:controller index/Sql –plain

浏览器访问控制器(sql.php)

  • tp5.io/index/Sql
  • 域名/模块/控制器

ThinkPHP的控制器操作方法支持不同的响应输出,对于不同的响应输出会调用不同的Response子类 ,例如‘json’,‘xml’等,可以直接return一个数组或者对象数据,然后交给‘Response’对象。还有一种情况是,你可以统一使用系统提供的钩子输出进行额外的处理,而如果你使用了‘echo’直接输出,将无法享受这些功能特性。

更简短的url

路由定义前URL:tp5.io/index/Sql

  • Route::get(‘sql’,’Sql/index’);

路由定义后URL:tp5.io/Sql

添加操作方法

操作方法命名规范,小写字母开头的驼峰命名。
如果定义路由并且没有开启强制开路由的话,那么系统会根据下面的默认规则解析URL

  • 域名//模块名/控制器/操作器名/参数1/值//参数2/...入口文件>

对于操作方法的必要参数,我们使用了方法的参数定义。对于方法的参数,通常包含两种类型:对象类型和普通类型。所有的参数都是来自于当前的请求,对象类型参数比较特殊一些,通常是通过依赖注入自动完成。

  • 注意:并不是所有的URL地址中的变量都是‘$GET’变量。

在ThinkPHP中URL中属于pathinfo地址,这个地址的的解析过程是由框架内部实现的,并且会自动解析成一个URL变量(但却不是GET变量,证据就是你用原生的‘$_GET[‘id’]’是获取不到‘id’的值的),一个真正的GET变量应该是下面的请求URL。

  • index/sql/read?id=123

这个时候id变量才是一个真正的‘$_GET’变量,因为我们可以通过原生的‘$_GET[‘id’]获取到‘id’值。 不过框架封装看一个‘param’变量,可以让你不管当前的请求类型是‘GET’还是‘POST’,都能无差别的统一获取当前的请求变量。
所以,上面两种URL地址,我们都可以统一使用‘input(‘id’)’(这是一个助手函数,其实是调用‘Request’对象的‘param’方法)来获取当前请求的‘id’变量,当然‘$_GET’和’$_POST’ ThinkPHP定义操作名时不能用‘php’关键词,如果php版本低于5.6+,如果你担心你的应用可能存在此类问题,那么可以尝试使用方法后缀功能,代价就是你在定义方法的时候可能要多写几个字符了。

  • // 操作方法后缀
  • ‘action_suffix’ => ‘Action’,

也是就说每个操作方法名都要加‘Action’的后缀,不过访问操作名的时候‘不需要加Action’
如果使用的是‘PHP7.0+’版本的话,基本不用担心这个问题,但可能会受到来自IDE自动格式化的困惑。

电信用户投诉攻略

 因为我手机卡是合约卡的原因,又临近毕业了,这张卡握在手里真的没得用,36元套餐1G流量,这样的套餐我来说真的是难受,只能更换套餐。以前我都是靠着校园网和移动纯流量卡,校园网出了校门就不能用了,不划算我给停了,那个打幌子的移动纯流量卡更是个坑,在此提醒不推荐大家办理这种卡。
 因为我这卡是合约卡,不能随意转套餐,然后我就拨打10010转人工客服说需求说转腾讯大王卡套餐,不过客服百般阻挠说这个不好那个不好,不过我态度比较强硬,之后在24号晚上8:25我收到了工单。

Alt text

 25号早上11点,我老家周口淮阳县城联通分公司给我打电话说,刚开始通话她说合约卡期间不能更换套餐又说让我准备证件回老家办理业务;我的天,这都9012年了,换个套餐还回老家,

Alt text

 歹说好说最后她加我微信我把证件资料给了她,委托她帮我办理。差不多10分钟左右,她给我打电话说合约卡内不能更换套餐唯理由让我重新办卡…此时想到了一句话“老用户与狗不得办理”

Alt text

 客服解决不了事情也不是几次了,直接拔打联通总部电话,这里我搜集了三大运营商总部投诉电话

  • 中国联通:010-10015(注意必须要加 010 北京区号)
  • 中国移动:4001110086
  • 中国电信:4008810000

 果不其然,还是联通总部电话好使,拨打电话后直接人工服务诉说你的事情。差不多5分钟左右,联通总部那边说12小时内会有处理人员主动联系我。然而在下午4:25,那个劝我办理新卡的姐姐给我打电话声音很温柔的说她又试了一下办理好了说下月套餐生效…与上午完全不是一个人。
 下面真香警告!

Alt textAlt text

 后来我查了一下,其实早在2006年,工信部就出了文件,即〔2006〕630号文件,其中明确规定“在同一移动电话归属地内(俗称“移动电话本地网”),移动通信企业应保证本企业同一网络的原有用户,可以在不改变号码的情况下,自主选择使用本企业的所有资费方案(已停止发展用户的除外)”,这意味着运营商对于目前老用户的限制是违规的。

在2008年又出台了指导意见,用户对资费方案享有自主选择权,电信企业应给予充分尊重,不得限制用户选择其指定的资费方案,未经用户同意,不得擅自更改与用户约定的资费方案。

 如果总部投诉都不好使大家可以直接去工信网投诉相应的服务商,这是最有效的解决方法(如果总部投诉不好使再去投诉),成功案例可以google一下

点击进入电信用户申诉受理中心

(转)微不足道的坚持

本文转自玉伯的博客


在你最擅长的专业领域内,可曾有过一件让你骄傲,持续坚持下来的事情?每当别人谈论它时,你总是偷偷窃喜?

我曾经这样勉励我的弟兄们,在满负荷的工作压力面前,总要找到一件有意义、自己感兴趣且擅长的事情,坚持做,是一种解压,更是为了让自己的专业所长,找到有意义的落脚点。分享三件事情,大家共勉

第一件事:

两前年,流火曾经找我聊想搭建一个行业内前端社区,他有清爽的界面,有优雅的交互体验,我当时并不看好,担心接下来的工作压力会让人抗不住,两年过去了,在百度搜索f2e可以在显要位置看到这个极简的域名”f2e.im“。社区雏形完整,代码完全开源,我也很幸运成为F2E社区第三号成员

我当然听到过太多质疑的声音,诸如”这东东和某东东很类似?”,或者”这里菜鸟太多了,没有我想要的高手?”云云。

Whatever!~

评论终将归于浮云,留下的是两年的坚持,以及它换来的微不足道的满足感!同样,当流火在解决了成百上千个无关紧要又繁琐冗碎的bugfix时,换来了对python的精通,对代码版本迭代的专业性把握,对网络协议和内容渲染模式的深刻理解,收获了直面一线用户的宝贵经验,更重要的是,懂得坚持的意义。这些硬技能直接促成了今天流火在业务上的钻研与精通。

第二件事:

前年年底,我们团队发起了针对校招同学的”魔鬼训练营”,宗旨是艰苦一年,主动成长,我们无条件的侵占了大家的加班、午餐、晚餐、甚至周末时间。工作的紧张压力下,还要准备给大家反讲,质量良心保证,精力严重透支,要不是靠着年轻的身子板,扛下来简直是个奇迹。

迟伤、影逸、夕剑 全程坚持了下来。

当年反讲的JavaScript设计模式、熟读W3C标准、NodeJS、Git、字符编码、HTTP协议这些基础知识,让充电加速。现在当面对业务压力和未知bug时,他们显然更勇于去透过表面看本质。并在 WebRocket(@影逸)、机票YUI体系的KISSY化(@迟伤)等项目中淋漓紧致的体现出自身专业水平。毕业一年,无压力的开始挑大梁了。

回想起来,魔鬼训练营中辛苦的坚持,是如此微不足道。

第三件事:

两年前,我作为PM落地了一件看似不可能完成的任务,JavaScript权威指南(第六版)的翻译。认领这个任务的初衷仅仅是想挑战下自己的能力极限,看清楚自己几斤几两,没想到一做就是一年。

这段时间在工作和业余时间谨慎的保持平衡,压力面前各种放弃的冲动仍历历在目,但仍抵不过这本书上市之时的那股满足感。

过程中听到太多质疑的声音,WhatEver,你所掌握关于JavaScript的一切,始终是我的子集。纯技能上的收获更是大大的丰厚。

结语

任何一件事情,有多大投入,就有多大收获。当你还是局内人时,不清楚傻傻的坚持的意义,这时内心的纠结最容易让人动摇和放弃。但事成之日,才会真正拨云见月,一树百获。这时,压力是浮云,纠结是浮云,抱怨是浮云,斤斤计较更是浮云。“坚持到胜利”,是如此言轻,又是如此言重。

在你最擅长的领域内,可曾有过让你骄傲,持续坚持下来的事情?每当别人谈论它时,你总是偷偷窃喜?那么。。。

Redis笔记(安装与数据类型)

Redis的安装

 我用的系统是Ubuntu 16.04.1 LTS, 关于windows的安装就不说了

# 更新源
$ sudo apt-get update
# 安装
$ sudo apt-get install redis-server

# 启动
$ redis-server

# 启动之后再打开一个终端,连接redis服务
$ redis-cli

# 127.0.0.1是客户端ip, 6379是服务端口
# 我们发送一个 ping,如果返回PONG表示服务器正常
127.0.0.1:6379> ping
PONG

Redis的数据类型

 Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(Sorted Set:有序集合)
 关于这五中数据类型的操作,因为很多我都用不到,所以不是很内容不是很全面还请见谅,下面我们从字符串开始一一说

String(字符串)

 这个String(字符串)是Redis五中数据类型中最基本的数据类型,也是最简单的一个,它是二进制安全的,它可以包含任何数据,如jpg、序列化的数据……不过它的容量也是有限度的,String的Value值最大可以存储512MB

Redis的set是string类型的无需集合
集合通过哈希表表现的, 所以添加、删除、查找复杂度都是0(1)

# 添加一个string元素到key对应的set集合中,
SET key value

# 输出一个set集合
GET key

# 自增+1
INCY key

# 自减-1
DECY key

# 自定义自增+5
INCYBY key 5

# 自定义自减-5
DECYBY key 5

# 批量增加 set 
MSET key value [key value]...

List(列表)

 List它是链表而不是数组,这意味着list的插入和删除操作会非常的话,时间复杂度为0(1)但是索引定位很慢,时间复杂度为0(n)

# 向列表的左边插入
LPUSH books value

# 输出 lpush(value)
LPOP key

# 向列表的右边插入
RPUSH key key key

# 输出 rpush(value)
RPOP key

# 输出 rpush(value)值的数量(不是具体值)
LLEN key

# 输出列表内容(status/stop 可以理解为 0 -1索引)
LRANGE key status/stop

# 清除列表内容
LTRIM key status/stop

Hash(哈希)

 Hash是一个string类型的fieid和value的映射表,Hash特别适合存储对象,Hash结构用户信息,不同于字符串一次性需要全部序列化整个对象,Hash可以对用户结构中的每个字段单独存储。
 这样我们需要获取用户信息时,可以进行部分获取。如果以字符串形势保存用户信息的话,就只能一次性全部读取这样就会失效浪费流量。

# 创建一个 Haet值
HAST key fieid value

# 获取一个 Hest
HGET key fieid

# 更改 Hast值(也就是在创建的基础上重新赋值)
HAST key fieid value

# 批量添加
HMSET drinks milk 'value' tea 'value'

# 输出 Hast
HGETALL drinks

# 自增+5
HINCRBY drinks amount 5

# 自减-5
HINCRBY drinks amount -5

# 删除多个 Hash字段
HDEL drinks [fieid] [fieid]

Set(集合)

 Set它的内部的键值对无序的唯一的,它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值Null。
 Set结构可以用来存储活动中奖的用户ID,因为有去除功能,可以保证同一个用户不会中两次!

# 添加两个集合
SADD key meber meber

# 输出一个集合
SMEMBERS key

# 查询集合中,如果存在返回 1,否则返回 0
SISMEMBER key meber

# 在key集合中删除指定的元素或多个元素
SREM key meber

# 返回一个集合与给定集合的差集元素
SDIFF key key

# 移除集合中的指定 key 的一个或多个随机元素,并返回移除的元素
SPOP kecy count

# 交集,返回给定所有定集合的交集
SINTER key key

# 并集,返回一个集合的全部成员,该集合是所有给定集合的并集
SUNION key key

Sorted Set(有序集合)(重点!)

 Zset保证了内部的唯一性,另一方面它可以给每个value赋予一个score,代表value的排序权重。
 Zset可以用来存粉丝列表,value值是粉丝的用户ID,score是关注事件,我们可以对粉丝列表按照关注时间进行排序。  Zset也可以用来存储学生成绩,value是学生ID,score是学生考试成绩,我们可以对成绩按分数进行排序就可以得到它们的名字。

# 添加一个有序集合,30是数量,person是人数(添加也可以用于修改)
ZADD key 30 person

# 查看有序集合成员数量
ZCARD items

# 查看有序集合成员值(0 和 1 是索引)
ZRANGE key 0 1

# 查看有序集合成员值和名字,默认从小到大排序显示
ZRANGE key 0 -1 withscores

# 查看有序集合,成员值和名字,从大到小排序
ZREVRANGE key 0 -1 withsores

# 查看 member的值
ZSCORE key member

# 查看最小值和最大值之间的 member值
ZRANGEBYSCORE key min max

# 查看负无穷 - 2000之间的 member值
ZRANGEBYSOORE key -inf 2000

# 查看正无穷 - 2000之间的 member值
ZEANGEBYSOORE key inf 2000

# 批量删除有序集合
ZREM key member member

Redis通用操作

过期时间

 Redis所有的数据结构都可以设置过期时间,时间到了,Redis会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个Hash结构的过期,而不是其中的某个key。

# 设置 5 秒后数据过期,成功返回 1,否则 0
#(不会删除 key,只会删除 vlaue值)
EXPIRE key 5

# 查看过期时间
TTL key

# 失效过期时间
SETEX key secods value

事务

# 事务,当事务开启时,所有的命令都会延迟执行
MULTI

# 执行所有延迟命令
EXEC

回滚

# 回滚,回滚上一个操作前的状态(事务期间不支持回滚)
DISCARD

 这些都是这段时间记的笔记,全打出来了好累…不过辛苦的劳动得到了心理上的满足,很舒服,这五个Redis有序集合中我觉得 Sorted Set(有序集合)比较重要,需要多加练习,而Set(集合)是我的弱项也是需要多加练习。
 年后一直没发过什么博客,一直在忙着练习面向对象的项目,同时也在学习封装自己的MVC框架!本篇文章就到这里吧!Bai!

2018小总结

 岁月不居,时节如流。现在距离2019年还有5分钟,想一想年后…我的求学生涯也该结束了…挺难受的,不过收获还是一箩筐的!那些不该经历和不可能经历的事情都让我走了一遭…

关于生活方面

 我比以前更耐草了,适应力极强,耳麦脑袋挂,手掌键盘放,能折腾两宿…不过让我比较高兴的是近一年我把常期的网瘾给戒掉了,这玩意真是让我投入了太多精力,现在想一想当初的我简直就是在犯罪!不过偶尔放松心情,缓解压力还是不错的。不过今年我失眠比较难受,希望明年让我睡的早一些…

关于实践学习

 今年最让我扯淡的就是做一个Linux硬盘分区方案,明明很简单操作,我给大意了,后果导致我进几年来做的所有东西几乎都没了……编程方面学习的压力也挺大,可能是我脑子不太转圈…还有就是各种兼职,这个东西其实挺有意思的,时不时的周末兼职也让我学会很多东西。让我一个小白活生生的被迫成了老油条……

关于职业规划

 早就已经确定方向,回想小时候想把爱好当成工作,别提多开心,现在想一想我抬手就是给自己一巴掌,不!你不想!

好了,今天的熬夜点到为止, 2019 新的一年,祝大家乘风破浪!晚安。 Alt text

cmd笔记(长期更新)

最近从 linux 换到 windows 真是一言难尽下次发表磁盘阵列的时候再说这个事吧…
转到windows后安装mysql,又想起linux的终端了,但是windows没这玩意,cmd凑合一下吧,再温习一下cmd的一些命令…

查看

# 查看本地ip
ipconfig
# 检查Windows版本
winver
# 查看默认用户
net user
# 查看用户组
net localgroup

复粘删拷

# 复制文件
copy
# 移动文件
move
# 删除文件
del
# 拷贝文件
xcopy

创建文件与属性权限

# 创建文件
copy
# 创建共享文件夹
shrpubw

# 属性权限
attrib
    +r # 只读
    +h # 隐藏
    +a # 存档
    +s # 系统

重关注销

shutdown
    # 关机
    -s
    # 取消事件
    -a
    # 重启
    -r
    # 注销当前用户
    -i
    # [20] 定时设置默认值是20秒
    -t

网络安全

# 显示网络连接\路由表和网络接口信息
netstat - 
    netstat [选项]
        # 显示所有socket,包括正在的监听的
        -a
        # 每个一秒就重新显示一遍,直到用户中断它
        -c
        # 显示每个协议统计信息
        -s
        # 显示以太网统计信息,此选项可以与 -s 结合
        -e
        # 显示当前连接卸载状态
        -t
        # 显示 NetworkDirect 连接、侦听器和共享
        -x
        # 显示所有连接的TCP连接模板
        -y
        # 显示正在进行的工作
        -v

# 查看80端口占用情况
netstat -aon|findstr '80'
    TCP  0.0.0.0:80  0.0.0.0:0  LISTENING  6436
# 查找PID对应的进程
tasklist|findstr '6436'
    httpd.exe  6436 Services  0  12,040 K
# 结束名为 httpd.exe 的进程
taskkill /f /t im httpd.exe  

# 打开控制台
mmc
# DNS测试
ping []
# 注册表
regedit.exe
# 事件查看器
eventvwr
# 查看本地ip
ipconfig
# 任务管理器
taskmgr
# 系统组件服务
dcomcnfg
# 系统配置
msconfig.exe
# 设备管理器
devmgmt.msc
# 计算机管理
compmgmt.msc
# 本地服务设置
services.msc

系统维护

# 检查DirectX信息
dxdiag
# 垃圾整理
cleanmgr

功能

# 清除屏幕
cls
# 放大镜
magnify
# 启动计算器
calc
# 控制面板
control
# 鼠标属性
main.cpl
# 打开记事本
notepad
# 游戏控制器
joy.cpl
# 屏幕分辨率
desk.cpl
# 程序和功能
appwiz.cpl
# 打开资源管理器
explorer
# 共享文件夹管理器
fsmgmt.msc
# 木马捆绑工具,系统自带
iexpress
# 配置显示器和打印机中的色彩
colorcpl
# 远程桌面连接
mstsc

2019-06-03 更新

tskill

## 重启资源管理器
tskill explorer

php笔记(长期更新)

预定义变量

# 当前脚本所在的文档根目录(服务器VirtualHost定义)
DOCUMENBT_ROOT
# 获取当前域名
HTTP_HOST
# 获取当前页面地址 
PHP_SELF
# 获取完整url(包括?号后的参数)
REQUEST_URL
# 获得页面使用的请求方法
REQUEST_METHOD
# 当前php文件名
_FILE_
# 当前php文件中所在的行数
_LINE_
# PHP版本
PHP_VERSION
# 获得地址后的所有内容
REQUEST_URI

字符串函数

# 输出一个或多个字符串
echo
# 去除字符串首尾处的空白字符(或者其他字符)
trim
# 将一个字符串进行MD5算法加密
md5
# 将一个一维数组的值转化为字符串
implode
# 使用一个字符串分割另一个字符串
explode
# 将字符串解析成多.个变量
parse_str
# 使用另一个字符串填充字符串为指定长度
str_pad
# 重复一个字符串
str_repeat
# 子字符串替换
str_replace
# 随机打乱一个字符串
str_shuffle
# 将字符串转换为数组
str_split
# 获取字符串长度
strlen
# 从字符串中去除 HTML 和 PHP 标记
strip_tags
# 查找字符串首次出现的位置
strpos
# 查找指定字符在字符串中的最后一次出现
strrchr
# 计算指定字符串在目标字符串中最后一次出现的位置
strrpos
# 返回字符串的子串
substr
# 将字符串转化为小写
strtolower
# 将字符串转化为大写
strtoupper
# 反转字符串
strrev
# 指定文件进行MD5算法加密
md5_file
# 计算字符串的 sha1 散列值
sha1
# 以千位分隔符方式格式化一个数字
number_format
# 输出字符串
print
# 输出格式化字符串
printf

数组函数

# 新建一个数组
array
# 返回数组中所有的值
array_values
# 计算数组中的单元数目或对象中的属性个数
count
# 检查数组中是否存在某个值
in_array
# 将数组打乱
shuffle
# 将数组的内部指针指向第一个单元
reset
# 将数组的内部指针指向最后一个单元
end
# 将一个数组分割成多个
array_chunk
# 返回数组中指定的一列
array_column
# 创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值
array_combine
# 统计数组中所有的值出现的次数
array_count_values
# 用给定的值填充数组
array_fill
# 交换数组中的键和值
array_flip
# 检查给定的键名或索引是否存在于数组中
array_key_exists
# 返回数组中部分的或所有的键名
array_keys
# 合并一个或多个数组
array_merge
# 用值将数组填补到指定长度
array_pad
# 将数组最后一个单元弹出(出栈)
array_pop
# 从数组中随机取出一个或多个单元
array_rand
# 返回一个单元顺序相反的数组
array_reverse
# 在数组中搜索给定的值,如果成功则返回相应的键名
array_search
# 将数组开头的单元移出数组
array_shift
# 从数组中取出一段
array_slice
# 在数组开头插入一个或多个单元
array_unshift
# 对数组进行逆向排序并保持索引关系
arsort
# 对数组进行排序并保持索引关系
asort

GET 与 POST 的区别

GET在浏览器回退时是无害的,而POST会再次提交请求
GET产生的URL地址可以被Bookmark,而POST不可以
GET请求会被浏览器主动cache,而POST不会,除非手动设置
GET请求只能进行url编码,而POST支持多种编码方式
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
GET请求在URL中传送的参数是有长度限制的,而POST么有
对参数的数据类型,GET只接受ASCII字符,而POST没有限制
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
GET参数通过URL传递,POST放在Request body中

Ubuntu Server部署日记

 出于挂一些签到软件的原因我一直在用Windows Server2012,因为服务器配置较低图形界面操作卡的要死,干脆直接上纯命令操作系统的linux。 关于linux发行版我几乎都玩过。在选择linux服务器这块也没有太犹豫,我个人比较痴情Ubuntu,我个人用户对于服务器要求没有那么高,所以选择了 Ubuntu Server 16.04.1 LTS。不过我第一次部署纯命令操作系统,部署的过程中还是遇到一些小麻烦,不过办法总比困难多嘛!

root登录

首先,使用ssh命令在终端登录到远程主机

achuan@achuan-pc:~$ sudo ssh ubuntu@139.199.105.72
[sudo] achuan 的密码: 
ubuntu@139.199.105.72's password:
The authenticity of host '139.199.105.72 (139.199.105.72)' can't be established.
ECDSA Key fingerprint is SHA256:HNndgD6z/I2L8DzCA5nk9w9CEzHpW9WouGAmkMDR7LK.
Are you sure you want to continue connecting (yes/no)? yes
ubuntu@139.199.105.72's password:
Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-130-generic x86_64)

* Documentation:    https://help.ubuntu.com
* Management:       https://landscape.canonical.com
* Support:          https://ubuntu.com/advantage
New release '18.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

Last login: Mon Nov 5 16:38:39 2018 From 119.28.7.195
ubuntu@VM-22-96-ubuntu:~$

这时,登录时命令行出现了警告,它说这是一个新的地址,存在安全风险。简单了解一下之后,我选择面对风险 yes 顺利登入远程主机。
对我来说登入后第一件事莫过于设置root密码

$ sudo passwd root

安装 vim

# 习惯性的更新源...
$ sudo apt-get update
# 安装vim
$ sudo apt-get install vim-gtk
......
Do you want to continue [Y/n]? Y

命令行敲入 vi ,Tab一下,不出问题已经有 vim

配置vim

强迫症的我,简单的配置一下…

$ sudo vim /etc/vim/vimrc

文件内容里有这么一句:syntax on它的意思是“语法高亮”,别注释它!为了提升体验我们需要set一下

set nu          # 在左侧显示行号
set tabstop=4   # tab长度设置为4
set cursorline  # 覆盖文件时不备份
set ruler       # 在右下角显示状态栏
set autoindent  # 自动锁紧
set showmatch   # 高亮显示匹配的括号
# 编码设置
set fencs=utf=uft-8,ucs-bom,shift-jis,gb18030,gbk,gb2312,cp936
# 语言设置
set langmenu=zh_CN.UTF-8
set helplang=cn

安装apache

我们使用源安装apache

$ sudo apt-get install apache2

配置apache

启动apache的两种姿势

$ sudo /etc/init.d/apache2 start
# 或
$ sudo service apache2 star
# [start] [restart] [stop] [status]

推送文件到网站根目录

在这里我遇到一个问题

root@achuan-pc:/home/achuan/视频/ubuntu# scp -r htdocs ubuntu@139.199.105.72:/var/www
ubuntu@139.199.105.72's password: 
scp: /var/www/htdocs: Permission denied

用 scp 推送文件夹的时候出现了Permission denied
what the? 我被拒绝了?

想了想原来是网站的根目录/var/www没有写入权限,让我们chmod一下?

$ ssh ubuntu@139.199.105.72
......
$ sudo chmod 777 -R /var/www

已经解决了让我们再 scp 一下!

解决ssh自动断连

这个自动断连就让我很是难受,不管是利用终端还是客户端工具都会出现这个问题,经过了解原来是正常的…
使用ssh连接远程服务器隔段时间没有任何操作,客户端与服务器就会自动断连,解决办法如下:

$ sudo vim /etc/ssh/sshd_config
$ sudo service sshd reload

我们需要修改客户端或服务器端 /etc/ssh/sshd_config 配置文件,找到ClientAliveInterval,改成 60(分钟单位,默认0)意思的每一分钟向客户端发送一个消息,用于保持连接,

VirtualHost配置

关于ubuntu serverd的apache配置文件在/etc/apache2/apache.conf当apache启动时会自动读取这个文件的配置信息,而其他的一些配置文件,则是通过Include指令引入。奇怪的是我找不到httpd.conf,算了自己动手丰衣足食

# 进入apache文件夹 > 创建 httpd.conf
$ cd /etc/apache2/ && sudo vi httpd.conf
# 并写入以下配置内容

# 如果你的服务器有多个IP、不同的虚拟用户时,你可以更改它
<VirtualHost [IP]:[端口]>
...
</VirtualHost>

<VirtualHost *:80>
	ServerAdmin achuan@achuan.io         # 网站管理员邮箱,可填可不填
	DocumentRoot "/var/www/chenbtpig"    # 网站根目录
	ServerName chenchen1112.cn           # 域名
	<Directory "/var/www/chenbtpig">	 # 网站根目录权限设置
		Options Indexes FollowSymLinks   # 禁止显示Apache目录
		AllowOverride All				 # 允许重写apache默认配置
		Order allow,deny				 # 允许所有
		Allow from all
	</Directory>
	Errorlog "logs/chenchen1112.cn-error.log"   # 网站错误日志
	CustomLog "logs/chenchen1112.cn-access.log" common   # 网站访问日志
</VirtualHost>

<VirtualHost *:80>
	ServerAdmin achuan@achuan.io
	DocumentRoot "/var/www/code_rain"
	ServerName lmissyou.club
	<Directory "/var/www/code_rain">
		Options Indexes FollowSymLinks
		AllowOverride All
		Order allow,deny
		Allow from all
	</Directory>
	Errorlog "logs/lmissyou.club-errpr.log"
	CustomLog "logs/lmissyou.club-access.log" common
</VirtualHost>

# 打开apache配置文件并写入“Include httpd.conf”
$sudo vim /etc/apache2/apache2.conf

# 修改hosts /etc/hosts
$ sudo vi /etc/hosts
# 写入 IP与域名
# 139.199.105.72 www.chenchen.1112.cn

网站首页的优先级

#用于VirtualHost括号内,优先级从左往右依次降低
DirectoryIndex index.php index.html

错误日志

# 用于VirtualHost括号内,在apache配置目录应有logs文件夹,没有自己创建
Errorlog "logs/chenchen1112.cn-error.log"

访问日志

CustomLog "logs/chenchen1112.cn-access.log" common

错误页面的显示

ErrorDocument 404/missing.html

安装php5.6

# 添加PPA源
$ sudo add-apt-repository ppa:ondrej/php
# 若报错或没有发现命令则执行 $ sudo apt-get install python-software-properties
# 习惯性的更新源...
$ sudo apt-get update
# 安装php5.6
$ sudo apt-get install php5.6
# 验证版本
$ sudo php -v

apache2.conf文件,这个文件通过包含其他配置文件涵盖了所 让我们写个试试?

# 进入www并创建新文件index.php
$ cd /var/www && sudo vi index.php
# 写入到index.php	
<?php
phpinfo();
?>

配置php

ubuntu serverd的php配置文件在/etc/php/5.6/apache2/php.ini
近期发现php的时区是GMT(格林威治平时),而不是GMT+8(东八区)它们的显示时间会相差8个小时,这怎么行!

# 打开php.ini配置文件
$ sudo vi /etc/php/5.6/apache2/php.ini
# 找到 date.timezone 并赋值 PRC(中华人民共和国英文缩写) date.timezone = PRC

输入我的公网ip查看一下
Alt text

安装mysql

# 习惯性的更新源...
$ sudo apt-get update
# 安装mysql服务器与客户端,安装时会有两次交互,关于mysql密码的设置
$ sudo apt-get install mysql-server mysql-client

# 查看安装是否成功
$ sudo netstat -tap | grep mysql

root@VM-22-96-ubuntu:/etc/mysql/mysql.conf.d# sudo netstat -tap | grep mysql
tcp6       0      0 [::]:mysql              [::]:*                  LISTEN      7818/mysqld   

配置mysql

启动mysql的两种姿势

$ sudo /etc/init.d/mysql start
# 或
$ sudo service mysql star
# [start] [restart] [stop] [status]

远程连接mysql

ubuntu serverd的mysql配置文件在/etc/mysql/mysql.conf.d/mysqld.cnf

# 修改配置文件的端口绑定	
$ sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
# 注释掉 bind-adress = 127.0.0.1
# 进入数据库
$ mysql -u root -p
# 选择使用的数据库
mysql> use mysql;
# 修改host值(以通配符 % 增加主机/IP地址)当然你可以可以直接增加IP
mysql> update user set host = '%' where user = 'root';
# 让权限立即生效
mysql> flush privileges;
# 查看修改是否成功 **%         | root**说明远程连接已经开启 
mysql> select host, user from user;
+-----------+------------------+
| host      | user             |
+-----------+------------------+
| %         | root             |
| localhost | debian-sys-maint |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+


# 如下连接成功!

achuan@achuan-pc:/etc/apache2$ mysql -h 139.199.105.72 -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.7.24-0ubuntu0.16.04.1 (Ubuntu)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 

修改数据库编码

由于mysql默认编码为latin如果不修改的话中文会导致数据库乱码报错,所以我们要修改为utf8

# 打开my.cnf配置文件
$ sudo vi /etc/mysql/my.cnf
# 写入以下代码

[mysqld]
character-set-server = utf8
[client]
default-character-set = utf8
[mysql]
default-character-set = utf8

# 查看数据库编码
mysql> show variables like '%char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.01 sec)

安装phpmyadmin

不必要的情况下我还是喜欢可视化的操作mysql,开源的phpmyadmin就很不错!

# 习惯性的更新源...
$ sudo apt-get update
# 安装phpmyadmin
$ sudo apt-get install phpmyadmin
# 虽然我们安装好了,但是phpmyadmin基于php环境开发的,所以我们需要把它放到网站的根目录/var/www,我这里在网站的根目录下放了一个软连接。
$ sudo ln -s /usr/share/phpmyadmin /var/www

输入网址即可: https://localhost/phpmyadmin

这次部署Ubuntu Server体会不少,我真是越来越喜欢linux了,它真是一个非常有魅力的系统!哈哈哈,到这里吧,太晚了要回寝室睡觉了–
本篇文章如有写的不足之处,还请您多多指教!88!

解决ubuntu:E:无法获得锁(11:资源暂时不可用)

最近学习用到了php手册,平常我都是在网页查看,图个方便于是就下载了KchmViewer(开源的CHM 阅读器)不过效果不太理想,今天想卸载了它,结果出段小插曲……

Alt text

what the?
好吧,遇到问题解决问题

刚开始我是以为软件没有完全关闭就打开了终端……

# 打开终端列出进程,找含有 apt-get 进程,然后 sudo kill PID
$ ps aux 

我以为ojbk ?不过这并没有什么用(啪)……

这有什么难的?so easy 解决方式如下:

# 首先强制解锁
$ sudo rm /var/cache/apt/archives/lock
# 然后找到错误信息里“无法获得锁的地址”并 rm
$ sudo rm /var/lib/dpkg/lock-frontend

卸载 KchmViewer 并确认卸载(Y)

$ apt-get remove kchmviewer 

Alt text

dpkg 查一下 kchmviewer 是否存在

$ dpkg -s kchmviewer

Alt text

(转)论胡编乱造的写作技巧

本文转自廖雪峰的博客


编,我看你编......

今天给正在上小学二年级的女儿听写语文课文的词语时,发现有一篇课文讲的是爱迪生救妈妈的故事,大意就是爱迪生7岁那年,妈妈得了阑尾炎,医生在他家里准备给妈妈做手术,因为油灯的灯光太暗做不了,于是爱迪生想出了一个好办法:他找来很多镜子,把光聚在一起,让医生顺利地进行了手术。

这个故事的意义是说爱迪生从小聪明过人,肯动脑筋,不过我越读感觉这个故事越假,在家里做阑尾手术这医生胆子也够大的。于是本着实事求是的精神,上网考证了一番,发现这个故事漏洞百出。

首先,根据维基百科的记载,爱迪生出生于1847年,7岁那年是1854年,这么早的年份,到底能不能做阑尾炎手术呢?

答案是:不能。

因为根据医学资料记载,历史上最早使用“阑尾炎”的医生是美国人菲兹,他于1843年出生,也就是比爱迪生大4岁,爱迪生7岁那年他11岁,青春期才刚开始,呵呵。

菲兹提出应该用手术切除阑尾是在1886年,那一年爱迪生39岁,菲兹43岁。而且,菲兹不是外科医生,所以,第一例真正成功的阑尾手术是1887年由费城外科医生毛顿做的。

由于爱迪生没有发明过时光穿梭机,因此我们断定,该故事完全属于编造。

我们总是说文章源于生活而高于生活,对生活进行一定程度的“加工”是可以的,但是加工前作者最好能有一点点基本的科学知识。如果硬要胡编乱造,我个人有两点心得体会:

一种是写“很久很久以前,有一头大怪兽……”,神话故事,随便编。

另一种是写“公元3017年,抵抗外星人的战争已经进入了第9个年头……”,科幻故事,也随便编。

除此之外,故事里提到的人物、时间、事件,最好能用搜索引擎简单验证一下。

不过值得欣慰的是,作者没有写电灯而是油灯,说明作者对爱迪生发明了电灯还是略知一二的:7岁的爱迪生尚未发明电灯。

但我还是希望人教出版社能把这篇编造的课文从语文书里面删了,一是因为欺骗小孩子总是不好的,二是这篇文章暴露了人教社编辑的知识水平实在有限,出于面子的原因,也应该在再版时尽快删除。

更换了域名、邮箱并修复了移动端bug

 更换域名这件事我想了好久,原使用Github的三级域名但它太长,本来想换achuan.com不过早就被注册了,卖方叫价太高了,没有5000谈判都碰不着。ahuan.cn也是早已注册,人家买了域名都不解析挂着玩,也联系过人家,不过人家不回消息。.net .me 都没了。偶然我了解到“.io”,它是一个国别域名,同时,它还可被理解为[inputoutput]即“输入输出接口”之意。“io”看着是是不是有点像二进制的“0 与 1”!感觉很有技术内涵,而且近几年.io在区块链方面比较火,我感觉用来搭建技术博客也是不错的选择。
 在选择域名商我也是非常纠结,.io后缀国内域名商几乎是没有的,主要是不能备案,毕竟这是国别后域名,估计不会开放备案。国内不行那就国外吧,怎么要有免费的隐私保护、网站访问速度正常、支付方式、域名价,格至于客服响应、稳定性那都是必须的。
dynadot是我一个想到的,这家伙有大量的骚东西,特殊后缀域名.love .blog .wiki 等…说一下它的一些服务吧,购买域名即送终身隐私保护,但我也不是什么明星大腕,隐私保护自然用不到,dynadot所有域名都赠送终身免费的隐私保护,dynadot网站自带各国域名很多种,自然有中文,而且在北京设立有分公司,国内网站速度打开正常并且支持支付宝,解析稳定这都不用说,虽然这家口碑不是理想中的那么好,但是跑路估计是不存在的,至于转出暂时我没试过不知道,唯一就是感觉这家.io价格有点高 $29。
Namesilo也不错,号称全球最便宜,收费透明且支付支付宝,不过国内访问注册都过不了,翻墙就不至于了,不多考虑。Namecheap呢价格上不算便宜,但是各方面做的不错,不限于速度、稳定性、免费的SSL、客服响应等等、但是它不支持支付宝。至于GoDaddy,虽然最为业界的NO 1,但是在网上查看那些相关新闻评论,看我的我不想买了都,虽然它有中文、支持支付宝、国内访问还算凑合。但是pass。
 考虑了半天想想还是用“挑兵挑将”来选择吧 天选之商 dynadot 胜出….下了订单$29,吃土吧,快乐并痛苦着…买后即用,不用备案是真爽,进去NDS设置一下,ping achuanya.github.io获取IP,然后添加主域名A记录 185.199.109.153,子域名 www A记录随主域名。更换了域名,邮箱也改一下吧,网易企业邮箱免费,添加 MX记录 mx.ym.163.com 优先级10(邮箱:achuan@achuan.io),访问 achuan.io 完全ojbk。
 除了域名\邮箱也没闲着,修改了博客移动端的一些bug:

  • 输入域名后自动进入主页且不能退回到文章栏。

 移动端增加了一项功能:

  • 增加返回菜单栏按钮。

开源分布式管理控制系统——Git笔记

Git的诞生

 故事是自2002年开始,林纳斯·托瓦兹决定使用BitKeeper作为Linux内核版本控制系统,用于维护代码。你想想BitKeeper是专有软件,人家靠这玩意挣钱,这个决定社群中长期遭受质疑。不过还是有人主张使用开源软件。我dalao(林纳斯·托瓦兹)何尝没有想过(如Monotone),但是这些软件都存在一些问题,特别是性能不佳。现成的方案,如CVS的架构,不过dalao并不喜欢连同痛批了一顿。
 2005年的时候有个叫安德鲁·垂鸠破解了BitKeeper(这么做的不止他一个),BitMover公司决定收回Linux社区的免费使用权。事后双方公司进行过磋商,结果可想而知。事后dalao以十天的时间,用C撸出来一个分布式版本控制系统,它就是Git的第一个版本!于2005年以GPL发布。至于这个名字嘛,dalao自嘲地取了这个名字“Git”,该词源自英国俚语,意思大约是“混账”。不过Git刚面世的时候,世界上开源社群反对声音不少,最大的理由是Git太艰涩难懂?(博主:一个程序员难道不该有钻研精神吗?)
 2016年IT界报道,Git统治编程社区11年后,BitKeeper宣布开源。挑战Linux内核开发者?dalao反手一掏Git灭掉BitMover。(这段让我看的热血沸腾,抽颗烟压压惊)

在学习之前我门先学习一下四个专业术语

Alt text

  • Workspace:工作区
  • lndex/Stage:暂存区(又名索引)
  • Repository:仓库区(又名本地仓库、储存库)
  • Remote:远程仓库 开源分布式管理控制系统——Git笔记 开源分布式管理控制系统——Git笔记是工作区。
    开源分布式管理控制系统——Git笔记态。
    开源分布式管理控制系统——Git笔记库)。
    远程仓库:这个很简单但我却不知道怎么表达…可以说成云端仓库吧。

主要功能

 Git主要用于Linux内核开发的版本控制系统,与CVSSubversion这一类集中式版本控制系统的不同采用了分布式版本。它没有服务器端,就可以运行版本控制,这使得源码的发布与修改极其方便。git速度也是很给力。

实现原理

 Git和其他版本控制系统(如CVS)有不少的差别,Git本身关心文件的整体性是否有改变,但多数的版本控制系统如CVSSubversion系统则在乎文件内容的差异。
我觉得Git更像一个文件系统,直接在本机上获取数据,不必连线到主机端获取数据。 每个开发者都可有全部开发历史的本地副本,changes从这种本地repository复制给其他开发者。这些changes作为新增的分支被导入,可以与本地分支合并。
Git是用C语言开发的,以追求最高的性能。Git自动完成垃圾回收(也可以用 git gc –prune 调用)。
Git服务器典型的TCP监听,端口为9418。

Git的安装

 Git平台支持有POSIXWindowsOS X,不过今天我们重点讲Linux。
Git安装有许多种,主要分为两种:通过编译源代码、平台预编译安装包,源代码安装至少可以安装最新版本,不过Git慢慢壮大,不会出现安装包版本落后问题,这里也就不说了。
  在装软件之前我都会习惯的更新源

$ sudo apt-get update

基于二进制安装Git

$ sudo yum install git

ubuntu发行版

$ sudo apt-get install  git

查看Git当前版本

$ git version

初始化仓库

# 初始化当前目录为仓库
$ git init

克隆到本地仓库

# 第二个参数克隆到指定目录,可带可不带
$ git clone [项目链接] [指定本地目录]
# 帮助
$ git clone -h

增加/删除文件

# 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件
$ git add .
# 提交新文件(new)和已被删除(deleted)文件,不包括新文件
$ git add -u
# 提交所有变化。cunz
$ git add -A
# 提交当前目录.png后缀文件
$ git add *png
# 帮助
$ git add -h

检查工作目录/暂存区状态

# 检查工作目录与暂存区的状态
$ git status
# 只列出已被Git管理且被修改没提交的文件
$ git status -uno
# 帮助
$ git status -h

获得远程仓库权限

# Git是分布式管理控制系统,基于SSH,所以我们要生成SSH公钥,然后它让输入的话就 Enter
$ ssh-keygen

# 之后切换到主目录下的 ~/.ssh 并找到 .pub 且复制文件内容(Windows在:C:/Users/当前用户名/.ssh/id_rsa.pub)
$ cd ~/.ssh && ls
authorized_keys  id_rsa  id_rsa.pub  known_hosts

# 在Github页面 找到 Settigs -> SSH and GPG keys ->New SSH key 然后给这个秘钥取个名字,然后把Key填入并保存

# 当新文件添加到暂存区后,我们现在就准备创建第一个提交与注释了,不过在这之前我们要绑定Git账户,之后进行push操作需要填写账号\密码
$ git config --global user.email "achuan@achuan.io"
$ git config --global user.name "achuanya"

提交代码并注释

$ git commit -m "这是一个测试"
# 参数:
        # 自动检测修改文件
        -a
        # # 提交
        -m
# 帮助。
$ git commit -h

查看日志详情

# 显示当前分支的版本历史
$ git log
# 显示commit历史,以及每次commit发生变化的文件
$ git log --stat
# 搜索提交历史,根据关键词
$ git log -S [key Word]
# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
# 显示过去的5次提交
$ git log -5 --pretty --oneline
# 帮助
$ git log -h

提交到远程仓库

# 提交到远程仓库,master即为分支
$ git push origin master
# 强行提交
$ git push -u origin master
# 上传本地指定分支到远程仓库(分支操作后面会说到)
$ git push [remote] [branch]
# 推送所有分支到远程仓库
$ git push [remote] --all
# 强行推送当前指定分支到远程仓库(即使有冲突)
$ git push [remote] --force
# 帮助
$ git push -h

移动、重命名与删除文件

# 移动与重命名道理是一样的
$ git mv

# 继linux命令一样。
$ git rm

Alt text

撤销修改/切换分支

 git checkout 主要用于撤销修改或切换分支,它是一条较为常用的的命令,同时也很危险,因为这条命令会重新工作区!

# 恢复修改过的指定文件到工作区
$ git checkout [branch Name]
# 恢复暂存区所有文件到工作区
$ git checkout .
# 新建一个分支,并切换到该分支(切换分支不带参数 -b)
$ git checkout -b [name]
# 切换到上一个分支
$ git checkout -

管理分支

 Git分支,好比一包零食,你可以选择和其他小伙伴一起吃一包,也可以自己吃一包,他们吃他们的,你吃你的相互之间没有影响,作为这包零食的主人,你也可以随时可以让两包零食放在一起(合并)。 ​

# 现在让我们创建一包零食,不不不...一个新分支
$ git branch [name]
# 查看本地所有分支(当前分支前有个 * 号)
$ git branch
# 查看远程分支
$ git branch -r
# 查看本地和远程所有分支
$ git branch -a
# 重命名分支
$ git branch -m [name] [modify Name]
# 删除分支( -D 强行删除)
$ git branch -d [name]
注:删除远程分支需要 git push 操作
$ git push origin :[name]
######################### 合并分支

# 把现有demo分支合并到master上
$ git metge [branch Name]

root@achuan-pc:/home/github/demo# git branch
demo
* master
root@achuan-pc:/home/github/demo# git merge demo
Updating d46f35e..b17d20e
Fast-forward
README.txt | 1 +
1 file changed, 1 insertion(+)

# 注意上面的合并信息 fasht-forward 意思是这次合并是快进模式,也就是直接吧master指向demo的当前提交,所以合并速度非常快。 

Git常用命令

 上面说的都是一些基本操作,下面也是一些常用命令,出于个人用途可能存在有些命令不全面,不过我往后学习中会进行添加,还请见谅。

查看信息

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn
# 显示指定文件是什么人在什么时间修改过
$ git blame [file]
# 显示暂存区和工作区的差异
$ git diff
# 显示暂存区和上一个commit的差异
$ git diff --cached [file]
# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD
# 显示两次提交之间的差异
$ git diff [first Branch]...[second Branch]
# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"
# 显示某次提交的元数据和内容变化
$ git show [commit]
# 显示某次提交发生变化的文件
$ git show --name-only [commit]
# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]
# 显示当前分支的最近几次提交
$ git reflog

远程仓库的一些操作

# 下载远程仓库的所有变动
$ git fetch [remore Name]
# 跟踪存储库,不带参数,列出已存在的远程分支。
$ git remote
# 显示所有远程仓库
$ git remote -v
# 显示某个远程仓库的信息
$ gitremote shou [remote Name]
# 添加一个新的远程仓库,并命名
$ git remote add [shortname][url]
# 取远程仓库与本地分支合并  
$ git pull [remote] [branch]

拓展知识

克隆下载某项目中的单个文件(文件夹)

$ wget https://github.com/acversionuanya/achuanya.github.io/blob/master/README.mdjinxing

Github Chrome插件

Octotree,很直观的查看不同级文件夹,也方便进行文件跳转操作。
Octo Mate,增强你Githubt体验,它主要功能有四点:一键以.zip格式下载文件、 一键打开项目的Github Pages、显示仓库大小、显示未读消息数。

文件状态

未跟踪(untrack):新增加文件。
已修改(modified):已修改但为写入到暂存区。
已暂存(staged):已经添加到暂存区。
已提交(committed):已经添加到远程仓库。

Git专业术语中英文对照表

Alt text


 ok写完了!通过这段时间的学习,关于Git命令我用到的差不多就这么多,可能存在有些命令不全面,不过我往后学习中会进行添加,还请见谅。我现在真高兴当初出写博客这个决定,它让我每天学习都很有动力,有目标。最主要的是能把自己学会的知识分享给大家,也能让这个知识点记的更加牢固。。再过半年就要入职场了,压力也是不小。不过不能让压力阻止我向前跨步!下次文章关于“LAMP/LNMP架构”的相关内容。

2019-04-28 更新

# 撤销所有
git reset HEAD -- .

# 撤销指定文件
git reset HEAD -- filename [文件名]

# 将文件从缓存中删除
git rm -cached filepath [文件名]

linux命令笔记(长期更新)

今天在写一些刚最近学到linux命令,看到笔记本上记得有点乱重写整理了一下。接触了linux也有近大半年了,越来越离不开它了,windows也是偶尔开一下虚拟机,接触linux这段时间也是受益匪浅。

使用windows的时候虽然控制的住网瘾,但是偶尔也想打一把,自从独装了linux直接一棒子打死…挺好的…我也非常喜欢linux命令,它提升了我一些单词量。还让我接触了开源的概念、它的历史,这些我都非常喜欢。

咳咳,关于开源过几天再说吧,进入正题,这些是记的笔记,linux命令虽多,但每个人玩linux的目的固然不同,常用的命令也有差异,当然你也用到它时Google一下。

cd

切换目录,可以是绝对路径,也可以是相对路径

# 切换到目录/achuan/achuan
cd /home/achuan

# 切换到当前目录下的achuan目录中
cd ./achuan

# 切换到上层目录中的chuan目录中,".."表示上一层目录
cd ../achuan

# 切换到home
cd ~

# 切换到目录的上两级
cd ../..

ls

查看目录与文件,list之意

# 列出长数据串,包含文件的属性与权限数据等
-l
# 列出全部的文件,连同隐藏文件(开头为.的文件)一起列出来(常用)
-a
# 仅列出目录本身,而不是列出目录的文件数据
-d
# 将文件容量以较易读的方式(GB,kB等)列出来
-h
# 连同子目录的内容一起列出(递归列出),等于该目录下的所有文件都会显示出来
-R

grep

强大的文本搜索命令,它能使用正则表达式搜索

grep
# 将binary文件以text文件的方式查找数据
-a
# 计算找到‘查找字符串’的次数
-c
# 忽略大小写的区别,即把大小写视为相同
-i
# 反向选择,即显示出没有'查找字符串'内容的那一行
-v
例如:
# 取出文件/etc/man.config中包含MANPATH的行,并把找到的关键字加上颜色
grep --color=auto 'MANPATH' /etc/man.config
# 把ls -l的输出中包含字母file(不区分大小写)的内容输出
ls -l | grep -i file

find

基于查找文件命令,相对而言,使用复杂,参数多

# n为数字,意思为在n天之前的“一天内”被更改过的文件
-mtime n
# 列出在n天之前(不含n天本身)被更改过的文件名
-mtime +n
# 列出在n天之内(含n天本身)被更改过的文件名
-mtime -n
# 列出比file还要新的文件名
-newer file
# 例如:
# 在当前目录下查找今天之内有改动的文件
find /root -mtime 0


# 与用户或用户组名有关的参数

# 列出文件所有者为name的文件
-user name
# 列出文件所属用户组为name的文件
-group name
# 列出文件所有者为用户ID为n的文件
-uid n
# 列出文件所属用户组为用户组ID为n的文件
-gid n

与文件权限及名称有关的参数:

# 找出文件名为filename的文件
-name filename
# 找出比SIZE还要大(+)或小(-)的文件
-size [+-]SIZ

# 查找文件的类型为TYPE的文件,TYPE的值主要有:一般文件(f)、设备文件(b---术、c)
# 目录(d)、连接文件(l)、socket(s)、FIFO管道文件(p)
-tpye TYPE

cp

用于复制文件,还可以把多个文件一次性复制到一个目录下

# 将文件的特性一起复制
-a
# 连同文件的属性一起复制,而非使用默认方式,与-a相似(常用于备份)
-p
# 若目标文件已经存在时,在覆盖时会先询问操作的进行
-i
# 递归持续复制,用于目录的复制行为
-r
# 目标文件与源文件有差异时才会复制
-u
# 例如:
cp -a aa aab   //连同文件的所有特性把文件aa复制成文件aab
Cp aa aab aabb achuan   //把文件aa、aab、aabb复制到目录achuan中

mv

用于移动文件、目录或更名,move之意

# force强制的意思,如果目标文件已经存在,不会询问而直接覆盖
-f
# 若目标文件已经存在,就会询问是否覆盖
-i
# 若目标文件已经存在,且比目标文件新,才会更新
-u

# 注:该命令可以把一个文件或多个文件一次移动一个文件夹中,但是最后一个目标文件一定要是“目录”。
# 例如:
mv aa aab aabb achuan   //把文件aa、aab、aabb移动到目录achuan中
mv aa aab   //把文件aa重命名为aab

rm

用于删除文件或目录,remove之间(数据无价,操作需谨慎!!!)

# 就是force的意思,忽略不存在的文件,不会出现警告消息
-f
# 互动模式,在删除前会询问用户是否操作
-i
# 递归删除,最常用于目录删除,它是一个非常危险的参数
-r
# 例如:
rm -i achuan   //删除文件achuan,在删除之前会询问是否进行该操作
rm -fr achuan  //强制删除目录achuan中的所有文件

ps

  • 用于某个时间点的进程运行情况选取下来并输出,process之意

      # 所有的进程均显示出来
      -A
      # 不与terminal有关的所有进程
      -a
      # 有效用户的相关进程
      -u
      # 一般与a参数一起使用,可列出较完整的信息
      -x
      # 较长,较详细地将PID的信息列出
      -l
    
      # 用到不多,记住一般使用命令参数搭配即可
      # 例如:`
      # 查看系统所有的进程数据
      ps aux
      # 查看不与terminal有关的所有进程
      ps ax
      # 查看系统所有的进程数据
      ps -lA
      #  查看连同一部分进程树状态
      ps axjf
    

kill

用于向某个工作或者是某个PID(数字)传送一个信号,它通常与ps和jobs命令一起使用,# 例如:

kill -signal PID

signal的常用参数如下:
注:最前面的数字为信号的代号,使用时可以用代号代替相应的信号。

# 启动被终止的进程
1:SIGHUP
# 相当于输入ctrl+c,中断一个程序的进行
2:SIGINT
# 强制中断一个进程的进行
9:SIGKILL
# 以正常的结束进程方式来终止进程
15:SIGTERM
# 相当于输入ctrl+z,暂停一个进程的进行
17:SIGSTOP

# 例如:
# 以正常的结束进程方式来终于第一个后台工作,可用jobs命令查看后台中的第一个工作进程
kill -SIGTERM %1 
# 重新改动进程ID为PID的进程,PID可用ps命令通过管道命令加上grep命令进行筛选获得
kill -SIGHUP PID

file

用于判断file命令后的文件的基本数据,因为在linux下文件的类型并不是以后缀分的,这个命令显得非常有用,语法so easy

file filename
# 例如:
file ./test

tar

命令用于对文件进行打包,默认情况并不会压缩,如果指定相应的参数,还会调用相应的压缩程序(gzip和bzip等)进行压缩和解压

# 新建打包文件
-c
# 查看打包文件的内容含有哪些文件名
-t
# 解打包或解压缩的功能,可以搭配-C(大写)指定解压的目录,注意-c,-t,-x不能同时出现在同一条命令中
-x
# 通过bzip2的支持进行压缩/解压缩
-j
# 通过gzip的支持进行压缩/解压缩
-z
# 在压缩/解压缩过程中,将正在处理的文件名显示出来
-v
# filename为要处理的文件
-f filename
# 指定压缩/解压缩的目录achuan
-C achuan

命令有点多哈,但是通常我们只需要记住下面三条命令即可: # 压缩 tar -jcv -f filename.tar.bz2 要被处理的文件或目录名称 # 查询 tar -jtv -f filename.tar.bz2 # 解压 tar -jxv -f filename.tar.bz2 -C 欲解压缩的目录

chgrp

用于改变文件所属用户组

chgrp [-R] dirname/filename
-R   //进行递归的持续对所有文件和子目录更改
# 例如:
chgrp users -R ./achuan   //递归地把achuan目录下中的所有文件和子目录下所有文件的用户组修改为users

chmod

用于改变文件或目录的权限,非常重要的命令

# 当发生改变时,报告处理信息
-c
# 错误信息不输出
-f
# 进行递归的持续对所有文件和子目录更改
-R
# 运行时显示详细处
-v
# 读取权限
-4或r
# 写入权限
-2或w
# 执行或切换权限
-1或x
# 不具任何权限
-0或-
# 特殊权限
-s

注:该命令有两种用法,包含字母和操作符表达式的文字设定法;另一种是包含数字的数字设定法,在此我只说数字设定法。

  • 说数字设定法 0没有权限,1执行权限,2写权限,4读权限,然后将其相加。所以数字属性的格式应为3个从0到7的八进制数

    例如:
    如果想让某个文件的属主有“读/写”二种权限,需要把4(可读)+2(可写)=6(读/写)。 chmod 777 -R achuan

vim

vim命令要用于文本编辑,相比vim的话题不亚于,哪个编程语言更好…不过编辑器,在我看来不过是用哪个会提高我的工作效率,这因人而异。vim里面有很多好用的命令,以后我会单独说它。

gcc

前段时间学了点C才了解的,它主要用于编译,对于开发C语言的人来说,这是非常重要的命令

# output之意,用于指定生成一个可执行文件的文件名
-o
# 用于把源文件生成目标文件(.o),并阻止编译器创建一个完整的程序
-c
# 增加编译时搜索头文件的路径
-I
# 增加编译时搜索静态连接库的路径
-L
# 把源文件生成汇编代码文件
-S
# 表示标准库的目录中名为libm.a的函数库
-lm
# 连接NPTL实现的线程库
-lpthread
# 用于指定把使用的C语言的版本
-std=

例如:
# 把源文件demo1.c按照c99标准编译成可执行程序demo1
gcc -o demo1 demo1.c -lm -std=c99
# 把源文件demo1.c转换为相应的汇编程序源文件demo1.s
gcc -S demo1.c

shutdown
# 关机、重启命令,经常使用的命令

# 重启
-r
# 杀死所有运行中的程序后自动关机
-n
# 关机时,不进行磁盘检测
-f
# 关机时,进行磁盘检测
-c
# 例如:
# 立即重启
shutdown -r 、now
# 立即关机
shutdown now
# 5分钟后关机
shutdown +5 now

init是所有进程的祖先,它们的进程号始为1.所以发送TERM信号给init会终止所有的用户进程,守护进程等,shutdown就是使用这种机制,init定义了8个运行级别(runleve),inti 0 关机,init 1 重启……关于init就到这了。就不长篇大论了。

暂且到这里,欲知后续如何,请看下回分解

2019-2-15 更新

scp
# 多用于系统文件之间的复制,scp 是 secure copy 的缩写,中文就是"安全复制",  
scp是linux系统下基于ssh协议进行的远程文件

# 强制scp命令使用协议ssh1
-1
# 强制scp命令使用协议ssh2
-2
# 强制scp命令只使用IPv4寻址
-4
# 强制scp命令只使用IPv6寻址
-6
# 使用批处理模式(传输过程中不询问传输口令或短语)
-B
# 允许压缩(将-C标志传递给ssh,从而打开压缩功能)
-C
# 保留原文件的修改时间,访问时间和访问权限
-p
# 不显示传输进度条
-q
# 递归复制整个目录
-r
# 详细方式显示输出。scp和ssh(1)会显示出整个过程的调试信息。这些信息用于调试连接,验证和配置问题
-v
# Ssh_option:如果习惯于使用ssh_config(5)中的参数传递方式
-o
# port:注意是大写的P,port是指定数据传输用到的端口号
-P   
# 例如:
# windows传输到linux,[参数][文件][用户名][ip][地址]
scp -r - demo.text ubuntu@139.199.105.72:/var/www

面向对象(Object Oriented)学习笔记(一)

一. 面向对象(OOP)的发展历程

1. OOP的诞生

Simulaq面向对象技术最早是在编程语言Simual中提出,1967年2月20日,在挪威奥斯陆郊外的小镇莉沙布举行的IFIP TC-2 工作会议上,挪威科学家Ole-Johan Dahl和Kristen Nygaard正式发布了Simula 67语言。Simula 67,是面向对象的开山祖师,它引入了所有后来面向对象程序设计语言所遵循的基础概念:对象、类、继承,但它的实现并不是很完整。后来Smalltalk的诞生,第二个面向对象的程序语言的出现,而且是第一个完整实现了面向对象技术的语言,和第一个真正的集成开发环境(IED)。

二. 面向对象(OOP)的概念

OOP达到了软件工程的三个目标

  • 重用性灵活性扩展性

三. OOP三大特征

  • 封装(Encapsulation):对外部不可见
  • 继承(Inheritance):扩展类的功能
  • 多态(Polymorphism):对象的重载、对象的多态性

四. 类与对象

  • 类是对某一类事物的描述,是抽象的、概念上实际存在的该类事物的每个个体,因而也称实例(instance),换种方法解释类就像一个具体的事物,一个对象,用编程代码来描述出来,
  • 对象,简单的来说对象是可以使用的,而类只是对这件事物进行描述,它不能使用。

五. 类与对象的关系

这个我表达能力不太好哈…这样说吧,类就好比工厂车间的模具,而对象就是模具所产生的事物,一个模具是可以产生很多对象! 也就是说对象是类的实例化

六. 类的声明与对象的创建及使用

好了我们来了解一下怎么找对象,…不..创建对象..

1. 类的声明

class 类名称{
    // 这里成员属性修饰还有好几种下次一次详解
    // 声明成员变量(属性)
    数据类型 属性...

    // public 表示公有
    public 返回值的数据类型 方法名称 (参数1参数2){  // 定义方法
        // 程序语句..
        // return 等表达式
    }
}

2. 对象的创建及使用:

// 实例化对象,有参数别忘了加括号!
$定义对象名称 = new 类名([参数列表]);

  • 对象中成员的访问: // 输出对象的属性
    $对象名 -> 成员属性 = 赋值;

// 调用对象的方法:
$对象名 -> 成员方法(参数);
// 成员属性中还分有静态属性, 它是不能实例化的,往后我会补充

        <?php
        /*---------------------------------*\
            对象实例化
            //实例化指OOP编程中,类创建对象的过程
        \*---------------------------------*/
        class phone{
            # 成员属性
            public $width;
            public $height;
            public $size;

            # 成员方法
            public function call($name){
                echo "正在给{$name}打电话";}

            public function message($name){
                echo "正在给{$name}发短信";}
            public function play(){
                echo "正在玩游戏!";}

            public function info(){
                $this -> play();
                return "<br> 手机的宽度;{$this -> width},<br> 手机的高度;{$this -> height}";}
        }

        # 实例化对象
        $phone = new phone();
        $phone -> width = "5CM";
        $phone -> height = "10CM";

        //$phone -> width = "5CM"; //对象对成员属性的赋值(引用变量时 $ 符可以不写)
        //echo $phone -> width; //与对象取得成员属性存的值
        //$phone1 = new phone();
        //$phone1 -> height = "10CM";
        //$height = $phone1 -> height;
        //echo $height; 
        //echo $phone1 -> height;

        //$phone1 -> aaaa = "AAAA";font
        //var_dump("<br>",$phone1);
        $phone -> call("!list");
        $phone -> call("<br> tom");
        echo "<br>";
        $phone -> play();
        echo $phone -> info();

        ?>

呼~ 第一章写完了,我也是近期刚学OOP,可能哪个方面写的并不是很好 嘿嘿,如有写的不足之处,还请您在下方留言!

致简写作神器——Markdown

1. Markdown?

Markdown是什么?
Markdown 是由 John Gruber 在2004年创建的「轻量级标记语言」,至今它已经有14岁了!

Markdown最初的定义纯文本语法
设计理念就是易写、易读。 它那么的纯洁、致简、美妙感觉词汇已经不能形容Markdown的美。试着写一写,你也会爱上它。

2. Markdown 它有哪些优点?

前文说了它的定义与设计理念,易写、易读,无非就是Markdown最大的特点。

一. 所谓易写?

这里说的易写,是指写作的过程。因为它语法的原因且编辑器实时预览的情况下,差不多全程键盘,基本脱离对鼠标的依赖。这样就可以集中精力放在文章上。因为它语法简单,所以书写错误易发现。

二. 所谓易读?

所谓易读,可不要理解为排版之后呈现出来的结果,这里的易读是指源代码文件(也就是以 .md;.markdown;.mdown 后缀名的文件)它不会像写Html时那样,满屏幕都是密密麻麻的与选择器...标签>

3. Markdown的优势与局限

我们知道Markdown只是一个,相比xml、html或Word、Pages这类,Markdown在排版的功能上显的弱一点。轻量级标记语言>

一. Markdown无法对段落进行灵活处理

在word中,像文本框,图片你可以随意调整它的位置。但Markdown不行,相比Markdown只能线性的对文字进行排版。

二. Markdown对非纯文本元素的排版能力很差

一篇文章,如果只有文字的话,总有感觉有点枯燥枯燥,但是Markdown限于纯文本格式,Markdown几乎做不到像Word那样对图片灵活的调整位置。

三. Markdown专注你的文字内容,而不是排版

Markdown不像Word那样花哨,应为语法的缘故吧,感觉它是那么的纯洁,做纯文字处理很棒的,我每次使用Markdown写文章时,有种很美妙的感觉…

4. Markdown语法与编辑器

Markdown语法的目标是:成为一种使用网络的书写语言
毕竟我们的易写、易读是有目共睹的,立志做书写语言我们是认真的!

Retext的安装

日常我都是用Ubuntu18.04LTS,Markdown编辑器我用的是ReText,不过windows、Mac也有很多优秀支持Markdown的编辑器,如MarkdownPad、Sublime Text3、Marxico等等…在这里我就不一一介绍了。

1. 安装

$ sudo apt-get install retext

# 也可以到官网下载,有deb包。

2. 配置

# 添加数学公式的支持
$ sudo apt-get install libjs-mathjax

然后在”extensions.txt“中添加”mathjax“(没有这个文件创建一个) `

# 添加:mathjax
$ vim .config/markdown-extensions.txt

ok! 如果需要网页在线编辑器MaHua 在线markdown编辑器

语法比较简单,就不一一说了[Markdown 语法手册 (完整整理版)][4]

(转)如何学习开源项目

本文转自华为章宇的博客


学习各种开源项目,已经成为很多朋友不可回避的工作内容了。笔者本人也是如此。在接触并学习了若干个开源项目之后,笔者试图对自己工作过程中的若干体会加以总结,以期对一些希望借鉴的朋友有所裨益。

需要说明的是,笔者本人接触的开源项目大多属于计算机系统领域,例如Linux kernel,KVM,QEMU,OpenStack等。因此,此处介绍的经验必定也有些局限。请读者们自行分辨,区别对待。

学习分层和目标管理

对于一个开源项目,可以将与之相关的各种知识和技能的学习大致划分为如下五个层次:

第一层次:了解项目的基本概念、基本用途、逻辑结构、基本原理、产生背景、应用场景等基本知识。

这个层次的基本定位其实就是“科普”。如果对于一个项目只需要有些基本了解,且短期内并不需要上手进行实际技术工作,则学习到这个层次也就可以先应付一下了。

第二层次:掌握项目的基本安装流程和使用方法。

这个层次的基本定位是“入门”,以便对这个项目获得直观认识,对其安装和使用获得亲身体验。如果只是需要以as-is方式使用这个项目,则初步学习到这个层次即可。

第三层次:了解代码的组织,找到各个主要逻辑/功能模块与代码文件之间的对应关系,通过代码分析走通几个关键的、有代表性的执行流程。

这个层次的基本定位是“深入”,开始理解这个项目的实际实现,能够真正将项目的功能、工作原理和代码实现对应起来,获得对这个项目工作过程的直观认识。这个层次是学习开源项目代码的真正开始。如果希望基于这一项目进行应用开发,或者针对与这一项目密切相关的其他项目进行工作时,则对项目本身的代码进行这一层次的理解,会很有帮助。

第四层次:了解该项目所有代码模块、程序文件的作用,走通所有主要执行流程。

这个层次的基本定位是“掌握”,能够比较全面、系统地理解这个项目的设计和实现,并且熟悉项目各个部分的代码。如果希望对项目进行深度定制修改,或者对社区有所贡献,则应当以达到这个层次作为目标。

第五层次:钻研、领悟该项目的各种设计思想与代码实现细节。

这个层次的基本定位是“精通”,精益求精,学无止境。这是大神们追求的境界。如果希望成为项目社区的重要贡献者乃至核心贡献者,则应当以这个层次作为努力的目标。 综上,对于一个开源项目的学习过程可以大致分为五个层次。至于到底要学习到什么阶段,投入多少相关精力,则完全取决于学习的目的。

知识基础

学习一个开源项目需要的知识基础主要包括:

1)该项目涉及的技术领域的背景知识。
举例而言,分析Linux Kenrel,则应该了解操作系统原理;学习OpenStack,则应该知道什么是云计算。如果没有这些背景知识作为基础,上来就死磕源代码,只能是事倍功半。

2) 该项目开发使用的语言及其各种开发调试工具
这个就无需多言了。

3) 英语
很遗憾,目前为止真正流行的开源项目大部分不是起源于国内。因此,除了学习个别极其流行、文档完备的项目之外,大家还是需要自行搜集阅读英文资料参考。学好英语很重要。

当然,到底需要准备多少知识基础,完全取决于学习的目的和层次。如果只是想科普一下,也就不必太过麻烦了。

学习思路

学习一个项目的过程,其实就是由表及里了解分析它的过程。上述提及的五个学习层次便组成了这样一个逐渐深入的过程。在此基础之上,学习、分析代码的过程,也可以尝试做到由表及里、逐渐深入。

在刚开始接触一个项目的时候,我们看到的其实就是一个黑盒子。根据文档,我们一定会发现盒子上具有若干对外接口。通常而言,这些接口可以被分为三类:

  • 配置接口:用于对盒子的工作模式、基本参数、扩展插件等等重要特性进行配置。这些配置往往是在盒子启动前一次性配好。在盒子的工作过程中,这些配置或者不变,或者只在少数的情况下发生改变。
  • 控制接口:用于在盒子的工作过程中,对于一些重要的行为进行操纵。这是盒子的管理员对盒子进行控制命令注入和状态信息读取的通路。
  • 数据接口:用于盒子在工作过程中读取外部数据,并在内部处理完成后向外输出数据。这是盒子的用户真正关心的数据通路。

因此,在分析一个开源项目的代码时,可以围绕重要的配置、控制、数据接口展开分析工作,特别应该注意理解一个关键的接口背后隐藏的操作流程。例如,针对数据接口,至少应当走通一条完整的数据输入输出流程,也即在代码中找到数据从输入接口进入盒子后,经过各种处理、转发步骤,最终从输出接口被传输出去的整个执行过程。一旦走通了这样一条流程,则可以将与数据处理相关的各个主要模块、主要步骤贯穿起来,并将逻辑模块图上和文档中的抽象概念对应到代码实现之中,可以有效推进对于项目的深入理解。

在实践这一思路的过程中,笔者建议可以优先从控制接口和数据接口中各自选择一二重要者进行背后的执行流程详细分析,力争找到其中每一步的函数调用及数据传递关系(对于一些系统、应用库提供的底层函数可以先行跳过以节省时间)。这一工作完成之后,则第1节中第三层次的学习目标即可初步达成。

配置接口在不同的项目中的重要程度不同。对于一些架构极为灵活、配置空间甚大的项目(如OpenStack的Ceilometer),则可以适当多花些时间加以研究,否则简单了解即可。

对于这个学习思路,下文中还将结合实例进行进一步的说明。

若干小建议

以下是笔者的一些零散建议,供大家参考。

1)做好记录
在刚刚入手开始学习某个项目的源代码时,其实很有点破译密码的感觉。大量的数据结构和函数方法散落在代码的各个角落里,等待着学习者将它们贯穿到一个个重要的执行流程中。因此,在分析学习的过程中,无论有什么零散收获,都值得认真记录下来。珍珠自然会串成项链的。

2)不要过分纠缠于细节
立志搞懂一个项目的每行源代码是值得尊敬的,但至少在刚刚入手的时候是没有必要的。如果过于纠缠于代码的实现细节,则可能很快就被搞得头晕眼花不胜其烦了(看英文资料的时候,每遇到一个不认识的词都要立刻查词典么?)。不妨避免细节上的过度纠缠,还是先尽快走通关键的执行流程,将项目的骨干框架搭起来,然后再以此为参照,就可以清晰判断什么代码值得深入分析,什么地方可以简单略过了。

3)想像和联想很重要
如前所述,从零开始搞懂一个项目的代码,就像破译密码。因此,不妨展开合理的想象和联想,将各个零散的发现和理解联系起来,并加以分析印证。在这个过程中,对项目所在领域的背景知识、对项目本身的逻辑框架和工作原理等方面的理解,都是想像和联想的参照与指导。此外,一些关键的函数名、变量名等等都是联想的hint。本质上,编程语言也是语言,而程序代码就是说明文。在分析代码时,一定要超越语言和代码的细节去理解被说明的事物本身。

4)该搜就搜
分析代码的时候,很容易出现的情况就是,一个执行流程走到半截找不到下一步了。。。在这种情况下,当然首先还是推荐采用各种调试工具的单步执行功能加以跟踪。如果暂时不会,或者种种原因只能进行静态代码分析,那么该搜就搜吧。各种IDE工具的文本搜索都能用,哪怕是grep也行。至于到底以什么为搜索关键词,就需要琢磨琢磨了。

5)外事不决问google,内事不决问百度
如题,不解释。

一个例子:OpenStack Cinder分析

此处将以OpenStack Cinder为例,并结合KVM/Qemu和Ceph,说明如何参考上述思路对一个开源项目进行分析。 可能有朋友奇怪为什么选这么个东东做例子。这个吧。。。写文章是忽发起想,举例子是随手抓来。木有原因。。。

首先,想对Cinder进行分析,一定要了解若干相关的基础知识。什么是云计算?什么是块存储?什么是OpenStack?Cinder在OpenStack里的作用?等等等等。如果对这些东西没有概念,则后续学习是很难开展下去的。

在此基础上,如果有条件,则最好能够亲自部署和实际操作一下Cinder(包括必要的其他OpenStack组件),以便对Cinder获得一个直观的认识和体验,为后续分析提供一些参考。此处假定Cinder使用的后端是Ceph,而OpenStack上运行的虚拟机是KVM。

然后,应该从概念上对我们要分析的系统的逻辑框架有个理解。从总体的范畴上讲,应该了解Horizon和Nova各自的逻辑模块结构,以及它们和Cinder的协同工作方式、关系。这部分与Cinder的控制接口及执行路径分析密切相关。此外,还应该了解Cinder和KVM/QEMU、Ceph之间的相互关系。这对于真正理解Cinder很有帮助。从Cinder自身而言,应该了解其内部逻辑模块构成、各自的功能、相互间的控制、数据连接关系等。

在完成上述准备之后,则可以开始对Cinder的代码进行分析了。如前所述,应该考虑在控制接口和数据接口中各自选择一两个关键的、有代表性的加以分析。至于配置接口,假定其实现了某一配置即可,暂时不需要过多花费时间。 Cinder的核心功能其实是OpenStack上的volume管理。至少在Cinder+Ceph方案中,Cinder自身并不在数据传输关键路径上。因此,控制接口的分析就是Cinder源代码分析的重中之重。就入手阶段而言,则有两个接口及其对应执行流程可以作为Cinder分析的起点,即volume的create和attach操作。如果能够彻底打通这两个操作的执行流程(至少要看到Cinder与Ceph通过librbd交互的层面),则对于真正理解Cinder的功能与实现大有帮助。

虽然基于KVM的虚拟机在通过QEMU访问Cinder创建的、Ceph提供的volume时并不通过Cinder,也即,这一部分的源代码其实已经超出了Cinder源代码学习的范畴,但是,如果希望真正彻底地理解Cinder,则对于这一部分知识还是应该有所涉猎,至少应该有概念上的了解。

在达到上述阶段之后,则可以根据自身的需求决定后续计划了。

以上就是笔者结合个人经验对开源项目学习方法给出的若干建议。见解粗浅,欢迎指正,非常感谢~

(转)为什么你写不好一个快速排序? 谈程序员的职业发展

本文转自IT技术博客大学习


我常常在想,当初我若不离开完美,现在肯定也是总监级的title了,收入比现在高一倍不止。但是另一方面,在编码能力上我甚至不如某些刚毕业的本科生。比如,快速排序的算法我很熟悉,就一句话:“随机选一个元素,用它把输入集分成两半,对这两半继续递归,然后将递归得到(已排好序)的结果合并”。但几个月前看算法书的时候自己尝试写了一下快速排序,发现远远是另外一回事。虽然我对这个算法很清楚,但是用C++实现的时候充满了疑惑,写出来的代码BUG很多,调了很久才调对。相反,如果拿这个做面试题去面应届生,我相信对北大清华的学生通过率应该很高,至少过半。那么我比他们多了6-7年的工作经验,这些经验又是什么呢?

工作经验是人生最容易积攒的财富,只增不减。钱和不动资产,差不多也是如此。所以要想向年轻人炫耀成功项目,牛B的经历,是很容易的事情。在一个很好的平台上,与聪明的人共事,顺水推舟,再加上运气不是太差的话,工作3-5年后,必然会有一个值得吹嘘的项目。比如我就老喜欢说我第一次做游戏就带着人做了梦诛,在线多少万多少万,等等。

但是呢,对一个程序员来说,最核心的价值是什么?是快速的把想法,变成无bug的正确代码的能力。像前面提到的快速排序,如果需求已经很明确,怎么实现已经很清楚,语言你自己选,工具你随便挑。20分钟内代码写不出来,写不对,那就是自己的问题,水平差。

我之前走了一条弯路,听信软件工程的人说,不要重复造轮子。于是就忽视了这些基础训练。“不要重复造轮子“这句话在公司里是对的,但是在下班后,或是在学校,在自己写代码练手的时候,就绝对是错的。重写那些经典的算法,是绝佳的思维练习。写二分查找的时候,需要根据区间的长度是奇数还是偶数,判断结果是大于还是小于,分成2x2=4种情况考虑,然后再想怎么缩减重复代码。写快速排序的时候,除了实现基本功能外,若是能考虑下比较时发生的相等的情况如何处理,又能把执行效率提升不少。

看你想要什么。若是想挣更多的钱,谋求更高的职位(无论技术岗还是管理岗),那么这些都和上述无关。在我现在的公司,每个程序员唯一的出路就是当架构师。这和你能不能写好快速排序没有关系,能看懂牛人写的代码,偶尔做做bug fix或者加个小模块就wonderful了!这就是我牢骚的关键点,随着地位、荣誉、薪水的提升,却逐渐忘却了作为一个程序员,最最基本、最最重要的技能是什么。医生可不像我们这样。即便那医生已经50-60岁了,即便已经是科主任了,依然要做手术主刀。一个手术一不留神做败了,半生积攒的声誉尽毁。

于是我最近常在反复问自己,通过6年全职工作,我的编码能力有了多少提升?除了更喜欢用大括号把local variable局限在block level,除了更喜欢一心编码时被别人或自己打断,有什么显著、可量化的提升吗?若没有,为什么没有?

我的职业梦想是,当我说自己是一个程序员的时候,熟悉我的人愿用excellent这个词来形容我。我在傲剑上班时,老板总喜欢说,你们要用自己的实际能力,去赢得同行的尊重。所以我要是下班后多花点时间去coding,总会达成我的愿望吧!

Ubuntu调教日记之Sublime Text3

接触linux这段时间真的是各种折腾,我几乎所有把流行的发行版都装了一遍,最终选择了ubuntu18.04,毕竟ui强迫症,还好社区比较强大,遇到毛病解决不会太难,初次接触它的过程中也是遇到各种问题,不过通过这几个月的学习,现在已经我可以熟练敲出各种命令并解决一些日常小毛病。没办法,生命在于折腾吧。

Sublime Text3 是一款轻量级的编辑器,它干净、实用、漂亮,还有它那强大全面的插件库。不过我比较喜欢它的四个方面:跨平台、启动快、多行编辑和VIM模式。

好了,我们直接入正题,按顺序执行下面命令开始安装!

一.安装篇

1.通过终端运行命令安装密钥:

wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | sudo apt-key add -

2.安装apt-transport-https软件包

sudo apt-get install apt-transport-https

3.将Sublime Text稳定库添加到您的软件源中:

echo "deb https://download.sublimetext.com/ apt/stable/" | sudo tee /etc/apt/sources.list.d/sublime-text.list

4.更新软件源为最新版

sudo apt-get update

5.安装Sublime Text 耐心等待一会儿

sudo apt-get install sublime-text

以上5步就安装成功了!接下来是解决Ubuntu下Sublime不能输入中文的问题! 按顺序执行一下命令!

1.Git克隆项目到本地Clone

git clone https://github.com/lyfeyaj/sublime-text-imfix.git

2.运行脚本

cd sublime-text-imfix && ./sublime-imfix

执行完”运行脚本“命令后重启Sublime,就可以输入中文了! 如果还不行,你下方请留言咱们再讨论…

二.插件篇

如果说安装给予Sublime生命,那插件就是它的灵魂。Sublime拥有的插件非常全面且实用。做为一个UI强迫症的我,界面好不好看直接影响到工作的效率。下面这些插件都是我比较喜欢的,看一看说不定你也会深深爱上它。

1.首先我们打开Sublime Text,按Ctrl+`(它和qq输入法快捷键冲突)

2.复制粘贴以下代码添加至命令行,然后回车(它用来安装插件的工具)

    import urllib.request,os; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); open(os.path.join(ipp, pf), 'wb').write(urllib.request.urlopen( 'https://sublime.wbond.net/' + pf.replace(' ','%20')).read())

3.重启Sublime Text,查看 Perferences > Package settings 中是否有 Package control (命令面板)这一项,如果有,则安装成功

Package control 安装成功。方便以后更新、卸载Sublime插件啦。

Package control 安装好之后打开它快捷键Ctrl+Shift+P

输入Install Package输入简写 IP 也可以找到一下对应的提示,选择安装包,下载安装稍等片刻左下角 状态栏

下载好后会自动弹出该插件的配置信息,无需保存,关闭即可。 Perferences > Package settings中看到对应的包名

如果想要删除插件,调出命令面板输入remove 调出 Remove Package 选项并回车,选择你要删除的插件即可。

更新插命令upgrade packages

汉化Sublime,用Package conntrol装第一个插件localization 英文好的请略过…

调出命令面板并输入Install Package 然后在输入框中输入localization 就会发现汉化语音的相关插件,稍微等待,自动弹出插件安装信息

好了,折腾了半会儿,接下来的就是大家最喜欢的“皮肤主题和代码插件”

# 路径提示插件,如 <img src="" /> 就会出现提示,就不上图了...
AutoFileName

# 支持不被Sublime支持的文件,特别是中日韩用户使用的GB2312,GBK等
ConvertToUTF-8

# 拥有四个**高品质**的**主题**和多个配色方案
Boxy Theme

# 一个显示颜色代码的视觉颜色的插件,含颜色值的LESS,Sass,和Stylus变量,它是一个帮助您更直观处理颜色的插件
BracketHighlighter

# 修改配置路径方法如下:
Preferences -> Package Settings -> Color Highlighter -> Settings - User,
# 配置成如下内容: 
    {
    "search_colors_in": {
        "all_content": {
            "enabled": true,
            "color_highlighters": {
                "color_scheme": {
                    # 主要是修改这两项
                    "enabled": true,
                    # 填充的意思
                    "highlight_style": "filled"
                    }
                }
            }
        }
    }
# 然后重启 Sublime 就可以了。

# 缩进,代码高亮等转换为 html 代码
Color Highlight

# 直接从Sublime Text更改主题
Colorsublime

# DocBlockr很好用,不仅仅可以自动生成注释,还可以自定义注释的格式
DocBlockr

# 修改配置路径方法如下:
Preferences -> Package Settings -> DocBlockr -> Settings - User,

# 配置成如下内容:
    {
    "jsdocs_extra_tags":[
        "@Author Hybrid",
        "@DateTime ",
        "@copyright ${1:[copyright]}",
        "@license ${1:[license]}",
        "@version ${1:[version]}"
    ],
    "jsdocs_function_description": false
    }

# 可以帮助 html 快速输入代码,如快速新建html头部,打出"!" 或者 html:5,然后按下 Tab 键或 Ctrl+E
Emmet

# 多个高品质的主题和多个配色方案
Material Theme

# 命令面板,Sublime 最重要的功能之一
Package Control

# 检查语法是否有错误
SublimeLinter

# 快捷浏览 html 文件,可以配置快捷键
View inBrowser

三.快捷键篇

熟练的掌握的快捷键,事半功倍!下面是一些是我认为较为实用的,并不全面。

选择类:

# 选中光标所占的文本,继续操作则会选中下一个相同的文本
Ctrl+D

# 选中文本按下快捷键,即可一次性选择全部的相同文本进行同时编辑
Alt+F3
# 先选中多行,再按下快捷键,会在每行行尾插入光标,即可同时编辑这些行
Ctrl+Shift+L

# 光标移动至括号内结束或开始的位置
Ctrl+M

# 在下一行插入新行,即使光标不在行尾,也能快速向下插入一行
Ctrl+Enter
# 在上一行插入新行。举个栗子:即使光标不在行首,也能快速向上插入一行
Ctrl+Shift+Enter

# 选中代码,按下快捷键,折叠代码
Ctrl+Shift+[
# 选中代码,按下快捷键,展开代码
Ctrl+Shift+]

# 展开所有折叠代码
Ctrl+K+0

编辑类

# 合并选中的多行代码为一行
Ctrl+J

# 向右缩进
Tab
# 向左缩进
Shift+Tab

# 注释单行
Ctrl+/
# 注释多行
Ctrl+Shift+/ 

# 撤销
Ctrl+Z
# 恢复撤销
Ctrl+Y

# 保存
Ctrl+S
# 另存为
Ctrl+Shift+S

# 关闭文件
ctrl+W
# 重新打开最近关闭的文件
Ctrl+Shift+T

搜索类

# 打开底部搜索框,查找关键字
Ctrl+F
# 打开搜索框,输入项目文件名,快速搜索文件,输入@和关键字,查找文件中函数名,输入:和数字跳转到文件中该行代码,输入#和关键字,查找变量名
Ctrl+P

# 打开命令面板
Ctrl+Shift+P

# 退出光标多行选择,退出搜索框,命令面板等
Esc

显示类

# 按文件浏览过的顺序,切换当前窗口的标签页
Ctrl+Tab

# 窗口分屏,恢复默认1屏(非小键盘的数字)
Alt+Shift+1
# 左右分屏-2列
Alt+Shift+2
# 左右分屏-3列
Alt+Shift+3
# 左右分屏-4列
Alt+Shift+4

# 等分4屏
Alt+Shift+5

# 垂直分屏-2屏
Alt+Shift+8
# 垂直分屏-3屏
Alt+Shift+9
# 开启/关闭侧边栏
Ctrl+K+B

# 全屏模式
F11
# 免打扰模式
Shift+F11

如有写的不足之处,还请您多多指教!
最后再秀一下我的 Sublime 感觉字体很棒,看着很舒服 嘿嘿

抱拳

❌