格拉肖之蛇-溯源之旅

格拉肖之蛇-溯源之旅

故事要从2022年6月18日讲起。与六一八购物没什么关系,从这个意义上说,就是随便的一天。不过,由于有具体的日期,以后需要回顾更多细节的时候更容易追溯其他细节。

1. 汉英互译

最初,师兄陈昕若问我,“1986年的某一期纽约时报,有什么办法找到?”

我说,“告诉我哪一期,想找哪篇文章。”

陈昕若语音,“你听说过欧若波若斯蛇么?”

我说,“我就听说过法老蛇。”

我一下子想起法老蛇,是因为师兄的职业是高中物理教师,有精彩的网课,还经常做有意思的演示实验。一千根雪糕棍编织的蓄能长带,点火触发以后像蛇一下昂首在空中舞蹈,满教室学生欢呼。冷气四溢,低温超导体悬浮。跟我讨论乒乓球在空中悬浮时伯奴利力和什么力来着哪个占的比例高。所以,这么奇怪个名字,什么蛇,我一下子就想起化学实验那个法老之蛇,马上想提醒他那个挺危险的,有毒。

同时,我搜索,猜测着把欧若波若斯这个发音输到 bing 里,输入开头,后面的提示就来了。有时候,搜索的时候 bing 会给出发音或接写接近的建议。经常是对的。尽管有新华社音译要求,源头是英文的资源,在转译为汉语时,包括术语在内,但是现实情况是经常有多种不同翻译,音译更是不统一。所以,我找资料的时候会尽快确定英文原文是什么。

搜到了这个,Ouroboros。

C:\Users\young\AppData\Local\Temp\WeChat Files\91e8544178f2a1cd0f5766eeb561d92.png

2. 这个我认识

这个图我认识,龙神。形象是一只咬住自己尾巴的大蛇。如果搜索资料是个游戏,这算是作弊吧——我刚好知道答案。在技术讨论中却只能作为线索,而不能作为证据。总不能拍胸脯,别人就信你吧。

龙神,在科幻界非常有名。海因莱茵有著名作品《你们这些还魂尸/All You Zombies》,又译名《龙神》,就描写了这样的意向。当年科幻世界笔会的时候,某位作家给我们复述这个故事。我还记得他讲到,“这位女子长得非常难看。”有人打断他,“讲重点”。他说,这点非常重要。后面是个盘肢错节,循环往复的故事。给出的象征意向,就是吞吃自己尾巴的龙神。只是我忘了Ouroboros这个名字。

我给了陈昕若上面的截图,以及以下信息。

[https://www.britannica.com/topic/Ouroboros]

他给了我具体诉求,以及上下文。

我整理一下,要找的是符合下面要求的文章:

作者:著名的美国物理学家、诺贝尔奖得主 格拉肖

时间:1982 年 9 月 26日

期刊/报纸:《纽约时报》

篇名:未名

内容与物理学有关,可能涉及粒子物理和宇宙学。

重要线索:咬住自己尾巴的蛇。

3. 作者,报纸,墙

格拉肖,Glashow。诺贝尔奖得主这种级别的物理学家,名字容易找到。

纽约时报,这种级别的报纸,找特定日期的,也容易找到。

但是在目录里,这一天没有找到这一作者的文章。

我访问不到这个页面。

线索分成了两条,一条是作者,一条是报纸。

报纸这条线索不顺利。报纸的更多细节,我访问不到。后来new york times 找到了2015年合订本,没找到其他年份的。[https://www.nypl.org/research/collections/articles-databases/new-york-times-1980-present]

这个需要借书卡。

C:\Users\young\AppData\Local\Temp\WeChat Files\b8da6a35ff2f17fc07c0b56752a90c8.png 此外,网速特别特别慢,并且经常超时。我们找到了那天的报纸,看着一点也不像有大蛇的样子。以下是部分截图。

C:\Users\young\AppData\Local\Temp\WeChat Files\976a0d2c90294d637c91cfec8e728d0.jpg

作者这条线索,格拉肖确实提到过这种蛇。

[https://www.mediatheque.lindau-nobel.org/laureates/glashow]

C:\Users\young\AppData\Local\Temp\WeChat Files\50215d34f86d6cc10a8c36e0a9e0f8f.png

上面这张照片的左边,就是大蛇的一部分。

在他的书里,也提到过大蛇。Interactions - A Journey through the Mind of a Particle Physicist and the Matter of This World (Sheldon Lee Glashow, Ben Bova) ,1988年出版。

纽约时报书评提到过的,也许就是这本书。

[https://www.physics.harvard.edu/people/facpages/glashow]

格拉肖本人还在世,90岁,这里有邮箱。写邮件去问,也是一条分线索。

此时我开始怀疑,也许并没有那么一篇文章存在,作者是格拉肖,发表在那个报纸的那一天。

4. 到底要找啥

我问陈昕若,你那段中文,从哪里截的?原著是英文么?说不定翻译错了,如果是英文我去查下原文。

是这个。

陈昕若还找到格拉肖的另一本书里也有大蛇。

C:\Users\young\AppData\Local\Temp\WeChat Files\b14bd8306410ee40b855e28ac7f9c88.jpg C:\Users\young\AppData\Local\Temp\WeChat Files\ad25d25d6096deb5504d989f4e5562c.jpg

然而这并不能说明那份报纸那一天有这么一条大蛇。为啥非要找那张报纸呢?那是最早?还是就为了和那本中文书那句话对应上?

因为强迫症吧。这是不少中文科普资料也带给我的困惑,那就是,这个观点、这张图,是不是作者瞎编的,到底从哪来的,什么时候开始有这种说法的。这种说法是不是符合客观事实,这是另一个问题了。我长期困惑包括,历史书上的但丁为什么长得像个女人,伽利略和哥白尼怎么长得挺像的。溯源找到更清晰版本以后,困惑根本就不存在,问题在于印刷质量。

那么,这条大蛇用来像素物理法则到底何时出现的,谁画的,与纽约什么报纸有什么关系。

5. 文章,出版物,作者

我知道 New York Times中的 Times有翻译为时代广场的,还有说与泰晤士报(不是泰晤士河)有关的。但是没有想到 New York Times 和 New York Times Magazine 的区别会让检索麻烦这么多。

在跟踪作者格拉肖这条线索的过程中,终于报纸的线索也露头了。

有篇文章非常讲究地给出了参考文献。

http://ircamera.as.arizona.edu/Astr2016/images/extcosmo.htm

8. Sheldon Glashow, sketch reproduced in T. Ferris, New York Times Magazine, Sept. 26, 1982, p. 38.

所以,这份媒体根本不是《纽约时报/New York Times》,而是 New York Times Magazine。

C:\Users\young\AppData\Local\Temp\WeChat Files\40315cfe74ad73fa7235fcd5b51aabc.png

以上是免费即可看到的目录,红圈里,就是那条大蛇。

C:\Users\young\AppData\Local\Temp\WeChat Files\8a779199c742e24221ededc144e1429.png

作者也并非格拉肖,而是Tim Ferris。

“Sheldon Glashow sketch reproduced in T. Ferris N. New York Times Magazine, Sept. 26,1982: 38.” 那么,是glashow手绘,在这篇文章里发布的。

所有大蛇,可能最初的源头是这里,后来分化出很多细节,然而源头考证起来就麻烦了。

比如照片切一部分作为书的封皮。

还考虑过联系下面这位,问老先生的资源出处何来。没机会了,老先生去世了。

刘则渊(大连理工大学科学学与科技管理研究所暨WISE实验室),“他格外热心中学生的科学教育,1982年手工绘制了一幅呑食自己尾巴的巨蛇草图(图1),生动形象地描述了科学世界与大统一理论。”[https://blog.sciencenet.cn/blog-1557-661949.html]

C:\Users\young\AppData\Local\Temp\WeChat Files\bc9edf8952259a3531ce74495b85558.png

这个版本的风格非常接近手绘,像是原始出处。下图是对比。

C:\Users\young\AppData\Local\Temp\WeChat Files\71dca0e4cf0ba7beb89ad6dd2ce9780.png

陈昕若提供的,高中教材里的。明显重绘过,取其意,而失去了手绘风格。

C:\Users\young\AppData\Local\Temp\WeChat Files\bb54dfa89f5a9a01550af2cddde9f9e.png

教材里。

C:\Users\young\AppData\Local\Temp\WeChat Files\8a2cbab74b67c41266c88eaeb5d09b9.jpg

但是出处,就像此前我提到的,“我刚好知道”,没法作为依据。

我和陈昕若聊起来关于证据:我信又如何,难道也教给学生“我就是这么学的”,那样的话还要旁证干什么。他也赞同,所以希望清晰的图片,当然还要出处。

我引证 微信公众号 “我查查看”,还有

https://mp.weixin.qq.com/s/noeHEUMN-MkGMzTqLk9xqw

10年了,终于有人发现中文维基百科的俄罗斯历史是胡编的了。

陈昕若一本本给我看物理书,都有这条大蛇。

6. 原文

故事的最高潮不是由我们完成的,而是由图书馆的老师出手。当知道了文章名、作者、期刊以后,找原文是个专业工作,非我们所能为也。得到两个版本,一个有全文文字版,另一个配了图片。感谢图书馆的同事,牛!

原文的封皮即极有震撼感,电场中的基本粒子轨迹。

大蛇在此。

后面是对撞机一类大型设备的仰视图,雄伟壮观。

故事的尾声是这样的。

我说,得写参考文献,不是用来显摆真有这么本书很牛,而是用以表明观点不是自己的,其来有自,参考文献就是说明其来源的。以供读者核对和溯源。

陈昕若说,“咱俩整理一下检索的过程吧,你写个博客,我做个视频。期限就约在年底。”

 

一行脚本 不断自动恢复下载

1. 问题

你遇到过这样的情况么?用浏览器下载文件,网速不给力,或者你知道在从服务器到你的座机间网络的某个节点有个什么限制了下载速度,然而你无能为力。所以同一个文件在学校可能只需要15秒下载完毕,在家里就需要15分钟,而且时间不稳定,还可能慢慢变长。

可能突然就会这样,超时了。右边的叉(取消)变成了带箭头的圈(重试/恢复),你及时点击它,可以续传。

如果你一眼没照顾到,过一会儿就彻底超时了,只能重新下载。

如果只需要几分钟还可以忍受,但是往往涨成几十分钟,把你绑在椅子上,又不能安心去做别的。

为什么没有这样的工具呢?(也许有吧)我怀疑是因为在这个世界上并非处处都有你我遇到的这一困难,别人的网速没准特别快,根本不需要手动续传,甚至不会触发超时。

面临这个问题时,我通常在心里诅咒,然而于事无补。后来我终于忍无可忍写了一行脚本,替我看着下载进度,一旦需要恢复/重试,就去自动点击一下。

2. 使用方法(在Firefox下测试通过)

使用方法很简单,不需要会编程序。之所以没有做得界面更友好,是因为我懒,所以需要按以下步骤操作。

第一步 开始下载你的目标文件。

第二步 在地址栏中输入 about:downloads,回车。

Ctrl-J 并非总能符合下一步的要求。

第三步 F12,得到 console。

第四步 在 console 中输入以下这行代码,回车

setInterval(function myTimer(){$("[data-l10n-id='downloads-cmd-retry']").click()},1000)

这就是唯一可以算作我工作的部分,一会儿解释原理。

得到如下效果,其中的82可能是其他数字,不必关心。

效果是,从现在开始,所有正在下载的文件,如果出现 恢复/重试 按钮,会在1秒钟内自动被点击。我保持开着这个页面,切到后台,时不时过来瞅一眼,看进度还在,再放心切回去干别的活儿。这个页面关掉,也会不断在1秒内重试,直到关闭浏览器。

3. 原理

原理是,about:downloads 这个页面也是HTML/CSS/JS的,所以可以在 console 中在线修改。

点击inspector (这并不是它的名字,那个按钮的名字叫做 Pick an element from the page),

然后点击带箭头的圆圈,得到按钮的HTML代码。

既然能手动找到按钮,(1)我们可以用代码

 $("[data-l10n-id='downloads-cmd-retry']") 

找到这个按钮。

(2)找到按钮以后,我们用

$("[data-l10n-id='downloads-cmd-retry']").click()

点击它。

(3)触发动作的时机,我偷懒,并未(轮询或中断/回调)检测 恢复/重试 出现,而是每1秒钟尝试点击一次。

setInterval(function myTimer(){$("[data-l10n-id='downloads-cmd-retry']").click()},1000)

即使按钮并不是 恢复/重试 状态,此处也不会报错。

如果有多个恢复/重试按钮,这段代码会逐一点击。

4. 未尽事宜/后续工作

我本想写成 tampermokey 脚本,这样大家(以及我)用起来更方便一些。但是我遇到了困难。tampermonkey 脚本应该匹配特定站点,只在特定站点或地址执行。我不知道如何把 about:downloads写入 // @match 这一行。

如果有多个恢复/重试按钮,这段代码会逐一点击。这并不是我原始的态度,不够优雅。我本打算 想设置哪个文件为不断重试,就只重试它自己。然而,我在 console 中不会指定特定的某个按钮。试过用下标区分匹配的按钮,即类似 $("[data-l10n-id='downloads-cmd-retry']")[0] 这样的写法,实验表明需要在 tampermonkey中

// @require http://code.jquery.com/jquery-latest.js

才能支持下标语法。或者会触犯 firefox 或 chrome 的安全规则?总之我没成功。

在 Chrome 的 console 下我不知道如何(在不触犯安全规则未能导入jquery)使用 $("focus-type='retry']")

找到 恢复/重试 按钮。所以向 Chrome 移植失败。

哪位大侠知道如何改进,还请不吝指导,我尝试成功以后补进以后的博客中。在这之前,对付着用吧。

好工具 | 用键盘浏览网页 Vimium

在浏览网页的时候,如果一直用鼠标,要么压在桌子上右手腕外侧即尺骨一侧会疼痛,要么为了减轻手上的压力而端肩导致脖子、后背、肩膀疼。你可能也是这样。我还有个单独的问题,由于视力不好,离远了看不清,离近了鼠标经常会超出视野,就找不到了。

因于以上原因及其他理由,在使用word时,我常常全屏 alt- v u,多数操作通过快捷键完成;在使用IDE编程时,更多使用快捷键,除了画GUI的时候。有一种说法,适合解释我的习惯——编程时使用键盘,而不是鼠标。

但是浏览网页呢?不仅需要上翻页、下翻页,还要*点击*链接跳转,还要搜索。有时候可能还要 前一页,后一页,比如在论坛里,或者分成章节页面不连续的电子书。

Firefox有个插件 Vimium可以满足这些需求。又名 Vimium-FF,其中FF是 Firefox 的意思,指从 Chrome迁移而来。Vi,就是那个著名的vi,Unix下的全屏文本编辑器。

它还有个中文fork,如前一页、后一页,针对中文网站做了适配。

我常用以下几个功能。

一个是链接跳转。

例如在下面的页面中,准备点击链接。

不必使用鼠标,甚至比用鼠标还好,因为不必知道哪些图片或下划线文字或颜色不同的文字是链接。

按 f 键(代表find),会显示如下图所示,其中黄色上有两个字母的标签,是链接,字母用以区别各个链接。

想打开哪个链接,输入上面的字母,跳到新页面,与鼠标点击的效果一样。如“WR”代表“赞同了该文章”,在下图中箭头所指的方框中。

我重定义了按键,只使用键盘中左手的按键,右手可以用鼠标,或者端 咖啡。

在option里,

设置 link hints。

另一个我常用的功能,是 前一页/前一章、下一页/下一章。我重定义了按键,在有些站点可以通过 左箭头、右箭头 实现 上一章、下一章。

以下是 Viumium-C的定义,是不是本地化得不错?

有的网站这组按键无效,原因不明,我就仍使用 f键。

我还按在word中的使用习惯定义了一批导航用的按键,根据名字你容易猜出用途。也在option里,可以找到下图的条目。

用 t 开新tab。

用 x 关闭当前页面,比用 ctrl-w 略微好一点。ctrl-w有两个版本,各有一点麻烦。一个版本是右手按ctrl,左手按w,需要两只手;另一个版本是左小指按ctrl,左中指或无名指按w,略微有点扭曲手腕。

X 打开刚刚关闭的页面,比右键 reopen 能快半秒。这半秒的提速不见得带来多少愉悦,但是半秒卡顿可是令人非常不爽。

搜索可以用 /,不过firefox原带的 ctrl-f也不错。

使用键盘,通过力反馈和位置/本体感受器,大大降力了对视力的要求和负担,而且更加确切。我喜欢做 键盘 侠,不愿意 指点 江山。

 

好工具 | 做笔记 保存网页 SingleFile

古人还是谁说的,不动笔墨不读书。阅读只要不是纯娱乐享受的,都需要做笔记。纸质的,写在书边,比如费马,写在四折A4纸上,比如我。电子的,批注标记以后导出留存。

网页的资料,虽然碎片,也得保存,不然过了一阵就忘光了,跟没读过一样。吵架的时候,想拿出证据,会发现不仅你忘光了,连互联网上也没有痕迹。不复复制文字的话,可以保存成或打印成pdf,也可以存到云端。存成云端这事不保准,也可能不定哪天文档就消失了。“我的云盘”,你真的以为那是你的?好评的云端笔记也不少,但是用的时候总是心怀惴惴,即使网站说,只要你不共享我们绝不删除。然而,网站会不会被删除还不一定呢,更何况还有网站决定这个笔记业务不再搞了。

为什么我到现在还没有选择MD做笔记,这么不时髦。因为笔记是生产力工具,对稳定的要求非常高。记笔记的时间早于某种新技术/新网站,也很可能会持续得比这种技术或网站更长久。每当这时候,我就想起导师教育我的,“你觉得是硬盘值钱呢,还是数据值钱?”

所以网页做笔记也必须保存在本地。手中有粮,心中不慌。

保存或打印成PDF是个好办法,只是有时候排版会乱。相信用过的人都见过,在此不展示了。额外提一嘴,PDF最上面的链接,说从哪哪儿保存来的,是可以点击的,帮助找原始出处。

网页做笔记,截图也很行,不过有信息丢失,链接就没了,文字也不再能选择。

网页做笔记,目前我见到的最好方法,是可以保存成单个网页文件。不是浏览器的另存为,那会生成个文件夹,万一不小心容易和文件分离。

Firefox有个插件 SingleFile,可以把网页保存在单个网页文件。

可以单纯就是保存,右键,如下图。

可以只保存选择的内容。

在保存以前,可以编辑,删除一些不要的内容,比如广告、非常深的网站导航、无关推荐等等。

删除那段以后如下图所示。

添加注释文字,高亮文字。

保存后得到的格式是html文件。

html文件源代码中最前面有笔记的来源和时间。

也许你好奇,在单个html网页中是如何保存图片的,如上图所示,base64编码。原理在这里https://blog.csdn.net/younggift/article/details/42365707 网页内联图片 html inline image.

IEEE754 浮点数计算器 C语言版

1. 问题和别人的工作

IEEE754是浮点数在计算机中存储的技术规范。在学习手动计算 科学计数法/小数形式 与 二进制/十六进制 相互转换的过程中,我们可以使用IEEE754浮点数计算器帮助检验计算结果。

网上不少有在线的IEEE754浮点数计算器,都能符合要求1.你给出小数形式,计算器算出二进制;2.你给出二进制,计算机器算出小数形式。

比如

https://www.h-schmidt.net/FloatConverter/IEEE754.html

界面长这样。

再如

http://www.binary-calculator.com/

界面长这样。

如果没有网怎么办?这里给出一个C语言版的。

2. hex2float,我给出二进制,计算器求浮点数

偷了两个懒。第一,既然二进制和十六进制转换非常简单,是程序员的基本功,因此就不实现了。我给出的不是二进制格式,而是十六进制,如0x12345678。以下,不对二进制和十六进制作区分。第二,不从控制台或者命令行参数,而是硬编码,在代码中给变量赋值。这两点都不是技术难点。

代码如下。

#include <cstdio>

#include <cstdlib>

int main()

{

float f = 1;

*(unsigned int*)&f = 0x12345678;

printf("%x\n",*(unsigned int*)&f);

printf("%e\n",f);

return(0);

}

运行的结果是这样的:

>hex2float.exe

0x12345678

5.690457e-28

即,你手动把二进制0x12345678转换成浮点数,如果结果是5.690457e-28,那么就做对了。

涉及到的核心技巧,*(unsigned int*)&f = 0x12345678; 是把 float 型变量f 取地址得到指针,再转换指针基类型为(unsigned int*),最后去地址引用再赋值。

交叉检验一下。

二进制0x12345678对应的IEEE754浮点数在
https://www.h-schmidt.net/FloatConverter/IEEE754.html
可以得到,如下图所示。是5.690457e-28附近没错。

如果需要计算别的二进制,把代码中的0x12345678改成想求的数就行了。

3. float2hex,我给出浮点数,计算器求二进制

代码如下。

#include <cstdio>

#include <cstdlib>

int main()

{

float f = 5.6904566139e-28; //0x12345678

int a = *(unsigned int*)&f;

//a = 0x12345678;

printf("%x %x %x %x\n",a%0x100, a/0x100%0x100, a/0x10000%0x100, a/0x1000000);

FILE *fp;

fp = fopen( "file.bin" , "w" );

fwrite(&f, sizeof(char) , 4, fp );

fclose(fp);

return(0);

}

核心技巧是 int a = *(unsigned int*)&f; 这一行,把等号右边的(float型变量通过指针得到的)整型变量的值取出来。下一行,是标准的计算机等级考试二级题目思路,通过取整和求余操作,得到整数切成十六进制每2位一段。

运行结果如下。

>float2hex

78 56 34 12

为什么这么“颠倒”排列显示呢?明明代码里是先低权重后高权重的。我们可以把这4个字节作为4个char写到磁盘文件中对比一下。

用十六进制工具查看file.bin的内容,如下。

也是颠倒存放的。输出到控制台的写法,是为了与内存中的存放次序,也即磁盘文件的存放次序保持一致。之所以在内存中顺序如此,是因为我的计算机是Intel系列CPU,小端模式。为什么向控制台输出看起来像大端,所以还需要特意颠倒输出顺序呢?因为那是整数,不是内存映射,低字节 vs. 高字节,而是 低权重 vs. 高权重,且与地址无关。

IEEE754并不遥远,一点也不陌生,就在常见的C语言的代码之中,一直就在我们身边,只是我们不一定看得到。

改出一个 豆瓣好友列表导出工具

改出一个 豆瓣好友列表导出工具

豆瓣网络资源菊 提到 望晴 提到 “有没有批量导出微信好友通讯录的方法或小工具?”

如下,支持 导出 [头像链接、人名、此人主页的链接、签名 到excel/csv]文件。

https://pic4.zhimg.com/v2-9163f26e3b4e63c0843202aea3d591ef_r.jpg

需要在浏览器安装 Tampermonkey 插件,

然后安装下述脚本。

访问豆瓣 我的关注 或 我的豆瓣|我的关注,然后 成员,会增加一个链接“导出好友”。点击它。

// ==UserScript==

// @name 豆瓣好友列表导出工具

// @namespace https://younggift.net

// @version 0.1

// @description 将豆瓣好友导出到文件。启用本脚本,进入豆瓣个人页面后,在『我的关注|成员』里会有一链接『导出』,点击即可。

// @author younggift

// @copyright 2022, younggift; 2018, KiseXu (https://kisexu.com)

// @license MIT

// @match https://www.douban.com/contacts/list*

// @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 export_link = 'https://www.douban.com/contacts/list?tag=0&start=0&export=1'; //列表URL 及'&export=1'标记

$('#db-timeline-hd ul li.last').after('<li><a href="'+export_link+'">导出好友</a></li>') //根据css定位

}

if (location.href.indexOf('//www.douban.com/') > -1 && location.href.indexOf('export=1') > -1) {

// 开始导出

getPage();

}

// 获取当前页数据

function getCurrentPageList() {

var items = [];

$('li.clearfix').each(function(index) {

items[index] = {

icon:$(this).find('img.face').attr("src").trim(),

name:$(this).find('h3').find('a').text().trim(),

link:$(this).find('h3').find('a').attr("href").trim(),

motd:$(this).find('span.signature').text().trim(),

// 学习了 https://www.w3school.com.cn/jquery/jquery_ref_traversing.asp

};

// 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, icon, name, link, motd`

});

var items = getCurrentPageList();

db.items.bulkAdd(items).then (function(){

console.log('保存成功');

// 获取下一页链接

var next_link = $('span.next a').attr('href');

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, icon, name, link, motd`

});

db.items.orderBy('id').toArray().then(function(all){

all = all.map(function(item,index,array){

delete item.id;

return item;

})

JSonToCSV.setDataConver({

data: all,

fileName: 'contacts_list',

columns: {

title: ['头像', '人名', '链接','签名'],

key: ['icon', 'name', 'link', 'motd']

}

});

db.delete();

});

}

//以下younggift未修改

// 导出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;

}

};

})();

 

牛顿法解方程-使用浏览器(Geogebra可选)做实验

提起牛顿法解方程,大一以上的同学都听说过。但是如果要求做实验写代码,不少同学可能会心生畏惧,会不会很难很麻烦。事实上,核心代码只有一行而已。本文演示的实验只需要浏览器,可选Geogebra。

1.计算机问题求解的三类方法

裘宗燕老师把计算机问题求解的方法归结为三类,https://zhuanlan.zhihu.com/p/58943925

。例如就解二元一次方程

a * x * x + b * x - c = 0

而言,求解的方法可以从以下角度分为三类。

第一类方法是公式,

x1=(-b +(b^2-4*a*c)^1/2)/ (2*a)
x1=(-b -(b^2-4*a*c)^1/2)/ (2*a)

(在解存在的情况下,)我们把a,b,c代入公式就能求出方程的根。这种方法适用于 我们对于方程的规律全部都了解。对于计算机学生而言,这个公式相当于领域知识或者先验知识。

如果我们对方程的全局知识并不了解,比如五次及五次以上的方程已证明没有求根公式,那么就需要第二类方法,计算机的核心思路——搜索。我们可以想办法穷举/枚举解空间中的所有可能性,把这些x逐一代入方程a * x * x + b * x – c。如果哪个x代入方程以后得到的结果是0,那么当前的x就是一个解。

搜索问题需要考虑以下问题。1.是否可以穷举。有理数集可穷举,实数集不可穷举。二次方程有些解并非有理数,所以严格地说,第二类方法搜索不能得到方程的根。不过,在工程和技术领域,我们可以不那么严格,并不一定得到精确的实数解,而是可以允许在一定误差内(比如,方程代入x以后求得的值与0间的差的绝对值)的就算命中。2.如何穷举,按什么顺序枚举。从小大到?3.如何以更快的速度穷举。二分查找?

第二类方法搜索,需要了解问题的局部规律。方法的应用正是对局数规律在全局上的推广。

第三类方法甚至连局数规律也不必知道,只需要了解个别(也许很多)问题的实例和解的实例。你可能已经知道了,接下来是采用统计学的或神经网络或深度学习或机器学习的手段,把个例推广到一般(并且在一定范围内检验)。

牛顿法是第二类搜索方法,基于……一系列原理,简而言之,对牛顿的信仰,搜索速度飞快,可以采用。

2. 牛顿法 算法步骤的几何解释

假设我们要解的方程是y=(fx),要求误差小于0.01。

抛物线在平面直角坐标系里是这样的。

image2假设我们初始猜测的值是10。

此时误差不小于0.0.1。

过 (10, f(x))点做切线,红色的。

image3

以 切线与横轴交点 作为新的x1,过 (x1, f(x1))点做切线,绿色的。

image4

此时误差不小于0.0.1。

放大一下,是这样的。

以 切线与横轴交点 作为新的x2,过 (x2, f(x2))点做切线,蓝色的。

此时误差不小于0.0.1。

放大一下,是这样的。

image5

以 切线与横轴交点 作为新的x2,过 (x2, f(x2))点做切线,棕色的。

此时误差不小于0.0.1。

放大一下,是这样的。

image6

以 切线与横轴交点 作为新的x2,过 (x2, f(x2))点做切线,青色的。看起来比青色暗一些,是因为在这个分辨下与原方程的黑线重叠了。

此时误差不小于0.0.1。

放大一下,是这样的。

image7

青色的线与方程黑色的线几乎重叠,放大一下。

黑色的线是方程,青色的线是最后一次的切线。

此时的误差为F1E1=0.0014,小于0.01。搜索到了符合要求的解。

image8

总结,每个步骤都有一些共同的特征。

步骤1 猜测x_k,得到(x_k, f(x_k));

步骤2 过点(x_k, f(x_k))做方程的切线,交于(x_k+1, 0),这样得到x_k+1;

步骤3 检测f(x_k+1) 与 f(0) 间的差是否符合要求。

 

缩小看全局,可以看到 从T1(10, f(x))开始,切线 红-绿-蓝-棕-青 的倾斜过程,以及切线与横轴的交点迅速逼近方程的根。

image9

3. 递归公式推导

 

因为每个步骤有共同特征,因此可以抽象为递归或迭代过程。

我们根据几何解释求出递归公式。

图示为几何解释,其中(x_k, f(x_k))的高度为 f(x_k)。

image10

我们根据导数定义得到公式,其中f(x)和f'(x)根据方程由程序员手动求出。

公式在此!

image11

如果不会求导,可以借助符号运算工具,比如 Mathmatica,或者https://www.wolframalpha.com/。如果像我一样访问不到,再如 https://www.derivative-calculator.net/https://zh.numberempire.com/derivativecalculator.php

image12

或者

image13

3. JavaScript代码

为什么想起来用 JavaScrtip实现呢。因为正在重读 SICP(计算机程序的构造和解释,JavasScrtip版本),发现这个开发环境真是友好便利。本文即重现SICP中的例题,再次推荐SICP。

在浏览器里按F12,然后Ctrl+o(相信用菜单打开文件也一样)打开 newton.js 文件。还可以不编译调试,随手改随手运行。

代码很短,核心部分只有一行。以下分段解释代码。

// In Geogebra
// f: y=2 x^(2)+3 x-4
// NSolve(0=2 x^(2)+3 x-4)
// = {x = -2.35, x = 0.85}
a = 2;
b = 3;
c = -4;
epsilon = 0.01;

以上,要解的方程是y=2 x^(2)+3 x-4。为增加通用性(和趣味性),a,b,c三个参数可以修改。Epsilon是误差的最大值。

function f(x)
{
    return a*x*x+b*x+c;
}
function df(x) // derived function
{
    return 2*a*x+b;
}

方程,以及方程的切线。其中f(x)是方程,df(x)是方程的切线/导数。

以下就是核心部分了。

function guess(x)
{
    return Math.abs(f(x)) <= epsilon ? x : guess( x - f(x)/df(x));
}

这一行就是核心

Math.abs(f(x)) <= epsilon ? x : guess( x - f(x)/df(x) )

如果误差小于预期,那么当前的x就是方程的根;

如果误差大于预期,那么继续猜下去,猜测的x_k+1<= x_k – f(x)/df(x),即上面 递归公式推导中的“公式在此!” 手写公式中的f'(x)就是代码中的df(x)。

guess(0); //0.8517236753856471
guess (-1); //-2.350915564067371

这两行是测试运行,分别由左边和右边逼近得到两个不同的根。

4.验证

求解的根代入方程,可以确认f(x)误差小于预期。

也可以通过Geogebra算一下。在左边输入,求出方程的近似解。

image14

4.递归有多快

程序员会思前想后很多因素,也许在某个具体的问题中根本用不上,但是万一墨菲法则发作了呢。比如,牛顿法收敛有多快,递归需要多少次,会不会暴栈。

道理上,收敛非常快,非常~是多快。我们看一下感性认识。修改上述js代码,把guess改成下面这样。

function guess(x)
{  
    console.debug(x);
    return Math.abs(f(x)) <= epsilon ? x : guess( x - f(x)/df(x) 
);
}

加了一行调试,把浏览器按以后F12的debug设置为允许显示。

image15

猜测一下,以10作为初始值,guess执行6次。

image16

如果猜测得糟糕一些呢,比如10000?guess执行16次。

image17猜测的初始值为原来的1000倍,guess增加了10次。

收敛有多快呢,我看把x系列放到excel里看一下。

无标题

猜得再离谱一些,1000000000。33次。33次这种量级,对于计算机而言,就是瞬间。

33

而且我们观察上面两图可以看出,最初的收敛非常陡峭,也就是说,极快地逼近目标值。如果容忍的误差大一些,会有更好地表现。

当然,牛顿法有适用范围,或者说有不适用的范围。然后,没有一种工程方法是完美的,只有不完美的工程师把某种工程方法应用在不适合的问题上。所以,牛顿法(以及Geogebra,SICP,JavaScriipt,Excel等等)真是强大啊。

微信图片_20220814182439

抽象与权衡:用appium打卡百词斩和扇贝

用appium做个了小工具,用来每天打卡百词斩和扇贝。本贴总结一下其中一些设计的动机,特别是抽象和权衡。

1. 需求

手机上有 百词斩 和 扇贝单词 两个app,要求在PC上运行appium,每天完成在这两个APP中的打卡。

v2-3f1b54f6b4d3cd50e78133ae8a8de425_r

百词斩和扇贝分别需要这样几个步骤,包括 点击打卡、分享到特定的几个微信群、领取因分享带来的铜板之类的奖励。接下来,百词斩要进入小班里领"铜板",扇贝要进入听力计划中领"贝壳"。铜板和贝壳分别是对应APP中的"货币",攒着主要是习惯,其次可以在万一中断了的时候购买"补打卡"。

v2-ac7ccebf71b33c839d5ac2278def17d2_rv2-309dd1f6227d6356af91bb3481246500_r

2. 技术原型

THX和YP同学已经分别做过充分尝试,珠玉在前,在此不做赘述。参见
THX [https://www.cnblogs.com/ourshiningdays/p/16023291.html]
YP,系列博客 [Appium实现百词斩 - 萍2樱释 - 博客园](https://www.cnblogs.com/ping2yingshi/p/16206816.html)
微信图片_20220626134608
3. 抽象 与 权衡

能容易出来,PC需要向手机发出七八个或者更多的请求,点击控件,或者通过输入微信群的名字搜索,这一类的操作。如果抽象出这些动作的共性,就可以把动作视为代码,写个引擎跑这些代码。这一类抽象的思路参见《黑客与画家》,作者提到程序有三类,大意是,一种是手写业务逻辑;第二种是编译器或解释器,即引擎,跑代码,即数据;第三种是LISP。这里我实现的抽象就是第二种。

3.1 动作类别

打卡中的动作有哪些共性呢?在 Appium Inspector 中可以看到,可做的动作包括 tap, send keys,查找控件/元素可以根据 xpath, 各种 id 等。事实上,这并非*打卡*特有的动作,而是PC可以向手机发送的所有动作。考虑到打卡并无太多的特性,因此经过权衡,姑且认为 (打卡与一般的动作) 相同。

Appium Inspector 操作

这样,动作的类别包括: (1) tap, (2) sendkeys。前者用于点击按钮,后者用于向文本框发送文字。

PC与手机通信是异步非阻塞的,确保同步的最简单方便是 等待。考虑到在打卡的动作中,手机对不同步骤操作的响应时间有长有短,所以不同的步骤需要的等待时间可能不同,因此再加一种类型, (3) sleep,也算一个动作类别。

可能需要取得控件上的文字,作为下一步或进一步判断的依据。比如打卡多少天了,比如是不是达到某个条件。这个动作类别是(4)getText,在当前版本中没有实现,作为保留了。

某个特定动作是否可以执行,可能需要前置条件,例如只有当今天的单词全都背完了,才可以打卡。背完单词就是打卡的前置条件,在界面中通过检测到"打卡"而不是"开始复习吧!"来确认。这个动作类别是 (5)precondition。

在实现和测试的过程中考虑到,如果前置条件未满足,即"打卡"未出现,打卡程序应该如何呢,弹出个对话框提示我自己?不如抛异常崩掉实现起来更方便,而效果是相同的。给自己用的小工具与给别人用的程序的重要区别就在这里,友好的提示统统不必,前置条件统统可以认为满足了。要知道,所有这些都需要花时间才能写出来,如果效益不大,那就偷懒好了。

在设计的过程中,我只考虑到以上可能。其中的 precondition 可能用不到,只是隐约的感觉,还不清晰。

在代码过程中,发现自己漏掉了两种动作。 (6)back,退后一步,是个常用动作,相当于语法糖,不必获取和点击回到上一步按钮; (7)tapFirst,点击符合条件的第一个控件/元素。在搜索微信群这一类操作中,还有领铜板和贝壳界面中,手机APP的实现是类似数组中的多个元素,运行时动态生成的。所以搜索的时候会有多个匹配的,在当前问题中,只需要点击第一个。

enum ActionType
{
tap,
sendKeys,
precondition,
getText, //reserved
sleep,
back,
tapFirst, //只支持id,不支持xpath
}

3.2 元素/控件 类别

如何查找到要点击的元素/控件呢?支持了根据 (1) xpath和根据 (2) id查找。

类似 sleep 这种类型的动作,是不需要元素和控件的,此时 元素/控件 类别就是 (3) nop。

enum ElementType
{
xpath,
id,
nop; //不做任何动作
}

不支持按坐标点击或swipe,如果堕落到那种程度,就不如使用 AHK (鼠标键盘脚本) + Anlink (在PC上操作和显示手机)完成了,所占磁盘空间比 appium + appium client + android库 + jvm 要小得多。

3.3 动作

这样,动作的共性就包括 哪一种动作 (动作类型)、如何定位到控件 (元素类型)、控件地址 (无论xpath还是id,用字符串;同时复用这个属性,如果动作类型是sleep,那么代表单位为毫秒的duration)。

属性枚举型变量WhenNotFound的值有三种可能,abort,retry,ignore,分别代表当未找到控件/元素时的后续动作,是终止程序,再试一次,还是做动作序列中的下一个动作。

public class Act {

public ActionType action; //动作类型
public ElementType element; //元素类型
public String control; //控件地址

public String text;
//在procondition时作为判断条件,
//在sleep时是整数,单位毫秒,
//在其余情况下作为注释

public WhenNotFound notfound;

构造函数这样重载,在没有WhenNotFound参数的构造函数中为WhenNotFound指定了默认值abort。这也代表了小工具的典型思路,如果条件不符合要求,就终止运行,由我手动设置环境条件达到满足要求再跑。

public Act (ActionType a, ElementType e, String c, String t,    WhenNotFound f){
...
public Act (ActionType a, ElementType e, String c, String t){

3.4 动作序列

若干动作的"组合",以及对这一组合中动作的遍历,就构成了整个打卡过程。需要支持 动作之间的哪些关系呢?

首先想到的是 符合条件,一切满足要求,那么从头到尾执行一遍就行了。这是顺序执行。

其次想到,如果存在不符合条件的情况呢?例如,因为单词没背完或者听力时间不足,因此不能打卡。例如,APP今天多了一条广告,因此在特定步骤要找的控件/元素不存在,需要关了广告才行。例如,手机响应慢,sleep时间不足,需要再等3秒才能到下一动作期待的界面。例如,由于不明原因,appium java client在第一次搜索某个控件/元素的时候找不到(THX同学确认在python中没有类似现象),第二次搜索就能找到了,而且并非在第一次搜索以前sleep一会儿能够解决。这是要支持选择执行。

再次,要不要支持循环呢?向3个微信群发送打卡图片,用1组动作 (比如4个动作) 的3次循环,而不是展开成12个动作,这样好不好?这是要支持循环持行,也可以通过选择执行来实现。

每次APP发生变化时,改动以上组合中的动作,也可能需要修改 动作间的组合,引擎不应修改。

这里的其次和再次,都是对首先的更深层次的抽象。但是要考虑到,并非越抽象越好。我选择在当前版本只支持顺序执行,平坦的自前向后的执行。如果有必要,后续版本再做更深的抽象。先跑起来再说,最好是更好的敌人。

把动作放在了一个数组中,并且计划支持自前向后遍历,因此称为 动作序列。

// 初始化动作序列
Act actseq[]={
new Act (ActionType.sleep, ElementType.nop, "", "5000"),
// ---- 打卡
// new Act (ActionType.precondition, ElementType.id, "com.shanbay.listen:id/progress_bar", "41.0"),
new Act (ActionType.tap, ElementType.id, "com.shanbay.listen:id/check_status", "去打卡|已打卡"),
//  ---- 微信分享
new Act (ActionType.tap, ElementType.xpath, "//*[@text='微信好友']", "微信好友"),
new Act (ActionType.tap, ElementType.id , "com.tencent.mm:id/ebr", "搜索"),
new Act (ActionType.sendKeys, ElementType.id , "com.tencent.mm:id/cd6", "玫瑰花园"),//群
new Act (ActionType.tapFirst, ElementType.id , "com.tencent.mm:id/kpx", "玫瑰花园"),
new Act (ActionType.tap, ElementType.id , "com.tencent.mm:id/gv3", "分享"),
new Act (ActionType.tap, ElementType.id , "com.tencent.mm:id/gup", "返回扇贝听力"),
// //----------------------
new Act (ActionType.tap, ElementType.xpath, "//*[@text='微信好友']", "微信好友"),
new Act (ActionType.tap, ElementType.id , "com.tencent.mm:id/ebr", "搜索"),
new Act (ActionType.sendKeys, ElementType.id , "com.tencent.mm:id/cd6", "喵"),//群
new Act (ActionType.tapFirst, ElementType.id , "com.tencent.mm:id/kpx", "喵"),
new Act (ActionType.tap, ElementType.id , "com.tencent.mm:id/gv3", "分享"),
new Act (ActionType.tap, ElementType.id , "com.tencent.mm:id/gup", "返回扇贝听力"),
// //----------------------
new Act (ActionType.back, ElementType.nop , "", "back"),
// ---- 听力计划
new Act (ActionType.tap, ElementType.id , "com.shanbay.listen:id/mine", "我的"),
new Act (ActionType.tap, ElementType.xpath, "//*[@text='听力计划']", "听力计划"),
new Act (ActionType.sleep, ElementType.nop, "", "3000"),
new Act (ActionType.getText, ElementType.id, "com.shanbay.listen:id/plan_day", " / 天 听力计划"),
};

以上是扇贝的动作序列,百词斩的与此结构相同,差异只在动作序列中的每个元素不同。

3.5 引擎

引擎的作用是遍历动作的组合,在遍历到每个动作时执行这个动作。既然只支持顺序执行,那么就是从前向后依次访问。

引擎的触发是这样的。

//跑动作序列,loop-遍历
ci.runActionSeq (actseq);

引擎的作用是 (1)遍历,即 for (Act act : q), (2)执行序列中的当前动作,即根据动作类型 (以及参数)操作APP,即

switch (动作类型)
{   case 特定类型1:
操作APP;
break;
case 特定类型1:
操作APP;
break;
...
}

这是个标准的路子,类型于CPU执行指令。所不同者,这里因为只有序列,不支持跳转,所以没有修改下一动作下标的必要。为什么不支持跳转,因为假设所有环境条件都满足打卡要求,由我手动操作,比写代码的利益更高。这可以根据代码将 执行的次数*每次执行的时间 - 节省的手动操作时间 估算出来。

private void runActionSeq (Act q[])  throws Exception
{
AndroidDriver d = this.driver;
org.openqa.selenium.WebElement e = null;
for (Act act : q)
{
switch (act.action)
{
case tap:
e.click();
break;
case sendKeys:
e.sendKeys ( act.text);
break;
case precondition:
assert e.getText().equals (act.text);
break;
case getText:
//assert false;
System.out.println ( "==================" + e.getText() + " " + act.text + "====================");
break;
case sleep:
Thread.sleep( Integer.parseInt(act.text) );
break;
case back:
d.pressKey(new io.appium.java_client.android.nativekey.KeyEvent(io.appium.java_client.android.nativekey.AndroidKey.BACK));
break;
case  tapFirst:
java.util.List<org.openqa.selenium.WebElement> L = d.findElements(By.id(act.control));
e = L.get(0); //index从0开始
e.click();
break;
}
//默认每个动作后sleep 几秒
Thread.sleep( 2000 );
}
System.out.println ("-AFTER finally-------------------------------------------");
}
}

无论百词斩还是扇贝,以上引擎是完全相同的,二者的差别只在动作序列不同。不过在开发中,我又偷懒,没有把引擎单独做成class文件,编译和部署了两次。

这个版本的引擎,对调用者暴露了内部结构。更深层抽象的一个角度是更好的封装。可以提供一个 动作序列类,而不是 动作 (序列)数组。动作序列类提供一个方法,next或者execute_current_action,调用者在循环中不再遍历数组的元素,而是不断 next并执行当前动作,直至halt动作 (或者容器的迭代器到达 end) 。

3.6 复用

我只打卡两个APP,写第一个打卡工具的时候,连引擎带动作序列都需要写,花了较多时间。第二个打卡工具,只需要变更动作序列 (以及连接参数之类的),只几分钟就完成了。

如果有更多打卡工具要写,复用的利益可以更明显。

对于DIY这个原则,有人提到第二次遇到相同的代码就应该抽象 (成函数或者类) ,按复用的要求去写。我的耐受力更强一些,或者说更能忍,一般第三第四次遇到,才会动手抽象。并且抽象的深度要控制在这样的程度--在可预见的未来 (包括扩展和维护等) ,(节省时间的) 利益大于 (设计和写代码以及因为抽象带来的调试麻烦的)支出。

抽象/复用带来的一个直接副作用是对每个操作个体的单独处理的灵活性降低了,而只能顾及到共性。如果在共性的遍历中加上些if来强调个性,那么很快就会充满if,不如不抽象展开循环的好。

事实上,我在跑了一两个月以后,才遇到广告。后来又遇到当前单词书背完了,界面又有变化。这种小概率事件,没有必要总结,没有必要为它们变更代码做出应对。不值得。想起《额尔古纳河右岸》里,部落每到遇到什么苦难,当前做的事情就再也不做了,也不知道是为了避免悲剧再次发生,还是为了纪念。这会导致后来做什么都缚手缚脚,正月不能剪头,过年不能说不吉利的话,蒜要叫义和菜,醋要叫做忌讳,民部要改名叫户部,写代码前不能抠脚趾头,读博客前不能喝水?

微信图片_20220626134619

4. 其他讨论

还有些与工程技术有关,而与抽象/模型无关的。

4.1 环境安装,不用maven

我用appium依赖的是 java client,又不想用 maven 把挺大的库都装上,所以费了些时间。不想用maven的原因,是当时头脑不太清楚,但是基于的原则是对的。我当时误以为写完代码要部署给别人,那么运行环境与开发环境不同,不应该要求用户也装个maven。所以我花了些时间跑 appium java client,缺哪个依赖jar或class,就去找。

需要以下依赖,1.8 M。

slf4j.api-1.6.1.jar
java-client-8.0.0.jar
commons-lang3-3.12.0.jar
logback-core-1.2.11.jar
logback-classic-1.2.11.jar

以下是appium需要的,677 MB。

selenium\
appium-inspector\
appium\

还有以下是连接我的手机需要的,15.2 GB。这么大,所以,感觉节省的时间和
空间都不值得。装上maven也不会更大多少。

android-sdk-windows\

4.2 缺点

占用空间太大, 如前所述,不仅jar,class,还有android。感觉还不如用 AHK (图像搜索) + Anlink 来得轻量级(这个方案的缺点是1.对屏幕分辨率有依赖,2.语法我不如java熟悉)。就打个卡而已,16GB,太兴师动众了。

速度慢。网上有讨论,难以解决。而且速度不稳定,有时候快,有时候慢,即使环境条件毫无变化。确定的是,不用USB而用wifi连接手机,速度总是慢得不可容忍。

变更也不方便。一旦有变化,需要重新编译。在调试期间,我为此写了个bat,专门用于编译和运行。本着使用 发布环境/运行环境,而不是 开发环境 的原则,我没在IDE中开发。

编译:

set JAVA_TOOL_OPTIONS=-Duser.language=en

javac -cp %classpath%;c:\tools\selenium\*;c:\tools\appium_java_client\*;c:\tools\selenium\lib\* Act.java

javac -cp%classpath%;c:\tools\selenium\*;c:\tools\appium_java_client\*;c:\tools\selenium\lib\*CheckinBaicizhan.java

运行:

set JAVA_TOOL_OPTIONS=-Duser.language=en

java -cp %classpath%;c:\tools\selenium\*;c:\tools\appium_java_client\*;c:\tools\selenium\lib\* CheckinBaicizhan

adb -s 8KE0219819009788 shell settings put secure default_input_method com.iflytek.inputmethod/.FlyIME

运行中的最后一行,是重置手机输入法为讯飞。appium连接时有参数把手机输入法屏蔽了,为了输入文字。在此置回我常用的环境。

5. 为什么写这个贴子

首先,给学生留的作业,用appium做自动化测试的实验,比如打卡百词斩扇贝、打卡微信读书进度。我自己手痒也做了个打卡百词斩和扇贝的。本着发布是最好的保存这一原则,我也写贴子总结一下。其次,同学们经常提到的一个问题,或者在初学阶段容易陷入的一种状态,就是只有技术原型/可行性实验,然后就代码,编译并发布了。没有设计的过程,也觉得不需要模型,没啥可抽象的--老师,你看,都跑起来好使了,你还想咋样呢。我手痒写的这段代码,对打卡的动作序列稍微做了一些抽象,算是个有设计有模型的粗糙的演示。最后,在工程中,我本人也经常抽象上了瘾,停不下来,陷入到过度工程中,眼看着代码越来越抽象,越来越难维护和调试。什么时候停下抽象的脚步,什么时候妥协,是个一直需要讨论的问题。对这段小代码的讨论给出了我做妥协时的一些考虑。

同学们参与需求讨论,或者看着别的同学代码形成的过程,读起我的贴子来,可能更容易理解,也更容易挑出问题来。这也是为什么写作这一篇的一个原因。

成熟程序员读完上述贴子的感觉可能是:就这?出于对于教学的需要,简单的例子比大点的系统更容易接受,大系统里的宝藏过多,会让初学者淹灭在细节 (和各种难点) 里。而且,麻雀虽小五脏俱全,如果不能为初学者所用的技术,初学者就会感觉以后再掌握也不迟。最后,一个优秀的原则,难道不应该适用于非常广泛的领域么?按我们的所相信的生产,按我们所相信的生活,按我们所说地去做,按我们所做的去说。

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

代码不贴了,不止一个文件,也不太短,而且核心部分已经在正文中给出了。如果有感兴趣的同学,向我单独要吧,或者我发到 github 吧。

微信图片_20220626134622

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