Haskell 数据分析秘籍(四)

原文:annas-archive.org/md5/3ff53e35b37e2f50c639bfc6fc052f29

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:实时数据

本章将涵盖以下示例:

  • 为实时情感分析流式传输 Twitter 数据

  • 读取 IRC 聊天室消息

  • 响应 IRC 消息

  • 向 Web 服务器轮询以获取最新更新

  • 检测实时文件目录变化

  • 通过套接字进行实时通信

  • 通过摄像头流检测面部和眼睛

  • 用于模板匹配的摄像头流

介绍

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/ch10.jpg

首先收集数据然后再分析它是相当容易的。然而,有些任务可能需要将这两个步骤结合在一起。实时分析接收到的数据是本章的核心内容。我们将讨论如何管理来自 Twitter 推文、Internet Relay Chat(IRC)、Web 服务器、文件变更通知、套接字和网络摄像头的实时数据输入。

前三个示例将重点处理来自 Twitter 的实时数据。这些主题将包括用户发布的内容以及与关键词相关的帖子。

接下来,我们将使用两个独立的库与 IRC 服务器进行交互。第一个示例将展示如何加入一个 IRC 聊天室并开始监听消息,接下来的示例将展示我们如何在 IRC 服务器上监听直接消息。

如果不支持实时数据,常见的解决方法是频繁查询该数据。这一过程称为 轮询,我们将在某个示例中学习如何快速轮询 Web 服务器。

我们还将在文件目录中检测到文件被修改、删除或创建的变化。可以想象在 Haskell 中实现 Dropbox、OneDrive 或 Google Drive。

最后,我们将创建一个简单的服务器-客户端交互,使用套接字并操作实时网络摄像头流。

为实时情感分析流式传输 Twitter 数据

Twitter 上充满了每秒钟涌现的内容。开始调查实时数据的一个好方法是检查推文。

本示例将展示如何编写代码来响应与特定搜索查询相关的推文。我们使用外部 Web 端点来确定情感是积极、消极还是中立。

准备工作

安装 twitter-conduit 包:

$ cabal install twitter-conduit

为了解析 JSON,我们使用 yocto

$ cabal install yocto

如何操作…

按照以下步骤设置 Twitter 凭证并开始编码:

  1. 通过访问 apps.twitter.com 创建一个新的 Twitter 应用。

  2. 从此 Twitter 应用管理页面找到 OAuth 消费者密钥和 OAuth 消费者密钥。分别为 OAUTH_CONSUMER_KEYOAUTH_CONSUMER_SECRET 设置系统环境变量。大多数支持 sh 兼容 shell 的 Unix 系统支持 export 命令:

    $ export OAUTH_CONSUMER_KEY="Your OAuth Consumer Key"
    $ export OAUTH_CONSUMER_SECRET="Your OAuth Consumer Secret"
    
    
  3. 此外,通过相同的 Twitter 应用管理页面找到 OAuth 访问令牌和 OAuth 访问密钥,并相应地设置环境变量:

    $ export OAUTH_ACCESS_TOKEN="Your OAuth Access Token"
    $ export OAUTH_ACCESS_SECRET="Your OAuth Access Secret"
    
    

    小贴士

    我们将密钥、令牌和秘密 PIN 存储在环境变量中,而不是直接将它们硬编码到程序中,因为这些变量与密码一样重要。就像密码永远不应公开可见,我们尽力将这些令牌和密钥保持在源代码之外。

  4. twitter-conduit包的示例目录中下载Common.hs文件,路径为github.com/himura/twitter-conduit/tree/master/sample。研究userstream.hs示例文件。

  5. 首先,我们导入所有相关的库:

    {-# LANGUAGE OverloadedStrings #-}
    
    import qualified Data.Conduit as C
    import qualified Data.Conduit.List as CL
    import qualified Data.Text.IO as T
    import qualified Data.Text as T
    
    import Control.Monad.IO.Class (liftIO)
    import Network.HTTP (getResponseBody, getRequest, simpleHTTP, urlEncode)
    import Text.JSON.Yocto
    import Web.Twitter.Conduit (stream, statusesFilterByTrack)
    import Common
    import Control.Lens ((^!), (^.), act)
    import Data.Map ((!))
    import Data.List (isInfixOf, or)
    import Web.Twitter.Types 
    
  6. main中,运行我们的实时情感分析器以进行搜索查询:

    main :: IO ()
    
    main = do
      let query = "haskell"
      T.putStrLn $ T.concat [ "Streaming Tweets that match \""
                            , query, "\"..."]
      analyze query
    
  7. 使用Common模块提供的runTwitterFromEnv'函数,通过我们的 Twitter API 凭证连接到 Twitter 的实时流。我们将使用一些非常规的语法,如$$+-^!。请不要被它们吓到,它们主要用于简洁表达。每当触发事件时,例如新的推文或新的关注,我们将调用我们的process函数进行处理:

    analyze :: T.Text -> IO ()
    
    analyze query = runTwitterFromEnv' $ do
      src <- stream $ statusesFilterByTrack query
      src C.$$+- CL.mapM_ (^! act (liftIO . process))
    
  8. 一旦我们获得事件触发的输入,就会运行process以获取输出,例如发现文本的情感。在本示例中,我们将情感输出附加到逗号分隔文件中:

    process :: StreamingAPI -> IO ()
    
    process (SStatus s) = do
      let theUser = userScreenName $ statusUser s
      let theTweet = statusText s
      T.putStrLn $ T.concat [theUser, ": ", theTweet]
      val <- sentiment $ T.unpack theTweet
      let record = (T.unpack theUser) ++ "," ++ 
                   (show.fromRational) val ++ "\n"
      appendFile "output.csv" record
      print val
    
  9. 如果事件触发的输入不是推文,而是朋友关系事件或其他内容,则不执行任何操作:

    process s = return ()
    
  10. 定义一个辅助函数,通过移除所有@user提及、#hashtagshttp://websites来清理输入:

    clean :: String -> String
    
    clean str = unwords $ filter 
                (\w -> not (or 
                       [ isInfixOf "@" w
                       , isInfixOf "#" w
                       , isInfixOf "http://" w ]))
                (words str)
    
  11. 使用外部 API 对文本内容进行情感分析。在本示例中,我们使用 Sentiment140 API,因为它简单易用。更多信息请参考help.sentiment140.com/api。为了防止被限制访问,也请提供appid参数,并附上电子邮件地址或获取商业许可证:

    sentiment :: String -> IO Rational
    sentiment str = do 
      let baseURL = "http://www.sentiment140.com/api/classify?text="
      resp <- simpleHTTP $ getRequest $ 
              baseURL ++ (urlEncode.clean) str
      body <- getResponseBody resp
      let p = polarity (decode body) / 4.0
      return p
    
  12. 从我们的 API 的 JSON 响应中提取情感值:

    polarity :: Value -> Rational
    
    polarity (Object m) = polarity' $ m ! "results"
      where polarity' (Object m) = fromNumber $ m ! "polarity"
            fromNumber (Number n) = n
    polarity _ = -1
    
  13. 运行代码,查看推文在全球任何人公开发布时即刻显示。情感值将是介于 0 和 1 之间的有理数,其中 0 表示负面情感,1 表示正面情感:

    $ runhaskell Main.hs
    Streaming Tweets that match "x-men"

    查看以下输出:

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_01.jpg

我们还可以从output.csv文件中批量分析数据。以下是情感分析的可视化表现:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_02.jpg

它是如何工作的…

Twitter-conduit 包使用了来自原始包的 conduit 设计模式,原始包位于hackage.haskell.org/package/conduit。conduit 文档中指出:

Conduit 是解决流数据问题的方案,允许在恒定内存中进行数据流的生产、转换和消费。它是惰性 I/O 的替代方案,保证了确定性的资源处理,并且与枚举器/迭代器和管道处于相同的通用解决方案空间中。

为了与 Twitter 的应用程序编程接口(API)进行交互,必须获得访问令牌和应用程序密钥。我们将这些值存储在环境变量中,并让 Haskell 代码从中检索。

Common.hs文件负责处理单调的认证代码,应该保持不变。

反应每个 Twitter 事件的函数是process。我们可以修改process以满足我们特定的需求。更具体地说,我们可以修改情感分析函数,以使用不同的sentiment分析服务。

还有更多内容…

我们的代码监听任何与我们查询匹配的推文。这个 Twitter-conduit 库还支持另外两种实时流:statusesFilterByFollowuserstream。前者获取指定用户列表的所有推文,后者获取该账户关注的用户的所有推文。

例如,通过将statusesFilterByTrack查询替换为一些 Twitter 用户的 UID 来修改我们的代码:

analyze:: IO ()
analyze = runTwitterFromEnv' $ do
  src <- statusesFilterByFollow [ 103285804, 450331119
                                , 64895420]
  src C.$$+- CL.mapM_ (^! act (liftIO . process))

此外,为了仅获取我们关注的用户的推文,我们可以通过将statusesFilterByTrack查询替换为userstream来修改我们的代码:

analyze :: IO ()
analyze = runTwitterFromEnv' $ do
  src <- stream userstream
  src C.$$+- CL.mapM_ (^! act (liftIO . process))

通过github.com/himura/twitter-conduit/tree/master/sample可以找到更多示例。

阅读 IRC 聊天室消息

Internet Relay Chat(IRC)是最古老且最活跃的群聊服务之一。Haskell 社区在 Freenode IRC 服务器(irc.freenode.org)的#haskell频道中拥有非常友好的存在。

在这个配方中,我们将构建一个 IRC 机器人,加入一个聊天室并监听文本对话。我们的程序将模拟一个 IRC 客户端,并连接到现有的 IRC 服务器之一。这个配方完全不需要外部库。

做好准备

确保启用互联网连接。

要测试 IRC 机器人,最好安装一个 IRC 客户端。例如,顶级的 IRC 客户端之一是Hexchat,可以从hexchat.github.io下载。对于基于终端的 IRC 客户端,Irssi是最受欢迎的:www.irssi.org

在 Haskell wiki 上查看自己动手制作 IRC 机器人文章:www.haskell.org/haskellwiki/Roll_your_own_IRC_bot。这个配方的代码大多基于 wiki 上的内容。

如何做…

在一个名为Main.hs的新文件中,插入以下代码:

  1. 导入相关的包:

    import Network
    import Control.Monad (forever)
    import System.IO
    import Text.Printf
    
  2. 指定 IRC 服务器的具体信息:

    server = "irc.freenode.org"
    port   = 6667
    chan   = "#haskelldata"
    nick   = "awesome-bot"
    
  3. 连接到服务器并监听聊天室中的所有文本:

    main = do
      h <- connectTo server (PortNumber (fromIntegral port))
      hSetBuffering h NoBuffering
      write h "NICK" nick
      write h "USER" (nick++" 0 * :tutorial bot")
      write h "JOIN" chan
      listen h
    
    write :: Handle -> String -> String -> IO ()
    write h s t = do
      hPrintf h "%s %s\r\n" s t
      printf    "> %s %s\n" s t
    
  4. 定义我们的监听器。对于这个配方,我们将仅将所有事件回显到控制台:

    listen :: Handle -> IO ()
    listen h = forever $ do
      s <- hGetLine h
      putStrLn s
    

另见

要了解另一种与 IRC 交互的方式,请查看下一个配方,回应 IRC 消息

回应 IRC 消息

另一种与 IRC 交互的方式是使用Network.SimpleIRC包。此包封装了许多底层网络操作,并提供了有用的 IRC 接口。

在本教程中,我们将回应频道中的消息。如果有用户输入触发词,在本案例中为“host?”,我们将回复该用户其主机地址。

准备工作

安装Network.SimpleIRC包:

$ cabal install simpleirc

要测试 IRC 机器人,安装 IRC 客户端会很有帮助。一个不错的 IRC 客户端是 Hexchat,可以从hexchat.github.io下载。对于基于终端的 IRC 客户端,Irssi 是最好的之一:www.irssi.org

如何操作…

创建一个新的文件,我们称之为Main.hs,并执行以下操作:

  1. 导入相关的库:

    {-# LANGUAGE OverloadedStrings #-}
    
    import Network.SimpleIRC
    import Data.Maybe
    import qualified Data.ByteString.Char8 as B
    
  2. 创建接收到消息时的事件处理程序。如果消息是“host?”,则回复用户其主机信息:

    onMessage :: EventFunc
    onMessage s m = do
      case msg of
        "host?" ->  sendMsg s chan $ botMsg
        otherwise -> return ()
      where chan = fromJust $ mChan m
            msg = mMsg m
            host = case mHost m of
              Just h -> h
              Nothing -> "unknown"
            nick = case mNick m of
              Just n -> n
              Nothing -> "unknown user"
            botMsg = B.concat [ "Hi ", nick, "
                              , your host is ", host]
    
  3. 定义要监听的事件:

    events = [(Privmsg onMessage)]
    
  4. 设置 IRC 服务器配置。连接到任意一组频道,并绑定我们的事件:

    freenode = 
      (mkDefaultConfig "irc.freenode.net" "awesome-bot")
      { cChannels = ["#haskelldata"]
      , cEvents   = events
      }
    
  5. 连接到服务器。不要在新线程中运行,而是打印调试信息,按照相应的布尔参数来指定:

    main = connect freenode False True
    
  6. 运行代码,打开 IRC 客户端进行测试:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_03.jpg

另见

若要在不使用外部库的情况下连接 IRC 服务器,请参见之前的教程,读取 IRC 聊天室消息

轮询 web 服务器以获取最新更新

一些网站的变化非常频繁。例如,Google 新闻和 Reddit 通常在我们刷新页面时,立刻加载最新的帖子。为了随时保持最新数据,最好是频繁地发送 HTTP 请求。

在本教程中,我们每 10 秒轮询一次新的 Reddit 帖子,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_04.jpg

如何操作…

在一个名为Main.hs的新文件中,执行以下步骤:

  1. 导入相关的库:

    import Network.HTTP
    import Control.Concurrent (threadDelay)
    import qualified Data.Text as T
    
  2. 定义要轮询的 URL:

    url = "http://www.reddit.com/r/pics/new.json"
    
  3. 定义一个函数来获取最新的 HTTP GET 请求数据:

    latest :: IO String
    
    latest = simpleHTTP (getRequest url) >>= getResponseBody
    
  4. 轮询实际上是等待指定时间后递归地执行任务。在这种情况下,我们会等 10 秒钟再请求最新的网页数据:

    poll :: IO a
    
    poll = do
      body <- latest
      print $ doWork body
      threadDelay (10 * 10)
      poll
    
  5. 运行轮询:

    main :: IO a
    main = do
      putStrLn $ "Polling " ++ url ++ " …"
      poll
    
  6. 每次 Web 请求后,分析数据。在本教程中,统计 Imgur 出现的次数:

    doWork str = length $ T.breakOnAll 
                  (T.pack "imgur.com/") (T.pack str)
    

检测实时文件目录变化

在本教程中,我们将实时检测文件是否被创建、修改或删除。类似于流行的文件同步软件 Dropbox,我们每次遇到这样的事件时,都会执行一些有趣的操作。

准备工作

安装fsnotify包:

$ cabal install fsnotify

如何操作…

在一个名为Main.hs的新文件中,执行以下步骤:

  1. 导入相关的库:

    {-# LANGUAGE OverloadedStrings #-}
    import Filesystem.Path.CurrentOS
    import System.FSNotify
    import Filesystem
    import Filesystem.Path (filename)
    
  2. 在当前目录上运行文件监视器:

    main :: IO ()
    
    main = do
      wd <- getWorkingDirectory
      print wd
    
      man <- startManager
      watchTree man wd (const True) doWork
      putStrLn "press return to stop"
    
      getLine
      putStrLn "watching stopped, press return to exit"
    
      stopManager man
      getLine
      return ()
    
  3. 处理每个文件变化事件。在本教程中,我们仅将操作输出到控制台:

    doWork :: Event -> IO ()  
    
    doWork (Added filepath time) = 
      putStrLn $ (show $ filename filepath) ++ " added"
    doWork (Modified filepath time) = 
      putStrLn $ (show $ filename filepath) ++ " modified"
    doWork (Removed filepath time) = 
      putStrLn $ (show $ filename filepath) ++ " removed"
    
  4. 运行代码并开始修改同一目录中的一些文件。例如,创建一个新文件,编辑它,然后删除它:

    $ runhaskell Main.hs
    
    press return to stop
    FilePath "hello.txt" added
    FilePath "hello.txt" modified
    FilePath "hello.txt" removed
    
    

它是如何工作的…

fsnotify库绑定到特定平台文件系统的事件通知服务。在基于 Unix 的系统中,这通常是inotifydell9.ma.utexas.edu/cgi-bin/man-cgi?inotify)。

通过套接字实时通信

套接字提供了一种方便的实时程序间通信方式。可以把它们想象成一个聊天客户端。

在这个教程中,我们将从一个程序向另一个程序发送消息并获取响应。

如何做…

将以下代码插入到名为Main.hs的新文件中:

  1. 创建服务器代码:

    import Network ( listenOn, withSocketsDo, accept
                   , PortID(..), Socket )
    import System.Environment (getArgs)
    import System.IO ( hSetBuffering, hGetLine, hPutStrLn
                     , BufferMode(..), Handle )
    import Control.Concurrent (forkIO)
    
  2. 创建一个套接字连接以进行监听,并在其上附加我们的处理程序sockHandler

    main :: IO ()
    
    main = withSocketsDo $ do
        let port = PortNumber 9001
        sock <- listenOn port
        putStrLn $ "Listening…"
        sockHandler sock
    
  3. 定义处理每个接收到的消息的处理程序:

    sockHandler :: Socket -> IO ()
    
    sockHandler sock = do
        (h, _, _) <- accept sock
        putStrLn "Connected!"
        hSetBuffering h LineBuffering
        forkIO $ process h
        forkIO $ respond h
        sockHandler sock
    
  4. 定义如何处理客户端发送的消息:

    process :: Handle -> IO ()
    process h = do
        line <- hGetLine h
        print line
        process h
    
  5. 通过用户输入向客户端发送消息:

    respond h = withSocketsDo $ do
      txt <- getLine
      hPutStrLn h txt
      respond h
    
  6. 现在,在一个新文件client.hs中创建客户端代码。首先,导入库:

    import Network (connectTo, withSocketsDo, PortID(..))
    import System.Environment (getArgs)
    import System.IO ( hSetBuffering, hPutStrLn
                     , hGetLine, BufferMode(..) )
    
  7. 将客户端连接到相应的端口,并设置响应者和监听线程:

    main = withSocketsDo $ do
      let port = PortNumber 9001
      h <- connectTo "localhost" port
      putStrLn $ "Connected!"
      hSetBuffering h LineBuffering
      forkIO $ respond h
      forkIO $ process h
      loop
    
  8. 获取用户输入并将其作为消息发送:

    respond h = do
      txt <- getLine
      hPutStrLn h txt
      respond h
    
  9. 监听来自服务器的传入消息:

    process h = do
      line <- hGetLine h
      print line
      process h
    
  10. 先运行服务器,测试代码:

    $ runhaskell Main.hs
    
    
  11. 接下来,在一个单独的终端中运行客户端:

    $ runhaskell client.hs
    
    
  12. 现在,我们可以通过键入并按下Enter键在两者之间发送消息:

    Hello?
    "yup, I can hear you!"
    
    

它是如何工作的…

hGetLine函数会阻塞代码执行,这意味着代码执行在此处暂停,直到接收到消息为止。这允许我们等待消息并进行实时反应。

我们首先在计算机上指定一个端口,这只是一个尚未被其他程序占用的数字。服务器设置套接字,客户端连接到它,而无需进行设置。两者之间传递的消息是实时发生的。

以下图示演示了服务器-客户端模型的可视化:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_05.jpg

通过摄像头流检测人脸和眼睛

摄像头是另一个实时数据的来源。随着帧的进出,我们可以使用 OpenCV 库进行强大的分析。

在这个教程中,我们通过实时摄像头流进行人脸检测。

准备工作

安装 OpenCV、SDL 和 FTGL 库以进行图像处理和计算机视觉:

sudo apt-get install libopencv-dev libsdl1.2-dev ftgl-dev

使用 cabal 安装 OpenCV 库:

cabal install cv-combinators

如何做…

创建一个新的源文件Main.hs,并按照以下步骤操作:

  1. 导入相关库:

    import AI.CV.ImageProcessors
    import qualified AI.CV.OpenCV.CV as CV
    import qualified Control.Processor as Processor
    import Control.Processor ((--<))
    import AI.CV.OpenCV.Types (PImage)
    import AI.CV.OpenCV.CxCore (CvRect(..), CvSize(..))
    import Prelude hiding (id)
    import Control.Arrow ((&&&), (***))
    import Control.Category ((>>>), id)
    
  2. 定义摄像头流的来源。我们将使用内置的摄像头。若要改用视频,可以将camera 0替换为videoFile "./myVideo.mpeg"

    captureDev :: ImageSource
    captureDev = camera 0
    
  3. 缩小流的大小以提高性能:

    resizer :: ImageProcessor
    resizer = resize 320 240 CV.CV_INTER_LINEAR
    
  4. 使用 OpenCV 提供的训练数据集检测图像中的人脸:

    faceDetect :: Processor.IOProcessor PImage [CvRect]
    
    faceDetect = haarDetect  "/usr/share/opencv/haarcascades/haarcascade_frontalface_alt.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20)
    
  5. 使用 OpenCV 提供的训练数据集检测图像中的眼睛:

    eyeDetect :: Processor.IOProcessor PImage [CvRect]
    eyeDetect = haarDetect "/usr/share/opencv/haarcascades/haarcascade_eye.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20)
    
  6. 在人脸和眼睛周围画矩形框:

    faceRects = (id &&& faceDetect) >>> drawRects
    
    eyeRects = (id &&& eyeDetect) >>> drawRects
    
  7. 捕获摄像头流,检测面部和眼睛,绘制矩形,并在两个不同的窗口中显示它们:

    start = captureDev >>> resizer --< (faceRects *** eyeRects) 
                 >>> (window 0 *** window 1)
    
  8. 执行实时摄像头流并在按下某个键后停止:

    main :: IO ()
    main = runTillKeyPressed start
    
  9. 运行代码并查看网络摄像头,以检测面部和眼睛,结果如以下命令后的截图所示:

    $ runhaskell Main.hs
    
    

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_06.jpg

它是如何工作的…

为了检测面部、眼睛或其他物体,我们使用haarDetect函数,它执行从许多正面和负面测试案例中训练出来的分类器。这些测试案例由 OpenCV 提供,通常位于 Unix 系统中的/usr/share/opencv/haarcascades/目录下。

cv-combinator 库提供了 OpenCV 底层操作的便捷抽象。为了运行任何有用的代码,我们必须定义一个源、一个过程和一个最终目标(也称为sink)。在我们的案例中,源是机器内置的摄像头。我们首先将图像调整为更易处理的大小(resizer),然后将流分成两个并行流(--<),在一个流中绘制面部框,在另一个流中绘制眼睛框,最后将这两个流输出到两个独立的窗口。有关 cv-combinators 包的更多文档,请参见hackage.haskell.org/package/cv-combinators

摄像头流的模板匹配

模板匹配是一种机器学习技术,用于寻找与给定模板图像匹配的图像区域。我们将把模板匹配应用于实时视频流的每一帧,以定位图像。

准备工作

安装 OpenCV 和 c2hs 工具包:

$ sudo apt-get install c2hs libopencv-dev

从 cabal 安装 CV 库。确保根据安装的 OpenCV 版本包含–fopencv24–fopencv23参数:

$ cabal install CV -fopencv24

同时,创建一个小的模板图像。在这个实例中,我们使用的是 Lena 的图像,这个图像通常用于许多图像处理实验。我们将此图像文件命名为lena.png

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_07.jpg

如何操作…

在一个新的文件Main.hs中,从以下步骤开始:

  1. 导入相关库:

    {-#LANGUAGE ScopedTypeVariables#-}
    module Main where
    import CV.Image (loadImage, rgbToGray, getSize)
    import CV.Video (captureFromCam, streamFromVideo)
    import Utils.Stream (runStream_, takeWhileS, sideEffect)
    import CV.HighGUI (showImage, waitKey)
    import CV.TemplateMatching ( simpleTemplateMatch
                               , MatchType(..) )
    import CV.ImageOp ((<#))
    import CV.Drawing (circleOp, ShapeStyle(..))
    
  2. 加载模板图像并开始对摄像头流进行模板匹配:

    main = do
      Just t <- loadImage "lena.jpg"
      Just c <- captureFromCam 0
      runStream_ . sideEffect (process t) . 
        takeWhileS (\_ -> True) $ streamFromVideo c
    
  3. 对摄像头流的每一帧执行操作。具体来说,使用模板匹配来定位模板并围绕其绘制一个圆圈:

    process t img = do
      let gray = rgbToGray img
      let ((mx, my), _) = 
        simpleTemplateMatch CCOEFF_NORMED gray t
      let circleSize = (fst (getSize t)) `div` 2
      let circleCenter = (mx + circleSize, my + circleSize)
      showImage "test" (img <# circleOp (0,0,0) 
        circleCenter circleSize (Stroked 3))
      waitKey 100
      return ()
    
  4. 使用以下命令运行代码并显示模板图像。会在找到的图像周围绘制一个黑色圆圈:

    $ runhaskell Main.hs
    
    

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_08.jpg

还有更多内容……

更多 OpenCV 示例可以在github.com/aleator/CV/tree/master/examples找到。

第十一章 数据可视化

在本章中,我们将介绍以下可视化技术:

  • 使用 Google 的 Chart API 绘制折线图

  • 使用 Google 的 Chart API 绘制饼图

  • 使用 Google 的 Chart API 绘制条形图

  • 使用 gnuplot 显示折线图

  • 显示二维点的散点图

  • 与三维空间中的点进行交互

  • 可视化图形网络

  • 自定义图形网络图的外观

  • 使用 D3.js 在 JavaScript 中渲染条形图

  • 使用 D3.js 在 JavaScript 中渲染散点图

  • 从向量列表中绘制路径图

引言

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/ch11.jpg

可视化在数据分析的所有步骤中都非常重要。无论我们是刚开始接触数据,还是已经完成了分析,通过图形辅助工具直观地理解数据总是非常有用的。幸运的是,Haskell 提供了许多库来帮助实现这一目标。

在本章中,我们将介绍使用各种 API 绘制折线图、饼图、条形图和散点图的技巧。除了常见的数据可视化,我们还将学习如何绘制网络图。此外,在最后一个技巧中,我们将通过在空白画布上绘制向量来描述导航方向。

使用 Google 的 Chart API 绘制折线图

我们将使用方便的 Google Chart API (developers.google.com/chart) 来渲染折线图。该 API 会生成指向图表 PNG 图像的 URL。这个轻量级的 URL 比实际的图像更易于处理。

我们的数据将来自一个文本文件,其中包含按行分隔的数字列表。代码将生成一个 URL 来展示这些数据。

准备工作

按如下方式安装 GoogleChart 包:

$ cabal install hs-gchart

创建一个名为 input.txt 的文件,并按如下方式逐行插入数字:

$ cat input.txt 
2
5
3
7
4
1
19
18
17
14
15
16

如何实现…

  1. 按如下方式导入 Google Chart API 库:

    import Graphics.Google.Chart
    
  2. 从文本文件中获取输入,并将其解析为整数列表:

    main = do
      rawInput <- readFile "input.txt"
      let nums = map (read :: String -> Int) (lines rawInput)
    
  3. 通过适当设置属性,创建一个图表 URL,如以下代码片段所示:

      putStrLn $ chartURL $
        setSize 500 200 $
        setTitle "Example of Plotting a Chart in Haskell" $
        setData (encodeDataSimple [nums]) $
        setLegend ["Stock Price"] $
        newLineChart
    
  4. 运行程序将输出一个 Google Chart URL,如下所示:

    $ runhaskell Main.hs
    http://chart.apis.google.com/chart?chs=500x200&chtt=Example+of+Plotting+a+Chart+in+Haskell&chd=s:CFDHEBTSROPQ&chdl=Stock+Price&cht=lc
    
    

确保网络连接正常,并导航到该 URL 查看图表,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_01.jpg

工作原理…

Google 会将所有图表数据编码到 URL 中。我们的图表越复杂,Google 图表 URL 就越长。在这个技巧中,我们使用 encodeDataSimple 函数,它创建了一个相对较短的 URL,但只接受 0 到 61 之间的整数(包括 0 和 61)。

还有更多内容…

为了可视化一个更详细的图表,允许数据具有小数位数,我们可以使用 encodeDataText :: RealFrac a => [[a]] -> ChartData 函数。这将允许 0 到 100 之间的十进制数(包含 0 和 100)。

为了在图表中表示更大的整数范围,我们应使用 encodeDataExtended 函数,它支持 0 到 4095 之间的整数(包括 0 和 4095)。

关于 Google Charts Haskell 包的更多信息,请访问 hackage.haskell.org/package/hs-gchart

另请参见

此配方需要连接互联网以查看图表。如果我们希望在本地执行所有操作,请参考 使用 gnuplot 显示折线图 配方。其他 Google API 配方包括 使用 Google 的 Chart API 绘制饼图使用 Google 的 Chart API 绘制条形图

使用 Google 的 Chart API 绘制饼图

Google Chart API 提供了一个外观非常优雅的饼图界面。通过正确地输入数据和标签,我们可以生成设计精良的饼图,如本配方所示。

准备工作

按如下方式安装 GoogleChart 包:

$ cabal install hs-gchart

创建一个名为 input.txt 的文件,每行插入数字,格式如下:

$ cat input.txt 
2
5
3
7
4
1
19
18
17
14
15
16

如何操作…

  1. 按如下方式导入 Google Chart API 库:

    import Graphics.Google.Chart
    
  2. 从文本文件中收集输入并将其解析为整数列表,如以下代码片段所示:

    main = do
      rawInput <- readFile "input.txt"
      let nums = map (read :: String -> Int) (lines rawInput)
    
  3. 从以下代码中显示的饼图属性中打印出 Google Chart URL:

      putStrLn $ chartURL $
        setSize 500 400 $
        setTitle "Example of Plotting a Pie Chart in Haskell" $
        setData (encodeDataSimple [nums]) $
        setLabels (lines rawInput) $
        newPieChart Pie2D
    
  4. 运行程序将输出如下的 Google Chart URL:

    $ runhaskell Main.hs
    http://chart.apis.google.com/chart?chs=500x400&chtt=Example+of+Plotting+a+Pie+Chart+in+Haskell&chd=s:CFDHEBTSROPQ&chl=2|5|3|7|4|1|19|18|17|14|15|16&cht=p
    
    

确保有网络连接,并访问该网址以查看下图所示的图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_02.jpg

如何运作…

Google 将所有图表数据编码在 URL 中。图表越复杂,Google Chart URL 越长。在此配方中,我们使用 encodeDataSimple 函数,它创建一个相对较短的 URL,但仅接受 0 到 61(包括 0 和 61)之间的整数。饼图的图例由 setLabels :: [String] -> PieChart -> PieChart 函数按照与数据相同的顺序指定。

还有更多…

为了可视化一个包含小数的更详细的图表,我们可以使用 encodeDataText :: RealFrac a => [[a]] -> ChartData 函数。该函数支持 0 到 100(包括 0 和 100)之间的小数。

为了在图表中表示更大的整数范围,我们应使用 encodeDataExtended 函数,该函数支持 0 到 4095(包括 0 和 4095)之间的整数。

关于 Google Charts Haskell 包的更多信息,请访问 hackage.haskell.org/package/hs-gchart

另请参见

  • 使用 Google 的 Chart API 绘制折线图

  • 使用 Google 的 Chart API 绘制条形图

使用 Google 的 Chart API 绘制条形图

Google Chart API 也很好地支持条形图。在本配方中,我们将生成包含两组输入数据的条形图,以展示该 API 的实用性。

准备工作

按如下方式安装 GoogleChart 包:

$ cabal install hs-gchart

创建两个文件,名为 input1.txtinput2.txt,每行插入数字,格式如下:

$ cat input1.txt 
2
5
3
7
4
1
19
18
17
14
15
16

$ cat input2.txt
4
2
6
7
8
2
18
17
16
17
15
14

如何操作…

  1. 按如下方式导入 Google Chart API 库:

    import Graphics.Google.Chart
    
  2. 从两个文本文件中获取两个输入值,并将它们解析为两个独立的整数列表,如以下代码片段所示:

    main = do
      rawInput1 <- readFile "input1.txt"
      rawInput2 <- readFile "input2.txt"
      let nums1 = map (read :: String -> Int) (lines rawInput1)
      let nums2 = map (read :: String -> Int) (lines rawInput2)
    
  3. 同样设置柱状图并打印出 Google Chart URL,如下所示:

      putStrLn $ chartURL $
        setSize 500 400 $
        setTitle "Example of Plotting a Bar Chart in Haskell" $
        setDataColors ["00ff00", "ff0000"] $
        setLegend ["A", "B"] $
        setData (encodeDataSimple [nums1, nums2]) $
        newBarChart Horizontal Grouped
    
  4. 运行程序将输出一个 Google Chart URL,如下所示:

    $ runhaskell Main.hs
    http://chart.apis.google.com/chart?chs=500x400&chtt=Example+of+Plotting+a+Bar+Chart+in+Haskell&chco=00ff00,ff0000&chdl=A|B&chd=s:CFDHEBTSROPQ,ECGHICSRQRPO&cht=bhg
    
    

确保存在互联网连接并导航到该 URL 以查看以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_03.jpg

如何实现…

Google 将所有图表数据编码在 URL 中。图表越复杂,Google Chart URL 就越长。在本教程中,我们使用encodeDataSimple函数,它创建了一个相对较短的 URL,但仅接受 0 到 61 之间的整数。

还有更多…

若要可视化更详细的图表并允许数据具有小数位,我们可以改用 encodeDataText :: RealFrac a => [[a]] -> ChartData 函数。该函数允许介于 0 和 100 之间的小数。

若要在图表中表示更大的整数范围,我们应使用 encodeDataExtended 函数,该函数支持介于 0 和 4095 之间的整数。

关于 Google Charts Haskell 包的更多信息,请访问 hackage.haskell.org/package/hs-gchart

另见

若要使用其他 Google Chart 工具,请参考使用 Google Chart API 绘制饼图使用 Google Chart API 绘制折线图的教程。

使用 gnuplot 显示折线图

绘制图表通常不需要互联网连接。因此,在本教程中,我们将展示如何在本地绘制折线图。

准备工作

本教程使用的库通过 gnuplot 渲染图表。我们应首先安装 gnuplot。

在基于 Debian 的系统(如 Ubuntu)上,我们可以使用 apt-get 安装,如下所示:

$ sudo apt-get install gnuplot-x11

gnuplot 的官方下载地址是其官方网站 www.gnuplot.info

安装 gnuplot 后,使用 cabal 安装 EasyPlot Haskell 库,如下所示:

$ cabal install easyplot

如何实现…

  1. 按照以下方式导入EasyPlot库:

    import Graphics.EasyPlot
    
  2. 定义一个数字列表进行绘图,如下所示:

    main = do
      let values = [4,5,16,15,14,13,13,17]
    
  3. 如以下代码片段所示,在X11窗口上绘制图表。X11 X Window 系统终端被许多基于 Linux 的机器使用。如果在 Windows 上运行,我们应使用Windows终端。在 Mac OS X 上,我们应将X11替换为Aqua

      plot X11 $ 
        Data2D [ Title "Line Graph"
               , Style Linespoints
               , Color Blue] 
        [] (zip [1..] values)
    

运行代码将生成一个 plot1.dat 数据文件,并从选定的终端显示可视化图表,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_04.jpg

如何实现…

EasyPlot库将所有用户指定的代码转换为 gnuplot 可理解的语言,用于绘制数据图表。

另见

若要使用 Google Chart API 而不是 easy plot,请参考使用 Google Chart API 绘制折线图的教程。

显示二维点的散点图

本教程介绍了一种快速简单的方法,可以将 2D 点列表可视化为图像中的散点。

准备工作

本食谱中使用的库通过 gnuplot 来渲染图表。我们应先安装 gnuplot。

在基于 Debian 的系统(如 Ubuntu)上,我们可以使用 apt-get 安装,方法如下:

$ sudo apt-get install gnuplot-x11

下载 gnuplot 的官方网站是 www.gnuplot.info

在设置好 gnuplot 后,使用 cabal 安装 easyplot Haskell 库,如下所示:

$ cabal install easyplot

同样,安装一个辅助的 CSV 包,如下所示:

$ cabal install csv

同样,创建两个逗号分隔的文件 input1.csvinput2.csv,这两个文件表示两组独立的点,如下所示:

$ cat input1.csv
1,2
3,2
2,3
2,2
3,1
2,2
2,1

$ cat input2.csv
7,4
8,4
6,4
7,5
7,3
6,4
7,6

它是如何工作的…

  1. 导入相关的包,如下所示:

    import Graphics.EasyPlot
    
    import Text.CSV
    
  2. 定义一个辅助函数将 CSV 记录转换为数字元组,如下所示:

    convertRawCSV :: [[String]] -> [(Double, Double)]
    convertRawCSV csv = [ (read x, read y) | [x, y] <- csv ]
    
  3. 读取这两个 CSV 文件,如下所示:

    main = do
      csv1Raw <- parseCSVFromFile "input1.csv"
      csv2Raw <- parseCSVFromFile "input2.csv"
    
      let csv1 = case csv1Raw of 
            Left err -> []
            Right csv -> convertRawCSV csv
    
      let csv2 = case csv2Raw of 
            Left err -> []
            Right csv -> convertRawCSV csv
    
  4. 在同一图表上,使用不同颜色将两个数据集并排绘制。对于许多基于 Linux 的机器,使用 X11 终端来支持 X Window 系统,如下代码所示。如果在 Windows 上运行,则使用 Windows 终端。在 Mac OS X 上,应将 X11 替换为 Aqua

      plot X11 $ [ Data2D [Color Red] [] csv1
      , Data2D [Color Blue] [] csv2 ]
    
  5. 运行程序以显示下方截图中所示的图表:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_05.jpg

它是如何工作的…

EasyPlot 库将所有用户指定的代码转换为 gnuplot 可理解的语言来绘制数据。plot 函数的最后一个参数可以接受多个数据集的列表来绘制图形。

另见

要可视化 3D 点,请参阅 与三维空间中的点交互 这一食谱。

与三维空间中的点交互

在可视化 3D 空间中的点时,交互式地旋转、缩放和平移表示非常有用。本食谱演示了如何在 3D 中绘制数据并实时交互。

准备工作

本食谱中使用的库通过 gnuplot 来渲染图表。我们应先安装 gnuplot。

在基于 Debian 的系统(如 Ubuntu)上,我们可以使用 apt-get 安装,方法如下:

$ sudo apt-get install gnuplot-x11

下载 gnuplot 的官方网站是 www.gnuplot.info

在设置好 gnuplot 后,使用 Cabal 安装 easyplot Haskell 库,如下所示:

$ cabal install easyplot

同样,安装一个辅助的 CSV 包,如下所示:

$ cabal install csv

同样,创建两个逗号分隔的文件 input1.csvinput2.csv,这两个文件表示两组独立的点,如下所示:

$ cat input1.csv
1,1,1
1,2,1
0,1,1
1,1,0
2,1,0
2,1,1
1,0,1

$ cat input2.csv
4,3,2
3,3,2
3,2,3
4,4,3
5,4,2
4,2,3
3,4,3

它是如何工作的…

  1. 导入相关的包,如下所示:

    import Graphics.EasyPlot
    
    import Text.CSV
    
  2. 定义一个辅助函数将 CSV 记录转换为数字元组,如下所示:

    convertRawCSV :: [[String]] -> [(Double, Double, Double)]
    
    convertRawCSV csv = [ (read x, read y, read z) 
                        | [x, y, z] <- csv ]
    
  3. 读取这两个 CSV 文件,如下所示:

    main = do
      csv1Raw <- parseCSVFromFile "input1.csv"
      csv2Raw <- parseCSVFromFile "input2.csv"
    
      let csv1 = case csv1Raw of
            Left err -> []
            Right csv -> convertRawCSV csv
    
      let csv2 = case csv2Raw of
            Left err -> []
            Right csv -> convertRawCSV csv
    
  4. 使用 plot' 函数绘制数据,该函数会保持 gnuplot 运行以启用 Interactive 选项。对于许多基于 Linux 的机器,使用 X11 终端来支持 X Window 系统,如下代码所示。如果在 Windows 上运行,则使用 Windows 终端。在 Mac OS X 上,应将 X11 替换为 Aqua

      plot' [Interactive] X11 $ 
        [ Data3D [Color Red] [] csv1
        , Data3D [Color Blue] [] csv2]
    

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_06.jpg

它是如何工作的…

EasyPlot 库将所有用户指定的代码转换成 gnuplot 能理解的语言,以绘制数据图表。最后一个参数 plot 可以接受一个数据集列表进行绘图。通过使用 plot' 函数,我们可以让 gnuplot 持续运行,这样我们可以通过旋转、缩放和平移三维图像与图形进行交互。

另请参阅

要可视化二维点,请参考 显示二维点的散点图 示例。

可视化图形网络

边和节点的图形化网络可能很难调试或理解,因此可视化可以极大地帮助我们。在本教程中,我们将把一个图形数据结构转换成节点和边的图像。

准备工作

要使用 Graphviz 图形可视化库,我们首先需要在机器上安装它。Graphviz 的官方网站包含了下载和安装说明(www.graphviz.org)。在基于 Debian 的操作系统上,可以通过以下方式使用 apt-get 安装 Graphviz:

$ sudo apt-get install graphviz-dev graphviz

接下来,我们需要通过 Cabal 安装 Graphviz 的 Haskell 绑定,具体方法如下:

$ cabal install graphviz

如何操作…

  1. 导入相关的库,如下所示:

    import Data.Text.Lazy (Text, empty, unpack)
    import Data.Graph.Inductive (Gr, mkGraph)
    import Data.GraphViz (GraphvizParams, nonClusteredParams, graphToDot)
    import Data.GraphViz.Printing (toDot, renderDot)
    
  2. 使用以下代码行创建一个通过识别形成边的节点对来定义的图形:

    myGraph :: Gr Text Text
    myGraph = mkGraph [ (1, empty)
                      , (2, empty)
                      , (3, empty) ]
              [ (1, 2, empty) 
              , (1, 3, empty) ]
    
  3. 设置图形使用默认参数,如下所示:

    myParams :: GraphvizParams n Text Text () Text
    myParams = nonClusteredParams
    
  4. 如下所示,将图形的 dot 表示打印到终端:

    main :: IO ()
    main = putStr $ unpack $ renderDot $ toDot $ 
           graphToDot myParams myGraph
    
  5. 运行代码以获取图形的 dot 表示,并将其保存到一个单独的文件中,如下所示:

    $ runhaskell Main.hs > graph.dot
    
  6. 对该文件运行 Graphviz 提供的 dot 命令,以渲染出如下的图像:

    $ dot -Tpng graph.dot > graph.png
    
  7. 现在我们可以查看生成的 graph.png 文件,截图如下所示:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_07.jpg

如何工作…

graphToDot 函数将图形转换为 DOT 语言,以描述图形。这是图形的文本序列化形式,可以被 Graphviz 的 dot 命令读取并转换成可视化图像。

更多内容…

在本教程中,我们使用了 dot 命令。Graphviz 网站还描述了其他可以将 DOT 语言文本转换成可视化图像的命令:

dot - "层级"或分层绘制有向图。如果边具有方向性,这是默认的工具。

neato - "弹簧模型"布局。如果图形不太大(约 100 个节点)且你对图形没有其他了解,这是默认的工具。Neato 尝试最小化一个全局能量函数,这等同于统计多维尺度化。

fdp - "弹簧模型"布局,类似于 neato,但通过减少力来完成布局,而不是使用能量。

sfdp - fdp 的多尺度版本,用于大图的布局。

twopi - 径向布局,基于 Graham Wills 97。节点根据与给定根节点的距离,放置在同心圆上。

circo - 圆形布局,参考 Six 和 Tollis 99,Kauffman 和 Wiese 02。这适用于某些包含多个循环结构的图,如某些电信网络。

另见

要进一步更改图形的外观和感觉,请参考 自定义图形网络图的外观 这一食谱。

自定义图形网络图的外观

为了更好地呈现数据,我们将介绍如何定制图形网络图的设计。

准备工作

要使用 Graphviz 图形可视化库,我们首先需要在机器上安装它。Graphviz 的官方网站包含了下载和安装说明,网址为 www.graphviz.org。在基于 Debian 的操作系统上,可以使用apt-get命令来安装 Graphviz,方法如下:

$ sudo apt-get install graphviz-dev graphviz

接下来,我们需要从 Cabal 安装 Graphviz Haskell 绑定,方法如下:

$ cabal install graphviz

如何操作…

  1. 导入相关的函数和库,以自定义 Graphviz 图形,方法如下:

    import Data.Text.Lazy (Text, pack, unpack)
    import Data.Graph.Inductive (Gr, mkGraph)
    import Data.GraphViz (
      GraphvizParams(..),
      GlobalAttributes(
        GraphAttrs,
        NodeAttrs,
        EdgeAttrs
        ),
      X11Color(Blue, Orange, White),
      nonClusteredParams,
      globalAttributes,
      fmtNode,
      fmtEdge,
      graphToDot
      )
    import Data.GraphViz.Printing (toDot, renderDot)
    import Data.GraphViz.Attributes.Complete
    
  2. 按照以下代码片段,首先指定所有节点,然后指定哪些节点对形成边,来定义我们的自定义图形:

    myGraph :: Gr Text Text
    myGraph = mkGraph [ (1, pack "Haskell")
                      , (2, pack "Data Analysis") 
                      , (3, pack "Haskell Data Analysis")
                      , (4, pack "Profit!")] 
             [ (1, 3, pack "learn") 
             , (2, 3, pack "learn")
             , (3, 4, pack "???")]
    
  3. 按照以下方式定义我们自己的自定义图形参数:

    myParams :: GraphvizParams n Text Text () Text
    myParams = nonClusteredParams { 
    
  4. 让图形引擎知道我们希望边缘是有向箭头,方法如下:

        isDirected       = True
    
  5. 设置图形、节点和边缘外观的全局属性如下:

      , globalAttributes = [myGraphAttrs, myNodeAttrs, myEdgeAttrs]
    
  6. 按照我们自己的方式格式化节点如下:

      , fmtNode          = myFN
    
  7. 按照我们自己的方式格式化边缘如下:

      , fmtEdge          = myFE
      }
    
  8. 按照以下代码片段定义自定义内容:

     where myGraphAttrs = 
                     GraphAttrs [ RankDir FromLeft
                                        , BgColor [toWColor Blue] ]
                 myNodeAttrs = 
                      NodeAttrs [ Shape BoxShape
                                       , FillColor [toWColor Orange]
                                       , Style [SItem Filled []] ]
                 myEdgeAttrs = 
                      EdgeAttrs [ Weight (Int 10) 
                                       , Color [toWColor White]
                                       , FontColor (toColor White) ]
                 myFN (n,l) = [(Label . StrLabel) l]
                 myFE (f,t,l) = [(Label . StrLabel) l]
    
  9. 将图形的 DOT 语言表示打印到终端。

    main :: IO ()
    main = putStr $ unpack $ renderDot $ toDot $ graphToDot myParams myGraph
    
  10. 运行代码以获取图形的dot表示,可以将其保存在单独的文件中,方法如下:

    $ runhaskell Main.hs > graph.dot
    
    
  11. 在此文件上运行 Graphviz 提供的dot命令,以渲染图像,方法如下:

    $ dot -Tpng graph.dot > graph.png
    
    

我们现在可以查看生成的graph.png文件,如下所示的截图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_08.jpg

工作原理…

graphToDot 函数将图形转换为 DOT 语言,以描述图形。这是一种图形的文本序列化格式,可以被 Graphviz 的 dot 命令读取,并转换为可视化图像。

还有更多……

图形、节点和边缘的所有可能自定义选项都可以在 Data.GraphViz.Attributes.Complete 包文档中找到,网址为 hackage.haskell.org/package/graphviz-2999.12.0.4/docs/Data-GraphViz-Attributes-Complete.html

使用 D3.js 在 JavaScript 中渲染条形图

我们将使用名为D3.js的便携式 JavaScript 库来绘制条形图。这使得我们能够轻松地创建一个包含图表的网页,该图表来自 Haskell 代码。

准备工作

设置过程中需要连接互联网。

按照以下方式安装 d3js Haskell 库:

$ cabal install d3js

创建一个网站模板,用于承载生成的 JavaScript 代码,方法如下:

$ cat index.html

JavaScript 代码如下:

<html>
  <head>
    <title>Chart</title>
  </head>
  <body>
    <div id='myChart'></div>
    <script charset='utf-8' src='http://d3js.org/d3.v3.min.js'></script>
    <script charset='utf-8' src='generated.js'></script>
  </body>
</html>

如何操作…

  1. 按照以下方式导入相关的包:

    import qualified Data.Text as T
    import qualified Data.Text.IO as TIO
    import D3JS
    
  2. 使用bars函数创建条形图。输入指定的值和要绘制的条形数量,如以下代码片段所示:

    myChart nums numBars = do
      let dim = (300, 300)
      elem <- box (T.pack "#myChart") dim
      bars numBars 300 (Data1D nums) elem
      addFrame (300, 300) (250, 250) elem
    
  3. 定义要绘制的条形图的值和数量如下:

    main = do
      let nums = [10, 40, 100, 50, 55, 156, 80, 74, 40, 10]
      let numBars = 5
    
  4. 使用reify函数从数据生成 JavaScript D3.js代码。将 JavaScript 写入名为generated.js的文件,如下所示:

      let js = reify $ myChart nums numBars
      TIO.writeFile "generated.js" js
    
  5. index.html文件和generated.js文件并排存在的情况下,我们可以使用支持 JavaScript 的浏览器打开index.html网页,并看到如下所示的图表:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_09.jpg

它是如何工作的…

D3.js库是一个用于创建优雅可视化和图表的 JavaScript 库。我们使用浏览器运行 JavaScript 代码,它也充当我们的图表渲染引擎。

另请参阅

另一个D3.js的用法,请参阅使用 D3.js 在 JavaScript 中渲染散点图食谱。

使用 D3.js 在 JavaScript 中渲染散点图

我们将使用名为D3.js的便携式 JavaScript 库来绘制散点图。这样我们就可以轻松地创建一个包含图表的网页,该图表来自 Haskell 代码。

准备工作

进行此设置需要互联网连接。

如下所示安装d3js Haskell 库:

$ cabal install d3js

创建一个网站模板来承载生成的 JavaScript 代码,如下所示:

$ cat index.html

JavaScript 代码如下所示:

<html>
  <head>
    <title>Chart</title>
  </head>
  <body>
    <div id='myChart'></div>
    <script charset='utf-8' src='http://d3js.org/d3.v3.min.js'></script>
    <script charset='utf-8' src='generated.js'></script>
  </body>
</html>

如何操作…

  1. 导入相关库,如下所示:

    import D3JS
    import qualified Data.Text as T
    import qualified Data.Text.IO as TIO
    
  2. 定义散点图并输入点列表,如下所示:

    myPlot points = do
      let dim = (300, 300)
      elem <- box (T.pack "#myChart") dim
      scatter (Data2D points) elem
      addFrame (300, 300) (250, 250) elem   
    
  3. 定义要绘制的点列表,如下所示:

    main = do
      let points = [(1,2), (5,10), (139,138), (140,150)]
    
  4. 使用reify函数从数据生成 JavaScript D3.js代码。将 JavaScript 写入名为generated.js的文件,如下所示:

      let js = reify $ myPlot points
      TIO.writeFile "generated.js" js
    
  5. index.htmlgenerated.js文件并排存在的情况下,我们可以使用支持 JavaScript 的浏览器打开index.html网页,并看到如下所示的图表:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_10.jpg

它是如何工作的…

graphToDot函数将图表转换为 DOT 语言来描述图表。这是图表的文本序列化格式,可以通过 Graphviz 的dot命令读取并转换为可视化图像。

另请参阅

另一个D3.js的用法,请参阅使用 D3.js 在 JavaScript 中渲染条形图食谱。

从向量列表绘制路径

在这个食谱中,我们将使用diagrams包来从驾驶路线中绘制路径。我们将所有可能的旅行方向分类为八个基本方向,并附上相应的距离。我们使用下图中 Google Maps 提供的方向,并从文本文件中重建这些方向:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_12.jpg

准备工作

如下所示安装diagrams库:

$ cabal install diagrams

创建一个名为input.txt的文本文件,其中包含八个基本方向之一,后面跟着距离,每一步用新的一行分隔:

$ cat input.txt

N 0.2
W 0.1
S 0.6
W 0.05
S 0.3
SW 0.1
SW 0.2
SW 0.3
S 0.3

如何操作…

  1. 导入相关库,如下所示:

    {-# LANGUAGE NoMonomorphismRestriction #-}
    import Diagrams.Prelude
    import Diagrams.Backend.SVG.CmdLine (mainWith, B)
    
  2. 从一系列向量中绘制一个连接的路径,如下所示:

    drawPath :: [(Double, Double)] -> Diagram B R2
    drawPath vectors = fromOffsets . map r2 $ vectors
    
  3. 读取一系列方向,将其表示为向量列表,并按如下方式绘制路径:

    main = do
      rawInput <- readFile "input.txt"
      let vs = [ makeVector dir (read dist)
               | [dir, dist] <- map words (lines rawInput)]
      print vs
      mainWith $ drawPath vs
    
  4. 定义一个辅助函数,根据方向及其对应的距离创建一个向量,如下所示:

    makeVector :: String -> Double -> (Double, Double)
    makeVector "N" dist = (0, dist)
    makeVector "NE" dist = (dist / sqrt 2, dist / sqrt 2)
    makeVector "E" dist = (dist, 0)
    makeVector "SE" dist = (dist / sqrt 2, -dist / sqrt 2)
    makeVector "S" dist = (0, -dist)
    makeVector "SW" dist = (-dist / sqrt 2, -dist / sqrt 2)
    makeVector "W" dist = (-dist, 0)
    makeVector "NW" dist = (-dist / sqrt 2, dist / sqrt 2)
    makeVector _ _ = (0, 0)
    
  5. 编译代码并按如下方式运行:

    $ ghc --make Main.hs
    $ ./Main –o output.svg –w 400
    
    

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_11.jpg

它是如何工作的…

mainWith 函数接收一个 Diagram 类型,并在终端中调用时生成相应的图像文件。我们通过 drawPath 函数获得 Diagram,该函数通过偏移量将向量连接在一起。

第十二章 导出与展示

本章将涵盖如何导出结果并通过以下食谱优雅地展示它们:

  • 将数据导出到 CSV 文件

  • 将数据导出为 JSON

  • 使用 SQLite 存储数据

  • 将数据保存到 MongoDB 数据库

  • 在 HTML 网页中展示结果

  • 创建 LaTeX 表格以展示结果

  • 使用文本模板个性化消息

  • 将矩阵值导出到文件

介绍

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/ch12.jpg

在数据收集、清洗、表示和分析后,数据分析的最后一步是将数据导出并以可用格式展示。 本章中的食谱将展示如何将数据结构保存到磁盘,以供其他程序后续使用。此外,我们还将展示如何使用 Haskell 优雅地展示数据。

将数据导出到 CSV 文件

有时,使用像 LibreOffice、Microsoft Office Excel 或 Apple Numbers 这样的电子表格程序查看数据更为便捷。导出和导入简单电子表格表格的标准方式是通过逗号分隔值CSV)。

在这个食谱中,我们将使用cassava包轻松地从数据结构编码一个 CSV 文件。

准备工作

使用以下命令从 cabal 安装 Cassava CSV 包:

$ cabal install cassava

如何实现……

  1. 使用以下代码导入相关包:

    import Data.Csv
    import qualified Data.ByteString.Lazy as BSL
    
  2. 定义将作为 CSV 导出的数据关联列表。在这个食谱中,我们将字母和数字配对,如以下代码所示:

    myData :: [(Char, Int)]
    myData = zip ['A'..'Z'] [1..]
    
  3. 运行encode函数将数据结构转换为懒加载的 ByteString CSV 表示,如以下代码所示:

    main = BSL.writeFile "letters.csv" $ encode myData
    

它是如何工作的……

CSV 文件只是记录的列表。Cassava 库中的encode函数接受实现了ToRecord类型类的项列表。

在这个食谱中,我们可以看到像('A', 1)这样的大小为 2 的元组是encode函数的有效参数。默认情况下,支持大小为 2 到 7 的元组以及任意大小的列表。元组或列表的每个元素必须实现ToField类型类,大多数内置的原始数据类型默认支持该类。有关该包的更多细节,请访问hackage.haskell.org/package/cassava

还有更多……

为了方便地将数据类型转换为 CSV,我们可以实现ToRecord类型类。

例如,Cassava 文档展示了以下将Person数据类型转换为 CSV 记录的例子:

data Person = Person { name :: Text, age :: Int }

instance ToRecord Person where
     toRecord (Person name age) = record [
        toField name, toField age]

另请参见

如果是 JSON 格式,请参考以下导出数据为 JSON食谱。

导出数据为 JSON

存储可能不遵循严格模式的数据的便捷方式是通过 JSON。为此,我们将使用一个名为Yocto的简便 JSON 库。它牺牲了性能以提高可读性并减小体积。

在这个食谱中,我们将导出一个点的列表为 JSON 格式。

准备工作

使用以下命令从 cabal 安装 Yocto JSON 编码器和解码器:

$ cabal install yocto

如何实现……

从创建一个新的文件开始,我们称其为Main.hs,并执行以下步骤:

  1. 如下所示导入相关数据结构:

    import Text.JSON.Yocto
    import qualified Data.Map as M
    
  2. 如下所示定义一个二维点的数据结构:

    data Point = Point Rational Rational
    
  3. Point数据类型转换为 JSON 对象,如下方代码所示:

    pointObject (Point x y) = 
      Object $ M.fromList [ ("x", Number x)
                          , ("y", Number y)]
    
  4. 创建点并构建一个 JSON 数组:

    main = do
      let points = [ Point 1 1
                   , Point 3 5
                   , Point (-3) 2]
      let pointsArray = Array $ map pointObject points
    
  5. 将 JSON 数组写入文件,如下方代码所示:

      writeFile "points.json" $ encode pointsArray
    
  6. 运行代码时,我们会发现生成了points.json文件,如下方代码所示:

    $ runhaskell Main.hs
    $ cat points.json
    [{"x":1,"y":1}, {"x":3,"y":5}, {"x":-3,"y":2}]
    
    

还有更多内容…

若需更高效的 JSON 编码器,请参考 Aeson 包,位于hackage.haskell.org/package/aeson

参见

要将数据导出为 CSV,请参考前面标题为导出数据到 CSV 文件的食谱。

使用 SQLite 存储数据

SQLite 是最流行的数据库之一,用于紧凑地存储结构化数据。我们将使用 Haskell 的 SQL 绑定来存储字符串列表。

准备工作

我们必须首先在系统上安装 SQLite3 数据库。在基于 Debian 的系统上,我们可以通过以下命令进行安装:

$ sudo apt-get install sqlite3

使用以下命令从 cabal 安装 SQLite 包:

$ cabal install sqlite-simple

创建一个名为test.db的初始数据库,并设置其模式。在本食谱中,我们只会存储整数和字符串,如下所示:

$ sqlite3 test.db "CREATE TABLE test (id INTEGER PRIMARY KEY, str text);"

如何实现…

  1. 导入相关库,如下方代码所示:

    {-# LANGUAGE OverloadedStrings #-}
    import Control.Applicative
    import Database.SQLite.Simple
    import Database.SQLite.Simple.FromRow
    
  2. TestField(我们将要存储的数据类型)创建一个FromRow类型类的实现,如下方代码所示:

    data TestField = TestField Int String deriving (Show)
    instance FromRow TestField where
      fromRow = TestField <$> field <*> field
    
  3. 创建一个辅助函数,用于仅为调试目的从数据库中检索所有数据,如下方代码所示:

    getDB :: Connection -> IO [TestField]
    
    getDB conn = query_ conn "SELECT * from test"
    
  4. 创建一个辅助函数,将字符串插入数据库,如下方代码所示:

    insertToDB :: Connection -> String -> IO ()  
    insertToDB conn item = 
      execute conn 
      "INSERT INTO test (str) VALUES (?)" 
      (Only item)
    
  5. 如下所示连接到数据库:

    main :: IO ()
    
    main = withConnection "test.db" dbActions
    
  6. 设置我们希望插入的字符串数据,如下方代码所示:

    dbActions :: Connection -> IO ()
    
    dbActions conn = do
      let dataItems = ["A", "B", "C"]
    
  7. 将每个元素插入数据库,如下方代码所示:

      mapM_ (insertToDB conn) dataItems
    
  8. 使用以下代码打印数据库内容:

      r <- getDB conn
      mapM_ print r 
    
  9. 我们可以通过调用以下命令验证数据库中是否包含新插入的数据:

    $ sqlite3 test.db "SELECT * FROM test"
    
    1|A
    2|C
    3|D
    
    

参见

若使用另一种类型的数据库,请参考以下食谱保存数据到 MongoDB 数据库

保存数据到 MongoDB 数据库

MongoDB 可以非常自然地使用 JSON 语法存储非结构化数据。在本例中,我们将把一组人员数据存储到 MongoDB 中。

准备工作

我们必须首先在机器上安装 MongoDB。安装文件可以从www.mongodb.org下载。

我们需要使用以下命令为数据库创建一个目录:

$ mkdir ~/db

最后,使用以下命令在该目录下启动 MongoDB 守护进程:

$ mongod –dbpath ~/db

使用以下命令从 cabal 安装 MongoDB 包:

$ cabal install mongoDB

如何实现…

创建一个名为Main.hs的新文件,并执行以下步骤:

  1. 按照如下方式导入库:

    {-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}
    import Database.MongoDB
    import Control.Monad.Trans (liftIO)
    
  2. 如下所示定义一个表示人物姓名的数据类型:

    data Person = Person { first :: String 
                         , last :: String }
    
  3. 设置我们希望存储的几个数据项,如下所示:

    myData :: [Person]
    myData = [ Person "Mercury" "Merci"
             , Person "Sylvester" "Smith"]
    
  4. 连接到 MongoDB 实例并存储所有数据,如下所示:

    main = do
        pipe <- runIOE $ connect (host "127.0.0.1")
        e <- access pipe master "test" (store myData)
        close pipe
        print e
    
  5. 按照以下方式将Person数据类型转换为适当的 MongoDB 类型:

    store vals = insertMany "people" mongoList 
      where mongoList = map 
                        (\(Person f l) -> 
                          ["first" =: f, "last" =: l]) 
                        vals
    
  6. 我们必须确保 MongoDB 守护进程正在运行。如果没有,我们可以使用以下命令创建一个监听我们选择目录的进程:

    $ mongod --dbpath ~/db
    
    
  7. 运行代码后,我们可以通过以下命令检查操作是否成功,方法是访问 MongoDB:

    $ runhaskell Main.hs
    $ mongo
    >  db.people.find()
    { "_id" : ObjectId("536d2b13f8712126e6000000"), "first" : "Mercury", "last" : "Merci" }
    { "_id" : ObjectId("536d2b13f8712126e6000001"), "first" : "Sylvester", "last" : "Smith" }
    
    

另见

对于 SQL 的使用,请参考之前的使用 SQLite 存储数据的做法。

在 HTML 网页中展示结果

在线共享数据是触及广泛受众的最快方式之一。然而,直接将数据输入到 HTML 中可能会耗费大量时间。本做法将使用 Blaze Haskell 库生成一个网页,来展示数据结果。更多文档和教程,请访问项目网页jaspervdj.be/blaze/

准备工作

从 cabal 使用以下命令安装 Blaze 包:

$ cabal install blaze-html

如何操作…

在一个名为Main.hs的新文件中,执行以下步骤:

  1. 按照以下方式导入所有必要的库:

    {-# LANGUAGE OverloadedStrings #-}
    
    import Control.Monad (forM_)
    import Text.Blaze.Html5
    import qualified Text.Blaze.Html5 as H
    import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
    import qualified Data.ByteString.Lazy as BSL
    
  2. 按照以下代码片段将字符串列表转换为 HTML 无序列表:

    dataInList :: Html -> [String] -> Html
    dataInList label items = docTypeHtml $ do    
      H.head $ do
        H.title "Generating HTML from data"
      body $ do
        p label
        ul $ mapM_ (li . toHtml) items
    
  3. 创建一个字符串列表,并按以下方式将其渲染为 HTML 网页:

    main = do    
      let movies = [ "2001: A Space Odyssey"
                   , "Watchmen"
                   , "GoldenEye" ]
      let html = renderHtml $ dataInList "list of movies" movies
      BSL.writeFile "index.html" $ html
    
  4. 运行代码以生成 HTML 文件,并使用浏览器打开,如下所示:

    $ runhaskell Main.hs
    
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_12_01.jpg

另见

要将数据呈现为 LaTeX 文档并最终生成 PDF,请参考以下创建一个 LaTeX 表格来展示结果的做法。

创建一个 LaTeX 表格来展示结果

本做法将通过编程方式创建一个 LaTeX 表格,以便于文档的创建。我们可以从 LaTeX 代码生成 PDF 并随意分享。

准备工作

从 cabal 安装HaTeX,Haskell LaTeX 库:

$ cabal install LaTeX

如何操作…

创建一个名为Main.hs的文件,并按照以下步骤进行:

  1. 按照以下方式导入库:

    {-# LANGUAGE OverloadedStrings #-}
    import Text.LaTeX
    import Text.LaTeX.Base.Class
    import Text.LaTeX.Base.Syntax
    import qualified Data.Map as M
    
  2. 按照以下规格保存一个 LaTeX 文件:

    main :: IO ()
    main = execLaTeXT myDoc >>= renderFile "output.tex"
    
  3. 按照以下方式定义文档,文档被分为前言和正文:

    myDoc :: Monad m => LaTeXT_ m
    
    myDoc = do
      thePreamble
      document theBody
    
  4. 前言部分包含作者数据、标题、格式选项等内容,如下代码所示:

    thePreamble :: Monad m => LaTeXT_ m
    
    thePreamble = do
      documentclass [] article
      author "Dr. Databender"
      title "Data Analyst"
    
  5. 按照以下方式定义我们希望转换为 LaTeX 表格的数据列表:

    myData :: [(Int,Int)]
    
    myData = [ (1, 50)
             , (2, 100)
             , (3, 150)]
    
  6. 按照以下方式定义正文:

    theBody :: Monad m => LaTeXT_ m
    
    theBody = do
    
  7. 设置标题和章节,并按照以下代码片段构建表格:

      maketitle
      section "Fancy Data Table"
      bigskip
      center $ underline $ textbf "Table of Points"
      center $ tabular Nothing [RightColumn, VerticalLine, LeftColumn] $ do
        textbf "Time" & textbf "Cost"
        lnbk
        hline
        mapM_ (\(t, c) -> do texy t & texy c; lnbk) myData 
    
  8. 运行以下命令后,我们可以获取 PDF 并查看:

    $ runhaskell Main.hs
    $ pdflatex output.tex
    
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_12_02.jpg

另见

要构建一个网页,请参考前面的做法,标题为在 HTML 网页中展示结果

使用文本模板个性化消息

有时我们有一个包含大量用户名和相关数据的列表,并且希望单独向每个人发送消息。本做法将创建一个文本模板,该模板将从数据中填充。

准备工作

使用 cabal 安装template库:

$ cabal install template

如何操作…

在一个名为Main.hs的新文件中执行以下步骤:

  1. 按如下方式导入相关库:

    {-# LANGUAGE OverloadedStrings #-}
    
    import qualified Data.ByteString.Lazy as S
    import qualified Data.Text as T
    import qualified Data.Text.IO as TIO
    import qualified Data.Text.Lazy.Encoding as E
    import qualified Data.ByteString as BS
    import Data.Text.Lazy (toStrict)
    import Data.Text.Template
    
  2. 定义我们处理的数据如下:

    myData = [ [ ("name", "Databender"), ("title", "Dr.") ],
               [ ("name", "Paragon"), ("title", "Master") ],
               [ ("name", "Marisa"), ("title", "Madam") ] ]
    
  3. 定义数据模板如下:

    myTemplate = template "Hello $title $name!"
    
  4. 创建一个辅助函数,将数据项转换为模板,如下所示:

    context :: [(T.Text, T.Text)] -> Context
    context assocs x = maybe err id . lookup x $ assocs
      where err = error $ "Could not find key: " ++ T.unpack x
    
  5. 将每个数据项与模板匹配,并将所有内容打印到文本文件中,如下代码片段所示:

    main :: IO ()
    main = do
      let res = map (\d -> toStrict ( 
                      render myTemplate (context d) )) myData
      TIO.writeFile "messages.txt" $ T.unlines res
    
  6. 运行代码以查看生成的文件:

    $ runhaskell Main.hs
    
    $ cat messages.txt
    
    Hello Dr. Databender!
    Hello Master Paragon!
    Hello Madam Marisa!
    
    

将矩阵值导出到文件

在数据分析和机器学习中,矩阵是一种常见的数据结构,经常需要导入和导出到程序中。在这个方案中,我们将使用 Repa I/O 库导出一个示例矩阵。

准备工作

使用 cabal 安装repa-io库,如下所示:

$ cabal install repa-io

如何做……

创建一个新文件,我们命名为Main.hs,并插入接下来步骤中解释的代码:

  1. 按如下方式导入相关库:

    import Data.Array.Repa.IO.Matrix
    import Data.Array.Repa
    
  2. 定义一个 4 x 3 的矩阵,如下所示:

    x :: Array U DIM2 Int 
    x = fromListUnboxed (Z :. (4::Int) :. (3::Int)) 
      [ 1, 2, 9, 10
      , 4, 3, 8, 11
      , 5, 6, 7, 12 ]
    
  3. 将矩阵写入文件,如下所示:

    main = writeMatrixToTextFile "output.dat" x
    

工作原理……

矩阵简单地表示为其元素的列表,按行优先顺序排列。文件的前两行定义了数据类型和维度。

还有更多内容……

要从此文件中读取矩阵,我们可以使用readMatrixFromTextFile函数来检索二维矩阵。更多关于此包的文档可以在hackage.haskell.org/package/repa-io找到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值