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:
- Herunterladen der Datei
ol_dump_authors_latest.txt.gzüber HTTP - Dekomprimieren mit einer
gunzip-ähnlichen Funktion - Aufteilen in Zeilen und Verwerfen alles außer dem JSON aus diesen Zeilen
- Angemessenes Dekodieren des JSON und Extrahieren des
name-Feldes, oder Ignorieren der Zeile wenn Parsen nicht möglich ist - Schreiben aller besagten Namen in eine Datei (
authors.txtin diesem Beispiel), eine pro Zeile.
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.
{-# 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"