小名开开

在春天的光影里喘息

最近 AlphaGo 与李世石的人机围棋大赛很火,也蹦出一大波跳梁小丑。你会发现什么热点事件都少不了这种人,总是挡在路上污人视线,比如那个输入法王。我记得上次 SoloLens 它也跳出来大谈 Matrix 来着。

牢骚完毕,这次深夜短篇不是写围棋也不是写计算机的,只说数数,更严格的说,只是数『0』。比如:

围棋棋盘共有 19×19 个点,每个点有『无子』、『黑子』、『白子』三种可能性。不考虑无气提子等特殊情况,则一共有 3^361 种盘面。如果考虑到下子进程,则有 361! 种棋谱。

因为有围棋规则在,实际数字会小一些,但这不是本文重点。这两个数字分别是一个 173 位数和一个 769 位数:

1
2
3
3^361=17408965065903192790718823807056436794660272495026354119482811870680105167618464984116279288988714938612096988816320780613754987181355093129514803369660572893075468180597603

361!=1437923258884890654832362511499863354754907538644755876127282765299227795534389618856841908003141196071413794434890585968383968233304321607713808837056557879669192486182709780035899021100579450107333050792627771722750412268086775281368850575265418120435021506234663026434426736326270927646433025577722695595343233942204301825548143785112222186834487969871267194205609533306413935710635197200721473378733826980308535104317420365367377988721756551345004129106165050615449626558110282424142840662705458556231015637528928999248573883166476871652120015362189137337137682618614562954409007743375894907714439917299937133680728459000034496420337066440853337001284286412654394495050773954560000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

呼应本文标题的问题来了——这两个数字末尾各有多少个『0』?

你当然可以靠眼力劲儿去数,前者容易看出末尾没有 0,后者大概费些功夫能数出有 88 个 0。当然你也可以使用查找替换,数起来方便些。

但是如果没有事先把总数算出来,又如何数出一个数的末尾有多少个 0 呢?毕竟连乘 300 多次算个 700 多位的数字恐怕是更累的一件事情。能少算就少算点吧。

办法是有的,数质因素 5 就可以了。

要注意到,无论多少个数连乘,只有 2×5 才会在末尾添加一个 0。有人说 8×5 也会出 0,4×25 会出两个 0。但 8×5=40=22x2x5,有两个 2 其实浪费掉了。而 4×25 = (2×5)2,本质上正好是两对 2×5 相乘,完全符合之前的设定。又因为在阶乘中,2 作为质因数的出现次数远远超过质因数 5 出的次数。每个偶数都能至少贡献一个质因素 2,而每个 5 个数字才能贡献一或若干个质因数 5。因此可得结论:

只要数清连乘中一共有多少个质因数 5,就知道最终数字末尾有多少个 0 了。

对于 3^361,显然一个质因数 5 都没有。所以末尾没有 0。

对于 361!,

  • 每隔 5 个数字,可以提供一个 5。即 5, 10, 15, 20, 25, 30, ……, 360。一共 361 / 5 向下取整,有 72 个。
  • 每隔 25 个数字,可以一次提供两个 5。即 25, 50, 75, ……。一共 361 / 25 向下取整,有 14 个。由于上一条已经重复计算一次,因此这里的 14 不必再翻倍了。
  • 每隔 125 个数字,可以一次提供三个 5。即 125, 250。一共 361 / 125 向下取整,有 2 个。由于上一条已经重复计算一次,因此这里的 2 不必再翻倍了。

再上去是 625,直接超了。于是数 5 活动就此停止。

一共有 72+14+2=88,而 2 的数量肯定是足够的,于是会产生 88 组 2×5,即末尾有 88 个『0』。

这个方法可以广泛用于各类无聊的数 0 活动中,例如:

任取一个足够大的数字 N,比如 10100 即 1 googol,把小于 N 的所有素数乘起来,那么多素数的乘积的末尾有几个 0?

——答案是只有一个 0,算对了么?

那么第二个问题来了,为什么数 5 知 0?

答案是,这个和 10 进制有关。

仔细思考,『进制』的本义就是每满多少就 归 0 进位。换句话说,如果 要让末尾为 0,就必须不多不少,最末尾那些余量恰恰等于当前进制。这个 『余量』不是严谨的用词,意会即可。在 10 进制里,你总要正好凑出 10 个或者 10 的倍数个,才能让末尾是 0。在 8 进制里就要凑 8 的倍数个让末尾是 0。16 进制凑 16,N 进制凑 N。

所以归根结底,要让若干个数连乘得到末尾零,就是看能凑出几份当前进制。10 进制就是要凑 2×5,8 进制要凑 2x2x2,每三份质因数 2 会多一位尾 0,只看 2,别的都没用。

既然 10 进制下,10 的唯一质因数分解就是 2×5,而 2 又远多于 5,所以数 5 知 0 就变成了最简单的办法。

理解了这些,下面这些问题也不难回答了:

3361在 3 进制下,末尾有几个 0?

答案是 361 个 0。这个问题等效于 10 进制下 10361末尾有几个 0,等效于 N 进制下 N361末尾有几个 0。——都是361个。

21024在 16 进制下,末尾有几个 0?

答案是 256 个。因为 16 = 24,1024 / 4 = 256。

最后一个问题:

31024在 2 进制下,末尾有几个 0?

HTML 5 提供的 <audio> 给了前端相当多的方便,不再需要 js 库和 swf 作为播放器。但是新带来的坑也是坑得我要死要活的。

这是 W3School 提供的一张简表:

Browser MP3 Wav Ogg
Internet Explorer YES NO NO
Chrome YES YES YES
Firefox YES YES YES
Safari YES YES NO
Opera YES YES YES

信了你就掉坑了。

HTML5 的 <audio> 有着比看起来的大得多的坑。主要在于各浏览器的实现上有区别,自带的音频解码器各有不同,导致实际支持的格式各有不一。

并且,在宣称支持的格式里,各浏览器也只支持其中一部分编码格式。例如:

Firefox、Chrome 可以正常播放 Microsoft PCM, 16 bit, mono 8000 Hz 的wav,但是无法播放 GSM 6.10, mono 8000 Hz 的wav

以及来自于 这篇文章 的:

Playing MP3 files in Firefox is actually OS/hardware dependent. That’s right, instead of using a built in, true native support, we have to rely on outside variables that we have no control over. Kind of defeats the purpose of that whole no extensions, native support thing that HTML5 is good for. Yes, this is to avoid patent issues (and so Mozilla doesn’t have to pay licensing fees). If you are serving to a lucky person below, here’s the real MP3 support for Firefox:

  • Windows 7+ (Firefox 21.0)
  • Windows Vista (Firefox 22.0)
  • Android (Firefox 20.0)
  • Firefox OS (Firefox 15.0)
  • OS X 10.7 (Firefox 22.0)

大意是 Firefox 的 MP3 播放支持其实是依赖于系统和硬件的,这样可以免去专利费。相应的,支持列表就变得有点复杂了。

这里有几个测试页, 可以大致用来测试主流浏览器的音频文件支持情况,但是也不完全。就如上文所述,Chrome 对 wav 文件只能说部分支持。

测试页一:http://hpr.dogphilosophy.net/test/

测试页二:https://html5test.com/

所以在实际测试中,某浏览器播放不了指定音频有两种可能性:

  • 音频格式不支持
  • 音频编码格式不支持

对于相关的编码格式,大致查了一下,列表如下:

OGG

同样后缀名为 .ogg 的音频文件,可能有以下几种编码格式: Vorbis, speex, OggPCM, FLAC,其中,Vorbis 是被建议的。

WAV

.wav 文件通常被理解为原始采样,未经压缩。但其实并不一定如此。最广泛标准的 .wav 文件是指微软 PCM 编码的音频文件(其实也已经编码过了,毕竟从模拟信号转数字信号无论如何都会有一个采样过程。)PCM 细分又可分为定长(16bit、32bit 等)和浮点两类。另外.wav 还可能是 GSM 编码格式,这种格式广泛使用于通讯网络,但在互联网上并不常用,最有可能的来源是电话录音导入电脑。

MP3

MP3 规范并没有详细规定如何编码,但却有细致的解码定义。也就是说『符合这一类规范的才叫 MP3』。因此,MP3 的格式反而才是最一致的,除了采样率、变长/定长和声道数量外,不存在其它变化。

Opus

Opus 是设计为同时包含音频和视频的编码格式。* 可以取代复杂的编码后封装方案。但实际上普及率很低,支持也不好。

AAC

AAC (Advanced Audio Coding)主要是得到了苹果的支持。但除此以外,在互联网的使用并不广泛。

Mozilla 提供了另一张更为详细的表格:(注意这个表还是只能作为参考)

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 3.0 3.5 (1.9.1) 9.0 10.50 3.1
<audio>: PCM in WAVE (Yes) 3.5 (1.9.1) Not supported 10.50 3.1
<audio>: Vorbis in WebM (Yes) 4.0 (2.0) Not supported 10.60 3.1
<audio>: Streaming Vorbis in WebM via MSE ? 36.0 (36.0) ? ? ?
<audio>: Vorbis in Ogg (Yes) 3.5 (1.9.1) Not supported 10.50 Not supported
<audio>: MP3 (Yes) (Yes) 9.0 (Yes) 3.1
<audio>: MP3 in MP4 ? ? ? ? (Yes)
<audio>: AAC in MP4 (Yes) (Yes) 9.0 (Yes) 3.1
<audio>: Opus in Ogg 27.0 15.0 (15.0) ? ? ?
<video>: VP8 and Vorbis in WebM 6.0 4.0 (2.0) 9.0 10.60 3.1
<video>: VP9 and Opus in WebM 29.0 28.0 (28.0) ? (Yes) ?
<video>: Streaming VP9 and Opus/VP8 and Opus in WebM via MSE ? 36.0 (36.0) ? ? ?
<video>: Theora and Vorbis in Ogg (Yes) 3.5 (1.9.1) Not supported 10.50 Not supported
<video>: H.264 and MP3 in MP4 (Yes) (Yes) 9.0 (Yes) (Yes)
<video>: H.264 and AAC in MP4 (Yes) (Yes) 9.0 (Yes) 3.1
any other format Not supported Not supported Not supported Not supported 3.1

所以解决方案大概就是先通过测试确定一两种被绝大部分浏览器支持的音频编码格式,然后服务器后端将音频文件转码为这一到两种适用的音频格式再传递给前端。

看起来最通用的是 MP3?简单地说,是的。但要注意的是不是任意一种 mp3 编码格式都支持,最优化编码的 VBR 支持性就存疑。同时,由于 Firefox 的 MP3 支持是基于系统的,所以假如 Ubuntu 没有安装 ubuntu-restricted-extra 包也可能出现支持性问题。

* 附:

更复杂的是 <video> 标签,由于视频通常是包含声音和图像两部分的,因此实际上视频文件数据是由已经编码的视频数据和已经编码的音频数据,再次进行数据交错编制而成的。因此除 编码格式 外,又衍伸出 封装格式 的概念。深受大家喜爱的 .avi 格式,即 Audio Video Interleave 的简写,就是一种封装格式。当你使用 avi 封装器折开文件后,就会见到一个无声的视频文件和一个无图像的音频文件。至于各自的编码格式又五花八门了。因此对于 <video> 标签的支持更加混乱,几乎必然需要通过服务器转码才能确保在尽可能多的浏览器上前端一致,这也是各视频网站为什么会有大量集群转码处理用户上传的视频的原因。

Update: Ubuntu 16.04 系的解决了这个问题。此文终结。


无标题

这个 Bug 出现在几乎所有版本的 VMWare Workstation、VMWare Fusion 而且按原理来说似乎也会出现在 VirtualBox 上(未验证)。用户选择右键点击喇叭图标手动连接,则可以使用一段时间,Rhythmbox 之类的也可以正常播放声音,但只要打开声音设置或其它类似操作就又会断开。

遇到这种情况,可以先尝试在宿主机接上麦克风/耳麦,随便弄个录音设置,或者把普通耳机插头插进录音孔也行。对的,尽管实际上录不了音,但只要让录音孔插着东西就行。无标题

然后重启虚拟机,如果一切正常该提示不再出现,就继续往下看真·解决办法,如果依然不行,很抱歉你的问题不是这篇文章所能解决的。

这个问题的真正原因是:Ubuntu 默认会检测音频硬件设备,包括音频输入和输出两种设置,但 VMWare 不能正常反馈宿主机声卡的状态。感觉更多的是 Ubuntu 的锅,它没有检测设备存在就直接调用录音设备。

解决办法:

1
sudo apt-get install pavucontrol

安装 pavucontrol 软件包,然后在 Term 中输入 pavucontrol 启动旧版的音量控制:

Ubuntu 64-2016-02-27-20-44-14

在音量控制界面,选择『配置』选项卡,选择『模拟立体声输出』,不要选任何带“输入”的项。然后注销用户再重新进入桌面。

Ubuntu 64-2016-02-27-20-45-50

以上,VMware Ubuntu 就可以正常出声了。

无标题

关于数组公式解释见这里:<Excel 数组公式的概念解释>

补充一个实际应用里出现的公式

1
{=INDEX($A$4:$A$27,MATCH(1,IF(ISERROR(FIND(C4,$A$4:$A$27)),0,1),0))}

作用是在 A 组数据的各项文字中寻找到第一条包含 B 组词汇的数据,如图:

matchindex

原理是这样的,将 find()函数数组化,令其在 A4:A27 中逐个寻找 C4 中的『愿望』两字。当找到匹配时,Find() 函数会返回一个具体数值,即『愿望』两字在被查找的字串中在第几个字符出现;当找不到匹配时,则 Find() 函数会返回 #Value!。在数组化后,返回的数组大致为 { #Value!, #Value!, #Value!, #Value!, 2, #Value!, …, #Value! }。

再通过 if() 和 iserror() 判断这个数组中每个数的有效性,转为 {0, 0, 0, 0, 1, 0, …, 0}。

再通过 Match() 函数将数值 1 的位置找出来,即数组第 5 项。最后再通过 Index() 函数把原歌词中的第五句摘出来,并返回到屏幕上。

通过这个组合数组函数,可以查找某个词在多个单元格中的某一个出现过。

将函数改造成:

1
{=INDEX($A4:$A$27,MATCH(1,IF(ISERROR(FIND(C$4,$A4:$A$27)),0,1),0))}

则通过复制粘贴数组公式,可以知道 C4 这个词分别在哪几句中出现过,而不限于仅仅找出第一句。

Excel 公式,本质就是输入原始数据,处理后再输出结果数据,放在公式的单元格里。

有些公式,输入是一个数据,输出也是一个数据,例如取整 int()、10 底对数 log()。若 A1=5.5,=Int(A1) 显示为 5。

有些公式,输入是一组数据,输出一个数据,例如 Sum。这一组数据整个是一个参数。若 A1:A5={1,2,3,4,5},输入公式 =Sum(A1:A5),显示为 15。A1:A5 数组是 Sum() 的一个参数。

有些公式,输入是一个数据和一组数据,输出一个数据,例如 Match(A2,A1:A5)。两个参数,A2 是待查数据,A1:A5 是被搜索的数组。

而数组公式,则是输入一组数据,输出一组数据

以 Match() 为例,Match 公式的形式为 =Match(lookup_value, lookup_array, [match_type]),其中 第三参数 match_type (查询模式)在本文讨论中忽略。则本文讨论的简化为 =Match(_lookup_value 待查询数值, lookup_array 被搜索数组)

可以看到,一个 Match 公式一次只能在 lookup_array 里查找一个数值。而把 Match 公式改写为数组公式,并用 Ctrl+Shift+Enter 确认以后,实际公式则变成了 {=Match( lookup_value_array, _lookup_array, [match_type] )}。

在公式里,本来应该是单一数值的地方,被替换成了一个数组,待查询数值 变成了 待查询数组。则 Excel 会自动响应 Ctrl+Shift+Enter 命令,把该公式拆分成多次分别执行,每次取待查询数组里的一项,单独给出一个结果,然后循环到该数组里的每个元素都被查询一遍。

例如,选择 C1:C5 单元格并在公式栏中输入 =Match( B1:B5, A1:A10, 0 ) ,按 Ctrl+Shift+Enter 回车。Excel 会自动内部展开五次 =Match() 查询,每次查询在第一个参数位分别填入 B1 – B5。即在 A1:A10 中分别查找 B1 – B5 的值,查 5 遍,并把 5 个结果分别放在对应的 C1:C5 单元格里。

array_match

所以:

1. 因为往往有多个输出结果,使用数组公式需要先选择好输出位置,再在公式栏写公式,写完用 Ctrl+Shift+Enter 确认。注意,这多个单元格包含的是『一个公式』。

2. 数组公式需要你在写公式时,把『一个数据』的参数改写为『一组数据』。(例子中 Match() 函数本来的 lookup_value 即『需要查找的值』改成了『需要查找的数组』。)Excel 会自动循环这个改写数组里的每一个数据,然后把公式计算结果填到对应的单元格里。

3. 数组公式修改起来较为费劲,经常会出现『不能更改数组的某一部分』,正确的方法是先按 Ctrl+/ 全选该数组公式的整体占用位置,然后再在公式栏进行修改。

4. 某些公式,例如 Sum()、Len() 使用数组公式和直接使用该公式往往没有区别。所以如果你见到某个教程在以 Sum 举例讲数组函数时就不用往下看了。百度搜出来有不少是这样的。

5. 一般来说,常用的数组计算 Excel 都已经提供了特定的函数,比如 Logest()、Frequency 等。如果返回的值有两个以上的,也通常都拆成了多个公式,比如线性回归的 Slope()、Linest()、Steyx() 等。当需要多个计算结果时,也无需使用数组公式,使用 Excel 的公式复制粘贴就可以完成绝大部分工作。上文的例子即是如此,选择 C1:C5 然后输入 Match() 数组公式,和先在 C1 输入普通的 Match 公式 =Match( B1, $A$1:$A:$10,0 ),然后把公式复制到 C2:C5 上,效果是一样的,后续处理起来还方便一些。

那么数组公式有什么用呢?

大部分情况其实没什么用,确实没什么用,所以很多人用了好久也没用过数组公式。哲学点地说,等到你需要用数组公式时,数组公式就有用了。

数组公式的最大特点是『输出的是一个数组』,所以它需要用多个单元格才能放下,同时,它可以作为数组参数供其它函数使用。所以数组函数最大的使用场景是通过复杂嵌套函数,实现更大程度的 Excel 自动化。

例如,去除重复单元格,可以使用 Alt+A+M 的『删除重复项』实现,但这样意味着每次数据更新,都需要重新进行人工操作,当处理步骤较多时,往往意味着后续步骤也需要重新操作。而使用数组公式,则可以一劳永逸地解决这个问题。

1
{=INDEX(A:A,SMALL(IF(MATCH(A$2:A$20,A$2:A$20,)=ROW($1:$19),ROW($2:$20),4^10),ROW(A1)))&""}

因为『删除重复项』本质上就是一个『输入一个数组,输出一个数组』的操作。在这个例子里,Match() 函数的第一个参数和两个 Row() 参数进行了相同的对应循环,并把每个计算结果填入相应的单元格里。

而另一个例子

1
{=Mid(A1,Row(1:100),1)}

则相当于把 A1 单元格中的每个字符都单拆出来。辅以其它公式嵌套,可以比较方便地计算诸如『若干个单元格一共包含多少个特定字符』之类的问题。

一句话总结:当你在使用 Excel 时,需要处理『处理若干个数据,过程中包含若干数据,结果也是若干个数据』时,在宏程序之外,还可以考虑使用数组公式。

一个需求:如何批量建立 Github 帐号下的库。

Github 库通常的建立方式是通过网页,其次是通过 Github 客户端 Publish。

但这两种方式都无法批量建立库,每新建一个库都需要用户重复操作一遍。

查了一下,还真有这样的办法。Github 提供了 Restful API 来帮助企业管理代码库。不光是批量建立,还包括添加删除用户组等等,非常完善。就建库这一需求,Github 提供了这样的一个 API:

https://developer.github.com/v3/repos/

第5节就讲的是如何 Create 一个库,提供了非常详尽的设置参数:

Name Type Description
name string Required. The name of the repository
description string A short description of the repository
homepage string A URL with more information about the repository
private boolean Either true to create a private repository, or false to create a public one. Creating private repositories requires a paid GitHub account. Default: false
has_issues boolean Either true to enable issues for this repository, false to disable them. Default: true
has_wiki boolean Either true to enable the wiki for this repository, false to disable it. Default: true
has_downloads boolean Either true to enable downloads for this repository, falseto disable them. Default: true
team_id integer The id of the team that will be granted access to this repository. This is only valid when creating a repository in an organization.
auto_init boolean Pass true to create an initial commit with empty README. Default: false
gitignore_template string Desired language or platform .gitignore template to apply. Use the name of the template without the extension. For example, “Haskell”.
license_template string Desired LICENSE template to apply. Use the name of the template without the extension. For example, “mit” or “mozilla”.

当然其中大部分参数可以忽略,理论上只有库名是必须的。

所以使用以下命令就可以在不登录网站或者客户端的前提下建立一个库,通过 Sublime Text 等多行文本编辑软件(甚至通过 Excel)编辑一个多行批处理文件,然后运行即可:

1
2
3
4
5
curl -u "Github用户名:Github密码" https://api.github.com/user/repos -d '{"name":"库一名字"}'
curl -u "Github用户名:Github密码" https://api.github.com/user/repos -d '{"name":"库二名字"}'
curl -u "Github用户名:Github密码" https://api.github.com/user/repos -d '{"name":"库三名字"}'
curl -u "Github用户名:Github密码" https://api.github.com/user/repos -d '{"name":"库四名字"}'
curl -u "Github用户名:Github密码" https://api.github.com/user/repos -d '{"name":"库五名字"}'

库名重复时,Github 会自动报错,并不影响批处理继续执行。

#Update:

改进了。现在的样式如下:

0

第一步,先做一张报销单:

从自动化的角度来说,尽量采用公式帮助计算、限定单元格可输入的内容、以及设置单元格格式。在这张单据里,编号单元格为 K1

公式:

对于报销单而言,使用 =SUM() 和 =VLOOKUP() 通常就足够了,至多再加个 =SUMIF()、=COUNTIF() 就足以做出挺复杂的报销单了。

单元格格式:

选定单元格后 『右键 → 设置单元格格式』。标准的可以选择第三类货币格式,复杂的可以自行定义格式,使用#代表一个数位,使用 0 代表『当这个数位是最高位但为 0 时依然显示』。

step1-2

限定填充序列:

选定单元格后菜单栏选择『 数据 → 数据验证 → 数据验证』。允许序列,来源直接写入需要的内容,用英文逗号分隔。

step1-3

第二步,添加 Excel 的开发工具菜单:

依次点击菜单上的 『文件 → 选项 →(跳出 Excel 选项窗口)→ 自定义功能区 →☑ 开发工具』打钩。

step2

第三步,设置打印宏:

点击菜单上的 『开发工具 →Visual Basic』,打开 VBA 面板,左侧双击 『ThisWorkbook』,把以下代码粘贴到出现的窗口内,然后关闭窗口

注意 K1 为这篇文章中的编号位置,其它表单需要相应调整。

1
2
3
4
5
6
7
8
9
Sub PrintWithNumber()                                       '定义宏名
Dim startnum, loopnum, N As Long '定义变量:起始编码,打印份数和循环值
loopnum = Application.InputBox(Prompt:="需要打印多少份?") '定义打印份数
startnum = Sheet1.Range("K1") '读入表格上的份数的起始值
For N = startnum To startnum + loopnum - 1 '开始循环
ActiveSheet.PrintOut '打印当前表格
Range("K1") = N + 1 '份数加一
Next N '下一个循环直到结束
End Sub '结束

step3

第四步,添加宏按钮:

点击『开发工具 → 插入 → 按钮(窗体按钮)』。

step4

然后用鼠标在 Excel 空白处拖出一个方块,当松开鼠标时,会自动跳出『指定宏』窗口,选择窗口内的『ThisWorkbook.PrintWithNumber』,然后确定。

step4-2

右键点击该按钮,选择『编辑文字』,修改为『打印报销单』。

step4-3

第五步,设置打印区域:

这一步是为了防止把按钮本身也打印出来。

全选需要打印的区域,选择菜单 『页面布局 → 打印区域 → 设置打印区域』。

step5

第六步,另存为 xlsm:

由于使用了 vba,必须把文件存为 xlsm 格式。(启用宏的 Excel 格式)

完工。


今天接到一个小任务,寥寥一记。

在 Excel 中,常见的一种需求是打印带份数号码的各种表单,例如报销单、外出单等。通常财务都会提供模板给员工自行填写,再由财务按入帐顺序统一写上序号。但当某人一次性要写几十份,或者说类似的需求,需要一次性打印 N 份时,有没有办法让打印的表格上自动带上序号?

解决办法需要依赖 Excel 中的宏程序,在 Excel 中按 Alt+F11 可以直接打开宏编辑器。或需要先通过『视图』→『宏』→『录制宏』,再经由编辑宏的办法来打开。

核心宏程序如下,一目了然:

1
2
3
4
5
6
7
Sub test()
Dim N As Integer
For N = 1 To 10
Range("E1") = N
ActiveSheet.PrintOut
Next N
End Sub

E1 为需要填序号的单元格。程序先定义一个变量用来作为页码,然后不断用它替换指定单元格中的值,每替换一次就打印一张。这一方法硬要说有什么缺点的话,大概也就是打印任务数比较多,可能在几千份时会占用不少内存。

更完整的模板还需要在界面上做个按钮和两个输入框,以便让完全没有宏能力的财务也能定义起始和结束页码并打印。但今天这需求仅仅是帮助打印而不是做个新模版,所以这任务也就完成了不再搞复杂了。

样例下载 模板-日常费用报销表

Chrome 在更新到 44.0.2403.125 m 后,存在一个由拼写检查导致的性能低下 bug,该 bug 只在 win 版 Chrome 下呈现,同时影响 32 位和 64 位版本。

chrome-44-about就是这货

具体表现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css">
p {margin: 20px;font-weight: bold;text-align: center;width: 33%}
.input {display: block;width: 33%;margin: 20px;border: 2px solid #999;}
.editor {display: block;width: 33%;border-radius: 4px;margin:20px;overflow: scroll;padding: 8px;min-height: 100px;max-height: 400px;}
.area {border: 2px solid #fa0;}
.div {border: 2px solid #0af;}
</style>
</head>
<body>
<input type="text" class="input">
<p>Textarea</p>
<textarea class="editor area"></textarea>
<p>Contenteditable DIV</p>
<div class="editor div" contenteditable='true' role='textbox'></div>
</body>
</html>

将上面这段代码另存为 html 网页,在 Chrome 44 (win) 版本下打开。

用文本编辑器制造 10000 行会被触发单词拼写识别的文本,比如使用 Excel 连续生成 10000 行『AKB0048』。复制粘贴到橙色 Textarea 框中,然后用鼠标点击灰色 Input 框……

你会发现,非常的卡,光标在三个输入框中切换,每次切换都需要花费很长时间,对于性能不佳的电脑,和彻底进入死循环没区别了。

刷新页面重置数据后,再将这 10000 行粘贴到 Contenteditable DIV 的蓝色框里,第一次粘贴也略有卡顿,毕竟数据量比较大。但等卡顿过后,鼠标再在三个框中反复切换,就非常顺畅毫无迟滞感了。

10000linta

这就是这个 bug 的真身了:

在 Textarea 有大量会触发拼写检查的内容时,Chrome 44 的 Spellcheck 功能会极大消耗计算机资源,使得整台电脑近乎死机。

解决办法

由于这是浏览器 bug,前端程序员对此几乎无能为力,只能绕过这个 bug。

通常来说,HTML5 已经提出了 Textarea 的一些标准属性,比如 spellcheck=”false” 可以强制浏览器不检查内容,webkit 内核支持这一属性。正常来说,这似乎是很容易解决的一个兼容性问题。

但偏偏 Chrome 却强制忽略了这一设置,就像它强制忽略 font-size < 10 时那样。禁用拼写检查只在用户手动输入时有效,而对于 Ctrl+V 进来的内容,拼写检查依然正常运行,搞死你机。

即使你把相关属性全部列全:

1
<textarea class="editor area" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"></textarea>

也是然并卵,在别的浏览器一切正常,而 Chrome 下依然故我。并没有解决问题。

解决方法一:

你可以通过 JS 检查当前浏览器是否是 Chrome,在需要粘贴大量内容的 textarea 上提示用户去关闭 SpellCheck 功能,具体位置为:

Chrome 菜单 → 设置 → (最下方)显示高级设置 → 语言和输入设置 → 启用拼写检查。

但这并不是一个好方法,尤其是需要用户主动去对 Chrome 进行设置。

解决方法二:

使用 JS,将一次的粘贴行为,模拟为连续字符添加的行为。比如当用户 Paste 后得到 Text 值,立刻删除该 Textarea 元素再重建一个。然后根据 text.length 使用 JQuery 插件 Jquery.Caret 不断循环插入字符。

这一方法缺点更多,首先是作为 walkaround 的方案,引入了太多并非用户参与的互动。其次,这一 bug 本来就是在大文本量 paste 下才会出现,如果再使用额外的一重循环,并不会使得体验有太多改善。最后,假如 Chrome 的再次升级把 JS 插入字符也纳入拼写检查的话,就会导致页面更 n 倍重的卡顿,到时候大概是再高性能的电脑都扛不住了。

解决方法三:

使用带 contenteditable=’true’ 属性的 DIV 元素代替 Textarea,并使用 innerText 或者 $.text() 来获取用户填写的文本内容。

10000lintv

这也是这篇博客所写的内容,这样的方法是最合法且绕过 Chrome 44 拼写检查 Bug 的方法。完全相当于是『有两种实现方法选择了另一种』。contenteditable = ‘true’ 属性的 DIV 往往也是很多 JS 版所见即所得编辑器的常用办法。比如 WordPress。这一方法不需要用户操作,也只需要少量 JS 代码。最后,即使这个 BUG 的影响范围继续蔓延,也不过是和现有的情况一样,不会更坏。

关于 MAC 版 Chrome:

Mac 下无此 bug,是因为暂时还没有拼写检查功能。

==== edit ====

现在 Mac 下也有了。

==== edit ====

现在修正了。

先上代码:
1
2
3
4
5
6
7
8
9
10
> new Date("2015-07-08")
< Wed Jul 08 2015 08:00:00 GMT+0800 (中国标准时间)
> new Date("2015/07/08")
< Wed Jul 08 2015 00:00:00 GMT+0800 (中国标准时间)
> new Date("2015 Jul 08")
< Wed Jul 08 2015 00:00:00 GMT+0800 (中国标准时间)
> new Date("2015 Jul 08 GMT+400")
< Wed Jul 08 2015 04:00:00 GMT+0800 (中国标准时间)
> new Date("2015-07-08 GMT+400")
< Wed Jul 08 2015 04:00:00 GMT+0800 (中国标准时间)
再说结论:

在 Javascript 中,当为某个变量只赋值日期时,会默认使用该日期的 0 点 0 分 0 秒作为完整的值。而根据表现格式的不同,在具体赋值时会使用不同的时区时间赋值。

当使用『-』作为年月日的分隔,即 YYYY-MM-DD 的格式时,赋值会根据 GMT/UTC 时间的当日 0 点赋值,再根据浏览器提供的系统时间,折算为当地时间。也就是说,如果你在北京,并且电脑时区正常。那么当你为某变量赋值『今天』时,实际上赋的值是 0 时区的 0 点 0 分 0 秒,换算成北京时间为早上 8 点。如果在东京,则是早上 9 点。

也就是说,无论是在哪个时区,相同的一套代码在不同的电脑上,会得到相同的值。

而当使用『/』作为年月日的分隔,即 YYYY/MM/DD 的格式时,赋值则根据电脑时间,取到本地时间该日的 0 点 0 分 0 秒。由于各时区都差一个小时整,实际上会导致 JS 代码在不同时区电脑上执行相同代码时,得到的时间值在绝对值上并不相等。即在北京时区时,会取到北京时区的 0 点 0 分 0 秒,同一时刻在东京则已经是当日 1 点 0 分 0 秒了。

换句话说,不同客户端执行相同代码会取得不同的值。

但即使如此,两者也有表现相同的时候。只要在赋值时就明确指定时区,则两者的值可以是相同的。例如:

1
2
3
4
> var a = new Date('2015-07-01 GMT+800')
> var b = new Date('2015/07/01 GMT+800')
> a.getTime() == b.getTime()
< true

同时,无论是哪种情况,当通过 JS 转换成时间戳时,其值都是相同的。原因在于时间戳本身的定义,就是指当前时间距 0 时区的 1970 年 1 月 1 号 0 点 0 分 0 秒的秒数。所以在执行环境中,函数会根据电脑的时区设置来加减相应的小时数,先转换为 0 时区,然后再进行时间距离的计算。

参考:

http://www.w3schools.com/js/js_date_formats.asp

起了个小学生式标题。

这东西又是浏览器历史的一桩恩怨……当最初的 HTML 标记式的语言结构逐步成形后,人们开始对网页内的互动有了要求。当然,这事本来并不难,所有的用户互动归根结底就是『触发事件 – 判断当前状态 – 执行相应命令』的逻辑,而 HTML 本身就已经通过开标签和闭标签组对的形式,很好地形成了后来被称之为『DOM 元素』的数据结构,理论上只要把触发事件分别加到一个个的元素上,或者按需加到某些特定的元素上,就可以执行命令了。

然后问题就来了。

由于 HTML 的表现是盒式结构,都是一些大框框包含小框框的情况。比如如下代码:

1
2
3
4
5
6
7
<div id="blue">
<div id="green">
<div id="red">
sometext
</div>
</div>
</div>

形成的 DOM 盒式包含结构大约就是这样:

无命名

当用户点击红块时,实际上浏览器是无法判断这次点击的意图的,也就是说,到底应该是点在红块上,红块对这次点击作出反应,还是点在绿块上,亦或是蓝块上。即使是三者都算作被点到了,三者都有反应,那怎么也得有个先后顺序吧。

有两种顺序都可以,一种是先子后父,先里后外,红绿蓝顺序;第二种是先父后子,先外后里,蓝绿红顺序。

两者都可以,两者都有理,然后老 IE 用的是前者,老 Netscape 用的是后者……结的梁子够多了,也不差这一个了。

于是新的协议为了兼容,提出了 DOM 事件流的三个阶段的设计,即……首先,都别争了,顺序就按『蓝绿红绿蓝』这么来吧……然后,这一顺序又分成三个阶段,第一阶段我称之为『下沉』,顺序为蓝绿;第二阶段『触底』,系统发现红色已经是最底层 DOM 元素了,第三阶段『上浮』,顺序变成了红绿蓝。

这三个阶段是有标准的命名的,分别是『捕获阶段』、『目标阶段』、『冒泡阶段』。并且严格来说,捕获阶段不包括最底层元素,而冒泡阶段包括目标阶段。盗图一张:

001108_Ry8q_214423

尽管规范上来说,要求捕获阶段只注册事件,而在冒泡阶段执行相应阶段。但事实上各浏览器在实现时,都允许在捕获阶段也执行相应的事件。结果就是,对于一个目标,有两次执行事件的机会。这在为 DOM 元素添加事件的 JS 函数里就写得很明白:

1
addEventListener(type, listener, useCapture)

其中第三参数就是决定这个事件是否在捕获阶段就开始执行的。

当我们使用 jQuery 等第三方库时,这一特性被 jQuery 很好地隐藏封装起来了,并良好地处理了各浏览器不一致的特性,使得我们可以通过 .click() 函数方便地添加点击事件,并不用考虑三个阶段的问题。

0%