如何读取(和解析)一个文件,然后附加到同一个文件而不会出现异常?
How do I read (and parse) a file and then append to the same file without getting an exception?
我正在尝试从 Haskell 中正确读取文件,但我似乎遇到了这个错误。
*** 异常:neo.txt:openFile:资源繁忙(文件已锁定)
这是我的代码。
import Data.Char
import Prelude
import Data.List
import Text.Printf
import Data.Tuple
import Data.Ord
import Control.Monad
import Control.Applicative((<*))
import Text.Parsec
( Parsec, ParseError, parse -- Types and parser
, between, noneOf, sepBy, many1 -- Combinators
, char, spaces, digit, newline -- Simple parsers
)
这些是电影字段。
type Title = String
type Director = String
type Year = Int
type UserRatings = (String,Int)
type Film = (Title, Director, Year , [UserRatings])
type Period = (Year, Year)
type Database = [Film]
这是为了从文件中正确读取所有类型的解析
-- Parse a string to a string
stringLit :: Parsec String u String
stringLit = between (char '"') (char '"') $ many1 $ noneOf "\"\n"
-- Parse a string to a list of strings
listOfStrings :: Parsec String u [String]
listOfStrings = stringLit `sepBy` (char ',' >> spaces)
-- Parse a string to an int
intLit :: Parsec String u Int
intLit = fmap read $ many1 digit
-- Or `read <$> many1 digit` with Control.Applicative
stringIntTuple :: Parsec String u (String , Int)
stringIntTuple = liftM2 (,) stringLit intLit
film :: Parsec String u Film
film = do
-- alternatively `title <- stringLit <* newline` with Control.Applicative
title <- stringLit
newline
director <- stringLit
newline
year <- intLit
newline
userRatings <- stringIntTuple
newline
return (title, director, year, [userRatings])
films :: Parsec String u [Film]
films = film `sepBy` newline
这是主程序(在winghci中写入"main"启动程序)
-- The Main
main :: IO ()
main = do
putStr "Enter your Username: "
name <- getLine
filmsDatabase <- loadFile "neo.txt"
appendFile "neo.txt" (show filmsDatabase)
putStrLn "Your changes to the database have been successfully saved."
这是加载文件函数
loadFile :: FilePath -> IO (Either ParseError [Film])
loadFile filename = do
database <- readFile filename
return $ parse films "Films" database
另一个 txt 文件名为 neo 并且包括一些像这样的电影
"Blade Runner"
"Ridley Scott"
1982
("Amy",5), ("Bill",8), ("Ian",7), ("Kevin",9), ("Emma",4), ("Sam",7), ("Megan",4)
"The Fly"
"David Cronenberg"
1986
("Megan",4), ("Fred",7), ("Chris",5), ("Ian",0), ("Amy",6)
只需复制粘贴所有内容,包括同一目录中的 txt 文件,然后对其进行测试以查看我描述的错误。
问题是 readFile
实际上并没有立即将整个文件读入内存;它打开文件并立即 returns 一个字符串。当您 "look at" 字符串时,正在读取文件的幕后。因此,当 readFile
returns 时,文件仍会打开以供阅读,您无法对其进行任何其他操作。这称为 "lazy I/O",许多人认为它是 "evil",正是因为它会导致像您目前遇到的问题。
您可以通过多种方式解决此问题。可能最简单的方法是在继续之前将整个字符串强制存入内存。计算字符串的 length
就可以做到这一点——但前提是你 "use" 某些东西的长度,因为长度本身是惰性的。 (看看这如何迅速变得混乱?这就是人们避免懒惰的原因 I/O。)
您可以尝试的最简单的方法是在您尝试追加到数据库之前打印加载的电影数量。
main = do
putStr "Enter your Username: "
name <- getLine
filmsDatabase <- loadFile "neo.txt"
putStrLn $ "Loaded " ++ show (length filmsDatabase) ++ " films."
appendFile "neo.txt" (show filmsDatabase)
putStrLn "Your changes to the database have been successfully saved."
看起来很简单的打印消息实际上是使代码正常工作的基础,这有点邪恶!
另一种方法是以不同的名称保存新数据库,然后删除旧文件并在旧文件之上重命名新文件。这样做的好处是,如果程序在保存过程中途崩溃,您不会丢失所有东西。
哎呀雏菊,懒惰
倾向于 to make file changes crazy.
文件的 not closed, as supposed
因此 the error 被强制执行。
这个小诡计,作者loadFile
是你必须调和的。
但不要担心,至少现在还没有,
我会告诉你,让我们开始吧。
与在 System.IO
中使用 IO
的许多其他函数一样,readFile
实际上并不消耗任何输入。它很懒惰。因此,文件 doesn't get closed,除非它的所有内容都已被消耗(然后半关闭):
The file is read lazily, on demand, as with getContents
.
我们可以用一个更短的例子来证明这一点:
main = do
let filename = "/tmp/example"
writeFile filename "Hello "
contents <- readFile filename
appendFile filename "world!" -- error here
这会失败,因为我们从未真正检查过 contents
(完全)。如果你得到了所有的内容(例如打印,length
或类似的),它就不会再失败了:
main = do
let filename = "/tmp/example2"
writeFile filename "Hello "
content <- readFile filename
putStrLn content
appendFile filename "world!" -- no error
因此,我们需要 真正 关闭文件的东西,或者我们需要确保在尝试追加到文件之前我们已经阅读了所有内容。
例如,您可以将 withFile
与某些 "magic" 函数 force
一起使用,以确保内容真正得到评估:
readFile' filename = withFile filename ReadMode $ \handle -> do
theContent <- hGetContents handle
force theContent
然而,force
很难实现。您可以使用 bang patterns, but this will evaluate the list only to WHNF (basically just the first character). You could use the functions by deepseq
,但这会增加另一个依赖项,并且可能不允许在您的 assignment/exercise.
中使用
或者您可以使用任何函数,以某种方式确保所有元素都被评估或排序。在这种情况下,我们可以使用一个小技巧 mapM return
:
readFile' filename = withFile filename ReadMode $ \handle -> do
theContent <- hGetContents handle
mapM return theContent
这已经足够好了,但您可以在生产中使用 pipes
或 conduit
之类的东西。
另一种方法是确保我们确实使用了所有内容。这可以通过使用另一个 parsec 解析器方法来完成,即 runParserT
。我们可以将其与上面的 withFile
方法结合起来:
parseFile :: ParsecT String () IO a -> FilePath -> IO (Either ParseError a)
parseFile p filename = withFile filename ReadMode $ \handle ->
hGetContents handle >>= runParserT p () filename
同样,withFile
确保我们关闭文件。我们现在可以在您的 loadFilm
:
中使用它
loadFile :: FilePath -> IO (Either ParseError [Film])
loadFile filename = parseFile films filename
此版本的 loadFile
将不再锁定文件。
我正在尝试从 Haskell 中正确读取文件,但我似乎遇到了这个错误。
*** 异常:neo.txt:openFile:资源繁忙(文件已锁定) 这是我的代码。
import Data.Char
import Prelude
import Data.List
import Text.Printf
import Data.Tuple
import Data.Ord
import Control.Monad
import Control.Applicative((<*))
import Text.Parsec
( Parsec, ParseError, parse -- Types and parser
, between, noneOf, sepBy, many1 -- Combinators
, char, spaces, digit, newline -- Simple parsers
)
这些是电影字段。
type Title = String
type Director = String
type Year = Int
type UserRatings = (String,Int)
type Film = (Title, Director, Year , [UserRatings])
type Period = (Year, Year)
type Database = [Film]
这是为了从文件中正确读取所有类型的解析
-- Parse a string to a string
stringLit :: Parsec String u String
stringLit = between (char '"') (char '"') $ many1 $ noneOf "\"\n"
-- Parse a string to a list of strings
listOfStrings :: Parsec String u [String]
listOfStrings = stringLit `sepBy` (char ',' >> spaces)
-- Parse a string to an int
intLit :: Parsec String u Int
intLit = fmap read $ many1 digit
-- Or `read <$> many1 digit` with Control.Applicative
stringIntTuple :: Parsec String u (String , Int)
stringIntTuple = liftM2 (,) stringLit intLit
film :: Parsec String u Film
film = do
-- alternatively `title <- stringLit <* newline` with Control.Applicative
title <- stringLit
newline
director <- stringLit
newline
year <- intLit
newline
userRatings <- stringIntTuple
newline
return (title, director, year, [userRatings])
films :: Parsec String u [Film]
films = film `sepBy` newline
这是主程序(在winghci中写入"main"启动程序)
-- The Main
main :: IO ()
main = do
putStr "Enter your Username: "
name <- getLine
filmsDatabase <- loadFile "neo.txt"
appendFile "neo.txt" (show filmsDatabase)
putStrLn "Your changes to the database have been successfully saved."
这是加载文件函数
loadFile :: FilePath -> IO (Either ParseError [Film])
loadFile filename = do
database <- readFile filename
return $ parse films "Films" database
另一个 txt 文件名为 neo 并且包括一些像这样的电影
"Blade Runner"
"Ridley Scott"
1982
("Amy",5), ("Bill",8), ("Ian",7), ("Kevin",9), ("Emma",4), ("Sam",7), ("Megan",4)
"The Fly"
"David Cronenberg"
1986
("Megan",4), ("Fred",7), ("Chris",5), ("Ian",0), ("Amy",6)
只需复制粘贴所有内容,包括同一目录中的 txt 文件,然后对其进行测试以查看我描述的错误。
问题是 readFile
实际上并没有立即将整个文件读入内存;它打开文件并立即 returns 一个字符串。当您 "look at" 字符串时,正在读取文件的幕后。因此,当 readFile
returns 时,文件仍会打开以供阅读,您无法对其进行任何其他操作。这称为 "lazy I/O",许多人认为它是 "evil",正是因为它会导致像您目前遇到的问题。
您可以通过多种方式解决此问题。可能最简单的方法是在继续之前将整个字符串强制存入内存。计算字符串的 length
就可以做到这一点——但前提是你 "use" 某些东西的长度,因为长度本身是惰性的。 (看看这如何迅速变得混乱?这就是人们避免懒惰的原因 I/O。)
您可以尝试的最简单的方法是在您尝试追加到数据库之前打印加载的电影数量。
main = do
putStr "Enter your Username: "
name <- getLine
filmsDatabase <- loadFile "neo.txt"
putStrLn $ "Loaded " ++ show (length filmsDatabase) ++ " films."
appendFile "neo.txt" (show filmsDatabase)
putStrLn "Your changes to the database have been successfully saved."
看起来很简单的打印消息实际上是使代码正常工作的基础,这有点邪恶!
另一种方法是以不同的名称保存新数据库,然后删除旧文件并在旧文件之上重命名新文件。这样做的好处是,如果程序在保存过程中途崩溃,您不会丢失所有东西。
哎呀雏菊,懒惰
倾向于 to make file changes crazy.
文件的 not closed, as supposed
因此 the error 被强制执行。
这个小诡计,作者loadFile
是你必须调和的。
但不要担心,至少现在还没有,
我会告诉你,让我们开始吧。
与在 System.IO
中使用 IO
的许多其他函数一样,readFile
实际上并不消耗任何输入。它很懒惰。因此,文件 doesn't get closed,除非它的所有内容都已被消耗(然后半关闭):
The file is read lazily, on demand, as with
getContents
.
我们可以用一个更短的例子来证明这一点:
main = do
let filename = "/tmp/example"
writeFile filename "Hello "
contents <- readFile filename
appendFile filename "world!" -- error here
这会失败,因为我们从未真正检查过 contents
(完全)。如果你得到了所有的内容(例如打印,length
或类似的),它就不会再失败了:
main = do
let filename = "/tmp/example2"
writeFile filename "Hello "
content <- readFile filename
putStrLn content
appendFile filename "world!" -- no error
因此,我们需要 真正 关闭文件的东西,或者我们需要确保在尝试追加到文件之前我们已经阅读了所有内容。
例如,您可以将 withFile
与某些 "magic" 函数 force
一起使用,以确保内容真正得到评估:
readFile' filename = withFile filename ReadMode $ \handle -> do
theContent <- hGetContents handle
force theContent
然而,force
很难实现。您可以使用 bang patterns, but this will evaluate the list only to WHNF (basically just the first character). You could use the functions by deepseq
,但这会增加另一个依赖项,并且可能不允许在您的 assignment/exercise.
或者您可以使用任何函数,以某种方式确保所有元素都被评估或排序。在这种情况下,我们可以使用一个小技巧 mapM return
:
readFile' filename = withFile filename ReadMode $ \handle -> do
theContent <- hGetContents handle
mapM return theContent
这已经足够好了,但您可以在生产中使用 pipes
或 conduit
之类的东西。
另一种方法是确保我们确实使用了所有内容。这可以通过使用另一个 parsec 解析器方法来完成,即 runParserT
。我们可以将其与上面的 withFile
方法结合起来:
parseFile :: ParsecT String () IO a -> FilePath -> IO (Either ParseError a)
parseFile p filename = withFile filename ReadMode $ \handle ->
hGetContents handle >>= runParserT p () filename
同样,withFile
确保我们关闭文件。我们现在可以在您的 loadFilm
:
loadFile :: FilePath -> IO (Either ParseError [Film])
loadFile filename = parseFile films filename
此版本的 loadFile
将不再锁定文件。