2011年10月14日星期五

PHP Closure的一个应用

Closure是一种很灵活的技术,如果运用得当,可以大大提高代码的可维护性,尤其对于象PHP这样的动态语言更是如此。Closure的定义可以在Wikipedia上查到,关于这一技术的学术文章也可以轻易的搜索到,在此就不赘述了。

在工作中,我需要实现一个事件处理系统。我把它设计为一个类,在类的一个方法中,我设计了一个回调函数来处理事件。它的主要逻辑如下:
  1. //main.php
  2. require 'event_handlers/index.php';
  3. $type = 'event1';
  4. function handleEvent() {
  5.     global $type;
  6.     if (function_exists('handle_'.$type)) {
  7.         call_user_func('handle_'.$type);
  8.     }
  9. }
  10.  
  11. //event_handlers/index.php
  12. require 'event1.php';
  13.  
  14. //event1.php
  15. function handle_event1() {...}
在上面的例子中,我使用了一个全局的变量$type,这种做法只是为了少打一些字,突出主要逻辑,在实际代码中handleEvent是一个类的方法。

它的结构很简单,主程序中引用了一个存放所有事件的处理函数的“索引”文件,即event_handlers/index.php,每个种类的事件处理程序都被放置在一个独立的文件中,由索引文件引用。

现在问题来了。由于系统设计中的不确定性,我不能保证事件类型(即$type)将来不会改名。一旦类型的名字改了,就需要改变每个类型处理函数的名字,而一 类事件会有多个不同的处理函数(即上例中event1.php中的函数个数会有多个)。为了使将来的代码维护尽量简单,我从#php学了一个新的方法,即 利用closure使得事件处理函数的名字是动态的。一旦发生事件类型改变,只需要修改事件处理“函数库”的文件名即可。下面的例子演示了这种用法:

//main.php
$event_type = $argv[1];
require_once "$event_type.php";
$handler = 'handle_'.$event_type;
if (is_callable($$handler)) {
    call_user_func($$handler);
} else {
    echo "$handler is not defined\n";
}

//event1.php
$event_type = basename(__FILE__, '.php');
$handle_this = 'handle_'.$event_type;
$$handle_this = function() {
    global $event_type;
    echo "Testing handler of $event_type...\n";
};

在 这个例子中,event1.php可以任意重命名,主程序只需要以那个名字调用它即可以引用其中定义的事件处理函数。需要注意的是这里必须用 is_callable代替function_exists,因为Closure从本质上说是匿名函数,不能用function_exists检测其存在 与否。另外,两次解引用的方法(即$$handler、$$handle_this等)也需要注意不要弄错了。

PHP的Mixin

日前看了一篇在PHP中实现Mixin的博文,感到非常的有用,就仔细研究了一下作者的思路,并且到#php上去咨询了一下泡在上面的PHP牛,然后对原文作者的实现方案进行了一些改良,记录下来备用。

一、何谓Mixin?

Mixin是一种编程的风格,在Lisp等语言里面早已有了,但我首次知道这个概念是在Ruby语言中。我对Mixin的理解是:一种行为注入(Behavior Injection)的方法。要了解Mixin,也就是“行为注入”的好处,需首先了解OOP中的“多重继承”和“接口”的概念。

在我的印象中,接口是在继承这个概念出现以后才进入主流语言的。首先有了C++的多重继承,然后才有了Java中否定多重继承而代之以接口的概念(当然,一些先驱性或革命性的语言如Lisp、Eiffel可能早就有此类概念了)。接口带来的好处不言而喻,但它仍然有不够灵活的地方:如果一个类声明了某个接口,它就必须实现此接口,而如果它没有声明某接口,也就无法在运行时动态地获取此接口相应的行为。

Mixin则灵活的多。它相对于接口的优势在于,无需规定接口,可以随时添加/删除行为。这种动态添加行为的能力非常有用。它可以让我们写出既面向对象而又面向切面 的代码。比如,在ERP系统中有一个员工类。假设“主管”级别的员工有查看报表的权利,我们可以利用Mixin,使得系统中只需要一个员工类,就可以方便 的实现权限控制 -- 只需要在合适的地方注入“主管”行为即可,而无需针对员工类派生出各种具有不同权限的主管类、工人类等等,或者到处写一大堆复杂的、不那么OO的权限判断代码。

二、在PHP中实现Mixin

先来看一下代码:

<?php
class Greeting {
    public function greet($obj) {
        echo $obj->getName()." says hello\n";
    }
    private function __construct() {} //一个private的构造函数使得这个类不能直接被实例化
}

class Mixable {
    private $_mixinlookup = array();
    public function __construct() {
        if (is_array($this->mixins)) {
            foreach($this->mixins as $mixin) {
                $methods = get_class_methods($mixin);
                if (is_array($methods)) {
                    foreach($methods as $method) {
                        $this->_mixinlookup[$method] = $mixin;
                    }
                }
            }
        }
    }
    public function __call($method, $args) {
        if (isset($this->_mixinlookup[$method])) {
            $class = $this->_mixinlookup[$method];
            array_unshift($args, $this);
            return call_user_func_array(array($class, $method), $args);

        }
        trigger_error('Call to undefined function '.$method, E_USER_WARNING);
    }
}

class Mixture extends Mixable {
    protected $mixins = array("Greeting");
    public function getName() {
        return get_class();
    }
}

$o = new Mixture();
$o->greet();
?>

在这个例子中,Mixture类被注入了Greeting这种行为而具备了greet()的能力。这种行为是在运行中可以随时改变的。想象一下这种能力的用途吧。

三、针对原文的改进之处

原文中使用了eval方法来实现Mixin,带来了两个弊端:1)被资深PHP人一致痛斥的eval具有很大的安全隐患和很差的性能;2)我发现按照原方案,这些被注入的方法只能接受简单的数字或字符串参数,而不能使用复杂的如array或object类型的参数。

改进的方法是,避免在需要注入的方法中使用$this,而是学习python,在方法的第一个参数传入当前对象的指针(也就是例子代码中greet的参数$obj)。只要习惯了这种做法,其实它和$this是一样方便的。

给网站加个SSL证书

以NginX为例,本文描述两种证书安装方式:

非正式证书

出于测试的目的,可以自行生成证书,步骤相当的简单:

一、生成密钥和证书
  1. openssl genrsa -des3 -out server.key 2048
  2. openssl req -new -key server.key -out server.csr
  3. cp server.key server.key.org
  4. openssl rsa -in server.key.org -out server.key
  5. openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
以上步骤的要点说明如下:
  • 第一步中需要输入密码,这个密码要记住,因为第二步中需要这个密码。
  • 此证书的密钥设置为2048 bit (第一步)、有效期为一年(第五步)。
  • 最终生成的文件有两个:server.key (密钥)、server.crt(证书)。
二、安装证书
  1. 将server.key、server.crt复制到nginx/conf目录下。
  2. 修改nginx.conf文件,打开被注释掉的SSL服务器配置模板,填入所需的密钥和证书文件名。
此处仅说明一点:如果密钥和证书文件在nginx/conf目录下,配置文件中可以直接使用文件名,不然的话,配置文件中需要使用全路径。

正式证书

正式证书的申请制作与非正式的类似。关键是非正式操作中的第五步在正式操作中是由认证机构完成的。你把server.csr提交给认证机构,他们就会发还一个server.crt(注意,可能收到两个文件,其中一个是认证机构的证书,需用cat命令附加在crt文件末尾)。详情可阅读参考文献,并按照认证机构的指示操作。参考文献中介绍了一个值得尝试的免费证书提供商:StartCom的StartSSL(http://www.startssl.com)。

参考文献

http://love.ulnmp.com/?p=271

自动建立SSH链接的辅助脚本

由 于工作需要,我在GNOME的启动程序中添加了一条用autossh建立SSH链接的命令,但它就是无法执行。经研究发现,当这条命令执行的时 候,NetworkManager的WiFi链接尚未建立!网上有人说用wicd来代替NetworkManager,但我怕麻烦,就自己写了一段脚本, 自动检测是否有网络链接,直到检测到网络链接即执行autossh。程序相当简单:

#!/usr/bin/env python
import time
import subprocess
import syslog
import sys

def connected():
    with open('/proc/net/route') as f:
        routes = f.read().strip().split("\n")
        return len(routes) > 1

cmd = sys.argv[:]
cmd[0] = 'autossh'
syslog.openlog()
syslog.syslog(' '.join(cmd))
while not connected():
    syslog.syslog("network disconnected, retry in 5 seconds...")
    time.sleep(5)

syslog.syslog("network connected!")
pid = subprocess.Popen(['killall', '-q', 'autossh']).pid
time.sleep(1)
try:
    pid = subprocess.Popen(cmd).pid
    syslog.syslog("PID of autossh process: " + str(pid))
except OSError as e:
    syslog.syslog(str(e))
syslog.closelog()

Netbeans中文字体显示问题

  1. 如果中文显示不出来,变成方块,可在jre的lib目录下建立一个font fallback,例如/usr/lib/jvm/default-java/jre/lib/fonts/fallback目录下,符号链接到系统的一 个中文字体,一般在/usr/share/fonts/truetype下面。
  2. 如果显示的中文很粗糙,则在/usr/local /netbeans/etc/netbeans.confdefault_options最后添加:-J- Dawt.useSystemAAFontSettings=on -J-Dswing.aatext=true
  3. 如果显示的式样很难看,则可以在上述default_options最后添加:--laf GTK

[译] 相似性HASH算法


如果你仔细读了我曾经在先前的一篇博文中提及的《如何在爬网过程中检测到几乎相同的网页》(Detecting Near-Duplicates for Web Crawling)一文,你可能会对simhash算法的工作原理感到好奇。该算法没有在此文中详述,作者只是提及了他引用的由Moses Charikar撰写的《扰动算法中的近似度估算技巧》(Similarity Estimation Techniques from Rounding Algorithms)一文。不幸的是,simhash这个词并未在Charikar的论文中提及,如何将两者联系起来对一般读者而言并非是显而易见的。 然而,论文中的一些描述对我们理解整个概念还是提供了有用的帮助。

在经典的信息提取(Information Retrieval,IR)算法中,要估算两篇文本的相似度一般是将其转换成所谓的“特征向量”,然后计算这些向量在“欧几里德空间”中的夹角。如果两篇文本是完全相同的,它们之间的夹角为0。它们之间的差异越大,夹角也越大。

你可以把特征向量想象成高维度空间中的一个点。其中每一个维度对应于特征向量中的一个特征值。用一个实际的例子来说,假设我们的特征值就是英文单词,那么这 个高维度空间的坐标标签就是象quick、brown、fox这样的英文单词,而不是我们熟知的x、y、z。这个空间的维度数就是英语中的单词的数目。

一篇文本可以用一个在此高维度空间中的点(即特征向量)表示。这个特征向量的第i个元素代表了单词w,可能取以下值:
  • 0,如果单词w没有在此文本中出现
  • 1,如果单词w在此文本中至少出现一次
  • k,如果单词w在此文本中出现k次,或者我们要表示这个单词出现的重要性(权重)为k
任意给定两个向量,你就可以计算出它们之间的夹角(就是它们的点积的arccos值)。

在Charikar的论文中,他设计了一种“轮廓”特征向量。两个“轮廓”之间的相似性就表示了两个特征向量之间的相似性。所谓“轮廓”,就是一个对象的浓 缩的表示,例如字符串的哈希码。由于“轮廓”是高度浓缩的,他们在存储和计算上有很大的实用价值。论文第三章“用于向量的随机超平面哈希函数” (Random Hyperplane Bashed Hash Functions for Vectors)描述了一种将n维特征向量浓缩到k维比特向量(n远大于k)的方法。具体的做法是:

  • 在目标向量空间中随机选择k个向量,假设为r_i,(0 <= i < k)
  • 定义k个单比特的哈希函数如下:
    • h_i(v) = 1,如果dot(r_i, v) >= 0
    • h_i(v) = 0,如果dot(r_i, v) < 0

这个长度为k比特的浓缩的哈希值就是将特征向量v依次用这k个哈希函数计算后,将结果串起来。

数学上可以证明,将上述的”轮廓“算法应用于两个向量u和v,sketch(u)和sketch(v)相等的概率是1 - theta / pi。其中theta是向量u和v之间的夹角。

现在,整个链条中的最后一环是,simhash和这里描述的”轮廓“算法是怎么联系起来的?答案是,它们基本上就是一回事。Charikar的论文中没有明 确地证明它们是等价的,考虑到这篇博文已经够长的了,我在这里也不想做这个数学证明。如果你有兴趣,请详细地读一下Charikar论文的第三章,也许你 能自己完成这个证明。

bash: ./sp-sc-auth No such file or directory


今天下载了一个sopcast,执行了一下,报出如标题所示的错误。感觉真是奇怪,明明那个文件已经在了,怎么还提示找不到?又用ls查看了一下,确认权限是755,可以执行的。话说就算不能执行也不会报这个错误信息啊!

折腾了半天,下载了sopcast.org上提供的libstdc++.so.5,也没什么效果。最后在google上找到一个西班牙语的网页,专门说的就 是这个问题,这才明白,原来那个sp-sc-auth是32bit的ELF文件,而我的系统是64bit的,因此报如此奇怪的错误。解决办法是:

sudo apt-get install ia32-libs

最后,linux下有一个file命令可以查看文件的类型的,例如:

$ file /usr/local/bin/sp-sc-auth 
/usr/local/bin/sp-sc-auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.2.5, stripped
 
$ uname -a
Linux fxr01 2.6.35-27-generic #47-Ubuntu SMP Fri Feb 11 22:52:49 UTC 2011 x86_64 GNU/Linux

关于BloomFilter容量的研究

一、BloomFilter的原理

BloomFilter是 一个时间和空间都非常高效的存储结构,它的基本用途是检查任意一个key(字符串)是不是在一个给定的集合(bit数组)里。基本原理是对于给定的key 计算一系列hash的值,将每一个hash的值作为索引到bit数组里去寻找相应的bit是不是为1。只有当一个key对于所有的hash值检索的结果都 是1,这个key才被认为在BloomFilter集合里面;反之,只要有一个hash值检索的结果为0,则表明该key一定不在集合中。

BloomFilter有以下特性:
  1. 只能查询给定的key是否在给定的集合中, 或者在该集合中有多少个key。但不能列举出这些key。
  2. 只 能被用于检索某个key是否在给定的集合中,而不能判断这个key在集合中出现几次,也不能象hash表一样给每个key关联一个value。一些扩展算 法,例如Bloomier Filter、Counting Bloom Filter等,可以用时间和空间代价换取这些特性。
  3. 无法删除。一旦一个key被加入Bloom Filter后,就无法将它从中删除。Counting Bloom Filter可以以时间和空间代价实现删除操作。
  4. 存 在一定的可能性,BloomFilter对于不存在于集合中的key可能返回不正确的结果,即报告其存在于集合中。这个被称之为False Positive。但是,BloomFilter报告为不存在于集合中的key则一定不存在。即它的False Negative为0。
二、最优化参数
一个Bloom Filter有以下参数:

mbit数组的宽度(bit数)
n加入其中的key的数量
k使用的hash函数的个数
fFalse Positive的比率

Bloom Filter的f满足下列公式:





在给定mn时,能够使f最小化的k值为:





此时给出的f为:





本文的第一个目的是求出最佳的k使得在给定的mf下能使n最大化,亦即得到最佳空间利用率。根据以上公式,对于任意给定的f,我们有:

n = m ln(0.6185) / ln(f)     [1]

同时,我们需要k个hash来达成这个目标:

k = - ln(f) / ln(2)                [2]

由于k必须取整数,我们在Bloom Filter的程序实现中,还应该使用上面的公式来求得实际的f

f = (1 – e-kn/m)k                [3]

以上3个公式是程序实现Bloom Filter的关键公式。

下一步,我们要确定各种不同的Hash算法的实际速度。参考文献为:

身份证校验算法

下列ruby代码可校验18位身份证校验码是否正确。如有需要可以加上检验日期、地区等等。
 
#!/usr/bin/env ruby
def checksum(idnum)
    w = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    r = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]
    if [17, 18].include?(idnum.length) then
        sum = 0
        (0..16).each do |i|
            begin
                sum += w[i] * Integer(idnum[i, 1])
            rescue ArgumentError
                return false, "ERROR: Invalid ID: #{idnum}, at position: #{i} [#{idnum[i, 1]}]"
            end
        end
        csd = r[sum % 11]
        if idnum.length == 17 then
            return true, csd
        elsif csd == idnum[17, 1] then
            return true, csd
        else
            return false, "ERROR: Invalid ID: #{idnum}, checksum expected: #{csd}, given: #{idnum[17, 1]}"
        end
    else
        return false, "ERROR: Invalid ID: #{idnum}, length: #{idnum.length}, expected: 17 or 18"
    end
end

if ARGV.length == 1 then
    idnum = ARGV[0]
    res, msg = checksum(idnum)
    if res then
        idnum += msg if idnum.length == 17
        puts "Valid ID: #{idnum}, checksum digit: #{msg}"
    else
        puts msg
    end
else
    puts "USAGE: #{$0} ID-number"
end

提升用户体验的相对时间设计

使用相对时间而非绝对时间来显示诸如博客文章的发布时间等信息能简单而有效地提升用户体验。我在工作中设计的一个网站就有这种需求。记录下我自己的思考和设计方案备考。

1. 分段、分精度显示方案

时间段 说明 显示示例(过去) 显示示例(未来)
0~4分钟 不显示实际的分钟数 刚才 现在/马上
5~59分钟 显示分钟数 23分钟前 23分钟后
1~4小时 显示小时数 1小时前 1小时后
5~47小时 按下述昨今明规则处理昨天上午
明天傍晚
48小时以上 显示天数 2天前 1天后
7天以上 显示周数 3周前 1周后
5周以上 显示月数 1个月前 4个月后
12个月以上 显示年数 2年前 1年后

2. 进位规则

为了使时间的模糊化尽量贴近用户心理,我决定按照“黄金分割”法对尾数进行舍入。例如:
  • 大于37秒算1分钟
  • 大于37分钟算1小时
  • 大于14.8小时算1天
  • 大于4.3天算1周
  • 大于7.4个月算1年
以上进位规则是在分段方案的基础上进行的。比如4.3天是在48小时以上7天以下的,应该显示天数,只有大于7天以后才适用4.3天算1周的规则,亦即:7~11.3天都算1周,11.3天以后算两周。

3. 昨今明规则

若时差超过4小时但又不足48小时,适用“昨今明”规则:如果时间点落在昨、今、明三天内的,按下表显示,否则直接显示天数。

称呼 时段(含,不含) 时长(小时) 标签
凌晨 0:00~6:00 6 dawn
早上 6:00~8:30 2.5 morning
上午 8:30~11:30 3 am
中午 11:30~13:00 1.5 noon
下午 13:00~17:00 4 pm
傍晚 17:00~18:00 1 dusk
晚上 18:00~22:00 4 evening
深夜 22:00~24:00 2 night

上表中的标签用作本地化。亦即,函数或对象只返回标签,由调用者将标签翻译至恰当的语言。

PHP后期静态绑定(Late Static Binding)

最近又一次开始大用特用PHP。同以往多年的PHP经验不同,这次大规模的采用了PHP的OOP方式设计程序。用下来发现PHP的OOP功能相当的强大,比起Java也是不徨多让。

PHP的“后期静态绑定”功能就是一个很有特色的功能,参考下面的例子:
 
<?php
class Class1 {
    public static function hello() {
        echo "Class1 say hello\n";
    }
    public static function goodbye() {
        echo "Class1 say goodbye\n";
    }
    public function greeting() {
        echo "I am a ".get_class($this)."\n";
        static::hello();
        self::goodbye();
    }
}
class Class2 extends Class1 {
    public static function hello() {
        echo "Class2 say hello\n";
    }
}
class Class3 extends Class2 {
    public static function goodbye() {
        echo "Class3 say goodbye\n";
    }
}
$c1 = new Class1();
$c2 = new Class2();
$c3 = new Class3();
$c1->greeting();
$c2->greeting();
$c3->greeting();
?>

以上程序的输出是:
 
I am a Class1
Class1 say hello
Class1 say goodbye
I am a Class2
Class2 say hello
Class1 say goodbye
I am a Class3
Class2 say hello
Class1 say goodbye

可以看出,当使用static::来引用静态方法的时候,PHP调用的是当前类的同名方法,如果没有找到,顺着继承顺序往上找。如果使用的是self::来引用,则直接调用定义该静态方法的类里面的那个。

地暖操作备忘录

  1. 锅炉:按钮上有雪花、太阳标志,如果是雪花亮起,标识地暖供水已经开启。如果太阳亮起,表示锅炉仅供生活用水。
  2. 楼下分水器插上电源插座(无开关)以后分水器即开始工作。如果长时间不用,一定要把插头拔掉。
  3. 二楼分水器无泵,所以电源插头无须拔下(电源拔下地暖即不工作)。
  4. 控制面板:手动模式:设定温度、自动模式:时间段