2009年11月19日星期四

eD2k Hash in Ruby

很久以来一直都以为eD2k Hash就是MD4(罪魁祸首),这几天才发现错得很离谱。

花了点时间查资料,写了个用以生成eD2k链接的模块,包括AICH部分。代码有个偷懒的地方,读取文件的缓存大小必须为PART_SIZE,如果要拿去用的话请留意。Ruby 1.8.6和1.8.7的File.size在Windows平台下有bug,当文件尺寸比较大时会返回负数,这一点也请留意。

部分代码移植自pyaich和Python的Base64模块,GPL许可证。

require 'openssl'

module OtNtH
  module Digest
    # fit for eD2k Hash
    PART_SIZE = 9_728_000
    BLOCK_SIZE = 184_320

    class ED2K
      def initialize
        @h = ''
        @l = 0
      end
      
      def update(part)
        @h += OpenSSL::Digest::MD4.new(part).digest
        @l = part.size
      end
      
      def hexdigest
        @h += OpenSSL::Digest::MD4.new('').digest if @l == PART_SIZE
        OpenSSL::Digest::MD4.new(@h).hexdigest
      end
      
      def name
        'ED2K'
      end
    end # class ED2K

    class AICH
      class HashTree
        attr_accessor :dataSize, :isLeft, :baseSize, :hash, :leftTree, :rightTree
        
        def initialize(dataSize, isLeftBranch, baseSize)
          @dataSize = dataSize
          @isLeft = isLeftBranch
          @baseSize = baseSize
          @hash = nil
          @leftTree = nil
          @rightTree = nil
        end
        
        def loadLowestLevelHashes(hashList)
          if @dataSize <= @baseSize then
            @hash = hashList.shift
            return true
          else
            extra = @dataSize % @baseSize != 0 ? 1 : 0
            numBlocks = @dataSize / @baseSize + extra
            extra = isLeft ? 1 : 0
            sizeLeft = ((numBlocks + extra)/2) * @baseSize
            sizeRight = @dataSize - sizeLeft
            leftBaseSize = sizeLeft <= PART_SIZE ? BLOCK_SIZE : PART_SIZE
            @leftTree = HashTree.new(sizeLeft, true, leftBaseSize)
            
            rightBaseSize = sizeRight <= PART_SIZE ? BLOCK_SIZE : PART_SIZE
            @rightTree = HashTree.new(sizeRight, false, rightBaseSize)
            return @leftTree.loadLowestLevelHashes(hashList) && @rightTree.loadLowestLevelHashes(hashList)
          end
        end
        
        def reCalculateHash
          if @leftTree and @rightTree then
            return false if (not @leftTree.reCalculateHash) or (not @rightTree.reCalculateHash)
            h = OpenSSL::Digest::SHA1.new
            h.update(@leftTree.hash)
            h.update(@rightTree.hash)
            @hash = h.digest
            return true
          else
            return true
          end
        end
      end
      
      def initialize
        @hash_list = []
        @data_size = 0
      end
      
      def update(part)
        psize = part.size
        @data_size += psize
        pos = 0
        while pos < psize
          @hash_list.push OpenSSL::Digest::SHA1.new(part[pos, BLOCK_SIZE]).digest
          pos += BLOCK_SIZE
        end
      end
      
      def hexdigest
        blockSize = @data_size <= PART_SIZE ? BLOCK_SIZE : PART_SIZE
        tree = HashTree.new(@data_size, true, blockSize)
        tree.loadLowestLevelHashes(@hash_list)
        tree.reCalculateHash
        return Base32.encode32(tree.hash)
      end
      
      def name
        'ED2K'
      end
    end # class AICH
  end # module Digest
  
  module Base32
    B32TAB = 'abcdefghijklmnopqrstuvwxyz23456789'
    module_function
    
    def encode32(str)
      parts = []
      quanta, leftover = str.size.divmod(5)
      if leftover != 0 then
        str += ("\0" * (5 - leftover))
        quanta += 1
      end
      quanta.times do |i|
        c1, c2, c3 = str[i*5..(i+1)*5].unpack('nnC')
        c2 += (c1 & 1) << 16
        c3 += (c2 & 3) << 8
        parts.push(
          B32TAB[c1 >> 11],
          B32TAB[(c1 >> 6) & 0x1f],
          B32TAB[(c1 >> 1) & 0x1f],
          B32TAB[c2 >> 12],
          B32TAB[(c2 >> 7) & 0x1f],
          B32TAB[(c2 >> 2) & 0x1f],
          B32TAB[c3 >> 5],
          B32TAB[c3 & 0x1f]
        )
      end
      encoded = parts.join('')
      case leftover
      when 1
        encoded[0..-6] + '====='
      when 2
        encoded[0..-4] + '===='
      when 3
        encoded[0..-3] + '==='
      when 4
        encoded[0..-1] + '='
      else
        encoded
      end
    end
  end # module Base32
end # module OtNtH

if __FILE__ == $0 then
  exit if not File.exists?(ARGV[0])
  a = OtNtH::Digest::AICH.new
  e = OtNtH::Digest::ED2K.new
  File.open(ARGV[0], 'rb') do |f|
    buf = ''
    while f.read(OtNtH::Digest::PART_SIZE, buf)
      a.update(buf)
      e.update(buf)
    end
  end
  puts "ed2k://|file|#{File.basename(ARGV[0])}|#{File.size(ARGV[0])}|#{e.hexdigest}|h=#{a.hexdigest}|/"
end

2009年9月19日星期六

小改ImageZ

从网上抓图,Imagez是个很不错的程序。可是在该程序在选择保存目录的时候,无法设置起始目录,也不能直接输入已存在路径,只能从我的电脑开始一级一级点。虽然官网上说0.7Final会提供该功能,可一年半后的现在版本号都0.82了却仍然没有兑现,说不得只好自己想办法了。

选择目录的API函数是SHBrowseForFolder,通过LPBROWSEINFO的pidRoot和ulFlags两个成员可以实现我上面说到功能。pidRoot没办法,ulFlags只需要加上BIF_EDITBOX即可,这倒是可以动动脑筋。

用UPX解压后再用FileInfo查看可得知,ImageZ是用VB6写的。而VB6调用外部API都是动态载入DLL,用W32DASM静态分析会比较困难,所以还是请出大神OllyDbg。

在OllyDbg中打开ImageZ.exe,F9执行。Alt+E,找到shell32双击。Ctrl+N,找到SHBrowseForFolderA,F2下断点。切换到ImageZ窗口,点击保存按钮,跳回OllyDbg。按Alt+F9执行到用户代码,再切回ImageZ取消掉选择目录的窗口。

0042AE69    894D BC         mov dword ptr ss:[ebp-44],ecx
0042AE6C    C745 CC 4100000>mov dword ptr ss:[ebp-34],41
0042AE73    E8 20A8FEFF     call ImageZ.00415698
0042AE78    8B1D B0104000   mov ebx,dword ptr ds:[<&MSVBVM60.__vbaSetSy>; MSVBVM60.__vbaSetSystemError
0042AE7E    8BF8            mov edi,eax

0042AE6C这一行就是我们要找的东西,其含义为将ulFlags设置为41。光标移动到这一行,按空格,将41改为51。鼠标右键,“复制到可执行文件”,在弹出的窗口中右键,保存。

好久好久没弄类似的东西,手好生的说。

Update 18:03 2009-11-3

ImageZ 0.82p2中改用GetOpenFileName选择文件夹,虽然提供了设置初始目录的功能,但仍然不允许手工输入目录,也不能记忆上次选择的路径,真不知道作者究竟是怎么想的。

定制GetOpenFileName对话框的外观无法通过修改OPENFILENAME结构体的成员来实现,必须要挂一个消息钩子处理WM_INITDIALOG,向需要隐藏或修改内容的控件发送CDM_HIDECONTROL、CDM_SETCONTROLTEXT等消息,所以断点需要下在SendMessage上。

大概的修改过程都跟上次的差不多,以下是我找到的代码:

0045B6AE   /0F85 28010000   jnz ImageZ.0045B7DC
0045B6B4   |8D4D A0         lea ecx,dword ptr ss:[ebp-60]
0045B6B7   |895D A0         mov dword ptr ss:[ebp-60],ebx
0045B6BA   |51              push ecx
0045B6BB   |68 7C040000     push 47C
0045B6C0   |68 69040000     push 469
0045B6C5   |57              push edi
0045B6C6   |E8 5565FBFF     call ImageZ.00411C20
0045B6CB   |FFD6            call esi

CDM_HIDECONTROL的值为0x469,所以0045B6C0这一行就是我们要找的东西了,将该行以及其下共5行push 469改为push 0即可。

2009年9月18日星期五

ActiveScriptRuby和RubyInstaller的兼容性问题

近来一直在用Luis做的RubyInstaller,这个版本的Ruby用Mingw编译,比VC6编译的快了很多。而且Mingw和VC6编译出来的东西都依赖msvcrt.dll,也算解决了Ruby极端讨人厌的DLL文件名问题。

可是RubyInstaller却跟Arton写的ActiveScriptRuby有兼容性问题,regsvr32无法注册库文件,提示“找不到指定的程序”。

用dumpbin检查了一下,Arton发布的ActiveScriptRuby从msvcrt-ruby191.dll中导入了一个vsnprintf函数,但该函数在Mingw编译的msvcrt-ruby191.dll中并不存在。

抓来ActiveScriptRuby的代码,将win32oleex.cpp第568行的vsnprintf改为_vsnprintf,使用VC6版本的include和lib文件编译通过。

写信给Arton报告了这个问题,下一个版本的ASR应该就不需要自己再改一次了。

2009年8月15日星期六

Tumblr vs Publr

现在才写似乎有些Out了,不过我的确是最近才开始用类似的东西。因为就是用来贴图而已,所以我只关注了这两家网站关于贴图的相关功能。

因为墙的关系,最先开始用的是Publr。本以为作为Tumblr的后来者,功能方面应该较弱才对,结果用了一段时间发现事实并非如此。Publr可以自定义Theme,网站提供的三款也都是设计优良的作品。发布照片的界面也很好用,能够一次选择多张图片,并且在上传的时候去写文字内容。基本上我介意的核心功能,Publr都能很好地提供。

要说缺点,不能设置Post发布时间算一个,无法调整Photoset顺序也算一个,不过都还可以忍受就是。真正让我起意尝试其他相似服务的原因并不在于功能,而是出于对数据安全性的担心。

Tumblelog,看名字就知道,这一类网站Tumblr肯定才是老大,Publr是否能找到自己的位置?上线有一年了吧,我只用了月余就抓出了三个Bug,用户少到没人发现?API至今还很不完整,站方想要添加的SNS功能也进展缓慢,种种迹象让我觉得至少要找个备份的地方才算保险。

于是我慕名翻墙来到了Tumblr,可是却有那么点失望。Tumblr的功能的确较Publr丰富,创建多个Blog、SNS,等等等等。可是我看了官方网站才知道,发布Photoset的功能七月才上线,无论可用性还是易用性都有不小的缺陷。

Tumblr将单张的Photo和多张的Photoset分成了两种不同的Content类型,假如第一次发布时只选择了一张照片,Post类型即为Photo,以后修改也不能再添加照片并转化为Photoset了。呈现方面,如浏览器安装了Flash插件则Photoset会被显示为一个Flash相册,否则就是所有大图的堆叠,相比Publr点击略所图用Javascript放大的方式差了不少。另外Tumblr的Photoset还有最多十张的限制,上传时必须逐张进行选择,用起来实在有些不太舒服。

综合来看,仍然是Publr更为契合我的需求。至于备份,写了一个Ruby脚本将Photo类型的Content从Publr同步至Tumblr,而Photoset就只能手工处理了。

2009年8月6日星期四

利用GAE实现Twitter短信通知

简介

随着国内Twitter Clone集体被自杀,利用叽歪绑定飞信,通过手机短信获取Twitter更新的法子就失效了,于是我再一次开始折腾。

这次用到的东西包括cocobear写的PyFetion、Yahoo Pipes和Google App Engine。基本思路就是利用GAE的Cron功能定时检查Yahoo Pipes抓取的RSS,如果有新的东西就用PyFetion给自己的手机发短信。

除了Twitter,这个程序可以用来做的事情挺多,比如接收实时天气预报之类,各位可以自由发挥。

安装及使用说明

由于PyFetion、FeedParser等第三方库都采用GPL许可证发布,所以我的代码也使用GPL v2许可证,使用及再发布请留意相关限制。

代码放到Github上了,下载请走这里,点击download即可获得最新源文件的压缩包。

我并没有为本程序单独建立一个代码仓库,目前还整合了杜晓刚GAppProxy等其他程序。用得上的话就不用为本程序单独建立一个GAE了,不然的话放在那里也成,并不影响使用。

下载及解压后需要修改的文件有两个。app.yaml中第一行“application: ogaeo”,将ogaeo改为你的GAE项目名称。另外则是Conf.py中RSS2Fetion的相关配置信息。

默认的RSS只抓取Twitte的replies和direct messages,应该不至于太烦人。据说用飞信给自己发短信有每天600条、每月1000条的限制,如需要自定义抓取来源请留意。

我将Cron任务设置为每5分钟执行一次,实际延迟约在5到10分钟的样子。如需要更改,请查阅cron.yaml文件。

需要GAE申请及上传帮助,请查阅GAppProxy帮助文档。使用中出现问题,请在本页留言。

使用本程序会造成飞信PC及手机客户端掉线,如果你经常使用飞信聊天并且只有一个移动的号码,本程序并不适合你。

如果希望临时关闭本程序,可以采用短信修改飞信密码的办法来实现。编辑短信,内容为新密码,发送至12520050。详情请查阅飞信帮助说明

更新历史

Update 22:36 2009-8-11:

本想偷懒直接用memcache,可是发现memcache无法提供长时间的缓存,所以还是得用数据库。

另外还发现GAE一个问题,pickle反序列化似乎得这样用:pickle.loads(str(dbcache.txt))。

Update 23:56 2009-8-13:

用pickle序列化数据并保存至数据库仍然会有重复发送的问题,而且两次重复发送都在凌晨6点多。找不到问题出在哪里,于是再次修改缓存机制,将RSS永久保存在数据库中。

一定要记住两件事:一、TextProperty不可查询,只能用StringProperty;二、看文档需要仔细些,再仔细些……

Update 14:43 2009-8-17

经过上次修改后没有再出现重复发送短信的情况,问题应该是解决了吧,只是出错的原因还不太确定。

嫌疑集中在Model(Query、GqlQuery)类的get方法上。按照文档的说法,query.get()应该返回数据库中符合查询条件的第一条纪录,没有则返回None。可是用query.get() == None进行条件判断时就会出现重复发送的情况,改用query.count() == 0时则不会。初步判断,有可能是get方法在发生错误(如超时)时,并没有抛出异常所致。

Update 22:57 2009-8-20

上次将get()修改为count()后忘记上传代码了,汗。

Update 23:15 2009-8-30

有人说希望可以用父母的手机申请飞信并给自己的手机发短信,这样就不影响飞信使用。貌似的确会有这样的需求,所以加上。

Update 21:46 2009-9-17

某人提意见,去掉replay里[@自己的用户名]看起来会怪怪的。想想也是,反正省不了几个字符,于是小改一下默认的Pipe。

2009年7月1日星期三

N78印象

在刚发布的时候,Nokia N78就吸引到了我的注意力,只是手上的SE K750一直都没坏,所以就一直都没换。月前花了1600大洋将其捧回,就这一段时间的使用感受来说,印象其实不太好。

2008年5月N78上市时要价3500以上,一年间跌去2000,入手时间应该说没什么问题。尺寸方面虽然比K750大了很多圈,不过放牛仔裤兜里也还好。长条形按键适应良好,没我想象中的难用。价格和外观我都基本满意。

(这篇写得我相当没兴趣,连一个我非常在意的缺点都忘记说了:手汗重的同学谨慎考虑指纹机,很恶很麻烦。)

最不爽的是续航时间。我的电话非常少,如果不用其他功能,N78也就能撑两天多。要是开了WIFI上网,半天时间就可以把电池耗光。而K750即使偶尔听听广播,一次充电差不多也可以用一周。N78如此表现,实在有点差强人意。

另一个很不爽的地方是WIFI的兼容性。我的无线路由是当年也很便宜的腾达TWL542R,虽然功能和信号很一般,不过配合过的网卡也不少了,只有在使用N78的时候出过问题。表现为可以搜到AP,但连接后有时会提示“无网关回应”或“系统错误”,必须重启路由器才行。

系统方面,N78用的是Symbian v9.3 S60 v3.2 FP2,看着就很复杂了吧。由于塞班有太多版本,再加上分辨率、键盘等硬件方面的差异,导致塞班系统上的软件很容易出现兼容性问题。譬如按键偶尔失灵(点讯、Jbak TaskMan嫌疑比较大),找不到可用的闪光灯常亮软件等。

虽不至于让我觉得买了后悔,不过我的下一支手机大概也不会是Nokia的了。

2009年6月7日星期日

公历农历互转的Ruby实现

网上只能找到公历转换到农历的程序,反过来的Ruby实现似乎没有,所以就自己写了个,取名rbCCal。程序采用查表法,大约能实现公元1900年至2100年间200年的公历和农历互转。

数据部分出自Zhuo Meng的CCAL,该程序用GPL发布,所以rbCCal也用GPL发布。

代码放在Github。其实觉得Google Code会更靠谱些,个人用SVN和Git也不会有太大区别。可这个站是Ruby写的,而且还很漂亮。

另,因为Github只支持ASCII和UTF-8,为了能在网站上显示中文,我只好将代码存成了UTF-8,这样在Windows里直接用就会有问题。需要的话请将代码保存为GBK,或者重载LunarDate类。

下载请往这边走:https://github.com/oTnTh/ccal

require 'ccal'

l = Time.now.to_lunar()
puts l.lyear, l.lmonth, l.lday
puts Time.from_lunar(l.lyear, l.lmonth, l.lday)

l = Date.new(2009, 6, 4).to_lunar()
puts l.to_s()
puts Date.from_lunar(l.lyear, l.lmonth, l.lday)

2009年6月4日星期四

Mobile Over The Cloud

Apple MobileMe和Microsoft MyPhone都只支持特定平台,所以我的手机没法用,features倒是看起来很诱人。

Nokia Ovi用的是SyncML协议,理论上应该很多手机平台都支持。不过同步使用的密码需要以OTA的方式获得,非塞班手机能不能用没试过。支持同步通讯录、日历和记事本,貌似不错,可是服务器经常停机,频繁到影响使用的程度。

Google Sync则SyncML和ActiveSync两种协议都支持,S60上自然是用SyncML。有Google帐户就可以直接使用,能同步通讯录和日历。

因为我还没有忙到需要用计算机管理日程的程度,所以比较关注的只有通讯录一项。Ovi既然是Nokia出的,似乎应该对自家手机支持比较好才对,事实上也不尽然。

我先试了Google Sync,发现其不支持生日和备注字段。接着再试Ovi,生日备注是支持了,可是“手机(家庭)”字段却不会被同步。两家对vCard的支持只是半斤八两,都不算好。

Web端方面,Ovi差Google太多。Ovi的网站虽然比较华丽,但是速度太慢,经常停机,并且功能薄弱——连批量编辑和删除联系人的功能都没有。

个人信息中心这类东西,Google的优势应该比Nokia大很多。毕竟Google有Gmail、Gtalk,除了已经支持的Contacts和Calendar,还有Bookmark、Notebook,有心想做都是现成的。而且用Google,不必担心换了非Nokia非WinMo的手机就没法用。

在Google Sync进一步完善前(或许有得等),跟Ovi搭配着一起用或许是个不错的主意。

另,SyncML协议可以同步的东西其实很多,比如彩信和短信。不过我找了很久,都没有找到一家真正提供彩信同步功能的网站。国内有一家MyTT虽然号称可以支持,但实际上也不行。可以同步短信的倒是有好些,我试过之后发现也就O-Sync尚算可用。可是这家也不支持导出备份,总有一点点担心长期运营的问题。

2009年5月29日星期五

Side-Effect

近来说FP(函数式语言)很好很强大的人越来越多了,只是因为没需求,所以就一直没动力去学。也正是因为这样,我对副作用、引用透明之类的概念总是不甚了了。昨晚忍不住拽了一位童鞋聊了聊,算是有了点收获吧。

首先是引用透明,某童鞋的说法是:引用透明就是可以用函数的返回值代替函数的调用。

我觉得的这样的说法并不完整,于是写了这样一段代码问他:

int x;

int fa(int m, int n) {
  x = m + n;
  return x;
}

int fb(int z) {
  return x + z;
}

他说fa函数不算引用透明,因为用fa的返回值代替函数调用后,整个程序的运行结果会发生变化。可问题是,如果fb从来没调用过,且没有在其他地方读写过全局变量x的值呢?

于是试着总结一下,一个引用透明的函数,似乎应该满足以下三个条件:

  1. 只通过传值调用输入数据;
  2. 只通过返回值输出数据;
  3. 不且不能读取和修改函数外的任何其他数据。

如果一个程序中的所有函数都满足这三个条件,那么就可以保证所有函数的调用都可以用返回值来代替。

而所谓副作用指的是上述第三条。如此,“没有副作用即是不对环境产生影响”是一种没错,但是没说清楚的解释。

那么,“没有副作用即是没有变量的值发生变化”又是怎么一回事?以我的理解,这是一个混乱而错误的说法。

若我对引用透明的阐述没有错,那么所谓副作用就只是指针和作用域的问题。假如C语言中没有指针和全局变量(IO的部分另说),那么也可以说C的函数是引用透明的。

扯出变量,似乎只是命令式语言的习惯性思维作祟,因为在FP里是没有变量这种东西的。

假如在某一门FP里支持这样的语法:“int x=1”,这并非是“把整数1赋值给变量x”,而是“定义符号x代表整数1”,更像数学里的说法。即使有“x=x+1”这样的语法,也并不是“计算表达式并修改变量x的值”,而是“删除符号x与原有值的关系并重新定义一个”,内存中x原指向的1的值并没有发生变化,2被存到另外的地方去了。

事实上,FP中的“x=x+1”更有可能代表的意思是累加,即“x = (x + 1) = ((x + 1) + 1) = ...”

值只能被定义,不能被修改,岂不是又慢又浪费内存?又为什么要强调引用透明?仔细想想,却也不是没有好处的。

引用透明了,垃圾回收就容易了。可以定义无穷表,且定义时不需要计算其值,也就是所谓的延迟计算。编译器可以缓存一部分函数的运算结果,加速程序运行。无副作用,所以对并发友好,更容易发挥多核CPU的计算能力。

连我这种不会FP的人都能想到这么多好处,FP果然是种很天才的东西。

奇怪的是,除了emacs和唐凤的Perl6实现,怎么没见过用FP实现的其他东西?

2009年5月28日星期四

AMR Plugin For Winamp & foobar2000

好吧,我快要叛变了。又写了个foobar2000的AMR插件,下载链接等在文末。

KMplayer就可以放AMR,但启动太慢了。foobar2000早就有相关插件,问题是歌词外挂支持不良。惦记了很久Winamp的AMR插件一直没出现,今天终于自己写了一个。

(从*UIX系统来的不算,命令行工具不算,Winamp是我现在还常用的辈分最老的程序了,总觉得有那么点英雄迟暮的味道。)

AMR由rfc3267定义,网上最容易找到的实现是3GPP发布的3GPP TS 26.104(AMR-NB)和3GPP TS 26.204(AMR-WB)。

由于写这个插件就是为了播放手机录制的音频文件,而手机录制的音频文件全部都是AMR-NB IF1格式,所以这个插件目前只实现了AMR-NB IF1的解码功能。ETSI和IF都没有文件头,应该不是设计给独立文件用的。至于AMR-WB,以后遇到了再说吧。

AMR-NB IF1格式很简单,头部为6个字节的标识信息"#!AMR\n",余下部分为若干个frame。每个frame第一字节的前4位决定了frame的字节长度(包括首字节),如1表示13,2表示14,3表示16,有个表可以查。

AMR-NB的目标码率只有一种,8kHz、13bit、单声道。而且每frame表示的数据量也是一定的,即160次采样的数据,20ms长。

因为AMR-NB格式没有任何的索引信息,所以不扫描整个文件就无法得知文件时长,这也是KMplayer播放AMR-NB时为什么不能拖动的原因。

Winamp - in_amr_v0.1_20090902.zip

Google Drive
OneDrive

foobar - foo_input_amr_v0.4.0_20220901.zip

Google Drive
OneDrive

2022-9-1 oAMpF v0.4.0
Update with SDK v20220810
Tested with fb2k v2.0b3
Add x64 support
2017-9-18
Box的下载链接挂掉了,换成了Google Drive,也不知道能用多久…
2012-8-13 oAMpF v0.3.1
增加单声道AMR-WB支持,改了几个bug。
2010-9-24 oAMpF v0.2.1
忘记具体改了什么,好像是一点bug的说。这一版的foobar插件已经比较完善了,起码我自己用没遇到什么问题。
2010-4-15 oAMRpF v0.2
基本功能跟Winamp版一样,多了示波功能
AMR-NB IF1最后一个frame的实际长度有可能小于应当值,不知道合不合规范,懒得查了
2009-5-28 oAMRpW v0.1
支持AMR-NB IF1格式
可拖放,可显示文件时长
播放损坏的文件时可能crash
不支持Winamp示波器及相关插件

2009年5月26日星期二

Build Ruby With ICC

前些日子试图用WinDDK编译Ruby失败,不过却一直惦记着。这两天突然想起,天底下除了VC和GCC,还有一个叫Intel C++ Compiler的东西,于是就试了试。

我用的ICC是v10.1.021版,虽然v11早就有了,可看起来新版本不再支持VC6了的样子,所以只好用旧版。

正常运行configure.bat,打开生成的Makefile,在最后一句include前面加上如下三行:

CC = icl
CPP = icl
LDFLAGS = -incremental:no -debug -opt:ref -opt:icf /nodefaultlib:libmmd.lib

最后一句的nodefaultlib是为了去掉对libmmd.dll的依赖,虽然据说会快一点,不过能少一个库也是好的。

编译过程没出什么问题,除了编译速度很慢很慢以外。ZLib和PDCurses用ICC重新编译过,IConv和OpenSSL则犯懒了,用的是以前VC6编译的版本,链接也都OK。

nmake test时出错,跟了一下发现是Win32API的bug。CFunc的默认调用约定是cdcel,而绝大部分Windows APIs应该用stdcall。可奇怪的是,VC6编译出来的代码,用cdcel方式调用也可以。

跑到Ruby官网上逛了一圈,发现该问题已修正,只是要下个版本才会发布。自己编译1.9.1-p120的话,还是得在编译前手工改代码。

暂时没遇到其他的问题,copy了一个用VC6编译的sqlite3_api.so,能用,ICC和VC的兼容性还蛮不错的。

benchmark没很严格地跑,随便写了一点小东西比较,比VC6编译的版本快很多,但却比MinGW(gcc 3.4)编译的略慢些,不知道算不算正常现象。我的老机器跑ICC太累,OpenMP和SSE2之类的也都用不上,先这样吧……

2009年5月21日星期四

Ruby中的负数整除问题

Ruby中两个整数相除,其结果为整数,例如1/2为0,7/3为2。这跟C是一样的,所以我一直以为Ruby的除法运算跟C是一回事。前些日子移植一个C程序时结果出错,这才发现自己又想当然了。在Ruby中,-7/3不等于-2,而是-3。

翻书,《The Ruby Programming Language》有提到这个问题。书中解释,Ruby作整除运算时对结果进行向下(负无穷大)取整,而C则是向零取整。相应的,Ruby中-7%3的结果就是2,而非-1,想来这样的结果是为了满足(a/b)*b+(a%b)=a这一等式。

C当然也可以进行向下取整,函数名为floor。近似地,向上(正无穷大)取整的函数名则叫ceil。Ruby中也有同名的两个取整方法,若想在Ruby中进行向零取整,需要调用truncate方法,而想获得跟C一样的求余结果则要用remainder方法。

仔细想想,总觉得Ruby这样的处理方式有点不妥。

像Ruby这样的动态语言,由于不存在类型声名,运算结果类型不确定就很容易出错,而且这种错误很难发现。既然都自诩为高级语言了,还不如像Javascript那样直接返回实数,这样更明确,也更贴近于数学。

退一步说,即使要取整,似乎也应该用truncate而非floor。前者是直接丢弃小数部分,应该比较快,而且跟广为流行的C保持一致。

很想知道为什么Ruby、Python、Tcl都采用(过)floor,查了查却不得其解,算了。

2012-06-28 Update

经某位家住清华园的友人指点,原来是为了所谓数学上的完备性。

当a和b都是正数时,a整除b,得数q,余数r,则满足b*q + r == a,且0 <= r < b。

当a是负数,b是正数时,向下取整可以保证上述表达式仍然成立。

至于为什么要使modulo返回正数,请参考Wikipedia

似乎不是个很有趣的原因…

2009年4月29日星期三

VC2008链接MSVCRT.DLL不完全折腾

Ruby在Windows上有一个很讨厌的问题,其动态链接库文件名中包含了CRT库的文件名。例如用VC6编译的Ruby 1.8.6库名为msvcrt-ruby18.dll,而VC2008编译的则叫msvcr90-ruby18.dll。网上能找到的Ruby C库几乎都是用VC6编译的,如果你用的Ruby不是VC6编译的那就没法用。

造成这个问题的主要原因是不同版本VC的CRT库不兼容,ERRNO、alloc、free等在不同版本的CRT中混用会出问题。虽然这些问题似乎都可以绕过去,可Ruby社区的大牛小牛都不太喜欢搭理Windows的样子。总之现状就是,要么老实用VC6,要么所有东西都自己编译一遍。

我为啥会对这个问题有兴趣?因为新版本的VC编译出来的东西通常情况下会快一点,例如Ruby 1.9.1中的dl模块,VC2008编译出来的代码比VC6编译的至少要快50%。

几天前在网上翻到一个法子,说是链接Windows Driver Kit中包含的msvcrt.lib即可使用旧版CRT,于是忍不住试了试。结果,还是有问题。

尝试用VC2008和WDK编译一个Hello World,的确是依赖msvcrt.dll。可是在编译Ruby 1.9.1的时候,其他错误都可以通过修改代码解决,但最后却卡在environ上,“无法解析的外部符号 __imp___environ”。

写段测试代码:

#include <stdio.h>
#include "stdlib.h"

int main( int argc, char *argv[] ) {
  char **rb_origenviron = environ;
  printf( "Hello World!\n" );
  printf("%s\n", rb_origenviron[0]);
  
  return 0;
}

cl /MD t.c,问题依旧。

折腾失败……

2009年4月9日星期四

Unicode In Ruby 1.9

Ruby 1.9终于抛弃了丑陋的jcode,提供一定程度上的Unicode支持。只不过我总觉得有点儿不对劲,Ruby当前采用的处理方式,很可能会带来一些新的问题。

String有了encoding属性,部分方法的处理单位也由字节改为字符。源码文件的默认编码为US-ASCII,如果在代码中写了中文,就必须指定encoding。如果不想写,也可以用BOM。

# encoding: utf-8

s = '幺贰和叁'
puts s.encoding     # => UTF-8
puts s.length     # => 4
puts s.bytesize     # => 12

不同encoding字符串之间可以直接比较,也就是说从今以后比较字符串都要考虑编码问题,搞不好会有很多Bug因此而产生。

# encoding: utf-8

s = '位'.encode('gbk')
t = 'λ'
puts s.bytes.to_a == t.bytes.to_a # => true
puts s == t # => false
puts s == t.force_encoding('gbk') # => true

正则表达式方面更乱,encoding不同“=~”一定会报错,但match方法却不一定。

# encoding: utf-8

s = 'abc'.encode('gbk')
p s.match(/b/) # => #<MatchData "b">

s =~ /b/ # => incompatible encoding regexp match (UTF-8 regexp with GBK string) (Encoding::CompatibilityError)

s = '位'.encode('gbk')
p s.match('λ') # => incompatible encoding regexp match (UTF-8 regexp with GBK string) (Encoding::CompatibilityError)

IO方面也添加了encoding支持,不过还不支持BOM,所以读取带BOM的文件得多一个步骤。

File.open('utf8_with_bom.txt', 'r:utf-8') do |f|
  f.ungetc c unless (c = f.getc)=="\uFEFF"
  # ...
end

Python 3.0中一个很大的变化就是区分了String和Byte,所有String都是Unicode,说不定有一天Ruby也会走Python的老路,希望这只是我乌鸦嘴吧。

2009年4月7日星期二

Ruby/GD2

已更新,新内容参见:RGD - libgd binding for Ruby

Ruby/GD2是一个GD library的Ruby binding,支持GD2、Ruby 1.9,可运行于MSWin32平台。

本扩展很多代码都源自Ruby/GD v0.8.0,但由于API变化比较大,且不打算支持GD2之前的版本,故改名Ruby/GD2发布。由于我没有查到Ruby/GD采用的是何种许可证,如果将本扩展用于商业用途,请自行联系Ruby/GD当前维护者Alain Hoang,以确定没有法律问题。(真有人去问,也请告诉我一声。)

如果跟我一样在Windows下用Ruby 1.9.1,压缩包中有编译好的文件可以直接用。将bgd.dll放到Ruby的bin下,GD2.so放到site_ruby\1.9.1\i386-msvcrt下即可。我不确定是否可以在Ruby 1.8下编译,应该没有问题。不过用在Linux或OSX下,可能需要修改一下函数指针的定义。

文档暂时没有,RDoc感觉有点不好用且不会用。不过除了Ruby/GD的原始文档外,也可以参考CPAN上的GD模块,尽可能与该模块保持API兼容是我的初衷之一。

我自己肯定用不到GD库的这么多功能,错漏在所难免,有任何意见、建议、补丁,欢迎留言。

最后是一句废话:拿这个扩展去打水印的话,麻烦请考虑一下你是否有修改他人图片的权利,以及水印本身是否美观,谢谢。

require 'GD2'

def copy_rect(tname, fname, offx, offy, width, height, oname)
  timg = GD2::Image.new(tname)
  fimg = GD2::Image.new(fname)
  
  w, h = timg.bounds
  
  timg.copy(fimg, w-offx, h-offy, w-offx, h-offy, width, height)
  
  timg.jpeg(oname, 80)
end

2009年4月2日星期四

Move Ruby/GD To Ruby 1.9.1

If you are looking for Ruby extension to use GD library, plese check here.

因为实在无法忍受JRuby调用JAVA API处理Jpeg的龟速,所以花了点时间改了一下Ruby/GD的代码,使其能运行在mswin32版Ruby 1.9.1下。成果是有一点,不过还是有暂时无法解决的问题。

Ruby/GD用到的API主要是IO部分,从Ruby的头文件定义来看,变动不小。以下这几个宏,是我从RMagick等其他扩展里抄来的:

// GetReadFile doesn't exist in Ruby 1.9.0
#if !defined(GetReadFile)
#define GetReadFile(fptr) rb_io_stdio_file(fptr)
#define GetWriteFile(fptr) rb_io_stdio_file(fptr)
#endif

// rb_io_t replaces OpenFile in Ruby 1.9.0
#if defined(HAVE_RB_IO_T)
#undef OpenFile
#define OpenFile rb_io_t
#endif

// These macros are required in 1.9.1 but aren't defined prior to 1.8.6.
#if !defined(RSTRING_LEN)
#define RSTRING_LEN(s) (RSTRING((s))->len)
#endif
#if !defined(RSTRING_PTR)
#define RSTRING_PTR(s) (RSTRING((s))->ptr)
#endif

// Backport these two macros to 1.8
#if !defined(RARRAY_LEN)
#define RARRAY_LEN(a) RARRAY((a))->len
#endif
// Matz says this macro is read-only! (see http://www.ruby-forum.com/topic/146072)
#if !defined(RARRAY_PTR)
#define RARRAY_PTR(a) RARRAY((a))->ptr
#endif

变化很大,而且还没解决的问题在rb_io_t结构体上。Ruby 1.8中,该结构体包括两个*File指针,虽然不清楚区别是什么,反正是可写的。而Ruby 1.9中rb_io_t增加了很多内容,*File指针变为一个,而且不可写。

static VALUE
img_from_jpeg(VALUE img, VALUE out, VALUE quality)
{
    gdImagePtr im;
    OpenFile *fptr;
    FILE *f;

    Data_Get_Struct(img, gdImage, im);
    Check_Type(out, T_FILE);
    rb_io_binmode(out);
    GetOpenFile(out, fptr);
    rb_io_check_writable(fptr);
    f = GetWriteFile(fptr);
    gdImageJpeg(im, f, FIX2INT(quality));
    return img;
}

以上函数就这么看似乎没什么不对,可就是会出错。

另外,我还发现Ruby/GD的API设计也怪怪的,有那么点不合理。比如载入Jpeg文件,Ruby/GD有两个方法:GD::Image.newFromJpeg和GD::Image.new_from_jpeg。这两个并非同名函数,前者接受的参数是File对象,后者接受文件路径。再比如copy方法,其定义为:srcimg.copy(destimg, dx, dy, sx, sy, w, h)。调用该方法后,有变化的是作为参数的destimg,而非调用者的srcimg,这似乎与平常的习惯相反。

改到最后,虽然只有newFromXXX系列函数不能用,可怎么着都觉得有点不舒服。最后还是以Ruby/GD为蓝本,改写了一个扩展出来,有兴趣的话参见这里。至于我改的Ruby/GD就不提供下载了,实在需要的话,留言吧。

2009年3月14日星期六

Psiphon2试用

在网上翻到Psiphon2,没仔细看究竟是啥玩意儿,就发了封信到english@sesawe.net(写英文)要邀请。不曾想才过半个小时,就收到了Freerk Ohling的回信。那个时候加拿大应该是半夜才对,莫非Freerk也是宅男夜猫子?

具体怎么用,请到这里去看吧,很简单,我就只说说一天来的使用感想好了。

Psiphon2跟go2一样,其实就是一个在线代理。比如你被分配到的代理地址是https://xx.xx.xx.xx/xxx/,若要访问被墙了的http://fackgfw.cn,那就可以这样用:https://xx.xx.xx.xx/xxx/http://fackgfw.cn。

在线代理有的缺陷Psiphon2自然都有,如只支持http和https协议,以及访问采用AJAX技术的网站可能会有问题等等。优点则是不需要修改系统或浏览器的任何设置,也就是说网吧等地方也能用。

Psiphon2跟别的在线代理相比,最大的不同应该就是速度。我试着从一个被墙的日本网站下载文件,最快能达到30KB/S左右,很不错了已经。而且Psiphon2没有广告,如果能一直维持这样的速度,我会非常非常感恩的。

虽然就目前的使用状况来看,我对Psiphon2挺满意,不过该说的还是要说。基于在线代理的运行机制,你通过Psiphon2所进行的任何操作,对Psiphon2服务器来说都是可见的,Psiphon2完全有能力收集你的访问记录和某些帐户信息。

当然了,用某门某界翻墙同样有类似的问题。被蹂躏了这么久,我个人真是觉得能用就好。而且相比跟某功某志扯上关系的某门某界,Psiphon2或许还更值得信赖一点点。

以下是一些Psiphon2使用小贴士。

javascript:(function(){var proxy = 'https://xx.xx.xx.xx/xxx/';location.href = proxy + location.href;})()

将以上代码存为一个Bookmarklet(记得修改服务器地址),遇到被墙的网站点一下就会自动调用Psiphon2访问。

<script language="JavaScript">
if (document.title == '503 - Connect failed (Privoxy@localhost)') {
  var proxy = 'https://xx.xx.xx.xx/xxx/';
  location.href = proxy + location.href;
}
</script>

上面这个更好一点,存为Maxthon的按钮插件并设置自动运行,或存为Opera、Firefox的用户脚本,被墙时可以自动调用Psiphon2。

上述代码用的是Title来判断是否被墙,因为Javascript似乎没办法获得HTTP Response Code。我用Privoxy,所以判断语句才长这样,不同浏览器不同环境要依据实际情况修改才行。

Update:

还没爽快多久,我被分配到的地址就不能用了。搜了一下发现,中文网络上关于P2的信息突然多出来不少,连我写的这点东西都有人“转载”,甚至还有人直接贴出自己被分配的地址(跟我的一样)。别的no comments,没想到我这里也不如我想象的那么冷清,有那么点意外。

发现问题后给Ohling又写了封信,回信还是一如既往的快,并告诉我说,过几天我就会收到一个新地址。不过那已经是一周以前的事了,到今天我也什么都没收到。以现状来看,P2所掌握的资源估计有限。

除了P2本身出问题,其运行的安全性建立在两个前提之上:一是没人故意捣乱;二是没有小白泄漏地址。仔细想想这两条都难以保证,就不知道P2是不是真的栽在其中之一上。

2009年3月8日星期日

纠结

我很爱EmEditor,从Windows 98时代开始一直用到现在。每次打开机器我肯定都会用到他,也因此Ctrl+Alt+M这个系统级热键被其占据已经很多年了。

前些日子,我发现了EmEditor简体中文语言包一处莫名其妙的翻译:菜单栏上,工具、自定义、窗口,“切换到下一个文档命令最后使用的文档”。换成英文一看,原来是“Switch to Last Used Document for Next Document Command”。

查看info.ini,汉化者是莫尼卡,版本为8.00。完美主义作祟,于是跑到EmEditor官方网站下了一个语言包,发布日是2008年12月12日,作者为“逐浪清风”。谁知装上后再看,仍然是“切换到下一个文档命令最后使用的文档”。

对的一样还情有可原,翻错了还一样?我检查了两个文件的MD5值,都是21FB5484522A03ED6078F44100323C77,这根本就是同一个文件。

因为经过了若干次升级安装,我也无法说清包含作者信息的INI文件和资源文件的原始来源。于是为了弄清真相,我从汉化新世纪找了若干个EmEditor v8的汉化版来进行对比。结果发现,我果然还是弄错了。在我硬盘上的那个资源文件的制作和发布者并非莫尼卡,而是tracky。

那么接下来的问题是,tracky和逐浪清风谁抄了谁,或者这两个名字其实是同一个人?

通过搜索,我找到了tracky的blog,在其发布EmEditor v8.01汉化版的页面上,我看到了这么一条留言:

再点击用户名,查看该用户的信息:

“个人主页”跟EmEditor官方网站上的一样,此“逐浪清风”就是彼“逐浪清风”。

连稍作修改用以遮掩都免了,直接向官方提交别人编译好的二进制文件,换来一个署名,一个链接,还有一个目前售价39.99美金的EmEditor Professional版授权。坦白说,我真的很想用“无耻”来形容这个人,我觉得他当得起。

本来,我应该告知tracky,并且向EmEditor说明此事。然而仔细想想之后,我却悲哀地发现,不能这么做。

tracky的劳动成果被剽窃,这件事上他是受害者。可是他将EmEditor重新打包发布,同样也违反了EmEditor的用户协议。更不要说,tracky的发布页面上还提供了一个EmEditor的注册码。

剽窃和盗版其实是两个不同的问题,而且在我个人的道德观中,前者远远要比后者恶劣,但我却无法否认盗版的非正当性。

于是在这件事上,我只好什么都不做。

2009年3月6日星期五

用脚本控制Tor更换IP

不多说,直接看代码吧,虽然我觉得用得上这个脚本的地方都很无聊。

require 'net/telnet'
require 'net/http'
require 'uri'
require 'time'

def torNewIP()
  tor_addr = '127.0.0.1'
  tor_ctrl = 9051
  
  host = Net::Telnet.new(
    'Host' => tor_addr,
    'Port' => tor_ctrl,
    'Prompt' => /\n/n)
  r = host.cmd('AUTHENTICATE')
  return false if r != "250 OK\n"
  r = host.cmd('signal NEWNYM')
  return false if r != "250 OK\n"
  return true
end

def doLoop(url, proxy_addr, proxy_port, sleeptime)
  n = 0
  uri = URI.parse(url)
  while true
    req = Net::HTTP::Get.new(uri.request_uri)
    req['User-Agent'] = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0)'
    req['Referer'] = 'http://otnth.blogspot.com'
    
    http = Net::HTTP.new(uri.host, uri.port, proxy_addr, proxy_port)
    res = http.request(req)
    n += 1
    yield(n, res)
    torNewIP
    sleep(sleeptime)
  end
end

url = 'http://otnth.blogspot.com'

proxy_addr = '127.0.0.1'
proxy_port = 8118

sec = 10

doLoop(url, proxy_addr, proxy_port, sec) do |n, res|
  puts "#{Time.now.iso8601.to_s} #{n}: #{res.code} #{res.msg}"
end

2009年3月5日星期四

Tips of Hotspot Shield

Hotspot Shield是架还不错的翻墙梯,至少速度还不错。只是广告很烦人,要装客户端也很烦人,而且默认情况下连接成功后所有网络请求都会走Hotspot Shield的线路,没什么必要。

曾试着用Windows自带的拨号工具代替Hotspot Shield客户端,只是没找到怎么样才能向服务器提交CA证书通过IPSec验证,所以尝试失败,客户端还是得装。不过另外两个问题,倒是有办法解决。

先说说比较简单的广告问题。最直接的法子当然是过滤,可是也太麻烦了些。实际上有人发现,使用Opera的时候Hotspot Shield不会添加广告。所以这个问题可以通过伪造User-Agent来实现,比如你要是也用Privoxy的话,可以这样写:

{ +hide-user-agent{Opera/9.64 (Windows NT 5.1; U; Edition IBIS; zh-cn) Presto/2.1.1} }

另外,据说Hotspot Shield针对免费用户也是有流量限制的,每个月3GB,而这个限制可以通过修改网卡的MAC地址绕过。我个人倒是没遇到过流量超限的情况,顺便提一句备用吧。

想实现只有发往特定服务器的请求才使用Hotspot Shield的功能则比较麻烦,需要去设置路由表。

打开C:\Program Files\Hotspot Shield\config\config.hvpn,将“;route-noexec”行最前端的分号去掉并保存。这一设置能让Hotspot Shield在连接成功后不去修改路由表,方便我们自己处理。

执行Hotspot Shield Launch连接服务器并纪录IP地址,该地址可以从Launch弹出的IE窗口中获得。例如某次连接分配到的地址是10.55.160.44,那对应的网关就是将最后一位改为1,即10.55.160.1。

假设现在en.wikipedia.org无法访问,我们先在网上随便找一个可以进行域名解析的网站,例如http://www.hostip.info/,填入域名提交并获得en.wikipedia.org对应的IP地址为208.80.152.2。

打开一个命令提示行窗口,执行该命令:route add 208.80.152.0 mask 255.255.255.0 10.55.160.1

管理路由表还是有点麻烦,而且貌似也没有简单方便的GUI工具可以代劳,想要自动化一点只能自己写脚本,这也是我更喜欢代理服务器的原因。

2009年2月20日星期五

尝试IE8 RC1

其实前不久才升级了IE7,但因为CSS Filter导致ClearType强制关闭的问题,我又不愿意挂上GDI++,一直用得不太舒服。这几天看到IE8 RC1发布的消息,虽然有评测说IE8内存占用率很高,但想想用Maxthon应该影响不大,于是就装上试了试,感觉变化还蛮大的。

UI有变漂亮一点,菜单上多了很多Icon。“管理加载项”对话框Live风格明显,不过总觉得有点粗糙。内存占用似乎是比较高一点,但速度还不错。地址栏跟Chrome一样了,会突出显示顶级域名主体。

新增加功能方面。“加速器”(Accelerators)这个名字怪怪的,作用其实和BHO类似,叫Launcher或许还准确点。SmartScreen的作用机制不明,如果跟IE7反钓鱼功能类似,那其实没什么用。InPrivate隐私保护模式开启时,Cookie、历史纪录、Internet临时文件等内容不会被保存,工具栏和扩展也会被禁用。简而言之,都没多大用处。

比较实在的变化都在最终用户看不到的地方。IE8增加了一个看起来很强的调试工具,随便试了试感觉蛮好用的。JScript和VBScript更新至v5.8,内置了JSON对象,XMLHttp可以处理Timeout事件,以及一个新增的跨域访问对象XDomainRequest。尤其是XDomainRequest,如果Mozilla和Chrome等不跟进的话,网页设计师会很挣扎吧,真是命苦。

接下来就是我比较关心的部分。IE8有三种渲染模式,分别是IE5 Quirks模式、IE7 Strict模式和IE8 Standards模式。为了兼容IE6时代的网页,有一个Quirks模式好理解。但IE7自2006年10月发布至今不到3年,其造成的兼容性问题竟然也需要一个独立的Strict模式来解决,真是有出息。

默认情况下,IE8使用DOCTYPE来决定使用何种渲染模式。下表是不同DOCTYPE时,document.documentMode属性的值,5表示Quirks模式,8表示Standards模式。

Empty 5
<!DOCTYPE HTML> 8
HTML 5
HTML 2.0 5
HTML 3.0 5
HTML 4.0 8
HTML 4.0 Transitional 5
HTML 4.0 Strict 8
XHTML 8
XML 8

如果想为特定页面指定渲染模式,需要在head标签里加一个meta标签,比如想强制使用Standards模式可以写:<meta http-equiv="X-UA-Compatible" content="IE=8" />

IE8的默认渲染模式是IE=EmulateIE8,即根据DOCTYPE判断使用Quirks模式还是Standards模式。类似的还有一个IE=EmulateIE7,目前的Maxthon中默认采用了该模式(可以在设置中勾选“启用 IE8 标准渲染模式”启用EmulateIE8)。

扯这么多,我想说的其实还是CSS Filter导致ClearType强制关闭的问题。在IE8的Quirks模式和Strict模式下,这个问题依然存在,Standards模式下倒是修复了。所以,针对有问题的网页,只要在广告过滤器里把<head>替换为<head><meta http-equiv="X-UA-Compatible" content="IE=8" />即可解决问题。

顺带一提,Standards模式字体渲染效果不错,包括过去一直存在的微软雅黑字体下划线位置错误的问题也一并修复了。

2009年2月10日星期二

如何开启Live Mail的POP3功能

在网上看到Live Mail,也就是曾经的Hotmail、MSN提供POP3支持的消息,一点也不高兴:该功能只对有限的几个国家的用户开放,显然,不包括中国。

好吧,根据以往的经验,试试能不能冒充一下外国友人,享受非国民待遇。因为某些原因,先试了荷兰,不成,接着试了英国和加拿大,还是不成。心想说微软不至于这么小气吧,于是Google之,找到了这里,照做之下还是不成。

最后才发现,原来是我用以测试的Magic Mail Monitor这个程序有问题。可叹我用了他那么些年,直到正式皈依GMail为止。GMail的POP3貌似是独立的,在Web中删除或归档的信件不会反映在POP3连接中,于是把POP3当作检查新邮件的手段就不好使了。

呃,不废话了,以下是开启Live Mail POP3功能的办法:

  • 用浏览器打开:https://account.live.com/?mkt=en-gb
  • 点击“Registered information”
  • 将Home location和Work location的如下项都改掉
    • Country/region: United Kingdom
    • Postal Code: WC1B 3DG
    • Constituent Country: England
    • Time zone: London, United Kingdom - GMT
  • 保存后,点页面最上边的“Mail”进入Live Mail
  • 右边的“选项”,点一下,把界面改成英文

这样就搞定了,似乎并不需要登出登入。在激活了POP3功能之后,可以把你的个人信息改回去。或者干脆就冒充外国人到底,只改Work location。为了在FF中好看点,顺便偷图一张。

EMail客户端需要设置的参数如下:

  • 用户名:SomeBody@hotmail.com
  • 服务器:pop3.live.com
  • SSL:要且一定要
  • 端口号:995

我终于可以用GMail通过POP3来收取Live Mail的信件,实现天下同G了?很遗憾,可以,也不可以。

跟GMail刚开始提供IMAP功能时的毛病一样,Live Mail目前的POP3功能不是8-bit clean的,未经编码的GBK、UTF8通通有问题。用通俗的话来说就是有可能会乱码,所有中文字符显示为问号。因此暂时还必须在Live Mail中保存原件,或者只把POP3功能当检查新邮件的手段用。

目前开放了POP3功能的国家名单中还有日本,所以这个问题微软肯定早就知道了,等着吧。

另外还有一个不大不小的问题,通过POP3收取的信件不包括junk,也没有选项可以进行设置。

在折腾的过程中还顺便发现Live Mail Web端Bug一枚:

当邮件标题为未编码的GBK字符时,如界面语言为中文则显示正常,否则邮件列表界面中会乱码。点开邮件,只要其Content-type正确,在任何语言的界面下,包括邮件标题都显示正常。

不过无所谓了,我只希望POP3的Bug尽快修复,我就可以早一点跟Live Mail界面上那条硕大的广告说再见了。

2009-3-11 12:54 Update:

8bit clean问题已修复。

2009年2月8日星期日

帮人搬家之导入Blogger

在跟我实在不熟的Python奋斗了若干时间后,我才悲哀地发现,Blogger现在对API导入添加了限制。每天通过API发布一定量的帖子后,再发就得输入验证码,我太阳。

还好,通过尝试发现,Blogger GUI中提供的导入功能可以用。不然的话,某人的小500张帖子,还不知道要弄到什么时候去。

用Javascript写这段代码,看起来就乱七八糟的。不过没办法,还是因为Ruby没有好用的HTMLParser。虽然可以在Ruby里可以调用ActiveX COM,但是会有回车换行符混乱的问题,将就用吧。

// 这个函数用来处理内文需要修改的地方
// 如果没什么可改的那就不需要调用
function parseHTML(src) {
  var doc = new ActiveXObject('htmlfile');
  doc.write(src);
  
  var es = doc.getElementsByTagName('img');
  for (var i=0;i<es.length;i++) {
    var s = es[i].src;
    if (s.indexOf('foto.ycstatic.com')>0) {
      es[i].src = imgmap[encodeURIComponent(s)];
    }
  }
  return doc.body.innerHTML;
}

function parseXML(path) {
  var xml = new ActiveXObject('MSXML2.DOMDocument.3.0');
  xml.load(path);
  
  var ret = "<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href='http://www.blogger.com/styles/atom.css' type='text/css'?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-7617775710611731452.archive</id><updated>2009-02-08T18:10:51.127+08:00</updated><title type='text'>幺贰和叁</title><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://otnth.blogspot.com/feeds/archive'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7617775710611731452/archive'/><link rel='http://schemas.google.com/g/2005#post' type='application/atom+xml' href='http://www.blogger.com/feeds/7617775710611731452/archive'/><link rel='alternate' type='text/html' href='http://otnth.blogspot.com/'/><author><name>小八</name><email>noreply@blogger.com</email></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator>";
  var es = xml.getElementsByTagName('feed/entry');
  for (var i=0;i<es.length;i++) {
    ret += '<entry><id>' + es[i].getElementsByTagName('id')[0].firstChild.nodeValue + '</id>';
    
    ret += '<published>' + es[i].getElementsByTagName('published')[0].firstChild.nodeValue + '</published>';
    ret += '<updated>' + es[i].getElementsByTagName('updated')[0].firstChild.nodeValue + '</updated>';
    
    ret += "<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/blogger/2008/kind#post'/>";
    var cats = es[i].getElementsByTagName('category');
    for (var j=0;j<cats.length;j++) {
      if (cats[j].getAttribute('scheme') != 'http://www.google.com/reader/') {
        ret += "<category scheme='http://www.blogger.com/atom/ns#' term='" + cats[j].getAttribute('term') + "'/>";
      }
    }

    ret += '<title type="text">' + es[i].getElementsByTagName('title')[0].firstChild.nodeValue + '</title>';

    s = '<div class="oldpost_ycool">' + es[i].getElementsByTagName('summary')[0].firstChild.nodeValue.replace(/\.{3}$/, '')  + '</div>';
    //s = parseHTML(s);
    ret += '<content type="html"><![CDATA[' + s + ']]></content>';
    
    ret += "<author><name>小八</name></author>";
    
    ret += "<thr:total>0</thr:total></entry>";
  }
  ret += '</feed>';
  
  var xn = new ActiveXObject('MSXML2.DOMDocument.3.0');
  xn.async = false;
  xn.loadXML(ret);
  
  if (xn.parseError != 0) {
    var oError = xn.parseError;
    throw new Error("An error occurred:\n错误代码: "
      + oError.errorCode + "\n"
      + "行数: " + oError.line + "\n"
      + "列数: " + oError.linepos + "\n"
      + "原因: " + oError.reason);
  } else {
    xn.save(path + '.txt');
  }
}


function main() {
  var fso = new ActiveXObject('Scripting.FileSystemObject');
  var fd = fso.GetFolder('.');
  var fc = new Enumerator(fd.Files);
  for (;!fc.atEnd();fc.moveNext()) {
    var s = String(fc.item());
    if (s.substr(s.length-4) == '.xml') {
      parseXML(s);
    }
  }
}

main();

帮人搬家之导入图片至Picasa

Blog搬家,文字部分其实很好处理,麻烦的是图片。如果BSP下手狠一点直接删掉,而且本地又没有备份的话,那就全完了。

歪酷到目前为止相册都还没动,不知道是没检查到还是怎样,总之现在还能抓就是了。

因为Ruby没有好用的HTMLParser类,所以以下代码用的是Javascript。由于我不确定歪酷服务器上的图片文件名是否唯一,所以直接将URL处理后作为文件名保存。

function parseHTML(src) {
  var doc = new ActiveXObject('htmlfile');
  doc.write(src);
  
  var es = doc.getElementsByTagName('img');
  for (var i=0;i<es.length;i++) {
    var s = es[i].src;
    if (s.indexOf('foto.ycstatic.com')>0) {
      var c = 'wget -O ' + encodeURIComponent(s) + ' ' + s;
      // 批处理文件需要
      c = c.replace('%', '%%');
      
      WScript.echo(c);
    }
  }
}

function parseXML(path) {
  var xml = new ActiveXObject('MSXML2.DOMDocument.3.0');
  xml.load(path);
  
  var es = xml.getElementsByTagName('feed/entry/summary');
  for (var i=0;i<es.length;i++) {
    parseHTML(es[i].firstChild.nodeValue);
  }
}


function main() {
  var fso = new ActiveXObject('Scripting.FileSystemObject');
  var fd = fso.GetFolder('.');
  var fc = new Enumerator(fd.Files);
  for (;!fc.atEnd();fc.moveNext()) {
    var s = String(fc.item());
    if (s.substr(s.length-4) == '.xml') {
      parseXML(s);
    }
  }
}

main();

上传至Picasa的代码倒没什么可解释的,只需要留意一点:每个Picasa相册最多只能容纳500张图片。

require 'net/https'
require 'uri'
require 'rexml/document'
require 'FileUtils'

def getAuth(email, passwd)
  uri = URI.parse('https://www.google.com/accounts/ClientLogin')
  
  req = Net::HTTP::Post.new(uri.path)
  req.set_form_data({'Email'=>email, 'Passwd'=>passwd, 'service'=>'lh2'})
  
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  res = http.start {|h| h.request(req)}
  
  case res
  when Net::HTTPSuccess
    res.body.split("\n").each { |s|
      return s if s[0,5] == 'Auth='
    }
  else
    puts res.body
    res.error!
  end
end

def ul2Picasa(auth, albumid, title, summary, filename, ftype='image/jpeg')
  uri = URI.parse("http://picasaweb.google.com/data/feed/api/user/default/albumid/#{albumid}")
  
  body = <<EOF
Media multipart posting
--END_OF_PART
Content-Type: application/atom+xml

<entry xmlns='http://www.w3.org/2005/Atom'>
  <title>#{title}</title>
  <summary>#{summary}</summary>
  <category scheme="http://schemas.google.com/g/2005#kind"
    term="http://schemas.google.com/photos/2007#photo"/>
</entry>
--END_OF_PART
Content-Type: #{ftype}

#{File.open(filename, 'rb') {|f| f.read()}}
--END_OF_PART--
EOF

  
  req = Net::HTTP::Post.new(uri.path)
  req.body = body
  req.set_content_type('multipart/related; boundary="END_OF_PART"')
  req['MIME-version'] = '1.0'
  req['Authorization'] = "GoogleLogin #{auth}"
  req['Content-Length'] = body.length
  
  http = Net::HTTP.new(uri.host, uri.port)
  res = http.start {|h| h.request(req)}
  
  case res
  when Net::HTTPSuccess
    xmldoc = REXML::Document.new(res.body)
    yield xmldoc.get_elements('entry/content')[0].attributes.get_attribute('src').to_s
  else
    puts res.body
    res.error!
  end
end

email = 'USERNAME@gmail.com'
passwd = 'USERPASSWORD'

# 该值可通过收工创建相册并查看源代码获得
albumid = 'NNNNNNNNNNNNNNNNNN'

# 把上传完成的文件移动到该目录
mvdir = '@up'

auth = getAuth(email, passwd)

# 作纪录备用
flog = File.open('piclog.txt', 'w')
Dir.glob('*.jpg') { |fn|
  ul2Picasa(auth, albumid, fn, '', fn) { |url|
    puts fn
    flog.puts fn + "\t" + url.sub(/(\/[^\/]+)$/, '/s800\1')
    FileUtils.move(fn, mvdir)
  }
}
flog.close

帮人搬家之导出Google Reader缓存

某人的Blog原来放在歪酷上,因为歪酷上榜第七批,所以。

目前歪酷被关Blog的帖子部分无论前台后台均无法访问,日后是否会提供下载不得而知。好在某人的Blog自从2006起就有人(敝人)在Google Reader上订阅,歪酷的Feed也是全文输出,所以能救一部分回来。

但是借助Google Reader缓存搬迁Blog的法子也有不小的局限性:

  • 原Blog提供了全文Feed输出;
  • 只能取回自第一次有人在Reader订阅之后发布的文;
  • 如修改过不包含在Feed中的旧文,则修改部分无法取回;

本想用Javascript写代码,但因为XMLHTTP不能自定义Cookie(ServerXMLHTTP好像可以,不过我没试),所以改用Ruby。

require 'net/https'
require 'uri'

def getSID(email, passwd)
  uri = URI.parse('https://www.google.com/accounts/ClientLogin')

  req = Net::HTTP::Post.new(uri.path)
  req.set_form_data({'Email'=>email, 'Passwd'=>passwd});
  
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  res = http.start {|h| h.request(req)}
  
  case res
  when Net::HTTPSuccess
    res.body.split("\n").each { |s|
      return s if s[0,4] == 'SID='
    }
  else
    res.error!
  end
end

def parseXML(src)
  m = src.match('([^<]+)')
  if m then
    return m[1]
  else
    return ''
  end
end
    
def exportGRCache(feedurl, email, passwd, fn = '000001', c = '')
  sid = getSID(email, passwd)
  
  begin
    # n尽量大一点,方便后续处理
    url = 'http://www.google.com/reader/atom/feed/' + feedurl + '?n=100'
    url += '&c=' + c if c != ''
    uri = URI.parse(url)
    
    headers = { 'Cookie' => sid, }
    puts 'DL ' + url
    res = Net::HTTP.start(uri.host, uri.port) { |h| h.get(uri.request_uri, headers) }
    
    puts 'Save ' + fn + '.xml ...'
    f = File.new(fn + '.xml', 'w')
    fn.next!
    f.write(res.body)
    
    c = parseXML(res.body)
  end while c != ''
end

# Google Reader中订阅的Feed地址
url = 'http://BLOG.DOMAIN.com/PATH.xml'

# Google Account,Reader API目前必须要验证才能用
email = 'USERNAME@gmail.com'
passwd = 'USERPASSWORD'

exportGRCache(url, email, passwd)

2009年2月5日星期四

禁止WLM自动运行

Windows Live Messenger在每次登陆Live.com的时候都会自动运行,很烦人。网上多方寻觅不得其解,只好自己写办法。

用IE浏览某个网站就执行一个本地程序,怎么想都是ActiveX最可疑。经过尝试,在IE菜单栏中选择“工具”、“Internet选项”,“程序”页中点击“管理加载项”,禁用下图红框中的Windows Live项目即可解决问题。

经过如上设置,用IE登陆Live.com时Messenger是不会自动运行了,但Maxthon或TheWorld却还是照旧。IE中禁用的项在Maxthon中却可以加载,不知这算不算是一个Bug。

试了试直接将“C:\Program Files\Windows Live\Messenger\msgsc.14.0.8050.1202.dll”改名,但一运行WLM就会修复。只好用老办法了,直接屏蔽,将下述内容导入注册表即可:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\ActiveX Compatibility\{E1771B7F-98BE-407F-BA67-AA16ADA5D0C5}]
"Compatibility Flags"=dword:00000400

ActiveX Compatibility项目就是一众“IE插件免疫”工具用到的注册表项,将对应CLSID项中的Compatibility Flags设置为1024即位禁用,改为0则为启用。

Update:

想了想,如果要在Maxthon内部屏蔽该控件应该也是可以的。在JScript中调用ActiveX控件,必然要传递ProgID参数建立ActiveXObject对象,那么只要将注册表里查到的"MSNMessenger.Hotmail2Control"替换为空字符串就行了。

2009年2月3日星期二

Bug In Blogger HTML Gadget

在Blogger中添加一个HTML/Javascript Gadget,贴入以下代码:

<script type="text/javascript">
function do_something() {
  var a = document.getElementsByTagName('div');
  for (var i=0;i<a.length;i++) {
    if (a[i].className=='XX') a[i].className='OO';
  }
}
</script>

保存以后再点修改,会发现代码变成了这个样子:

<script type="text/javascript">
function do_something() {
  var a = document.getElementsByTagName('div');
  for (var i=0;i<a.length;i++) (a[i].classname="XX" ) a[i].classname="OO" ;
 {
 if }
}
></script>

折腾了老半天,我发现把for语句改成如下两种样式都没问题:

<script type="text/javascript">
for (var i=0;i<(a.length);i++) { };
for (var i=a.length-1;i>=0;i--) { };
</script>

抓狂。

2009年1月30日星期五

IE7中ClearType自动关闭的问题

我的系统是Windows XP,装上了微软雅黑并打开ClearType。前些日子更新了IE7之后,很快我就发现,有很多页面(如百度空间)上的雅黑中文显示不正常,发虚,很模糊,而英文却又全是锯齿,总之没法看。

拜了Google大神,都说是IE7默认打开了ClearType导致中文发虚,可我这里的问题明明是ClearType被关了才会这样。小白太多,把Google都淹了,于是只好靠自己。在百度空间上随便找一个页面保存下来,一点一点找,总算是确定了问题所在:“FILTER: none”和“ZOOM: 1”。

有了关键字才好Google。IEBlog上有一篇《Notes on the interaction of ClearType with DXTransforms in IE7》,说是启用了filter就会强制关闭ClearType。IE 透明度失效和 zoom:1 有關?还提及,会出问题的还并不仅仅是ZOOM。

问题找到了,但在IE7内部似乎没办法解决。不过还是有一个曲线救国的法子,那就是用GDI++。GDI++是一个日本人开发的字体渲染工具,借助他可以把Linux下的FreeType2搬到Windows里边用。既然渲染引擎都被换掉,ClearType关不关也就无所谓了。

GDI++日文WIKI上找到并下载gdi0850.zipgdi0869.7z,解到同一个目录,然后用这样的命令启动IE:"gdi++.exe" "C:\Program Files\Internet Explorer\iexplore.exe"

下图中左边的窗口是ClearType被强制关闭的效果,右边是使用GDI++后的效果:

GDI++早已停止开发,但因为其是开源软件,所以有很多修改版,有点乱。有兴趣的话就去拜Google大神吧,这东西会很是需要一番折腾。

用了一段时间IE7,感觉也没有找到值得升级的理由。IE7不比IE6快,增加的功能我也不需要,问题还比IE6多。至于说标准,时至今日IE6还有20%以上市场占有率,有人敢无视?未来会有IE6被淘汰的一天,但显然不是现在。

2009年1月29日星期四

WSH中XMLHttp的怪异表现

参考如下JScript代码:

var xmlhttp = new ActiveXObject("MSXML2.XMLHTTP");
xmlhttp.onreadystatechange = function () {
 WScript.Echo(xmlhttp.readyState);
}
xmlhttp.open("GET", "http://otnth.blogspot.com/", true);
xmlhttp.send();

保存为js文件并用cscript或wscript执行,上述程序输出两次1后退出。如果把open挪到onreadystatechange前面,则只输出一次1后退出。

网上有很多人遇到移步方式下onreadystatechange只执行一次的问题,解决办法是把onreadystatechange写在open前面。但这里的问题似乎没有那么简单,感觉像是Windows Scripting Host脚本宿主未等待onreadystatechange变化就直接退出了,于是我写了如下代码求证:

var xmlhttp = new ActiveXObject("MSXML2.XMLHTTP");
xmlhttp.open("GET", "http://otnth.blogspot.com/", true);
xmlhttp.send();
while (true) {
 var n = xmlhttp.readyState;
 WScript.Echo(n);
 WScript.Sleep(100);
 if (n == 4) break;
}

这次输出了N个1后,直接跳到4。所以,WSH是单线程的?

解决问题的办法也有,即使用WScript.Shell新开一个进程运行所需代码,并通过命令行传递参数:

if (WScript.Arguments.length>0) {
 var xmlhttp = new ActiveXObject("MSXML2.XMLHTTP");
 xmlhttp.open("GET", WScript.Arguments.item(0), false);
 xmlhttp.send();
 WScript.Echo(xmlhttp.status + ' : ' + xmlhttp.statusText);
} else {
 var wsh = new ActiveXObject("WScript.Shell");
 var s = 'http://otnth.blogspot.com/';
 wsh.run('wscript ' + WScript.scriptfullname + ' "' + s + '"');
}

能用,不过看起来很不舒服。另外需要注意的是,命令行参数有长度限制,还要留意引号的问题。

好吧,就这样了。

2009年1月20日星期二

从解码Base64的宏想起

从找到那天起,一直都很爱EmEditor。轻巧快速,完善的正则表达式和多内码支持。除了编辑超大文件时的表现不如UEdit32外,我几乎找不到别的缺点。而且在没有丢失已有优点的前提下,EmEditor也有在一点点变得更好更强大,是个用起来很窝心的编辑器。

好了,工商时间结束。话说最近一次升级到EmEditor v8后发现,以前的Base64解码插件不能用了。正好现在的EmEditor借助Windows Scripting Host(WSH)提供了宏功能,于是想自己写个解码宏试试感觉。

这个宏需要做的有两件事:一、将Base64字符串解码;二、如需要,进行内码转换。

WSH的标准配备Javascript和VBScript都没有直接提供解决这两件事的函数或对象,也没有所谓标准库一说,要用JS或VBS的话就必须找现成的纯实现或自己写。当然,如果喜欢Perl、PHP、Python、Ruby等等等,WSH其实也能够支持。

这两种法子都不好。Base64肯定有成熟的纯实现,内码转换却是个很复杂的问题,UTF-8至GBK或许OK,Big5至GBK呢?但如果用Ruby一类的语言,写出来的宏就只有自己用了,普通人哪会去装个Ruby解释器?

其实,所谓的“完美”解决办法是有的,而且看起来很简洁,很清晰:

#title = "Base64Decode"
#tooltip = "Decode Selected Base64 String."

function Base64DecodeText(Base64Str, sCharset){
 var xml_dom = new ActiveXObject("MSXML2.DOMDocument");
 var tmpNode = xml_dom.createElement("tmpNode");
 tmpNode.dataType = "bin.base64";
 tmpNode.text = Base64Str;

 var ado_stream = new ActiveXObject("ADODB.Stream");
 // Default gb2312
 ado_stream.Charset = (typeof(sCharset) == "undefined") ? "gb2312" : sCharset;
 ado_stream.Type = 1; // 1=adTypeBinary 2=adTypeText
 ado_stream.Open();
 ado_stream.Write(tmpNode.nodeTypedValue);
 ado_stream.Position = 0;
 ado_stream.Type = 2; // 1=adTypeBinary 2=adTypeText
 var str = ado_stream.ReadText(-1); // -1=adReadAll
 ado_stream.Close();

 return str;
}

if (!document.selection.IsEmpty) {
 var s = document.selection.Text;
 //s = Base64DecodeText(s); // gb2312
 s = Base64DecodeText(s, "UTF-8");
 OutputBar.Clear();
 OutputBar.writeln(s);
 OutputBar.Visible = true;
 OutputBar.SetFocus();
} else {
 alert('Select First!');
}

这一小段东西至少说明了一个问题:那些所谓《YY天精通XX语言》的书,纯属胡扯。

2009年1月15日星期四

用手机接收Twitter Replies

本来Twitter只是我用来作为一个自闭宅男自言自语的地方,没事偷着乐而已。可是近来接连被人在那里抓住现行,这下问题就出来了。

先是发现,即使在Notices设置中选择Show me all @ replies,未follow对象@我的消息仍然不会显示在Home Time Line里。继而又发现,没有任何方便的法子获取replies提醒,没email,没im,当然更不会有短信。

要知道,我更新Twitter都是通过叽歪同步的方式,难道我得三不五时用浏览器去访问Twitter @Replies Tab才成?好吧,于是开始折腾。

在经过艰苦卓绝的探索,克服了一系列难关之后,我终于实现了用手机接收Twitter Replies提醒短信的终极目标,完成了一次互联网上的二万五千里长征。

好吧,我不贫了,下面就说具体过程。

据说,过去叽歪和饭否等国内网站,都提供手机短信下行提醒服务。只要想办法弄到Twitter Replies的RSS,通过Feedlr一类的服务广播到帐户A,再用帐户B绑定手机并关注帐户A,就可以达到目的。

只是现在日子不好过,饭否直接取消了短信下行服务,叽歪没取消,可是怎么设置都收不到。还好叽歪支持飞信,通过用帐户B绑定飞信的办法,仍然可以达成目的。

先去叽歪注册两个帐户,Xo和XoJwBot。用Xo绑定飞信帐户,设置获取提醒方式为飞信,并关注XoJwBot,叽歪上的设置就弄好了。如果注重隐私问题,可以将两个帐户都设置为不公开。

然后是想办法获得Twitter Replies RSS。@Replies Tab页最下面有提供RSS连接,可是这个地址需要Auth,所以得这么用:http://[ttUser]:[ttPw]@twitter.com/statuses/replies.rss

接着去Feedlr注册。但在创建广播的时候Feedlr会告诉你,如上地址非法。咋办?如果不想让全世界都知道你的Twitter密码的话,就不要考虑FeedSky一类的法子了,用Yahoo Pipes吧。

Yahoo Pipes用起来不算难,或者说,不算太难。我们还可以顺便在Pipe里把原始RSS中“@User”的部分删掉,以免广播到叽歪时出问题。

最后将Yahoo Pipes输出的RSS用Feedlr广播到叽歪帐户XoJwBot,整个过程就完成了。只要飞信不取消Client离线就发短信的功能,可以干的事还蛮多的,希望这篇东西流传别太广吧。

就为了这么点破事儿,数数注册了几个帐户:一个Twitter、一个Yahoo、一个Feedlr、两个叽歪、一个飞信,还有两个email,还有一部手机和一张中移动的SIM卡……

Update:

出问题乐,收不到短信……

经过排查,问题出在叽歪至飞信的环节。即使我打开了飞信客户端,仍然收不到叽歪发来的信息,从飞信客户端发送更新至叽歪也没反应,看来是叽歪的飞信机器人当掉了。

这么不靠谱,残念……

2009年1月11日星期日

Tips of Chrome

目录

最近一直在用Google Chrome,下面就是我遇到的一些问题及解决方式。这一篇应该会持续更新吧……

如何获知Chrome的更新信息?

Chrome的更新信息都可以在Google Chrome Releases Blog上看到。

有时一些微小更新并不会导致版本号变化,如修正了Hotmail和Yahoo! Mail问题之后的Chrome仍然是1.0.154.46版。如果想及时了解Chrome的更新信息,就订阅吧。

如何下载Chrome的离线安装包?

从Google Chrome主页上只能下载到在线安装包,需要一次部署到多台计算机,或是想制作绿化版Chrome,就会有些麻烦。其实Google是有提供离线安装包的,地址如下:

http://dl.google.com/chrome/install/154.46/chrome_installer.exe

这是当前stable版本的下载地址,154.46是版本号后两位。上哪去知道这两个数字呢?Google Chrome Releases Blog

如何制作可移动版(绿化)Chrome?

将安装文件下载回来,用7-Zip解压缩。Chrome-bin改名成Chrome搬到目的目录。将其中X.X.XXX.X(版本号)子目录下的文件都搬到上级目录,Locales目录下除了en_US和zh_CN都可以删掉。

右键点击chrome.exe,创建快捷方式。再右键选中刚才建立的快捷方式,选属性,在“目标”栏最后添加“--user-data-dir=Profile”就可以了。

如果要把Chrome装U盘里,则建立一个名为Chrome.bat的批处理文件,打开并写入“start chrome.exe --user-data-dir=Profile”。

如何获得未发布的最新版本?

Night Build,或者叫Latest Trunk Build,可以到这里(需翻墙)下载针对Windows编译的最新版本。Chrome有什么新特性肯定会最先在这里出现,但Bug也是,所以最好还是别用。

如何使用用户脚本(Greasemonkey)?

该功能只有v2.0以后才支持,在将相关选项做进UI以前,可以通过在启动参数中添加“--enable-user-scripts”打开该功能。

至于用户脚本的存放位置,2.0.156.1及之前的版本必须放到“C:\scripts”目录下,后续版本可以放在Profile目录的“User Scripts”子目录里。

播放Flash很卡怎么办?

真正解决问题,那得靠Adobe和Google,用户能做的除了等,其实也没多少。

短期内,个人建议是不用Chrome看Flash,有需要时单独开一个IE窗口来处理。删掉系统目录(system32)和plugins下的NPSWF32.dll文件,打开Chrome目录中的chrome_plugins_file.xml并删除Flash相关段落,这样至少可以保证Chrome流畅运行。

在v2.0正式发布以后,通过用户脚本应该也可以解决一部分问题。但考虑到用户脚本的运行机制,具体效果怎么样,还是要等到时候看。我有在v2.0预测试版中尝试过,发现对embed和object标签设置display:none都没作用,就懒得继续折腾了。

如何过滤广告内容?

Google就是一个很大的广告商,对广告过滤不积极很正常,所以Chrome未来也未必会加入官方的广告过滤功能。使用用户脚本进行过滤并非不可能,但是会很麻烦。想要简单方便的广告过滤,恐怕要等Chrome正式加入第三方插件机制以后了。

如果现在就要广告过滤的功能,也可以通过安装第三方程序的方式来实现,实际上在各家浏览器提供自己的广告过滤解决方案之前,就早已有这样的程序存在了。

目前Windows平台上的广告过滤程序所采用的技术大概也就两种,即Winsock和Proxy。前一种的典型代表是Ad Muncher,而后一种则有ProxomitronPrivoxy等。

采用Winsock技术的广告过滤软件不需要对浏览器进行设置,而且这类软件大多是为普通用户开发,易用性方面会更好些。而Proxy类的广告过滤软件其实就是架设在本机的代理服务器,这类软件的功能通常都更为强大,但也更复杂。

我个人用的是Privoxy,因为配置代码看起来比Proxomitron要直观些。而且翻墙需要Tor,Proxomitron又不支持Socks4a的代理,虽然两个可以串起来用。

如果不懂正则表达式、英文比我还烂,就用Ad Muncher吧。

如何修改书签图标?

如Post to Delicious一类的书签,由于其内容只是一段Javascript脚本,通常情况下只会显示为一个蛮难看的图标。想改也不是不成,就是有点麻烦。

(不好意思我懒得截图了,如果看不明白下面这些说的是什么,就别折腾了的好。)

以下内容适用于v1.0.154.42,后续版本或许会有变化。

首先用Chrome访问目的网站,如delicious.com。待页面加载完成,并正确显示了Delicious的图标后,关闭Chrome。

用文本编辑器打开“Profile\Default\Bookmarks”,这里就是Chrome保存书签的地方。文件格式应该是Javascript吧,不过没有可以指定icon的地方。找到想要修改icon的书签,记下url。

Chrome中很多数据,例如Cookies等,都是以Sqlite格式保存的,而历史纪录则是在“Profile\Default\History”文件中。去下载一个SQLite Database Browser,用该程序打开History数据库文件。

点“Browse Data”选项卡,在Table下拉菜单中选中urls表。这里稍微解释一下数据结构,url和title字段不需要多说,favicon_id字段就是用来确定该url用哪一个编号的icon来显示。若favicon_id为0,则使用默认图标。

点包含了表名称的下拉菜单后面的放大镜,字段选url,运算符选contains,在下面的文本框内输入delicious,点Search。窗口下部的列表框这时会显示出url字段中包含delicious的所有纪录,记住纪录的id,例如165,关闭该窗口。

在数据浏览窗口里往下拉,找到第165条纪录。该纪录的favicon_id只要不为0,那就是我们需要的数字了,比如13,记下来。(SQLite Database Browser每次只载入1000条纪录,如果你的历史纪录很多,可能会需要先翻页。)

点New Record(我这里遇到了点问题,不能新建纪录的话,随便把一条不重要的改掉也可以。),将url改为Bookmarks文件中找来的地址,例如:

javascript:(function(){location.href='http://delicious.com/save?url='+encodeURIComponent(window.location.href)+'&title='+encodeURIComponent(document.title)+'&v=5&jump=yes'})()

title随意,Post to Delicious好了,其余的都可以照猫画虎,只要把favicon_id改为刚才找到的13。

保存数据库并关闭,去Chrome里看看吧。

如何查看缓存文件?

直到2.0版Chrome也没有内建缓存查看功能(其实有一个about:cache,可Geek过头了),需要借助第三方程序如ChromeCacheView

2009年1月10日星期六

Chrome 2.0 预测试版发布

1.0正式版发布没多久,2.0 Pre-Beta版就来了。2.0.156.1版下载地址

新闻提到的新功能:

自动完成:
似乎不是谷歌输入框里的那种,倒是有根据历史纪录提示URL的功能,可我也不确定是不是新加的功能。比如访问过我的blog,打个“幺”会提示出访问过的所有Title包括“幺”的地址。
页面缩放:
Ctrl+“+”放大,Ctrl+“-”缩小,Ctrl+“0”还原,还不错,不过只能通过键盘操作。
自动滚动:
按鼠标中建,也就是滚轮,上下左右移动就行,没什么用。
简易切换配置文件:
命令行参数--user-data-dir=PATH的菜单版。
支持Greasemonkey脚本:
早就说要加上的功能,说不定Chrome以后的插件指的就是用户脚本了(跟Opera一样),也是驱使我尝鲜的原因。

Update,发现两个bug,问题还都不小:

  • 中文输入法不能实现光标跟随,起码搜狗拼音不行。
  • HTTPS访问Reader,提示“您到www.google.com的连接未加密”,新浪等BSP的图片无法显示。

其实我以前没留意,1.0.154.42同样提示连接未加密,但看图是没问题的。为了验证,我又找来了latest trunk build(需翻墙),版本号是2.0.157.0(7878)。这次提示信息是对了,但新浪等BSP的图片还是无法显示。

HTTPS页面引用的元素也发送了Referer?或许跟新实现的HTTP协议代码有关吧。反正这个问题不修正,新版Chrome就没法用……

Flash支持似乎有变好一点,不过因为上面提到的两个问题,我也没动力继续试下去了。其实只要支持用户脚本,实现一个类似Flash Block之类的东西也不困难。


当前版本启用用户脚本功能,需要手动添加命令行参数。修改Chrome的快捷方式,在“目标”栏最后添加“ --enable-user-scripts”即可。

用户脚本放哪,我是翻墙跑到Chromium开发者站才知道的。当前版本得放到“C:\scripts”,以后可以放在Profile目录的“User Scripts”子目录里。

正好前几天才念叨,Google Reader不能显示Feed自定义Icon,找来Google Reader: Show Feed Favicons,效果如下:

2009年1月6日星期二

选个Feed来订阅

有一点点矛盾,写Blog的人可能不会喜欢Feed Cache这东西,但读者肯定喜欢。比如我在订阅Feed的时候,一般都会选Cache量最大的地址订阅。

怎么样判断哪个地址的Cache量最大呢?都订阅上然后手工去翻,或者借助服务提供商的API,如果有的话。Google Reader是有的,Bloglines和NewsGator不太清楚,应该也有吧。

我写了一个Google Reader API检查Cache量的小东西,附在文末。在文本框中填入Feed URL,点后面的按钮就可以了,结果会以表格的样式显示在文本框下面。为了图方便,代码用了同步模式获取数据,如果老半天没反应就手动刷新一下页面再试。

Google Reader API目前还没有正式发布,需要先验证,而且还没JSON版本,所以会遇到著名的跨域问题。IE6用户在菜单上选工具、Internet选项,在安全页中点自定义级别,找到其他项下的“通过域访问数据资源”,选提示,确定。这样脚本运行时会出现提示,授权即可。

直接使用IE6的话,先访问Google Reader并登陆,然后在同一个窗口打开本页就可以正常使用了。Maxthon之类的浏览器就随便了,只要登陆过,在新开的选项卡里也可以正常使用。

代码只在IE6中测试过,不好意思。

以下是一些BSP提供的不同Feed地址,列表应该会慢慢增加的吧。其实只要BSP做好转向,啥事都没了。

新浪
http://blog.sina.com.cn/rss/BLOGID.xml
http://blog.sina.com.cn/rss/BLOGNAME.xml
http://blog.sina.com.cn/myblog/index_rss.php?uid=BLOGID
Blogger
http://www.blogger.com/feeds/BLOGID/posts/default
http://BLOGNAME.blogspot.com/feeds/posts/default
http://BLOGNAME.blogspot.com/atom.xml
http://BLOGNAME.blogspot.com/rss.xml
歪酷
http://rss.yculblog.com/BLOGNAME.xml
http://rss.ycool.com/blog/BLOGNAME.xml
BlogCN
http://BLOGNAME.blogcn.com/rss.xml
[404]http://www.blogcn.com/rss.asp?blog=BLOGNAME

2009年1月4日星期日

又到春运时

今年春运计划从1月11号开始,而北京更是根据客流量情况,提前10天进入春运。

每年一到这个时候,售票处、火车站、飞机场都是兵荒马乱的地方。排一、两小时甚至更长时间的队,只是为了问上一句话,而得到的答案很可能却是:“没票了”。那么多那么多人的脸上,除了焦急就是疲惫,一点都看不到回家的喜悦。

小时候的我也曾相当传统,认为回家是天大的事,所以会想尽办法提前动身,或者咬着牙买全价机票。不过人总是会长大的,会有第一次、第二次和更多次。然后我也开始疑惑,为了那一顿饭,值得么?

时至今日,我也没有一个确定的答案。尤其是想到那冗长的队伍,拥挤的人群,污浊的空气,我就会愈加动摇。可是当我坐在家里,想到“家”这个字所代表的这些那些时,我又会觉得,大概是值得吧。

今年的我不需要再经历春运,或许以后也不再需要。不过还是有些我认识的和在乎的人,不会回家,或想回而回不了家。希望他们能过个不错的春节,不要,至少不要太寂寞。

2009年1月1日星期四

Feed迷思

Feed,或者叫RSS,总之就是那玩意儿,我现在的生活已经有点离不开了。每当我又不知道该做什么才好的时候,总会打开Feed阅读器看看有啥新东西了没。

我有不止一台机器,也并非完全没有用别人机器的可能,所以一开始对在线阅读器还是挺有兴趣的。不过在尝试使用了一段时间Bloglines之后,我放弃了。

实在是太慢了。Bloglines服务器在国外,本来就不快,还要再加上等待页面重绘的时间,慢得让我无法忍受。我可是至今都还在用AcdSee 2.4的人,图的就是一个爽利。

之后我遇到了国产的GreatNews离线阅读器,曾一度惊为天人。体积小巧,资源占用不多,而且还能绑定Bloglines帐户。GreatNews+Bloglines的组合,我用了很长一段时间。

然而渐渐的,我也发现了GreatNews的不少缺点。首先是与Bloglines的集成度不高,Clippings只是本地操作,无法反映到Bloglines帐户中,所谓社会化功能就更没有了。更重要的是程序本身也有不少问题。Bloglines端的Feed发生302重定向后,GreatNews中新旧地址会同时存在,不会自动删除旧地址。手动删除也不行,因为Clippings是保存在本地的,删除旧地址会一并将Clippings也删掉。最让人无法接受的是,会莫名其妙自动退出。

GreatNews是免费软件,开发者好像也只有一个,缺乏激励的情况下后续乏力也无可指责。随后我换回了FeedDemon,其时该程序已经被NewsGator收购了。

当时的FeedDemon还是共享软件,不过最近已经免费了。被收购后的FeedDemon与NewsGator配合很好,Feed订阅的双向同步都没有问题,Clippings数据在服务器端也同样有保存。一直到最近,FeedDemon都是我日常使用的首选。

不过FeedDemon也不完美就是了。相比GreatNews,FeedDemon的资源占用要大很多。另外当单一Feed在本地未读Posts达到100条时,FeedDemon将不再继续下载新数据,这和我的使用习惯不符,也觉得FeedDemon太过自作聪明。

如果说对FeedDemon还有什么不满,那就是社会化功能太弱了。通过对比不同用户的订阅进行推荐,我觉得是很棒的功能。而NewsGator这方面的表现很差,不知是中文用户太少,还是别的什么缘故,NewsGator推荐给我的Feed一个我有兴趣的都没有。

于是我只好想起来了,就导出FeedDemon的OPML,再导入Google Reader和鲜果,根据推荐新增几个订阅。一直就这么波澜不惊的将就用着,直到Google推出了自己的浏览器Chrome,让我的心思又活泛起来了。

将Chrome作为日常首选,暂时还没可能。不过Chrome真的很快,无论是启动还是加载GMail、Google Reader,都很快。而且Chrome绑定了Google Gears,只要多等一会儿,在Chrome跑使用了Gears的Reader并不太慢,虽然还是比不了FeedDemon。

要不要换?我也不知道,看看再说吧。

Update

试着用了几天Chrome加Reader的组合,发现问题还不少……

  • Chrome的Flash支持非常糟,会占用大量CPU时间甚至造成卡死。只好删掉系统和plugin目录下的npswf32.dll,不用Chrome看Flash。
  • Reader不使用Feed的自定义Icon,找特定BSP的Feed时很不方便。
  • 当抓取Feed出问题的时候,GUI上没有任何直观的提示,必须打开Feed点show details。
  • 只有Stream一种内容呈现方式,滚动条越往下拉,内存占用越大。
  • Feed和Clipping都用Tag来分类,用起来挺不方便的。我个人的解决办法是用英文Tag分类Feed,用中文Tag做Clipping。
  • Clipping时添加Tag必须手工输入,不能点选。针对英文Tag有自动完成和提示,但用中文Tag就很麻烦。
  • Share和Share with note还不就是一回事,感觉有点啰嗦。
  • FeedDemon针对某些Feed,可以显示已有多少条评论,Reader没这功能。