【译】用 parsec 库解析 json - Haskell

378 阅读5分钟

简单的 JSON 解析器 注释版

{
  "rsvp_limit": 15,
  "status": "upcoming",
  "visibility": "public",
  "venue": {
    "name": "Hyperconnect, Inc",
    "address": "14F, 5 Seocho-daero 78-gil, Seoc..",
    "city": "Seoul",
    "country": "kr",
    "lat": 37.49721,
    "lon": 127.027374
  },
  "id": "140930019688259",
  "time": 1474326000,
  "event_url": "https://www.facebook.com/events/14..",
  "name": "Simple JSON Parser",
  "group": {
    "id": 1065398240148353,
    "name": "Haskell School",
    "join_mode": "open",
    "group_url": "https://www.facebook.com/group.."
  }
}

解析 Boolean

true 或者 false

步骤:

  • import parsec 库
  • 使用 string 函数,返回解析后的 string string :: String -> Parser String
  • 使用 parse 函数,它接收解析器、名称、输入,返回 Either 包裹的结果 parse :: Parser a -> name -> s -> Either ParseError a

实现:

import Text.ParserCombinators.Parsec

matchTrue :: Parser String
matchTrue = string "true"

使用:

ghci> :set -package parsec
package flags have changed, resetting and loading new packages...
ghci> import Text.ParserCombinators.Parsec
ghci> matchTrue = string "true"
ghci> parse matchTrue "a json parser" "true"
Right "true"
ghci> parse matchTrue "a json parser" "false"
Left "a json parser" (line 1, column 1):
unexpected "f"
expecting "true"

解析 "false":

matchFalse :: Parser String
matchFalse = string "false"

使用:

ghci> parse matchFalse "a json parser" "false"
Right "false"

Parsec 库

Daan Leijen 写的一个 Monadic 的 Parser 组合子库

  • 解析器组合子是用对应编程语言写的解析器,不像 yacc 那样有单独的语法。
  • Parsers 在 haskell 中是一类值(first-class value, 指在编程语言中,某个类型的值可以像任何其他值一样被操作、传递和存储。)

Parsec 模块

-- Parsec v2 的兼容层
import Text.ParserCombinators.Parsec

-- Parsec v3
import Text.Parsec
import Text.Parsec.String

Parser 类型构造器

Parser a = ParsecT String () Identity a

type Parser = Parsec String ()
type Parsec s u = ParsecT s u Identity
data ParsecT s u m a

ParserT 是一个 Monad Transformer,各参数含义:

  • 流类型 s
  • 用户状态类型 u
  • 底层 Monad 类型 m
  • 返回类型 a

辅助函数

parse :: Stream s Indetity t => (Parser s () a) -> SourceName -> s -> Either ParseError a

parse p filePath input 在没有用户状态的情况下,对 Identity 进行解析器 p 的解析。filePath 仅在错误消息中使用,可以是空字符串。返回值要么是一个 ParseError(Left),要么是一个类型为 a 的值(Right)。

parseTest :: (Stream s Identity t, Show a) => (Parsec s () a) -> s -> IO ()

表达式 parseTest p input 对输入 input 应用解析器 p,并将结果打印到标准输出 stdout。这通常用于测试解析器。

真正的 Boolean

注意到用 matchTrue 解析后的返回值是字符串 "true"

ghci> parse matchTrue "a json parser" "true"
Right "true" -- string

而我们真正想要的是布尔值 True,怎样实现这样的解析器 realTrue?

realTrue :: Parser Bool

处理带上下文的值

import Control.Applicative (pure)
pure :: a -> f a  -- Bool -> Parser Bool

import Control.Monad (return)
return :: a -> m a -- Bool -> Parser Bool

realTrue :: Parser Bool
realTrue = pure True

组合解析器

>> (bind) 操作符

按顺序组合两个操作,丢弃第一个操作产生的任何值。

(>>)  :: m a -> (_ -> m b) -> m b
(>>=) :: m a -> (a -> m b) -> m b
boolTrue :: Parser Bool
boolTrue = matchTrue >> pure True

do-natation 风格

boolTrue :: Parser Bool
boolTrue = do
  matchTrue
  return True

Applicative 风格

按顺序组合两个操作,丢弃第一个操作产生的任何值。

(*>)  :: Parser a -> Parser b -> Parser b

丢弃第二个操作产生的任何值。

(>*)  :: Parser a -> Parser b -> Parser b
boolTrue  = matchTrue *> realTrue
boolFalse = realFalse <* matchFalse

解析真正的布尔值

ghci> parse boolTrue "a json parser" "true"
Right True
ghci> parse boolTrue "a json parser" "false"
Left "a json parser" (line 1, column 1):
unexpected "f"
expecting "true"
ghci> boolFalse = realFalse <* matchFalse
ghci> parse boolFalse "a json parser" "false"
Right False
ghci> parse boolFalse "a json parser" "true"
Left "a json parser" (line 1, column 1):
unexpected "t"
expecting "false"

匹配多个解析器之一

伪代码如下:

bool = boolTrue || boolFalse
-- choice 组合器
(<|>) :: Parser a -> Parser a -> Parser a

这个组合器实现了 choicep <!> q 这个解析器先应用 p,如果成功了就返回,否则匹配 q。

bool :: Parser Bool
bool = boolTrue <|> boolFalse

使用:

ghci> parse bool "a json parser" "true"
Right True
ghci> parse bool "a json parser" "false"
Right False

解析字符串

"rsvp_limit"

使用以下函数:

char    :: Parser Char
noneOf  :: [Char] -> Parser Char
many    :: Parser p -> Parser [p]

实现:

stringLiteral :: Praser String
stringLiteral = char '"' *> many (noneOf "\"") <* char '"'

使用:

ghci> parse stringLiteral "a json parser" "\"rsvp_limit\""
Right "rsvp_limit"

ghci> parse stringLiteral "a json parser" "rsvp_limit"
Left "a json parser" (line 1, column 1):
unexpected "r"
expecting "\""

返回值

无法用 <|> 组合两个不同类型的解析器

ghci> value = bool <|> stringLiteral

<interactive>:28:18: error:
    ? Couldn't match type ‘[Char]’ with ‘Bool’
      Expected: Text.Parsec.Prim.ParsecT s u m Bool
        Actual: Text.Parsec.Prim.ParsecT s u m [Char]
    ? In the second argument of ‘(<|>)’, namely ‘stringLiteral’
      In the expression: bool <|> stringLiteral
      In an equation for ‘value’: value = bool <|> stringLiteral

需要先定义一个新类型处理这种情况:

data JSONVal = Bool Bool
             | String String
             deriving (Show)

parseJSON :: Parser JSONVal
parseJSON = bool <|> stringLiteral

没想到还是报错了:

<interactive>:32:22: error:
    ? Couldn't match type ‘[Char]’ with ‘Bool’
      Expected: Text.Parsec.Prim.ParsecT s u m Bool
        Actual: Text.Parsec.Prim.ParsecT s u m [Char]
    ? In the second argument of ‘(<|>)’, namely ‘stringLiteral’
      In the expression: bool <|> stringLiteral
      In an equation for ‘parseJSON’: parseJSON = bool <|> stringLiteral

这是因为 bool 是解析布尔值的,而非 Bool Bool,所以需要构造新的解析器

解析器 Boolean

data JSONVal = Bool Bool deriving (Show)

parseBool :: Parser JSONVal
parseBool = Bool bool

处理带上下文的值 - 续

以下的函子可以用来处理能 map 的类型

fmap  :: (a -> b) -> f a -> f b
(<$>) :: (a -> b) -> Parser a -> Parser b

所以可以这样用:

parseBool = Bool <$> bool   -- fmap Bool bool

让我们试一下:

ghci> parse parseBool "a json parser" "true"
Right (Bool True)
ghci> parse parseBool "a json parser" "false"
Right (Bool False)

解析字符串

data JSONVal = Bool Bool 
             | String String
             deriving (Show)

parseString :: Parser JSONVal
parseString = String <$> stringLiteral

parseJson :: Parser JSONVal
parseJson = parseBool <|> parseString

使用:

ghci> parse parseJson "a json parser" "\"rsvp_limit\""
Right (String "rsvp_limit")
ghci> parse parseJson "a json parser" "true"
Right (Bool True)

优化错误提示

ghci> parse parseJson "a json parser" "apple"
Left "a json parser" (line 1, column 1):
unexpected "a"
expecting "true", "false" or "\""

可以看到,错误提示非常不友好,让我们用 <?> 优化下,其定义为:

<?> :: Parser p ‐> String ‐> Parser p

解析器 p <?> msg 的行为与解析器 p 相同,但是每当解析器 p 在不消耗任何输入的情况下失败时,它将期望错误消息替换为指定的期望错误消息 msg。

parseJson = (parseBool <?> "boolean")
        <|> (parseString <?> "string literal")
ghci> parse parseJson "a json parser" "apple"  
Left "a json parser" (line 1, column 1):
unexpected "a"
expecting boolean or string literal  -- 可以看到,错误提示已经替换了

解析数字

15

使用一些新的辅助函数:

many1 :: Parser p ‐> Parser [p]
digit :: Parser Char
read :: String ‐> a

使用下:

ghci> parse (many letter) "many vs many1" "20th"
Right ""
ghci> parse (many1 letter) "many vs many1" "20th"
Left "many vs many1" (line 1, column 1):
unexpected "2"
expecting letter

愉快的解析数字吧:

data JSONVal = Bool Bool 
             | String String
             | Number Integer
             deriving (Show)

parseNumber :: Parser JSONVal
parseNumber = do
  n <- many1 digit
  return (Number (read n))

使用:

ghci> parse parseNumber "a json parser" "15"
Right (Number 15)

处理带上下文的值 - 再续

-- 将一个函数提升为一个 Monad
liftM :: Monad m => (a1 ‐> r) ‐> m a1 ‐> m r
liftA :: Applicative f => (a ‐> b) ‐> f a ‐> f b
fmap :: (a ‐> b) ‐> f a ‐> f b

应用到 parseNumber 上:

import Control.Monad (liftM)

parseNumber :: Parser JSONVal
parseNumber = liftM (Number . read)   -- String -> JSONVal
                    (many1 digit)     -- Parser String

使用:

ghci> parse parseNumber "a json parser" "15"
Right (Number 15)

解析浮点型

37.4972

parseNumber 在解析浮点数时会报错

ghci> parse parseNumber "a json parser" "37.4972"
Right (Number 37)

先实现一个 parseFloat

data JSONVal = Bool Bool 
             | String String
             | Number Integer
             | Float Double
             deriving (Show)

parseFloat :: Parser JSONVal
parseFloat = do
  whole <- many1 digit
  char '.'
  fraction <- many1 digit
  return (Float (read (whole ++ "." ++ fraction)))

使用:

ghci> parse parseFloat "a json parser" "37.4972"
Right (Float 37.4972)

解析数字和浮点型

应该怎么写呢?

parseJson = (parseBool <?> "boolean") 
        <|> (parseString <?> "string literal") 
        <|> parseNumber
        <|> parseFloat

试一试:

ghci> parse parseJson "a json parser" "15"
Right (Number 15)
ghci> parse parseJson "a json parser" "37.4972"
Right (Number 37)

结果不符合预期,没有正确解析浮点数,而是解析了它的整数部分。
那我们交换两个 parser 的顺序试试:

parseJson = (parseBool <?> "boolean") 
        <|> (parseString <?> "string literal")
        <|> parseFloat
        <|> parseNumber
ghci> parse parseJson "a json parser" "37.4972"
Right (Float 37.4972)
ghci> parse parseJson "a json parser" "15"
Left "a json parser" (line 1, column 3):
unexpected end of input
expecting digit or "."

这次正确解析了浮点型,但是解析 15 的时候却报错了。

predictive 解析器

(<|>) :: Parser a -> Parser a -> Parser a

p <|> q,这种 parser 称为 predictive,因为 q 只会在 parser p 不消费任何输入时才会执行。 这种非回溯行为既可以实现高效的解析器组合子,又可以生成良好的错误消息。像下面例子这样:

testOr = string "(a)"
     <|> string "(b)"
ghci> parse testOr "test" "(b)"
Left "test" (line 1, column 1):
unexpected "b"
expecting "(a)"

try 组合子

try :: Parser a -> Parser a

解析器 try p 的行为类似于解析器 p,唯一的区别是当出现错误时,它会假装自己没有消耗任何输入。

parseJson :: Parser JSONVal
parseJson = (parseBool <?> "boolean") 
        <|> (parseString <?> "string literal") 
        <|> try parseFloat
        <|> parseNumber

试一试:

ghci> parse parseJson "a json parser" "37.4972"
Right (Float 37.4972)
ghci> parse parseJson "a json parser" "15"
Right (Number 15)

解析数组

["Hello", "Goodbye", true, false, true]

新辅助函数

sepBy :: Parser a -> Parser sep -> Parser [a]

添加定义:

data JSONVal = Bool Bool 
             | String String
             | Number Integer
             | Float Double
             | Array [JSONVal]
             deriving (Show)

array :: Parser [JSONVal]
array = char '[' *> sepBy parseJson (char ',') <* char ']' 

parseArray :: Parser JSONVal
parseArray = Array <$> array

parseJson = (parseBool <?> "boolean") 
        <|> (parseString <?> "string literal") 
        <|> try parseFloat
        <|> parseNumber
        <|> parseArray

使用:

ghci> parse parseJson "a json parser" "[\"Hello\",\"Goodbye\",true,false,true]"
Right (Array [String "Hello",String "Goodbye",Bool True,Bool False,Bool True])

解析对象

{"name": "Jun", "male": true}

先解析一个键值对:

objectEntry :: Parser (String, JSONVal)
objectEntry = do
  key <- stringLiteral
  char ':'
  value <- parseJson
  return (key, value)

测试:

ghci> parse objectEntry "a json parser" "\"male\":true"
Right ("male",Bool True)

在解析整个对象:

data JSONVal = Bool Bool 
             | String String
             | Number Integer
             | Float Double
             | Array [JSONVal]
             | Object [(String, JSONVal)]
             deriving (Show)

parseObject :: Parser JSONVal
parseObject = do
  char '{'
  obj <- sepBy objectEntry (char ',')
  char '}'
  return $ Object obj

parseJson = (parseBool <?> "boolean") 
        <|> (parseString <?> "string literal") 
        <|> try parseFloat
        <|> parseNumber
        <|> parseArray
        <|> parseObject

让我们试一试:

ghci> parse parseJson "a json parser" "{\"name\":\"Jun\",\"male\":true}"
Right (Object [("name",String "Jun"),("male",Bool True)])

空格

刚才的测试用例中都是没有空格的,如果有空格就会报错:

ghci> parse parseJson "a json parser" "[true, true, true]"
Left "a json parser" (line 1, column 7):
unexpected " "
expecting boolean, string literal, digit, "[" or "{"

辅助函数:

oneOf :: [Char] -> Parser Char

顾名思义,oneOf 接收一个字符列表,如果下个字符在这个列表中则成功,返回解析到的字符。

ws :: Parser String
ws = many (oneOf " \t\n\r")

lexme p = p <* ws

parseBool = lexme $ Bool <$> bool
parseString = lexme $ String <$> stringLiteral
parseNumber = lexme $ liftM (Number . read) (many1 digit)

array = (lexme $ char '[') *> 
        (sepBy parseJson (lexme $ (char ',')))
        <* (lexme $ char ']') 
parseArray = Array <$> array

objectEntry = do
  key <- (lexme stringLiteral)
  (lexme $ char ':')
  value <- (lexme parseJson)
  return (key, value)

parseObject = do
  (lexme $ char '{')
  obj <- sepBy objectEntry (lexme $ char ',')
  (lexme $ char '}')
  return $ Object obj

测试下效果

ghci> parse parseJson "a json parser" "[true, true, true]"
Right (Array [Bool True,Bool True,Bool True])

ghci> parse parseJson "aaa" "{ \"a\" : 1 , \"b\": [ \"2 \" ] }"
Right (Object [("a",Number 1),("b",Array [String "2 "])])

简单的 json 解析器

直接解析最初的 json 串:

parse parseJson "a json parser" "{\
\  \"rsvp_limit\": 15,                                      \
\  \"status\": \"upcoming\",                                \
\  \"visibility\": \"public\",                              \
\  \"venue\": {                                             \
\    \"name\": \"Hyperconnect, Inc\",                       \
\    \"address\": \"14F, 5 Seocho-daero 78-gil, Seoc..\",   \
\    \"city\": \"Seoul\",                                   \
\    \"country\": \"kr\",                                   \
\    \"lat\": 37.49721,                                     \
\    \"lon\": 127.027374                                    \
\  },                                                       \
\  \"id\": \"140930019688259\",                             \
\  \"time\": 1474326000,                                    \
\  \"event_url\": \"https://www.facebook.com/events/14..\", \
\  \"name\": \"Simple JSON Parser\",                        \
\  \"group\": {                                             \
\    \"id\": 1065398240148353,                              \
\    \"name\": \"Haskell School\",                          \
\    \"join_mode\": \"open\",                               \
\    \"group_url\": \"https://www.facebook.com/group..\"    \
\  }                                                        \
\}"

Right (Object [("rsvp_limit",Number 15),("status",String "upcoming"),("visibility",String "public"),("venue",Object [("name",String "Hyperconnect, Inc"),("address",String "14F, 5 Seocho-daero 78-gil, Seoc.."),("city",String "Seoul"),("country",String "kr"),("lat",Float 37.49721),("lon",Float 127.027374)]),("id",String "140930019688259"),("time",Number 1474326000),("event_url",String "https://www.facebook.com/events/14.."),("name",String "Simple JSON Parser"),("group",Object [("id",Number 1065398240148353),("name",String "Haskell School"),("join_mode",String "open"),("group_url",String "https://www.facebook.com/group..")])])

处理状态

使用 runParser:

runParser :: Parser s u a -> u -> SourceName -> a -> Either ParseError a

runParser p state filePath input 在输入列表 input 上运行解析器 p,该输入列表是从源文件 filePath 中获取的,初始用户状态为 state st(u)。

getState :: Monad m => ParsecT s u m u
putState :: Monad m => u ‐> ParsecT s u m ()

parseObject :: Parsec String Int JSONVal
parseObject = do ...                 
  c <‐ getState
  putState (c+1) ... ‐‐ modifyState (+1)
  
liftM (runParser (parseJson >> getState) 0 "")
      (readFile "meetup.json")
Right 3 

另一件事

aeson 和 megaparsec ...

总结

  • 解析器(parsers):string、char、noneOf、oneOf等。
  • 类型构造器(type constructor):parser、parsec、parsecT。
  • 辅助函数(helper functions):parse、parseTest、parseFromFile、runParser。
  • 处理带有上下文的值(dealing with a value with a context):pure、return、liftM、fmap。
  • 合并解析器(combining parsers):>>、op、do-notation、<、>、applicative。
  • 匹配多个解析器中的一个(matching one of multiple parsers):<|>。
  • 数据(data)、类型(type)。
  • 改进错误消息(improving error messages):<?>。
  • 预测分析器(predictive parser):try。
  • 处理状态(handling state):getState、putState、modifyState。

引用

代码汇总

import Control.Monad (liftM)
import Text.Parsec

type Parser = Parsec String ()

data JSONVal = Bool Bool 
             | String String
             | Number Integer
             | Float Double
             | Array [JSONVal]
             | Object [(String, JSONVal)]
             deriving (Show)
ws = many (oneOf " \t\n\r")

lexme p = p <* ws

matchTrue = string "true"
matchFalse = string "false"
bool = (matchTrue *> pure True)
   <|> (matchFalse *> pure False)
parseBool = lexme $ Bool <$> bool

stringLiteral = char '"' *> many (noneOf "\"") <* char '"'
parseString = lexme $ String <$> stringLiteral

parseFloat :: Parser JSONVal
parseFloat = do
  whole <- many1 digit
  char '.'
  fraction <- many1 digit
  return (Float (read (whole ++ "." ++ fraction)))

parseNumber = lexme $ liftM (Number . read) (many1 digit)

array = (lexme $ char '[') *> 
        (sepBy parseJson (lexme $ (char ',')))
        <* (lexme $ char ']') 
parseArray = Array <$> array

objectEntry = do
  key <- (lexme stringLiteral)
  (lexme $ char ':')
  value <- (lexme parseJson)
  return (key, value)

parseObject = do
  (lexme $ char '{')
  obj <- sepBy objectEntry (lexme $ char ',')
  (lexme $ char '}')
  return $ Object obj

parseJson = (parseBool <?> "boolean") 
        <|> (parseString <?> "string literal") 
        <|> try parseFloat
        <|> parseNumber
        <|> parseArray
        <|> parseObject

main = do
    print $ parse parseJson "a json parser" "{\
\  \"rsvp_limit\": 15,                                      \
\  \"status\": \"upcoming\",                                \
\  \"visibility\": \"public\",                              \
\  \"venue\": {                                             \
\    \"name\": \"Hyperconnect, Inc\",                       \
\    \"address\": \"14F, 5 Seocho-daero 78-gil, Seoc..\",   \
\    \"city\": \"Seoul\",                                   \
\    \"country\": \"kr\",                                   \
\    \"lat\": 37.49721,                                     \
\    \"lon\": 127.027374                                    \
\  },                                                       \
\  \"id\": \"140930019688259\",                             \
\  \"time\": 1474326000,                                    \
\  \"event_url\": \"https://www.facebook.com/events/14..\", \
\  \"name\": \"Simple JSON Parser\",                        \
\  \"group\": {                                             \
\    \"id\": 1065398240148353,                              \
\    \"name\": \"Haskell School\",                          \
\    \"join_mode\": \"open\",                               \
\    \"group_url\": \"https://www.facebook.com/group..\"    \
\  }                                                        \
\}"