2023年4月

“我苦心锻炼了三年,我变秃了,也变强了。” —— 琦玉老师

0x00 大纲

0x01 前言

四个月前,我在《
你是来找茬的吧?对自己的博客进行调优
》一文中探讨了以博客的使用者而不是开发者身份去进行优化,究竟能做到何种程度的问题。当时以
Edge
浏览器的开发者工具里的
lighthouse
评分和加载时间作为基准,经过一系列的针对性优化调整,将博客首页的评分逼近到了“性能(99/100)”、“无障碍阅读(100/100)”的水平,但是当时有一个遗憾,就是没能完成双百,那么时隔多日,这次就要集中一点,登峰造极,完成双百挑战。

0x02 书接上回

上回说到,我们优化后的首页
lighthouse
评分如下:

old-main-page

那么性能部分扣掉的一分是在哪里扣掉的呢?点击“查看计算器”链接,里面会有详细的一个评分情况,如下图所示:

old-scores-detail

可以看到评分是相当苛刻的,而且五项标准的分数权重不一样,也即是说,只要你有任何一项有短板,就算其它分数再高,也没办法获得100的评分。可以看到这里主要的扣分项是 FCP 首次内容绘制时间和 LCP 最大内容绘制时间,至于原因,其实在上一篇文章的末尾有提到过,就是有些资源属于页面强制加载项,我们作为使用者是没有办法去裁剪和控制的。

既然堵不住,那能不能加速呢?

0x03 性能调优

DNS 预获取(DNS-prefetch)

在自己的博客首页,按 F12 打开开发者工具,切换到“网络”标签,然后刷新博客首页,在 URL 一列,可以查看到所有资源的加载的URL地址。然后你会发现,这些资源并非全部来源于同一个服务器,至少可以看到以下不同于主站地址 www.cnblogs.com 的二级或三级子域名:

https://common.cnblogs.com/
https://images.cnblogs.com/
https://pic.cnblogs.com/
https://blog-static.cnblogs.com/
https://account.cnblogs.com/

我们打开命令提示符或者其它你喜欢的终端,输入
nslookup
命令(注意不能用
ping
指令,部分服务器出于安全考虑,是禁用 ICMP 协议访问的),查看各个域名对应的 DNS 解析地址,就会发现它们的 IP 地址是不一样的,以主站和 pic.cnblogs.com 为例:

nslookup-result

那么如果将所有要访问的域名 DNS 提前进行解析,是不是可以加快访问速度呢?答案是肯定的。借助 DNS 预获取(DNS-prefetch)技术,可以达到我们的目的,在 MDN Web Docs 上,对于该技术是这样阐述的:

DNS-prefetch
(DNS 预获取) 是尝试在请求资源之前解析域名。这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。

当浏览器从(第三方)服务器请求资源时,必须先将该跨域域名解析为 IP 地址,然后浏览器才能发出请求。此过程称为 DNS 解析。DNS 缓存可以帮助减少此延迟,而 DNS 解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。

DNS-prefetch
可帮助开发人员掩盖 DNS 解析延迟。

以我自己的博客为例,我将以下域名加入了预取列表(注意因为这些地址都是基于 HTTPS 协议进行访问,所以我这里省略了协议名称,但如果你是加载第三方资源的,务必知晓其访问协议,可能需要指定 HTTP 前缀,请根据实际情况修改):

<link rel="dns-prefetch" href="//common.cnblogs.com">
<link rel="dns-prefetch" href="//images.cnblogs.com">
<link rel="dns-prefetch" href="//pic.cnblogs.com">
<link rel="dns-prefetch" href="//blog-static.cnblogs.com">
<link rel="dns-prefetch" href="//account.cnblogs.com">

注意这里不需要添加主站的地址,因为主站的 DNS 在浏览器访问那一刻已经被解析过了。

预连接(preconnect)

DNS 预获取(DNS-prefetch)技术可以与预连接(preconnect)技术联用,这里同样援引 MDC Web Docs 的解释:

DNS-prefetch
仅执行 DNS 查找,但
preconnect
会建立与服务器的连接。如果站点是通过 HTTPS (提供)服务的,则此过程包括 DNS 解析,建立 TCP 连接以及执行 TLS 握手。将两者结合起来可提供进一步减少跨域请求的感知延迟的机会。

在上面的 DNS 预获取列表后,增加下面的预连接列表:

<link rel="preconnect" href="//common.cnblogs.com">
<link rel="preconnect" href="//images.cnblogs.com/">
<link rel="preconnect" href="//pic.cnblogs.com/">
<link rel="preconnect" href="//blog-static.cnblogs.com/">
<link rel="preconnect" href="//account.cnblogs.com/">

我们看下效果,以
userinfo
接口调用为例(访问 account.cnblogs.com)为例,增加 DNS 预获取优化之前耗时是这样的:

dns-lookup-time-before

优化之后,变成了这样,可以看到还是有效果的:

dns-lookup-time-after

预加载(preload)

如果页面里面有大量链式加载的资源就要注意了,这往往意味着前置资源加载之前,用到了后续关键资源的地方就有可能被阻塞,理想情况下,所有资源的请求链应尽可能的短。但是对于没有办法避免链式加载,而所需的资源又确定会在后续渲染当中用到的场景,就可以用预加载(preload)技术来优化。

以自定义的头像为例,需要由自定义的主题脚本通过
prepend
的方式动态添加节点,这意味着主题脚本和
jquery-2.2.0.min.js
完成加载之前,该头像都暂时不可用。但是头像的 URL 地址我们是预先知道的,这个等待就白白浪费了许多时间。

request-chains-before

我们可以把即将要用到的有确定地址的资源告诉浏览器,让它把数据提前准备好,像这样:

<link rel="preload" as="image" href="//images.cnblogs.com/cnblogs_com/mylibs/1647185/o_200214034545avatar.png">

request-chains-after

可以看到请求链变短了,而且加载时间也提前了。同时也要注意,预加载(preload)并非在所有浏览器中都支持,不过好在它可以安全降级,顶多是不生效,不会影响到页面的正常使用。

compatibility-of-preload

减少不必要的 HTTP 调用

尽管我已经在博客园后台-选项页面中的“侧边栏控件”部分,取消勾选了“日历”模块,但不知为何网络请求中还是有一次日历接口的调用,尽管它啥也没干,却白白浪费了几十毫秒……

remove-useless-ajax-call-0

这个调用是从公告栏里面的
loadBlogDefaultCalendar
函数发起的,这个函数定义在
blog-common.min.js
文件里——上一篇文章提到过,这个文件是默认加载的,是博客园的公共
JavaScript
函数库,无法屏蔽。

remove-useless-ajax-call-1

但是
loadBlogDefaultCalendar
是作为全局函数发起匿名调用的,那么在
blog-common.min.js
文件加载之后,
loadBlogDefaultCalendar
函数执行之前,我们是可以利用
JavaScript Function Hijacking
把它替换掉的,像这样:

<script>window.loadBlogDefaultCalendar=Function.prototype</script>

它刚好可以与前面所有的优化一起放在博客园后台-设置页面中“页首 HTML 代码”处,保存后刷新页面,再看原来的 AJAX 请求已经没有了:

remove-useless-ajax-call-2

使用自定义的语法高亮

原先我使用的是园子自带的
prism.js
语法高亮,它会根据页面中的
language type
自动加载对应语言的高亮模块。对于一个页面有多种语言或开启了行号显示的情况,可能会发生多次的模块加载,那么你会在控制台看到
prism-autoloader.min.js
同时加载了若干个
prism-*
开头的脚本。那为什么说是可能呢?因为
prism.js
里面其实已经集成了多种常用语言,除非你用到了里面没有的语言模块或者插件,才会触发加载动作。

出于减少网络请求次数和缩减体积的目的,我决定使用自定义的语法高亮,在
prism.js

官网
上可以很方便的定制模块,只勾选自己常用的几种语言即可,BTW,我还使用了彩虹括号插件……总之,像买菜一样选择自己想要的东西就好了:

custom-prism

最终你会得到一份 JavaScript 和一份 CSS 文件,把它像自定义脚本和主题一样引入到博客里面就可以了。如果你追求极限,可以将它们与你自己的脚本和样式文件合并——当然这是有代价的,不利于后期的独立维护和升级。

进一步精简 JavaScript 和 CSS

在开发者工具里找到”覆盖范围“标签,如果没有,可能要点击旁边的”+“号手工添加。点击”开始检测覆盖率并刷新页面“,覆盖率统计在你手工点击停止前会一直进行,这时候你可以去页面进行各种操作,尽可能地触发代码。随后在覆盖率报告中,可以看到当前各个 JavaScript 和 CSS 的使用情况:

coverage-01

点击具体的文件,会跳转到”源代码“标签。 蓝色表示该代码已被执行过,红色表示这一行代码未运行,在加载网页时不需要:

coverage-02

注意:仅仅依靠红色来判断代码未执行是不靠谱的,因为这并不表示该代码永远不会用到,这里仅仅是统计了页面加载时的覆盖率,如果你在页面执行一些其它的操作,很有可能就会触发更多的代码执行,覆盖率也会随之发生变化。

这样做的目的是找出优先加载和优先执行项,如果脚本和样式表体积较大,就可以按照执行优先级拆分,利用前面提到的预加载(preload)技术将最基础部分优先加载,后续使用到的脚本和样式延迟加载或者按需加载。尽量减少或加快关键资源的加载,依然是提高 FP、FCP 和 LCP 分数的关键。

性能辅助分析

如果说
lighthouse
能帮你评估页面的总体情况的话,那么性能分析工具则可以助你从细节入手找到瓶颈。同为开发者工具里,切换到“性能”标签,点击”开始分析并重新加载页面“按钮,能够自动刷新当前页面并对其进行采样分析,最终生成的报告如下所示:

performance

在这里可以看到浏览器在渲染当前页面时所做的各项工作的耗时统计以及负载分析,在报告摘要中,详细罗列了各个步骤所消耗的时间占比,建议从占比最大的部分开始优化,因为这样的收益可能是最高的。

如果性能报告中出现了红色三角形长任务(被标记红色),也是需要重点关注的,它指示主线程上耗时过长且性能缓慢的工作,通过查看对应时间轴火焰图上最宽的部分,找到耗时的原因。

此外,还可以关注 FP、FCP、LCP 的触发时机,以及 CLS 的触发次数和位置等诸多细节部分。前面我们提到过,如果 CLS 发生次数过多,将会使用户体验下降,同时严重影响评分。

另一类比较关键的事件是”重新计算样式“事件,如果发现了长时间运行的 “重新计算样式”事件,可以选中它,然后在下方点击 “选择器统计信息”功能来了解哪些 CSS 选择器占用的时间最多。从我的个人经验来看,通常来说 CSS 的优化收益不是很大(微秒级),除非有很严重的性能问题,选择前面几项耗时最突出的选择器进行优化是比较划算的方案。

coverage-03

由于每个使用者的页面情况不尽相同,只能针对性的进行分析,这里只能描述下大概的思路。

0x04 小结

在经过一系列的究极折磨后,这是首页最终的
lighthouse
评分:

new-main-page

new-scores-detail

可以看到,我们其实并没有拿到满分,只是近似满分:

(0.1×98 + 0.1×100 + 0.25×99 + 0.3×100 + 0.25×100)/(0.1 + 0.1 + 0.25 + 0.3 + 0.25) = 99.55

看来还可以继续寻找新的优化方法。

1. 说明
1> linux内核关于task调度这块是比较复杂的,流程也比较长,要从源码一一讲清楚很容易看晕,因此需要简化,抓住主要的一个点,抛开无关的部分才能讲清楚核心思想
2> 本篇文章主要是讲清楚在cfs公平调度算法中,CGroup如何限制cpu使用的主要过程,所以与此无关的代码一律略过
3> 本篇源码来自CentOS7.6的3.10.0-957.el7内核
4> 本篇内容以
《极简cfs公平调度算法》
为基础,里面讲过的内容这里就不重复了
5> 为了极简,这里略去了CGroup嵌套的情况
2. CGroup控制cpu配置
CGroup控制cpu网上教程很多,这里就不重点讲了,简单举个创建名为test的CGroup的基本流程
1> 创建一个/sys/fs/cgroup/cpu/test目录
2> 创建文件cpu.cfs_period_us并写入100000,创建cpu.cfs_quota_us并写入10000
表示每隔100ms(cfs_period_us)给test group分配一次cpu配额10ms(cfs_quota_us),在100ms的周期内,group中的进程最多使用10ms的cpu时长,这样就能限制这个group最多使用单核10ms/100ms = 10%的cpu
3> 最后创建文件cgroup.procs,写入要限制cpu的pid即生效
3. CGroup控制cpu基本思想
1> 《极简cfs公平调度算法》中我们讲过cfs调度是以se为调度实例的,而不是task,因为group se也是一种调度实例,所以将调度实例抽象为se,统一以se进行调度
2> CGroup会设置一个cfs_period_us的时长的定时器,定时给group分配cfs_quota_us指定的cpu配额
3> 每次group下的task执行完一个时间片后,就会从group的cpu quota减去该task使用的cpu时长
4> 当group的cpu quota用完后,就会将整个group se throttle,即将其从公平调度运行队列中移出,然后等待定时器触发下个周期重新分配cpu quota后,重启将group se移入到cpu rq上,从而达到控制cpu的效果。
一句话说明CGroup的控制cpu基本思想:
进程执行完一个时间片后,从cpu quota中减去其执行时间,当quota使用完后,就将其从rq中移除,这样在一个period内就不会再调度了。
4. 极简CGroup控制cpu相关数据结构
4.1 名词解释
说明
task group
进程组,为了支持CGroup控制cpu,引入了组调度的概念,task group即包含所有要控制cpu的task集合以及配置信息。
group task
本文的专有名词,是指一个进程组下的task,这些task受一个CGroup控制
cfs_bandwidth
task_group的重要成员,包含了所要控制cpu的period,quota,定时器等信息
throttle
当group se在一个设定的时间周期内,消耗完了指定的cpu配额,则将其从cpu运行队列中移出,并不再调度。

注意:处于throttled状态的task仍是Ready状态的,只是不在rq上。
unthrottle
将throttle状态的group se,重新加入到cpu运行队列中调度。
4.2 cfs调度相关数据结构
structcfs_rq
{
struct rb_root tasks_timeline; //以vruntime为key,se为value的红黑树根节点,schedule时,cfs调度算法每次从这里挑选vruntime最小的se投入运行 struct rb_node* rb_leftmost; //最左的叶子节点,即vruntime最小的se,直接取这个节点以加快速度 sched_entity* curr; //cfs_rq中当前正在运行的se struct rq* rq; /*cpu runqueue to which this cfs_rq is attached*/ struct task_group* tg; /*group that "owns" this runqueue*/ int throttled; //表示该cfs_rq所属的group se是否被throttled s64 runtime_remaining; //cfs_rq从全局时间池申请的时间片剩余时间,当剩余时间小于等于0的时候,就需要重新申请时间片 };structsched_entity
{
unsigned
int on_rq; //se是否在rq上,不在的话即使task是Ready状态也不会投入运行的 u64 vruntime; //cpu运行时长,cfs调度算法总是选择该值最小的se投入运行 /*rq on which this entity is (to be) queued:*/ struct cfs_rq* cfs_rq; //se所在的cfs_rq,如果是普通task se,等于rq的cfs_rq,如果是group中的task,则等于group的cfs_rq /*rq "owned" by this entity/group:*/ struct cfs_rq* my_q; //my_q == NULL表示是一个普通task se,否则表示是一个group se,my_q指向group的cfs_rq };structtask
{
structsched_entity se;
};
structrq
{
struct cfs_rq cfs; //所有要调度的se都挂在cfs rq中 struct task_struct* curr; //当前cpu上运行的task };

本文中的sched_entity定义比
《极简cfs公平调度算法》
中的要复杂些,各种cfs_rq容易搞混,这里讲一下cfs公平调度挑选group task调度流程(只用到了my_q这个cfs_rq),以梳理清楚其关系

1> 当se.my_q为NULL时,表示一个task se,否则是group se
2> 选择当group task3的流程
3> 选择当group task的代码
task_struct *pick_next_task_fair(struct rq *rq)
{
struct cfs_rq *cfs_rq = &rq->cfs; //开始的cfs_rq为rq的cfs do{
se
= pick_next_entity(cfs_rq); //《极简cfs公平调度算法》中讲过这个函数,其就是取cfs_rq->rb_leftmost,即最小vruntime的se cfs_rq = group_cfs_rq(se); //取se.my_q,如果是普通的task se,cfs_rq = NULL,这里就会退出循环,如果是group se,cfs_rq = group_se.my_q,然后在group se的cfs_rq中继续寻找vruntime最小的se } while(cfs_rq);returntask_of(se);
}

cfs_rq
*group_cfs_rq(struct sched_entity *grp)
{
return grp->my_q;
}

4.3 CGroup控制cpu的数据结构
structcfs_bandwidth
{
ktime_t period;
//cpu.cfs_period_us的值 u64 quota; //cpu.cfs_quota_us的值 u64 runtime; //当前周期内剩余的quota时间 int timer_active; //period_timer是否激活 struct hrtimer period_timer; //定时分配cpu quota的定时器,定时器触发时会更新runtime };structtask_group
{
struct sched_entity** se; /*schedulable entities of this group on each cpu*/ struct cfs_rq** cfs_rq; /*runqueue "owned" by this group on each cpu*/ struct cfs_bandwidth cfs_bandwidth; //管理记录CGroup控制cpu的信息 };

1> task_group.se是一个数组,每个cpu都有一个其对应的group se

2>task_group.cfs_rq也是一个数组,每个cpu都有一个其对应的cfs_rq,每个cpu上的group se.my_q指向该cpu上对应的group cfs_rq,group下的task.se.cfs_rq也指向该group cfs_rq
3> cfs_bandwidth是CGroup管理控制cpu的关键数据结构,具体用途见定义
5. 极简流程图
从throttle到unthrottle:
6. 极简code
6.1 检测group se cpu quota的使用
1>
《极简cfs公平调度算法》
中我们讲过,task调度的发动机时钟中断触发后,经过层层调用,会到update_curr()这里,update_curr()不仅++了当前se的vruntime,还调用 account_cfs_rq_runtime()统计并检测group se是否使用完了cpu quota
void update_curr(struct cfs_rq*cfs_rq)
{
struct sched_entity* curr = cfs_rq->curr;
curr
->vruntime += delta_exec; //增加se的运行时间 account_cfs_rq_runtime(cfs_rq, delta_exec);
}
2> account_cfs_rq_runtime()--了cfs_rq->runtime_remaining,如果runtime_remaining不足就调用assign_cfs_rq_runtime()从task group中分配,当分配不到(即表示当前周期的cpu quota用完了)就设置resched标记
void account_cfs_rq_runtime(struct cfs_rq*cfs_rq, u64 delta_exec)
{
cfs_rq
->runtime_remaining -=delta_exec;if (cfs_rq->runtime_remaining > 0)return;//如果runtime_remaining不够了,则要向task group分配cpu quota,分配失败则设置task的thread flag为TIF_NEED_RESCHED,表示需要重新调度 if (!assign_cfs_rq_runtime(cfs_rq) && likely(cfs_rq->curr))
resched_curr(cfs_rq
->rq);
}

3> assign_cfs_rq_runtime()就是从task_group.cfs_bandwidth.runtime减去要分配的时间片,如果其为0就分配失败
/*returns 0 on failure to allocate runtime*/
int assign_cfs_rq_runtime(struct cfs_rq*cfs_rq)
{
struct cfs_bandwidth* cfs_b = cfs_rq->tg->cfs_bandwidth;;//如果有限制cpu,则减去最小分配时间,如果cfs_b->runtime为0,那就没有时间可分配了,本函数就会返回0,表示分配失败 amount = min(cfs_b->runtime, min_amount);
cfs_b
->runtime -=amount;
cfs_rq
->runtime_remaining +=amount;return cfs_rq->runtime_remaining > 0;
}

6.2 throttle
6.1中我们看到cpu quota被使用完了,标记了resched,要进行重新调度了,但并没有看到throttle。这是因为上面的代码还在中断处理函数中,是不能进行实际调度的,所以只设置resched标记,真正throttle干活的还是在schedule()中(还记得
《极简cfs公平调度算法》
中讲的task运行时间片到了后,进行task切换,也是这样干的吗?)
1> 每次中断返回返回或系统调用返回时(见ret_from_intr),都会判定TIF_NEED_RESCHED标记,如有则会调用schedule()重新调度,《极简cfs公平调度算法》中未暂开讲put_prev_task_fair(),而throttle就是在这里干的
voidschedule()
{
prev
= rq->curr;
put_prev_task_fair(rq, prev);
//选择下一个task并切换运行 next =pick_next_task(rq);
context_switch(rq, prev, next);
}
2> put_prev_task_fair() → put_prev_entity() → check_cfs_rq_runtime()
void put_prev_task_fair(struct rq* rq, struct task_struct*prev)
{
struct sched_entity* se = &prev->se;
put_prev_entity(se
->cfs_rq, se);
}
void put_prev_entity(struct cfs_rq* cfs_rq, struct sched_entity*prev)
{
check_cfs_rq_runtime(cfs_rq);
}

3> check_cfs_rq_runtime()这里判定runtime_remaining不足时,就要调用throttle_cfs_rq()进行throttle
void check_cfs_rq_runtime(struct cfs_rq*cfs_rq)
{
if (cfs_rq->runtime_remaining > 0)return;
throttle_cfs_rq(cfs_rq);
}

4> throttle_cfs_rq()将group se从rq.cfs_rq中移除,这样整个group下的task就不再会被调度了
void throttle_cfs_rq(struct cfs_rq*cfs_rq)
{
struct sched_entity* se = cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))]; //取对应cpu rq上的group se dequeue_entity(se->cfs_rq, se, DEQUEUE_SLEEP); //从cpu rq中删除group se cfs_rq->throttled = 1; //标记group cfs_rq被throttled }

6.3 cpu quota重新分配
6.2中group se被从rq移除后,不再会被调度,这时经过一个period周期,定时器激活后,就会再次加入到rq中重新调度
1> cfs_bandwidth的定期器初始化回调函数为sched_cfs_period_timer()
viod init_cfs_bandwidth(struct cfs_bandwidth*cfs_b)
{
hrtimer_init(
&cfs_b->period_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
cfs_b
->period_timer.function =sched_cfs_period_timer;
}

2> 定时器到期后回调sched_cfs_period_timer(),其只是简单调用实际干活的do_sched_cfs_period_timer()
enum hrtimer_restart sched_cfs_period_timer(struct hrtimer*timer)
{
idle
=do_sched_cfs_period_timer(cfs_b, overrun);return idle ?HRTIMER_NORESTART : HRTIMER_RESTART;
}

3> do_sched_cfs_period_timer()调用__refill_cfs_bandwidth_runtime()重新分配task_group的runtime,然后调用distribute_cfs_runtime()进行unthrottle
int do_sched_cfs_period_timer(struct cfs_bandwidth* cfs_b, intoverrun)
{
__refill_cfs_bandwidth_runtime(cfs_b);
distribute_cfs_runtime(cfs_b, runtime, runtime_expires);
}

4> __refill_cfs_bandwidth_runtime()就是将task_group.cfs_bandwidth.runtime重置为设置的cpu quota
void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth*cfs_b)
{
cfs_b
->runtime = cfs_b->quota;
}

5> distribute_cfs_runtime()调用unthrottle_cfs_rq()将所有se加回到rq上去,这样group下的task就能重新调度了
u64 distribute_cfs_runtime(struct cfs_bandwidth*cfs_b, u64 remaining, u64  expires)
{
struct cfs_rq*cfs_rq;
list_for_each_entry_rcu(cfs_rq,
&cfs_b->throttled_cfs_rq, throttled_list)
{
unthrottle_cfs_rq(cfs_rq);
}
}
void unthrottle_cfs_rq(struct cfs_rq*cfs_rq)
{
se
= cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))];
enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP);
//将se加回rq.cfs_rq的红黑树上 }

哈喽大家好,我是咸鱼。今天跟大家分享一个关于正则表达式的案例,希望能够对你有所帮助

案例现象

前几天有一个小伙伴在群里求助,说他这个 shell 脚本有问题,让大家帮忙看看

可以看到,这个脚本首先将目标文本文件的名字当作该脚本的第一个参数($1)传递进去,然后查看这个文本文件的内容(
cat $1
),并将内容赋值给
firstLine
变量

接着对文本内容的每一行进行遍历然后正则匹配,并将匹配到的内容绿色高亮输出出来,不匹配的内容红色高亮输出,并显示提示信息

其中,正则匹配表达式
^\[(\d+)+\].+$
匹配一组方括号 [ ] ,方括号后还有内容且方括号之间由数字组成:

  • 第一部分:
    ^\[(\d+)+\]


    • ^
      \
      [ 表示以 [ 开头,\ 为转义字符

    • (\d+) +表示匹配多个数字,\ 为转义字符

    • \
      ] 表示匹配右方括号,\ 为转义字符

  • 第二部分:
    .+$


    • .+ 表示匹配任意字符

    • $ 表示结尾

因此上面的正则表达式可以匹配:[123] this is a test line 这样的内容

我们来执行一下这个脚本,首先看下目标文件内容

由脚本的执行结果得知,文本文件中的内容均没有匹配到

奇怪,正则表达式写的没有问题,为啥脚本里面的正则不匹配呢

定位问题

在解决这个问题之前,我们先来了解一下
正则表达式(Regular Expression)

什么是正则表达式

正则表达式是一种通用的文本匹配工具

它允许你使用特定的语法来描述和匹配文本中的模式,可以说是描述文本内容组成规律的表示方式

正则表达式的发展史

正则表达式的起源,可以追溯到早期神经系统如何工作的研究

在 20 世纪 40 年代,有两位神经生理学家(Warren McCulloch 和 Walter Pitts),研究出了一种用数学方式来描述神经网络的方法

1956 年,一位数学家(Stephen Kleene)发表了一篇标题为《神经网络事件表示法和有穷自动机》的论文。这篇论文描述了一种叫做「正则集合(Regular Sets)」的符号

随后,大名鼎鼎的 Unix 之父 Ken Thompson 于1968年发表了文章《正则表达式搜索算法》,并且将正则引入了自己开发的编辑器 qed,以及之后的编辑器 ed 中,然后又移植到了大名鼎鼎的文本搜索工具 grep 中

自此,正则表达式被广泛应用到 Unix 系统或类 Unix 系统 (如 macOS、Linux) 的各种工具中,但是百家争鸣的场面使得各种语言和工具中的正则虽然功能大致类似,但仍然有不少细微差别

正则表达式两种流派之 POSIX 流派

上个世纪八十年代,
POSIX (Portable Operating System Interface) 标准
公诸于世,它制定了不同的操作系统都需要遵守的一套规则

其中就包括正则表达式的规则,遵循 POSIX 标准的正则表达式,称为 POSIX 派系正则表达式

POSIX 规范定义了正则表达式的两种标准

  • 基本正则表达式 BRE(Basic Regular Expression)


    • 不支持量词问号和加号,也不支持多选分支结构管道符

  • 扩展正则表达式 ERE(Extended Regular Expression)


    • BRE 在使用花括号,圆括号时要转义才能表示特殊含义。由于BRE 功能不够强大,导致了 ERE 扩展标准的诞生

像 Unix 系统或类 Unix 系统上的大部分工具,如 grep 、sed 、awk 等都属于 POSIX 派系

BRE 标准和 ERE 标准

早期 BRE 与 ERE 标准的区别主要在于,
BRE 标准不支持量词问号和加号,也不支持多选分支结构管道符

BRE 标准在使用花括号,圆括号时要转义才能表示特殊含义。BRE 标准用起来这么不爽,于是有了 ERE 标准,在使用花括号,圆括号时不需要转义了,还支持了问号、加号和多选分支

我们现在使用的 Linux 发行版,大多都集成了 GNU 套件。GNU 在实现 POSIX 标准时,做了一定的扩展,主要有以下三点扩展

  1. GNU BRE 支持了
    +、?
    ,但转义了才表示特殊含义,即需要用
    \+、\?
    表示

  2. GNU BRE 支持管道符多选分支结构,同样需要转义,即用
    \|
    表示

  3. GNU ERE 也支持使用反引用,和 BRE 一样,使用
    \1、\2…\9
    表示

POSIX 字符组

POSIX 流派还有一个特殊的地方,就是有自己的字符组,叫 POSIX 字符组

正则表达式两种流派之 PCRE 流派

除了 POSIX 标准外,还有一个 Perl 分支,随着 Perl 语言的发展,Perl 语言中的正则表达式功能越来越强悍,为了把 Perl 语言中正则的功能移植到其他语言中,我们熟知的 PCRE 就诞生了

PCRE 是一个兼容 Perl 语言正则表达式的解析引擎,是由 Philip Hazel 开发的,为很多现代语言和工具所普遍使用

除了 Unix 上的工具遵循 POSIX 标准,PCRE 现已成为其他大部分语言和工具隐然遵循的标准

目前大部分常用编程语言都是源于 PCRE 标准,这个流派显著特征是有
\d

\w

\s
这类字符组简记方式

在 UNIX/LINUX 系统里 PCRE 流派与 POSIX 流派的对比,可以参考下表

在 Linux 中使用正则

在遵循 POSIX 规范的 UNIX/Linux 系统上,按照
BRE 标准
实现的有 grep、sed 和 vi/vim 等

而按照
ERE 标准
实现的有 egrep、awk 等

但是在 Linux 的 bash 中,原生的正则表达式语法是基于 POSIX 标准的(支持 ERE 标准和 BRE 标准),不直接支持 PCRE 标准

在了解完正则表达式之后,我们再回到问题本身

脚本中的正则表达式出现了 \d ,而 \d 是属于 PCRE 标准,而 Linux Bash (基于 POSIX 标准)不支持 PCRE 标准,所以匹配不到

3.解决问题

知道了 Linux Bash 不支持 PCRE 标准,我们将脚本中的正则表达式由 PCRE 标准改成 ERE 标准即可

  • 改成 ERE 标准

  • 修改脚本:在脚本里面使用 grep 进行正则匹配过滤
    • -P 参数表示支持 PCRE 正则

    • -v 参数表示取反操作

看下执行结果

参考链接:

  • https://zq99299.github.io/note-book/regular/03/03.html#正则表达式简史

  • https://askubuntu.com/questions/1143710/regex-with-d-doesn-t-work-in-if-else-statement-with

转载请注明出处❤️

作者:
测试蔡坨坨

原文链接:
caituotuo.top/4bedb73c.html


你好,我是测试蔡坨坨。

说到WebUI自动化测试,首当其冲的当属Selenium,在很长的一段时间内,Selenium统治着Web自动化,Selenium其实经历了四个阶段,从2006年发布的Selenium 1.0到最新的Selenium 4.8.3。

  • 2006年,Selenium 1.0发布

    Selenium 1.0包含Selenium IDE(浏览器插件,可以帮助我们录制并生成脚本,对于不会代码的同学来说,它是一个非常好的开始,但现在基本被淘汰了)、Selenium Grid(分布式,将一套脚本分发到不同的机器上执行,功能非常强大,现在还在使用)、Selenium RC(脚本控制浏览器实现自动化的原理,但是它的实现并不完善,到Selenium 2.0时代被WebDriver替代)。

    同年,Google工程师Simon Stewart发起了一个名为WebDriver的项目,它也是一个自动化测试工具,彼时刚刚起步,后来它也成为Selenium的竞品之一。

  • 2009年,Selenium 2.0发布

    2009年,在Google测试自动化会议上,Selenium和WebDriver两个团队的开发人员在沟通后决定合并这两个项目,新项目被命名为Selenium Web Driver,也就是Selenium 2.0。很多人接触Selenium,也是从Selenium 2.0开始的。WebDriver的实现原理其实就是在
    Web浏览器

    我们的脚本
    之间有一个WebDriver,通过WebDriver协议去驱动并操作浏览器(具体实现+源码解析可参考往期文章「
    Selenium底层逻辑源码解析
    」)。

    WebDriver的作者是这样解释二者合并的原因:“一方面WebDriver解决了Selenium存在的缺点(例如:可以绕过JavaScript沙箱,WebDriver有出色的API),另一方面Selenium解决了WebDriver存在的问题,还有就是Selenium主要贡献者和WebDriver的作者都认为合并项目是为用户提供最优秀框架的最佳途径。”

    Selenium 2.x(WebDriver)真正兴起是在2014年开始,到2016年左右成为Web自动化最热门的框架。几乎谈及Web自动化,那就是Selenium。它不仅在Web自动化测试领域很火,在爬虫领域也是非常热门。

  • 2016年,Selenium 3.0发布

    这个版本并没有引入新的工具,主要是加强了对浏览器的支持。

    完全移除了Selenium RC;WebDriver暴露一个供浏览器接入的API,通过各浏览器厂商提供的Driver来接入;将内置的Firefox Driver剔除;支持Firefox通过GRCKO Driver来接入Selenium;通过Apple提供的Safari Driver来接入Safari;通过Edge Driver支持IE接入。

    所以,3.x和2.x其实没有太大的区别。

  • 2021年,Selenium 4.0发布

    在Selenium 3.x中,与浏览器的通信基于JSON-wire协议,因此Selenium需要对API进行编解码。而Selenium 4遵循W3C标准协议,Driver与浏览器之间通信的标准化使得它们可以直接通信。

    除此之外,Selenium 4还做了一些其他的改动,可参考往期文章「
    Selenium 4 有哪些不一样?
    」。

由于Selenium在3.x和4.x两个版本的迭代中并没有发生多大的变化,因此Selenium一统天下的地位也由于新框架的出现变得不那么稳固。

2020年左右出现的Cypress、TestCafe、Puppeteer,在技术雷达中被誉为后Selenium时代Web UI自动化的三驾马车。但是由于这三个框架都是基于JavaScript开发的,所以就需要测试人员熟悉JavaScript,并且都不支持Python,所以使用率并没有很高。

Playwright简介

2020年,微软(Microsoft)开源了一个名为
Playwright
的工具,与Selenium一样入门简单,支持多语言(Python、Java、Node.js、.NET),大厂出品,必属精品。它刚出现的时候,并没有多火,随着时间的推移,到2022年左右,Playwright已经被更多的人注意到并接受它,甚至比Selenium更好更强大。

Playwright是一款定位于端到端(End-to-End,端到端就是用于测试整个应用程序的流程是否符合预期的测试技术,模拟用户真实使用场景,为实现这一目标,该系统通常被视为黑盒子,也就是所谓的黑盒测试,主要通过GUI和API等公共接口对其进行操作)的测试工具。

虽然目前在国内Web自动化测试的扛把子还是Selenium,但是国外已经有很多企业都在使用Playwright,比如:Adobe、微软自带的某些项目(Visual Studio Code、Bing)、Disney Hostar等。

既然在市面上已经有非常成熟好用的Selenium框架,并且在持续的更新,社群也十分活跃,微软为什么还要整一个额外的自动化测试工具出来呢?

两个工具的定位不同,由于Playwright是后出来的工具,所以在一些细节方面会处理得更好。

  • 跨浏览器

    虽然Selenium提供了几乎所有浏览器的兼容和支持,但是还需要额外下载对应的浏览器版本的驱动,否则Selenium WebDriver可能就无法启动。而Playwright内置了各种浏览器以及浏览器驱动(支持所有现代渲染引擎,包括Chromium、Firefox和WebKit),并且不需要额外的升级和管理,这样一来整个自动化测试项目将会更加稳定。

  • 自动等待

    在使用Selenium的时候,我们通常会根据测试需求,在代码中加入各种等待,而在Playwright中没有所谓的隐式等待,而显示等待也已经封装好了,直接调用即可。

  • Web优先断言

    Playwright会根据网络环境进行自动断言,直到满足某种条件为止。

  • 追踪

    Playwright可以很方便地配置重试策略,并进行自动的追踪结果,以截图和录屏的方式进行记录。

  • 并行执行

    在Selenium中,执行测试用例是单线程的,但是在Playwright中,由于运行机制不一样(Selenium的指令发送是通过http协议,而Playwright则是使用socket协议;Playwright是通过上下文管理浏览器,相当于每一个测试用例都会创建一个独立的上下文,浏览器的上下文其实就是一个全新的浏览器,这种方式的好处就是在提速的同时又实现测试与测试之间的隔离,使得测试结果更加准确),所以可以实现并行执行。

  • 强大的工具集

    Playwright给我们提供了强大的工具集,例如:脚本录制工具codegen、脚本编写以及调试工具playwright等。

Playwright初体验

以Python语言为例,编写第一个Playwright脚本。

PS:Playwright 需要 Python >=3.7 版本

安装playwright

pip install playwright

安装内置浏览器

使用以下命令安装Playwright自带的各种浏览器:

playwright install

由于安装的内容比较多,所以这一步需要花费一点时间。

第一个Playwright脚本

1. 使用with方式启动
# author: 测试蔡坨坨
# datetime: 2023/4/8 2:18
# function: 第一个playwright脚本,使用with写法

from playwright.sync_api import sync_playwright, expect


def run(playwright):
    chromium = playwright.chromium  # or "firefox" or "webkit".
    browser = chromium.launch(headless=False)  # headless表示是否使用无头浏览器(也就是无GUI模式)
    page = browser.new_page()
    page.goto("https://caituotuo.top")
    # other actions...
    print(page.title())
    browser.close()


with sync_playwright() as playwright:
    run(playwright)

# 这里使用的是with方法,Python中的with方法可以很方便处理一些需要提前设置,事后需要清理的工作
# playwright正好有上下文处理,所以使用with写法会使代码更加简洁
# 比如:
# with open("/caituotuo.txt") as f:
#     f.read()
#
# 非with方式可能存在问题:1.可能忘记关闭文件句柄 2.文件读取数据时发生异常,但是没有进行任何处理
# f = open("/caituotuo.txt")
# data = f.read()
# f.close()
2. 使用start()方式启动
# author: 测试蔡坨坨
# datetime: 2023/4/8 2:20
# function: 使用start()写法,直接实例化playwright同步方法

from playwright.sync_api import sync_playwright

playwright = sync_playwright().start()
chromium = playwright.chromium  # or "firefox" or "webkit".
browser = chromium.launch(headless=False)  # headless表示是否使用无头浏览器(也就是无GUI模式)
page = browser.new_page()
page.goto("https://caituotuo.top")
# other actions...
print(page.title())
browser.close()

总结

Selenium和Playwright都是流行的自动化测试工具,都拥有丰富的功能和API,都可以用于Web应用程序的自动化测试。选择一个测试框架,必须考虑日常工作中的场景以及当前的团队,进而选择合适的自动化测试工具来进行测试。而不是使用一个新的花里胡哨的框架,然后一段时间后因为它没有满足我们的需求而抛弃它。

当然,同时掌握多种技术栈再好不过了,正所谓“兵来将挡,水来土掩”,无论企业中选择哪种工具,都可以快速上手,也是自身能力的体现。

最重要的是要行动起来,多写多练,日积月累,自然会如鱼得水,真正把自动化测试在企业中落地,带来对应的效率和质量保障方面的提升。

在我接触了一系列AI技术后,不禁产生了许多思考。我先后尝试了AI编程、AI写论文、AI写小说、AI绘画等,最近看到了一些关于AI构建虚拟世界以及Auto-GPT的AI类新闻。在这个过程中,我心头涌现出诸多疑问和想法。

首先,我不禁感到迷茫:传统编程在这样一个日益智能化的世界中还有前途吗?面对AI的崛起,未来的程序员该如何应对和适应?

其次,我开始思考人类与AI之间的关系。我们究竟是合作伙伴、甲方和乙方,还是像老板与助手一般相辅相成?在未来十年,这个世界将会变得如何?人类、个体和无产阶级将面临怎样的挑战和发展?

要回答这些问题,我们需要凭借对未来的想象。

(PS. 本文仅上面开头部分由GPT润色,其余完全手打)


编程&科研领域

AI编程 VS. 传统编程

基本上有点麻木了,最开始是震撼。前段时间,弄论文需要写一些代码,我真是完全没有思路,也完全没接触过AI类编程、什么Pytorch一概不会,如果按照原始的方式,起码得整个半个月,从了解,到稍微系统点的学,再到真正编程实现,还会遇到很多BUG。然后那个时候刚接触GPT还没有出4,还是3.5,基本上我借助3.5半个下午就搞定了代码,而且我后面反复调参,也没有GPT给出的原始方案好。虽然存在一些随机性。但这个仅仅是GPT3.5,4的准确性大大提高。

另外就是代码重构方面,以前维护的“屎山”,基本上难以人为的扒拉开来看,现在直接丢给GPT完事儿。

最近的Auto-GPT,本人也只是看了下新闻,好像大概意思就是只需要一个想法就可以得到结果程序。

感觉上像是建立了
“想法”->“实际代码”
的直接映射。

而传统编程则是
“想法”->“可行性调研、技术选型等”->“各种学习尝试”->“实际编程写BUG等”->“实际代码”

速度方面自然不用说,可以说是彻底的“降维打击”。有人说AI写的没人类好,这个真不一定。现在最前沿我不了解,但是仔细想下,AI不仅可以学习官方文档和海量优质的网络教程,还可以学习大量成熟、优秀的开源源代码。不管个体还是团体阅读代码的能力应该都远远不如AI,而且钻研源代码是个“苦差”,做这事的人本身就是少数,甚至说老老实实看官方教程的都不是很多。个人感觉,未来,不管是顶层设计还是代码实现,AI吊打人类程序员都是迟早的事情。

总之,未来,传统的“技术、经验”可能不是重要的,唯一重要的东西可能只是“想法”以及“想法的质量”。甲乙方可能不再存在,因为甲方可以直接通过AI得到需要的结果,所需要的只是一个稍有编程知识和能熟练运用AI生产工具的产品经理,而且这个产品经理还可能就是AI本身,而不需要一个实际的人。

有时候我想这东西太逆天了,以后也没有程序员了,只要有人维护AI就行了。但是AI本来就是人类几十年知识的结合体,碾压人类本来就是很正常的事情。那人类知识是不是不创新了?不增长了?其实AI的创新能力吊打人类,因为人类个体基本上只会知道个别领域的知识,基本没有全领域的专家,但是AI它不仅精通哲学、医学、物理、天文、地理、编程,基本上你知道的他都知道,你不知道的他也知道。我之前写论文的时候,写“未来的工作”部分,自己想到了几个点,然后又让GPT4写几个点,结果我想到的它都“想”到了,而且想法质量上还有提高,并且还提供了几个我没想到的学科融合的绝妙“想法”。设想一下,它基于这些想法,自主设计实验也完全是可行的,再多给它几个现实世界的接口,例如让它联网,那么它自己去实验证明然后出论文也是完全没问题的。

所以通过AI自主科研推进人类知识水平的发展,我个人感觉是没问题的。

当然这个可能要在未来,目前我用GPT来写论文感觉还是不太可,但是这个有随机性,有时候你自己写的很垃圾,AI可能给出一个王炸,有时候AI怎么写都感觉很呆板,说不到要点、纯扯蛋。目前感觉,GPT最好用的是编程领域。因为它基本上有一个大致确定的结果,并且可以通过跑程序验证正确性。


小说&绘画领域

这个本人是完全的外行,就是浅浅试了下。首先是小说,我大概用GPT辅助写了七八章小说,首先一个,质量是随机的,有时候效果好、有时效果差,另外一个很核心的问题,无法保持章节之间的设定一致,因为我是外行,我的理解是,GPT维护的上下文太小了。

感觉这个问题是容易解决的,例如在生成小说的时候对一些设定进行固定等。

另外我发现写西方风格的小说效果比写中文好。最后,相对编程,个人感觉写小说可能更不容易被取代。可能是因为相对编程,小说更涉及一些灵感创造,而编程则是遵循想法的机械动作。

关于绘画,我主要试了Stable Diffusion。稍微了解了下原理,感觉最大问题还是随机性,就是感觉出图就像抽卡(从刺激性角度看可能也是一个优点),有时候是金色传说,有时候跑了半天出来一个凤姐恨不得把电脑砸了。也就是说,这种随机性有时候可能是对人类资源的严重浪费,你烧了显卡,排了二氧化碳,浪费了电,但是得到可能不是人类所想要的东西。当然这个领域进步也很快,配合controlnet等东西,可以进一步对结果进行控制,这个我暂时还有没有进一步尝试。

未来,精细控制应该不是梦。AI可以根据你的想法推断出你最大概率想要的结果。和编程一样,绘画的“技术和经验”可能不再重要。重要的是“想法”和“想法的质量”,最多就是再补充一个对想法的描述。然后就是作品质量问题,我觉得AI可以综合莫奈、梵高、达芬奇,我觉得相比人类AI可能更容易找到优秀画家的共性,因为AI从海量作品中学习,或许可以学习到真正描述优秀作品共性的“潜在变量”。

AI可能在绘画领域扮演一种“超级剽窃师”,最近看到新闻,貌似说有种技术可以让AI剽窃得毫无痕迹。但是不得不承认,AI的作品可能真的比很多中低端画师的作品优秀,甚至个别超低端画师作品的存在可能是在污染AI的训练集。我特别能想象,那种幸苦学画N年,然后画了一天发现没AI几秒画的好的那种心情。但是你得知道,AI是人类N年知识水平的结合体,在很多工作上肯定是比个体优秀的。生产力的发展是不可阻止的,我觉得AI辅助创作应该是未来制图工作者的必备技能。


AI辅助学习

本人是一个纯纯的数学白痴,但是借助一些提示词例如说“我是一个数学白痴,我希望你用卖菜大妈也能听懂的语言帮助我解释xxxx,让我彻底理解xx,结合具体、简单例子”,我确实理解了很多以前需要很大功夫才能理解的知识。我把这个技巧用到了很多领域的学习当中去,甚至借助这个技巧去看了很多医学论文。彻底理解可能严重夸张了,但是对于大部分东西,AI都可以帮你了解个半桶水,作为开始学习某领域知识的起点真的是一个绝佳的办法。


其它领域(电影、动画)&小结

最近也设想制作一些动画、短片之类的,于是做了一些调研,发现即便是最简单的工具,对于一个完全外行的人来说,制作一个质量稍好的动画也是复杂的,需要有一定的时间去学习、练习。在学习工具的过程中,我感到这个领域如果类比到编程(当然就仅我接触到的低端工具而言),就仿佛停留在“面向过程”时期。

本人想象的方式是,已经存在预定义的角色,然后我作为“导演”,安排他们完成我的动画。预定义的角色作为服务的提供者、作为演员。这种才是OOP的方式。最近不是有一篇AI构建虚拟世界的论文很火爆,它好像是说25个AI在虚拟游戏世界里面生活。因此我认为这个是完全可以实现的,而且可以实现得很好。还有一些想法,例如说未来我们可以通过对真人进行拍照建模,类似于数字人,不管是外在、还是脑子都可以克隆到计算机上,这样演员这个职业可能也不需要存在了。甚至于在你死之后,你的克隆数字人,还可以和100年后的人类后代交流。

总结一下,就是说,未来每个人都可以是程序员、画家、导演。重要的不再是特定领域的“技术和经验”,重要的是想法及其质量。然后个体变成一个超级甲方,AI变成乙方,个体需要学习的就是怎样和AI沟通,让AI彻底理解你的想法。