如何在Haskell中使用wreq解析维基百科API的JSON数据?
Haskell解析维基百科Mediawiki API JSON搜索结果的问题
我正在用Haskell编写程序,通过wreq模块调用维基百科的Mediawiki Action API获取JSON格式的搜索结果,但在解析JSON时遇到了困难。
我的函数代码如下:
requestWikiSearch :: Text -> IO (???) requestWikiSearch searchQuery = do let urlParams = [ ("action", "query"), ("format", "json"), ("list", "search") ] let opts = defaults & params .~ urlParams & param "srsearch" .~ [searchQuery] r <- getWith opts wikipediaApiUrl return $ r ^. responseBody . key "query" . key "search" ....???
API返回的responseBody JSON示例(搜索"Aerosmith"的结果):
{ "batchcomplete": true, "continue": { "sroffset": 10, "continue": "-||" }, "query": { "searchinfo": { "totalhits": 3254 }, "search": [ { "ns": 0, "title": "Aerosmith", "pageid": 149216, "size": 190407, "wordcount": 19118, "snippet": "<span class=\"searchmatch\">Aerosmith</span> is an American rock band formed in Boston in 1970. ... ", "timestamp": "2025-05-07T14:23:27Z" }, { "ns": 0, "title": "Dream On (Aerosmith song)", "pageid": 2761666, "size": 22786, "wordcount": 1972, "snippet": "&quot;Dream On&quot; is a power ballad by American rock band <span class=\"searchmatch\">Aerosmith</span>, from their 1973 eponymous debut album...", "timestamp": "2025-05-12T19:32:05Z" }, ... ] } }
我需要提取query.search对应的Haskell列表,进而遍历获取page ID、标题等信息,但尝试Lens和Aeson相关操作时遇到了几个问题:
- 如何遍历通过
r ^. responseBody . key "query" . key "search"获取到的"search"列表? - 尝试用
Data.Vector和Data.Aeson.KeyMap获取数组第一个元素后,无法从Data.Aeson.Types.Internal.Value类型的Number中提取数值:vector = r ^. responseBody . key "query" . key "search" . _Array KeyMap.toList $ Vector.head vector ^. _Object - 导入
Data.Aeson.Lens (_Number, nth)后尝试以下代码,GHC repl报错No instance for ‘Monoid Integer’ arising from a use of ‘key’:r ^. responseBody . key "query" . key "search" . nth 0 . key "size" . _Number
问题
- 如何让
_Number(或_Integer、_Integral)正常工作? - 更通用地说,使用Haskell的Aeson/Lens/Vector/KeyMap等工具解析该JSON的最佳方案是什么?
解决方案
1. 解决_Number相关的报错问题
你遇到的Monoid Integer报错,本质是key操作默认返回带Monoid约束的折叠结果,和_Number的类型推导冲突。最安全的解决方式是用^?(预览操作符,返回Maybe类型)替代^.,因为JSON字段可能不存在:
-- 提取单个字段的Double类型值,返回Maybe Double r ^? responseBody . key "query" . key "search" . nth 0 . key "size" . _Number -- 如果需要整数类型,用_Integral r ^? responseBody . key "query" . key "search" . nth 0 . key "pageid" . _Integral
如果确定字段一定存在,也可以结合fromJust(不推荐,JSON结构可能变更)和类型注解:
import Data.Maybe (fromJust) pageId :: Integer pageId = fromJust $ r ^? responseBody . key "query" . key "search" . nth 0 . key "pageid" . _Integral
2. 解析整个search列表的最佳方案
最稳健、可读性最强的方式是定义对应JSON结构的Haskell数据类型,自动派生Aeson解析实例,再结合wreq的asJSON直接解析响应:
第一步:定义数据类型并派生解析实例
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-} import GHC.Generics import Data.Aeson import Data.Text (Text) -- 单个搜索结果的结构 data WikiSearchResult = WikiSearchResult { ns :: Int , title :: Text , pageid :: Integer , size :: Integer , wordcount :: Integer , snippet :: Text , timestamp :: Text } deriving (Show, Generic, FromJSON) -- 外层包含query的结构 data WikiSearchResponse = WikiSearchResponse { query :: Query } deriving (Show, Generic, FromJSON) -- 包含search列表的query结构 data Query = Query { search :: [WikiSearchResult] } deriving (Show, Generic, FromJSON)
第二步:修改函数解析整个响应
import Network.Wreq import Data.Text (Text) wikipediaApiUrl :: String wikipediaApiUrl = "https://en.wikipedia.org/w/api.php" requestWikiSearch :: Text -> IO (Either String [WikiSearchResult]) requestWikiSearch searchQuery = do let urlParams = [ ("action", "query"), ("format", "json"), ("list", "search") ] opts = defaults & params .~ urlParams & param "srsearch" .~ [searchQuery] response <- getWith opts wikipediaApiUrl case asJSON response of Left err -> return $ Left $ show err Right (respBody :: WikiSearchResponse) -> return $ Right $ search $ query respBody
调用示例:
main :: IO () main = do result <- requestWikiSearch "Aerosmith" case result of Left err -> putStrLn $ "Error: " ++ err Right results -> mapM_ (\res -> putStrLn $ "Title: " ++ show (title res) ++ ", PageID: " ++ show (pageid res)) results
3. 不定义数据类型的Lens直接解析方案
如果不想定义数据类型,可以用Lens的traverse遍历整个数组,提取目标字段:
import Control.Lens import Data.Aeson.Lens import Data.Text (Text) import Data.Vector (Vector) import Data.Maybe (fromJust) -- 提取所有搜索结果的(pageid, title)列表 extractSearchResults :: Response Value -> [(Integer, Text)] extractSearchResults r = let searchVector = r ^. responseBody . key "query" . key "search" . _Array in searchVector ^.. traverse . to (\val -> ( fromJust $ val ^? key "pageid" . _Integral , fromJust $ val ^? key "title" . _String ))
这种方式需要手动处理字段缺失的情况,不如定义数据类型安全。
内容的提问来源于stack exchange,提问作者tonebender
相关产品推荐
相关产品推荐

