2007年01月17日

《程序员》杂志2007年第1期刊载了洪伟铭先生的文章《Spider系统中LRU算法的使用和实现》(以下简称《使用与实现》),恰好,本人也有过开发Spider软件的经验,对文中的某些看法不敢苟同,特商榷如下。

不错,如《使用与实现》所言,Spider需要解决的一大问题就是URL排重。可以说,抓取的效率在很大程度上取决于排重的效率,因为互联网的各页面之间是靠超链接建立联系的,页面抓取程序必须依靠链接来抓取各个页面,如果多个链接指向同一个地址,而又没有排除重复,就可能多次抓取一个页面,不但效率低下,而且可能造成数据冗余(此处暂不考虑不同URL对应重复内容的问题)。
《使用与实现》文中举了抓取新浪新闻的例子,如果每秒抓取10个页面,就必须进行900-1000次排重操作(实际上,笔者认为,这样的抓取必须首先筛选出符合某种Pattern的URL,而不能眉毛胡子一把抓),但“新”(也就是未抓取过)的URL只有几个,(所以)这种情况非常类似于操作系统中的虚拟存储器管理,所以可以使用LRU算法来解决此问题。

我所不同意的,正是“所以”的推论。

众所周知,LRU是虚拟存储系统采用的算法之一(这里姑且不论《使用与实现》文中混淆了算法的实现手段(“每次命中则加1”只是实现手段之一)和算法本身),虚拟存储系统所要解决的问题是,在以少量资源(物理空间)虚拟大量资源(逻辑空间)时,可用资源的使用(替换)策略,也就是说,“虚拟”哪些逻辑资源的问题。LRU算法的思想是,“虚拟”最近使用过的资源,“不虚拟”长时间未使用过的资源。LRU算法的理论依据是程序的局部性原理:最近使用过的资源,最有可能在将来使用,因此需要替换长时间未使用过的资源。
这样一来,LRU算法的思想,及其适于解决的问题的性质,就大致清楚了。下面我们来看,URL排重是否“非常类似于”虚拟存储系统管理。

首先,URL排重是否需要用少量可用资源来虚拟大量逻辑资源,如果答案是肯定的,也就是说,将所有已经抓取过的URL作为“大量资源”,而Cache作为“少量资源”,那么策略就是,遇到一个URL,首先在Cache中检索,如果不存在(未命中),则去后备存储器(如数据库)中寻找,不幸的是,我们在原文中未能发现这类逻辑。而且,如果抓取的规模不大,完全可以在内存中以Hash方式进行排重,这样的时间代价是O(1),如果抓取规模很大,维护链表进行URL排重显然不能胜任。
其次,URL排重是否具备“局部性”特征,也就是说,最近出现过的URL是否最有可能在未来出现?以我的经验,似乎并不存在这样的规律。一方面因为网络上各个页面之间的关系错综复杂,例如“相关新闻”关联的很可能并不是最近出现的新闻;另一方面,需要排重的URL序列与Spider抓取机制有很大的关系,如果多个Spider的工作比较分散,则“局部性”几乎不可见。

实际上,我认为,进行URL排重,最合适的思维应该是集合(Set),这才是解决重复问题的要点。
《使用与实现》使用一个HashMap加上双向链表(其实LRU的实现思路是带启发式规则的自组织线性表)的实现,不但编写起来很麻烦(“将命中的节点提前到链表开头”的操作,因为只能使用线性检索,如果链表达到一定长度,效率很低),也不容易理解(没有按照思维模型进行包装)。
如果我来设计,此系统首先必须是实现Set接口,以便外部操作,内部以规模有限的,能够按照某种规则进行调整的数据结构(例如SortedSet,用以实现LRU)来实现。这样,对外部调用程序隐藏了实现,程序的逻辑也较为清晰。

2007年01月10日

我们都知道,如果SQL语句的Where条件中包含了函数表达式,即使条件所引用的列已经有索引,也不一定会使用索引,这是因为,索引只能描述数据的“原生状态”(也就是表中的原始数据),而不能描述数据经过函数作用之后的状态(此时数据的分布和聚集情况可能已经变化,索引不再适用)。
有时候,即使我们“知道”经过函数运算之后,索引仍然有用,MySQL也不会“聪明”到这种地步,如此一来,查询的效率就会急剧下降。
对于某些情况,我们可以使用MySQL的临时变量,保存先期计算出的函数运算的结果,再利用这些结果进行进一步查询,通过explain可以看到,某些情况下,这种措施确实能够利用索引,提高查询效率。

MySQL的临时变量有两种使用方法:
1.通过Set语句
set @test1 := 0;
set @test2 := @test1 := 5;
select @test1, @test2;
+——–+——–+
| @test1 | @test2 |
+——–+——–+
| 5      | 5      |
+——–+——–+

2.通过Select语句
select @current_time := now();
select @current_time;
+————————+
| @current_time|
+————————+
| 2007-01-10 11:02:40    |
+————————+

通过临时变量,我们只需要SQL语句就能进行更复杂的查询(而不需要在数据库和程序语言之前来回倒数据)。

需要注意的是,MySQL的手册中提到,MySQL doesn’t evaluate expressions containing user variables until they are sent to the client, 也就是说,包含用户变量的表达式,在发送给客户端以前,是不会进行计算的,所以下面的SQL语句可能导致非预期的结果:
SELECT (@aa:=id) AS a, (@aa+3) AS b FROM tbl_name HAVING b=5;
having子句中的b其实引用的是变量@aa,因为SQL语句的执行是非过程化的,我们不能保证@aa的计算会在having子句执行之前(实际上,在MySQL中,b引用的@aa其实是上一次查询时的@aa的值,而不是针对本次查询的值)。
要解决这种问题,需要一些更复杂的设置,这里不多说。

另外还有一点就是,我原本以为,在Python中,把这一组SQL语句用;连接起来,调用cursor.execute(sql),就能得到最后一条语句的结果集,其实并非如此,看情况似乎是,如果cursor.execute(sql)的sql包含分号分隔的多条SQL语句,取得的结果集永远是空,而且调用一次之后,再调用cursor.execute(sql),会报告:
MySQL error 2013, ‘Lost connection to MySQL server during query’(这个问题很怪异)
解决的办法是,多次调用cursor.execute(),每次执行的参数的均为单条的SQL语句,最后取结果集,这样就一切正常了。

2007年01月05日

1.Be sure to keep this in mind.
不是
“确认把这点记在脑海中。”
而是
“万不可忘记这一点。”

2.The situation was so bad that even the CEO broke his promise.
不是
“形势是如此的糟糕,以致于甚至是CEO也不能兑现自己的承诺。”
而是
“形势太糟糕,连CEO都无法兑现自己的承诺。”

在最近的开发中,遇到两个比较奇怪的问题,把解决办法写在这里:

1.使用Python操作MySQL数据库时,如果插入的数据条目很多,一条条地执行insert…into语句可能会导致connection error,正确的办法是使用executemany(这有点类似JDBC中的batch操作):
T = ((’s11′, 1), (’s21′, 2))
cursor = conn.cursor()
cursor.executemany("insert into table_a (str_column, int_column) values (%s, %d)", T)

但是,执行这段程序却会报告错误:
Type Error, expect int……
开始百思不得其解,上网搜索才发现,executemany执行的sql语句中,所有变量都必须写作%s的形式

T = ((’s11′, 1), (’s21′, 2))
cursor = conn.cursor()
cursor.executemany("insert into table_a (str_column, int_column) values (%s, %s)", T)

测试通过

2.两台环境一样的机器,一台使用MySQL 4.1.18,另一台使用MySQL 4.1.21,在4.1.18上能够正常运行的SQL语句,在4.1.21上报错:
ERROR 1030 (HY000): Got error 28 from storage engine
检查两台机器的storage engine,未发现问题;上网搜索才发现,出现此问题的原因是,临时空间不够,无法执行此SQL语句。
解决的办法是,清空/tmp目录,或者修改my.cnf中的tmpdir参数,指向具有足够空间的目录,即可。
 

2007年01月04日

接连两个周末,看完了几本书:Programming Python, Learning Python, Dive into Python, How to think like a computer scientist:learning with python,以及一些其它资料,有几点想法:

良好的编程习惯,面向对象的设计和思维,这两点上Python与其它语言是一致的。
具体说起来,它包括,清楚的层次,易懂的命名,恰到好处的复用,合理的责任分配,规范的接口设计,准确的抽象,合适的异常处理,等等。这些都是作为软件开发人员必须掌握并且长期锻炼的基本功,如果掌握得不牢固,而Python语法又很随意,写出来的程序很可能是一团乱麻,完全无法维护。

单元测试,思想与其它语言是一样的。
单元测试的思想,应该是与语言无关的,或许是JUnit影响力太大了,“单元测试”仿佛与Java有脱不开的关系。其实,养成单元测试的习惯,掌握单元测试的思维(哪怕只是一些最基本最简单的原则,例如测试equal关系时,不但要测试等价关系的三条属性,更要测试“not equal的对象确实不相等”),都能极大地提升我们的开发效率。Python内置了单元测试的module,加上灵活的编程风格(比JUnit要灵活许多),所以在Python中使用单元测试非常舒服。

函数式编程,不一样的思维。
Python能够进行多范型编程,除了传统的命令式编程,还可以进行函数式编程。第一次看到函数可以作为参数传递时,我很是惊奇(虽然知道“函数指针”这玩意),当时只是把function作为“对象”来理解的,认为传递函数其实也就是传递“对象”,也是这样用的。看完了Concepts of Programming Language里讲解Functional Programming的章节之后,才算真正摸到一点函数式编程的门道——按照我的理解,函数式编程的思维模式是不同于命令式编程的,函数式编程不太看重“数据类型”,而注重“函数”(也就是定义域和值域之间的一种映射关系)。我们使用map, reduce, filter之类的功能时,并不是按照for循环的思路,并不是依次把每个元素传递给函数,而是将函数(一种运算规则)作用于每个元素——这些元素是否有序,如何组织,作用的顺序如何,都不必关心,也不应该关心(或许正是因为这个原因,函数式编程在处理分布式、同步/互斥问题时异常方便)。函数的组合也非常方便,我们可以用类似 f*g(x) = g(f(x)) 的规则轻松自如地动态构建出自己所需的复杂功能,这一点有些类似面向对象中的Composite,合理运用能够带来极大的便利。
Dive into Python中有一处讲到,使用Python时,开发人员应该进行“以数据为中心”的程序设计(Data-centric programming),考虑如何对这些数据进行操作,命令式语言中常见的for循环结构,往往会影响甚至破坏这种以数据为中心的思考,开发人员于是被实现方式限制了思维,过早地纠缠于实现的细节。虽然有经验的程序员在使用其它语言进行开发时,能够在一定程度上避免这种情况,但Python的灵活性显然更有利于进行“以数据为中心”的程序设计。

少而精炼的数据类型,更有利于快速开发。
刚刚开始接触Python时,确实很难理解,没有了类型,程序应该如何写。现在反而越来越觉得顺手,这样不但能让开发人员把精力集中于程序的逻辑本身,而且去掉类型的束缚也省却了许多繁琐的工作。以Java为例,在Java中,即使是临时用到的key-value数据结构,也必须写上一堆代码(为保证类型安全,还必须使用范型),如果需要在不同的class之间调用则更加麻烦;Java的反射功能固然强大,但使用起来仍然比较麻烦。Python则不同,程序可以随心而动,感觉不像Java一招一式规规矩矩庄严齐整,而是行云流水般的流畅(当然,需要良好的开发习惯来保证,否则就成一团乱麻了);而且,灵活地使用Python的and/or逻辑运算,一条语句就能够完成以往需要许多条语句才能完成的功能。

总的来说,我认为,不同的语言有不同的思维和特性。笼统地说哪门语言更好,或者“所有语言其实都是一样的”,并不合理。重要的是:
根据具体的环境选择适当的语言;
努力体会不同背后的思维模式,把这当作拓展自己思维的途径。

2006年12月14日

常言说,屁股决定脑袋,身份决定立场。
现实生活中,处处可见这条规律的身影——不过,一旦涉及到某些问题,情况就变得复杂起来了。
最常见的论调就是:中国这样做,是有道理的……

这样的说法,我是很反感的。
须知,人首先必须是人,其次才是一国的国民。考虑问题的时候,基本的出发点应该是人的良知,人的道德,而不是政府(国家)的道德——人的道德叫做道德,政府的“道德”,如果有的话,应该叫做权谋、利益。放弃道德而取权谋,说得不好听一点,就是自作贱。
其次,我们作为普通公民,当然应该有普通公民的价值体系,不同于政府的价值体系。否则,何来公域与私域的区分?何来权力与权利的博弈?抛弃自己的立场,替对方考虑,对亲人或许还适用,但对于政府,就成了一种错位——敢问那些为利维坦辩护的人,你时刻为它着想,关键的时候,它会知恩图报地为你着想吗?

哈维尔曾说:国家是人的产物,而人是上帝的产物。
这话,真是一点也没有错。

2006年11月28日

如果要学Shell编程,我推荐一个好的网站:LinuxCommand.org,那里有我看过最清晰的Tutorial。

在那里乱逛的时候,发现两个错误的链接,按照链接的文本,用Google搜出了正确的地址,写了email给站长(奇怪的是我很久没练习英语作文了,居然还算流畅)。

昨天收到回信,说已经修正了,非常感谢提醒。

考研的时候,我曾给参考书的作者(其实应该是编者,也是报考院系的教师)写信指出书中的错误。对方回信把我教育了一通,说我理解有错,死抠细节云云。

适应能力与生活感受,其实是不相干的。

2006年11月23日

早上刚来,就被Tiny抓住了:
“你终于出现了啊”
……

原来是关于正则表达式的问题,要求如下:
某个网站,比如somehost.com,需要用一条正则表达式来过滤所有的子域名,筛选出(匹配)除www和help之外的所有子域名;或者说,匹配类似tiny.somehost.com, foo.somehost.com的域名,而www.somehost.comhelp.somehost.com则不能够匹配。

这个问题,如果用程序是很好解决的,最简单的办法就是分组(grouping),然后判断——当然,不能只用一个正则表达式。

如果要用正则表达式,似乎只能从lookaround(http://www.regular-expressions.info/lookaround.html)入手。
lookahead肯定是不行的,它只能判断子域名的第一个字符,而不能判断之后的字符——换言之,lookahead可以保证子域名的第一个字母后面接的不是www或者help,但是,完整的匹配可能从子域名的第二个字母开始:
(?i)(?!www)\w+\.somehost\.com
用这个正则表达式来匹配www.somehost.com,得到的结果就是
ww.somehost.com
因为第一个w(www)之后的位置(这是一个空位置,Zero-Width Position)之后存在www,而第二个w(www)之后的位置之后(同样是空位置,Zero-Width Position)不存在www;所以www.somehost.com在匹配时无法满足lookaround,但www.somehost.com就能满足。

只能考虑lookbehind。
思路是:保证子域名最后一个字母之后的位置(这也是一个空位置,Zero-Width Position)之前不出现www或者help。
首先我想到的是
(?i)\\b\w+(?<!(www|help))\.somehost\.com
但这显然是不行的,因为目前在lookaround中支持完整表达式的工具只有PowerGrep和.NET(JDK自带的regex package支持有限长度的表达式),而help和www长度不一样。

用(?<!(www|elp))似乎也不行,这样倒是排除了help这个子域名,但也排除了delp(如果有的话)之类的子域名。

既然lookaround检查的是空位置,我于是猜测,可以并列两个lookaround:
(?i)\\b\w+(?<!www)(?<!help)\.somehost\.com
经测试,可行。

不过,这个表达式还有值得改进的地方,它同样会排除wwwwwww.somehost.com,所以,我们需要添加word boundary:
(?i)\\b\w+(?<!\\bwww)(?<!\\bhelp)\.somehost\.com
这是我的最终方案,并且在Python下运行通过。

p.s.
后来tiny告诉我说,这条规则用在lighttpd的rewrite规则中,但他发现了另一个办法:
\w+\.somehost\.com => somehost
www\.somehost\.com => otherhost
只要把www的规则写在后面,就可以达到需要的目的。
Apache, Lighttpd的这种处理机制,比起IIS,有点像SAX与DOM的关系——这或许也解释了,为什么IIS的规则增多以后,效率迅速下降的原因。

Update:
昨天晚上又想了想,其实用lookahead也是可以的:
(?i)\\b(?!www\\b)(?!help\\b)\w+\.somehost\.com

2006年11月15日

Lighttpd(http://www.lighttpd.net)是一个功能丰富的“轻量级”Web Server。根据Jason Hoffman的经验,lighttpd的优点有:

  1. 内建了FastCGI模块,配置FastCGI比Apache2要简单;
  2. 对PHP的支持很好;
  3. 在comox.textdriver这个站点,lighttpd消耗的内存是Apache的1/5,动态调用FastCGI的吞吐量还比Apache稍高一些;
  4. 提供了Apache2不具备的流量控制(traffic shaping)功能;
  5. ……

在lighttpd中,如果我们要限制某个IP的并发连接数目(在应对Spider程序时很有用),可以做在lighttpd.conf文件中做如下修改:
server.modules = (
……
"mod_evasive"
……
);

evasive.max-conns-per-ip = 10

如果要限制流量,可以做如下设置:
#此参数默认值为0,表示无限制
connection.kbytes-per-second = 128
……

$HTTP["host"] == "www.example.org" {
server.kbytes-per-second = 128
}

2006年11月06日

煮玉米+蚕豆爆肉末
读书的时候,冬天下自习以后,经常会跑去买两个煮玉米(当时流行一个笑话:吉林农大的考研题是,玉米和苞米有什么区别)。我总是要等图书馆关门了才离开,不一定每次都能买到嫩的玉米,现在终于可以自己掌握火候了。

冰糖蒸雪梨
秋冬天气干燥,嗓子容易发涩。吃一晚冰糖蒸雪梨,清热润喉,非常有效。