1
7
2012
26

Haskell 实战:获取ArchLinux已安装的所有架构相关的软件包名

本文来自依云's Blog,转载请注明。

学而不用则惘。

任务内容

通过读取 pacman 数据库,获取本机已安装软件包中所有架构相关的软件包名。pacman 的数据库中,包描述文件位于/var/lib/pacman/local/*/desc,其中星号部分为软件包名加版本号。该文件中,%NAME%的下一行为软件包名,%ARCH%的下一行为架构,我这里是i686或者any。任务就是找出所有 i686 的软件包名。

任务解析

先写个纯函数,通过一块描述文本(Data.Text)判断这个包是否是架构相关的。类型声明为:

import qualified Data.Text as T
isArchDependent :: T.Text -> Bool

然后看看我们怎么才能办到这点。首先,用T.lines把这「块」文本解析成行的列表。然后我们来找为%ARCH%的这一行。怎么找呢,把前边的行丢掉好了:

(dropWhile (/= archstart)) . T.lines
  where archstart = T.pack "%ARCH%"

现在列表的第二项就是我们要的架构类别。先取两行,最后一行就是了:

last . (take 2) . (dropWhile (/= archstart)) . T.lines

然后做比较,得到最终的结果:

isArchDependent = (/= anyarch) . last . (take 2) . (dropWhile (/= archstart)) . T.lines
                  where archstart = T.pack "%ARCH%"
                        anyarch = T.pack "any"

知道一个包是不是我们要的了,但我们还不知道它的名字。此信息我可以肯定在第二行,就不慢慢 drop 了:

getPackageName :: T.Text -> T.Text
getPackageName = last . (take 2) . T.lines

再来个筛选函数,把将要显示的包描述信息找出来:

filterArchDependent :: [T.Text] -> [T.Text]
filterArchDependent = filter isArchDependent

接下来,是程序中「不纯」的部分。我们需要列出目录/var/lib/pacman/local下的所有目录,然后读取其中的desc文件。

getPackagePaths :: IO [FilePath]
getPackagePaths = (filter ((/= '.') . head)) `fmap` getDirectoryContents "."

getPackageDesc :: FilePath -> IO T.Text
getPackageDesc = TIO.readFile . (++ "/desc")

最后,把以上这些函数组合起来:

topDir = "/var/lib/pacman/local"

main = do
  setCurrentDirectory topDir
  getPackagePaths >>= mapM getPackageDesc >>= ((mapM TIO.putStrLn) . (map getPackageName) . filterArchDependent)

首先为了避免一大堆的路径拼接,进入topDir里边来。然后(main的第三行)写到:获取所有软件包的路径;对于每个路径,获取对应软件包的描述信息并处理;怎么处理呢?先过滤filterArchDependent,再逐个获取包名,最后把它打印出来。

代码

完整的代码如下:

import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import System.Directory (getDirectoryContents, setCurrentDirectory)
import Control.Monad

isArchDependent :: T.Text -> Bool
isArchDependent = (/= anyarch) . last . (take 2) . (dropWhile (/= archstart)) . T.lines
                  where archstart = T.pack "%ARCH%"
                        anyarch = T.pack "any"

filterArchDependent :: [T.Text] -> [T.Text]
filterArchDependent = filter isArchDependent

getPackageName :: T.Text -> T.Text
getPackageName = last . (take 2) . T.lines

topDir = "/var/lib/pacman/local"

getPackagePaths :: IO [FilePath]
getPackagePaths = (filter ((/= '.') . head)) `fmap` getDirectoryContents "."

getPackageDesc :: FilePath -> IO T.Text
getPackageDesc = TIO.readFile . (++ "/desc")

main = do
  setCurrentDirectory topDir
  getPackagePaths >>= mapM getPackageDesc >>= ((mapM TIO.putStrLn) . (map getPackageName) . filterArchDependent)

性能分析

我使用这个 Perl 脚本来计时,跑 20 次取平均时间。Shell 算起算术来太麻烦了 :-(

#!/usr/bin/perl
 
use Time::HiRes qw(gettimeofday);
 
sub gettime {
  my ($sec, $usec) = gettimeofday;
  $sec * 1000_100 + $usec;
}
 
my $times = 20;
my $start = gettime;
for(my $var = 0; $var < $times; $var++){
  `$ARGV[0]`;
}
my $end = gettime;
printf "%lfus\n", ($end - $start) / $times;

作为对照的是个 Python 脚本:

#!/usr/bin/env python3

import os

topDir = "/var/lib/pacman/local"

def checkPackage(file):
  for l in open(file):
    l = l.rstrip()
    if l == '%NAME%':
      next = 'name'
    elif l == '%ARCH%':
      next = 'arch'
    else:
      if next == 'name':
        name = l
      elif next == 'arch':
        return name, l != 'any'
      next = ''

def main():
  for name in os.listdir(topDir):
    if name.startswith('.'):
      continue
    file = '%s/%s/desc' % (topDir, name)
    name, show = checkPackage(file)
    if show:
      print(name)

if __name__ == '__main__':
  main()

这两个脚本长度都差不多,但效率相差挺显著的:

>>> ~tmp/t.pl './packagestat > /dev/null'
86055.100000us
>>> ~tmp/t.pl './packagestat.py > /dev/null'
248090.450000us

花絮

最开始,我用的是Data.Text.LazyData.Text.Lazy.IO这个包里的 Lazy 文本类型,结果是——

>>> ./packagestat
packagestat: glpng-1.45-4/desc: openFile: resource exhausted (Too many open files)

评论

写完这两个脚本,我体会到了Real World Haskell里说的,Even with years of experience, we remain astonished and pleased by how often our Haskell programs simply work on the first try, once we fix those compilation errors. Haskell 程序基本上编译通过后就能正确运行——只是要先修正各种编译错误。Python 那个跑了几遍才得到正确的结果。不过我觉得,除了 GHC 的强大之外,编写逻辑简单、没有状态变量也是正确率高的重要原因之一。

疑问

如果我想同时统计这些软件包的总大小(包描述信息里有),怎么才能只读一遍这些文件就同时做到这两件事呢?

Category: Haskell | Tags: Haskell | Read Count: 12103
李帅 说:
Jan 07, 2012 07:49:07 PM

沙发~你要做helloworld党?

hitsmaxft 说:
Jan 07, 2012 09:39:48 PM

翻墙过来留言
如果改为拼接字符串再一起输出会不会比较快呢, 速度提升还是比较明显的

hitsmaxft 说:
Jan 07, 2012 10:40:44 PM

刚才说快了, 自己动手测试了一下, 没多大区别

官方文档说多文件流应该用fileinput, 改成fileinput也没见提升, 反而时间更长了

Avatar_small
TBY 说:
Jan 08, 2012 12:02:03 AM

这里你用LazyIO会提示资源耗尽,是因为Lazy的openFile行为是这样的,先打开文件句柄,然后发现需要读的时候开始读,你用mapM去read一个List的话,会先打开所有List中的路径的文件句柄。理想的行为是这样的,Lazy地打开句柄(原先的错误在于打开不是Lazy的),Lazy地读。strict的缺点在于如果单个文件很大,则需要读入整个文件装进内存,这里当然没问题。继续使用LazyIO的方法如下,getPackageDesc = unsafeInterleaveIO . TIO.readFile . (++ "/desc"),需要导入模组System.IO.Unsafe。这样就可以Lazy地打开句柄,Lazy地读,Lazy地释放。具体的函数说明参考文档。至于LazyIO的一个十分有意义的讨论见这里:http://www.reddit.com/r/haskell/comments/cs54i/how_would_you_write_du_in_haskell

Avatar_small
TBY 说:
Jan 08, 2012 12:05:19 AM

当然,你要把上面相关函数替换成Lazy版本。

Avatar_small
TBY 说:
Jan 08, 2012 12:15:10 AM

你的那个疑问只要改写后半部分的处理函数即可,不过既要在IO里print,又要保留一份状态信息,可以考虑StateT。

Avatar_small
依云 说:
Jan 08, 2012 12:27:31 AM

哦,原来还有个 unsafeInterleaveIO,我是说怎么一下子就把全部文件都打开了,原来还有个不这样的版本。今天太晚了,明天继续研究。

Avatar_small
依云 说:
Jan 08, 2012 12:28:33 AM

是得翻墙,没办法 :-(

Avatar_small
依云 说:
Jan 08, 2012 12:30:07 AM

看第三遍时才意识到你说的是 Python 的 fileinput 模块……不过这个比较像 Perl 里的 <> 我喜欢,只是能够用时想不到。

Avatar_small
TBY 说:
Jan 08, 2012 12:32:44 AM

那个LazyIO的陷阱,几乎每个人都会撞上的,你很幸运,一开始就撞上了。囧。。。

Avatar_small
依云 说:
Jan 08, 2012 12:47:47 AM

嗯。。。这个我去掉就发现了,证据在这里—— http://garfileo.is-programmer.com/guestbook

Avatar_small
依云 说:
Jan 08, 2012 12:50:03 AM

s/去掉/去年/
重码字真多。。。

Avatar_small
Garfileo 说:
Jan 08, 2012 10:23:23 AM

我觉得跟 python 脚本那个效率对比不够有说服力。这个 haskell 程序是编译后运行的,而 python 程序则是直接由文本形式的脚本来运行的,每次运行的时候将解析器文本解析时间也算了进去,应当先将 python 脚本编译成字节码,然后再运行它。

Avatar_small
Garfileo 说:
Jan 08, 2012 10:24:56 AM

或者拿 runghc 去跟 python 脚本对比 :)

Avatar_small
依云 说:
Jan 08, 2012 12:46:34 PM

实践证明 Python 的编译速度是很快的:
>>> simpletime 'python packagestat.pyc'
240679.900000us

Avatar_small
依云 说:
Jan 08, 2012 12:48:10 PM

还有加 -OO 优化的版本:
>>> simpletime 'python packagestat.pyo'
234202.150000us

Avatar_small
依云 说:
Jan 08, 2012 09:15:40 PM

试过了,strict 版本比 lazy 版本更快、占用内存更少。。。
>>> simpletime ./packagestat
209428.450000us
>>> simpletime ./packagestat_lazy
252819.650000us
为什么。。。
RTS 统计信息:http://p.vim-cn.com/cxx

Avatar_small
依云 说:
Jan 08, 2012 09:17:45 PM

贴错了: http://p.vim-cn.com/cxy

Avatar_small
TBY 说:
Jan 08, 2012 10:02:05 PM

Lazy速度上不如Strict是正常的,Lazy需要生成thunk再eval。内存占用上,如果文件很小,也会比strict多。具体的话,要dump core看了。
這裏有一篇Haskell Heap的blog,很生動,要翻牆:http://blog.ezyang.com/2011/04/the-haskell-heap/

Avatar_small
TBY 说:
Jan 08, 2012 10:11:09 PM

Lazy超過Strict版本,需要高階函數能夠deforestation和fusion,也就是在高階函數中盡可能的不生成中間結構體,具體可以參看斯坦福那章Library Level Optimization.

Avatar_small
TBY 说:
Jan 08, 2012 10:15:35 PM

Algorithms──A Functional Programming Approach ,這本比較基礎的,我覺得你已經可以看了。
http://book.douban.com/subject/1949067/
搜得到的。
Pearls of Functional Algorithm Design,這本更深點,這裏有下:http://www.ppurl.com/2010/11/pearls-of-functional-algorithm-design.html

Avatar_small
依云 说:
Jan 08, 2012 10:37:27 PM

没有皮皮书屋的帐号,也注册不了。。。

Avatar_small
依云 说:
Jan 08, 2012 10:41:53 PM

还是去新浪爱问下到了。好多书要看啊。。。

MaskRay 说:
Mar 03, 2012 02:56:09 PM

用 Attoparsec 的 Word8 会更快,不过没太大意义

Avatar_small
依云 说:
Mar 03, 2012 06:06:29 PM

有意义啊,学习的意义啊!


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter

| Theme: Aeros 2.0 by TheBuckmaker.com