Open-Library-Autor-Namen mit Haskell Conduits extrahieren

Fast jeder, der Haskell produktiv nutzt, ist bereits über Michael Snoymans Conduit-Bibliothek gestolpert.

In diesem Beitrag zeige ich, wie man die Macht von Conduit-interleaved-IO nutzen kann, um Autorennamen aus den Open Library-Daten-Dumps zu parsen.

Mit traditionellen Sprachen wie Java oder C wäre es deutlich schwieriger und fehleranfälliger, die Verarbeitungsschritte der Pipeline zu interleaven:

Da der unkomprimierte Dump eine Multi-GiB-Datei ist, wird ein Streaming-Parser benötigt. Die Verwendung von Pipes oder FIFOs zum Herunterladen und Parsen von stdin wäre in diesem einfachen Beispiel sicherlich möglich, jedoch

Obwohl es Bibliotheken für andere Sprachen gibt, die natives interleaved-IO erleichtern, erweist es sich als recht schwierig, Drittanbieter-Funktionalität wie gunzip zu integrieren, insbesondere wenn man versucht, Performance-Engpässe zu vermeiden. Bei Fokus auf Performance bleibt man oft bei der manuellen Manipulation von Speicherpuffern hängen, was zu komplexem, fehleranfälligem Code führt.

Conduits bieten eine gebrauchsfertige Lösung für diese Probleme. Mit Bibliotheken wie conduit-extras ist es auch leicht möglich, Streaming-Gunzip zu integrieren und dabei nahezu konstante Speicheranforderungen und deterministische Ressourcennutzung zu erfüllen.

Bei gleichbleibend hoher Performance ermöglichen sie dem Programmierer, nicht über die Details des Zusammenfügens der einzelnen Schritte der Verarbeitungspipeline nachdenken zu müssen.

parse_open_library_authors.hs
{-# LANGUAGE OverloadedStrings #-}
{-|

Ein Programm zum Stream-Parsen des Open-Library-Autoren-Dumps und Extrahieren einer Liste von Autorennamen.

(C) 2014 Uli Koehler

Veröffentlicht unter der Apache License v2.0

-}

import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Conduit.Zlib (ungzip)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Network.HTTP.Conduit (parseUrl, withManager, http, responseBody)
import Data.Conduit
import qualified Data.Conduit.Combinators as CC
import qualified Data.Conduit.List as CL
import qualified Data.Conduit.Binary as CB
import Control.Applicative
import Data.Char
import Data.Aeson
import Data.Maybe
import Control.Monad

data Author = Author {authorKey :: Text, -- ^ Open library author key, e.g. /authors/OL5900296A
                      authorName :: Text -- ^ Name of the author person
                      } deriving (Show, Eq)

instance FromJSON Author where
    parseJSON (Object v) = Author <$>
                            fmap T.pack (v .: "key") <*>
                            fmap T.pack (v .: "name")
    parseJSON _ = mzero

main :: IO ()
main = do
    -- Definiere unsere Conduit-Verarbeitungskette:
    --  - Zeilen überspringen, die nicht geparst werden können
    --  - Auch alles bis zum JSON-Beginn in jeder Zeile überspringen
    let parseConduit = CL.mapMaybe (decode . LB.fromStrict .  B.dropWhile ('{' /=))
    let showAuthor = CL.map (TE.encodeUtf8 . authorName)
    let processingConduit = ungzip =$= CB.lines =$= parseConduit =$= showAuthor =$= CC.unlinesAscii
    -- HTTP-Anfrage initialisieren
    request <- parseUrl "http://openlibrary.org/data/ol_dump_authors_latest.txt.gz"
    withManager $ \manager -> do
        response <- http request manager
        -- Interleaved-IO verwenden, um inkrementell abzurufen und zu verarbeiten
        responseBody response $$+- processingConduit =$ CB.sinkFile "authors.txt"

Check out similar posts by category: Haskell