Real World Haskell - Chapter 8. Efficient File Processing, Regular Expressions, and Filename

 

 

 

Chapter 8. Efficient File Processing, Regular Expressions, and Filename

  Matching

 

Efficient File Processing

 

String 来进行I/O 操作性能很糟糕

 

-- in.txt

{-

10

11

12

-}

main = do

        contents <- readFile "c:/in.txt" -- getContents

        print (sumFile contents)

            where sumFile = sum . map read . words

 

{-

let numbers = ["10","11","12"]

let rr = (map read numbers)::[Int]  -- read 返因的类型是a ,最后map 返回的是[a],所以要显示类型转换。

print rr

-}

 

注意:read 返回的类型是a ,可以显示转成Int Dobule 等,但转成String 是会出错的。因为read 就是用于解析的,读一个串,再转成一个串没有意义。

 

虽然String 类型是读写文件的默认设置,但是效率不高。上面这段代码的性能非常糟糕。

 

 [Char] 中的元素都是是单独分配,而且有一些bookkeeping overhead

 

对于I/O bytestring 库是一种比String 更快,开销更小的选择。

 

Data.ByteString 库中的ByteString 是真正的二进制或文本串(想像一下C 语言的串)

 

Data.ByteString.Lazy 库中的ByteString 是一个chunk list(注意这里list 的含义,不知道是不是指列表的意思),最大64 KB

 

如果数据有数百MB 或数百TB ,则通常Data.ByteString.Lazy 的性能最好。它的chunk size 对现代CPU L1 cache 比较友好。垃圾收集器能很快地丢弃不再使用的chunk 流数据。

 

Binary I/O and Qualified Imports

 

测试一个文件是否是linux 可执行文件

 

-- 需要在IDE 中加入bytestring 包,保存后重启IDE 才会生效。

module Main where

 

import qualified Data.ByteString.Lazy as L  -- 注意as L 的写法

import Data.Word

 

hasElfMagic :: L.ByteString -> Bool

hasElfMagic content = L.take 4 content == elfMagic

    where elfMagic = L.pack [0x7f, 0x45, 0x4c, 0x46]

 

isElfFile :: FilePath -> IO Bool

isElfFile path = do

    content <- L.readFile path

    return (hasElfMagic content)

 

main = do

    let aa = 0x7f::Word8

    isElf <- isElfFile "c:/echo"

    print isElf

 

echo linux 中的可执行文件。

 

Hakell 中用Word8 来表示byte

 

ByteString 一次最多读64 Kchunk

 

lazy ByteString 就是为binary I/O 准备的。


 

Text I/O

 

prices.csv 文件中的数据记录了每月股票价格,如何找出最高收盘价?

 

计算最高股票收盘价

 

-- prices.csv

{-

Date,Open,High,Low,Close,Volume,Adj Close

2008-08-01,20.09,20.12,19.53,19.80,19777000,19.80

2008-06-30,21.12,21.20,20.60,20.66,17173500,20.66

2008-05-30,27.07,27.10,26.63,26.76,17754100,26.76

2008-04-30,27.17,27.78,26.76,27.41,30597400,27.41

-}

 

import qualified Data.ByteString.Lazy.Char8 as L

 

closing = readPrice . (!!4) . L.split ','  -- point-free style

 

readPrice :: L.ByteString -> Maybe Int

readPrice str =

    case L.readInt str of

        Nothing -> Nothing

        Just (dollars,rest) ->

            case L.readInt (L.tail rest) of

                Nothing -> Nothing

                Just (cents,more) ->

                    Just (dollars * 100 + cents)

 

highestClose = maximum . (Nothing:) . map closing . L.lines  -- 亮点,注意(Nothing:) 的用法,“: 是一个列表构造器,左边已经有一个元素参数了,右边还需要一个列表参数。

 

highestCloseFrom path = do

    contents <- L.readFile path

    print (highestClose contents)

 

 

main = do

    contents <- L.readFile "c:/prices.csv"

    let byteStrs = L.lines contents

    print byteStrs -- >[Chunk "Date,Open,High,Low,Close,Volume,Adj Close/r" Empty,Chunk "2008-08-01,20.09,20.12,19.53,19.80,19777000,19.80/r" Empty,Chunk "2008-06-30,21.12,21.20,20.60,20.66,17173500,20.66/r" Empty,Chunk "2008-05-30,27.07,27.10,26.63,26.76,17754100,26.76/r" Empty,Chunk "2008-04-30,27.17,27.78,26.76,27.41,30597400,27.41" Empty]

    let byteStr = (!!1) byteStrs

    print byteStr  -- >Chunk "2008-08-01,20.09,20.12,19.53,19.80,19777000,19.80/r" Empty

    let byteStrs' = L.split ',' byteStr

    print byteStrs'  -- >[Chunk "2008-08-01" Empty,Chunk "20.09" Empty,Chunk "20.12" Empty,Chunk "19.53" Empty,Chunk "19.80" Empty,Chunk "19777000" Empty,Chunk "19.80/r" Empty]

    let byteStr' = (!!4) byteStrs' 

    print byteStr'  -- >Chunk "19.80" Empty  -- 类型是L.ByteString

    let price = readPrice byteStr'

    print price -- >Just 1980

 

    let prices =  map (readPrice.(!!4) . L.split ',') (L.lines contents)

    print prices  -- >[Nothing,Just 1980,Just 2066,Just 2676,Just 2741]  -- Nothing 的出现是因为文本开头的那段英文

 

    let prices' = Nothing: prices  -- 亮点,加一个Nothing 元素保证列表不空,因为有些函数一遇上空列表就出错。

    print prices' -- >[Nothing,Nothing,Just 1980,Just 2066,Just 2676,Just 2741] 

 

    let maxprice = maximum prices  -- maximum 接受一个有序集

    print maxprice -- > Just 2741

 

   

    highestCloseFrom "c:/prices.csv"

    print $ highestClose L.empty  -- 测试没有数据文件的情形  -- L.readFile L.empty 返回Nothingmaximum [Nothing] 返回Nothing

    putStr =<< readFile "c:/prices.csv"  -- readFile 返回一个monad ,“=<< 运行符取出monad 中的String,然后传给putStr

 

(!!4) 运算符从list 中取第4 个元素。

 

L.readInt 用于解析一个整数。返回一个整数和剩余String pair ,并封装在Mabe 中。

 

我们用了一个技巧来绕过这个事实,既不能将maximum 应用于空列表。因为如果不存在股票数据时,我们不希望程序抛出异常。

 

(Nothing:) 表达式保证Maybe Int 的列表不空。

 

因为已经将I/O 和逻辑分开,所以我们能够测试没有数据文件的情形:print $ highestClose L.empty


 

Filename Matching

 

三种模式:glob patternswild card patterns shell-style patterns

 

*”表示匹配任意字串

 

?”表示匹配任意一个字符

 

[]”表示匹配括号里的任意内容,可以在括号里加“!”表示“不是”。

 

a–z”表示匹配a z

 

一个例子,pic[0-9].[pP][nN][gG]


 

Regular Expressions in Haskell

 

Haskell 的正则表达式库比其它语言要丰富。

 

=~ 操作符大量用于多态,结果是造成这个操作符的签名很难理解,所以这里暂时不做介绍。

 

=~ 操作符的两个参数都是typeclass,而且返回值也是typeclass 。左边的参数是要匹配的text ,右边的参数是一个模式(正则表达式)

 

=~ 操作符的参数既可以是String 也可以是ByteString


 

The Many Types of Result

 

出错:“unable to load package `regex-posix-0.94.2 的解决

 

{-

:module +Text.Regex.Posix

 

"your right hand" =~ "(hand|foot)" :: Bool

 

出错:  : unable to load package `regex-posix-0.94.2'

 

cmd 下运行以下两个命令:

 

cabal update

 

cabal install regex-posix

-}

 

RegexContext 是一个typeclass ,它描述了target 类型应具有的行为。

 

Bool 类型是RegexContext 的实例,Int 也是RegexContext 的实例。

 

=~ 是个多态模式匹配运算符,根据你所指定的返回值类型不同它完成的功能也不同。Bool 类型的返回值,它的功能是验证存在性;Int 类型的返回值,它的功能是计算发生成功匹配的次数。

 

 

Bool 返回值,“=~ 的功能是验证是否能够成功匹配

 

在整个表达式的后面加“:: Bool 表示表达式的结果类型是Bool ;当ghci 不能自动推断结果类型时可以这样做。

 

使用“=~ 运算符测试模式是否可以成功匹配

 

"your right hand" =~ "(hand|foot)" :: Bool  -- >True  -- 可以成功匹配

 

Int 返回值,“=~ 的功能是计算发生成功匹配的次数

 

使用“=~ 运算符测试模式可以成功匹配多少次

 

"honorificabilitudinitatibus" =~ "[aeiou]" :: Int  -- >13  -- 计算一个字串含有多少个元音

 

 

String 返回值,“=~ 的功能是捕获第一个成功匹配的串,如匹配不能功就返回空串。

 

使用“=~ 运算符捕获第一个成功匹配的串

 

"I, B. Ionsonii, uurit a lift'd batch" =~ "(uu|ii)" :: String  -- >"ii"  -- 捕获第一个匹配

 

 [String] 返回值,“=~ 的功能是捕获所有匹配,然后封装进list 并返回

 

使用“=~ 运算符捕获所有成功匹配的串

 

"I, B. Ionsonii, uurit a lift'd batch" =~ "(uu|ii)" :: [String]  -- >["ii","uu"]

 

BoolIntString[String] 都是简单“result types”,还没完呢。

 

(String,String,String) 返回值,“=~ 的功能是返回第一次成功匹配的位置之前的串,成功匹配的串,成功匹配的位置之后的串。如果匹配失败,就只返回之前的串,后接两空串。

 

使用“=~ 运算符,以模式为准将字串分成三部分

 

pat = "(foo[a-z]*bar|quux)"

"before foodiebar after" =~ pat :: (String,String,String)

-- >("before ","foodiebar"," after")

 

使用“=~ 运算符,以模式为准将字串分成三部分,最后在加上[“成功匹配的串”]

 

pat = "(foo[a-z]*bar|quux)"

"before foodiebar after" =~ pat :: (String,String,String,[String])

-- >("before ","foodiebar"," after",["foodiebar"])

 

使用“=~ 运算符,计算第一次成功匹配串的位置和长度

 

-- pat = "(foo[a-z]*bar|quux)"

-- "i foobarbar a quux" =~ pat :: (Int,Int)

-- >(2,9)

 

使用“=~ 运算符,计算全部成功匹配串的位置和长度

 

-- pat = "(foo[a-z]*bar|quux)"

-- "i foobarbar a quux" =~ pat :: [(Int,Int)]  -- 出错,待解决

-- >[(2,9),(14,4)]

 

还有更多RegexContext classtype 的实例,请参见:Text.Regex.Base.Context


 

More About Regular Expressions

 

Mixing and Matching String Types

 

 =~ 是以typeclass 作为其参数和返回值的类型。

 

-- :type pack "foo"

-- >pack "foo" :: ByteString

 

是否匹配?

-- pack "foo" =~ "bar" :: Bool  -- 左边用“ByteString 参数,右边用“String 参数(模式)

-- > false  -- 不匹配

 

匹配多少个?

"foo" =~ pack "bar" :: Int  -- 右边用“ByteString 参数(模式),左边用“String 参数

-- >0  -- 零个

 

获得所有匹配的位置和长度

pack "foo" =~ pack "o" :: [(Int, Int)]  -- 错误,待解决

-- >[(1,1),(2,1)]

 

注意这种错误

 

pack "good food" =~ ".ood" :: [ByteString]  -- ok

"good food" =~ ".ood" :: [ByteString]  -- Error

"good food" =~ ".ood" :: [String]  -- OK

 

Other Things You Should Know

 

当你看Haskell 库文档时会发现有许多正则表达式相关的模块。这些模块在Text.Regex.Base 下定义了通用API 接口。可以有多种实现同时存在,在本书写作时,GHC Text.Regex.Posix 作为默认的正则表达式实现,这个包提供了POSIX 正则语义。

 

其它的库可以从Hackage 下载,其中一些比POSIX engine 的性能要好,如regex-tdfa。它们都有相同的接口。

 

Translating a glob Pattern into a Regular Expression

 

module GlobRegex (globToRegex, fnmatch) where

 

import Text.Regex.Posix ((=~))

 

globToRegex :: String -> String

globToRegex cs = '^' : globToRegex' cs ++ "$"

 

globToRegex' :: String -> String

globToRegex' "" = ""

globToRegex' ('*':cs) = ".*" ++ globToRegex' cs

globToRegex' ('?':cs) = '.' : globToRegex' cs

globToRegex' ('[':'!':c:cs) = "[^" ++ c : charClass cs

globToRegex' ('[':c:cs) = '[' : c : charClass cs

globToRegex' ('[':_) = error "unterminated character class"

globToRegex' (c:cs) = escape c ++ globToRegex' cs

 

escape :: Char -> String

escape c | c `elem` regexChars = '//' : [c]

         | otherwise = [c]

    where regexChars = "//+()^$.{}]|"

 

charClass :: String -> String

charClass (']':cs) = ']' : globToRegex' cs

charClass (c:cs) = c : charClass cs

charClass [] = error "unterminated character class"

 

matchesGlob:: FilePath -> String -> Bool

name ` matchesGlob` pat = name =~ globToRegex pat

 

 

-- globToRegex "f??.c" 

-- >"^f..//.c$"

 

-- "foo.c" =~ globToRegex "f??.c" :: Bool 

-- >True

 

-- "^f..//.c$" 风格的模式

-- "foo.c" =~ "^f..//.c$" :: Bool 

-- >True

 

文件名匹配

fnmatch :: FilePath -> String -> Bool

name `fnmatch` pat = name =~ globToRegex pat

 

fnmatch "*.cvs" "abc.cvs"  -- > True

fnmatch "*.cvs" "abc.c"  -- > False

 

An important Aside: Writing Lazy Functions

 

我们看到globToRegex' 是递归的,但不是尾递归的。

 

Making Use of Our Pattern Matcher

 

错误:“ Ambiguous type variable `e' in the constraint: 的解决

 

{-

 

Real World Haskell 使用的是旧接口的handle 函数。

 

需要将 import Control.Exception (handle)

改成:import Control.OldException (handle)

-}

 

 

System.FilePath 模块抽象了操作系统处理路径名的细节。

 

使用“</> 函数将两个路径components 连接起来

 

-- import System.FilePath

"foo" </> "bar"  -- >"foo//bar"

 

使用dropTrailingPathSeparator 去掉路径最后面的“/

 

-- import System.FilePath

dropTrailingPathSeparator "foo/"  -- >"foo"

 

使用splitFileName 函数把路径和文件名分开

 

-- import System.FilePath

splitFileName "foo/bar/Quux.hs"  -- >("foo/bar/","Quux.hs")

splitFileName "zippity"  -- >("","zippity")

 

forM 函数将它的第二个参数(一个action),作用于第一个参数(一个列表),并返回result 的列表。

 

const 函数总是返回第一个参数,不管第二个参数是什么。

 

Handling Errors Through API Design

 


 

Putting Our Code to Work

 

flip 函数用来改变另一个函数的参数顺序。

 

C 盘下的“*.cc 文件全部改名成“*.cpp

 

-- running.bat

@echo off

runghc main.hs && Pause

 

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

-- main.hs

module Main where

 

import Glob

import RenameFiles

 

 

main = do

    -- 获得C 盘下所有“.cc 文件

    strs <- namesMatching "c:/*.cc"  -- ["c:/2.cc","c:/1.cc"]

    print strs

 

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

 

-- GlobRegex.hs

module GlobRegex (globToRegex, matchesGlob) where

 

import Text.Regex.Posix ((=~))

 

globToRegex :: String -> String

globToRegex cs = '^' : globToRegex' cs ++ "$"

 

globToRegex' :: String -> String

globToRegex' "" = ""

globToRegex' ('*':cs) = ".*" ++ globToRegex' cs

globToRegex' ('?':cs) = '.' : globToRegex' cs

globToRegex' ('[':'!':c:cs) = "[^" ++ c : charClass cs

globToRegex' ('[':c:cs) = '[' : c : charClass cs

globToRegex' ('[':_) = error "unterminated character class"

globToRegex' (c:cs) = escape c ++ globToRegex' cs

 

escape :: Char -> String

escape c | c `elem` regexChars = '//' : [c]

         | otherwise = [c]

    where regexChars = "//+()^$.{}]|"

 

charClass :: String -> String

charClass (']':cs) = ']' : globToRegex' cs

charClass (c:cs) = c : charClass cs

charClass [] = error "unterminated character class"

 

matchesGlob :: FilePath -> String -> Bool

name `matchesGlob` pat = name =~ globToRegex pat

 

 

-- globToRegex "f??.c"

-- >"^f..//.c$"

 

-- "foo.c" =~ globToRegex "f??.c" :: Bool

-- >True

 

-- "^f..//.c$" 风格的模式

-- "foo.c" =~ "^f..//.c$" :: Bool

-- >True

 

-- 文件名匹配

 

-- matchesGlob "*.cvs" "abc.cvs"  -- > True

-- matchesGlob "*.cvs" "abc.c"  -- > False

 

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

-- Glob.hs

 

module Glob (

              namesMatching

            ) where

 

import System.FilePath (dropTrailingPathSeparator, splitFileName, (</>))

 

import System.Directory (doesDirectoryExist, doesFileExist,

                            getCurrentDirectory, getDirectoryContents)

 

import Control.OldException (handle)

import Control.Monad (forM)

import GlobRegex (matchesGlob)

 

 

isPattern :: String -> Bool

isPattern = any (`elem` "[*?")

 

namesMatching pat

    | not (isPattern pat) = do  -- 如果我们传的String 不含pattern ,就简单的检查给定的名字是否在文件系统中。

      exists <- doesNameExist pat

      return (if exists then [pat] else [])

    | otherwise = do    -- 处理glob pattern

      case splitFileName pat of

        ("", baseName) -> do

            curDir <- getCurrentDirectory

            listMatches curDir baseName

        (dirName, baseName) -> do

            dirs <- if isPattern dirName

                    then namesMatching (dropTrailingPathSeparator dirName)

                    else return [dirName]

            let listDir = if isPattern baseName

                          then listMatches

                          else listPlain

            pathNames <- forM dirs $ /dir -> do

                            baseNames <- listDir dir baseName

                            return (map (dir </>) baseNames)

            return (concat pathNames)

 

doesNameExist :: FilePath -> IO Bool

doesNameExist name = do

    fileExists <- doesFileExist name

    if fileExists

       then return True

       else doesDirectoryExist name

 

listMatches :: FilePath -> String -> IO [String]

listMatches dirName pat = do

    dirName' <- if null dirName

                then getCurrentDirectory

                else return dirName

    handle (const (return [])) $ do

        names <- getDirectoryContents dirName'

        let names' = if isHidden pat

                     then filter isHidden names

                     else filter (not . isHidden) names

        return (filter (`matchesGlob` pat) names')

 

isHidden ('.':_) = True

isHidden _ = False

 

listPlain :: FilePath -> String -> IO [String]

listPlain dirName baseName = do

    exists <- if null baseName

              then doesDirectoryExist dirName

              else doesNameExist (dirName </> baseName)

    return (if exists then [baseName] else [])

 

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

-- RenameFiles.hs

 

module RenameFiles (

    --rename

   cc2cpp

) where

 

import System.FilePath (replaceExtension)

import System.Directory (doesFileExist, renameDirectory, renameFile)

import Glob (namesMatching)

 

 

renameWith :: (FilePath -> FilePath)

           -> FilePath

           -> IO FilePath

 

renameWith f path = do

    let path' = f path

    rename path path'

    return path'

 

rename :: FilePath -> FilePath -> IO ()

rename old new = do

    isFile <- doesFileExist old

    let f = if isFile then renameFile else renameDirectory

    f old new

 

cc2cpp =

    mapM (renameWith (flip replaceExtension ".cpp")) =<< namesMatching "*.cc"

 

flip 函数用来改变另一个函数的参数顺序。

 

获得C 盘下所有“.cc 文件

 

-- namesMatching "c:/*.cc"

 

-- strs <- namesMatching "c:/*.cc"  -- >["c:/2.cc","c:/1.cc"]

-- replaceExtension :: FilePath -> String -> FilePath

-- replaceExtension "c:/1.cc" ".cpp"  -- >"c:/1.cpp"

-- flip replaceExtension ".cpp" "c:/1.cc"   -- >"c:/1.cpp"

-- (flip replaceExtension ".cpp") "c:/1.cc"  -- >"c:/1.cpp"

-- rename "c:/1.cc" "c:/1.cpp"  -- 1.cc 被改名成1.cpp

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值