geogebra求解二元二次方程一例

工作的时候每个人表现各自不同。我写代码的时候骂骂咧咧,写到兴头上“破玩意”“啥玩意”之声不绝于口。二猫聚精会神的时候嘟嘟囔囔,“怎么回事怎么回事,怎么会有俩解呢”。二猫妈就会出现,“是因为有俩解,你看这里……”两个开始嘀嘀咕咕解题,一会儿哈哈大笑,“对啊对啊,你看果然吧。”

我猜测应该是个二次方程,如果画图的话,两条曲线两个交点,这样讲解两个解会更一目了然。于是祭起强有力的法器 geogebra,问,“啥题这么有意思,给我看看呗。”

1. 题目

是这么道题。

image1

二猫口头讲解,x2就是x^2,y2就是y^2。也不知道从哪找来的题,排版可真够粗糙的了。

我看都没看,就往 geogebra 里录入。

右上解,选 CAS和绘图区两个视图(这玩意叫做视图么?)。

image2

两个方程

eq1: x*y + x + y = 10
eq2: x^(2) y + x y^(2) = 24
image3

对应XOY平面直角坐标系中的两条曲线, eq1设置成蓝色,eq2设置成红色。

image4

交点呢,没看到啊?放大一下。在第一象限,两条曲线贴得很紧,不过确实有两个交点。书中暗表,我的错误就从这里开始埋下伏笔。这是两个交点不假,但是题目所求的并不是交点,而是x^2+y^2. 此两解并非彼两解,我无意中偷换了概念。

image5

当时我并不知道此处有坑,就昂首阔步走下去了。

2. 解方程

精确解({eq1,eq2},{x,y})

此处忍不住跳出来评论一下。不知道是geogebra原生如此,还是国内做的本地化。用汉语指令“精确解”,确实能为使用者降低门槛,翻译也符合国内的使用习惯。然而,当用户想查细节去找手册的时候,却没有汉语只有英文的。如果不知精确解这条指令在英文的手册中是如何称呼的,就连条目都查不到。

solve({eq1,eq2},{x,y})
image6两个交点,两个解。而且这两个解在图中正是两条曲线的交点,横轴以3为中心对称,±根号5,纵轴以3为中心对称,±根号5,两个值刚好互换……

等等!互换?这道题求的是x^2+y^2,如果把x和y互换,那不就应该只有一个解吗?这时注意到了刚刚的坑,解非解。

3. 验算

二猫和二猫妈解出两个解,数值不等。

验证一下,我猜得对不对,看起来x和y可以互换啊。顺便,我求出来的是两个解中的哪一个呢?

element(element(solutions({eq1,eq2},{x,y}),1),1)^2+element(element(solutions({eq1,eq2},{x,y}),1),2)^2
image7

二猫和二猫妈的两组解中,确实有一组是28。然而另一组解不是28,我的另一组,交换x和y却仍然是28。

4. 再验算

把求得的x和y代回方程中,看左右两边是否相等。

x1:element(element(solutions({eq1,eq2},{x,y}),1),1)
y1:element(element(solutions({eq1,eq2},{x,y}),1),2)
x1^(2)*y1+x1*y1^(2)
x1*y1+x1+y1
image8

都与预期完全符合。

或者

x1:element( element(solve({eq1,eq2},{x,y}),1),1)
-> x = -sqrt(5) + 3
y1:element( element(solve({eq1,eq2},{x,y}),1),1)
-> y = sqrt(5) + 3

5. 咦?

我求得的两组x,y(以及再求得的一组x^2+y^2),代回方程没问题,只能证明这两组解是对的。但是并不能表明就没有别的解了。

6. 复数解

突然就对 精确解 这个术语所暗示的含义产生了怀疑。通常会觉得,与精确相对的是模糊吧,即,不用 3+sqrt(5),而是求解为 5.24 这样?

image9

跟二猫妈讨论,咋知道你们的另外一组x^2+y^2解对应的x和y是实数呢?二猫妈说,另一组x^2+y^2是4,是实数啊,而且是正的。

x^2如果是正的,那么x就是实数。但是x^2+y^2是正的,可能x^2是负的(因此x不是实数),但是y^2是个更大的正数呢。

再解一次。

csolve({eq1,eq2},{x,y})
image10

果然,坏人在这里!还有两个解,都是有i的。

验算略去,代回原方程也都对,x^2+y^2也是正的没错,并且不是28。

image11

矩阵一下,对换x和y以后确实应该相等。。

image12

7. 近似解

既然到了这一步,近似解也跑一遍吧。

image13

很好玩。

8.竞赛题

按题目要求,x^2+y^2到底几个解呢?回顾一下。

image1

你以为是两个解?并不。二猫和二猫妈辛苦解出的答案为4的那组解需要舍掉。因为题目里说“已知x,y都是实数”。表面上看,这半句是用来降低题目难度的,然而,这是用来提升题目难度的。因为这是一道竞赛题,而不是 geogebra演示题。所以,拿到这道题的人,如果不是像我一样傻乎乎地求出每个x和y,那么一定会换元,直接求出x^2+y^2,而不经过求出x和y这一步骤。非常容易地,就会以为这道题不过如此非常容易,忽略了对求解结果的delta的检验。明明多求了两个复数解,却不符合题目要求了。

这是一道竞赛题,竞赛题哪有不骗人的。

所以说,处处是坑。我最开始做的结果就是对的,然而我并不知道自己用错了函数,瞎猫碰到死耗子,盲人骑瞎马跨过去没踩到而已,并不是远远地躲开了坑。二猫和二猫妈用了高级的手法,但是没有注意到表面降难度的条件却是坑。掉进去又出来,出来以后发现踩在另一个坑里,再出来发现还有一个坑。这就是最好玩的地方。

微信图片_20220528215636

读书打卡和锻炼打卡,以及AHK脚本

1. 习惯

长期有阅读的习惯,经常同时读几本。有时候读着读着就搁下,过几年想起来再接着读,甚或忘记读过一部分,又重头来读。读着读着觉得似曾相似,抚掌大笑原来是你,老朋友。

好记性不如烂笔头,得记下来。除了读书笔记以外,也尝试过不少记录进度的方法。比如尝试过读完归类,分成工程、素材、文学、非功利性……因为兴趣广泛,并且多数书籍都既是文学,同时非功利,也许我看重的正是里面的工程思想可以当作讲课或科幻的素材。MECE不重不漏,太难做到了。

笔记和日志的范围,很容易就超出五年十年。时间流逝,由于兴趣变化,关注点迁移,后来不仅分类,我连标签都懒得加。就是流水账。

这样,除了读书笔记,页边吐槽,A4纸做的书签写满画满,读书的记录就完成了一行。有的读得酣畅淋漓,想来当初读的时候一定不舍昼夜。比如 黑客与画家 [2011-12-14 Wed]--[2011-12-17 Sat],只有短短三天。有的读得了很久,像 伯罗奔尼撒战争史 [2012-04-03 Tue]--[2012-07-04 Wed] ,足足三个月,当时应该是细细品味,不断掩卷追思来着。有的书读了又放下,捡起来又放下,如是者三,到现在也还在不断慢慢耗着。有的书记录的阅读时间非常短,但是训练内化的过程极其漫长艰难,个中滋味不足道哉,也就没有记到那一行里去。像 囚徒健身 [2013-11-13 Wed]--[2013-11-16 Sat],这很可能是第一轮阅读的记录。后来再无记录,但是每周的训练里仍然有这本书的影子,框架,循序渐近,尽可能避免受伤,阶段指标,这些影响还在。

像锻炼,还有其他需要训练的,数学、编程、英语、写作、美术、音乐,这些的训练时间远超过阅读的时间,也都没有记录,不算阅读。

说起锻炼,我也保持了相当长时间记录的习惯。本来断断续续写日志,后来在YK老师倡导打卡的群里打卡,索性每天记录,也充作打卡的内容。记录包括练了什么 (动作、重量、多少组,每组多少个,有时包括休息时长) ,什么感觉。感觉只记了较为客观的体验,受伤了,疼了,费劲,容易,什么的。事情上,在训练量大的时候,多数时间的感觉就是想骂人而已。我还记得一边卷腹,一边咒骂教练的智慧,是怎么想到这些折磨人的姿势的,刚好不容易发力。教练说,应该如何如何,要加油什么的,我就恶毒地隔空骂回去。这些过于个人化的情绪都没有记。

对仪式感我挺不能适应的,特别是别人施加给我的仪式感,哪怕是为了我好,哪怕我也认可真好的。不过,我也很意外,相信不少熟悉我的人也会非常意外,我对遵从合规居然能够接纳得还不错。

微信图片_20220520220212

长达几年的打卡,几乎每天不断,我就是用手写的,一个一个群里发过去。锻炼,现在一般是两个群,读书三个群。百词斩三个群,扇贝两个群。疫情打卡一个群。每天日志推git,手动执行 add,checkin,push 4年,才开始写个简单的脚本,改吧改吧,改动不多,用到现在。

想起群里有人问过我,发博客的时候,我一般都发在三四个地方,用了什么工具。我看到问题最初一楞,没有想到要问的是什么。因为,我并不以发在三四个地方,分别排版,不以此为苦。排版的时候,有时我还挑挑错别字,回味一下哪里写得不够好。

与博客发往多个站点一样,记录日志和群发打卡也是,我就一下一下,每天分别打的。没有自动化工具,难道就不干活了么。大刘先生在《全频道阻塞干扰》的结尾借美国将军之口说:我们也不是从来一直就有最好的武器,也并非没有最好的武器就不能战斗――对面已经全线压上,退后就是大海,他在简短的演讲后下达命令:士兵,上刺刀。

较早的锻炼记录是这样的,到现在也没有大的变化。

>* 俯卧撑[2011-01-16 Sun 20:21]
>50个。流汗不多,喘得不太厉害。
>事先做了左手腕康复训练,不过还是有些胀痛。
>后背和前胸舒服一些。
>
>查用锻炼治疗后背疼。用矿泉水瓶的那个教练的视频提到,俯身飞鸟有效,尤其
>是对肩胛骨下疼痛。

读书进度的记录和打卡晚近一些,像这样:

>[2020-03-22 Sun]
>* 读书打卡
>北野武的小酒馆 第四章
>莎士比亚戏剧集 喜剧I 仲夏夜之夜 第二幕
>瓦尔登湖 The Ponds
>Bodyweight Strength Training Anatomy 第二章 手臂

2. 关键技术,使用AHK脚本

微信图片_20220520220235

我知道DRY原则。有人说,如果要重复两次的,就应该写函数了。我能忍到三次。

在代码中,写作时重复三次的,运行时可能会重复几十万次。单单在后续代码的维护和变更中,可能就要有几十次修改涉及到重复的地方,所以抽象/重用,就有特别的必要。

在工作和生活中这些打卡动作都具有这样一些特点。1.涉及的动作种类不多。比如只有搜索群、输入群名、发送固定的内容,甚至不必检查群里的响应;2.重复次数不多。虽说"长期"打卡,你打过多少年?一年不过350多次,即使打卡十年不过3500多次而已。分布在许多天里,实在算不上什么负担。3.每次动作持续时间不长。我测量过时间,打卡百词斩和扇贝这类需要五六个动作的,也不足一分钟。打卡阅读和锻炼,可能操作的时间半分钟都用不上。写完日志或报告,剩下的操作时间,没有多长,可以全算作休息。

只有两种情况例外。一种是你对打卡这一富有仪式感的动作序列厌烦了,那么这时候自动化能降低心理的负担,有利于保持愉快。另一种是你闲着技痒,把做个打卡代码当成富有 (微小)创造性的游戏,好玩。

最简单的打卡,是只打卡,不看回应。

从需求到技术

1.系统显示文本框,用户在里面填写打卡内容。我就是把日志复制粘贴进去。比如今天的一部分:

>傍晚,跳一组心肺。从提膝跳膝下拍掌开始测心率, 137,140,13X,160。
>
>附:
>心肺一组
>开合跳 50
>提膝跳膝下拍掌 50
>提膝跳拍膝 50
>深蹲跳摸地 50
>交替箭步蹲跳 50

2.用户单击发送按钮。为了点击方便,我把点击按钮做得特别大,鼠标大致往那个方位一按就行,避免鼠标急走急停耗费的精力、腕力、时间。按钮大到你可能找不到它,就在文本框的下面,大小跟文本框差不多,占整个对话框一半,上面有个小小的“OK”。

OK

3.在微信里,向特定的几个群发送消息。这涉及到,找到微信窗口,找到群,向群里发消息。

找到特定控件,或者向特定控件发消息,AHK我用过几种方法。包括 图像匹配找到控件 (像素匹配,不能缩放) ,快捷键 (按键组合,比如 ctrl-f) ,鼠标按坐标点击 (需要根据显示器分辨率标定)。在这里,我只用快捷键,相当于全用键盘操作。ctrl-f 开始搜索,键入群名,回车就到了聊天窗口,发送文本框中的内容。

在寻找群时,我操作了两次。因为在其他项目 (情绪稳定,监听回复) 的需求中,需要跳到群聊天记录的最新一条,操作两次可以保证达成这一效果。在这里我复制了那段代码,操作两次并不带来负面效果,所以就没改保留了。

一个小技巧,AHK虽然功能强大,可能由于本地化/国别/输入法的关系,偏偏发送内容不太可靠。一个简单的方法是用剪贴板。把要发送的内容赋值给剪贴板,然后发送 ctrl-v 快捷键粘贴上去。

4. 星期几

打卡时有日期,还包括星期。我日志里长期保持使用英文缩写"Mon""Tue",但是AHK按默认操作系统本地化以后,会显示中文的"星期一""星期二"。我写了一小段查表转成英文缩写,保持与我日志风格相同。

>Switch A_DDD
>{
>Case "周一":    ddd=Mon
>Case "周二":    ddd=Tue
>Case "周三":    ddd=Wed
>Case "周四":    ddd=Thu
>Case "周五":    ddd=Fri
>Case "周六":    ddd=Sat
>Case "周日":    ddd=Sun
>}
>current_day = %A_YYYY%-%A_MM%-%A_DD% %ddd%

微信图片_20220520220238

3. 代码

代码没多长,总计50多行。去除测试和注释,估计50行有效的?

以下。

#SingleInstance force

; 测试模式
test_mode = false

; 微信群名 列表

group_name := ["玫瑰花园", "天天向上"]

;---------------------------------

; 用户输入发送的文字
Gui, +Resize  ; Make the window resizable.
Gui, Add, Edit, vMsg WantTab W600 R20
Gui, Add, Button, default  w600 h200, OK
Gui, Show
return  ; End of auto-execute section. The script is idle until the user does something.

;--------------------------

ButtonOK:
    Gui, Submit
    out = %Msg%

; 时间戳
Switch A_DDD
{
Case "周一":    ddd=Mon
Case "周二":    ddd=Tue
Case "周三":    ddd=Wed
Case "周四":    ddd=Thu
Case "周五":    ddd=Fri
Case "周六":    ddd=Sat
Case "周日":    ddd=Sun
}
current_day = %A_YYYY%-%A_MM%-%A_DD% %ddd%


; 找微信窗口
Run, C:\Program Files (x86)\Tencent\WeChat\WeChat.exe
Sleep 500
WinActivate, ahk_class WeChatMainWndForPC

; 找群
for index, element in group_name
{
    Sleep 1000
    Send ^f
    Send %element%
    Sleep 1000
    Send {Enter}
    Sleep 1000

    Clipboard = [ %current_day% ]
    Send ^a
    Send ^v
    Sleep 500
    Send ^{Enter}
    Sleep 500

    Clipboard =  %out%
    Send ^v
    if (test_mode != "true"){
       Send {Enter}
    }

}

time_out = 5
msgbox , , 信息 , 完成,%time_out%秒钟后退出, %time_out%


ExitApp
return

稳定了,解决传输途径不畅的心路历程:AHK操作微信群第3集

微信图片_20220512152526

1.前情回顾

为……我写了个AHK脚本,每天在微信群里问同学们“你情绪稳定不”,同学们帮助我一起测试,也调侃我。调试的过程虽然不漫长但是曲折,值得一讲的故事写成了两篇博客。一篇是《写个脚本,问候你今天情绪稳定不》[https://zhuanlan.zhihu.com/p/488537616],介绍了如何用AHK脚本自动化定时向微信群发送消息,需要应@尽@。另一篇是《情绪稳定不稳定的心路历程 - AHK监听微信群回复》[https://zhuanlan.zhihu.com/p/496703430],介绍读取微信群中同学们的稳定回应,然后用正则表达式匹配找到不稳定或沉默的同学。image1在第二篇中报告了,当时有个相当对付的解决手法,活儿干得有点脏。背景是AHK操作在微信中选择聊天记录,复制到剪贴板。然后在AHK中按名单列表通过正则表达式匹配获取稳定不稳定。在这样的技术背景下,发现正则表达式匹配有问题,谁都能匹配到,无论他发言是不是“稳定”,并且匹配到的偏移量总是整个剪贴板文本字符串的最开头,索引下标是1。然而,保持同一个剪贴板的内容,贴到浏览器里,通过在线的 pcre online 工具匹配,没毛病,匹配的发言和位置都正确。又,把剪贴板粘到记事本里,再复制回剪贴板,也没毛病。 image2

如下图所示,从AHK操作微信得到的同一剪贴板,到正则表达式匹配,有毛病;过记事本,再到正则表达式匹配,没毛病;通过浏览器使用 PCRE regex online工具匹配正则表达式,也没毛病。

当时解决这一故障的手段挺对付,用AHK打开个记事本,把剪贴板粘到记事本里,再全选、复制到剪贴板中。对从记事本 过 过一遍的剪贴板做正则表达式匹配,达到了想要的效果。

然后终究是不优雅,意难平。

2.心路历程

故事的最终结局是 问题解决了,仅一行代码。难点是在哪里,加入一行什么代码。有趣的部分是定位故障的原因,就像侦探小说一样,到底谁是坏人不重要,重要的是寻找坏人的过程和论证、揭示的严密性。

那行代码在本文最后,接下来是过程。

2.1 验证性实验

要想解决问题,先要知道原因。不知道原理的实验,就是瞎试。瞎试的实验,说得好听一点,可以叫做“探索性实验”。这改改,那捅咕一下,看看会发生什么。婴幼儿和小猫对这类活动乐此不疲,借此认识世界。然而这不是干活的路子,因为一爪子下去是毫无反应还是天崩地裂,我也没谱。干活得有计划,至少要有预期。不然,就是纯玩儿了。

干活什么样呢。我们事先就知道一共就这么四五种方法,哪种方法有什么优缺点也知道,这个活有什么需要也知道。优缺点和需要匹配一下,就知道哪个方法最适合当前需求。解决问题也差不多。造成这个症状的可能一共就那么四五种,挨个试一下,有可能的话探测一下这四五种可能原因的特有现象,有哪个现象就是哪种原因。这是“验证性实验”。

严格地说,应该使用“证伪性实验”。所有的实验都不是为了验证你猜测的可能原因是符合事实的,而是尽可能设计刁钻的实验试图推翻猜测,如果没推翻,那么就可以暂时认为也许、大概、差不多就是这个原因,或者最不坏的就选它了。当然,证伪性实验的第一步与验证性实验相同,就是要先验地知道有哪些可能的原因。所谓”不重不漏”中的“不漏”在这里特别重要。如果你事先没想到,实验是万万什么也发现不了的,因为干活的成年人不探索。

微信图片_20220512152529

2.2 猜测-剪贴板里的不是文本

这就是瞎猜的,并非从知识体系中回忆起“此类情形有如下三种可能”这样的问答题。如果猜测正确,即剪贴板里不是文本,而是包括文本、格式,以及图像等在内的一些东西(也许不止富文本),就能解释为什么通过记事本以后能好使。事实上,我正是从记事本是纯文本想到了这种可能,也就是记事本与AHK得到的剪贴板的区别是什么,区别是记事本是纯文本的。

验证实验的话,第二篇博客的解决方案就是了。你看,好使,通过记事本把剪贴板转换为纯文本好使了,因此剪贴板原来并非纯文本就是问题的原因。

这个逻辑是错误的,“不重不漏”中有漏。漏了什么呢,把剪贴板转换为纯文本固然使症状消失了,A方法使症状消失,A方法就是原因么?用酒精擦身子能退烧,皮肤缺酒精是发烧(或不退烧)的原因么?喝咖啡心情好,缺咖啡是抑郁的原因么?

攻击的实验是这样的,不少博客说 “Clipboard := Clipboard”,这样一行AHK代码,或者类似的代码,可能把剪贴板转换为纯文本。我加了这行代码,不好使,经过记事本转化就好使。我们还可以补充猜测,这行代码不好使,博客不可信。但是,AHK官方手册中文英都这么说。手册也烂?有时候是这样,那么——有人可能接着想到的是如何解决呢,联系手册作者……但是故障现象这么漫长,而且是微信这种特定场景下,怎么描述呢。

接下来不是解决,而是分析原因。既然怀疑“把剪贴板转换为纯文本”可能不是原因,那么就该设计实验验证这一假说了。

微信图片_20220512152528

2.3 剪贴板里到底是什么

这是个验证性实验,探测(不是探索)一下剪贴板里到底是什么,什么格式,什么内容。我需要的是剪贴板内容查看工具。

找了几个,没有好的。在 sandboxie 中试了一下,有要要弹出广告,有的要安装服务,有的要常驻内存、修改启动项。Codeproject老牌好工具聚集地,要求注册。我的账号忘了,注册超时。

看不到。

2.4 穷人的必要技能,手动

 没有工具,创造工具吧。如果不能做到剪贴板“在线”时查看,那么,输出来。

image3

类似于电路的探针,在不干扰系统运行的情况下,我把剪贴板通过“写文件”操作,写到文件系统中。然后使用第三方查看工具看文件。

写出来了,写了两种。一种是 clipAll,AHK手册说是带格式的。文件内容是二进制,看不懂,得查MSDN看偏移量吧。搁置。另一种是AHK声称为纯文本的,也就是经过“Clipboard := Clipboard”变换过的 clipBoard。真的是纯文本! 而且第一种输出作为对比或者空白实验表明,AHK输出的文件可以是二进制的。那么,输出的东西是纯文本,只能是它原本就是纯文本,而不是在输出过程中转换了。

由验证性实验变成了证伪性实验,否定了我的猜测。我原本猜测“Clipboard := Clipboard”不好使,输出的东西不是纯文本。这样后面正则表达式匹配错误等现象就全都可以解释了。然而,这一猜测不能解释为什么输出了纯文本这一现象,或者说,输出了纯文本这一现象证否了猜测。

猜错了,也没有新猜测了,卡住了。

2.5 捋一捋

我们捋一捋都发生了些什么。

假设第一步输出的剪贴板与第二步输入的剪贴板是同一的,也就是说操作系统和AHK都没碰它,非易失的,这个暂不验证。

猜测过正则表达式匹配有问题,用PCRE regex onlin证否过了。把记事本里的文本复制到剪贴板里,在AHK代码里为字符串赋值,都表明正则表达式匹配没问题。

猜测剪贴板中的并非文本格式,刚刚证否了,就是纯文本。

探索!死马当活马医,再跑一步。把文件读回来。

image4

假设剪贴板写出的文件与读回的文件是同一的,那么
经过“Clipboard := Clipboard”剪贴板 = 文件 = 新的剪贴板

这样,

既然 经过“Clipboard := Clipboard”剪贴板 匹配正则表达式有问题,

那么 新的剪贴板 匹配正则表达式也会有问题。

这就是假设,验证一下。

再跑一步,把写出的文件读回来是探索,但是会发生什么,我有明确的预期。我猜测匹配还是不好使才对,这是验证/证伪。

神奇的事情发生了,意外。好、使、了。把不好使的剪贴板写出去,再读回来,好使了。就像经过记事本(末端相同)、经过浏览器(末端不同)一样,这中间发生了什么变换?我此前假设过记事本的作用是转换剪贴板为纯文本,但是这个假说被证否了,剪贴板本来就是纯文,差异(由匹配错误到匹配正确,由不好使到好使)的原因不是转换为纯文本。也就是说,在经过记事本,或者经过写读文件这个过程,中间除了转换为纯文本,还发生了点别的什么。

别的什么,是什么?

有的同学可能插话,好使了,那不就是解决了么。好使了,只是故障现象消失,我并不知道原因。莫名其妙消失的,也可能莫名其妙突然再次出现。就在你论文答辩现场,在你项目验收现场,在甲方大老板亲临现场的时候,在最重要的客户试用的时候,突如其来,让你措手不及。

微信图片_20220512152532

2.6 变小了?

我用 msgbox 作为探针把这几个东西都打出来,一个个看。由微信复制得到的剪贴板,经过“Clipboard := Clipboard”的剪贴板,写出的文件(用记事本查看),读回的文件……它们全都一样。

看起来都一样,但是表现不一样。说明,有我看不到的东西存在。

什么呢?一个字符一个字符打印出来……试了一会儿,太长了,我很快疲劳,鉴别能力下降,比对结论值得怀疑了。表面上这是探索,然后这是验证性实验,验证/证伪的猜测是“它们不一样”。

猜测,写出文件,应该是文件变小了,格式发生了什么变化,去除了 非文本 的什么东西。

我做实验,打印出这些东西的字节数。由微信复制得到的剪贴板,经过“Clipboard := Clipboard”的剪贴板,写出的文件(用记事本查看),读回的文件……

由微信复制得到的剪贴板最大,这是意料之中的。因为根据AHK手册,经过“Clipboard := Clipboard”变换,得到的 clipboard 去除了非文本元素,变小是对的。

意外又出现了。写出的文件,比写出前的剪贴板,大了。我以为会变小,这是猜测,实验结果证伪了这一猜测。好,所有的意外都提供了新的信息。

为什么会变大,增加的是什么,值得对比了。比对之前,我发现增加的字符数似乎与行数相同。有的同学到此可能已经猜到原因了,我当时没猜到。

我做了个短的剪贴板,1.用AHK转换为纯文本,假设转换成功。逐一打印出字符的ASCII。因为有东西看不见,所以ASCII更可靠;2。把写出的文件用 emacs hexl-mode (十六进制)打开。对比二者。

一目了然。拍大腿,这么简单啊。所有的bug,都是简单的,只是在定位以前我们不知道它在哪里。

以下是当时的日志截图。

image5

回车这个词是有二义性的。Windows操作系统下,回车是两个字符,CR Carriage Return 译为回车 13(0x0d),和 LF line feed 译为换行 0x10(0x0a)。Unix操作系统下,回车是单个字符,LF line feed 换行 0x10(0x0a)。

微信复制到剪贴板的文本,即使在windows操作系统下,也遵循了Unix的习惯,回车是单个字符。

2.7 解决

我写的正则表达式,匹配回车的时候只匹配1个字符。为什么在Windows下能工作呢?因为从微信复制到剪贴板以后,一直在AHK中工作,与操作系统无关。写入文件、贴到记事本里、贴到浏览器中,都涉及进程间通信,经过操作系统之手了。

既然知道了原理,就有了比写出文件再读回来更好的解决方法,当然比开个记事本不能碰机器更好。

只修改了一行代码,确切地说,增加了10个字符,匹配任意回车。

新的正确写法 needle := "(*ANYCRLF).*" element ".*:\R([^不:]*?)稳定"
旧的错误写法 needle :=           ".*" element ".*:\R([^不:]*?)稳定"

回顾一下整个实验和代码中的数据流,如下。在数据流图中,特别关注变换,数据的类型的变换。

image6

3.未来

修改以后,代码跑了一段时间了,代码稳定,我们也每天说自己稳定。

昨天,发现了两个bug,暂不修复。不过挺有意思,所以在这里讲一下。

都是剪贴板的问题,在转换成纯文本以前就出毛病了,不过仍然可以通过剪贴板贴到记事本里作为探针查数据来诊断。

第一个bug是有位WT同学明明回复了“稳定”,但是监听脚本仍然报告他不稳定或者未回应。有诸多可能的原因,我首先要排除 剪贴板的内容错了 这种可能性。选哪里开始,是另一个漫长的话题,以后再说。

对比粘到记事本里的剪贴板和微信聊天记录,可以发现,WT同学在微信聊天记录里回复了“稳定”,但是粘到记事本里的剪贴板中,他的发言刚好没了。前一名和后一名同学的发言都在,WT同学连人名带发言都消失了。猜测是昨天我们大家讨论的内容比较多,在AHK询问的时候仍然在频繁互动,所以攒下了不少聊天记录。聊天记录长到AHK由下向上滚动复制的过程,需要翻页才能到达询问的时刻。在触发翻页展开这个动作中,跳过了一些(这个案例中是一条)消息。

感慨 生产场景比测试场景更复杂,充满了我们没有预料到也因此未做约束的情况。

第二个bug是有位TYQ同学,他的回应刚好在22:34,这正是AHK开始操作微信群复制聊天记录开始的时刻。TYQ回复的时候,AHK代码已经开始向上复制内容,所以没有抓到他的消息。

第一步 我开始复制;

第二步 他回复了,我没复制到;

第三步 我的正则表达式匹配,稳定数组里没有他;

第四步 我发报告,稳定数组里没有他。

TYQ同学总结,这类似于线程安全问题,在AHK汇总的时候,有线程乱入了。我的AHK代码不是原子操作,也没有在开始汇总(复制、匹配、报告)之前冻结聊天现场,因此有微小的几率,就是在22:34:00 到 22:34:02 大约这么长的时间内,回应的消息不能被我的AHK代码采集到。

感慨 即使几率微小,在大样本的情况下,也必然发生。

微信图片_20220512152530

这样,做出发送模块,又做出了采集模块和匹配模块。最后解决了采集模块和匹配模块这两个模块间的接口剪贴板问题。解决最后这一行代码10个字符,过程曲折,我日志的部分标题如下。猜测,然后证伪,不断尝试,直至找到坏人。结果甚至是次要的,这个过程充满趣味,令我乐此不疲。

image7