乐者为王

Do one thing, and do it well.

打错multipart引发的血案

浪费几个小时,杀死无数脑细胞,最终发现是单词打错了。不过错有错招,这个问题也让我重新温习了一遍关于form数据编码的知识。

在做上传表单时,一直报告如下错误:

1
undefined method 'original_filename' for "example.csv":String

http://guides.rubyonrails.org/form_helpers.html#what-gets-uploaded 有这样一段文字:

The object in the params hash is an instance of a subclass of IO. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of File backed by a temporary file. In both cases the object will have an original_filename attribute containing the name the file had on the user's computer and a content_type attribute containing the MIME type of the uploaded file.

说明example.csv应该是IO类型的,这里怎么显示是String呢?

表单代码为

1
<%= form_tag import_stocks_path, mutlipart: true do %>

1
<%= form_tag import_stocks_path, method: :post, mutlipart: true do %>

生成后的HTML代码都是:

1
<form action="/stocks/import" method="post" mutlipart="true">

正确的HTML代码应该是:

1
<form action="/stocks/import" method="post" enctype="multipart/form-data">

使用HttpFox工具抓取表单提交信息如下:

1
2
3
4
5
6
POST /stocks/import HTTP/1.1
Host localhost:3000
Referer http://localhost:3000/stocks/import
Connection keep-alive
Content-Type application/x-www-form-urlencoded
Content-Length 112

什么是application/x-www-form-urlencoded?含有file类型字段的表单编码不应该是multipart/form-data吗。

form的enctype属性通常有两种:application/x-www-form-urlencoded和multipart/form-data,默认为前者。当method=get时,浏览器用application/x-www-form-urlencoded编码方式把form数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串附加到url后面,用?分割。当method=post的时候,浏览器把form数据封装到post-body中。如果没有type=file的控件,就用默认的application/x-www-form-urlencoded编码。但是如果有type=file的话,就要用到multipart/form-data了。浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition(form-data或者file),Content-Type(默认为text/plain),name等信息,并加上分割边界(boundary)。

这时才发现原来是把multipart打错成mutlipart了,真是惨痛的教训啊!

如何禁止VirtualBox虚拟机和物理机之间的时间同步

主机是Windows Server 2008,虚拟机Windows XP,VirtualBox的版本为4.3.6。

因为某种原因,需要修改XP系统的时间设置。但在设置后不到10秒钟就又和主机的时间自动同步了。

实时同步时间功能是由Guest Additions提供的,把它卸载就可以修改时间,不过这不是好的解决方法。

翻阅VirtualBox User Manual,找到“Disabling the Guest Additions time synchronization”章节,说明如何能把时间同步给禁止掉。在主机环境执行以下命令:

1
VBoxManage setextradata "YOUR_VM_NAME" "VBoxInternal/Devices/VMMDev/0/Config/GetHostTimeDisabled" 1

YOUR_VM_NAME是你的虚拟机名字,可以通过VBoxManage list vms命令查询到。

还有种方法是修改虚拟机的注册表,把HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\VBoxService项下ImagePath值改为system32\VBoxService.exe --disable-timesync,禁止Guest Additions启动时间同步。修改好后重启虚拟机就可以了。

另外就是虚拟机冷启动时都会和主机时间同步先,这意味着关机再开机的话上次的时间修改就失效了,必须再次手动调整。

Ruby中读二进制文件时大小错误

经常会遇到这类场景,要把文件内容一次性全部读取出来。使用IO.read('example.bin')读取二进制文件时,发现读出来的大小与实际结果不符合。原来默认不加参数时仅限于读文本文件,需要指定mode为b。

1
IO.read('example.bin', { mode: 'rb' })

还有种简洁的读取方式是:

1
File.open('example.bin', 'rb').readlines.join

Octopress 2.0使用技巧

Octopress 2.0带的RDiscount支持表格等Markdown扩展语法了。具体语法看 https://michelf.ca/projects/php-markdown/extra/#table 。不过默认表格是不具有边框的,在显示数据时会很难看。http://programus.github.io/blog/2012/03/07/add-table-data-css-for-octopress/ 修复了这个问题,只是它的修改有些复杂,其实只要把data-table.css的内容粘贴到sass/custom/_styles.scss里就出效果了。

还有就是Octopress中的列表项应该是右移的,实际左移了。可以在sass/custom/_styles.scss添加以下代码解决:

1
2
3
4
5
article {
  ol, ul {
    padding-left: 3em;
  }
}

更简单的办法是把sass/custom/_layout.scss中被注释的这行代码打开:

1
//$indented-lists: true;

列表、表格前要有空行,例如

1
2
3
4
5
6
7
8
9
10
List
* item
* item
* item

Table
column | column
------ | ------
value  | value
value  | value

不会正常显示,必须在List和Table后空一行才行。就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
List

* item
* item
* item

Table

column | column
------ | ------
value  | value
value  | value

如果你已经发表了许多的文章,Octopress站点的生成速度将会非常之慢,解决方式是使用:

1
rake isolate['title']

隔离你所工作的文章(使用此命令前请确保该文章已经存在)。

现在使用以下命令:

1
2
rake generate
rake preview

将会仅工作在被隔离的文章上。

解除文章的隔离可以使用以下命令:

1
rake integrate

如果想在rake new_post/page后使用指定编辑器自动打开生成的文件,可以编辑Rakefile,在Misc Configs段中添加以下代码:

1
2
3
4
5
editor="open"
# open,使用系统默认编辑器
# open -a Mou,使用Mou打开
# open -a Byword,使用Byword打开
# subl,使用Sublime Text2打开

然后在task :new_post和task :new_page的末尾添加如下代码:

1
2
3
if #{editor}
  system "sleep 1; #{editor} #{filename}"
end

在rake preview后自动打开浏览器,也可以编辑Rakefile,在task :preview任务中添加:

1
system "sleep 2; open http://localhost:#{server_port}/"

如果日后Octopress有新版本发布,可以使用以下指令升级:

1
2
3
4
5
git remote add octopress git://github.com/imathis/octopress.git
git pull octopress master  # Get the latest Octopress
bundle install             # Keep gems updated
rake update_source         # update the template's source
rake update_style          # update the template's style

Wordpress迁移到Octopress

GitHub Pages提供免费的静态站点托管服务,有两种类型:User/Organization Pages和Project Pages。前者是用户/组织的主页,一个用户/组织仅有一个;后者是每个项目的主页。

安装Octopress时,rake setup_github_pages会要求输入GitHub Pages项目的URL,根据URL判断是哪种服务。如果是前者,会在新建的_deploy目录上创建master分支,用于存放发布的静态文件,并把原master分支改为source分支;后者则仅在_deploy目录上创建gh-pages分支。

Octopress默认的permalink是/blog/:year/:month/:day/:title/,想把它改成/:title/这种格式。修改后,发现生成的文章都跑到了public目录下,默认应该是在public/blog目录里的。为保持目录的简洁,把它改成了/blog/:title/,这样虽然生成的文章没有按年月日分目录,但是用户在访问系列文章时就可以只修改相应数字,不然必须得记住几乎不可能知道的文章的发布年月日。

至于评论的迁移,不想费过多的手脚,直接使用Octopress自带的Disqus。在Wordpress中安装好Disqus插件,通过插件设置将现有的评论内容导入Disqus。不过Disqus处理导入数据的时间有点长,需要等待一段时间,可以在 http://import.disqus.com 查看导入进度。导入完成后,把Wordpress文章的permalink改成和Octopress相同,登录Disqus后台,找到Migrate Threads栏目,在Domain Migration Wizard里把旧域名改成新域名,然后一路Next就大功告成了。

不过在实践中碰到了问题,打开某篇有评论的文章,发现只有评论框,没有评论。搞不明白是怎么回事。后来受到 http://www.ducea.com/2012/11/12/disqus-comments-not-visible-in-octopress/ 的启发,原来是_config.yml中的URL还是生成项目时的,与CNAME中配置的域名不一致,修改后就没问题了。

在侧边栏显示最新评论也很简单,添加recent_comments.html到source/_includes/custom/asides目录下,在_config.yml的default_asides做好配置,最新评论内容就会出现在侧边栏中了:

1
2
3
4
5
6
<section>
  <h1>Recent Comments</h1>
  <div class="dsq-widget">
    <script type="text/javascript" src="http://codemany.disqus.com/recent_comments_widget.js"></script>
  </div>
</section>

在侧边栏显示Tag Cloud参照了 http://codemacro.com/2012/07/18/add-tag-to-octopress/ 教程,没几分钟就搞定了。

分享使用的是JiaThis,只要把source/_includes/post/sharing.html中的代码替换成以下代码就可以:

1
2
3
4
5
6
7
8
9
10
<div id="jiathis_style_32x32">
  <a class="jiathis_button_qzone"></a>
  <a class="jiathis_button_tsina"></a>
  <a class="jiathis_button_tqq"></a>
  <a class="jiathis_button_weixin"></a>
  <a class="jiathis_button_renren"></a>
  <a href="http://www.jiathis.com/share/" class="jiathis jiathis_txt jtico jtico_jiathis" target="_blank"></a>
  <a class="jiathis_counter_style"></a>
</div>
<script type="text/javascript" src="http://v2.jiathis.com/code/jia.js" charset="utf-8"></script>

相关文章本想用Octopress自带的lsi模块实现,只是当文章较多时生成速度实在是巨慢,按照推荐安装gsl模块,结果在Windows Server 2008系统上死活装不上,采用手动编译方式也总是死在rb-gsl的安装上。失望之余找到 https://github.com/LawrenceWoodman/related_posts-jekyll_plugin 这个插件,它使用起来非常简单,只需将related_posts.rb放到自己的plugins文件夹中,然后在source/includes/post中新建relatedposts.html文件:

1
2
3
4
5
6
7
8
9
10
{ % if site.related_posts % }
  <h3>Related posts</h3>
  <ul class="posts">
  { % for post in site.related_posts limit:3 % }
    <li class="related">
      <a href=""></a>
    </li>
  { % endfor % }
  </ul>
{ % endif % }

修改source/_layouts/post.html,在

1
2
3
{ % include post/author.html % }
{ % include post/date.html % }{ % if updated % }{ % else % }{ % endif % }
{ % include post/categories.html % }

后面添加

1
{ % include post/related_posts.html % }

就可以了。使用这个插件会有个小问题,就是它和Jekyll 2.1不兼容,rake generate时会报错,可以使用jumanji27提供的fork版本

在Wordpress的Tools/Export页面选择导出文章内容,保存为wordpress.xml文件。然后使用工具把它转换成markdown格式。这里使用了YORKXIN的修改版本,将脚本和wordpress.xml放到Octopress根目录下,然后运行:

1
ruby -r ./wordpressdotcom.rb -e Jekyll::WordpressDotCom.process

会把转换好的文章都放到source/_posts目录下,文件后缀名是html,直接改成markdown就是。

最后就是苦力活:修改文中的站内链接、上传的图片路径以及代码高亮语法等。

Nokogiri抓取页面URL含有中文参数的问题

使用Nokogiri抓取某网站的长江现货数据,被抓取页面的URL中含有中文参数,使用以下的代码抓取数据失败:

1
2
url = 'http://example.com/search.asp?type=长江有色&sort=asc'
doc = Nokogiri::HTML(open(url))

http://dingr.iteye.com/blog/647244 讲是因为浏览器给服务器发送参数的时候是经过编码的,按照该文的意思试着也给URL里的中文编了下码:

1
2
3
url = 'http://example.com/search.asp?type=长江有色&sort=asc'
url = URI.escape(url)
doc = Nokogiri::HTML(open(url))

结果还是抓取失败。查看URL的编码信息:

1
puts url.encoding  # 输出utf-8

网站页面采用的是GB2132编码,猜测网站后台处理数据时很有可能也是采用的GB2132。做个实验就清楚了,将URL转成GB2132后再编码:

1
2
3
4
url = 'http://example.com/search.asp?type=长江有色&sort=asc'
url = url.encode('gbk', 'utf-8')
url = URI.escape(url)
doc = Nokogiri::HTML(open(url))

发现果然OK了。

逻辑题-几条病狗

村子中有50个人,每人有一条狗,在这50条狗中有病狗(这种病不会传染),于是人们就要找出病狗,每个人可以观察其它49条狗,以判断它们是否生病,只有自己的狗不能看。观察后得到的结果不得交流,也不能通知病狗的主人。主人一旦推算出自己家的是病狗就要枪毙自己的狗,而且每个人只有权利枪毙自己的狗,没有权利打死其他人的狗。第一天,第二天都没有枪响,到了第三天传来一阵枪响,问有几条病狗?

推论:

A、假设有1条病狗,病狗的主人会看到其它狗都没有病,那么就知道自己的狗有病,所以第一天晚上就会有枪响。因为没有枪响,说明病狗数大于1。

B、假设有2条病狗,病狗的主人会看到有1条病狗,因为第一天没有听到枪响,是病狗数大于1,所以病狗的主人会知道自己的狗是病狗,因而第二天会有枪响。既然第二天也没有枪响,说明病狗数大于2。

由此推理,如果第三天枪响,则有3条病狗,第n天枪响,则有n条病狗。

使用Pry调试Rails应用

调试Rails程序有更好的工具了,这就是Pry,它是一套全新的IRB替代方案,最闪亮点是它的语法高亮功能。

首先,在Gemfile中添加以下代码:

1
gem 'pry', group: :development

然后执行

1
bundle install

在需要设置断点的地方添加binding.pry,当程序运行到这行代码时会打开一个窗口,可以在这里操作当前代码的上下文变量。

1
2
3
4
def index
  @articles = Article.all
  binding.pry
end

退出调试用exit-all,但如果在循环中会遭遇失败,这时可以使用exit-program无条件地退出。

1
2
3
4
(1..100).each do |i|
  binding.pry
  puts i
end

Pry默认是没有调试中经常用到的上一步,下一步等命令的,可以安装pry-nav,然后就可以使用step, next, continue跳来跳去了。

在Raspberry Pi上使用无线网卡

使用的是EDUP EP-N1556这款无线网卡,体积小巧,支持300M速率,买回来插在RPi上直接就能识别了。

输入lsusb命令,可以见到设备列表中有RTL8192CU 802.11n WLAN Adapter字样,说明网卡已经被系统识别,芯片是RTL8192。

下面就给无线网卡设置静态IP地址:

修改/etc/wpa_supplicant/wpa_supplicant.conf

1
2
3
4
5
6
7
8
9
10
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
  ssid="NETGEAR"
  psk="12345678"
  proto=RSN  # 也可以写成WPA2
  key_mgmt=WPA-PSK  # 一定不要写成WPA2-PSK
  id_str="edup1556"
}

修改/etc/network/interfaces

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
auto lo

iface lo inet loopback

iface eth0 inet static
  address 192.168.0.100
  netmask 255.255.255.0
  gateway 192.168.0.1
  dns-nameservers 8.8.8.8

allow-hotplug wlan0
iface wlan0 inet manual
  wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface edup1556 inet static
  address 192.168.0.200
  netmask 255.255.255.0
  gateway 192.168.0.1
  dns-nameservers 8.8.8.8

iface default inet dhcp

然后用ifconfig命令列出所有的网络设备,可以看到wlan0的IP是192.168.0.200了。

你的树莓派自由了,不再需要网线拖着了。

我是如何让Ruby脚本速度提升156倍的

以前写过使用Ruby批量修改繁体文件名为简体,可惜脚本的性能很有问题,批量重命名时运行速度非常慢。这次准备优化下代码,提升脚本的执行效率。

profile.rb是为Ruby程序准备的profiler,它可以统计并输出各方法的运行时间,以便于找到程序执行的性能瓶颈。这次就用它来剖析脚本的运行时间。使用方法很简单,加上命令行选项-r profile就可以:

1
ruby -r profile rename.rb

运行结束后,会把统计信息输出到标准错误输出中。如下图所示:

profile-before-tuning

profile统计的是各方法的运行时间,分为两类。一类是计算的是从方法调用到方法返回之间的时间,称为整体时间;另一类则是从整体时间中扣除在该方法中调用其它方法所耗费时间之后得到的时间,称为实际时间。

上图输出信息每行中各字段含义如下(从左到右):

  • 该方法执行时间占整体时间的百分比,比例越高越说明这行代码可能需要优化
  • 整体时间的总和
  • 实际时间的总和
  • 被调用的次数
  • 每次调用的平均实际时间(毫秒)
  • 每次调用的平均整体时间(毫秒)
  • 方法名

由上图可以看出,脚本执行的时间大部分耗在了循环上。解决方法有两个:消除循环或减少循环次数。前者很难实现,暂且还没有想到办法,也许根本就没有可能。脚本中mapping的大小为2685,所以每修改一个文件名需要执行2685次循环,且循环中的encode和gsub!都是耗时操作。通常文件名的长度不超过30个字符,通过遍历文件名中每个字符的方式重命名就可以把循环次数缩减到不超过30次。

修改代码后重新执行分析命令,得到的结果是脚本运行时间从379395秒变成2418秒,性能整整提升了156倍,达到2个数量级的效果。

profile-after-tuning