20160329

Debug一例,Wordpress 插件 Blogger Importer Extended 导入 blogger 图片问题


1. 背景和问题

2月25日至29日,约12小时,中间有预计会有长时间导入的过程,享受咖啡和小说。

起初,高博先生微信我,向我提供博客空间。他鼓励我,这样爱写作的人,应该坚持下去。我申请了域名,高博先生提供空间并架设wordpress已毕。既然准备长时间驻守,我就打算把以前在 blogger、CSDN、短乎 上的博客全导过来,计1478篇。

问题就是从导入 blogger 开始的。我和高博先生都发现,有些贴子的图片不见了。

最先确认的是,一部分不见的图片,是因为我从知乎贴过来,如果我作为知道用户登录了知乎,那么我是可以在导入后的文章中看到图片的;而高博先生及其他人就看不到。我以前从知乎向CSDN转贴的时候,还想过,知乎真是大方有礼的网站啊,居然可以直接盗链。恩,似乎也不是。如果盗链的时候就告知还好,竟然不声不响么……这部分图片我手动一张张重新上传过,OK了。

但是仍然有几张图片显示不出来。在网上搜索了一下,有人提到 (大意) ,"图片都下载到wordpress服务器了,在文件系统中找到了,但是在博文中不显示"。似乎正是这样啊。

跟着讨论走下去,发现导入插件的作者9个月前说打算解决,然后就没了消息。下面一大堆回贴,"我也是这个毛病啊"。

高博先生说,用正则表达式容易定位出哪些图片有问题,不过如果找到bug,方便大家就更好了。他提到用正则表达式的时候,我心说惭愧,竟然没有第一时间想到;当他说方便大家的时候,我觉得"压出皮袍里的小来了"。

见贤思齐,跟踪一下这个bug在哪里吧。于是动手,12个小时,及这篇贴子。

我用的wordpress插件,实现 blogger -> wordpress 的,是这个:

Blogger Importer Extended Migrates your Blogger blog to WordPress.  Version 1.3 | By Yuri Farina

2. 故障重现,实验床: 受控,孤立

得先找到bug,然后才能改bug,而找bug,又需要先重现bug的现象。这是个简单而直接的道理,不过不少同学似乎不怎么相信和实践呢。我经常听到这样的问题,"老师你说我看啥书才能编程好呢",这类似于"你说我吃点啥能减肥"。

万军丛中定位将军,是件困难的事,杀了他,通常不过是一刀的事。

故障重现,要求能一次又一次,在相同的条件下,触发相同的现象。而这个现象,就是你希望它消失的那个。要给药退烧,必先测体温。你猜大约是发烧了吧,猜大约是肺部感染,猜大约哈希表比数组来得快,都是猜,所有的猜测,无论是否有理论支持,在实验数据面前都是猜瞎。不少同学上来就改改这看看吧,改改那看看吧。然后某次现象消失了,他就误认为问题解决了。这叫做work around (绕过问题),不是解决。

故障重现有时需要1.特别长的时间,或者2.特别宝贵的资源,或者3.生产环境不允许动作,或者4.代码特别长特别复杂,这时需要 实验床。实验床有利于在*受控*的环境下触发特定的条件,从而导致特定的现象,方便重复。实验床有利于孤立问题使之与其他复杂的环境和因素解耦。

如果能证实实验床与生产环境是一致的,那么bug的触发条件、现象也会是一致的。实验床就成为生产环境的一个模型。在模型中思考和操作,是重要的科学、技术和工程手段。

这次找bug架设实验床,我注意了以下两点。

2.1 先本地测试,再更新到远程,xampp

重现故障现象,而且不知道是哪些图片 (更重要的是在何种条件下) 有问题,最朴素的做法是从 blogger 到 wordpress 同步实验,一次又一次。每次实验改变单一的因素,看看故障是否再现/消失。

从 blogger 到 高博先生提供的wordpress,速度也没有多快,而且我担心他的流量和带宽占用。

所以实验床应该架设在本地。

我在我自己的PC机上架设了一个 xampp,装上 wordpress,从 blogger 到我本地的 wordpress 同步。这样,我至少不用担心高博先生在哪个时区,是否方便打扰,何时可以响应我的请求重启服务器、清空数据库、重置 wordpress。等我本地找到bug修改完,再在远程的服务器上修改。

我不止一次对编程的伙伴们说,把手册下载安装到本地,但是网断的时候他们还是会说,"网断了查不出手册啊,编不了程序了啊。"我想说,没有人可以依靠,除了你自己;成为可以被别人依靠的人。

2.2 小规模实验,确保触发故障现象快速精确

同步所有的博客文章,才能触发问题。但是执行一次同步,需要耗费两三个小时。有些动作,并不需要触发问题,所以同步的不必是博客文章中的全部。这些动作包括不限于,1.下面会提到的加入日志记录语句 (并测试是否生效),2.猜测某个因素是造成问题的原因,修改这个因素以后测试效果。

所以实验床应该短小精悍。

我注册了一个新的 blogger 站点,发一两个贴子,专用于 wordpress 拉取。这样能确保触发故障现象的因素 快速 而 精确。想调校哪个参数立即就可以测试,不必等到一千多篇博文同步的两三个小时。而且更容易确保造成现象的原因就是某个因素,而不是整个庞大项目中的某个你无意中设置的变量。

我就在这个新的 blogger 站点上发贴子,粘图片,每次同步几秒钟。

架设实验床的时间,考虑实验床如何架设的时间,这些都是值得的,甚至单轮测试所花费的时间缩短的效益就值回来了。

3. 故障定位,日志: 受控,易于追溯

有了实验床,就可以罗列可能造成问题的因素A,B,C,D ,然后设计实验,一一验证或证否。所以,故障定位,是这样的技术手段,在受控 (各种因素)条件下,重复过程,以检验假设。

3.1 跟踪手段选择,error_log,可长期留存的 vs. 瞬间消失的

重现故障,初学者希望立即、马上、直接看到效果,铛地一声弹出个对话框,告诉他出错了。

人类不同于动物的一个重要特征,是可以理性地推迟期待的效果。然后成批次处理。日志输出输出输出输出,然后分析分析分析分析,而不是 输出分析输出分析输出分析。在操作系统进程调度算法中,效率最高的,是成批处理,不分时,不中断,不切换进程。学习和工作效率最高的做法,是集中注意力,完成一件工件,然后再开启新的任务。

日志输出、TDD、自动化测试、脚本,甚至程序本身,都遵循了这样的原则。

用日志跟踪,而不是实时显示,能避免不少问题。比如,工程师借口说,"当时我操作啥我也不记得了,反正故障消失了,等以后再出现再说吧。"你可以一次又一次分析日志,条件、现象都在日志中记录中,而不必非得再开启一次实验才能回想起来。"我输入什么这个现象才出现来着,唉呀状态不好,忘了。"工程方法让每一个人都不必非得是天才,也能做好工作。

我选择 php 本身内置的 error_log,日志写到error.log中,而不是另打开一个文件写进去。如无必要,勿增实体。紧着现成的用。

3.2 读代码框架,了解流程,确保日志可以被触发

接下来我大致读了 wordpress (之前粗读过一本书和 wordpress 手册)代码,然后沿着这样的顺序找到准备设置日志记录的代码行:

wordpress -> blogger-importer-extended插件-> importer.php -> 函数-> 代码行

设置位置的原则,跟猎人设置陷阱的原则是一样的,1.要确保目标猎物会经过此处,2.目标猎物会触发陷阱,3.当触发时能够把猎物抓住。

就这个案例而言,对应的是,1.跑每次实验流程的时候,希望执行 发现图片、下载图片、改博文中图片的地址 这些操作的时候,能够记录这些动作,成功的和失败的。2.失败的图片,那些图片下载了,但是博文中的地址指向却没有变更的,一定要记录下来。不希望跑了一次实验,有失败的现象出现,但是却没有捕捉到。3.当触发了日志记录动作以后,希望把准备用于分析的信息都记录下来。不希望跑了一次实验,现象也发生了,也记录下来现象发生了,但是发生的现象以及此刻变量的现象没有记录。

3.3 输出的字段,0.易于找到,1.触发条件,2.assert

记录哪些字段呢?我想的包括: 0.易于找到,1.触发条件,2.期待的,实际的

第一,易于找到,要容易在很多行日志里找到,所以加上明显的分隔线和换行。

在Linux下,我就开个终端,tail -f error.log,完成一次实验,往上翻到刚刚开始实验时候,往下读。这次是windwos,我记一下实验开始的时间,然后在日志里找,或者在每次实验时在日志里写个明显标记,从结尾往上,最后一个标记就是本次实验的开始。

第二,要记录触发的条件

分析的时候,找到出故障的那条,看何种条件下会触发。这就是程序设计语言里的" if ( condition ) "里的condition。如果每当这个条件都会导致故障,就可以怀疑这个条件可能就是原因。

需要注意的是,因为找到bug以前还不知道触发的条件,所以尽可能多记一些怀疑的条件 (在眼花缭乱以前),每次实验就能多分析一些因素。

我按下面这样日志。

error_log("-foreach post----\n"
  .  print_r($post->post_date,true)."\n"
  .  print_r($post->post_title,true) . "\n"
  .  print_r($found_images, true), 0);

这一条记录了触发日志记录 (不是触发故障,那需要根据日志人工判断)的条件foreach post,遍历每篇博文的时候;记录了博文的日期 post_date,博文的标题 post_title,图片的某个信息 (路径?名字?很多图片的名字拼接在一起?)。

第三,你期待会是某个值的变量,如果它值得怀疑,记录它。对比你期待的值和实践发生的值,如果有差异,那就是问题的原因。这个变量,既可能是条件中的,也可能是参与计算和输出的。

在这里 $found_images 就是值得怀疑的,我打印了不少次这个变量,在不同的位置。C++开发原则之一,assert你认为本该如此的那些量。这时我做的差不多根据同一原理。

3.4 日志分析,触发事件的特征

接下来就是人力工程了,一行行读代码,找到故障发生 (我们记录了进入那样的分支,比如那些成功的操作以外的else)的地方,看触发故障时 各个因素 (条件)都是什么样的。

故障发生的条件,是不是有规律?每当这个时候我就想起包师弟,归纳能力超强,能从乱七八糟毫无规律的现象中发现模式。恩,他的模式匹配算法可能比较好,很多智商测试题都是考察归纳能力的。

这次,我独立找到了模式。中文。

如果文件名是中文,比如"鹦鹉螺",那么就会发生故障。一共15张图片符合条件。

进而发现,所有文件名是中文的,都发生了故障;所有文件名是英文的,都没有故障;所有没有故障的,都是英文文件名;所有发生故障的,都是文件名中文。

4. 解决

万军丛中取上将首级。万军丛中找到上将很花时间,此次约10小时。斩杀上将,只是一刀,约几秒钟。验证上将确实死透了,需要的时间稍微长一些,先实验床,然后生产环境。

一共只改了一行,如下,正是中文文件名容易出现的问题。原来的代码,在文件系统中和在博文的 img src 中,分别 urlencode 和 没有urlencode,所以当查找那个文件时,按期待的名字,它不存在。

下面的代码,注释了的那行,就是bug所在,注释那行的下面,是我修改的结果。

wordpress\wp-content\blogger-importer-extended\includes\importer.php:
if(!is_wp_error($image)) {
  $attachment = wp_get_attachment_image_src($image, 'large');
//$content = str_replace($found_image, $attachment[0], $content);
  $content = str_replace($found_image, dirname($attachment[0]).'/'.urlencode(basename($attachment[0])), $content);

5. 修改工具,还是修改产品;一般性的程度

我没有修修改插件的代码,仅仅测试通过。我手动修改了在高博先生那的wordpress里的15篇博文。我选择的是修改产品,而不是工具。

鉴于出现这个问题的,可能主要都是能读懂中文的,所以我写了这篇博文,而不是修改插件再发布。

修改工具能更一般性地解决问题,更大范围地产生效益。我惭愧,还是没有高博先生那么有高度和乐于助人。

5. 感谢

感谢高博先生的空间和时间。

感谢winguse先生提供链路,使我得以访问 blogger。blogger,就是 google 的 blogspot,如果不能访问到的话,我没法授权 google 允许导出,也不能导到我的本地测试PC机中。

在跟高博先生和winguse先生讨论的时候,我都很享受,高效率地交流使用条款和技术路线。三言两语可决,毫不罗嗦。

感谢叶卡编辑让我认识高博先生,及那么多精彩的人。

感谢 Yuri Farina 开发 Blogger Importer Extended 插件,使我得以取回自2007年2月以来的历史博文。

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

博客会手工同步到以下地址:

[http://zhuanlan.zhihu.com/younggift]

[http://younggift.net/]

[http://blog.csdn.net/younggift]

[http://giftdotyoung.blogspot.com]