听出打电话拨的什么号码,以及巫术/神迹/天赋/技艺/科学/技术

有些画面,有些声音,只有你具有那些知识,或者受过那些训练,才能体会其中的情感。只有这样,你才参与历史其间,它也才能融入为你的一部分。

  1. 缘起

曲同学分享了一段视频,感慨了一代老家伙的共同记忆。是一段声音的的频谱图,把时间通过在空间中的延展表现出来。时间从过去到未来,频率从低频到高频,构成平面。在这个平面上下凸起凹陷的,是特定时间、特定频率的声强,像山峰或者海浪。动态频谱图对应的声音,也同步播放出来。

image1

典同学说,能听出来这段声音是什么的,年龄也都不小了。

先是持续的"嘟--"电话拔号音。在手机没有流行以前,有一种叫做座机的电话,没有屏幕。与有屏幕的手机不同,需要先把电话听筒拿起来,响起表示线路空闲的"嘟--",此时才能拨号。

接下来是拨号,滴滴嗒嗒嘟嘟。

最后是zizi~~gagadidi持续一顿响。这是你的调制解调器在与对端的调制解调器对话,然后就建立连接,可以开始传输数据了。当年的163、169、互联网,还有更古老的BBS就是这样通信的。在《魔鬼终结者》等电影里也能听到这个声音,当时还挺先进的。如今这种联网方式甚至电话本身,都已经成为了时代的陈迹。

我听了一会儿,感叹了一会儿,说出了这7位电话号码。

157 xxxx xxxx。归属地重庆,可能是刚刚或者最近两年做着玩儿的?

微信图片_20220427021403

  1. 巫术、神迹、天赋、技艺、科学、技术

以前有过新闻,说有个小伙在新闻里听到有人拨打某企业家的电话,他靠耳朵听出了号码,给那位企业家打电话,得到了不错的工作岗位。当时谁给我讲的这个段子来着,我可能没有表达出预期的惊讶。

当年有人做了个3d游戏,应该就是给游戏引擎加了些素材,可能脚本都不一定需要。当时NZG推荐给我,我看了以后也没有表达出应有的尊重。NZG同学说,你行你上啊。我觉得不值得,没意思,还挺花时间的,没有也做一个。

听电话号码这个,记得我当时讲了下原理,对方也没有表达我期待的认同。

你行你上。还是这样,如果我不展示一下我也行,挺容易的,你就会觉得我是吹牛。如果不是你花个十几分钟就能学会,你就会觉得这玩意是巫术、神迹。

原理非常简单,我一会儿说。

那小伙能用肉耳朵听出来,确实是需要一些技艺的。技艺,就得花些时间训练。受过音乐训练的人,学习这个应该很容易。米特尼克、乔布斯、莫里斯这些老一辈的黑客传记里提到过一个人,他也能听出电话号码,作者特意提到这家伙的家里有一架钢琴。还有一个人,能用口哨吹出早期电话的调试员的拨号音接通免费电话。

需要特定人物实施,无法由你通过常规办法检验的,我说了你就得信,那是巫术或者神迹。

需要特定人物实施,你可以通过常规办法检验的,是天赋或者技艺。天赋可以重现验证,是你可能永远学不会。技术能学会,可能要花些时间。肉耳朵听出电话号码,还有绝对音高听钢琴、小提琴,甚至能听到椅子腿摩擦地板的和弦。这种谁都能检验,但是除非训练你不能做到,甚至训练也做不到的,是天赋或者技艺。要么你也做不到,要么挺难的,就像知道生在伯努利家就有出息,或者苦学数十年就知道自己的数学天份不行了,所以知道也没有用处。

借助一些非常容易找到的工具,不要求是专用工具。或者没有这些专用工具,替代品也非常容易找到,借助这些,几分钟就能识别出电话号码,你也能,谁都能。这是科学或者技术。科学或技术与巫术、神迹、天赋、技艺的区别就是,借助(根据原理研发的或者现成的普通)工具,而不是借助信念、不可改变的素质、极难的训练,就能在短时间内由任何人类达成。

只要我行,你就也行。

微信图片_20220427021405

  1. 技术路线

操作是这样的,一共五步。如果工具装好了,就是几分钟的事。比肉耳朵慢,但是绝对有效,甚至更有效。

第一步 保存为mp4

文件命名为modem拔号音.mp4。

第二步 转换为mp3

ffmpeg -i modem拔号音.mp4 m.mp3

ffmpeg也可以替换为任意一款格式转换软件,用录音机也行。

image2

第三步 切一段

我用audacity(GNU开源、免费)在m.mp3上把拔号音那7个音切了下来,从时域的波形 (横坐标时间,纵坐标声强) 上,用视觉比听觉更很容易找到是7个音,在哪一段。用听力确认切得对。

audacity也可以换成任意一种声音编辑软件。这一步可以省略,也为了发博客的时候如果能贴声音,可以更容易说清楚是哪一段。

image3

第四步 频谱分析

audacity 菜单,分析|频谱分析,找到2个峰,记下这2个频率。如下图,左上角从7个拨号音中选择了一段拨号音,频谱分析的结果2个频率的尖峰分别是698Hz和1337Hz。

下图,选中的是左边的尖峰,测量显示为698Hz。

image4

下图,选中的是右边的尖峰,测量显示为1337Hz。

image5

这步操作,也可以换成origin、MATLAB、python之类的软件或语言完成,只要能做FFT的都行。Excel也行,麻烦一点。还有个办法,不需要工具,只需要耳朵,需要一点听力基础,参见下一步。

第五步 参考DTMF码表

698Hz和1337Hz,这两个频率构成“和弦”的声音决定了唯一的号码。

参见下表,接近698Hz的是697,接近1337Hz的是1336。697这一行和1336这一列的交点是2。这个“和弦”声音对应的拨号是2。

image6表格来自[https://www.cnblogs.com/xiangyuecn/p/13200894.html#%E4%BA%8Cdtmf%E9%A2%91%E7%8E%87%E6%8C%89%E9%94%AE%E5%AF%B9%E7%85%A7%E8%A1%A8]。如果链接失效,在网上搜索 “DTMF码表”,有很多公开的页面。

按下拨号盘上的每个按键,都会同时发出2个频率的声音,叠加在一起。按2这个数字,发出的声音就是697Hz和1336Hz。这个声音与我上一步从7个拨号音中选择了的那段拨号音“听”起来一样。是的,需要点听力,不完全一样,就像小提琴拉do和钢琴弹do,音色和音量都不同,但是你能听出来是同一个音。如果这有点难,听出来两个按键的音不同,你一定能。

所以,上一步我提到的不需要工具的方法来了。在网上找个 DTMF声音生成器 (DTMF tone generator),有许多在线版本。挨个键按一遍,听听哪个和这一段的声音“听”起来一样,就是它。或者挨个排除那些“听”起来不一样的,剩下的唯一那个就是了。一般地,你不需要上述16个数字,只试0~9就行了。

这样,我就得到了视频中的电话号码:157 XXXX XXXX。

第六步 复现

这一步对于听出电话号码并非必须,只是为了说服你真的有效。

用audacity测量,得到每个拨号音持续100毫秒时长,空白100毫秒。

下图是拨号时长0.96秒,约100毫秒。

image7

下图是空白时长0.96秒,约100毫秒。

image8

找几个拨号音生成器,发出声音来,你可以用肉耳朵听出来是不是一样的。这些拨号音生成器(DTMF tone generator)也可以用来在第五步中不经过频谱、仅使用肉耳机比对。

[https://www.venea.net/web/dtmf_generator]

可调节拔号间时长和空白时长。不能保存。可使用录屏,导出声音。

[https://www.audiocheck.net/audiocheck_dtmf.php]

可下载wav。

[https://aggemam.dk/code/dtmf]

php的,可下载au文件。

微信图片_20220427021406

  1. 原理

原理说起来也不复杂。每个拨号音都由两个频率的声音唯一标识,这就是DTMF中的DT(Dual Tone)双频所暗示的。频谱分析,或者FFT(快速傅里叶变换),可以由时域(横从标时间,纵坐标强度)向频域(横坐标频率,纵坐标强度,可以简单假设这小段声音期间频率不变)变换,从而方便测量出这两个频率是什么。然后查表就可以根据这两个频率知道对尖的按键。

image9

如上图所示,时间、频率(低频、高频)、强度,形成三维空间。线路空闲是双频长时间持续的,与第一个拨号音有部分重叠,用灰色文字标出。图中的红圈内,是3个拨号音,每个号码有两个频率。

事实上,人类的肉耳机是优秀的频谱分析仪,所以虽然脱口说出频率的精确数字有点困难,但是判断未知号码含有两个频率组分的声音 和 另外由生成器得到的9个样本中的哪一个相同,是较容易做到的。

即使有绝对音高听力,由于这些音与钢琴上的按键都不一样,听起来可能是某个键的跑调,大概不太容易识别。从这一点上看,使用工具的科学和技术不仅人人可用,也更加稳定和有效。

有的同学可能会说,如果科学与技术如此强大,磨练技艺无论多久,也比不上凝结了人类智慧的工具吧。练一辈子,在围棋上能战胜阿尔法狗么?如果功利的看,非常可能是这样,人生而有之的天赋和后来艰辛训练方能点滴进步的技艺,在几千年以来正在被人类发明的工具不断超越。似乎人类除了大脑,其余的都不足道哉。并非如此。发挥天赋,磨练技艺,让自己成为更快、更高、更强,更有趣的人,这些本身就很好玩,本身就是目的。并不是要让自己成为更强有力的工具,而是我们自身,就是目的本身。同样的,发挥智慧的天赋,磨练智慧的技艺,与锻炼肌肉、手指、眼力一样,也很有意思。

有些画面,有些声音,只有你具有那些知识,或者受过那些训练,才能体会其中的情感。只有这样,你才参与历史其间,它也才能融入为你的一部分。

微信图片_20220427021400

推荐什么书

一觉醒来看朋友圈和前辈留言才知道,今天是世界读书日。知道世界读书日,保持每天读书,却不知道世界读书日是哪一天。看不少公众号和朋友推荐了有少书,我是不是应该也应景推荐几本?

去年底今天初的时候,读书群主X老师说,大家推荐几本一年来喜欢的书吧。挺有仪式感的事,后来我没有响应。为什么呢?每一天,不过是个平常的日子,或者我装作如此吧。就像我和我的老学生、在读学生们背单词打卡,每周五 (就是最初打卡的那一天)发红包。有时候恰是个好日子,想庆祝一下,但是想到这样的好日子以后不知道还有多少,觉得小题大作,或者可能今天就是从此以后最好那天,心下黯然。照例在红包上写"一周单词"。每日读书,如此平常就像谁还会关注和纪念喝水、吃饭、喘气、父母的爱呢。

又,我喜欢的书,你可能十多年前早就读过了,我推荐的书,可能太低于你的层次过于基础。还可能,你推荐的书,确实需要相当基础才能领悟,我此刻读起来味同嚼蜡也说不定。

但是且住,果真人人如此么?

微信图片_20220423170550

这样不换位思考的看法,我确实见过一次,刚好当时屁股坐在换位的另一方,才意识到。所以读书这事,也不见得人人认为是平常。不换位思考的小故事,也刚好与书有关,今天可以当个段子讲来。

在豆瓣上看到的。有位网友说,根本就不应该存在市立、区立图书馆这种东西,里面的书都没有意思,反正喜欢读书的人家里肯定有个图书馆。作者是位嗜书如命的青年,年轻轻的已经坐拥书城了,而且还在加紧买。我读的不过是一本又一本,他研究的已经是这个版本和那个版本了。我也同意市立和区立图书馆的藏书对我没啥吸引力,要么没意思,要么我看过了。不过,除此以外的观点,我忍不住反对了。

并非人人生而有书可读。如果不是通化市图书馆及其少儿部,我的小学和初中能读到的书可真是非常有限了。半本《三国演义》、缺后半截的《水浒传上》、被我们当成教科书一样的小说《警犬黑豹》,还有小人书《铁腿红心》《人民的好医生李月华》《江南七怪》。还有《魔方大厦》,是一年六一的礼物,哭着喊着要来的。我妈说,你连故事内容都猜出来了,看着还有什么意思呢。如果没有图书馆,我的阅读大概就止于上述范围了。

书本,打开了通往另一扇世界的大门。我当时还看不懂的《丁丁历险记》让我神往了很多年,等自己识字。缺结尾的《倒长的树》,在同学家翻了半遍的《星球大战》,在WTD同学家看的计算机入门书,CJ家的《星际旅行》 (即星际迷航1)都带给我几十年的深刻影响。初中和高中学校的图书馆,更是展现一片阔野,万里风光。

微信图片_20220423170534

毛姆说得对,那是尘世艰苦生活的避难所。别人是如何生活的,历史上的那些英雄和巧匠,其他地方的风物,爱琴海的巨浪,极地的雪原,西伯利亚的莽林。格立高里直举的战刀第一次劈下,日本科普书里暗夜中的星座旋转。草原上的废弃建筑是小伙伴们的战舰。啊对了,还有叶圣陶的稻草人——世界上还有这么不温馨不柔情也不粉红的童话。但那是我喜欢的能看懂的第一篇童话故事。

这就说到推荐什么书了。

看你能看懂的,看你喜欢的。只要还在阅读,只要还在体味和感知这个外在的世界,而不是单纯沉湎于对自己内部的纠结,那么修行就还在继续,一切还不错。

微信图片_20220423170546

你喜欢什么呢?从别人的推荐上是看不出来的。

通常我们看到的推荐列表都是人文类的,历史、政治,如何认识人类社会,该当如何做人。这里推荐里深刻地包含着推荐人的世界观、价值观,他认为世界如何运转,世界应该如何。对于教我做人的,即使他身体力行,我也经常充满反感。我不想做自己反感的那种人,所以对推荐非常慎重。我经常想对推荐的人说,你推荐的我看过了,你说的我也"知道""了解"(经常地我还知道点别的你不知道的),然而我还是不认可。事实上,哲学之争论如此深刻,你拿出哪个被驳倒无数次的烂哲学家的观点,把我们打得体无完肤也都轻而易举。我只是懒得举例,如果讨论以扣帽子结束,"你这个异教徒",归于信仰、信念、你是坏人,又有什么意思。

推荐人文类的可能有个好处,就是人人可读,大家都认为上面的字认识。即使懵懂如我,也可以说读过……我可别吹了,哪本也不敢说读懂了。但是,这不妨碍我一页页翻下去,和理工科的著作可不一样。我可以吹嘘说,康德的《实践理性批判》谈的是道德,是关于"头顶的星空和内心的道德律法"一样是实体,是实实在在的存在的。康德反对把人作为工具,认为人只能是目的本身。又怎么样,不影响我把别人当工具,不影响我看着别人把别人当工具而怯懦闭嘴,不影响我看着别人一边嘴里讲康德一边行动上反康德而别过脸去,甚至不敢别过脸去。如果能超脱书本与世事脱离的痛苦的话,那么倒是容易装作读过的样子推荐。我可以读一下邓晓芒的演讲,或者听听他的语录,或者读一下几十页的小册子、二三千字的公众号文章,精华俱在,似乎我已读完康德上身,推荐三大批判,年轻人,一定要读一下啊,不然你还能算是个人么。嗯。

推荐人文类可能还有个原因,就是推荐者希望你也成为他那样的人,或者他希望你相信他是他所声称的那样的人,那么善良、睿智、博学。

微信图片_20220423170525

至于他自己读过没读过怎么考核,有个办法,挺古老的:听其言,观其行。他会用他推荐的书解释他所看到的世界么,他按他推荐的书行事么,他除了这篇推荐还在别的地方讨论过他推荐的书么。除了过年,还有什么会期待家人团聚、国泰民安、如你所原?难道,不应该每天、每时、每刻。一个只有过年才会唠的嗑,有多么廉价。我以前一直觉得三毛没事就怀念追求过她而终身未婚等着她的男性(不止荷西),感觉她挺自恋的。后来才渐渐明白,这里的坏人不是三毛,而是那些男性。没事说一句"达令,我一直怀念你呢"有什么用处,有什么价值。你是夜夜睡不着的时候就怀念一下以至于更睡不着呢,还是除了这句达令以外,其实连怀念也没有。推荐给你的那些书,推荐人自己也像希望你做的那样,自己亲自再三阅读么?

考核他读没读过次一点的办法是,考试。略过。

微信图片_20220423170536

推荐人文类可能还有一个原因,就是他真的喜欢,只不过喜欢的刚好是人文类的。前几天在群里有人说,要捐一批书到监狱去,问什么比较好。刚开始大家开玩笑,《基督山恩仇记》,要归到教育类里,要不要配个铁勺子一起阅读。还是《圣经》挖洞藏小地质锤更适合。后来严肃的讨论,TY老师提到要书法类的,有老师提到要人物传记。我提到想看物理化学地理历史。

人文类的对我来说太难了,如果需要心眼儿和对人性的体悟,那就难上加难。这么说吧,色盲非要让天天挑红花绿叶,不喜欢抽象非天天让做数学,不喜欢逻辑非让你天天编程,先天性心脏病非要让锻炼锻炼去跑马拉松,你能高兴么。有些英文小说,翻译成汉语男生都看不懂,为什么非要那么难为自己呢。《简爱》《傲慢与偏见》,还有《红楼梦》。并非巨著不好,是我不好。

微信图片_20220423170538

本轮疫情开始时,我也立志正是读书的好时候。我低估了身边随时可能到来的危险对我的干扰,低估了饥饿对人的意志能削弱的程度,低估了领导对给你找足够的事儿做充满你的生活有多么自信和执着。加上疲劳,几乎摧毁了我长期保持的锻炼计划,更不用说阅读。我只读好玩的,不累的,容易读的。

照着书刻木头,刻了四只猫头鹰,一头猪。我想,集中注意力于物外,是不是就不被物所役了。后来有天,一刀削在手套上,刀刃贴着皮肤一掠而过,留下一道白茬。手指分毫未伤,但是我一下就想到现在出不了小区,去不了医院。刻木头的是重新开刃的瑞士军刀,每次雕刻以前都要再磨一次。这么飞快的刀,一刀切到骨头非常容易。想起了二猫妈告诉我最近不要锻炼对心肺、体能、技巧要求太高的动作,可能送不了医院,可能还没等核酸结果出来就结束。冷汗涔岑而下,呆坐半晌。

微信图片_20220423170532

群里讨论书的时候,我还提到不要励志的,正能量过多,看着闹心。最近,看电影都困难。不是因为片源,而是因为心境。看英雄主义的,怕自己冲动;看深刻反应人性的,太压抑了,他们怎么能那么坏啊;看现实主义的,可xxxx吧,看看窗外不就得了。看大部头,经常怀疑自己,有用么,知道不知道毕生所学就在此刻。

应该只读自己能读的,读起来轻松,因此能长期保持的。无须考虑有用。日有所进,无须顾及功会不会唐捐,无所考虑最后会是个什么结果。每天只有半小时一小时,你还想练出什么专业什么名堂么。享受这个过程吧,喜欢什么就读什么。

我就喜欢理工科的书,心烦的时候就只读这个。三两能完成代码的小项目,技术手册,中英文古汉语词典。一章一章的小说,每次只看一章。不分章节的书,每次看半小时一小时,不贪多。

做题,写笔记。痛骂作者写得烂,都很好。

微信图片_20220423170543

当年和计算机系的同事们一起去伪皇宫,大家感叹溥仪有个媳妇关冷宫后来疯了。我当时说,找本高数书,天天做题 (当时还没有刷题一词),有的是事可做,怎么会疯。如果有这样的体力,我并非指意志,如果有这样的体力,刷题挺好的。就像刻木头,画画,写小说,写代码,刻意锻炼某组肌肉,练琴,令人心静。《双城记》里那老头关巴士底狱里,难受就修鞋。后来他出狱了,难受也修鞋。挺好的。

专注于手头的这一本书,这一件事。不为了未来的功利,仅仅享受一页页一行行融在你生命里,这就是修行这就是生命本身。善哉。

微信图片_20220423170544

如果说非得推荐一本。上次你看别人吹牛读过,你自惭形秽没读过的那本是什么来着?就它吧,或者介绍它的书。

疫情期间买不到?不要违法犯罪,不要能从网上找到电子书,但是要始终保有这样的能力。

如果非得推荐一本,那么往往只是推荐者当时的心境,不见得是人类文明史上多么重要的作品。就像肉蛋奶才是最重要的营养,但是不少人拼命想喝的是不健康的可乐。如果非得推荐一本,我推荐读读《一九八四》,如果读过,不妨再读一遍。

微信图片_20220423170553

情绪稳定不稳定的心路历程 - AHK监听微信群回复

情绪稳定不稳定的心路历程 AHK监听微信群回复

其实我们都知道,情绪有时是只薛定谔的猫,你不问还稳定点,你一问就不稳定了。然而,我还得问,你还得答,我还得知道你答了稳定。如果解析一下这个过程,觉知其中的细节,放到宏观的技术背景中,也许能感觉情绪更稳定一些吧。

一、问题的提出

上回书说到,每天 1024 pm,AHK脚本向微信群发消息,应@尽@,问每位同学情绪是否稳定。然后仅仅这样并不是关心询问,而是通知。通知的意思是,你得知道,至于你是不是知道了,那我不关心,也就是说你爱知道不知道。这怎么能行呢。我得问,你得答,我还得知道你的回答。如果我不知道你的回答,就是光运行程序,不关系代码的运行结果。所以,得想办法 监听微信群发消息的回应。

以下是心路历程,想过几种技术路线,以及对这几种技术路线的讨论。按理说,对技术路线的讨论应该只包括(1)该方案的优点,(2)该方案的缺点,(3)当前问题的需求、条件、限制。从而帮助选定技术方案。然后,实践操作的时候,还要考虑 可行性。比如,你不会,资料缺少,还有代价。

二、技术路线讨论 心路历程

考虑过几下几个方案,有的还走了两步。最后一个方案实施成功了。

1. appium

这是田HX同学的方案。在上一版本中,他同时发布的不仅包括群发,也包括监听回应。他使用了夜神仿真器和真手机两种方案。夜神仿真器中跑微信,被微信识别出来,锁了两次账号,我们不敢再尝试第三次了。手机,需要他插着usb线,保持debug模式。然而我觉得占用他的手机,而且还要插着线,对学生利益侵占过多,已经超过技术训练的范围了。

他的方案在这里[https://www.cnblogs.com/ourshiningdays/p/16023291.html],实测效果非常不错。在我完成监听回应脚本以前,每天就是田同学的脚本在跑,大大降低了我一个个查"稳定"的工作量。我的脚本,不仅对监听回应的报告,还有群发消息,都模仿了他的文本。

如果仿真器和真机都不适合当前问题,那么appium这条技术路线就不可行了。

到这时,我一行代码还没写呢。

2. OCR

要分析回应的消息,先要取得微信群中的历史消息。appium通过遍历控件或先验的知道从哪些控件中取得文字。如果不使用appium,那么如果取得学生回复的"稳定"或"不稳定"或无回应呢。

还是在windows操作系统下使用AHK脚本 读 微信客户端的群消息。WindowSpy.ahk表明,右侧的群消息与左侧的通讯录等,在同一个窗口之中,不可拆分和遍历控件。

既然不能拆分和遍历控件,我们可以向 第一性原则 走得更近一些,更接近问题的底层或者本质。这种程度的简单和朴素需求,无非是计算机对人类动作的模仿,包括模仿人类的传感器 (视、听)和动作 (鼠标、键盘)。在这个问题中,人类是如何监听微信群消息的呢?

用眼睛,图像识别。

所以最初想到是OCR。先对窗口截屏,然后OCR。这样就得到了群消息的文本。至于视域以外的历史消息,可以用AHK向上滚屏,这个技术风险不高,虽然没实测过,但是估且不必更多考虑。

不要!找个OCR接口就开始编程。这是不少初学者的习惯,发现个钉子,看淘宝上有个锤子,就赶紧下单了。万一不好使呢,这些时间投入就白费了。虽然你可能觉得在探索的过程中"学到了",但是……说起来话长,此节估且搁置。

找这个编程接口的demo成品,测试一下效果,以确定技术可行性。我们测了几种。位JY同学报告他用过微软的库,马HB同学报告他以前用过微软的OneNote,效果不错。我实测识别错误不少,不能达到实用的程度。原因猜测可能是微信群消息截图中有浅色的文字。我找了几个在线的OCR,上传截图,错误率同样不低。OCR效果最好的是 手机 讯飞输入法 的拍照输入,错误率最低,可以容忍。讯飞也提供在线API,需要认证等准备工作,可能涉及到免费限额和经费支出。这条线索暂时搁置。

到这时,我一行代码还没写呢。
微信图片_20220411171333
3. OpenCv 模板匹配

回到OCR技术路线的第一性原则,我们需要不一定是OCR图像到文字,而是图像的发现能力。如果能找到头像,在微信里点击头像,在弹出的窗口(WindowSpy.ahk表明有单独的窗口)中可以选择和复制文字。这样就知道了是谁在回复。回复的消息,可以通过鼠标选择和复制得到。

在后面的进一步实验,并非OpenCv的实验,在进一步的实验中想到并实现,不必击头像得到文字,只需要通过图像发现头像,自然就知道这个头像是谁的。这是后话。

OpenCv如何找到头像呢。要么有OpenCv能力的知识结构,要么想想网上的贴子和手册会如何描述这个功能,搜索。

这个功能叫做 模板匹配,在大图中找到小图,在窗口截图中找到头像。

查了一下网上的贴子和手册,模板匹配,需要查以下几点。

(1)什么样的入口auguments,如何向模板匹配算法/函数传递大图和小图。在查之前有猜测,要么是数组一类的,要么是URL或者文件地址,要么是图片对象。要么就是没猜出来得细读探索,而不是验证猜想,麻烦会大一点。如果是数组或对象,要考虑跨语言编程的类型映射或转换。如果是文件地址,要考虑当前目录以及文件不可达时的错误处理或者约定。

(2)什么样的出口return。小图在大图上的坐标,以何种方式传出。是二维坐标对象,还是rect,还是两个数字,是float (是的,没写错,像素可能是float),还是int。如果有多次匹配,如何传出,用线性表么,如何排序,按匹配的程度由高到低,还是按大图中的坐标从左到右从上到下。编程的过程中,如何检验出口值正确,是打印出来,还是叠画在大图上。

(3)工作过程,模板匹配有几种不同的算法,哪个最适合当前问题。最大的可能是,由于当前问题如此朴素,所以哪个都行,都差不多,都足够好。可能需要担心的是,这么多种算法,每种算法的门限这一类参数parameters,需要通读文档,确定所有这些参数的物理意义,别写错了。

找了几个例子,读代码。这几个例子都是python的。要不要ahk调用python,通过python调用OpenCV,从而降低跨语言类型映射和转换的工作量?查了下ahk调用OpenCv,需要用到COM,入口和出口的类型转换都不少行代码。当然可以抄,然后不优雅。

到这时,我一行代码还没写呢。
微信图片_20220411171331
4. AHK ImageSearch

在上述过程中,偶然发现一个贴子,提到AHK可以匹配 (原文用的似乎是"搜")图像,在大图里查小图,这个函数是ImageSearch。这就是我知识结构残缺、读手册不完整的恶果。居然不知道,或者知道过然而忘了。以前试用过一个日本人做的AHK类软件,能在屏幕上匹配图像,移动鼠标过去做点击一类的操作。也忘了是哪个软件,再也没找到。现在,AHK也具备了这个功能。应该是后加的功能,我曾经通过读AHK手册,当时还没有这个功能。

与OpenCV对比,ImageSearch的缺点是,不能匹配"差不多"像的图。算法不够复杂,估计就是像素比对的,开个二维窗口,KMP之类的。然而这足够解决当前问题。从OCR到OpenCV到ImageSearch,由功能强大而具有不确定性到功能弱小而结果确定。ImageSearch要求像素完全匹配,当前问题的条件和约束是在窗口截图里找头像截图,在无损压缩的情况下,输入足够好。至于俩人头像特别像,ImageSearch比OpenCV解决得会更好一些。至于俩人头像一样……有个老乌鸦教小乌鸦如何防范人类的故事,以后讲给你。

到这时,我一行代码还没写呢。向同学们报告,我就选这条技术路线了。偶尔在群里做实验,打扰各位。开始实验。

截YP同学的头像,截微信群消息窗口的图。写AHK技术原型代码,调用ImageSearch,找到了。再写技术原型,多次匹配也找到了。第3个技术原型,从特定坐标向下找,方便遍历,也找到了。32行代码。

到这时,针对当前项目的需求,我一行代码还没写呢。

分解需求。

(1)需要确定分辨率

因为ImageSearch要求大图中的目标与小图源图的大小相同,又鼠标点击操作,甚至包括偏移多少像素,都对屏幕分辨率敏感。所以要么针对不同的分辨率做标定,要么简单粗暴的做法就是,如果分辨率不符合要求,就要求用户改。这是给我自己用的脚本,冻结这一需求,就用我的当前分辨率。 最大化窗口,这样能少滚动几次。

(2)往上翻到所有的回复消息之前

往上翻到 ,多翻几下,假设能超过 询问消息 “今天你情绪还稳定么?"。由此向下截图。

进入群以后,把鼠标光标移至文本光标的上方一点儿 (这个一点是根据分辨率标定的,比如100像素),鼠标光标就进入了历史消息。此时可以上翻历史消息。

怎么往上翻,用鼠标滚轮,还是PageUp。如果滚轮,每次会滚动多大,按比例,还是按像素。如果是PageUp,我没想到,PageUp和PageDown不是对称的,每次PageUp翻动的长度也不稳定。

为什么要对超过 询问消息 “今天你情绪还稳定么"做假设,而不是判断是否到达呢。因为这时还没有技术原型支持取得询问消息。只能知道那是我的头像,却不能确定这个头像发出了这条消息。如果我在这条消息以后发出过别的消息,那么由向下向上找到的第一个我的头像就不是这条消息。如果要求我在发出这条消息以后不得发出其他消息,那么就是对用户需求做限制。这个限制有点过份,用户/我不会妥协的。

找特定的人。由下向上,找到一个头像,看看消息是不是想要的。如果不是想要的消息,再往上找下一个头像。如果一直没找到,那么滚屏。下面是这个需求的代码片断。因为写着写着发现比我想得复杂,所以加了不少调试信息,"正找呢""没找到""找到头像了",这些调试信息注释以后也相当于……相当于注释了。

: ; 鼠标移到 历史消息,操作滚轮向上
: gap = 100 ; 由 群消息编辑 到 群消息历史 的偏移像素,根据分辨率校准
: MouseMove, A_CaretX,  A_CaretY-gap, 0
:
: ; 在历史消息中找今天群发询问的位置
: ; 找到 群发消息询问人 头像,根据消息确认
: not_found :=0
: y := 0 ; 当前可见范围内如果有若干个头像符合,从y开始找下一个
: loop {
:      ImageSearch, OutputVarX, OutputVarY, 0, y, Width, Height, 杨贵福.png ; %sender%
:      ; 未找到,上滚,继续找 100次以内
:      msgbox ,,, 正找呢
:      if( ErrorLevel!=0 && not_found<100) {
:         msgbox ,,, 没找到,滚屏再从上面重新找
:     not_found += 1
:         y = 0
:     Send {WheelUp 50} ; 根据分辨率校准    
:     Sleep 1000
:         continue
:      }
:      ; msgbox %OutputVarX% %OutputVarY%
:      ; 找到头像了,根据发出的消息确认
:      ; 消息文本在 鼠标向左1个头像宽度,向下1/2个头像高度,根据分辨率校准
:      ; 假设群发消息的是"我",因此消息在头像左侧
:
:      ; 取消息文本
:      w := OutputVarX - avata_w
:      h := OutputVarY + avata_h / 2
:      MouseMove, w , h , 0
:      ; msgbox ,,, %w% %h%
:      clipboard := ""
:      ; 不仅可能是文字,
:      ; 可能是图片,没有想到关闭的方法,所以复制不用 ^c,而用鼠标右键复制
:      ; 还可能是链接、表情,右键菜单都不同,因此鼠标右键也不行
:      Click , Right
:      MouseMove, 0 , 10 , R
:      Sleep 1000
:      Click
:      Sleep 1000
:      msgbox ,,, %Clipboard%
:     
:      if(InStr(Clipboard, question)) { ; 消息匹配
:          msgbox ,,, 终于找到消息的开头了
:     break
:      }
:      else
:      {
:          msgbox ,,, 消息不对,继续向下找
:          y := OutputVarY + 1
:      continue
:      }
: }
:
:
: ; 在历史消息中,从今天群发询问的位置开始向下滚动

(3)消息文本

在上一需求的代码编写中,发现了一个悲剧。有个技术原型没有测试,原以为风险不高,结果不可或缺。

如何复制出消息文本。人眼是能看到的,就在头像旁边。问题还可以更简单和确定。我的在窗口右侧,消息在头像左边。同学们的回复在窗口左侧,消息在头像右侧。偏移量确定。

但是!消息不仅可以是文本,还可能是图片或链接。鼠标双击的如果是文本,那么会选中,接下来可以复制。鼠标双击的如果是图片,会打开图片,并且不自动关闭。鼠标双击的如果是链接,会打开浏览器 (在我的机器上这样配置的)。难点是,AHK不知道要双击的是文字、图片,还是链接。当然,你可能会说,可以用OpenCV什么的判断那是文字、图片、链接。然后,技术原型阶段已经过去了,现在是写代码实现功能,这样的意外会导致计划变更,还需要评估这部分的工作量、可行性、对需求和执行环境施加的新约束。

换个操作方式呢?鼠标右键,弹出的上下文菜单中,有时是复制,有时是删除(如果是未下载的图片)。如果消息是文本,消息的长度 (?)不同会导致有时上下文菜单在左,有时再右。再用OpenCV判断上下文菜单的位置?

没有把握的技术路线,需要的时间是无穷无尽的。这种不确定性大大降低了技术工作的愉悦。没把握,这是第二不爽。第一不爽是被监督和被指导。

复制不出来文本,就没法识别回应的消息是稳定还是不稳定。这条路线有事先未考虑到的细节,要完。

此时,不算技术原型,对需求实现产生了120行代码。两句三年得,一吟泪双流。有点累了,而且感觉前途渺茫。缺肉吃,饿。联系社区组织团购,没人理。自己组织,商家说不敢卖。运输也是问题,社区没功夫,自己运不让出门。所以停下来休息睡觉。
微信图片_20220411171320
5. AHK 全选 + 正则表达式

午夜令人警醒,因此也倾向于过高估计困难的危险。天一亮就感觉好多了。

再次回到第一性原则,发现人手的操作还有别的路线。可以鼠标按下,上推鼠标光标,选中几屏头像、图像、文字等,然后复制。粘贴出来就是纯文本的了。

这么大一片纯文本,大可不必像上面找头像的方式,找一个头像检查一次文本,而是可以启用伟大的工具 正则表达式,匹配 "某同学 一堆字符 稳定",再去除"不稳定"。

有两个技术难点。

(1)稳定地选中内容。试了几种方案,不都稳定,故障现象也无法重现。有时选不上,有时选的内容有遗漏。一种方案是拖动。鼠标左键接下不抬起,向上拖动;鼠标到最顶,触发滚屏。保持几秒,假设能到达今天的第一条回应。不能用AHK MouseClickDrag,这个函数的结束时会自动抬起鼠标左键,因此无法触发滚屏。变换各种角度操作鼠标,现象无法重现或者不稳定。我甚至一度以5次连续正确为现象稳定的标准,后来想想这不就跟有些同学调超参数一样了么。跌入玄学?又感觉希望渺茫了。

后来想到鼠标左键按下以后,向上滚动鼠标。现象稳定了。

滚动一次,复制,分析文本,遇到的第一次 “今天你情绪还稳定么?",那就是到地方了,从这里向下都是我想要的。这样解决了滚到到什么时候停下来这个需求。

(2)正则表达式。这个单独写了一个AHK脚本,作为一个模块。这个模块与复制模块的接口,可以是文件,可以是进程间通信,我选择了剪贴板。前一脚本运行完毕,复制的历史消息在剪贴板里。 (正式部署deploy以前) 再运行后一脚本,从分析剪贴板中的文本,遍历每位同学,看他是否答复过稳定。要么稳定,要么不稳定,要么没吱声。

以上两部分技术难点,刚好是两部分需求中的技术重点,分别解决了,分别通过了单元测试。

集成测试出毛病了。

稳定选中内容,眼睛能看到。复制到的东西,作为检查点,从剪贴板里粘贴出来,到文本文件,或者到正则表达式工具 (见附录) ,做第三方检验,都是对的。保持这个剪贴板,立即运行第二个模块,即正则表达式的AHK,匹配结果不对。异常如何不是这里讨论的重点,略过,总之就是要么有人稳定报成不稳定,要么不稳定报成稳定,要么有人不稳定报成全员稳定。

单元测试正确,集成测试错误。毛病一定在模块间的接口上。但是,剪贴板这么简单的东西能有什么错。正则表达式写错了?把AHK手册正则表达式三章的英文和中文分别读了,见附录。还是感觉没毛病。按AHK手册,我特意写了一行代码,clipboard := clipboard 表示把剪贴板中可能的各种格式转为 plain text。

但是,我应该如此相信手册么?

做个实验。第一步,从第一个模块的输出得到剪贴板;第二步,手动把剪贴板粘贴到记事本,再从记事本全选、复制;第三步,跑第二个模块。成功了。

我知道为什么clipboard := clipboard不好使,但是它显然没有得到与向记事本
粘贴再复制出来相同的效果。

算了,简单粗暴非常有效。既然通过记事本可以,那么就通过记事本吧。所以,我的脚本跑的时候,第一模块结束,会运行记事本,转完剪贴板的格式以后,再把记事本关了。因为脚本是自用的,我甚至没有禁用鼠标和键盘,也免得万一执行有问题夺回操作权困难。

测试了几个晚上。同学们帮助测试,有时回复"我还对付",有时拖过10分钟等我监听完群消息再回复。测试通过了。

事实上,正则表达式就是对付写的,能应对简单的情况。稍微复杂点的……再说吧。就是下面这行。

: needle := ".*" element ".*:\R([^不:]*?)稳定"

某位同学,他回复了,回复的消息里包括"稳定",并且不是"不稳定"。这个"稳定"是他说的,而不能错误地匹配到他后一位同学的回复。

此时,去除 ImageSearch 版本不算,我又写了210行代码。
微信图片_20220411171327
三、后续

GYB同学曾说,如果某位同学用脚本回复呢?这是个非常值得一试的问题。得识别教师的发言 (或者以时刻1024pm作为依据?)。然后呢,教师发言时附带个问题,比如"一加二等于几""从以下图像中找到所有的 火车""把下面拼图中的小块向右拉到正确位置"?然后呢,学生写个脚本自动回答。

计算机与计算机流畅地交流着,留痕也合规。

此时,我们终于可以有时间培养一下师生情谊,顺便讨论一下技术了。
微信图片_20220411171330

附1:

AHK手册中文版
[https://wyagd001.github.io/zh-cn/docs/AutoHotkey.htm]

正则表达式 在线的测试工具
[https://regex101.com/]

代码如下。

: ; 在规定时间 10:34 启动
: ; 代码参考 [https://blog.csdn.net/liuyukuan/article/details/53860122]
: ; 检测回应的时刻, 22:34:00,格式223400
: 定时= 223400
:
: ;--------------------------------
: ;---------------------------------
: time_out = 5
: msgbox , , 信息 , %time_out%秒钟后AHK脚本开始倒计时, 启动对回应的监听, %time_out%
:
: StringLeft,当前日期,A_Now,8
: 提醒的时刻 = %当前日期%%定时%
: 提醒的时刻 -= %A_Now%, Seconds
: if (test_mode != "true"){
: ; debugmode ...
:    Sleep 提醒的时刻*1000
: }
:
: ; part 1 模拟鼠标键盘 操作微信 复制聊天历史
: ;----------------------
: ; 微信群名
: group_name = 软件所-现役
: group_name_temp = 玫瑰花园
:
: ; --------------debugmode todo
: ; BlockInput, On
: ; BlockInput, MouseMove
:
: ; 找微信窗口
: ; 最大化,以方便滚动
: Run, C:\Program Files (x86)\Tencent\WeChat\WeChat.exe
: Sleep 500
: WinActivate, ahk_class WeChatMainWndForPC
: WinMaximize , A
: WinGetActiveStats, Title, Width, Height, XXX, YYY
:
: find_group(group_name, group_name_temp)
:
: Sleep 1000
: ; debugmode
: ; Send 开始检测情绪稳定的回应
: ; Send {Enter}
: ; Sleep 1000
:
: ; 往上翻到 ,多翻几下,假设能超过 询问消息 “今天你情绪还稳定么?"
: ; todo 分析复制得到的文本,如果未能达到询问消息的位置,就再向上翻
:
: ; 鼠标移到 历史消息
: gap = 50 ; 由 群消息编辑 到 群消息历史 的偏移像素,根据分辨率校准
: x := A_CaretX
: y := A_CaretY-gap
: ; 拖动鼠标,复制消息,向上滚动10秒,假设复制到足够多的消息
:
: Clipboard = ""
: MouseMove, x-15,  y, 0 ;向上,到达历史消息栏,比文字光标向左一点,避开头像
: Click  ; 如果已有选中,清除
: Sleep 1000
: Click, Down ; 鼠标左键按下
: Sleep 1000
: count := 0
:
:
: loop {
:     count += 1
:     MouseClick , WheelUp, , , 50 ; 保持鼠标左键不抬起,向上滚动鼠标轮
:     Sleep 500
:     ; 复制,检测是否有 当前日期标志,如果有,那就是到了我发的消息。
:     Send ^c ;
:     ClipWait
:     StringLeft,当前日期,A_Now,8
:     if ( InStr(clipboard, 当前日期) || count > 10)  ; 找到了(或者滚动10次),停止滚动 todo 日期时刻 改为变量
:     {
:         if (clipboard == "") ; 没有复制到文本,重来
:         {
:             find_group(group_name, group_name_temp)
:
:             Clipboard = ""
:             MouseMove, x-15,  y, 0 ;向上,到达历史消息栏,比文字光标向左一点,避开头像
:             Click  ; 如果已有选中,清除
:             Sleep 1000
:             Click, Down ; 鼠标左键按下
:             Sleep 1000
:             count := 0
:
:             continue
:         }
:         break
:     }
: }
: Click, Up ; 抬起鼠标左键
: Sleep 500
:
: ; debugmode
: ; msgbox ,,, %clipboard%
:
: ; todo debugmode
: ; WinRestore , A
:
: ; debugmode
: ;BlockInput, Off
: ;BlockInput, MouseMoveOff
:
:
: ;-------------------------------------------
: ; part 2 在聊天记录中匹配回复信息 以及 发送提醒消息
:
: message_receiver := ["位军营", "杨萍", "张宵", "2020-史志腾", "杜蕾", "韩亚光", "田洪轩", "唐一钦", "李娜", "马洪博", "王涛"]
:
: ; 需要监听这些人员的回答
: check_list :=       ["位军营", "杨萍", "张宵", "史志腾",       "杜蕾", "韩亚光", "田洪轩", "唐一钦", "李娜", "马洪博", "王涛"]
:
: ; 未答复或情绪不稳定的人员清单,值是 平行数组 message_receiver 或 check_list 的下标
: mute_or_unstable_list := []
:
: Clipboard := Clipboard
: run, notepad
: sleep 500
: send ^v
: sleep 500
: send ^a
: sleep 500
: send ^c
: ClipWait
: send !{f4}
: send n
: ; debugmode
: ; msgbox  ,,, ok
: hay := Clipboard
: ; msgbox ,,, % hay
:
: check(hay, check_list, mute_or_unstable_list)
:
: ; ----debug mode
: ; 找微信窗口
: Run, C:\Program Files (x86)\Tencent\WeChat\WeChat.exe
: Sleep 500
: WinActivate, ahk_class WeChatMainWndForPC
:
: ; 找群
: find_group(group_name, group_name_temp)
: ; --------------------
:
: for index, element in mute_or_unstable_list ; message_receiver 平行数组
: {
:     Send {@}
:     Sleep 500
:
:     receiver_label := message_receiver[element]
:     Send %receiver_label%
:     Sleep 500
:     Send {Enter}
:     if (index < mute_or_unstable_list.Count() ) {
:        Send {、}
:     }
:     else {
:        Send {,}
:     }
: }
:
: if (mute_or_unstable_list.Count() ==0 )
: {
:      Send 全体稳定。
: ; debugmode ...
:     Send {Enter}
:     Sleep 500
:     exit
: }
:
: Send ^{Enter}
: Send 以上同学回复未检出到或者情绪不稳。
: Send {@}
: Sleep 500
: Send 杨贵福,请关注。
: ; debugmode...
: Send {Enter}
:
:
:
: ; -----------------------------------------------------------
: check(hay, check_list, m_or_u)
: {
:     for index, element in check_list
:     {
:         ; 未匹配 不带 不 的稳定,此人未回复 或 不稳定
:         needle := ".*" element ".*:\R([^不:]*?)稳定"
:         FoundPos := RegExMatch(hay, needle , OutputVar)
:         ; debugmode
:         ; msgbox ,,, %element% %FoundPos%
:         if(FoundPos == 0 || ErrorLevel != 0)
:         {
:             m_or_u.push(index)
:             ; msgbox ,,, %element%
:         }
:     }
: }
:
: ; 找群
: find_group(group_name, group_name_temp)
: {
:     Send ^f
:     Send %group_name_temp%
:     Sleep 1000
:     Send {Enter}
:     Send ^f ;并显示群的历史消息最下方
:     Send %group_name%
:     Sleep 1000
:     Send {Enter}
:     Sleep 1000
: }