Sep 2

Arch Linux 连接网络可以使用其官方开发的 netctl 系列命令行工具。要想在开机(以及从挂起/休眠状态唤醒)时自动连接到可用的无线网络,以下是设置步骤。

首先,你得告诉 Arch Linux 你知道哪些无线热点。Arch Linux 不会自动帮你破解别人的 Wi-Fi 密码的。就算 Wi-Fi 热点没有加密,你不说 Arch Linux 怎么知道它应当连接到那个热点呢,也许那是个钓鱼用的热点也说不定哦。

cd 到 /etc/netctl 目录下,可以看到 examples 目录下有一堆示例配置。复制你所需要的配置文件到上一层目录(/etc/netctl)。比如绝大多数 Wi-Fi 热点使用的是 WPA 加密,那就复制 examples/wireless-wpa 文件。目标文件名比较随意,起个方便自己的名字就行,比如 work、home 之类的。复制完成之后记得 chmod 600 禁止非 root 用户访问,因为配置文件里会包含你的 Wi-Fi 热点密码。

然后编辑配置文件,修改 ESSID 和 Key 为你的 Wi-Fi 热点 ID 和密码就可以了。之所以要先更改权限再编辑,是因为某些编辑器(如 Vim)会生成同权限的备份文件;那里有可能也会包含密码。可以放多份配置文件在这里,netctl-auto 默认会去找一个可用的连接。有多个可用的时候不太清楚它会连上哪一个,可以使用更复杂的配置文件来指定优先级(参见 examples/wireless-wpa-configsection 示例配置)。

配置文件写好之后,当然是启动相应的服务啦。Arch Linux 一贯的传统是不启动不必要的服务,除非用户说要启动之。netctl-auto 的 systemd 服务名是 netctl-auto@interface.service(当然 .service 后缀还是可以省略的)。interface 部分写你的无线网络接口的名字,可以通过 ip linkifconfigiwconfig 等命令看到。我禁用了 systemd 的可预测网络接口名称,所以我的无线网络接口名唤 wlan0。我使用如下命令启动服务:

$ sudo systemctl start netctl-auto@wlan0.service

如果一切顺利的话一小会儿之后就应该连上网了:

$ systemctl status netctl-auto@wlan0.service
● netctl-auto@wlan0.service - Automatic wireless network connection using netctl profiles
   Loaded: loaded (/usr/lib/systemd/system/netctl-auto@.service; enabled)
   Active: active (running) since 二 2014-09-02 20:23:31 CST; 2h 45min ago
     Docs: man:netctl.special(7)
  Process: 340 ExecStart=/usr/bin/netctl-auto start %I (code=exited, status=0/SUCCESS)
   CGroup: /system.slice/system-netctl\x2dauto.slice/netctl-auto@wlan0.service
           ├─402 wpa_supplicant -B -P /run/wpa_supplicant_wlan0.pid -i wlan0 -D nl80211,wext -c/run/network/wpa_supplicant_wlan0.conf -W
           ├─404 wpa_actiond -p /run/wpa_supplicant -i wlan0 -P /run/network/wpa_actiond_wlan0.pid -a /usr/lib/network/auto.action
           └─501 dhcpcd -4 -q -t 30 -K -L wlan0
...

或者通过 netctl-auto list 命令也可以看到连接上了哪个配置文件里指定的热点。

如果满意的话,就让它开机自启动啦:

$ sudo systemctl enable netctl-auto@wlan0.service

参考资料:ArchWiki 上的 netctl 条目

Aug 31

刚刚竟然梦到了高中英语老师,那个带领我们猜测试题的意图的脾气挺好的女老师。不过这里我要说的是另一位老师,一位脾气很坏的老师,一位造成我即使对很好相处的老师也感到害怕与不友好的老师,一位部分地导致我在很长的时期内与年长的人的交际障碍的老师。

她叫刘静,和中国广播电台经济之声一位主持人重名了。武汉市蔡甸区第三小学的一名教师。1997年至2000年是我所在班级的班主任,教「数学」(我不认可那时的教学方式和教学内容是数学;算术罢了)。当时她年龄大概30来岁。脾气很坏,教课无方。最常见的教学手段有二:一、拿教鞭跳起来敲讲台,敲得讲台上的粉笔头到处乱蹦,粉笔灰弄成雾霾;二、拿教鞭打手心来惩罚学生。当然那个时代,几乎所有老师都体罚学生,有些手段还更讨厌。只是体罚加上她在讲台上的「表演」,让我们在下面的学生都噤若寒蝉、惊恐不已。更重要的是,她的女儿也在这个班级,所以我们经常可以观看到一场场惊心动魄的家祖暴力。哦对了,我当初转到她这班里时没送礼,据说要不是成绩单上的数字比较大她还不会收。所以大家看,成绩好有时候并不是什么好事。

「上帝欲使其灭亡,必先使其疯狂。」不知道她现在灭亡了没有。但愿她没有继续毁人不倦。

刘静是我印象最深刻的一位老师。同时期还有一位叫张攀的男计算机老师给了留下了深刻的印象。我从那时就很喜欢计算机了,当然能力也在同学之上。但是这位张攀老师对我的提问不理不睬,因为他偏爱另一位成绩也很好的萌妹子。不仅如此,他似乎对我怀有敌意。每个孩子都会犯错。当时课上在关机的情况下练习使用鼠标,我觉得颇为无聊,把鼠标当成非电器左右推来推去地玩儿,结果招致他很严厉的一句话。具体是什么我已经忘记了。他平时脾气挺好的,经常和学生有说有笑的。那是唯一一次我见到他生气。

也就是从那时候起,我开始对各种老师敬畏有加,生怕不小心又招惹到某位疯狂或者敌意的大人。所以,尽管后来遇到过不少挺不错的老师,我也只能辜负他们的友善和帮助了。孩童时代教育中受到的伤害,和在爱情中所犯下的错误一样,要花费数十倍的时间才能慢慢弥补。

Aug 5

这里有人给出了自己的解法,但是我不喜,所以有了本文。注意,本文中的 shell 代码均为 zsh。如果你在用 Windows,那建议还是不要玩了,那个对付这种事情太难用了。

可惜知道这个题晚了两天,没能进前X名 :-(

0. Fuck your brain

机器上没有 brainfuck 编译器,于是 Google「brainfuck online」,得到这个。贴进去运行即可。

1. Multiply

一个值是 42。另一个要观察数列。直接把数列贴到数列百科全书即可。然后乘起来。

2. Keyboard

这个也很容易,不是把 Dvorak 键盘当成 Qwerty,那就是把 Qwerty 当成 Dvorak 了。对照着 Dvorak 的键位输入下边那串字符串,得到一 C 源码。编译、运行之即可。

3. QR Code

扫码,得到:

[abcdefghijklmnopqrstuvwxyz] <=> [pvwdgazxubqfsnrhocitlkeymj]

是个字符映射关系。Python 有现成的函数来处理这个。也是有正反两种可能,都试试就可以了。

>>> T = str.maketrans('pvwdgazxubqfsnrhocitlkeymj', 'abcdefghijklmnopqrstuvwxyz')
>>> s = 'Wxgcg txgcg ui p ixgff, txgcg ui p epm. I gyhgwt mrl lig txg ixgff wrsspnd tr irfkg txui hcrvfgs, nre, hfgpig tcm liunz txg crt13 ra "ixgff" t
r gntgc ngyt fgkgf.'
>>> s.translate(T)
'Where there is a shell, there is a way. I expect you use the shell command to solve this problem, now, please try using the rot13 of "shell" to enter next level.'

按照提示执行命令:

rot13 <<< shell

Arch 上,rot13 命令位于 bsd-games 包。或者在 Vim 里把光标移动到「shell」单词上按g?aw也能得到结果。

题目开始有趣起来了~

4. cat

这个题目更有趣了。源码里一堆乱七八糟的数据。先把它们弄到一个单独的 Vim 缓冲区,然后找到所有的五字符回文字符串:

%!grep -oP '(.)(.)(.)\2\1'

不是所有回文都被接受。仔细观察示例可以发现,中间一定是个小写字母,左边一定是一字母一数字。但是过滤后还是有太多结果。限制左边的字母为大写字母之后可以得结果。删掉不符合条件的,然后把中间的字符连起来即可。

v/\v^([A-Z][0-9]|[0-9][A-Z])\l/d

5. variables

初看,提示莫名其妙。后来注意到图片链接到了有意思的地方。访问得到另一个数字「32722」。显然是要用这个数字放在 URL 上继续访问了。直接拿 shell 访问:

$ n=1024
$ while true; do n=$(curl -sS http://fun.coolshell.cn/n/$n); echo $n; done

访问上百次之后出现一句话,给出了下一关的地址。

6. tree

这关要求从一棵二叉树的中序和后序遍历中还原其最深的路径。不知道怎么做,直接 Google「reconstruct a binary tree from in-order and post-order」,看来有不少人都在做类似的东西啊。我看的是 LeetCode 上的这篇文章。有代码,但我懒得写程序把树画出来或者是找最深的了。反正这树也不大,懂得了方法,直接在 dia 里手工构建出来了。当然,我只构建了最深的那部分。SVG 导出图片

然后就是拿密码解那个字符串了。要注意的是,不要自己去解 base64,不然 openssl 报错的……

7. N Queens

八皇后问题的变种。我直接使用了 Rosetta Code 上的代码。当然要小改一下,直接输出结果而不是打印出图案:

main = mapM_ print $ queens 9

然后找到符合那个 SHA1 值的解就可以了:

$ ./queens | tr -d ',[]' | while read code; do [[ $(sha1sum <<<zWp8LGn01wxJ7$code | awk '{print $1}') == e48d316ed573d3273931e19f9ac9f9e6039a4242 ]] && echo $code; done

8. Excel Column

26 进制转十进制:

>>> def debase26(x):
...   return sum(26 ** i * (ord(d) - ord('A') + 1) for i, d in enumerate(x[::-1]))
...
>>> debase26('COOLSHELL') // base26('SHELL')
85165

结果得到的页面说要转回 26 进制。好吧:(可惜没能在一行内搞定)

>>> def base64(x):
...   L = []
...   while True:
...     x, d = divmod(x, 26)
...     if d == 0: break
...     L.append(d)
...   return ''.join(chr(x + ord('A') - 1) for x in L[::-1])
...
>>> base64(85165)
'DUYO'

9. Fraternal Organisation

这个我没能解出来 QAQ 这两个图片看起来有些莫名其妙。我没注意到图片的名字和鼠标放上去的小提示。最后是看前边那个链接里的答案才知道原来还有个「猪圈密码」-_-|||

PS: 最近博客访问和评论速度都挺慢的,请见谅。

Jul 18

缘起

在一群文件里搜索特定的文本,第一个想到的工具是经典的 grep。自从知道 ag——The silver searcher 之后,我就只在命令行管道里用 grep 啦。

ag 的优势:

  • 命令短
  • 和 ack 以及 git grep 一样,默认会忽略掉你通常不想看的文本(二进制文件、被版本控制系统忽略掉的文件)
  • C 编写的,比 ack 更快!

作者挺在乎程序的执行效率。这也很重要,因为文件多啊,几十上百兆的源码找起来可费时了。

不过在我这里,ag 取代掉的既不是 grep 也不是 ack。因为我之前用的是 cgvg,和 ack 一样也是 Perl 写的,但不一样的是,它包含两个命令:一个(cg)用来搜索,另一个(vg)用来在编辑器里打开!

不知道为什么其它工具的作者都没有想到这一点。匹配的地方找到了,大部分情况都需要用编辑器打开看看,也许再改改吧?cgvg 免去了复制路径到编辑器里并跳转到特定位置这一烦琐的步骤。

所以换用 ag 之后,我自然也希望能够很便利地使用编辑器查看匹配的地方了。为此,我做了三个工具。好吧,其实有一个是在 cgvg 时代就已经有了的。

搜索

实践一下。假设我要寻找 Vim 源码中涉及到p_enc变量的地方。使用 agg 命令来搜索并使用翻页器 less 显示结果(截图时没有显示出 less 的存在):

agg result

agg 脚本更改了 ag 的显示样式,每一项开头都有一个序号,就和 cg 命令一样。同样地,agg 也把这个结果保存在主目录下的一个文件,以供 agv 命令使用。

注意,agg 并不能用来按文件名搜索。这种情况还是用 ag -g pattern

使用编辑器打开

agv 命令不给定参数,会显示上一次搜索的结果。如果给出一个编号,就会将结果在AGV_EDITOR环境变量指定的编辑器里打开,并跳转到对应的地方。和 vg 不同的是,agg/agv 为每一个终端维护了一个结果列表,这样就可以在不同的终端里搜索不同的内容而不会相互干扰了。

比如我们要去第 14 号结果所在的地方,只要执行agv 14,然后就到了:

agv result

我这里是在已有的 gVim 里打开的哦。因为我设置的AGV_EDITOR的值是vv $file:$line:$col。这是一句包含占位符的 shell 命令。$file$line$col分别是结果所在的文件、行号、列号。而vv,则是我写的另一个工具,用来在已经打开的 gVim 里打开文件,并且跳转到特定的地方

vv 需要 Python easygui 库,以及 gVim(或者 Vim)的 +clientserver 支持。vv 不仅支持上述格式的参数,作为一名 Pythonista 所编写的工具,它也支持从 Python 报错时打印的 Traceback 的行中提取文件名和行号。比如:

vv 'File "/usr/lib/python3.4/sre_parse.py", line 358, in _parse_sub'

这里,不一定要复制一整行。包含必要的信息(文件名和行号)就可以了。

当然,手动复制粘贴比较累。所以配合我的 zsh 全局别名:

alias -g XS='"$(xsel)"'

我只需要选中那一行,然后执行

vv XS

就可以了。zsh 会帮我把XS展开成我刚刚选择的文本。

ag.vim

ag.vim 是一个 ack.vim 的修改版,用于在 Vim 中调用 ag,就像 ack.vim 在 Vim 中调用 ack、grep.vim 在 Vim 中调用 grep 一样。

因为经常搜索在当前光标下的内容,我写了这么一条自定义命令:

command Agg exe 'Ag -Q ' . expand('<cword>')

仓库地址

https://github.com/lilydjwg/search-and-view

Jul 15

准备

首先检查 CPU 支持。需要 CPU 支持虚拟化的。

grep -E "(vmx|svm|0xc0f)" --color=always /proc/cpuinfo

没输出就没戏了。现在的 CPU 一般都支持的。

然后是内核支持。

zgrep CONFIG_KVM /proc/config.gz
zgrep CONFIG_VIRTIO /proc/config.gz

官方内核是支持的。

最后是用户态软件。Arch Linux 一向不怎么分包,安装 qemu 这个包就可以了。

哦对了,要安装 Arch 的话,还要准备它的安装镜像。

开始啦

一切就绪。

先创建虚拟机所用的磁盘文件。

qemu-img create -f qcow2 ArchVM.img 15G

这样就创建了一个 15G 容量的 qcow2 格式虚拟磁盘文件。之所以选用 qcow2,是因为它支持「母镜像」功能,对应于 Virtual Box 的差分存储。

然后就可以启动系统了。为了避免老是输入一长串命令,遵循 Gentoo Wiki 的建议,我们创建一个脚本:

#!/bin/sh
exec qemu-system-x86_64 -enable-kvm \
       -cpu host \
       -drive file=$HOME/ArchVM.img,if=virtio \
       -netdev user,id=vmnic,hostname=archvm,hostfwd=tcp:127.0.0.1:2222-:22 \
       -device virtio-net,netdev=vmnic \
       -m 256M \
       -curses \
       -name "Arch VM" \
       "$@"

注意到这里我已经加上了hostfwd参数,将虚拟机的 22 端口映射到 host 的 2222 端口上,方便以后通过 ssh 连接。

我这里指定了-curses参数,它将虚拟机的显示器直接使用 curses 库显示在当前终端上。当然能显示的只有显示器处于文本模式的时候,图形模式就只能知晓当前分辨率了。因为我是在服务器上使用,所以加上这个参数。当然你也可以使用 VNC 去连。

然后执行命令:

./startvm -boot once=d -cdrom path_to_file.iso

首先从光驱启动一次(once=d),重启之后恢复到默认的从硬盘启动。

系统启动啦~然后就会发现引导器 isolinux 把显示器切换到图形模式了……

终端无法显示图形模式的内容

不过还好。Arch 的引导界面我们知道。按Tab,然后输入<Space>nomodeset并回车。不过待会进系统里,KMS 之后一直是图形模式就什么也看不到了。

然后进入系统安装啦。注意硬盘设备是/dev/vda。当然也要注意安装并让 sshd 在开机时启动,虽然说有 curses 模式的「显示器」也可以用。

装好之后、重启之前还要注意一点,把/boot/grub/grub.cfg包含gfxload_video之类的地方都去掉,不然会进图形模式的。

装好后就 reboot 吧。如果一切顺利的话就能看到已经安装好的 Arch 登录提示符了。

好不容易装好了系统,当然要把它作为母镜像,所有后续的修改放子镜像上啦:

qemu-img create -f qcow2 -b ArchVM.img ArchTest.img

然后修改一下启动脚本。以后就可以用./startvm脚本启动这个虚拟机啦。

参考文章

Jul 3

muttils 是一系列用于 mutt 等终端邮件客户端的小工具,其中 viewhtmlmsg 脚本用于在浏览器中阅读 HTML 邮件:

macro pager \eh "<pipe-entry>viewhtmlmsg<enter>" 在网页浏览器中查看

但一直以来有个问题:需要等好几秒脚本才会返回,因为它要给浏览器足够的时间来读取 HTML 等文件,然后在退出前清理掉它们。如果是 Linux 系统并且安装了 inotifyx 包会好一些,它检测到有程序读取了 HTML 文件之后就立即退出了。

不过我希望更快一些。到后台去处理,到后台去等待。我要继续阅读下一封邮件。

所以就有了这么个小脚本:读取标准输入传过来的数据之后立即 fork,父进程退出,子进程去把数据交给 viewhtmlmsg 来处理:

#!/usr/bin/env python3

import sys
import os
import subprocess

def main():
  content = sys.stdin.buffer.read()
  if os.fork() > 0:
    return
  p = subprocess.Popen(
    'viewhtmlmsg',
    stdin = subprocess.PIPE,
  )
  p.communicate(content)
  p.wait()

if __name__ == '__main__':
  main()
Jun 17

需要 Python 3.4+,一个参数用来选择测试搜索服务还是 GAE 服务。测试 GAE 服务的话需要先修改开头的两个变量。从标准输入读取 IP 地址或者 IP 段(形如 192.168.0.0/16)列表,每行一个。可用 IP 输出到标准输出。实时测试结果输出到标准错误。50 线程并发。

#!/usr/bin/env python3

import sys
from ipaddress import IPv4Network
import http.client as client
from concurrent.futures import ThreadPoolExecutor
import argparse
import ssl
import socket

# 先按自己的情况修改以下几行
APP_ID = 'your_id_here'
APP_PATH = '/fetch.py'

context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('/etc/ssl/certs/ca-certificates.crt')

class HTTPSConnection(client.HTTPSConnection):
  def __init__(self, *args, hostname=None, **kwargs):
    self._hostname = hostname
    super().__init__(*args, **kwargs)

  def connect(self):
    super(client.HTTPSConnection, self).connect()

    if self._tunnel_host:
      server_hostname = self._tunnel_host
    else:
      server_hostname = self._hostname or self.host
      sni_hostname = server_hostname if ssl.HAS_SNI else None

    self.sock = self._context.wrap_socket(self.sock,
                                          server_hostname=sni_hostname)
    if not self._context.check_hostname and self._check_hostname:
      try:
        ssl.match_hostname(self.sock.getpeercert(), server_hostname)
      except Exception:
        self.sock.shutdown(socket.SHUT_RDWR)
        self.sock.close()
        raise

def check_ip_p(ip, func):
  if func(ip):
    print(ip, flush=True)

def check_for_gae(ip):
  return _check(APP_ID + '.appspot.com', APP_PATH, ip)

def check_for_search(ip):
  return _check('www.google.com', '/', ip)

def _check(host, path, ip):
  for chance in range(1,-1,-1):
    try:
      conn = HTTPSConnection(
        ip, timeout = 5,
        context = context,
        hostname = host,
      )
      conn.request('GET', path, headers = {
        'Host': host,
      })
      response = conn.getresponse()
      if response.status < 400:
        print('GOOD:', ip, file=sys.stderr)
      else:
        raise Exception('HTTP Error %s %s' % (
          response.status, response.reason))
      return True
    except KeyboardInterrupt:
      raise
    except Exception as e:
      if isinstance(e, ssl.CertificateError):
        print('WARN: %s is not Google\'s!' % ip, file=sys.stderr)
        chance = 0
      if chance == 0:
        print('BAD :', ip, e, file=sys.stderr)
        return False
      else:
        print('RE  :', ip, e, file=sys.stderr)

def main():
  parser = argparse.ArgumentParser(description='Check Google IPs')
  parser.add_argument('service', choices=['search', 'gae'],
                      help='service to check')
  args = parser.parse_args()
  func = globals()['check_for_' + args.service]

  count = 0
  with ThreadPoolExecutor(max_workers=50) as executor:
    for l in sys.stdin:
      l = l.strip()
      if '/' in l:
        for ip in IPv4Network(l).hosts():
          executor.submit(check_ip_p, str(ip), func)
          count += 1
      else:
        executor.submit(check_ip_p, l, func)
        count += 1
  print('%d IP checked.' % count)

if __name__ == '__main__':
  main()

脚本下载地址


2014年9月3日重要更新:由于失误,之前的脚本没有检查 SSL/TLS 证书,所以将错误的 IP 认为是可用的。现已更新。

Jun 3

这是个虽然简单但是很有意思的问题,以前我竟然未曾想过。后来看到「宇宙的心弦」上对这个问题的回答写得太模糊(什么叫「镜子里头脚的位置没变」?「位置没变」的定义是什么?),所以这里写一个尽可能精确描述的回答。首先,我们讨论最容易引起问题的那种情景,即人站立时正面照镜子。

首先定义几个概念。

左右。以你为原点,你的左手方向为左,右手方向为右。(你知道哪只手是左手吧?)

上下。站在地球表面,在空中静止释放一物体,由于重力,它会运动起来。其运动方向为下,反之为上。

前后。这个有点奇特。你面前站了一个人,背心对着你的时候,这是那人的面。面对着你的时候,你看到那人的面。我们的问题隐含了作为观察者的你,去看外界的像,而不是考察你自己,对吧?

让我们再定义一下坐标系。

右为 x 轴正方向,上为 y 轴正方向,由你(观察者)的后背指向你的胸前为 z 轴正方向。

作为观察者,这里有一个很明白的变换:你所认为「正」的像,其坐标要绕 y 轴旋转 180°,才能与你观察时使用的坐标系一致。让我解释得更清楚一些——

拿鼠标指针选中你,按一下Ctrl-D(如果你不是 Inkscape 用户,那就按一下Ctrl-C再按一下Ctrl-V)。现在有了你的一个像。但是你看不到它,因为它和你重合了。让我们把这个像向 z 轴正方向平衡一段距离,比如 2m,你再看看?哟,它怎么背对着你呀?不行,再原地转身 180°,这样才能看到正面不是?

忽略掉平移,让我们把这个变换记作 \(T_1\),有

$$ T_1 = \begin{bmatrix} \cos{\pi} & 0 & \sin{\pi} \\ 0 & 1 & 0 \\ -\sin{\pi} & 0 & \cos{\pi} \end{bmatrix} = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \end{bmatrix} $$

我们再来考察一下镜子做了什么。

想像镜子前有一个坐标系的三根轴,就是我们刚刚定义的那个。其中 z 轴正方向指向镜面。于是乎,镜子里的 x 轴与外边的 x 轴是平行且方向一致的。y 轴也是这样。但是 z 轴的位置没有改变,方向却反了过来,箭头对箭头了。所以,这种放置法,使得像与物体的 z 轴反了,\(z\) 变成了 \(-z\)。还是忽略掉平移,让我们把这个变换记作 \(T_2\)

$$ T_2 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \\ \end{bmatrix} $$

所以,最终作为观察者的你,看到的镜中的自己经历的变换是:

$$ T_1 T_2 = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \\ \end{bmatrix} = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

即最终的像的坐标中的 \(x\) 变成了 \(-x\),即左右颠倒。

让我们再考虑另一种情况试试。如果把镜子放在头顶上,看过去会是什么感觉呢?

这时候,y 轴一头扎进了镜子,于是,我们的第三个变换 \(T_3\) 为:

$$ T_3 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

乘一下,结果是:

$$ T_1 T_3 = \begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & -1 \\ \end{bmatrix} $$

咦?这不是上下、左右、前后都颠倒了吗?找面位于天花板的镜子试试看。上下的确颠倒了不是么?再伸出左手试试,左右也和在面前的镜子里一样,也是颠倒的。可,前后感觉并没有颠倒啊?这是因为观察者和被观察对象位于同一垂直线上,感觉不一样了。仔细想想,天花板镜子里的像的前后确实与面前的镜子晨那个的前后是对着的,不是么?而我们定义后者没有颠倒,那么前者当然相对于后者是颠倒过了嘛。

个人拙见 =w=

Jun 1

听说 Ruby 支持分数字面量呢——

irb(main):001:0> 1/3r
=> (1/3)
irb(main):002:0> 1/3r+1/2r
=> (5/6)

可是在 Python 里只能这样子:

>>> from fractions import Fraction as R
>>> R(1, 3)
Fraction(1, 3)
>>> R(1, 3) + R(1, 2)
Fraction(5, 6)

学习《用 `accio` 代替 `import`:深入理解自举和 Python 语法》改改 Python 解释器,让它也支持分数字面量,应该会很有趣呢 =w=

去翻了一下 Grammar/Grammar 文件,有些失望。那里只有语法定义,没有词法定义呢。以为词法定义在另一个地方,所以找了找,无果。只有 C 代码。想到复数的 j 字尾,直接拿 ag 搜带引号的 "j"'j',最终确认它在 Parser/tokenizer.c 里。也就是说,Python 的词法分析是自己实现的。

在那个四千多行的tok_get函数里,有一部分代码在尝试解析一个数,也就是语法里的 NUMBER。照着解析复数的办法,把 d 后缀和 r 后缀也加进去:

diff -r bf65e7db066d Parser/tokenizer.c
--- a/Parser/tokenizer.c    Mon Apr 14 22:27:27 2014 -0400
+++ b/Parser/tokenizer.c    Fri May 30 20:12:07 2014 +0800
@@ -1528,6 +1528,10 @@
                 goto fraction;
             if (c == 'j' || c == 'J')
                 goto imaginary;
+            if (c == 'd' || c == 'D')
+                goto decimal;
+            if (c == 'r' || c == 'R')
+                goto rational;
             if (c == 'x' || c == 'X') {

                 /* Hex */
@@ -1621,6 +1625,12 @@
                     /* Imaginary part */
         imaginary:
                     c = tok_nextc(tok);
+                else if (c == 'd' || c == 'D')
+        decimal:
+                    c = tok_nextc(tok);
+                else if (c == 'r' || c == 'R')
+        rational:
+                    c = tok_nextc(tok);
             }
         }
         tok_backup(tok, c);

d 后缀是我给十进制数——就是会计里会用到的精确的十进制小数——准备的。

然后可以编译出来试试。这个 configure 命令是从 Arch 官方编译脚本里找的。

./configure --enable-shared --with-threads --with-computed-gotos --enable-ipv6 --with-valgrind --with-system-expat --with-dbmliborder=gdbm:ndbm --with-system-ffi --with-system-libmpdec --without-ensurepip
make

因为我不执行安装步骤,而又用了共享库,所以要这样子执行:

LD_LIBRARY_PATH=. ./python

试试看:

>>> 4d
ValueError: could not convert string to float: 4d

有效果,不报语法错了呢。

现在报ValueError,因为我还没告诉 Python 如何解析我新加的字面量表示呢。解析代码位于Python/ast.cparsenumber函数。最终的补丁如下:

diff -r bf65e7db066d Python/ast.c
--- a/Python/ast.c  Mon Apr 14 22:27:27 2014 -0400
+++ b/Python/ast.c  Fri May 30 20:12:07 2014 +0800
@@ -3650,12 +3650,29 @@
     long x;
     double dx;
     Py_complex compl;
-    int imflag;
+    char typeflag;
+    PyObject *mod, *type, *ret;

     assert(s != NULL);
     errno = 0;
     end = s + strlen(s) - 1;
-    imflag = *end == 'j' || *end == 'J';
+    switch(*end){
+        case 'j':
+        case 'J':
+            typeflag = 'j';
+            break;
+        case 'd':
+        case 'D':
+            typeflag = 'd';
+            break;
+        case 'r':
+        case 'R':
+            typeflag = 'r';
+            break;
+        default:
+            typeflag = 'i';
+    }
+
     if (s[0] == '0') {
         x = (long) PyOS_strtoul(s, (char **)&end, 0);
         if (x < 0 && errno == 0) {
@@ -3670,13 +3687,43 @@
         return PyLong_FromLong(x);
     }
     /* XXX Huge floats may silently fail */
-    if (imflag) {
+    if (typeflag == 'j') {
         compl.real = 0.;
         compl.imag = PyOS_string_to_double(s, (char **)&end, NULL);
         if (compl.imag == -1.0 && PyErr_Occurred())
             return NULL;
         return PyComplex_FromCComplex(compl);
     }
+    else if (typeflag == 'd') {
+      mod = PyImport_ImportModule("decimal");
+      if (mod == NULL)
+          return NULL;
+
+      type = PyObject_GetAttrString(mod, "Decimal");
+      if (type == NULL) {
+          Py_DECREF(mod);
+          return NULL;
+      }
+      ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1);
+      Py_DECREF(type);
+      Py_DECREF(mod);
+      return ret;
+    }
+    else if (typeflag == 'r') {
+      mod = PyImport_ImportModule("fractions");
+      if (mod == NULL)
+          return NULL;
+
+      type = PyObject_GetAttrString(mod, "Fraction");
+      if (type == NULL) {
+          Py_DECREF(mod);
+          return NULL;
+      }
+      ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1);
+      Py_DECREF(type);
+      Py_DECREF(mod);
+      return ret;
+    }
     else
     {
         dx = PyOS_string_to_double(s, NULL, NULL);

因为只是玩玩,所以不太认真,没仔细做错误处理;因为decimalfractions模块是从外部文件导入的,所以可能被覆盖掉,从而导致报错,并且这错误是无法通过异常处理捕获的。

不出问题的话,再次make之后,就可以开始玩了。不过在此之前,再多做几个补丁,让 Python 把分数和十进制数显示得简洁好看一点:

diff -r bf65e7db066d Lib/decimal.py
--- a/Lib/decimal.py    Mon Apr 14 22:27:27 2014 -0400
+++ b/Lib/decimal.py    Fri May 30 20:12:07 2014 +0800
@@ -1015,7 +1015,7 @@
     def __repr__(self):
         """Represents the number as an instance of Decimal."""
         # Invariant:  eval(repr(d)) == d
-        return "Decimal('%s')" % str(self)
+        return str(self) + 'd'

     def __str__(self, eng=False, context=None):
         """Return string representation of the number in scientific notation.
diff -r bf65e7db066d Lib/fractions.py
--- a/Lib/fractions.py  Mon Apr 14 22:27:27 2014 -0400
+++ b/Lib/fractions.py  Fri May 30 20:12:07 2014 +0800
@@ -280,7 +280,7 @@

     def __repr__(self):
         """repr(self)"""
-        return ('Fraction(%s, %s)' % (self._numerator, self._denominator))
+        return str(self) + 'r'

     def __str__(self):
         """str(self)"""
diff -r bf65e7db066d Modules/_decimal/_decimal.c
--- a/Modules/_decimal/_decimal.c   Mon Apr 14 22:27:27 2014 -0400
+++ b/Modules/_decimal/_decimal.c   Fri May 30 20:12:07 2014 +0800
@@ -3092,18 +3092,10 @@
 static PyObject *
 dec_repr(PyObject *dec)
 {
-    PyObject *res, *context;
-    char *cp;
-
-    CURRENT_CONTEXT(context);
-    cp = mpd_to_sci(MPD(dec), CtxCaps(context));
-    if (cp == NULL) {
-        PyErr_NoMemory();
-        return NULL;
-    }
-
-    res = PyUnicode_FromFormat("Decimal('%s')", cp);
-    mpd_free(cp);
+    PyObject *res, *str;
+    str = dec_str(dec);
+    res = PyUnicode_FromFormat("%Ud", str);
+    Py_DECREF(str);
     return res;
 }

下面是最终成果啦:

>>> 0.1 + 0.2 == 0.3
False
>>> 0.1d + 0.2d == 0.3d
True
>>> 1/3r + 1/2r
5/6r
>>> 0.4/1.2r
0.33333333333333337
>>> 0.4r/1.2r
1/3r

可以看到,与复数类似,分数字面量其实包含了一次除法。所以如果分子写浮点数的话,最终结果是会被转成浮点数的呢。这个和 Ruby 的行为是一样的 =w=

May 27

这东西挺好用的,可惜我只寻到一多年以前的 Python 2 版本的,作者是 AutumnCat,不认识。但注释里提到的修改者 bones7456 是鼎鼎大名的骨头兄,现其博客已经长草……

一直以来,我都是通过子进程调用来使用的,因为我写的代码是 Python 3 版,比如这个寻找文本里的 IP 地址并标记的 ipmarkup 脚本。配合 Python 3.2 加入的 functools.lru_cache,效率还不错的样子。但近期有大量 IP 需要查询,才感到每个 IP 都开个子进程的方式实在太慢。遂将其修改为 Python 3 版,并加入了些 Python 后来才流行的 idiom。

脚本还是扔到 winterpy 仓库里了。GPLv2 授权的。

2014年8月2日更新:增加了在线更新的功能,从此不需要 Wine 就能更新数据库啦 :-) 更新方法来自微菜。更新命令如下:

python3 -m QQWry update