情绪稳定不稳定的心路历程 - 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
: }

写个脚本,问候你今天情绪稳定不

用AHK (AutoHotkey) 写了个脚本,在PC/Windows操作系统中调用微信客户端,定时@ 特定人员 单发/群发 (?) 消息。

1. 问题及此前的工作

本轮疫情以来,我所在单位的不止一个有关部门发出了不止一次不止一个要求。其中一个要求,是要求导师每天向每名研究生询问情绪是否稳定。如何上报,等以后通知。不过,在如何上报出来以前,就应该有行动,不然一旦要求上报,报啥呢。我怕除了聊天和学术上技术上的讨论以外,万一忘了每件特别是这件重要的底线,又根据 DRY (Don't Repeat Yourself) 原则,所以考虑应该自动化。

当然,我们就如何实现进行了反复实验和深入的讨论,成功地把这个重要而 的任务变成了有趣的技术问题。不仅执行要求,也分析要求,按 正念 的观点,觉知正在进行的任务以及感受,有效避免了抑郁。

田同学用 appium + 真手机 做了一个版本,在这里[https://www.cnblogs.com/ourshiningdays/p/16023291.html]。非常成功,到每晚1024这个时刻,就由 appium 通过USB线调用手机上的微信,把消息发出来。

他发现,"@杨贵福"这种写法不行。"@杨贵福"并不是一段文字,我用实验复现了他的发现,确实如此。"@杨贵福"能复制,但是不能粘贴到记事本中。根据DDE应该能进一步确认复制出来的到底是什么以及格式,不过,知道不行也就够了。田同学发现必须先"@",等微信列出名单,再输入名字匹配才行,单纯输入"@杨贵福"或粘贴这段文字,不能起到@的"呼叫"效果。

田同学还做了加强版,等10分钟,然后解析同学们 (和我) 的回复,确认大家都表示了"稳定"而且不是"不稳定"。凡是没有回应或不稳定的,@导师提醒,并给出名单。

完美。我问,你咋不用夜神一类的仿真器呢,是因为微信在仿真器里登录以后,会踢手机上的微信下线,而你没有另一个微信号么?他说,他有另一个微信号,也在仿真器上试了。被微信侦测出使用仿真器,已经封号两次了,再封的话号就没了。

于是,有了我今天介绍的这个版本。

微信图片_20220327201946

2. 技术路线

AHK是个脚本工具,语法有点像VB。本轮安装以前,我没有想到,这东西如此友好,甚至不必安装,解压就行。因此可以认定干净并且不烦人。烦人,指安装全家大礼包、修改开机自动运行和服务、特别大个,等等。AHK这点不错,解压完以后9M。

AHK可以模仿键盘和鼠标的动作,可以窗口和屏幕上的控件,可以读颜色。这么说吧,你能干的事,它都能干。因此,也可以作为GUI的测试工具,而不仅是个自动化工具。

因为在微信上回消息,用手机实在太慢了。回消息的时候除非手头没有PC,我不用手机。所以我的PC微信客户端本来也经常长时间登录在我的座机上,虽然经常隐藏在我当前工作桌面以外的另一个桌面中。随时唤起,也可以被AHK唤起。

思路大致如下,用AHK模仿人手的动作。我先把下面的步骤想清楚,写在代码里,加上注释,算作大纲。然后开始从上向下写代码。

第一步,找到微信窗口

用ahk的小工具WindowSpy找到微信窗口,可以根据窗口的标题、进程名 (程序名)或进程号、win class之类的区分。我用 ahk_class。

: WinActivate, ahk_class WeChatMainWndForPC

实测时发现,如果微信窗口被ESC消隐,就不能 WinActivate 切换到前台。所以在此之前尝试运行一下。

: Run, C:\Program Files (x86)\Tencent\WeChat\WeChat.exe

然后sleep 500毫秒,等待万一微信需要启动。后面还要经常sleep,即使我们肉眼以为瞬间完成的,也需要等一下。下略。瞬间这种事,在计算机中不等价于同时。

微信图片_20220327201927

第二步,找群

找群这个动作,微信支持用快捷键 ctrl+f,然后输入群名,回车。

所以用AHK模仿键盘,不操作鼠标。

: Send ^f
: Send %group_name%
: Sleep 1000
: Send {Enter}

group_name是个变量。考虑到别人用这段代码时群名可能变化,所以在脚本最开头,定义群名。

微信图片_20220327201936

第三步,清空原有消息

此时已进入群聊的窗口中,光标在输入文本框中。考虑到一个场景,在此之前脚本的使用者可能正在使用微信,也许正在此群中,并且已经输入一些文字。但是这个场景并非MVP (Minimum Viable Product),所以推迟到以后的版本 (如果还有的话)再实现。

在这里,为避免原有文字干扰发送的问候,仅做简单粗暴处理——全选,清空。

: Send ^a
: Send {Del}

微信图片_20220327201943

第四步,循环,应@尽@

这一步构造要发送的消息,@每一位需要@的同学。一方面因为群里有已毕业的同学,不宜打扰。另一方面,"大家没事注意点儿群里的消息"或者"@所有人"都很烦人。

在脚本最前面把每一位需要@的同学的 群昵称 放到数组中作为一个元素,在此处遍历数组。

对于数组中的每个元素,先发送 @,sleep,然后发送群昵称一个,sleep,发送回车。这样就@完一位。每轮循环的最后根据是否最后一位同学决定发送句号还是逗号。

遍历数组以后,发送问候的消息。我还顺便附带了当前时间。

再发送个回车,把这条消息发送出去。

微信图片_20220327201940

第五步,定时

这是最后考虑的,但是不是在最后执行的。

定时可以像田同学那样,使用windows定时任务,或者用 linux cron这一类的机制。也可以按当年李记者告诉我的更常用的手法,轮询,跑死循环,在每次循环里检查是否要运行的时刻,然后sleep节省CPU。

也可以像我这次这样,抄了一段代码。从这里抄的,[https://blog.csdn.net/liuyukuan/article/details/53860122]。

在程序运行之初算一下还差多少秒到要运行的时刻,sleep这么长时间。

: StringLeft,当前日期,A_Now,8
: 提醒的时刻 = %当前日期%%定时%
: 提醒的时刻 -= %A_Now%, Seconds
: Sleep 提醒的时刻*1000

这就行了。在预定的时间每晚1024,应@尽@,发送问候。

微信图片_20220327201948

3. 实验结果及改进

白天试了两下,通知同学们不必理我。大家看完消息以后,讨论了一会儿,表达过和表达了对一些问题的担心/估算。比如要sleep,要sleep多久。万一同学也用脚本回复怎么办。也许应该讨论的"怎么办"不是如何应对,而是如何达成?、

当天晚上,AHK脚本如约运行。第一位同学的名字前面没有@。这位同学指出我一定是没有sleep。正是,改。

改的过程中发现,要测试好几次。要避免定时sleep,而希望马上执行看效果;要避免消息构造完成以后不慎回车把消息发出去。所以,我定义了 测试模式。

: test_mode = false ; true
: ...
: if (test_mode != "true"){
:    Sleep 提醒的时刻*1000
: }
: ...
: if (test_mode != "true"){
:    Send {Enter}
: }

4. 展望

工作总是可以更美,不过留到以后吧。当前版本就可以对付用了,就这样发布吧。

以后要工作:
(1) 发出消息后10分钟后查回应,如果没回复需要提醒

我们讨论到的方案是截屏(可能需要滚几屏),OCR,然后解析。

(2) 保存现场和恢复:发送消息前,由系统暂存未发出的消息,群发消息以后再粘回来

5. 附录 代码

; 要在以后的版本中再增加的功能
/*
1. 发出消息后10分钟后查回应,如果没回复需要提醒
2. 保存现场和恢复:发送消息前,由系统暂存未发出的消息,群发消息以后再粘回来
*/

#SingleInstance force

; 测试模式
test_mode = false ; true

; 微信群名
group_name = 软件所-现役

; 人名清单
;message_receiver := Object()
message_receiver := ["位军营", "杨萍", "张宵", "2020-史志腾", "杜蕾", "韩亚光", "田洪轩", "唐一钦", "李娜", "马洪博", "王涛"]

; 问侯语
question = 来来来,请回复一下,今天你情绪还稳定么?我还对付。

; 在规定时间 10:24 启动
; 代码参考 [https://blog.csdn.net/liuyukuan/article/details/53860122]
; 群发消息的时刻, 22:24:00,格式222400
定时= 222400

;--------------------------------
;---------------------------------
; 通用AHK脚本启动
time_out = 5
msgbox , , 信息 , 运行中,%time_out%秒钟后AHK脚本开始倒计时, %time_out%

StringLeft,当前日期,A_Now,8
提醒的时刻 = %当前日期%%定时%
提醒的时刻 -= %A_Now%, Seconds
if (test_mode != "true"){
Sleep 提醒的时刻*1000
}

;--------------------------
; 通知用户不要手动操作计算机
time_out = 5
msgbox , , 警告 , 请暂停手动作操作,%time_out%秒钟后AHK脚本将通过微信群发消息。`n如有必要立即保存当前消息。 , %time_out%

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

; 找群
Send ^f
Send %group_name%
Sleep 1000
Send {Enter}

; 清空原有消息
Sleep 1000
Send ^a
Send {Del}

; 循环
; 在群中@每一位用户
; 发消息
;Send {当前时刻%A_Now%,}

for index, element in message_receiver
{
Send {@}
Sleep 500
Send %element%
Sleep 500
Send {Enter}
if (index < message_receiver.Count() ) {
Send {、}
}
else {
Send {,}
}
}
Send %question%
Send {^Enter}
Send %A_Now%
if (test_mode != "true"){
Send {Enter}
}

; 下一版本再实现
; 10分钟后查回应
; 找群
; 选文字?读

ExitApp
return

改出个豆瓣书籍导出工具

1. 属主

多少年以来,就不断传说豆瓣要倒掉了,大家都担心里面辛苦记录的看过哪本书看过哪部电影,就都化为飞灰。所以我也关注过豆坟这样的工具,能把你走过的路导出来。后来,听说这些工具被封的被查的,各种说法,也都是传说。只是豆瓣还没有倒掉,也许正如比萨斜塔,倒掉的过程漫长到不知不觉它已经变了,早已低于你一次次定下的底线,而你居然仍能接受。这应该不算忘却初心,算与时俱进吧。

虽说,只有忘掉了以后还在深刻影响你的,才是真正的修养。然而,当你准备吹牛说某某书在你成长的过程中曾经让你喝了三天大酒每天早晨逃课,但是那位作家那本书的名字就在嘴边却偏偏想不起来了。"就是那本,你知道的,那本特有名的,叫什么什么,哎,一时没想起来。"说服力大打折扣,真是尴尬。那个令你夜不能寐泪湿枕套的人,居然忘了名字,说起来谁能信有过这几滴泪呢。

再说,你走过的路,你标过的电影和书,说到底,它是谁的呢。在饭店结账,有一次我不知怎么犯了混——经常有,每次不同,其他的以后再讲给你——非想把小票拿走。起因可能就是对方那句"这个我们要留着",我就一定要拿走。他一句我们有规定,又一句这个得入账,又一句给你个礼物,又一句你可以拍照,还有啥来着。他的每一句后面,我都只有一个问题,"这是你的还是我的?"

你标过的电影和书,说到底,它是谁的。

教师上课,领导不必打招呼随便进课堂检查,我也挺想知道,这课堂是谁的。谁,才应该是主人,谁,才有责任有主人翁意识。我就猜有人会说,课堂是学生的,学生才是主人。那么,你经过主人同意了么。得这么请示,"某同学,受我的上级领导委派,明天我不得不去旁听您所在班级的某门课,就是教师小那谁那堂。仰慕已久,正好可以当堂观摩您当堂指出教师授课问题时的风彩。您看是否合适?"如果有闲空,也可以对教师指出,"小那谁,明天本大人要去听你的课啊,提前耳提面命令尔知哓,是莫大的恩情呐!"我等即应"着"。如果没有闲空,发个通知,"本座可能突然莅临监听尔等任何一人任何一堂,望周知",即可。甚至不必等"着"这个确认。要知道如果你有了"着"的责任,就意味着还有不"着"的权利。鲁迅先生所说的有钱人对我喊完"滚",可是无须我表达听到或同意的。多么威风。

微信图片_20220318004330

当然,一切都是为了属主。属主喜欢什么就是什么,喜欢谁就是谁。

鉴权系统中有个概念,叫做 属主。属主真是说删就删,如果只读,我就把你的只读改掉还是要删。但是,得是属主。属主成天担心标过的书记过的电影正上着的课,这能算属主么。

非单身的同学请注意,你的收入可不是你一个人的。即,你并非属主。根据法律规定,你的收入归你和你的配偶共同所有。共同所有,你并非唯一的属主,因此也就不是属主。并非独占,即并非拥有。

出于对于并非独占而自以为拥有的战战兢心理吧,所以,爬豆瓣,把属于自己的数据取回来,这样的工具、方法、操作层出不穷。也是奇观。说起来,爬数据违法,爬自己的数据违法不?

微信图片_20220318004333

2. 爬

怎么爬呢。

正则表达式?这绝对是个好办法。正则表达式,等价于乔姆斯基3型文法,描述能力相当于有穷自动机。那是相当强悍。也算小众,不然乔姆斯基应该已经封杀了。说起来,如果乔姆斯基封杀了,正则表达式是不是不株连不足以平民愤呢。

不过,有比正则表达式更适合于 爬 这个具体需求的。杀鸡焉用牛刀,图灵机肯定行,图灵机干啥都行。但是回答"如果发贴子",你不能说"用图灵机",得用浏览器区别于微信,才好。

从网页或者类似的结果数据,而不是理解为可执行的代码,从结果数据中抽取文本的时候,匹配的特征描述可以用xpath获取dom节点。其关键思想是在 内容(如html中的字符串) 与 形式 (如css的字体)分离 的情况下,通过形式 (css符合哪个class,什么颜色,第几个li标签或者第几个p标签)找到内容 (一段文字)。

我们可以 (随便什么方法) 写个爬虫。然后

第一步,读到网页。

第二步,用xpath分析dom,从中提取到想要的信息,比如 我读过的书目列表,从中取出 书名、作者-出版社-出版时间-码洋、标注时间。记录这些信息到本地。或者放在你的脑子里,总之,你确定无疑地具有属主权限的地方。

不要放在云端!童话故事里恶魔把自己的心脏交给善良的小媳妇保管,坏人总是有那么一刻心软了,唉。后来你看到有几本书消失了,或者成了"未知书籍",就像有的"我的网盘"上的东西突然没了,我一按键盘,那不是"我的"网盘么?难道,这就像"我的理发师""我的老师""我的母校"那种,其实它并不像"我的缺点""我的负债""我未完成的任务"那样是"我的"?这书没了的时候,你却在吹牛的时候刚好想不起来那几本对你影响深远的书的名字是什么,那就是单纯吹牛了。

第三步,还是刚才那个网页,用xpath分析dom,找到"下一页"那个链接。

第四步,http get "下一步" 的 url。

第五步,跳转到第一步。直到找不到"下一页"了,那么就是到了最后一页。

无标题也可以到这时,把前面每一页的每个你想保留的字段存储到本地。不要放在云端!

3. 工具

以上步骤,可以用 python,也可以用世界上最好的语言 php。凡是支持 http get的,并且支持 xpath dom 的,一般也都支持写入本地,都可以。

我前两天翻 firefox 优秀插件 tampermonkey 的脚本时,发现了一个好东西,豆瓣电影标注的导出工具。名字是 豆瓣电影导出工具,[https://kisexu.com/]这位大侠写的,发布在这里 [https://openuserjs.org/scripts/KiseXu/豆瓣电影导出工具]。

我想,有导出电影的,肯定还有导出书籍的吧。没找到。都这么懒呢。后来到了每日三省吾身的时候想,圣人日,行有不得 (并、不是这个意思),反求诸己 。对啊,找不到东西,自己写啊。正好有这么像的,动手改啊。我就改了一个。

Kisexu大侠的代码流畅,容易读懂,与上述如何爬的思路一致,不必修改。kisexu大侠用了 jquery 求 xpath。电影和书籍页面的xpath不一样,我得每页看看,改改。对 jquery 语法不熟悉,还顺便复习/预习了一下。在这里学的,[https://www.w3school.com.cn/jquery/jquery_ref_traversing.asp]。

微信图片_20220318004336

写完了跑了一下,效果还不错。刚好1234本书,有意思的数,算是个纪念。

4. 使用

需要用 firefox 浏览器,安装 tampermonkey 插件。

在 tampermonkey 的 dashboard 中新增脚本,删除原有示例代码(ctrl-a del),然后把以下代码贴进去。保存。

接着在访问豆瓣时,"我的豆瓣",我读 的右边除了 在读、想读、读过、书单,多了个"导出读过书籍"链接。

微信图片_20220318004343点这个链接,firefox 会翻你标记了读过的书,每页15本,一页页翻下去。全翻完以后,产生一个excel (后缀csv)请你下载。这个excel就是你读过的书籍了。
微信图片_20220318004339

微信图片_20220318004341

tampermonkey 脚本如下。再次感谢kisexu大侠。

// ==UserScript==
// @name         豆瓣已读书籍导出工具
// @namespace    younggift.net
// @version      0.1
// @description  将豆瓣已读图书导出为csv文件。启用本脚本,进入豆瓣个人页面后,在『我读』部分会有一链接『导出读过书籍』,点击即可。无需登录,支持导出任意用户已读书籍。
// @author       younggift
// @copyright 2022, younggift; 2018, KiseXu (https://kisexu.com)
// @license MIT
// @match        https://book.douban.com/people/*/collect*
// @match        https://www.douban.com/people/*
// @require      https://unpkg.com/dexie@latest/dist/dexie.js
// @grant        none
// ==/UserScript==

// ==OpenUserJs==
// @author KiseXu
// ==/OpenUserJs==

(function() {
    'use strict';

    // 页面触发部分
    if (location.href.indexOf('//www.douban.com/') > -1) {
        // 加入导出按钮
        var people = location.href.slice(location.href.indexOf('/people') + 8, -1);
        var export_link = 'https://book.douban.com/people/' + people + '/collect?sort=time&start=0&filter=all&mode=grid&tags_sort=count&export=1'; //列表URL 及'&export=1'标记
        $('#book h2:first .pl a:last').after('&nbsp;·&nbsp;<a href="'+export_link+'">导出读过书籍</a>') //根据css定位
    }

    if (location.href.indexOf('//book.douban.com/') > -1 && location.href.indexOf('export=1') > -1) {
        // 开始导出
        getPage();
    }


    // 获取当前页数据
    function getCurrentPageList() {
        var items = [];
        $('li.subject-item').each(function(index) {
            items[index] = {
                title: $(this).find('h2').find('a').text().trim(),
                pub: $(this).find('.pub').text().trim(),
                mark_date: $(this).find('.date').text().replace(/读过/, '').trim(),
                link: $(this).find('h2').find('a').attr('href').trim(),
                // 学习了 https://www.w3school.com.cn/jquery/jquery_ref_traversing.asp
            };
//             alert("here");
//             alert(items[index].title);
//             alert(items[index].pub);
//             alert(items[index].mark_date);
//             alert(items[index].link);
    });

        return items;

    }

    // 采集当前页数据,保存到indexedDB
    function getPage() {
        const db = new Dexie('db_export');
        db.version(1).stores({
            items: `++id, title, pub, mark_date, link`
        });
        var items = getCurrentPageList();
        db.items.bulkAdd(items).then (function(){
            console.log('保存成功');
            // 获取下一页链接
            var next_link = $('span.next a').attr('href');  //todo 是否正确?
            if (next_link) {
                next_link = next_link + '&export=1';

                window.location.href = next_link;
            } else {
                exportAll()
            }
        }).catch(function(error) {
            console.log("Ooops: " + error);
        });

    }

    // 导出所有数据到CSV
    function exportAll() {
        const db = new Dexie('db_export');
        db.version(1).stores({
            items: `++id, title, pub, mark_date, link`
        });
        db.items.orderBy('mark_date').toArray().then(function(all){
            all = all.map(function(item,index,array){
                delete item.id;
                return item;
            })

            JSonToCSV.setDataConver({
                data: all,
                fileName: 'book',
                columns: {
                    title: ['书名', '出版信息', '标记时间','链接'],
                    key: ['title', 'pub', 'mark_date', 'link']
                }
            });
            db.delete();
        });
    }

    //以下未修改 todo
    // 导出CSV函数
    // https://github.com/liqingzheng/pc/blob/master/JsonExportToCSV.js
    var JSonToCSV = {
    /*
     * obj是一个对象,其中包含有:
     * ## data 是导出的具体数据
     * ## fileName 是导出时保存的文件名称 是string格式
     * ## showLabel 表示是否显示表头 默认显示 是布尔格式
     * ## columns 是表头对象,且title和key必须一一对应,包含有
          title:[], // 表头展示的文字
          key:[], // 获取数据的Key
          formatter: function() // 自定义设置当前数据的 传入(key, value)
     */
    setDataConver: function(obj) {
      var bw = this.browser();
      if(bw['ie'] < 9) return; // IE9以下的
      var data = obj['data'],
          ShowLabel = typeof obj['showLabel'] === 'undefined' ? true : obj['showLabel'],
          fileName = (obj['fileName'] || 'UserExport') + '.csv',
          columns = obj['columns'] || {
              title: [],
              key: [],
              formatter: undefined
          };
      ShowLabel = typeof ShowLabel === 'undefined' ? true : ShowLabel;
      var row = "", CSV = '', key;
      // 如果要现实表头文字
      if (ShowLabel) {
          // 如果有传入自定义的表头文字
          if (columns.title.length) {
              columns.title.map(function(n) {
                  row += n + ',';
              });
          } else {
              // 如果没有,就直接取数据第一条的对象的属性
              for (key in data[0]) row += key + ',';
          }
          row = row.slice(0, -1); // 删除最后一个,号,即a,b, => a,b
          CSV += row + '\r\n'; // 添加换行符号
      }
      // 具体的数据处理
      data.map(function(n) {
          row = '';
          // 如果存在自定义key值
          if (columns.key.length) {
              columns.key.map(function(m) {
                  row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(m, n[m]) || n[m] : n[m]) + '",';
              });
          } else {
              for (key in n) {
                  row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(key, n[key]) || n[key] : n[key]) + '",';
              }
          }
          row.slice(0, row.length - 1); // 删除最后一个,
          CSV += row + '\r\n'; // 添加换行符号
      });
      if(!CSV) return;
      this.SaveAs(fileName, CSV);
    },
    SaveAs: function(fileName, csvData) {
      var bw = this.browser();
      if(!bw['edge'] || !bw['ie']) {
        var alink = document.createElement("a");
        alink.id = "linkDwnldLink";
        alink.href = this.getDownloadUrl(csvData);
        document.body.appendChild(alink);
        var linkDom = document.getElementById('linkDwnldLink');
        linkDom.setAttribute('download', fileName);
        linkDom.click();
        document.body.removeChild(linkDom);
      }
      else if(bw['ie'] >= 10 || bw['edge'] == 'edge') {
        var _utf = "\uFEFF";
        var _csvData = new Blob([_utf + csvData], {
            type: 'text/csv'
        });
        navigator.msSaveBlob(_csvData, fileName);
      }
      else {
        var oWin = window.top.open("about:blank", "_blank");
        oWin.document.write('sep=,\r\n' + csvData);
        oWin.document.close();
        oWin.document.execCommand('SaveAs', true, fileName);
        oWin.close();
      }
    },
    getDownloadUrl: function(csvData) {
      var _utf = "\uFEFF"; // 为了使Excel以utf-8的编码模式,同时也是解决中文乱码的问题
      if (window.Blob && window.URL && window.URL.createObjectURL) {
          csvData = new Blob([_utf + csvData], {
              type: 'text/csv'
          });
          return URL.createObjectURL(csvData);
      }
      // return 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(csvData);
    },
    browser: function() {
      var Sys = {};
      var ua = navigator.userAgent.toLowerCase();
      var s;
      (s = ua.indexOf('edge') !== - 1 ? Sys.edge = 'edge' : ua.match(/rv:([\d.]+)\) like gecko/)) ? Sys.ie = s[1]:
          (s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
          (s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
          (s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
          (s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
          (s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
      return Sys;
    }
  };

})();

疫情数据简单抽取:Emacs正则表达式一例

某天,长春市发布了疫情数据,内容相当长,而格式有规格。所以我想到用正则表达式抽取出来,看着方便一些。

1. 原始数据,特征,正则表达式

原始数据有很多行,看起来形式大概这样。

: 新增确诊病例:
: 确诊病例1-35:现住九台区。系确诊病例的密切接触者,隔离期间核酸检测阳性,3月11日转入长春市传染病医院,诊断为确诊病例。
: 新增无症状感染者:
: 无症状感染者1-47:现h住九台区。系无症状感染者的密切接触者,隔离期间核酸检测阳性,3月11日被确定为无症状感染者。
: 无症状感染者48-63:现住九台区。系确诊病例的密切接触者,隔离期间核酸检测阳性,3月11日转入长春市传染病医院,被确定为无症状感染者。

能够看到明显的规律:

(1)数据由若干 行 组成;

(2)在每行中,需要提取的数据有共同特征,

比如下面这一行。

> 确诊病例1-35:现住九台区。系确诊病例的密切接触者,隔离期间核酸检测阳性,3月11日转入长春市传染病医院,诊断为确诊病例。

按原文与特征的对比如下。

| 原文                 | 特征                   |
| 无症状感染者1-47:现  | 不需要的任意字符序列... |
| 住                   | 住                     |
| 需要的任意字符序列... | 九台区                 |
| 。                   | '。'                   |
| 系无症状感染者的密切接触者,隔离期间核酸检测阳性,3月11日被确定为无 | 症状感染者。|不需要的任意字符序列... |

这样就得到了正则表达式,"住.*。",可以匹配需要的字符序列。比如上面这行,可以匹配得到"住九台区。"然后删除第一个字和最后一个字,得到"九台区"。对每一行均使用这一正则表达式,得到只有符合匹配要求的字符串。整行中都不符合特征的,这一行不会被选出来。

正则表达式可以用 命令行工具grep,程序设计语言python、js等。

我平时用emacs,支持正则表达式。

在emacs里,把上述数据粘贴到buffer(相当于文件)中。执行命令 M-x occur,

给出正则表达式,得到类似如下结果,不完全符合我的要求。下面的结果,是我对当前文档执行的结果,匹配的部分是高亮的。

图1

2. lazy

可以看到,匹配到的部分比我们预期的要长。不是在第一个遇到的句号 (。)停止,而是在最后一个句号,即尽可能长地匹配。

我隐约想起emacs正则表达式还有个 lazy 模式,查了一下,用?代表。

把正则表达式改为 住.*?。

好多了。

3. 仅保留匹配的部分

如果把匹配的结果复制出来呢。我只想要匹配的部分,不想要其余的文字,而现在的效果是给出了 包含匹配文字 的整行。

在网上查到一些emacs的lisp代码。还有一个简单的办法。

: As of Emacs 24, occur does in fact provide a simple solution:

: C-u M-s o .*pattern.* RET

[Emacs copy matching lines]

其中的"C-u M-s o" 表示 ctrl-u alt-s 再按 o。

怎么这么稀奇古怪的。

我事先知道 C-u 是前缀,后面的才是命令本身。用 f1 ? k 查询,按 M-s o 查

到原来就是 occur 啊。

: M-s o runs the command occur, which is an interactive compiled Lisp

我一直喜欢命令序列,可以减少硬记的工作量。所以,我用 C-u M-x occur。

C-u 有什么用呢?

occur 的手册说:

: When NLINES is a string or when the function is called interactively
: with prefix argument without a number (`C-u' alone as prefix) the
: matching strings are collected into the `*Occur*' buffer by using
: NLINES as a replacement regexp.

会把匹配的字符串 (而不是整行)收集到一个buffer (估且理解为文件) 里。

4. 去除首尾的字符

上述结果在一个只读的buffer中,复制出来,放到一个可写的buffer中。选中区

域,执行下面的指令。

: M-x replace-regexp
:  ^.
: 
: M-x replace-regexp
:  .$

得到以下结果。

: 九台区
: 九台区
: 九台区
: 九台区
: .*
: 九台区
: .*?

5. 去重

选中区域,执行下面的指令。

: M-x delete-duplicate-lines

得到以下结果。

: 九台区
: .*
: .*?

除"九台区"以外的两行,是匹配到了上文中我写的正则表达式的。

6. 排序

选中区域,执行下面的指令。

: M-x sort-lines

得到下面的结果。

.*
.*?
九台区

可以看到九台区被排到了正则表达式那两行的后面。

以上是在本文中运行的效果,真实数据的运行结果像下面这样。

: 九台区
: 公主岭市澜洋豪庭
: 公主岭市阳光首府
: 南关区十三局宿舍
: 南关区南城家园
: 南关区卫星商城
: 南关区星城国际大厦B座
: 双阳区太平镇桦木村9社
: 宽城区上台花园
: 德惠市中央公馆
: 德惠市国丰壹号院
: 德惠市幸福里岸
: 榆树市闵家镇东升村
: 汽开区东方之珠龙翔苑
: 经开区中海寰宇天下C区
: 经开区亚泰梧桐公馆
: 绿园区标记大厦
: 绿园区金色欧城
: 长春新区修正大厦
: 长春新区修正大学培训基地
: 长春新区北湾新城四期
: 长春新区恒盛豪庭

7. 后续工作

接着可以批量标注在地图中,看起来更方便。我没有使用方便,比如不需要身份认证的,没找到这样的工具。所以没再继续。

此外,emacs有个小工具 M-x re-builder,可以实时地在你修改正则表达式时,显示匹配的结果。适合正则表达式初学者探索。

手动测量经纬度,只使用简单工具

当年读儒勒·凡尔纳的《神秘岛》,以及此后反复阅读,每一次都为其中的智慧而击节赞叹。美国南北战争时期,五个人乘坐热气球从南方囚禁中逃出来,在一位远离人烟的荒岛上从一无所有到烧陶、制铁、电报机,几乎重建了工业。流落荒岛的第一件事,他们要确定自己的位置。是靠近陆地,还是孤岛。进而,才能决定要做长久打算,还是有可能尽快返回人类社会。

在夜里,工程师用六分仪测量南十字(?)的高度角,确定了纬度;在接近正午,用保持出发地时间的手表和当时时间之差确定了经度。由经纬度和回顾地图,他们确定远离陆地,缺乏远航的条件。

不用手机GPS,使用尽可能简单的工具,我们能何种程度精确地测量自己所在的经纬度呢?我试了一下。

1.原理

本节讨论实验所依据的原理,即如何由 测量的物理量 推算出 待求的物理量经度和纬度。

1.1 纬度

太阳的直射点以一年为周期,在北回归线和南回归线之间往复移动,导致正午太阳的高度角每天持续变化。因此,可以根据 正午太阳高度角、当天日期(确定了太阳直射点的纬线),可求得当地的纬度。

1(1)公式为 太阳高度角=90°- | 当地纬度 – 太阳直射点纬度 |

或 绝对值导致正负号对应的两个解,分别是太阳在赤道以南和以北的情况。

太阳在赤道以南或以北,可根据当天日期在春分或秋分前后确定。

涉及到的变量包括 太阳高度角,测量得到;太阳直射点纬度,根据当天日期查表或公式得到;当地纬度,待求的量。

(2)太阳直射点纬度

太阳直射点纬度 方法1 查表

以下表格都可以

Declination table

https://astronavigationdemystified.com/survival-declination-table/

https://moontracks.com/daily-declinations.html

https://tieba.baidu.com/p/5381726760

根据当天日期,可查表得到 赤纬,与太阳直射点纬度相同。

太阳直射点纬度 方法2 公式

太阳赤纬角 = 0.39795 * cos ( 0.98563 * ( n – 173 ) )

其中 n 是积日,即日期在一年中的序号,以1月1日为1。

(3)太阳高度角

置坚杆垂直于地面,测量正午前后日影的长度与杆的长度。日影长度/杆的长度,是太阳高度角的余切值,或者等比例画出日期的长度和杆的长度,用量角器测量。

2

(这里在实验还有需要讨论的细节,比如 如何确定正午,如果以北京时间为准,那么当地经度并非东八区,比如在南太平洋,带来的误差有多大;,如何保证竖杆垂直于地面,不那么垂直的误差会对测量精度带来多大影响。)

1.2 经度

根据 测得的当地时间 与 标准时间(已知经度)的时间差,可以求得当地的经度。当地时间的测量不以手机或广播或国家授时中心为准,而以太阳高度角最高点为 正午12:00。

环地球一周经度变化范围360度,时间变化范围24*60分钟。因此每度合4分钟。例如,长春在北京以东大约5度,因此正午12:00即太阳高度角最大比北京早大约4分钟/度*5度=20分钟。这样,我们可以根据测得的所在地正午时间与北京时间(东经120度经线的正午时间,不是北京的经线的正午时间)之差,求得所在地的经度。公式为

当地经度 – 东经120 = 时间提前分钟数 / 4

2.实验装置

朝南的窗台,假设水平良好。

硬质活页夹,纸。把硬质活页夹的三个角用铅笔标记在窗台上,以确保不慎移动后可以复位。

一支笔。笔帽,当作竖杆。在纸上标记竖杆的中心位置。

用手机作为手表。用笔在纸上标记竖杆顶端日影和时间。如图所示,竖杆是笔帽有弹性夹持部分(这东西叫什么名字?)。

3数据分析时使用 geogebra和excel,并非必须。

3.实验过程

2022年2月20日,11:26起大约每4分钟标记一次竖杆顶端影子的位置,同时记录时间。每4分钟太阳会移动大约1度(24*60分钟/360度=4分钟/度),更高的精度难以标记。

其间,因为玻璃较脏,有时会挡住阳光,竖杆影子的顶端看不清。在这之前,在纸上标记竖杆影子的顶端,然后把整个硬质活页夹移动到光线更好的地方,保证竖杆影子顶端与标记处仍然重合。

整个实验在12:07结束。后来在14点以后还测了几组数据,第二天21日上午又测了几组数据。对实验结果没有帮助,因此忽略这些数据。照片如下图所示。

4

4.实验数据

我们只需要角度,因此只需要知道杆和影子的长度比例作为余切,需要时间差,而不需要长度的绝对值。所以我并没有测量竖杆的长度数值,而是在geogebra中测量角度。

4.1 太阳高度角

把竖杆放倒,在纸上标记出底端到顶端的距离,如图中线段CD。

在geogebra中导入测量结果的照片。在图中把竖杆底端中心标记E点。令EF长度=CD长度。

可以看出,G点附近的竖杆顶端影子基本在同一条直线上,并与E点向下的直线大致垂直。因此G点选取对高度角精度影响不大。

角度FGF即太阳的高度角,用geogebra测量得34.95度。

5

4.2 当地正午与北京时间正午的时间差

此处实验设计的精度不好。我本以为,日影会是个大圆。拟合出圆以后,连接圆心和竖杆底端,这条直线与圆交点对应的时刻就是正午。

我没有考虑到,日影与杆间的关系是余切函数,而余切函数在90度附近变化缓慢,过零点不明显。所以,实验测得的竖杆末端几乎是一条直线,观察不到弧,因此找不到圆心。我需要测量更多离90度即正午较远的点,才能形成明显的弧降低误差。

6注意,不能仅测早晚几个点来确定正午。因为当时时间是未知的,不能根据北京时间确定数据的“中间”即正午的位置。事实上,正午,正是要测量的物理量。

所以,我补测了14点以后的几组数据和第二天的数据。但是,弧形仍然并不非常清晰,并且需要较长时间测量。

我粗糙地按如下方式处理,得到了测量值。把所有竖杆影子的末端大致拟合成直线LK,过竖杆末端E作LK的垂线,垂线与LK交于M。

7方法1 观察M点在记录的时间范围,在11:45附近,比这个时间略早。

方法2 线性映射

LK长度对应11:26到12:07即41分钟,求KM长度对应的分钟数。

LK/KM = 41/x  => x = 21(约)

求得x后,11:26加x分钟,得到正午时刻对应的北京时间11:47。

5.数据分析

根据公式(或查表)以及测量值,求得经度和纬度。

5.1 纬度

公式 太阳高度角=90°- | 当地纬度 – 太阳直射点纬度 |

2月20日

(1)测得正午太阳高度角34.95度。

(2)查表太阳直射点纬度 -11.21666667 (南纬)

求得当地纬度为 南纬66.26666667,或 北纬43.83333333。

根据指南针所示,太阳从南侧而不是北侧经过,因此长春纬度为 北纬43.83。

又,

公式 太阳高度角=90°- | 当地纬度 – 太阳直射点纬度 |

2月20日

(1)测得正午太阳高度角34.95度。

(2)根据公式0.39795 * cos ( 0.98563 * ( n – 173 ) ),积日n为51,得到赤纬为-11.47025014。

求得当地纬度为南纬66.52025014,或北纬43.57974986。

根据指南针所示,太阳从南侧而不是北侧经过,因此长春纬度为 北纬。

算得长春在北纬43.58。

5.2 经度

根据测量值当地正午为北京时间11:47,或者北京时间11:45。

分别比北京时间提前13分钟或15分钟。按4分钟/度,即0.25度/分钟计算,当地在东八区中央120经线以东3.25度或3.75度。

算得长春经度为东经123.25或东经123.75度。

6.误差讨论

6.1 对照标准

在网上搜索一下,长春市 经度 : 125.32 纬度: 43.90。

或者如下。

8在高德网页版上得到我所在的位置东经125.35,北纬43.8。

6.2 误差

(1)绝对误差和相对误差

我算得的数据 纬度为北纬43.58,经度为东经123.25或东经123.75度。此处应有表格,略。

纬度误差,约向南0.2度;经度误差约1度(最差3度左右)。相对误差都小于1%,尚可。

(2)误差换算成公里数

根据地球大圆或赤道4万公里(”坐地日行八万里”),经度和纬度都是360度,得4E4 km/360度 = 111.11公里/度。这样,我的测算误差大约100公里。

100公里看起来误差不小,相当于在如图所示的范围附近,即长春偏西100~300公里,略向南公里左右。在向通辽一带,可能比图中红圈更靠近长春一些。

9

这个结果貌似有点糟糕。然而在更大的尺度,如下图所示的范围,纬度几乎没错,经度从长春偏到沈阳。考虑到长春使用北京时间,相差达5个经度,甚至北京也与北京时间相差-4个经度,1~3度并不算太多。

a如果我们像《神秘岛》一样随热气球飘到一个荒芜人烟的地方,测算得到如下图所示的位置,判断是在陆地、陆地附近,还是大洋中心,结论还是有价值的。图中,红色地标是测得的结果,其左侧“家”的标志是长春。

b

7 热爱动手

一个硬质活页夹,一支圆珠笔,手表,得到下面这张粗糙的纸,进而推算出100公里以内范围。炫酷吧?

c也可能有人觉得,看下手机的GPS不就得了么,哪用得到这么麻烦。而且这也就是初高地理课水平的实验,说不定对高中生的精度要求更高呢。

当年我请教本科的导师李树杰老师,为什么要做实验。根据牛顿第二定律实验报告上的数据我都能编,连噪音都能随机啊。类似的疑问还有,高等数学为什么要证明,我信还不行吗?李老师教导我,做实验的目的不是为了验证实验结论,而是为了锻炼实验的能力。这段话我至今铭记,并且二十多年来令我受益良多。

微信图片_20220306173449

如果长白山天池火山喷发,影响方圆有多大,顺便讨论估算和查资料

长白山主峰白头山有湖泊,名为天池。天池下面是座活火山。就是随时可能喷,或者最近,指人类历史上,最近喷过的。事实上,不止天池下面是火山,附近有100多座火山,叫做长白山火山群。

从小就知道长白山自古著名,与千山山脉在东北的东部连成一线。小时候就知道长白山是活火山,有部电视剧讲长白山气象工作人员的,提到在风口差点被吹飞了,下着大暴雨,一个小年轻的喊:沉睡了三四百年,这长白山是不是又要喷发了。

但是我从来没怕过。也看过一些火山相关的片子,比如罗马庞贝古城、韩国电影《白头山》、BBC完美星球的第一集火山这一类的,感觉还是离得太远,对高温、火山灰、岩浆缺乏感性认识吧。贴近的接触是跟单位去日本旅游的时候见识过地面是温的,远处有烟,空气里有硫磺味。还有东汤镇的温泉。这些都不足以构成威胁,感觉远着呢。

1 近些年,看到些报道,提及韩国、朝鲜、日本也参与研究长白山火山群研究,还有韩国学者有耸人听闻之语,什么什么时候可能爆发。前文提到的韩国电影《白头山》的情节就是在这样的假设下展开的。哈哈一笑,离我还远。

后来看到了一些科普,提到长白山火山在16xx年,17xx年,190x年均有不同规模爆发。稍微引起我关注的是 火山灰影响远及日本,似乎还有人提到在日本能听到喷发的声音。

长白山到日本,比长白山到长春还要远。得关注一下了,这样的心理。

1. 估算

2 还有文章提到[如果长白山再次爆发,附近100平方千米的内的160万人将要遭殃][ https://zhuanlan.zhihu.com/p/328401889]。

我的思路就此发散。

160万人?里面有没有我。160万覆盖多大范围?作为对比参考,北京有多少人,3000万?长春有多少人,将近1千万?通化有多少人,以前是30万。长白山天池附近多大范围有160万人呢。

100平方千米,方圆有多大。等等,方圆是什么意思。方圆百里,有人说方圆是直径百里,有人说是半径百里,有人说是面积百平方里,有人说是周长。周长可笑么,据说朝鲜半岛三千里江山就是以周长计,我量了下,南北长750公里,合1500里,南北狭长,两个来回约合3000里,还真能对上。 3

原文用的是100平方千米而不是方圆这么模糊的字眼,更容易讨论一些——因此更可信。然而一旦喷发所影响的100平方千米……面积也太小了吧。

100平方千米乍一听好像挺大,但是,这是面积不是长度。按正方形计算,要开平方根,所以100平方千米就是10千米*10千米这么大。千米这个词咱们平时用得少,就是10公里见方。

10公里*10公里有多大,或者说,10公里有多远。没多远,打车20多块钱。 4

从东北师大自由校区,或者村上春树提到过的老虎公园或者金志文唱过的平泉路,到东北师大净月校区,大概就这么远。

几千年来整个地球都排得上第二的长白山火山群,就这么大点范围?我不信。而且,这么大范围不能有160万人吧。长春1千万人口,如果平均分布,那么长春的1/10面积上有大约100万人。这点范围,与长春的1/10相比也太小了。

下图中,红圈是长春的大概范围,绿圈是10公里*10公里。 5

也许需要考虑人口密度均匀,并且,只考虑长春市区。1/10倒是有可能。下图,红色的3*3格子,是我当时脑袋里想的9份,接近10份;绿圈,是10公里*10公里。 6

那意味着,1千万人口绝大部分挤在长春市区。1/10刚好差不多吧,至少数量级上。

然而,长白山火山附近,会有这么大的人口密度么。

假设长白山火山就在天池下面。这个假设有点依据,就是天池是长白山火山喷发的结果。那么,在长白山火山附近,10公里*10公里有多大范围呢?

710公里*10公里的范围内,看起来草木不生,不应该有大密度人类聚集才是。有人可能想到,山下的旅游区万达什么的。山下二字,作为对比,我小时候在通化,一般形容我们就在长白山的脚下,长白山山麓,距离天池大约200公里。另外,我们可以看一下行政区划图。

最近的度假村,将近50公里。

8著名的二道白河,50公里。

9长白山天池10公里*10公里的范围内,就不应该有几个人。

我把上述图示(不是原图,本文中的图是重新截的)发给了文章的编辑,说是不是数据有误啊。编辑很热心,提出了还有朝鲜呢以及类似的可能。如果有人口密度图就好了,讨论可以更直接一些。

以上,是估算部分。接下来是查资料。

  1. 查资料

根据估算,我不相信” 如果长白山再次爆发,附近100平方千米的内的160万人将要遭殃”这一论断。那么,这个论断从哪里来的呢?科普文章通常不给参考文献,不过总有蛛丝马迹可以查。

我最先查到了这个。

2.1 长白山保护开发区

长白山保护开发区管理委员会辖区

[https://mp.weixin.qq.com/s/rHg6cyHAuQx89Gt88pZqtQ]

: 全区南北最大长度为128公里,东西最宽达88公里。

仅长白山保护开发区管理委员会辖区就达到约100公里*100公里,比10公里*10公里大多了,是100倍。有多少人口呢,查

长白山保护开发区第七次全国人口普查公报[http://cbs.jl.gov.cn/shjj/tjxx/202106/t20210625_168941.html],

全区总人口为61146人。

6万多人,距离160万差很远。

这不是160万的数量来源。

2.2 新闻

查到一篇新闻。

: As of yet, scientists aren’t sure how immediate any threat might be, but with 1.6 million people living within 100 kilometers of Mount Paektu, safety is a major concern.

出处是新闻,链接如下

[https://advances.sciencemag.org/content/2/4/e1501513]。

新闻援引的论文没有查到提及人口数和范围。论文链接为 [https://advances.sciencemag.org/content/2/4/e1501513#ref-26]。

这条新闻中的用词是 100 kilometers,是复数的100公里直线距离,不是100平方公里面积。我怀疑是100公里直径或半范围内1.6 million即160万人口受到威胁。

2.3 数据库?

又一篇。

: 另有网页提到100公里160万人这一数据

: According to the database there are 160 million people that live within 60 miles (or 100 km) of the 245 mapped glacierized volcanoes.

: 链接如下

: https://www.forbes.com/sites/davidbressan/2020/11/30/fire-and-ice-new-database-maps-and-classifies-the-dangers-of-glacierized-volcanoes/?sh=4711364b281b

提到了 the database,看来数据是从这里出来的,有245座冰川(化?)火山,符合100公里内160万人口。100公里,是半径还是直径?

2.4 数据库

刚刚这个网页提到标准来自下述数据库,提到

: glaciers (2584 total) with a 5 km radius.

: Glacierized volcanoes occur predominantly (74%) in subduction zone settings.

: 20,000 people live within 5 km of a glacierized volcano, 160 million within 100 km.

提到了半径5公里,以及5公里有2万人,100公里160万人。看来是指半径100公里。

报道数据库的论文链接如下.

: 850 ± 290 km3 of glacier ice is within 5 km of volcanic vents globally.

: [https://www.sciencedirect.com/science/article/pii/S0921818120302472?via%3Dihub]

半径100公里有多大范围?接近临江、靖宇、抚松。白山市距离天池也不过137公里。

a

100公里*100公里,与半径100公里处于同一数量级,差不多吧,160万人口。

2.5 衍文的最初来源?

知乎的玛莉蓓尔提到“下面这个文字中的人口数,似乎不是160万人,而是1亿6千万人?[发呆]
According to the database there are 160 million people that live within 60 miles (or 100 km) of the 245 mapped glacierized volcanoes.
这数字好夸张啊。”

我把160 million,160百万,当成了160万。 想想吉林省才才2690.73万人,怎么会有160百万人口受到影响。

查了原文Global-mapping-of-future-glaciovolcanism_2020_Global-and-Planetary-Change。原文提到“in all, approximately 6.7 million people live with 30 km of a glacierized volcano, and 163 million people live within 100 km globally.” 是全球所有符合条件的冰川火山每一座100km直径以内受到影响的人口为160百万。

这,可能就是衍文的源头吧。就像我,把160万读成了160百万,可能还有人把100km读成了10km,把受全球所有这样火山影响的人口当成了任意一座?

  1. 修订,结果

我把上述资料发给编辑,对方说“谢谢!”。后来我看到微信公众号[https://mp.weixin.qq.com/s/5EjAXbHuPiqPnnLwKWZD5w]里的文字修改为” 如果长白山再次爆发,附近1万平方千米的内的160万人将要遭殃”。1万平方千米,100*100平方公里。

b写这篇博客的时候我发现,知乎上的同一文章还保留着100平方千米。[https://zhuanlan.zhihu.com/p/328401889]

c

  1. 缘起

为什么想起来回顾一下这件事呢。以一阵和大哥、WG喝酒的时候,我提到 梅河口的酒精生产占全国的90%,被他俩一顿嘲笑。大哥提到,要有常识。他说的是 common sense,包括论断内部的逻辑自洽、以及论断与现有证据之间不矛盾。如果梅河口的酒精生产占全国的90%,那么我们早就应该知道了,通过各种渠道佐证。我记得的这个数字,是2020年初疫情其间封省时在哪里读到的。回家没有查到。

查到了些其他的数字,看来我记错了,远没有90%那么多。

不要没事就瞎编或者瞎信,要自洽,要与所有已知的证据吻合。要主动、充分提供证据,以帮助别人检验。

吉林省酒精产量占全国的

200/984 约1/5 (2017年吉林省/2014年全国 发酵酒精)~ 上限75.90%(东北+华东),

-------------------------------------------------------

1.

全国没查到,只查到啤酒和葡萄酒。

 | total | per capita

beer | 38.12 million t/yr (metric tons per year) | 26.7 kg/person/yr (kilograms per person per year)

wine | 1.922 million t/yr (metric tons per year) | 1.346 kg/person/yr (kilograms per person per year)

(2018 estimates)

[https://www.wolframalpha.com/]搜索 alcohol production of china

 2.

2017年上半年,东北、华东地区酒精产量占到国内酒精行业总产量的75.90%。

 [http://www.siacn.org/index.php?optionid=1113&auto_id=27638]

 3.

根据国家统计局数字,2014年全国发酵酒精产量984.27万千升(年主营业务收入在2000万元及以上的工业法人企业)

 [www.alcoholnet.com/zxlm/179076.htm]

 约6百万吨

  2017 2018 2019

发酵酒精(折96度,商品量)(千升) 2,005,561.00  1,641,408.00  1,649,242.00

饮料酒(千升) 1,992,760.00  1,140,674.00  953,390.00

# 白酒(折65度,商品量)(千升) 777,787.00  193,901.00  22,631.00

啤酒(千升) 1,095,943.00  921,728.00  905,608.00

葡萄酒(千升) 98,804.00  21,393.00  21,369.00

果酒及配制酒(千升) 1,703.00  960.00  1,371.00

[http://tjj.jl.gov.cn/tjsj/tjnj/2020/ml/indexc.htm]

吉林省统计局

d

微信图片_20220216143930

指针和数组,到底哪个快,顺便讨论写实验报告

1个月又10天以前,MYL同学给我看了2段代码,是C语言的字符串接接函数strcat的实现。一段代码用的方法是数组,另一段用的是指针。问题是,一般使用指针实现,而不用数组,是因为指针更快呢,还是仅仅出于习惯/习俗/惯用法。

代码大致是这样的。

用指针实现的 strcat,如下。
: void str_cat_pointer(char* from, char* to)
: {
:     while(*to != 0)
:         to++;
:     while(*from != 0)
:         *to++ = *from++;
:     *to=0;
:
: }

用数组实现的 strcat,如下。

void str_cat_array(char from[], char to[])
{
int i,j;
for(i=0; to[i]!= 0; i++)
;
for(j=0; from[j]!=0; i++,j++)
to[i] = from[j];
to[i]=0;
}

插叙:MYL同学的代码有bug,并且我没有问她要授权,所以不宜发布在这里。她的代码有小bug,逻辑错误,对于当前讨论的问题是个题外话,所以不讨论了。不过,对于写实验报告这个主题,这是个有意义的话题。在实验中,不能假设原材料是干净的,不能假设工具是可用的,也不能假设仪器是校准过的。需要在实验前先"测空白"。就像在哪吒闹海里,祭童男童女就下雨 这个论断,需要先测空白,即不祭童男童女就不下雨,然后再去尝试伪原论断。如果不祭童男童女也下雨呢,那么原论断就不知道真假了。MYL同学的代码有毛病,得改成能跑、跑得对,才能进一步讨论。

插叙:当晚我做了实验答复她,但是为什么花了一个多月才写这篇博客呢。这也是实验报告写作中的一个问题,那就是大家都不乐意写,懒。实验比报告有意思。不过,即使不写实验报告,也应该按实验报告的大纲和要求设计实验、做实验并保证结论严谨。

上述代码,学过C语言的同学都很熟悉。一般教材在这里会指出 *(p+i) 与 p[i]是"等价"的。

诉诸权威,用教材来回答当然也是个路线,这样就不必做实验了。不过,那样需要找到教材,查到原文。不然的话,万一对原文理解错误,小细节没记清楚,被追究到以后就说"哎呀,我没记清啊",真是不严谨认真的人。这也是我不主动不积极回答同学们疑问的原因,特别是事涉人生发展的,因为真是累啊。也有的同学可能说,我对你的意见也不会那么重视,你为啥这么大压力呢。如果你不重视我的意见,还问我羞辱我干嘛,不如去扔个色子。

我当时手头刚好没教材,需要花点时间找。而且,找到教材,就有说服了么?在我的记忆里,教材里没有提到,这是习惯用法,还是效率就是高。即使教材提到"等价",也是指在这样不同的语法下,二者的语义是等价的。语义在这里包括什么呢,非常可能,只包括函数映射意义上的执行结果,而不包括对效率的讨论。即使教材说"习惯上"这么写,我也会认为作者在瞎扯。

好了,所以,需要做个实验看看 二者的效率 是不是一样。如果效率一样,我们就可以继续假设 单单是习惯上的原因。

还有一种可能,你读到这里已经早已按捺不住拍案而起,"我知道,不就是……"这时,要么,我们需要证据和论证过程,而不是拍胸脯;要么,你是权威,我们诉诸你。写实验报告还不利索的同学,你不是权威。或者你是权威,但是早有一天,你会遇到不信你的人,得给出证据和论证过程。这就是学数学的时候,我们哭丧着脸说"我信,我信,我服了还不行么"也不行的原因。早晚有一天,会有不信的人,只有理性、逻辑、证据和论证过程方能说服。

实验是这样设计的。

1. 准备材料,两个数组/指针字符串。

:     char f[11]="0123456789";
:     char t[30]="abcd";

2. 操作

用其中一个函数拼接字符串,拼接 1000000 次。

:         for(int i=0; i<1000000L; i++)
:         {
:             strcpy(f, "0123456789");
:             strcpy(t, "abcd");
:             str_cat_pointer(f, t);
:             //printf("\n%s\n", t);
:         }

为什么拼接这么多次呢?别笑。确实有不止一位同学在做实验的问过我,为啥我测不出来呢。因为次数不够、速度太快、时间太短,所以没测到。我们如何称量1粒米的重量?准备1kg米,然后数有多少粒,用1kg除以米粒的数量。

所以要拼接 1000000 次,为了 明显地可观察到结果。如果你的机器格外快怎么办,再拼接几次。如果 i 的大小超过了整数64位宽,开始回卷了怎么办?好问题,请阅读 CSAPP。

在回到主线——如果 拼接 1000000 次 就可以测到——之前,有的同学可能会问,你怎么知道拼接 1000000 次就够呢。我不知道,我瞎编的,我测了一下。在我机器上40多毫秒,能测出来了。如果时间太长,我就准备减小这个数;如果时间短到测不出来,我就准备增加这个数。二猫他们有一套计算方法,能大致估算出迭代多少次的数量级,我不会,又没请教,所以就靠实验方法了。

3. 测量

在拼接操作之前,测一下时刻。

:     clock_t begin, duration;
:         begin = clock();

在拼接操作之后,再测一下时刻。求开始与结束的时刻之差,这段时间间隔,就
是拼接1000000次所花费的时间,是我们要测量的物理量。

:         duration = clock() - begin;
:         printf( "\n%d ms in str_cat_pointer.", duration*1000 / CLOCKS_PER_SEC );

4. 多测几次

别笑。我确实见过不止一位同学,测了一次就给出结论。类似于,我看到过隔壁张大妈家小子从来不学习还能考上清华,因此我也能。计算机科学学生,误解了算法的可重现性。只有算法具有可重现性,算法在真实世界中的应用,其效率并不可重现,或者说是理论值的基础上叠加了噪音。即使在算法所应用于其上的数据是一致的,在CPU上跑的时候,运行的环境也*无法*保证一致。有没有别的进程,操作系统心情好么,CPU温度如何,对结果时间间隔这个物理量都有影响。

真实客观世界的物理量,如果测量与理论不符,那么就只能是理论错了。即使测量的方法有误,也是我们对测量的方法的理论理解有误。真实客观世界本身从来不会错。

所以外面套个循环,跑10次。

:     for(int k=0; k<10; k++)

5. 对比

跑10轮数组的版本,再跑10轮指针的版本。得到2*10组数据。

此外有图chart和表格table。

无优化

并且! 还要有结论啊同学们。经常有同学把数据拿出来,不给结论。那意思就是傲慢地说: 你看吧,结论多么显然。如果你没有看到这结论如此显然,就是你傻得如此显然。或者,那意思就是,尊敬的老师,证据我已经给出了,结论得由您才有资格尊贵地给出。

结论得写实验报告的人给。如果结论符合假设、实验目的,就把假设或实验目的再说一遍,并且说,实验证据支持 (事实上,波普尔说并不能证实)实验目的。如果实验证据与假设矛盾,就把实验目的再说一遍,然后说,证据与假设矛盾。

得说,明确说。

实验数据展示了,指针的写法比数组的写法 所花费的时间更短,即运行速度更快。拼接1000000次,重复10轮,每轮实验中指针版"大约"比数组版快5毫秒~9毫秒。我到这里就满意了,更严格的实验还应该给出误差范围和来源、方差分布、置信度什么的。

实验表明,在字符串拼写函数的实现中,指针的写法比数组的写法效率更高。结论是假设指针效率高,实验证据展示的是 (在这些特定的跑过的实验中) 指针的效率高,结论与证据间有联系——并且这一联系是否合理也公开出来接受别人的检验。

6. 反转

给出证据和结论,我顺手查了一下理论。是不是在语义上,指针和数组还是有微小的区别。请知道得受不了拍案而起的同学,把解释留在评论中供其他读者膜拜,在这里不是主线,我略过。

看这些资料时,我注意到有人提到 编译优化。我面无表情地做了个"吼"的表情,还是考虑不周。

按下述两种编译参数分别编译了代码跑一遍,发现故事有反转。

: "c:\Program Files (x86)\CodeBlocks\MinGW\bin\mingw32-gcc.exe" point.c

: "c:\Program Files (x86)\CodeBlocks\MinGW\bin\mingw32-gcc.exe" -O2 point.c

此外有图chart和表格table。

有优化

由于优化,代码运行的速度更快了。还是这些数据,还是这些代码,材料和工具都没变,测量方法也没变。原来35毫秒的指针,跑到了10毫秒,快了3倍有余。

更吸引我们注意的是,原来的数组版本受到优化的正面影响更大。数组版本跑到9毫秒,由优化前比指针版本慢一跃达到比指针版本更快了。

往上看,不仅要有图,还要有对图的解读。

优化以前,指针更快;优化以后,数组更快。所以,之前的结论是错的,也许写成指针,只是习惯吧。或者当年尚不能这样优化的时候,指针曾经更快过。

7. 未尽事项

实验"大致"得出了结论,至少比原始的猜测要更深入了。但是,还有些遗留问题,只是通常对结论没有影响,我们就没有讨论。这与"我的证据是假的"但是不影响我的结论不同。而是即使深入讨论,我们也已经准备好了答辩的应对。

比如,在指针和数组实验中,计时都包括了 strcpy 的时间。我们假设,strcpy的效率是稳定的,因此在1000000次计数中,我们可以剃除这部分时间。

诸如这类的假设还有很多,例如我们假设拼接更长或更短的字符串,不影响我们的结论。甚至可能进一步假设接接所花的时间会随字符串之一的长度成线性增长。我们假设 后++ 与 前++ 所花费时间区别不大,因此没有做 前++ 的实验。我们假设传参的时候用数组还是指针对时间没有影响。我们假设先做数组还是先做指针版的拼接,对于结果没有影响。

就像我们假设了物理定律的在宇宙中是普适的,我们假设材料各向同性。

这些假设可能被其他人指出"是错误的假设",或者这些假设并不总成立,或者假设成立是有条件的。我们的结论就要再修正。

公开实验证据,公开实验设计和实验数据;明确说明论证过程,明确给出结论和证据间的逻辑联系。期待别人指出其中的失误。如果暂时没有人指出,要么咱们暂时继续假设结论是对的吧,要么做的东西不值得讨论,不值得别人花时间否定。

全部代码是这样的。

/*
>"c:\Program Files (x86)\CodeBlocks\MinGW\bin\mingw32-gcc.exe"  point.c

>"c:\Program Files (x86)\CodeBlocks\MinGW\bin\mingw32-gcc.exe"  -O2 point.c
*/

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>

void str_cat_pointer(char* from, char* to)
{
while(*to != 0)
to++;
while(*from != 0)
*to++ = *from++;
*to=0;

}

void str_cat_array(char from[], char to[])
{
int i,j;
for(i=0; to[i]!= 0; i++)
;
for(j=0; from[j]!=0; i++,j++)
to[i] = from[j];
to[i]=0;
}

int main()
{
clock_t begin, duration;

char f[11]="0123456789";
char t[30]="abcd";

for(int k=0; k<10; k++)
{
//---------------------------
begin = clock();
for(int i=0; i<1000000L; i++)
{
strcpy(f, "0123456789");
strcpy(t, "abcd");
str_cat_pointer(f, t);
//printf("\n%s\n", t);
}
duration = clock() - begin;
printf( "\n%d ms in str_cat_pointer.", duration*1000 / CLOCKS_PER_SEC );
}

for(int k=0; k<10; k++)
{
//---------------------------
begin = clock();
for(int i=0; i<1000000L; i++)
{
strcpy(f, "0123456789");
strcpy(t, "abcd");
str_cat_array(f, t);
//printf("%s\n", t);
}
duration = clock() - begin;
printf( "\n%d ms in str_cat_array.", duration*1000 / CLOCKS_PER_SEC );
}

return 0;
}

装机器

装机器

用了7年的旧机器坏了。昨天晚上关机的时候还好好的,早晨开机没有反应了。用过一台型号接近的机器也是这样,当时请包师弟帮助诊断和尝试,没救回来。XL同学送去厂商,说是没救了。
微信图片_20220204221701
zhumao同学推荐过一台机器,真·没有风扇的。我对低频的噪音非常敏感,经常能达到生理的恶心想吐。当初选这台坏了的机器,就是因为它宣称自己没有风扇。但是它是吹牛的,CPU上有风扇,只是电源没风扇。最初仔细听就有声音,后来声音更大了。吹灰没用。

正好趁这个机会换台机器吧,就换zhumao推荐的那个。
微信图片_20220204221659
1. 硬件

新机器来以前,先把旧机器再拆开。确认故障和尝试维修的时候拆开过多次了,这次是为了分解。

原来的硬盘我想留着。因为TM同学说,说不定运气好,安在新机器上什么也不用改直接就能启动呢。旧硬盘数据备份,万一不能启动呢,就得重装系统,到时候别伤了数据。文件多而小,花了几个小时还没有结束。后来我终于不耐烦,只拷出觉得有用的东西。
微信图片_20220204221648
内存说不定也能用上?问厂商,说是能。然而,我瞅着不太对劲啊。旧的内存早就是过时的了型号DDR3,不能用在新机器DDR4的插槽上。照片比对,厂商再观察一次,并不能例旧。拆下来两条,一条加在二猫的机器上。

久不装机器,这许多规范得一一对照识别,再查看插槽和网卡、硬盘、内存是不是匹配。

过了几天,新机器到了。
微信图片_20220204221655
因为在几个不同的地方工作时都要用到,如果有多个电源那么携带时能更轻一些。旧电源没法用,电压不匹配,旧的是19V,新的是12V。找来旧笔记本的电源遍历一遍,电压或者功率或者机械部分,没有一个符合的。只有一个电源,每次必须随身带着了。不过,更糟的是,相比机箱而言,电源并不是最重的。机箱是全金属的,6个表面中有4个面是整块铝车出来的。沉。

为什么不用笔记本?因为我的视力差,严重看不清显示器,所以笔记本带显示器对我是没有意义的。显示器也只能用有架子的,因为需要比键盘离我更近一些。
微信图片_20220204221657
显示器。现在正用的DELL显示器,有dp口、dvi口、vga口,没有hdmi口;新计算机,有hdmi口和vga口。二者的交集是vga口。自从几年前典同学告诉我模拟器件不可用,并且给我一个VGA到数字的转换头以后,我就弃绝了再使用VGA的念头。所以,旧显示器和新计算机之间没有交集。

网上下单买了个hdmi和dp的转换头,想着这样就架起了计算机和显示器间的桥梁。转换器到了以后,试了半天没反应,亮都不亮。二猫妈端着手机,说,"听说这玩意有方向的。"

我令 dp接显示器,hdmi一侧接计算机。这种接法叫做 hdmi转dp,我在网上搜索商品时也使用了这个关键词。然而,买到的东西是 dp转hdmi,与我希望的hdmi转dp方向相反,虽然两端刚好能插进去。这帮家伙怎么学的墨菲法则,能插进去的东西居然有方向性。

好吧,再找 hdmi转dp吧。虽然不太容易找,不过确实有这样的转换头存在,需要另加usb电源,有人说会发热,并且贵,19X元。

型号还真是多种多样啊。而且历史在发展,旧显示器到手的时候,hdmi还没开始大行其道,所以并没有这样的口。后出的新计算机,居然没有dp口。

不能忍加电源和发热,也不想花将近200元这大头钱。解决方案是,买了个hdmi转dvi口,3x元,不需要另加电源。
微信图片_20220204221644
CPU 100%持续一会儿,机壳会温乎,再多一会儿,有点烫手。zhumao说太热并且时间长,可能会损坏硬盘。一般情况,室温20度左右,机壳是凉的。这个无风扇方案推荐了几位同学,fwyx和ztz同学分别说,打不了游戏。他们指的是我没玩过的新游戏。

2. 操作系统,以及必备的工具

原来硬盘上的是win8操作系统,启动失败了。进入命令行修复,查了一些网上的资料,不顺利,许多分支需要探索,很多坑需要填。TM同学说,你放弃吧,win8都是什么年代的古董了。

PE系统用的TM同学推荐的ventoy。按TM、FKJ、MC同学的建议,装了win10。只有2个没有自动识别的设备,用搜索就找到了。按TM同学的提示设置禁止了更新。

重装了操作系统,而且不能升级,所以硬盘格式化了。
微信图片_20220204221643
- emacs 日志

从旧备份里拷来的旧版本,所以配置文件稍微修改,仅注释了 color-theme,就能用了。以前装新机的时候下载了新版emacs,导致配置文件失效,改了半天,颇为后悔。新特性极少带来惊喜,经常带来麻烦。

- git 版本控制

下载了新版本,使用旧的repository。原来机器上修改过 hosts 文件,把repository 的 IP 设置成名为 git 的主机。忘了备份。查更旧的记录,不能确认是最新的IP,不过连接超时。从zhumao处要到新IP,好了。

ssh。win10下git通过ssh连接远程,需要单独设置一下:

: $ cat ~/.ssh/config
: Host git
:     User git
:     PubkeyAcceptedAlgorithms +ssh-rsa
:     HostkeyAlgorithms +ssh-rsa
:
: 我还执行了
: ssh -o HostKeyAlgorithms=+ssh-dss git@git

- 五笔

微软 win10的五笔支持与拼音混用,符合我的要求,不再装QQ或者搜狐五笔了。默认7字长的候选窗口在emacs下是黑色一长条,里面的内容看不到。设置成3个字,好了。有时候切换反应慢,重启机器好使了。原因不明。

这样,能写日志,能保存工程文件,我的机器就可以开始工作了。
微信图片_20220204221641
3. 软件

- 各种密码

大部分网站的密码都记在了浏览器里,而不是脑袋里,所以回想、尝试、找回密码。

- gpg, gnupg Gpg4win 加密

用cleopatra导入了旧的私钥。

//以下log有省略。
: c:\Users\Young\.ssh>gpg --import younggift.gpg
: $ gpg --edit-key younggift
: gpg> trust
: Your decision? 5
: Do you really want to set this key to ultimate trust? (y/N) y
微信图片_20220204221652
- calibre 电子书工具,转换、阅读、kindle管理

- 滴答清单

- firefox

- 火绒

- firefox及插件

-- adblocker ultrimate,屏蔽广告

-- dwhelper with coapp,帮助下载视频、音频

-- Export Tabs URLs,导出tab中的URLs

-- Tampermonkey,猴子脚本

-- Vimium C - All by Keyboard,仅用键盘打开网页上的链接、x关闭tab
微信图片_20220204221647
- total commander

从旧备份中复制,以前的授权和配置一并导入,配置仅略修改。设置部署字体为
microsft yahei mono,是 微软雅黑 和 consolas 的混和。

- vpn & 远程桌面

又 需要找到远程桌面服务器的地址、用户名、口令。

- everything 快速文件搜索工具

- filezilla client FTP文件上传下载

- 微信
微信图片_20220204221654
- 上下翻转显示器

这还需要物理世界里手动操作。我的显示器是上下颠倒的,因为显示器在键盘的正上方,下降时连线可能触到手背,所以显示器上下颠倒,插线的接口朝上。也要在操作系统中设置。

- 关掉操作系统休眠

节省硬盘空间,大小等同于内存大小。保留定时关闭显示器和硬盘,省电,避免不用时显示器太亮,希望能有助于硬盘寿命。

- office

有些工作要求只能使用word,不要尝试其他的工具能不能兼容。

- 蓝牙

以前的蓝牙模块在旧机器上表现不好,声音卡顿。同一副蓝牙耳机与手机连接就没有卡顿问题。在新机器上,这个现象消失了。新机器没有蓝牙模块,正好用旧的这个作为外接。设置为 hands free 模式,不卡顿;stereo模式有时卡顿,有时 stereo 模式会消失,原因不明。

- sai2 以及 手写板驱动
微信图片_20220204221703
- snadboxie plus x64 沙盒,避免应用程序污染操作系统

在 sandboixe 里面安装了不得不用的 360浏览器,教务处和研究院的系统只能用这个,别的浏览器没有一个好使的。

还在 sandboixe 里面安装了 迅雷影音 和 百度网盘。
4. 未来

昨天又安装了jre。很快,就会逐渐安装越来越多的工具,gcc, opencut, audacity, math...有什么活儿,就得装什么软件。虽然可能长时间不用,但是需要的时间希望它就在手边。工具正如书籍,需要时刻相伴。有过老同学借书被拒的时间说,"你也太抠了,我的书都可以借你。"我说,"我们都爱书,不同的是 你读书是爱好,我读书是工作。我需要的时候,他们就需要立即在,不能等。就是生命。"

微信图片_20220204221650

用大电视放电影

很久以前在图书馆工作的时候,同时开几台计算机,每台放着不同的片子,一起看。嗯,测试机器性能和练习软件配置。也许更早就喜欢看电影,不过,是从那时起开始养成看电影这个习惯的吧。当时一起工作的前辈已经多一半退休或即将退休,挺想念他们的。

只看电影,没买电视。所以对有些人担心孩子看电视拦不住难以共情,如果家里没电视当然也就不会有这个问题了。没买电视的原因不是因为钱或者时间,而是因为习惯。从小就不喜欢看直播,对提前半天半年知道哪一队胜了始终也提不起兴趣来。对电影电视剧里隔离出来的人大喊“你别告诉我世界杯结果”不能理解。

这么多年来,仅有三次用到电视。一次是大半夜正准备睡觉,电视开始放《秋日传奇》,我和二猫妈不睡觉又看了一遍。一次是北京申奥成功,我跟图书馆的同事正在角山长城附近玩,大家一起看了直播。一次是中国好声音,有一集看的不是回放。剩下的时间,包括春晚,很早就看一部分的回放,也没有觉得缺什么。

看什么由我选,不喜欢被推送。不少人没事就打开电视收音机,也不看不听,还能干活休息,我完全不能理解。结果就是家里有台大电视,我喜欢看的,它没有;它有的,我觉得是烂片。联网就升级,升一次级挺长时间,除了广告什么也没有更新,除了速度更慢没有什么变化。

看过不少电影和电视剧,用的都是计算机,不是电视。这几年时间紧,电视剧也很少看了,有剧集"在看"了近十年没有完成的。

对,就是时间。吃饭的时候看电影,是个好时间。可以把电影拆了,开始吃饭的时候定倒计时,每次看半小时。但是用计算机看电影对吃饭有影响,需要占点桌子空间,或者接计算机一两分钟。心理学告诉我们,如果你想干什么事,别犹豫,也别创造犹豫的环境,并且要去除导致犹豫一下的可能。所以,要更迅速地能开始看电影才好。

这台电视确实能看电影,它能播放U盘里的MP4。

1. 问题

这台电视播电影有些问题。 (1)兼容性。有些文件能放,有些不能放,系统就显示不能放。可能与格式有关。 (2)卡顿。有些电影放起来动作一格一格的,看十几秒我就快晕车了。通常大文件这种情况更容易出现。我的计算机性能低下 (是的,比很多同学觉得没法干活的机器要慢好几年),播放有些视频也卡。与文件大小不总相关,有的大文件8G、40G的倒是不卡,而2G的也可能卡。 (3)声音。有的电影放的时候没声,在计算机上放就没有问题。 (4)有的电影没有字幕。虽然有时候可以锻炼英语,但是奈何有些片子是日文的,在计算机上有英文字幕,在电视上却是生肉。

2. 解决方案

有相当长一段时间,就是选能看的片子看。能看就看,不能看就不看,或者在计算机上看。

有一天想起了邦哥的话,"你说你一个学计算机的"。他说这话时,我正犹豫给苹果越狱,担心变砖,他重复了这句我们常说的话,给了我勇气。我一个学计算机的,怎么能被一台电视难住。

祭起伟大的影音工具的祖宗 ffmpeg。

(0) 输入输出

实验全部完成以后,做了个bat文件,看起来像这样。

ffmpeg -i %1 ... ... %1.mkv

其中的"... ..."是此刻省略的内容,一会儿解释。

%1 代表bat的第一个命令行参数,这里就是电影的文件名。执行的时候这样:

to_h264.bat from.mkv

会生成一个同名的文件,叫做 from.mkv.mk。加个后缀以示区别。

(1) 格式

有的不能放,有的能放。那么看看能放的都是什么格式,都转成这种格式。h264和mpeg4都行,就用h264了。

参数为 -vcodec h264 。

(2) 压缩,码流,分辨率

大文件不能放的可能性大,很可能跟分辨率和码流有关,把文件整小点就行了吧。尝试几次以后,确定了一组码流的经验值,压成这样播放不卡顿,清晰度和连贯性也感觉无异。

分辨率,刚开始实验了几组,看了几个片子。后来有一天突然想到,怎么这么猪脑子。码流可能没有规范参数,分辨率应该有啊。找了下电视的"关于",确定了支持的分辨率。也不必更高,没用,还得做缩放,可能有锯齿,还费CPU。

参数为 -vf scale=1366:768 -b:v 2000k -bufsize 5000k -maxrate 2500k 。

(3) 声音

不幸的事,各有各的不幸。有的放不出声来,不必关心为什么。幸福的事,都是一样一样的,或者抽象出相同的因素来。只需要看看能放出声来的是什么声音编码器就行了。MP3肯定行吧,一查,果然。

参数为 -acodec mp3 。

(4) 考虑不够周全,修bug

按以上参数,播放了一些电影,没有问题。某天又遇到一个有问题,卡顿。用计算机对比,计算机播放不卡顿。检查生成文件的格式、码流都没有问题。

用 ffprobe 查详细参数,然后用excel与不卡顿的电影对比。

卡顿的两个 Stream #0:0(eng): Video: h264 (High 10), yuv420p10le(tv, bt709, progressive)

不卡顿的 Stream #0:0: Video: h264 (High), yuv420p(tv, bt709, progressive), 1920x1080

bitrate, fps等参数有区别,相差不大。

有意义的区别在于 a. yuv420p10le和yuv420p, b.还见过 h264(high 10) 和 h264(high)。

这些术语超出我知识结构了,查了下资料。

a. 概念 420p

: 420p 10bits little endian

b. 概念 h264 high

: H.264 Baseline profile、Extended profile和Main profile都是针对8位样本

: 数据、4:2:0格式的视频序列,FRExt将其扩展到8~12位样本数据,视频格式可

: 以为4:2:0、4:2:2、 4:4:4,设立了High profile(HP)、High 10

: profile(Hi10P)、High 4:2:2 profile(Hi422P)、High 4:4:4

: profile(Hi444P) 4个profile,这4个profile都以Main profile为基础。

如果不强行指定,应该是沿用了源文件的参数。

参数为 -pix_fmt yuv420p

(5) 总结

to_h264.bat :

ffmpeg -i %1 -vcodec h264 -acodec mp3 -vf scale=1366:768 -b:v 2000k -bufsize 5000k -maxrate 2500k -pix_fmt yuv420p %1.mkv

(6) 字幕

为了减少实验的反复,所以字幕当时制定了几套方案,全都跑一遍出结果,然后批量测试的。

a. 硬字幕,就是以图形图像的方式加在画面上那种。韩国人常这么干?压缩一遍时间巨长。

b. 软字幕。字幕以前可以从射手网下载,后来有一段封了。字幕还可以在迅雷影音这类工具播放的时候从网上匹配,导出成单独的文件。要把字幕嵌入到电影文件中,还要要考虑国别、字符编码等等。我一顿考虑各种可能,做了多组实验。

c. 我还没有固定脚本,二猫发现没有那么麻烦。把迅雷影音导出的字幕和电影文件放在同一个目录下,我的电视就可以自动找到字幕了。

(7) 播放器

电视原带的播放器不行,在开始压缩转换以前,就尝试了安装别的播放器。mx player最好。

(8) 剪切,实验用

测试的时候并不是每次都压一整部片子,那需要的时间太长。压缩通常是0.5倍播放速度到1.5倍播放速度。这样,需要0.5~3小时左右。所以做了个剪切一小段时间的版本。

to_h264_cut.bat :

ffmpeg -ss 00:01:00 -to 00:06:00 -i %1 -vcodec h264 -acodec mp3 -vf scale=1366:768 -b:v 2000k -bufsize 5000k -maxrate 2500k -pix_fmt yuv420p %1.mkv

3. 效果

运行有一段时间了,效果一直不错,感觉可以认为稳定了。

4. 以后的工作

现在电影还是要放在U盘里,每次插入,或者提前拷好。接下来,准备尝试架设流媒体服务器比如SRS,用OBS推流。这样,可以在计算机上实时推流电影,在电视或者手机上看。也可以推流bilibili边吃饭看看网课。卷吧。

修电钢琴

1. 前情回顾

前两天发了篇博客 修水壶盖,朋友们纷纷来电来函安慰我。

有的说,你居然还用水壶啊,我们一早就开始使用更高大上和现代化的烧水设备了,没有盖儿或者没有钮。我用的水壶也是用电的,不是用煤气或者煤或者柴火。时代久远,我已经遗忘了水壶还可能不用电,就像邮件也能不用电,报文也可能不用电,所以在文中没有申明,故造成此些误会。

有的说,换个得了,有贴心同学甚至发来了京东链接。我赶紧写了这篇,并借此声明,上一篇并非水滴筹大家为我买水壶,不然我担心很快发来的就不是链接而是实物,附带需要我署名的感谢信了。水壶不贵,我买得起。然而,修水壶的乐趣,哪怕只是修盖儿上的钮儿的乐趣,也是金钱换不来的。这乐趣非常昂贵,只有投入时间才能得到。此处有捂眼的哭脸表情。

有的啥也没说,就哈哈大笑。我理解为与民同乐了啊,而不是像有的同学解读二猫妈那句“304钢是嘲笑”。那并不是嘲笑,而是善意提醒,也是与民同乐。我妈告诉我,现在一般的壶,除了特别便宜的,都是304钢,特别便宜的那种是铁片子。总而言之,铝壶这种东西彻底消失了,就像不用电的各种东西一样。

也有同学质疑,这么便宜的水壶,值得一修嘛?前文已经答复,乐趣非此不可得。不仅如此,如果不在这种便宜东西里锻炼,怎么可能修更贵的东西呢?就像有的同学,希望上手就做个高大上的项目,可是,没有(你以为无聊但是非常有趣的)小项目的经验,哪个客户敢把真实项目给咱们做呢。

修点什么贵的东西呢?就电钢琴吧。

2. 问题

这台电钢琴已经停产了,2011年价值245,000 日元。

[https://jp.yamaha.com/products/musical_instruments/pianos/clavinova/clp-s406b/index.html#product-tabs]

> CLP-S406 生産完了品

> 希望小売価格: 245,000 円(税抜)

>     2011年7月 発売

按当今的汇率折算相当于13793.89 元。网上有售声称9500.00 元 或 8150.00元/台。

有的同学可能觉得这也不贵啊。问题是,我手头上就有一台,只有一点毛病,最先是偶尔,后来是经常,某几个常用的键发出的声音特别特别大,而且尖厉。这就像水壶盖上缺个钮,羽绒服的拉链坏了,这个部件微小且至关重要,同时其余的全是好的。

换新的,你可能有钱。但是,这台/壶/件旧的呢,难道扔了不成?

现象是 有时,有的键轻按会出很大声,就像重砸的效果,二猫称为“炸了”。有时,有的键按下以后,出声有延迟,影响节奏。中央do,低音sol最先出故障,不止这两个键,而且有增多趋势

炸了,持续了有一两年了吧。我要拆了修修,二猫说你等我过完某级吧,不然万一装不上可就影响练琴了啊。直到后来出声有延迟,我节奏大乱,简直弹不下去。本来并非大问题,也并非每次都有,但是越弹就越觉得明显。后来终致我中断练琴长达一个月,然而也没有找到合适的时间拆琴。没拆过,心里没底,估计怎么也得大半天吧。万一半天没修完,还装不回去,占地面积可不小呢。在这之前有过几天想尝试,二猫妈说“今天你是不是没睡好啊”“今天别的活儿都干完了么”“找个不那阴天的日子吧”。终于,某个下午,我们决定,就今天,拆了吧。

先剧透结局,拆下键盘总承及其上的电路板,用酒精擦拭导电橡胶,问题解决。声音完美,没有丝毫毛刺了。二猫评价,就像买了个新琴一样。长久以来,以为是弹得不够好的曲子,一下子变得柔和动听了。我上去试手,总炸的那几个键已经不会按了,声音特别小。二猫说,弹上十分钟就习惯了。果然,而且,突然对轻重有了些许顿悟的感觉。

修好以后,在朋友圈里发了照片,有几个简单的讨论。在此交待一下。

(1)电钢琴不必调律,音高和音色都是电子器件完成的,只需要拆开清灰。不过,拆开清灰也不容易。(2)拧螺丝的时候得小心,特别是旋进的时候,木头挺松的,令人担心会把木螺纹拧秃。拆卸时有的地方需要长而细的螺丝刀才能触达,10厘米左右吧。(3)有同学问我为什么不找专业人士。我问了音乐系G老师还有钢琴专家S老师,他们都说,这得找修琴的。问了包师弟,他的意思大概是这么简单的结构也能算是电子设备么。我还查了专营店,感觉不如我自己可靠。

品牌和型号

image1

image2

3. 工具

image3

image4其中,餐刀用于在拆下上盖以后拆单个键。这是条错误路线,单个键拆下后,不能暴露出导电橡胶。

要拆下哪个键,把餐刀插入键的缝隙中,左右是要拆下的键上面的箭头指的方向,上下是圆点的位置,

image5

image6放大镜几乎没用,太暗看不清。

另外,手机很重要,用拍照和照相加常亮的灯,用于观察钢琴内部,猜测内部结构。

image7

image8

image9

4. 步骤

后盖上半截中的所有木螺丝。

image10

移除上盖,左右两侧一起向前推。

image11

键盘上盖,两侧共6个螺丝。

image12

把键盘上盖向前推,暴露出中间2颗固定弹簧的螺丝。移除这2颗螺丝,然后移除键盘上盖。

image13

控制面板与底板之间,在内侧的螺丝。一排,每个柱有2颗螺丝。以下是举例,不是全部。

image14

image15

image16控制面板上层,左右两侧网状结构与框架连接的螺丝。左右各拆下1个就够了。(这次维修的时候多拆了1颗,不是必要的。)

image17

image18

面向钢琴,左侧喇叭下方,2颗铁螺丝。

image19

面向钢琴,右侧喇叭,2颗木螺丝。

image20

键盘总承与框架底板间的连接螺丝,粗大,不少根。

其中有2根细小一些,图中手指螺丝左侧的那个小的,喇叭正对着的,是其中一颗。

image21

键盘。

image24

键盘的前面与前面板之间,就是直接抵住的。

image23

下图手指位置的空间,需要把键盘总承前移,把这部分空间挤满。

image24

不是键盘放在下图的铁架上面。

image25

键盘左右与框架刚好顶住,没有空隙。键盘总承有一点柔性,可以稍微掰弯一点儿。

取下键盘总承。

image26

拆下键盘总承底下的电路板。暴露出导电橡胶。

image27

用酒精擦拭导电橡胶表面,铜触点表面干净,未擦拭。

导电橡胶的底座,柔性材料,反面很多灰尘,也擦拭了。

image28

导电橡胶装回有点困难,需要把上边缘里的突起插入框架。

image29

电路板下方(图中右侧)插入框架的卡子下面。图中右侧的白色矩形与框架上的卡子对齐。

image30

拆下键盘总承,安装回去以前,实在太饿了。我们订了外卖饺子,吃完才有力气继续。

5. 损失/失误

最左低音键划伤,原因不明。

image31

左侧喇叭,被我手指按瘪了。右侧喇叭也瘪了,原因不明。

不影响音质。

多了一块小塑料,似乎是键盘总承支腿的一部分。某次把键盘总承拆下来时,忘记把电线拆下来了,同时掉下来了这小块塑料。短边,下方的那条,是断裂处,有白茬。

image32

  1. 水壶盖的钮

然而,水壶盖的钮仍然没有修好。

image33