用词典查找代替VLOOKUP

从上一篇《PYTHON操作EXCEL》可以看到,Python 操作 Excel 已非常自如方便。但是 Python 和相关库毕竟是一个额外的依赖,若能从 Excel 自身解决此类问题,自然是更为易用。

1. VBA 中的哈希表

用 Python 的着眼点主要是 VLOOKUP 公式太慢了,所以关键是要找到一种更高效的算法或数据结构定位数据。VLOOKUP 要求对列进行排序,内部应该是对列内数据进行二分查找,算法上不好再优化了,那就只好更换一种数据结构。搜索了一下,VBA 提供了 Scripting.Dictionary 这一词典结构,而且有文章说内部是哈希表实现,那就正是我要的东西了。

这样,VLOOKUP(lookup_value,table_array,col_index_num,range_lookup) 这一公式就转为下面的词典查找方式来实现:

  • 使用要从中进行查找的 table_array 内容构建词典。用 table_array 第一列作为 key,table_array 第 col_index_num 列作为 value,插入 Dictionary 中:Dictionary.Add key, value;
  • 查找时只需直接取 Dictionary 内的值 Dictionary.Item(lookup_value),即可完成查找;

若是仅仅 VLOOKUP 一次,倒也不必费劲先建立起一个词典。但当使用同样 VLOOKUP 公式的单元格很多时(比如几万个),就显得其必要了。因为 Dictionary 只需要建立一次,就可以用 O(1) 的复杂度进行多次查找了。

2. VLOOKUP 慢,主要问题不在算法上

从算法角度,词典查找的确快于二分查找,但优势并不是那么明显。所以在具体执行时,我发现使用词典查找的 VBA 宏运行速度并不比 VLOOKUP 快多少,运行时 Excel 仍然会导致系统假死几个小时。按说如此简单的程序不应该那么慢,问题究竟在哪里呢?

经过一段摸索,我才发现问题的根源所在:

  • VBA 往 Excel 表格中填内容时,会引发表格中已有公式的自动计算,非常耗时;
  • Excel 表格内容更新时,会触发屏幕显示内容的自动刷新,代价也很高;

所以提高 VBA 脚本执行性能的关键点,在于计算时关掉公式自动计算和屏幕刷新,这也是我始料未及的。在 VBA 中实现这两点很容易,但由于 VLOOKUP 本身即是公式,我没能想通直接调用 VLOOKUP 时如何避免这两点带来的性能损失。

3. 示例 VBA 代码

在做了上面提到的两次优化之后,原来 VLOOKUP N 个小时才能完成的任务,只用了 7 秒钟就执行结束了。

下面是我写的一段示例代码。我不熟悉 VBA 语言,只是照葫芦画瓢。代码规范程度相差甚远,但题意应是体现其中了。有心的朋友可以用作参考。

Sub 在机器表上生成一级分中心()
'
' 在机器表上生成一级分中心 Macro
'
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False

t0 = Timer
' 词典
Set map_dict = CreateObject("Scripting.Dictionary")

' 打开分中心映射表
Set map_sheet = Worksheets("分中心映射表")
map_nrows = map_sheet.Range("A300").End(xlUp).Row
Set my_rows = map_sheet.Range("A2:B" & map_nrows).Rows

' 遍历分中心映射表,获得 分中心 对应的一级分中心,插入词典
For Each my_row In my_rows
   center = my_row.Cells(1, 1).Value
   city = my_row.Cells(1, 2).Value
   If Not map_dict.Exists(center) Then
       map_dict.Add center, city
   End If
Next my_row

' 打开机器表
Set dispatch_sheet = Worksheets("机器表")
dispatch_nrows = dispatch_sheet.Range("G99999").End(xlUp).Row
Set my_rows = dispatch_sheet.Range("K2:L" & dispatch_nrows).Rows

' 遍历开通表,通过词典获得 machine_id 对应的一级分中心,插入开通表
For Each o_row In my_rows
   center = o_row.Cells(1, 1).Value
   o_row.Cells(1, 2).Value = map_dict.Item(center)
Next o_row

MsgBox "在机器表上生成一级分中心。共处理 " & dispatch_nrows & " 条记录,总耗时" & Timer - t0 & "秒。"

' 销毁建立的词典
Set map_dict = Nothing

' 打开自动计算和屏幕刷新
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
'
End Sub

最后补充一点:我先实现的词典查找,后发现性能问题根源,所以未能去比较 VLOOKUP 与词典查找两种方式的具体性能差异。我想如果差异可以忍受,那么直接在 VBA 中调用 VLOOKUP 公式或许是一种更为简单的实现。

Python操作Excel

老婆单位有时候有一些很大的 Excel 统计报表需要处理,其中最恶心的是跨表的 JOIN 查询。他们通常采取的做法是,把多个 Excel 工作簿合成一个工作簿的多个表格,然后再跑函数(VLOOKUP之类)去查。因为用的函数效率很低,在 CPU 打满的情况下还要跑几个小时。

然后我就看不过去了,我也不懂 Excel,不知道如何优化,但我想用 Python+SQLite 总归是能够实现的。于是就尝试了一把,效果还不错,一分钟以内完成统计很轻松,其中大部分时间主要花在读 Excel 内容上。

1. Python 操作 Excel 的函数库

我主要尝试了 3 种读写 Excel 的方法:

1> xlrd, xlwt, xlutils: 这三个库的好处是不需要其它支持,在任何操作系统上都可以使用。xlrd 可以读取 .xls, .xlsx 文件,非常好用;但因为 xlwt 不能直接修改 Excel 文档,必须得复制一份然后另存为其它文件,而且据说写复杂格式的 Excel 文件会出现问题,所以我没有选它来写 Excel 文件。

2> openpyxl: 这个库也是不需要其它支持的,而且据说对 Office 2007 格式支持得更好。遗憾地是,我经过测试,发现它加载 Excel 文件的效率比 xlrd 慢 3 倍以上,内存使用在 10 倍以上,于是就放弃了。

3> win32com: Python Win32 扩展,这个库需要运行环境为 Windows+Office 对应版本。由于 Python Win32 扩展只是把 COM 接口包装了一下,可以视为与 VBA 完全相同,不会有读写格式上的问题。尝试了一下用 win32com 读取 Excel 文件,效率还是比 xlrd 慢一些。

由于读取效率上 xlrd > win32com > openpyxl,所以我自然选择了 xlrd 用来读取统计报表;而最终输出的报表格式较复杂,所以选择了 win32com 直接操作 Excel 文件。

2. Python 里的关系型数据库

SQLite 是一个非常轻量级的关系型数据库,很多语言和平台都内置 SQLite 支持,也是 iOS 和 Android 上的默认数据库。Python 的标准库里也包含了 sqlite3 库,用起来非常方便。

3. 用 xlrd 读取 Excel 并插入数据库样例

如果数据量不大,直接用 Python 内部数据结构如 dict, list 就够了。但如果读取的几张表数据量都较大,增加个将数据插入数据库的预处理过程就有很大好处。一是避免每次调试都要进行耗时较长的 Excel 文件载入过程;二是能充分利用数据库的索引和 SQL 语句强大功能进行快速数据分析。

#!/usr/bin/python
# -*- coding: gbk -*-

import xlrd
import sqlite3

# 打开数据库文件
device_city_db = sqlite3.connect('device_city.db')
cursor = device_city_db.cursor()

# 建表
cursor.execute('DROP TABLE IF EXISTS device_city')
cursor.execute('CREATE TABLE device_city (device_id char(16) PRIMARY KEY, city varchar(16))')
 
# 打开 device 相关输入 Excel 文件
device_workbook = xlrd.open_workbook('输入.xlsx')
device_sheet = device_workbook.sheet_by_name('设备表')

# 逐行读取 device-城市 映射文件,并将指定的列插入数据库
for row in range(1, device_sheet.nrows):
   device_id = device_sheet.cell(row, 6).value
   if len(device_id) > 16:
       device_id = device_id[0:16]
   if len(device_id) == 0:
       continue
   city = device_sheet.cell(row, 10).value
   # 避免插入重复记录
   cursor.execute('SELECT * FROM device_city WHERE device_id=?', (device_id,))
   res = cursor.fetchone()
   if res == None:
       cursor.execute('INSERT INTO device_city (device_id, city) VALUES (?, ?)',
                      (device_id, city))
   else:
       if res[1] != city:
           print '%s, %s, %s, %s' % (device_id, city, res[0], res[1])
device_city_db.commit()

4. 将结果写入 Excel 文件样例

使用 win32com 写入 Excel 的时候要注意,一定要记得退出 Excel,否则下次运行会出错。这需要增加异常处理语句,我这里偷了个懒,出了异常后要手动杀死任务管理器中的 excel 进程。至于 win32com 中类的接口,可以从 MSDN 网站查阅。

import win32com.client as win32
import os
excel = win32.gencache.EnsureDispatch('Excel.Application')
excel.Visible = False
# 貌似这里只能接受全路径
workbook = excel.Workbooks.Open(os.path.join(os.getcwd(), '输出.xlsx'))
month_sheet = workbook.Worksheets(1)
# 计算文件中实际有内容的行数
nrows = month_sheet.Range('A65536').End(win32.constants.xlUp).Row
# 操作 Excel 单元格的值
for row in range(5, nrows-4):
   month_sheet.Cells(row, 1).Value += something
# 保存工作簿
workbook.Save()
# 退出 Excel
excel.Application.Quit()

体验海淘-Google Nexus 7

一直想买个安卓 Pad 玩玩,正好 Google 家出了 Nexus 7,很眼馋。但遍览淘宝代购,发现 8G 版最便宜的还要 1670,排队者甚众。仔细算了一下,觉得代购有点儿黑。本来价格比算上关税、运费都要贵,还不是现货,需要预付款排队,不定得等多少天。于是就抱着体验的想法,直接海淘了一把。

海淘肯定得看攻略,Nexus 7 的攻略虽然不多,但也足够了。其实难选的无非是两点:一是在哪里买,除了 Google Play,还有其它网上商店,各有优劣;二是用哪个转运公司,这个也是各有优劣,都得看前人的经验。

我比较懒,转运公司选择了用的比较多的 CUL 中美速递。注册以后会给两个转运地址:一个 CA 加州的,一个 DE 特拉华州的。地址中有个人识别码,在网站下单后送到该地址的商品会被记录到个人的名下,并转运回国内。

我想买 8G 版,只能选择 Google Play 商店并且交 13.99$ 的运费。由于 Google Play 限制销售地域,我就挂了个美国的 SOCKS 代理,在 Google Wallet 顺利用一张 Visa 信用卡配合 CUL DE 的地址下单,没有被砍单。下面就简单用时间记录一下海淘过程,都转换为北京时间:

2012-07-25 21:19:Google Play 下单成功,Gmail 信箱收到订单收据;
2012-07-26 13:23:Gmail 信箱收到发货通知,拿到 UPS 追踪号码,于是去 CUL 页面填写货物预报。
2012-07-26 14:25:填写货物预报时发现收货地址写错(街道地址填成了加州的 914 Ajax Ave),在 Google Play 提交请求修改 UPS 包裹收货地址街道部分;
2012-07-27 02:17:UPS 显示包裹离开 Louisville, KY;
2012-07-27 05:56:收到客服响应,同意修改 UPS 包裹收货地址,但要求提交完整的地址,并说明只能尽力帮忙修改;
2012-07-27 08:03:提交完整地址给 Google 客服;
2012-07-27 08:14:Google 客服帮忙完成了 UPS 包裹收货地址的修改,UPS 追踪显示地址已修改;
2012-07-27 23:05:UPS 显示包裹已完成配送;
2012-07-28 09:00:登陆 CUL 发现包裹已入库,重2磅,于是提交订单,给了5元小费,支付宝付款,然后订单状态就一直显示为处理中;
2012-07-31 09:00:登陆 CUL 发现总算发货了,订单中能找到 EMS 追踪号。但我傻了吧唧地去 EMS 网站上查了两天,都没有查到追踪信息;
2012-08-02 21:00:总算发现早期那个追踪号应该在 UCS 网站上查,查了一下,是7月30日 10:26 发货的,看来包裹在 CUL 仓库里呆了两天。
2012-08-06 11:09:接到快递电话,Nexus 7 到手。

由于中间回了趟家,也没有预计到转运到国内速度会那么快,于是2日到6日之间一直没有关注包裹的追踪情况。到手之后查了一下 EMS 网站,才发现转运的速度还不算慢(EMS的时间貌似都是本地时间):

2012-07-29 22:26:00 UC 纽约 到达处理中心
2012-07-30 21:00:00 UC 纽约 离开处理中心,发往中国 上海
2012-08-04 07:32:00 上海邮政速递物流大宗邮件收寄处 收寄
2012-08-04 10:51:09 上海邮政速递物流大宗邮件收寄处 到达处理中心,来自上海邮政速递物流大宗邮件收寄处
2012-08-04 11:30:00 上海邮政速递物流大宗邮件收寄处 离开处理中心,发往上海市邮政公司邮政速递局
2012-08-04 14:04:11 上海邮政速递物流大宗邮件收寄处 离开处理中心,发往国内出口
2012-08-04 16:34:00 上海市 到达处理中心,来自上海邮政速递物流大宗邮件收寄处
2012-08-04 16:58:00 上海市 离开处理中心,发往北京市
2012-08-05 09:27:41 北京市 到达处理中心,来自上海市
2012-08-05 12:58:42 北京市 离开处理中心,发往北京邮政速递上地分公司上地营运部
2012-08-05 15:10:50 北京邮政速递上地分公司上地营运部 到达处理中心,来自北京市
2012-08-05 15:54:48 北京邮政速递上地分公司上地营运部 安排投递
2012-08-05 16:14:00 北京邮政速递上地分公司上地营运部 未妥投
2012-08-06 07:26:37 北京邮政速递上地分公司上地营运部 安排投递
2012-08-06 11:37:00 北京邮政速递上地分公司上地营运部 投递并签收

最后总结一下几点经验:

  • 全部时间:11天14小时
  • 全部费用:199$(8G版)+13.99$(美国国内运费)+ 75¥(CUL转运费) + 5¥(CUL小费),整体算下来比淘宝代购便宜 200+
  • 信用卡:工商银行信用卡网银支持开通/关闭境外无卡支付功能,支付前开通,扣款完关闭,海淘比较安全
  • CUL 服务:比较差,包裹入库、发出没有邮件通知,只能人肉登陆去看;QQ 客服常年离线;微博官方账号无响应。但从整个转运结果来看,也还算靠谱。更新:最近很多人抱怨CUL不靠谱,所以我这里也不建议大家用了。
  • CUL 羊毛:邀请用户注册,下单付款后,邀请人和被邀请人都能获得 20 积分,以后下单能抵 20 元运费。我的 CUL ID 是 solrex,也可以直接点击这个邀请链接注册,你懂的!

真是大悲大喜的一天

家家有本难念的经,生活中有很多事都是不足与外人道也。但我还是想说一说今天的经历,谈一谈我的懊悔,警示自己和别人。

我妈妈两年前的7月做了乳房切除术以治疗乳腺肿瘤。后来的这两年里,我主要关注她的术后恢复情况。因为术后恢复还算不错,所以去医院复查这样的事情我只是口头上催促催促她。

我的妈妈是一个出身于农村的普通妇女,虽然工作是小学教师,也调到县城工作,但生活经验和圈子都局限在一定范围内。再加上父亲去世后拉扯两个孩子生活比较困难,更是把自己的物质欲望压缩到最小范围。对于她而言,去医院看病是一件费力费钱且让人心生恐惧的事情。也是为了省钱,她的乳腺增生才拖成了肿瘤。

对这样的妈妈,口头上催促去复查总会变成不了了之。最近,我意识到这一点的危险性,于是专门请假回家,除了看望她,有一项重要的计划是带她去医院做术后复查。

不出所料,她依然是各种不愿意。但我还是说服她踏进了医院的大门。上午的检查很顺利,找到了原来的主治医师,前面几项的检查结果也不错,但问题出在了胸透检查项上。胸片显示胸骨左侧第四根肋骨处有个阴影,我看到胸片结果的第一眼忽然觉得胸闷难受,眼睛发红。家里有过肿瘤病人的,都应该知道阴影是一个多么恐怖的兆头!

因为医生中午不在,怀着沉重的心情回到家里,说话、吃饭,有说有笑,但都盖不住心中的担忧。担忧在下午看过医生,被要求再做CT时发展到表面。我跟妈妈说如果检查结果不好,那么明天就开始准备手术,两个月后我的婚礼也不举行了,让她好好养病。等待CT结果期间,我妈妈几分钟就想起来去取片室看一下好了没,那45分钟的等待真是煎熬,我也做好了最坏的准备。

但喜剧的转变发生在CT结果拿到后,CT显示胸部无任何异常。拿照片给主治医生判读后,也说没有任何问题。看到结果的那一刻,我的眼泪差点儿飙出来。虚惊一场,真好!!!

我真后怕,真怕是因为我的不果断导致妈妈拖延复查延误治疗。我也真庆幸,庆幸这一切只是虚惊一场。其实妈妈的胆在我身上,而不是在自己身上。在没见到我时,她不敢动手术,不敢去检查。只有我在她身边陪着,这生活的坎儿才没那么难迈。我一直知道这一点,却才发现自己没能做好。

父母对儿女是最无私的。妈妈退休后到病倒之前,还去私立学校教书,为了几百块钱早起晚睡。虽然她说自己是闲不住,但我知道她是想多攒点儿钱给我买房子。我一遍一遍给她讲,我不需要她的钱,也不希望她辛苦,但直到病倒也是不听。她固执,是出于对我的爱,我却没能固执地让她去做爱护自己的事情。

现在,我会一遍一遍地跟她说,我能挣多少多少钱,我有多少多少存款。我想让她知道,我现在能够支撑起我的小家,这个大家。不要再为钱的事情发愁,我很能挣钱,也不要再怕去医院,我会陪着。我想要的,只是她能开开心心、健健康康!

厦门-上海-苏州闲游图片

接上篇博文《厦门印象-文字版》,这是 6 月份的行程。早就想挑些照片出来,但总缺乏一些动力,导致延了这么久。

继厦门之后,中间途经上海,看望了一下老婆的好朋友。可惜天公仍不作美,在上海待的一天多下了好大的雨。仿佛我们走到哪里,雨就下到哪里。于是收拾一下,第二天就转向苏州了。

随着年龄的增长,我逐渐能领略建筑和园林的美,不再像以前只爱美丽的自然风光。本来计划在苏州主要是逛园林,但老婆忽然想到苏州的婚纱便宜,于是在苏州的婚纱市场花了一天的时间。加之我们都爱睡懒觉,在苏州就只花了一个下午看了苏州博物馆。实话说我感觉非常遗憾,但留点遗憾也好,有个下次再来的理由。

我们两个不算旅行者,倒像是换个城市生活。预先并没有做好计划,还以各种庸俗的原因修改计划:为了躲雨提前离开厦门,为了躲雨提前离开上海,忽然起兴去购物,为了躲端午节人流离开苏州,每天上午都浪费在睡梦中。留给看风景的时间太少,的确有些遗憾。但这样的安排感受到的自由度更大,倒也不算后悔。

照了很多照片,挑出来几张,贴在下面:

第一笔博客收入

博客上放百度网盟广告2318个月,终于迎来了第一笔联盟分成103.37元。我还以为是补充医疗保险的理赔款,因为款额不对纳闷了半天,刚刚登邮箱看到上周广告报表才突然想起来。我的博客总算赚钱了!

另外,感谢部分同事的关心:我注册百度联盟是早在进百度之前的事情,而且由于存在利益相关,我一年多前就将个人博客投放百度联盟广告报公司备案了,公司并没有禁止这种行为。

上图,都是一分一毛地攒出来的啊!

第一笔博客收入

厦门印象-文字版

外面的雨沥沥拉拉,从下午两点开始我们就坐在机场的肯德基里等待晚上十点半离开的飞机。由于做的功课不够加上请假时间没多少可选择,很不幸地赶上了厦门的雨季。在厦门盘桓的五天中,有三天都在下雨,包括现在。天气预报说今天是全天暴雨,于是我们只好放弃白天拖着行李箱再转转的想法,一点钟退完房,早早地赶到机场闲坐。幸好这肯德基有插座又有网,我得空写写这几天的印象。

厦门的机场名字叫做高崎国际机场,这个颇具日本范儿的名字据说来自于地名高崎村,至于高崎村的由来是否与日本有关就不得而知了。高崎机场就在厦门本岛上,这倒让交通变得非常便利,除了出租车或机场大巴,还可以直接选择市内公交线路。

厦门的市内公交非常发达,而且颇具特色。公交车站的亭盖非常完善,几乎没有像北京那样只树个站牌了事的车站,这大概是由于本地雨水和太阳都比较厉害,等车时需要遮蔽。除了环岛路一带,厦门岛上很多公交车站是只有站名没有站牌的,根本没办法知道这个公交车站有几路公交,都到什么地方。于是我们在厦门只能靠百度地图来决定到哪里坐车,坐几路车,实在不知道厦门的普通市民平时是怎样出行的。BTW 幸好百度地图在厦门还是比较准的,不然我们只能两眼一抹黑了。厦门的公交线路极其多,曾经路过的每个站点,都至少有五六路公交车路过,多的甚至有十几路,大部分上车投币一元,到哪里都很方便。

说到公交车,不得不说一下刚到厦门的一个意外经历。下飞机后我们坐 27 路公交车去宾馆,到站下车后忽然发现有装笔记本电脑和一个钱包的书包忘到了公交车上。心想完了,于是急忙打车去追公交车,但是的士司机不了解公交线路,再加上夜间公交速度很快,到最后还是没追上。正在 27 路公交车的底站手足无措,准备接受物品丢失的现实时,接到了公交总站打来的电话。因为在出租车上曾经打过总站电话报失,正好公交司机打回电话说捡到了一个书包,就直接通知到我们了。第二天到机场附近的县后总站拿到了失而复得的书包,包里分文未失,而且总站的工作人员执意不收我用现金表达的谢意。我无以为报,只能用文字记录下来这件事,非常感谢 27 路公交司机肖福明以及厦门思明公交公司的好人们!

失而复得的书包仿佛把我们的好运气用光了,接下来厦门的行程就是各种不顺利。来之前我们曾经在网上预订了一个叫做“爱尚假日”的家庭旅馆,在网上各处是好评如潮,让人感觉好得令人发指那种。我老婆也因而心动,选择了这家比其它家庭旅馆价格高出一截的旅店。谁知道入住了以后发现,这只是一家很普通的家庭旅馆,条件也只是比京郊的农家乐好一些。卫生间马桶是蹲式的,没有抽风机,地板居然是抹得高低不平的水泥地;房间里倒是有两个窗户,一个对着走廊,一个对着外面高高低低的村屋,大床一坐就吱呀响,没有有线网络,无线网络不知道是多少人分享,慢得让人回忆起拨号上网的日子;柜子里放着两罐可乐,却没有标识说是否收费(当然是收费哒!),拖鞋不是一次性,有线电视收不到湖南江苏台。凭心而论,并不是说这家旅馆条件多差,可能它还比周围的某些旅馆强些,但那么多超现实的好评总让人感觉有托儿的嫌疑,心里不太舒服。

在爱尚假日住了两天后,换到一家号称是“快捷酒店”的家庭旅馆——摩登世家曾厝垵店。在环岛路一带,曾厝垵的位置还是要好于台湾民俗村一带。离鼓浪屿、中山路更近,交通更方便,旅馆、饭馆也多。跟爱尚假日相比,摩登世家还是要正规一些,毕竟挂着连锁酒店的牌子,装修服务还算比较标准。我们住了一个有超大卫生间的豪华大床房,本来一切都挺满意,但是——但是居然有超大的蟑螂!!!半夜起来发现三只蟑螂在行李箱附近爬,快给我恶心坏了。北京的德国小蠊跟南方的美洲大蠊比体型真是小太多,恶心程度也差很多啊!

总的来说,曾厝垵附近的环岛路一带几乎没什么正经的经济型酒店。除了个别带海景的大酒店之外,大部分本质上是渔村中村屋改建的家庭旅馆,规范和卫生条件都一般,而且特别绕特别难找,基本上也不可能有什么海景。我也对这种“家庭旅馆”失去了信心,也对住海边儿失去了幻想,以后如果不舍得住大酒店海景房,还是老老实实去住中心地带的经济酒店吧。当然,如果是单身背包穷游,这种年青人多的家庭旅馆、青年旅馆可能还挺热闹挺适合的。

我们两个很懒,懒到每天都是睡到中午才起床。但得益于厦门本身景点不多,倒也没落下太多。环岛路逛了两次,从两个方向基本上把环岛路的木栈桥走遍了,风景的确不错。让我很感惊奇的是厦大白城附近的海水,胡里山炮台分开的两段海滩海水清洁程度有很大差距。厦大白城校门外面的海滩海水相当干净,直到岸边一两米才泛起黄沙;但是一岩之隔的东侧就离岸边老远就有一道明显的蓝黄海水分界线。

中山路没甚意思,就是一条商业街,民俗小吃、手工艺品与现代商场、专卖店和谐相处,与南京的夫子庙类似。厦门大学也没有想象中漂亮,唯一觉得有味道的是演武路校门进去后左手边的一排三层旧楼。鼓浪屿倒真是一个好地方,轮渡码头边的龙头路很多美食,摩肩接踵;再往里走却街巷交错,甚少人迹。很多漂亮的小地方,一下午时间却也没走完。鼓浪屿上的美食很平价,在岛上吃到最不值的大概就是20元一份的张三疯奶茶了。最赞的一个是牛肉香餐室的卤牛肉和沙茶面,另一个是马拉桑的果汁,现榨的100%橙汁最让人印象深刻,在厦门其它地方也未见到过。沙茶面在厦门各处的做法不同,但味道的确都不错。

本来计划在最后一天逛一逛岛上的内湖——筼筜湖,无奈天公不作美,看着阴沉的天气只好放弃。除了刻意去的景点外,还意外地去了一些地方。例如第一晚住的酒店附近的莲花公园,尝到了正宗的莲花公园煎蟹;有一天躲雨,还进到了整修中的启明寺,菩萨罗汉歪了一地。

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

在厦门机场,肯德基的免费 wifi 时断时续;中国电信 ChinaNet 连上却打不开验证界面,10 小时的免费时段无法使用;中国移动 CMCC 的收费 wifi 使用的确便利也飞快,40 小时赠送套餐总算有了用武之地。

春游东湖港

说起来已经是两周前的事情了,所在大组春游地点选在了京郊房山的十渡。可能对京郊大家兴趣都不是很大,算上家属才有不到四十人报名。

第一天去的是十渡还是十五渡已经搞不清了,都是些山寨的旅游项目:漂流、卡丁车和竹筏。以前有漂流的经验,怕进水所以根本就没带相机,只是偶尔用手机拍几张。可惜的照片放在内置 SD 卡上,被刷机给刷掉了。刷机前应该检查一下照片有没有被即时上传的,sigh~~~

晚上住的农家乐还可以,比去年春天去的农家乐好太多了。起码房间、被褥很干净,晚上睡觉不冷,吃的也还不错。

第二天去了一个比较正规的景点——东湖港,爬山。房山十渡一线的山和门头沟、怀柔的山区别比较大,别处的山远没有这里陡峭。东湖港整个上山的路虽然不算长,但都是陡峭的阶梯,而且很多地方是陡峭的栈道。从栈道上下,望见铁质楼梯缝下面就是空荡荡的绝壁,对胆识真的会有些考验。

上传几张同事拍的照片,留个纪念。

迁到丰台区

来北京已近六年,大小搬家经历了七八次,家当却是一次比一次多。实习和读书期间一直在海淀区晃悠。毕业以后在西二旗智学苑小住了3月,就因老婆来京工作地点交通不便搬到了曾经的宣武区牛街西里(现在属西城),北向一居开间。

都说牛街是穆斯林聚居区,美食众多。我虽在牛街住了一年半,老北京小吃却没尝到多少,比如说豆汁、焦圈就没有试过。其中大部分原因是懒得尝试,还有对太油太腻的食物不太热衷。最让我留恋的食物只有两样:一个是老城伊的羊蝎子,另一个是聚宝源的肉饼。大伙烤肉的牛肉面本来也颇合我口味,但自从涨价五块钱以后,就不甚满意了。

本来计划在牛街居住两年或更久,也习惯了在那里的生活。后来因房东的卖房而打断,很多朋友可能对这种情况都深有体会且深恶痛绝。但好歹我遇到了一位实诚的房东,不仅少收了我一个月的租金,还补偿了我一笔违约金,也算是好聚好散。当初这个房子是在豆瓣上找到的,这让我的很多同事都比较惊讶。他们不知道文艺青年聚集的豆瓣还能交换此类生活信息,但我想,关于豆瓣他们不知道的事情可能还有很多 :)。

再次找房,考虑到我们俩的公司位置,还是打算在牛街附近找。但是这次尝试了一个新的途径,在小区内部贴条找房。谁知本来目标是本小区的房子,却租到了一个本小区业主在其它小区的房子。房子在丰台区右安门附近,临着二环,还是一居开间,西向,包暖气。实话说,都是回迁小区,小区内部环境却比牛街西里差很多。但考虑到租房开支在生活支出里占大头,这个价格在周边也算便宜了,不能挑剔太多。

因为重新装修过没几年,房子内部的大体状况还算可以。唯一不好的是卫浴管道、设施比较陈旧,不少问题,我还没来得及修理。这种活我从来不愿意麻烦房东,自己就能干,省时省事。新家也不尽是坏处,附近交通上反而比牛街方便一些,在立交桥边公交线路多,也没那么堵。小区周边的小饭馆和超市也比牛街西里更多更近,尤其到晚上,临街小摊如同五道口一样兴盛。

看看房子,愈发能明白北京的区域发展不平衡。在房租房价上,南城要差北城一个档次,北城尤以西北房租、房价最高。大致原因应该是一方面西北是海淀区教育资源好,另一方面高校吸引高科技企业聚集,居民平均收入更高。我倒庆幸能在南城找房子,虽然上班路上得花费一个半小时,但好歹我老婆不用那么辛苦,且房租支出能不那么肉疼些。

今天本想骑我那尘封已久的公爵 600 出去转转来着,无奈天公不做美,只好补一补欠下来的近况。

PS: 今天是南京大学 110 年校庆,祝母校生日快乐!

Python JSON模块解码中文的BUG

很多语言或协议选择使用 ASCII 字符 “\”(backslash,0x5c) 作为字符串的转义符,包括 JSON 中的字符串。一般来说,使用 Python 中的 JSON 模块编码英文,不会存在转义符的问题。但如果使用 JSON 模块编解码中文,就可能面临着中文字符包含转义符带来的 bug。本篇文章给出了一个 badcase。

中文解码错误

测试用例文件里面包含繁体的“運動”二字,使用 GB18030 编码。使用 json 解码的错误如下:

$ cat decode.dat
{"a":"運動"}
$ python
>>> import json
>>> fp=open('decode.dat', 'r')
>>> json.load(fp, encoding='gb18030')
Traceback (most recent call last):
  File "", line 1, in 
  File "/home/yangwb/local/lib/python2.7/json/__init__.py", line 278, in load
    **kw)
  File "/home/yangwb/local/lib/python2.7/json/__init__.py", line 339, in loads
    return cls(encoding=encoding, **kw).decode(s)
  File "/home/yangwb/local/lib/python2.7/json/decoder.py", line 360, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/home/yangwb/local/lib/python2.7/json/decoder.py", line 376, in raw_decode
    obj, end = self.scan_once(s, idx)
UnicodeDecodeError: 'gb18030' codec can't decode byte 0xdf in position 0: incomplete
multibyte sequence

发生这个问题的原因,就存在于“運”字的编码之中。“運”的 GB18030 编码是 0xdf5c,由于第二个字符与转义符 “\” 编码相同,所以剩下的这个 0xdf 就被认为是一个 incomplete multibyte sequence。

我本来认为,既然已经提供了编码,json 模块就能够区分汉字与转义符(所以我觉得这应该是 json 的一个 bug)。但从实验来看,并非如此。对于一些不需提供字符编码的 JSON 解码器来说,我们倒可以用一种比较 tricky 的方法绕过上面这个问题,即在“運”字后面加一个额外的转义符:

{"a":"運\動"}

遗憾的是,这种方法对 Python 的 json 模块不适用。我仍不知道该如何解决这个解码问题。

中文编码——没错误!

对于相同的 case,Python 倒是能够编码成功:

$ cat in.dat
運動
$ python
>>> import json
>>> in_str = open('in.dat', 'r').read()
>>> out_f = open('out.dat', 'w', 0)
>>> dump_str = json.dumps({'a': in_str}, ensure_ascii=False, encoding='gb18030')
>>> out_f.write(dump_str.encode('gb18030'))
$ cat out.dat
{"a": "運動"}

所以这件事情就把我给搞糊涂了,Python 的 json 模块不能解码自己编码的 json 串。所以我觉得这可能是一个 bug,或者至少是 2.7.1 版本的 bug。

PS: 要仔细看文档

20120516:经网友 TreapDB 提醒,加载字符串时自己做 Unicode 转换,貌似能够解决这个问题。

$ cat decode.dat
{"a":"運動"}
$ python
>>> import json
>>> in_str = open('decode.dat', 'r').read().decode('gb18030')
>>> json.loads(in_str)

回头仔细看了一下 json 的文档,其中有这么一段:

Encodings that are not ASCII based (such as UCS-2) are not allowed, and should be wrapped with codecs.getreader(encoding)(fp), or simply decoded to a unicode object and passed to loads().

已经注明了 encoding 不支持非 ASCII-based 编码的参数,所以应该使用 getreader 进行转码,而不是让 json 模块去转码。看来是我没读懂文档,大惊小怪了,回家面壁去!

>>> json.load(codecs.getreader('gb18030')(fp))

WP Super Cache插件带来的500错误

今天博客服务器(Hostmonster 主机)全站从中午开始出现 500 错误,然后我登陆进 CPanel 各种查看日志、进程、数据库、PHP 状态,均未发现异常。后来又清理 php.ini、.htaccess,重启 PHP,也没有任何改善。只好给客服投了个 Ticket,准备等待客服解决。

后来灵机一动,发现同一主机 host 的其它 WordPress,有的活得很好,有的也是挂掉了。于是用排除法清理 wp-config.php,最终确定是 wp-config.php 中的 WP_CACHE 配置项有问题,删掉之后访问就恢复正常。

define('WP_CACHE', true); //Added by WP-Cache Manager

但由于 WP_CACHE 配置项是 WP Super Cache 自动增加的,一旦登陆进后台,WP Super Cache 就会自动把它再加上,后台页面又会出现 500 错误。于是乎我只好将整个 WP Super Cache 插件干掉(包括 wp-content 下的 php 脚本),终于一切恢复了正常。印象里删掉的 WP Super Cache 的版本是 0.9.9.*。

rm advanced-cache.php backup-*  cache/  wp-cache-config.php plugins/wp-super-cache/ -rf

考虑到 WP Super Cache 还是对性能有一定改善,又看了一下最新版的 WP Super Cache 是 1.0 版,我怀疑是 WP Super Cache 版本较旧造成的问题。虽然该版本已经使用了很长时间,不明白为什么今天才会爆出来 500 错误(也许 Hostmonster 主机程序进行了升级?),我还是装上了最新版本 WP Super Cache 插件。期望它不要再出现类似问题,否则只能弃用了。

既然我的博客不是同一主机上的个例,我想可能在 Hostmonster 上的其它主机也可能会遇到此类问题,特记录下来供参考。

警惕程序日志对性能的影响

做后台系统比做客户端软件的辛苦的地方,就是不能让程序轻易地挂掉。因为在生产环境中无法容易地复现或调试 bug,很多时候需要程序日志提供足够的信息,所以一个后台系统的程序员必须要明白该如何打日志(logging)。

很多语言都有自己现成的 logging 库,比如 Python 标准库中的 logging 模块,Apache 的 log4cxx(C++), log4j(Java)。如果你愿意找,很容易能找到基本满足自己需求的日志程序库。当然,自己实现一个也不是很困难。难点不在于写这些库,而是如何去使用它们。

大部分情况下,我们关注的都是日志的级别和内容。即哪些情况下,该打哪个级别的日志,日志语句中,该怎么写。

在程序开发的过程中,我们需要很多的日志协助分析程序问题;但在生产环境中,我们没有那么多的空间存储丰富的日志,而且日志量太大对于问题排查反而是累赘。有些人使用预处理解决这个问题,在 debug 版本和 release 版本中编译进不同的日志语句。这样能够解决一些问题,但却使得在生产环境中无法轻易地打印更多的日志。大部分人更接受的做法是,使用配置(参数)控制日志的打印级别,在需要更多日志的时候,可以随时打开它们。为了实现日志“少但是足够”的目标,开发人员必须明白日志信息的价值,即哪些日志应该属于哪个级别。

日志的作用是提供信息,但不同的日志语句,提供的信息量却是不一样的。有的日志里会写“Failed to get sth..”,但却忘记加上失败调用的返回值。同程序一样,日志语句中有的是变量(某个变量内容),有的是常量(提示信息)。常量你总能从程序源代码中获得,但变量不行。所以在一条日志中,信息量最大的是变量,是函数返回值/字符串内容/错误码,因而变量应该尽量放在靠前的位置。常量也不是一点价值没有,写得好的提示语句,会使问题一目了然,可以免去你到代码中 grep,然后重读代码的麻烦。

上面这两点,几乎所有知道 logging 重要性的同学都会了解。但关于 logging 对性能的影响,很多人没有足够的警惕心。例如有人会在一个按行解析文件的函数中写下这样的日志:

int parseline(...)
{
log_trace("Enter parseline with ...");
DO_SOMETHING;
log_trace("Exit parseline with ...");
return 0;
}

乍一看,由于 log_trace 级别不高,在生产环境中肯定会关闭,那么这样做看起来对性能没太大影响。但实际上 log_trace 可能是这样实现的:

#define log_trace(fmt, arg...) \
    xx_log(LVL_TRACE, "[%s:%d][time:%uus]" fmt, __FILE__, __LINE__,\
           log_getussecond(), ## arg)
#endif

可以看到 log_trace 宏中自动添加了很多信息,值得注意的是时间参数 log_getussecond()。大家都知道统计时间需要系统调用,那么无论 log_getussecond() 函数是如何实现的,它的代价肯定是高于一般的简单函数。

我们本以为 log_trace 在 LVL_TRACE 级别被关闭的情况下,消耗的代价仅仅是一个函数调用和分支判断,却没有发现宏参数中还隐藏着一个需要调用系统调用的函数。当文件不大是还算能够忍受,但当这个文件是一个数据库,扫描每一行都要执行两次 log_trace() 时,它对系统性能的影响就绝不可忽视了。

所以,最佳的做法还是,在性能攸关的代码中,使用可被预处理掉的 logging 语句,仅仅在 debug 发布中才能见到这些日志,release 版本中不把它们编译进来。

此外,上面这个 log_trace,是一个糟糕的设计。logging 模块只应该干 logging 的事情,开发人员需要时间统计时会自己完成。

64位Ubuntu上使用Network Connect

Network Connect 是 Juniper 公司出品的配合其安全硬件 VPN 解决方案的软件包,很多公司使用这个 VPN 解决方案,一般需要使用 RSA Token 动态密码登录。Network Connect 支持 Windows 和 Linux 操作系统,但很遗憾的是,它只支持 32 位 Linux。下面以 Ubuntu 为例,介绍如何

在 64 位 Linux 上安装并使用 Juniper Network Connect

1. 安装浏览器 Java 插件(64位);

$ sudo apt-get install icedtea-plugin

虽然看起来只安装了一个软件包,但实际上可能会下载依赖的 OpenJDK 的一系列软件包。这是为了在 Firefox 下能够正确启动 Network Connect 的安装过程。

2. 修改 root 密码;

$ sudo passwd

由于 Ubuntu 默认不使用 root 用户,下面自动安装 Network Connect 软件时候又必须提供 root 密码,所以这里必须先初始化 root 的密码。

3. 打开 Firefox,访问 VPN 网站。

像在 Windows 下那样,先登录进入 VPN 页面,再点击 start,启动 Network Connect 的自动安装过程。过程中会弹出一个很丑的终端,安装时需要输入 root 密码,但是最终必定是无法弹出 Network Connect 小图标,也连接不上。

4. 下载脚本 junipernc[1],并且安装到执行目录 /usr/bin 中;

$ wget http://mad-scientist.net/junipernc -O junipernc 
$ chmod 755 junipernc
$ sudo mv junipernc /usr/bin

5. 安装 Network Connect 二进制程序依赖的 32 位动态链接库;

NC 具体的可执行程序是 ~/.juniper_networks/network_connect/ncsv ,是 32 位的可执行程序。如果不安装它依赖的 32 位动态链接库[2],该程序是执行不了的。

$ sudo apt-get install libc6-i386 lib32z1 lib32nss-mdns

6. 执行 junipernc 脚本,会跳出各种对话框,对应填入各种参数;

$ junipernc --nojava

URL 就填入 VPN 网站的域名,USER 就是自己的用户名,REALM 比较麻烦,需要自己查看 VPN 网站登录页面的源代码,看对应 REALM 域实际表单提交的 value 是什么,填进去即可。

--nojava 的意思是,只执行 VPN 连接,不启动 Network Connect 小锁图标的 Java 程序。因为该 Java 程序要求 32 位 Java 环境。

连接失败会有提示;连接成功后,junipernc 会一直停在那里,终止连接可以使用 Ctrl-C 命令行,或者 sudo killall -9 ncsv。

6-1. 修改 junipernc 配置;

junipernc 有两个配置文件,一个是 ~/.vpn.default.cfg,保存着用户手工输入的配置;一个是 ~/.vpn.default.crt,这个是从网站上下载下来的证书。

这样,一般的 VPN 连接功能就实现了。如果希望启动 Network Connect 小锁图标并监控 VPN 的流量信息,就需要

在 64 位 Ubuntu 上安装 32 位 Java 环境[3]

如果不是特别需要,不建议折腾下面这套东西。

a. 到 Oracle 网站上下载 32位 Java 的 tar 包;

到这个地址:http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html#javasejdk下载例如 jre-7u3-linux-i586.tar.gz 的 Linux JRE 包。重要的特征是那个 i586。

b. 解压并安装到 jvm 目录,调整默认 java 的链接到 32位 JRE。

$ tar xzvf jre-7u3-linux-i586.tar.gz
$ sudo mv jre1.7.0_03 /usr/lib/jvm
$ sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jre1.7.0_03/bin/
java 73
$ sudo update-alternatives --config java

最后一个命令会给出一个 Java 的列表,如下所示,选择 jre1.7.0_03 对应的编号即可。

There are 2 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                      Priority   Status
------------------------------------------------------------
  0            /usr/lib/jvm/java-6-openjdk/jre/bin/java   1061      auto mode
  1            /usr/lib/jvm/java-6-openjdk/jre/bin/java   1061      manual mode
* 2            /usr/lib/jvm/jre1.7.0_03/bin/java          73        manual mode

Press enter to keep the current choice[*], or type selection number: 2

c. 验证 java 可以执行且版本正确(其实依赖上面的第 5 步);

$ java -version
java version "1.7.0_03"
Java(TM) SE Runtime Environment (build 1.7.0_03-b04)
Java HotSpot(TM) Server VM (build 22.1-b02, mixed mode)

d. 安装 32位 JRE 可能依赖的 32位动态链接库。

$ sudo apt-get install ia32-libs

这个包会依赖非常多的 32位链接库,安装过程会比较漫长。

参考文章:

[1] Using Juniper Network Connect on Ubuntu
[2] Debian6(64位)搞掂Juniper VPN
[3] Installing 32 bit java now that ia32-sun-java6-bin is not available

修改exvim目录过滤逻辑为匹配拒绝

exVim 是一个非常优秀的 Vim 环境,通过它能够省去很多 Vim 插件的配置工作。自从使用上 exVim 后,我基本没有再自定义 Vim 插件,完全依赖 exVim 打包的辅助功能。

最近让我略有不爽的使用问题是:exVim 默认的 file filter 和 dir filter 都是匹配通过的,即“匹配 filter 过滤条件的目录和文件被通过,列入项目目录、文件列表中”。

exVim 的 dir filter

对于文件来说,设置匹配通过毫无问题。因为我也想要项目中仅包含 “.cpp,.c,.h,.py” 这样的源代码文件,选出来匹配这些模式的文件就是我希望的结果。

但是对于目录来说,设置匹配通过就与我通常的需求相悖了。一般情况下,项目目录下的所有目录都是程序需要的。但是一些专门存放测试程序、测试框架、输出文件的目录,我其实不希望显示在我的项目中。而且 exVim 中的目录过滤貌似仅限在项目顶层目录中,过滤的意义不大。

所以我修改了一下 exVim 的代码,将默认的 dir filter 含义修改为匹配拒绝,即:“匹配 dir filter 的目录被拒绝(被过滤掉),无论它在哪一级。"例如,我将 dir filter 设置为 “test,output”,那么我项目目录下所有叫做 test 或者 output 的子目录都不会显示到项目目录列表中,而不妨碍其它名称目录的通过。

可以想见两个 filter 采用不同的通过逻辑并不是 exVim 开发者希望看到的,所以我想这个修改也没必要提交给开发者。不过我仍然觉得这是很有用的一个修改,所以拿出来分享一下。修改的补丁文件见:http://share.solrex.org/ibuild/exvim-dir_filter-8.05_b2.patch

PS: patch 文件中还有一个改动是将 quick_gen_project_PROJECT_autogen.sh 文件从项目目录下,移动到项目目录下的 .vimfiles.PROJECT/ 目录中,原因是看起来碍眼 :)

携友闲逛798

上周末正好有事到望京附近,顺便带老婆和朋友一起逛了逛 798 艺术中心,照了几张照片。我在里面逛得倒是依然自得,可惜同行的两位女同学兴致寥寥,于是没待太久就散了。

MIUI的贴心功能

预先声明:这不是一篇软文,我真的是出于对 MIUI 系统的喜爱才写的。

我的手机型号是华为 U8800,联通定制机,自带的 ROM 里太多乱七八糟的软件,后来就刷了华为官版的海外 ROM。这个 ROM 是比较干净的版本,但不知道为何老是黑屏重启,基本上每天会重启了一两次。毕竟追求的是性价比,这点儿缺陷我就忍了。

不过基于安卓用户的刷机习惯,后来我还是忍不住刷了修改的 MIUI ROM——因为我的手机型号不在 MIUI 的支持列表里。第一次刷的是 2011 光棍节版,前两天又更新到 2.3.2 版。MIUI 很多贴心改进让我爱不释手,下面我会列举一下。

  1. 菜单键+音量- 执行屏幕截图,下面的图片有些是用这个功能截的。

  2. 拨号键盘使用 T9 拼音直接搜索联系人,支持模糊查询。

    拨号键盘

  3. 通知栏内置很多有用的功能快捷开关。

    通知栏

  4. 系统自带防打扰过滤器功能,可以按照规则过滤垃圾短信和电话。

    防打扰

  5. 可在联系人列表中直接查看到手机号码的归属地,这样当某些朋友有多个手机号码时,容易判断该打哪一个。

    联系人
  6. 陌生人的未接电话可以显示号码归属地和响铃次数,而且第一声响铃静音。这对判断来电是否是垃圾电话非常有帮助。

    陌生人来电
  7. 内置流量监控和防火墙功能,可以为每个应用程序设置网络访问权限。

    流量监控和防火墙
  8. 内置音乐播放器与百度合作,提供在线音乐功能。

    音乐播放器
  9. 内置手电筒功能,且在锁屏时可以长按 Home 键打开手电筒。这个功能实在是太有用了!

    手电筒
  10. 电话功能设置很多贴心小功能:自动录音、接通挂断时振动、来电挂断时提供短信回复、通话记录点击显示联系人信息等等。

    电话设置
  11. 最后值得一提的是,虽然不算个功能,但刷了 MIUI 之后,每天重启的现象居然没了!我完全没想到第三方修改的第三方 ROM 居然比官版 ROM 还稳定,这个出乎了我的意料。

上海流水

我上次去上海是 8 年前,记忆都已经模糊了,只剩下几个场景比较清晰。与吴诗涛坐在沪宁双层城际列车的楼梯间地板上享受列车空调,路过华东理工收费的小足球场和封闭的草坪足球场,以及在臭水沟边秦嘉诚宿舍里塞紧蚊帐捉蚊子。在豫园吃过什么已经忘了,只记得自动售货机的可乐很贵,地铁也很贵。逛南京路步行街的时候在下雨,走到外滩雨更大。在雨中仰望了一下金茂大厦和东方明珠电视塔,不记得那时有没有环球金融中心。

上周因公差有幸免费重游上海。之所以说有幸是因为互联网公司不比其它行业,即使是大公司,出差机会也很少。对于华为、宝洁这样的公司来说,恐怕不出差反而是有幸了。我也是第一次坐飞机——不要笑我。通过飞机相连,两个城市间的距离变得好小!

这次到上海的第一印象是厚厚的云层,以及落地前虹桥机场附近的繁华,飞机就好像贴着闹市区的头顶飞过,宾馆饭店的霓虹灯广告牌都清楚可辨。出了航站楼,就闻到一股久违了的江南的潮湿空气,更重要的是,空气中没有煤烟味。经过北京漫长的冬天,这种感觉给人的冲击感太强烈了。

第一天是在张江工作,所以住在了锦江之星的张江店。我厂的上海研发中心跟安捷伦在同一幢楼里,很不起眼。室内装修跟大厦、奎科没有什么太大不同,哦,跟奎科没有什么太大不同。跟奎科一样,会议室也比较容易预定。开了一天会,不提。

由于次日是周末,所以当天晚上就坐地铁 2 号线进城住了,订的是汉庭南京西路店。地铁直达,通勤高峰期会比地面交通快一些。不得不说,2 号线人真多啊!跟北京的 1 号线类似,只要有很多人下的地方,总有很多人上,拥挤程度一路不见减轻。汉庭的大床房比锦江之星略好一些,当然价格也贵一些。

因为一个人吃晚饭无聊,约了三个朋友。一个是老同学翟效华,一个是学弟王信文,还有一个我内推入职的朋友钟云龙。地点是信文找的,在徐汇区的一个叫做厚味香辣馆的店,席间言谈尽欢,只怕是我说的太多,略显聒噪。

第二天本想早起逛逛,可起来发现外面淅淅沥沥的,只好放弃,等待中午与老同学的聚会。午饭是在宾馆附近的蝶园吃的,上海菜,味道还不错。桌上聊了聊各自的近况,还给余盈丰打了个电话。之后吴诗涛和谢成晟陪我逛了逛南京路,半路上老谢被夫人叫回家了,只剩阿涛这个单身贵族陪我逛到外滩,然后打车去机场。

妄谈时间序列表格型大数据系统设计

一直在特定领域的分布式系统一线摸爬滚打,曾取得一些微不足道的成绩,也犯过一些相当低级的错误。回头一看,每一个成绩和错误都是醉人的一课,让我在兴奋和懊恼的沉迷中成长。自己是个幸运儿,作为一个 freshman 就能够有机会承担许多 old guy 才能够有的职责。战战兢兢、如履薄冰的同时,在一线的实作和思考也让我获得了一些珍贵的经验,却直至今日才够胆量写出来一晒。这篇文章标题前面是“妄谈”两字,所持观点未必被所有人认可,我姑妄言之,有心之人姑听之。若有些友好的讨论,亦我所愿也。

我做的虽然也是分布式系统,却不够胆去讨论通用分布式系统的设计原则。因而这篇文章的主题限定到一个特定领域的分布式系统设计,这样即使别人有疑惑,我也可以把 TA 拖到我擅长的领域打败 TA :)

既然要限定,我们需要给这个系统下个定义,就有必要解释一下标题。

大数据(Big Data),这是由于分布式系统和云计算的风靡而变得很火的一个词。那么多大的规模才算大数据呢?目前没有定义,但要讨论这个问题,就必须给个确定的范围。在本文中,这个范围暂时定义为 10TB~1PB 的数据量。为什么是这个范围?我的理由是,小于 10TB 的数据规模有比较多的可选方案;大于 1PB 的数据规模,讨论的意义不大,下面会谈到。

表格型数据,是指数据是有结构的,类似于关系型数据库中的表,但不是关系型,至少不是完整的关系型。在大数据的范围内,不能说完全没有关系型的需求,但这个需求实际上是很小的。因为关系操作的复杂性,使得其在大数据上的性能非常差,此类的需求往往使用数据冗余等其它方式来实现。是性能原因,而不仅是实现难度导致它不被需求。

时间序列数据,是指数据是按照时间产生的,跟随时间而变化的分析型数据。其实分析型数据一般都是时间序列的。与操作型数据不同,在分析型数据中单单一条记录的信息是很小的,只有与其它数据进行对比、组合、分解,这条记录才会体现出其价值。

在这些限定词下,这个系统的用途就比较清楚了。它可以被用到很多地方:比如网站访问统计(Google Analytics 和百度统计)、APP 的数据统计、集群服务器状态收集、在线广告的展现和点击量等等。它是一个数据仓库,但庞大于一般的数据仓库,功能需求却少于一般的数据仓库,而且很强调性能。在这个级别上,我还没看到成熟的开放系统解决这个问题(也许我是孤陋寡闻),基本上每家都是自己实现,所以它也更值得讨论。

由于不知该如何系统地探讨,我下面只能把自己发散的思维整理为一条条简单的原则,可能会有很大的跳跃性。但是,谁在乎它连不连贯呢?

latency 对你很重要时,不要采用分层设计,优化做得越底层越好

事实上,对于有兴趣做这样一套系统的公司,latency 都很重要。因为 latency 不重要时它们完全可以使用 HBase。而且,当你有超过 1PB 数据时,你会发现其中很大一部分的 latency 不重要,那剥离出来这部分,用 HBase 吧。

在这个数据量上,必须采用分布式的实现方案。但不要为了系统逻辑的清晰而做存储层与应用层分离的实现,像 BigTable 那样。因为 locality 可以显著地降低 latency,做了存储层和应用层的分离,那你就放弃了很多可以优化的地方。否则你必须破坏分层的封装性,像 Facebook 对 HBase 做的那样。

MySQL 不是一个选项,分布式 MySQL 也不是,分布式 KV 也不是,做自己的系统吧

总会有人问这些问题:为什么数据库(分布式数据库、分布式 KV 存储)不能用于这样的场景?我只能说,原因关键是上面三个形容词:时间序列数据、表格型数据、大数据。此外可能还要加上性能、成本等其它因素。

问出上面这个问题的人,其实都可以去用数据库或者 KV 系统,大部分情况下他们的需求会被满足。因为实践过且不满足需求的人,不会问上面这个问题,所以自己找出为什么吧,更容易些。

索引很重要,但要注意控制粒度

上面说过,对于分析型数据而言,单条记录没那么重要,所以快速地获取一条记录不会成为此类系统的目标,而且索引会降低数据更新的性能。但是能不要索引吗?开玩笑,那你怎么查询!索引必须要有,但要考虑到业务场景,做到合适的粒度。所谓合适的粒度,就是能快速获得目标数据而又不至于影响数据更新的性能。

内存很重要,能省则省,能用就用完

内存的重要性大家都明白,但很少人能真正理解。能省则省——说的是不要用浪费空间的数据结构;能用就用完——说的是在保证服务器能正常工作的前提下,使用最多的内存。

IO 很重要,做任何能减少 IO 次数和数据量的事,如果要折衷,选择优化次数

对于分析型数据而言,CPU 向来不是瓶颈,IO 才是。做任何能减少 IO 次数和数据量的事,比如各种缓存(块缓存、索引缓存、请求结果缓存),比如数据压缩。如果在减少 IO 次数和减少数据量上做折衷,选择减少 IO 次数,除非这会导致数据量爆炸。

即使没分层,也不要随机写

即使能直接访问到本地文件系统,也不要使用随机写,不要向一个文件中插入内容,而是将更新与基准合并写入另一个文件。这样性能更高,真的。

支持 CRUD?不,只支持 CRA,A for aggregate

其实很多数据都可以表示成时间序列型数据,例如 MySQL 的数据表内容完全可以用时间序列的操作日志来表示,这也是 Twitter 首席工程师 Nathan Marz 提倡的,他说有 CR 就够了。虽然我没有那么极端,但是朋友,我们处理的就是时间序列数据啊,所以我们完全不需要 UD。增加 A 的原因是,聚合会减少数据量,聚合会提升查询性能。

一定要压缩数据,选择一个合适的压缩算法

原因很简单,这能够减少 IO 数据量。但不要傻乎乎地压缩整个文件,跟 BigTable 学,分块压缩。考虑到对数据更新和读取的性能偏重不同,选择对自己合适的压缩算法。因为列存储的压缩比一般而言更高,所以

如果能做列存储,就做吧

尽量分离更新和读取的压力

如果数据需要做清洗,可以聚合,那么在导入系统前做这件事,而不是让承担查询压力的系统做这件事。

实时性没那么重要,批量更新会让你更轻松

如果能接受一天的延迟,就每天一批;能接受一个小时的延迟,就不做分钟级更新。更新次数越少,预聚合效果越好,数据量越小;更新次数越少,一致性越容易保证;更新次数越少,事故处理越从容。实时更新的话,很多事情会变得非常复杂,尤其是故障处理。

用数据冗余实现关系型需求或者高性能需求

如果有关系型运算需求,一定要逼 PM 改掉。实在改不掉,在导入系统前(或者过一段时间后)计算得到结果,直接导入到系统中。高性能需求也是这样,提前在系统外聚合好再导入,让系统做最少的事情它才能更快。

分布式架构?不重要,重要的是可靠性

至于采取什么样的分布式架构,其实不重要。只要它能实现 IO 的(大致)负载均衡,并且可靠就够了。另外,值得一提的是,如果想实现中心机,选举,分片自动分裂、合并、迁移等 fancy 分布式技术,首先想想自己公司是不是行业领导者。Perfect is the enemy of good. 对于很多人来说,Zookeeper 足够了。

好的运维工具,比完美的设计更靠谱[20110222]

在完成一个大规模系统时,往往很难做到完美,尤其是当这个完美设计很复杂时。事实上 fancy 的功能也不是不能折中,例如可伸缩性通过运维工具而不是内建于系统中实现,其复杂度会大大下降,稳定性会大大提高。所以如果没有足够的能力或者时间去实现一个完美的系统,不如好好地去做一些简洁方便的运维工具。

借鉴别人经验

这个不用我解释了吧。找一切可利用的信息,和一些人讨论,自己做决定。 :)

(暂时写到这里,但我可能会更新这篇文章,当我想到更多时。)

灯泡接口

我以前是一个略具 geek 精神的人。现在不算了,写出来的好玩的计算机技术文章也没那么多了。虽然变无趣了,但我还有生活,所以我决定发掘一下其它的领域。生活中的琐事虽小,仔细琢磨下却有一些有趣的知识在里面。某些人从极小的时候就掌握的常识,对其他人来说可能到老都不明白。典型的例子有识别地图、指南针、手表或者分辨麦苗、韭菜等。

今天说的灯泡接口,也是类似。我从小到大,做过不少次爬上跳下换灯泡的活儿,本来觉得是一件很稀松平常的事情。但自食其力后才发现,原来也没那么简单,至少面临着一个复杂的问题:怎样选购正确的灯泡?

在我老家那个落后的小城镇里,很多东西都是二元的。免费电视信号只有两个:县台和县教育台;(铁路)地下道只有两个:东地下道和西地下道;灯泡也只有两种:(螺)丝口或者挂口。这样购买的风险很低。白炽灯泡一元一个,实在不行的话,买两个不同接口的灯泡就完了。反正在我小的时候有过买错灯泡的经历,算不得难堪。

长大后忽然发现,这世界不再是二元的了。典型的例子就是普通灯泡接口不再是两种,灯泡商品也不再是两种,当然价钱也不再是一元。去年年初,我老婆从公司带回来一个小台灯。它有一个圆盘形的底座,底座中央是一个笔直的灯杆,看起来像是一个倒立的图钉。在图钉的钉尖儿上是灯泡的接口,有一个可爱的圆柱状塑料灯罩可以把灯泡罩起来。灯泡的接口很奇怪,看起来是丝口,但又比丝口细。包装盒上的文字介绍极少,少到几乎无法阅读。在这些几乎无法阅读的文字中,我找到一个神奇的代码:E14。凭着直觉,我认出这应该是灯泡接口的型号。

我用来认知灯泡世界的模型改变了,只好重新建立模型。然后我才知道,原来我平常说的丝口,学名应该叫做“爱迪生螺旋(Edison Screw)接口” ,更确切一些,应该叫做“中型爱迪生螺旋(Medium Edison Screw)” 或者“E27”接口,即直径为 27 毫米的爱迪生螺旋接口。显而易见,除了E27,肯定还有其它的 E* 接口,例如上文提到的“E14”。此外,原来旧式手电筒上常见的小灯泡接口,也属于这一家族:“E10”。

从螺旋接口的型号上来看,中国普遍使用的是欧制接口。我不知道标准是如何制定的,但从查到的信息来看,中国最早的电灯公司是1861年英国商人办的“汉口电灯公司”,采用欧制接口可能跟我国 19 世纪首先被欧洲入侵的这一段历史有一定的关系。

与螺旋接口类似,卡口(或挂口,Bayonet Mount)也是一个家族 。我们通常家庭使用的,应该是“B22d”接口,即直径为 22 毫米,带双(double)接触点的卡口。此外还有射灯常用的“GU5.3”或者“GU10”接口,即插脚式U型接口,也属于卡口的一种。

在我看来,卡口要比丝口更安全,因为其露出的金属部分是不带电的,无意中摸到内部带电的弹簧突起比较困难。但奇怪的是周围的卡口灯座越来越少,我对这个现象非常好奇,却不知道其原因。

当然,除了上面说的这两个系列之外,还有其它的系列接口,可以参考这篇文档《灯头、灯座的型号命名方法及常用型号》 。不过其中一些,例如预聚焦式、凹点式或者汽车用灯接口,一般就只有专业人士才用得着了。