packages feed

tagsoup 0.4 → 0.6

raw patch · 7 files changed

+490/−40 lines, 7 filesdep +containersdep ~base

Dependencies added: containers

Dependency ranges changed: base

Files

Example/Example.hs view
@@ -56,9 +56,9 @@ googleTechNews :: IO () googleTechNews = do         tags <- liftM parseTags $ openURL "http://news.google.com/?ned=us&topic=t"-        let links = [ x-                    | TagOpen "a" atts:_:TagText x:_ <- tails tags-                    , ("id",'r':val) <- atts, 'i' `notElem` val]+        let links = [ text+                    | TagOpen "a" atts:TagOpen "b" []:TagText text:_ <- tails tags,+                    ("id",'u':'-':_) <- atts]         putStr $ unlines links  @@ -129,7 +129,7 @@ validate :: String -> IO () validate x = putStr . unlines . g . f . parseTagsOptions opts =<< openItem x     where-        opts = options{optTagPosition=True, optTagWarning=True}+        opts = parseOptions{optTagPosition=True, optTagWarning=True}          f :: [Tag] -> [String]         f (TagPosition row col:TagWarning warn:rest) =
Text/HTML/TagSoup.hs view
@@ -19,8 +19,7 @@ module Text.HTML.TagSoup(     -- * Data structures and parsing     Tag(..), Attribute,-    Options(..), options,-    parseTags, parseTagsOptions,+    module Text.HTML.TagSoup.Parser,     canonicalizeTags,      -- * Tag identification
Text/HTML/TagSoup/Parser.hs view
@@ -30,7 +30,7 @@  module Text.HTML.TagSoup.Parser(     parseTags, parseTagsOptions,-    Options(..), options+    ParseOptions(..), parseOptions     ) where  import Text.HTML.TagSoup.Type@@ -47,9 +47,9 @@ (?->) b true = if b then true else []  ------------------------------------------------------------------------ * Options+-- * ParseOptions -data Options = Options+data ParseOptions = ParseOptions     {optTagPosition :: Bool -- ^ Should 'TagPosition' values be given before every item     ,optTagWarning :: Bool -- ^ Should 'TagWarning' values be given     ,optLookupEntity :: String -> [Tag] -- ^ How to lookup an entity@@ -58,9 +58,9 @@     }  --- Default 'Options' structure-options :: Options-options = Options False False f (Just 10)+-- Default 'ParseOptions' structure+parseOptions :: ParseOptions+parseOptions = ParseOptions False False f (Just 10)     where         f x = case lookupEntity x of                   Nothing -> [TagText $ "&" ++ x ++ ";", TagWarning $ "Unknown entity: &" ++ x ++ ";"]@@ -68,9 +68,9 @@   parseTags :: String -> [Tag]-parseTags = parseTagsOptions options+parseTags = parseTagsOptions parseOptions -tagWarn :: Options -> String -> [Tag]+tagWarn :: ParseOptions -> String -> [Tag] tagWarn opts x = [TagWarning x | optTagWarning opts]  ---------------------------------------------------------------------@@ -92,13 +92,13 @@     _    -> Position r (c+1)  -tagPos :: Options -> Position -> [Tag]+tagPos :: ParseOptions -> Position -> [Tag] tagPos opts (Position r c) = [TagPosition r c | optTagPosition opts] -tagPosWarn :: Options -> Position -> String -> [Tag]+tagPosWarn :: ParseOptions -> Position -> String -> [Tag] tagPosWarn opts p x = optTagWarning opts ?-> (tagPos opts p ++ [TagWarning x]) -tagPosWarnFix :: Options -> Position -> [Tag] -> [Tag]+tagPosWarnFix :: ParseOptions -> Position -> [Tag] -> [Tag] tagPosWarnFix opts p = addPositions . remWarnings     where         remWarnings = if optTagWarning opts then id else filter (not . isTagWarning)@@ -108,7 +108,7 @@ --------------------------------------------------------------------- -- * Driver -parseTagsOptions :: Options -> String -> [Tag]+parseTagsOptions :: ParseOptions -> String -> [Tag] parseTagsOptions opts x = mergeTexts $ evalState (parse opts) $ Value x (Position 0 0)  @@ -211,7 +211,7 @@ --------------------------------------------------------------------- -- * Parser -parse :: Options -> Parser [Tag]+parse :: ParseOptions -> Parser [Tag] parse opts = do     Value s p <- get     case s of@@ -278,7 +278,7 @@  -- read a list of attributes -- return (the attributes read, if the tag is self-shutting, any warnings) -attribs :: Options -> Position -> Parser ([Attribute],Bool,[Tag])+attribs :: ParseOptions -> Position -> Parser ([Attribute],Bool,[Tag]) attribs opts p1 = do     dropSpaces     Value s p <- get@@ -295,7 +295,7 @@  -- read a single attribute -- return (the attributes read, if the tag is self-shutting, any warnings) -attrib :: Options -> Position -> Parser ([Attribute],Bool,[Tag])+attrib :: ParseOptions -> Position -> Parser ([Attribute],Bool,[Tag]) attrib opts p1 = do     name <- breakName     if null name then do@@ -327,7 +327,7 @@  -- read a single value -- return (value,warnings)-value :: Options -> Parser (String,[Tag])+value :: ParseOptions -> Parser (String,[Tag]) value opts = do     Value s p <- get     case s of@@ -356,7 +356,7 @@  -- have seen an &, and have consumed it -- return a [Tag] to go in a tag stream-entity :: Options -> Position -> Parser [Tag]+entity :: ParseOptions -> Position -> Parser [Tag] entity opts p1 = do     Value s p <- get     case s of@@ -384,7 +384,7 @@   -- return the tag and some position information-entityString :: Options -> Position -> Parser (String,[Tag])+entityString :: ParseOptions -> Position -> Parser (String,[Tag]) entityString opts p = do     tags <- entity opts p     let warnings = tagPosWarnFix opts p $ filter isTagWarning tags
+ Text/HTML/TagSoup/Render.hs view
@@ -0,0 +1,56 @@+{-|+    This module is preliminary and may change at a future date.+    If you wish to use its features, please email me and I will+    help evolve an API that suits you.+-}++module Text.HTML.TagSoup.Render+    {-# DEPRECATED "Not quite ready for use yet, email me if it looks useful to you" #-}+    (+    renderTags, renderTagsOptions,+    RenderOptions(..), renderOptions+    ) where++import Data.Char+import qualified Data.IntMap as IntMap+import Text.HTML.TagSoup.Entity+import Text.HTML.TagSoup.Type+++data RenderOptions = RenderOptions+    {optEscape :: Char -> String+    }++renderOptions :: RenderOptions+renderOptions = RenderOptions (\x -> IntMap.findWithDefault [x] (ord x) esc)+    where esc = IntMap.fromList [(b, "&"++a++";") | (a,b) <- htmlEntities]+++-- | Show a list of tags, as they might have been parsed+renderTags :: [Tag] -> String+renderTags = renderTagsOptions renderOptions+++renderTagsOptions :: RenderOptions -> [Tag] -> String+renderTagsOptions opts = tags+    where+        tags (TagOpen name atts:TagClose name2:xs)+            | name == name2 = open name atts " /" ++ tags xs+        tags (x:xs) = tag x ++ tags xs+        tags [] = []++        tag (TagOpen name atts) = open name atts ""+        tag (TagClose name) = "</" ++ name ++ ">"+        tag (TagText text) = txt text+        tag (TagComment text) = "<!--" ++ com text ++ "-->"+        tag _ = ""++        txt = concatMap (optEscape opts)+        open name atts shut = "<" ++ name ++ concatMap att atts ++ shut ++ ">"+        att (x,"") = " " ++ x+        att ("",y) = " " ++ "\"" ++ txt y ++ "\""+        att (x,y) = " " ++ x ++ "=\"" ++ txt y ++ "\""++        com ('-':'-':'>':xs) = "-- >" ++ com xs+        com (x:xs) = x : com xs+        com [] = []
+ Text/HTML/TagSoup/Tree.hs view
@@ -0,0 +1,68 @@+{-|+    This module is preliminary and may change at a future date.+    If you wish to use its features, please email me and I will+    help evolve an API that suits you.+-}++module Text.HTML.TagSoup.Tree+    {-# DEPRECATED "Not quite ready for use yet, email me if it looks useful to you" #-}+    (+    TagTree(..), tagTree,+    flattenTree, transformTree, universeTree+    ) where++import Text.HTML.TagSoup.Type+++data TagTree = TagBranch String [Attribute] [TagTree]+             | TagLeaf Tag+             deriving Show++++-- | Convert a list of tags into a tree. This version is not lazy at+--   all, that is saved for version 2.+tagTree :: [Tag] -> [TagTree]+tagTree = g+    where+        g :: [Tag] -> [TagTree]+        g [] = []+        g xs = a ++ map TagLeaf (take 1 b) ++ g (drop 1 b)+            where (a,b) = f xs++        -- the second tuple is either null or starts with a close+        f :: [Tag] -> ([TagTree],[Tag])+        f (TagOpen name atts:rest) =+            case f rest of+                (inner,[]) -> (TagLeaf (TagOpen name atts):inner, [])+                (inner,TagClose x:xs)+                    | x == name -> let (a,b) = f xs in (TagBranch name atts inner:a, b)+                    | otherwise -> (TagLeaf (TagOpen name atts):inner, TagClose x:xs)+                _ -> error "TagSoup.Tree.tagTree: safe as - forall x . isTagClose (snd (f x))"++        f (TagClose x:xs) = ([], TagClose x:xs)+        f (x:xs) = (TagLeaf x:a,b)+            where (a,b) = f xs+        f [] = ([], [])+++flattenTree :: [TagTree] -> [Tag]+flattenTree xs = concatMap f xs+    where+        f (TagBranch name atts inner) =+            TagOpen name atts : flattenTree inner ++ [TagClose name]+        f (TagLeaf x) = [x]+++universeTree :: [TagTree] -> [TagTree]+universeTree = concatMap f+    where+        f t@(TagBranch _ _ inner) = t : universeTree inner+        f x = [x]+++transformTree :: (TagTree -> [TagTree]) -> [TagTree] -> [TagTree]+transformTree act = concatMap f+    where+        f (TagBranch a b inner) = act $ TagBranch a b (transformTree act inner)+        f x = act x
tagsoup.cabal view
@@ -1,5 +1,6 @@+Cabal-Version:  >= 1.2 Name:           tagsoup-Version:        0.4+Version:        0.6 Copyright:      2006-8, Neil Mitchell Maintainer:     ndmitchell@gmail.com Author:         Neil Mitchell@@ -7,7 +8,6 @@ License:        BSD3 Category:       XML License-File:   LICENSE-Build-Depends:  base, network, mtl Build-type:     Simple Synopsis:       Parsing and extracting information from (possibly malformed) HTML documents Description:@@ -16,19 +16,32 @@     properly within any particular framework. This library is for situations where     the author of the HTML is not cooperating with the person trying to extract the     information, but is also not trying to hide the information.+Extra-Source-Files:+    tagsoup.htm -GHC-Options:    -Wall-Exposed-modules:-    Text.HTML.TagSoup-    Text.HTML.TagSoup.Entity-    Text.HTML.TagSoup.Match-    Text.HTML.TagSoup.Parser-    Text.HTML.TagSoup.Type-    Text.HTML.Download+Flag splitBase+    Description: Choose the new smaller, split-up base package. -Executable:     tagsoup-GHC-Options:    -Wall-Main-Is:        Main.hs-Other-Modules:-    Example.Example-    Example.Regress+Library+    if flag(splitBase)+        build-depends: base >= 3, network, mtl, containers+    else+        build-depends: base <  3, network, mtl++    GHC-Options: -Wall+    Exposed-modules:+        Text.HTML.TagSoup+        Text.HTML.TagSoup.Entity+        Text.HTML.TagSoup.Match+        Text.HTML.TagSoup.Parser+        Text.HTML.TagSoup.Render+        Text.HTML.TagSoup.Tree+        Text.HTML.TagSoup.Type+        Text.HTML.Download++Executable tagsoup+    Main-Is: Main.hs+    GHC-Options: -Wall+    Other-Modules:+        Example.Example+        Example.Regress
+ tagsoup.htm view
@@ -0,0 +1,314 @@+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">+<html>+    <head>+        <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />+        <title>Drinking TagSoup by Example</title>+        <style type="text/css">+pre {+    border: 2px solid gray;+    padding: 1px;+    padding-left: 5px;+    margin-left: 10px;+    background-color: #eee;+}++pre.define {+    background-color: #ffb;+    border-color: #cc0;+}++body {+    font-family: sans-serif;+}++h1, h2, h3 {+    font-family: serif;+}++h1 {+    color: rgb(23,54,93);+    border-bottom: 1px solid rgb(79,129,189);+    padding-bottom: 2px;+    font-variant: small-caps;+    text-align: center;+}++a {+    color: rgb(54,95,145);+}++h2 {+    color: rgb(54,95,145);+}++h3 {+    color: rgb(79,129,189);+}++p.rule {+    background-color: #ffb;+	padding: 3px;+	margin-left: 50px;+	margin-right: 50px;+}+        </style>+    </head>+    <body>++<h1>Drinking TagSoup by Example</h1>++<p style="text-align:right;margin-bottom:25px;">+    by <a href="http://www.cs.york.ac.uk/~ndm/">Neil Mitchell</a>+</p>++<p>+	TagSoup is a library for extracting information out of unstructured HTML code, sometimes known as tag-soup. The HTML does not have to be well formed, or render properly within any particular framework. This library is for situations where the author of the HTML is not cooperating with the person trying to extract the information, but is also not trying to hide the information.+</p>+<p>+	This document gives two particular examples, and two more may be found in the <a href="http://www.cs.york.ac.uk/fp/darcs/tagsoup/Example/Example.hs">Example</a> file from the darcs repository. The examples we give are:+</p>+<ol>+    <li>Obtaining the Hit Count from Haskell.org</li>+    <li>Obtaining a list of Simon Peyton-Jones' latest papers</li>+    <li>A brief overview of some other examples</li>+</ol>+<p>+	The intial version of this library was written in Javascript and has been used for various commercial projects involving screen scraping. In the examples general hints on screen scraping are included, learnt from bitter experience. It should be noted that if you depend on data which someone else may change at any given time, you may be in for a shock!+</p>+<p>+	This library was written without knowledge of the Java version of <a href="http://home.ccil.org/~cowan/XML/tagsoup/">TagSoup</a>. They have made a very different design decision: to ensure default attributes are present and to properly nest parsed tags. We do not do this - tags are merely a list devoid of nesting information.+</p>++<h3>Acknowledgements</h3>++<p>+    Thanks to Mike Dodds for persuading me to write this up as a library. Thanks to many people for debugging and code contributions, including: Gleb Alexeev, Ketil Malde, Conrad Parker, Henning Thielemann.+</p>++<h3>Version History</h3>++<h4>0.4-0.6</h4>++<ul>+    <li>Addition of <tt>Text.HTML.TagSoup.Tree</tt> and <tt>Text.HTML.TagSoup.Render</tt>. Both these modules need review, so if they are useful to you please do send suggestions and comments.</li>+    <li>In <tt>Text.HTML.TagSoup.Parser</tt>, the <tt>Options</tt> type has been renamed to <tt>ParseOptions</tt>.</li>+    <li>In <tt>Text.HTML.TagSoup.Parser</tt>, the <tt>options</tt> function has been renamed to <tt>parseOptions</tt>.</li>+</ul>++<h2>Potential Bugs</h2>++<p>+    There are two things that may go wrong with these examples:+</p>+<ul>+    <li>+        <i>The Websites being scraped may change.</i> There is nothing I can do about this, but if you suspect this is the case let me know, and I'll update the examples and tutorials. I have already done so once or twice, its only a few minutes work.+    </li>+    <li>+        <i>The <tt>openURL</tt> method may not work.</i> This happens quite regularly, and depending on your server, proxies and direction of the wind, they may not work. The solution is to use <tt>wget</tt> to download the page locally, then use <tt>readFile</tt> instead. Hopefully a decent Haskell HTTP library will emerge, and that can be used instead.+    </li>+</ul>+++<h2>Haskell Hit Count</h2>++<p>+    Our goal is to develop a program that displays the Haskell.org hit count. This example covers all the basics in designing a basic web-scraping application.+</p>++<h3>Finding the Page</h3>++<p>+	We first need to find where the information is displayed, and in what format. Taking a look at the <a href="http://www.haskell.org/haskellwiki/Haskell">front web page</a>, when not logged in, you may notice that there is no hit count. However, looking at the source shows us:+</p>+<pre>+&lt;div class="printfooter"&gt;+&lt;p&gt;Retrieved from "&lt;a href="http://www.haskell.org/haskellwiki/Haskell"&gt;+http://www.haskell.org/haskellwiki/Haskell&lt;/a&gt;"&lt;/p&gt;++&lt;p&gt;This page has been accessed 615,165 times.+This page was last modified 15:44, 15 March 2007.+Recent content is available under &lt;a href="/haskellwiki/HaskellWiki:Copyrights"+title="HaskellWiki:Copyrights"&gt;a simple permissive license&lt;/a&gt;.&lt;/p&gt;+</pre>+<p>+	So we see that the hit count is available, but not shown. This leads us to rule 1:+</p>+<p class="rule">+	<b>Rule 1:</b><br/>+	Scrape from what the page returns, not what a browser renders, or what view-source gives.+</p>+<p>+	Some web servers will serve different content depending on the user agent, some browsers will have scripting modify their displayed HTML, some pages will display differently depending on your cookies. Before you can start to figure out how to start scraping, first decide what the input to your program will be. There are three ways to get the page as it will appear to your program.+</p>++<h4>Using <tt>Text.Html.Download</tt></h4>+<p>+    Tagsoup provides a module <tt>Text.Html.Download</tt>, which contains <tt>openURL</tt>.+</p>+<pre>+import Text.HTML.Download++main = do src <- openURL "http://haskell.org/haskellwiki/Haskell"+          writeFile "temp.htm" src+</pre>+<p>+    Now open <tt>temp.htm</tt>, find the fragment of HTML containing the hit count, and examine it.+</p>++<h4>Using the <tt>tagsoup</tt> Program</h4>+<p>+    Tagsoup installs both as a library and a program. The program contains all the examples mentioned on this page, along with a few other useful functions. In order to download a URL to a file:+</p>+<pre>+$ tagsoup grab http://haskell.org/haskellwiki/Haskell > temp.htm+</pre>++<h4>Using <tt>Network.HTTP</tt></h4>+<p>+    An alternative fragment of text for reading in the original web page, using the network library, can be written as:+</p>+<pre>+import qualified Data.ByteString.Lazy.Char8 as BS+import Network.HTTP (rspBody)+import Network.HTTP.UserAgent as UA++main :: IO ()+main = do rsp <- UA.get "http://haskell.org/haskellwiki/Haskell"+          let src = BS.unpack $ rspBody rsp+          writeFile "temp.htm" src+</pre>++<h3>Finding the Information</h3>++<p>+	Now we examine both the fragment that contains our snippet of information, and the wider page. What does the fragment has that nothing else has? What algorithm would we use to obtain that particular element? How can we still return the element as the content changes? What if the design changes? But wait, before going any further:+</p>+<p class="rule">+	<b>Rule 2:</b><br/>+	Do not be robust to design changes, do not even consider the possibility when writing the code.+</p>+<p>+	If the user changes their website, they will do so in unpredictable ways. They may move the page, they may put the information somewhere else, they may remove the information entirely. If you want something robust talk to the site owner, or buy the data from someone. If you try and think about design changes, you will complicate your design, and it still won't work. It is better to write an extraction method quickly, and happily rewrite it when things change.+</p>+<p>+	So now, lets consider the fragment from above. It is useful to find a tag which is unique just above your snippet - something with a nice "id" property, or a "class" - something which is unlikely to occur multiple times. In the above example, "printfooter" as the class seems perfect. We decide that to find the snippet, we will start at a "div" tag, with a "class" attribute with the value "printfooter".+</p>+<pre>+haskellHitCount = do+    tags <- liftM parseTags $ openURL "http://haskell.org/haskellwiki/Haskell"+    let count = fromFooter $ head $ sections (~== "&lt;div class=printfooter&gt;") tags+    putStrLn $ "haskell.org has been hit " ++ show count ++ " times"+</pre>+<p>+    Now we start writing the code! The first thing to do is open the required URL, then we parse the code into a list of <tt>Tag</tt>s. We then apply the <tt>sections</tt> function, which returns all the lists whose first element matches the query. We use the <tt>(~==)</tt> operator to construct the query - in this case asking for the "div" we mentioned earlier. This <tt>(~==)</tt> operator is very different from standard equality, it allows additional attributes to be present but does not match them. We write <tt>"&lt;div class=printfooter&gt;"</tt> as syntactic sugar for <tt>TagOpen "div" [("class","printfooter")]</tt>. If we just wanted any open tag with the given class we could have written <tt>(~== TagOpen "" [("class","printfooter")])</tt> and this would have matched. Any empty strings in the second element of the match are considered as wildcards.+</p>+<p>+	Once we have a list of all matching prefixes, we take the <tt>head</tt> - assuming that only one will match. Then we apply <tt>fromFooter</tt> which needs to perform the traversal from the "printfooter" attribute onwards to the actual hit count data.+</p>++<h3>Extracting the Information</h3>++<p>+	Now we have a stream starting at the right place, we generally mangle the code using standard list operators:+</p>+<pre>+fromFooter x = read (filter isDigit num) :: Int+	where+		num = ss !! (i - 1)+		Just i = findIndex (== "times.") ss+		ss = words s+		TagText s = sections (~== "&lt;p&gt;") x !! 1 !! 1+</pre>+<p>+	This code finds <tt>s</tt>, the text inside the appropriate paragraph by knowing that its the second (<tt>!! 1</tt>) paragraph, and within that paragraph, its the second tag - the actual text. We then split up the text using <tt>words</tt>, find the message that comes after hit count, and read all the digits we can find - filtering out the comma. I'm pretty sure this could be done better using regular expressions, and I invite a reader to submit improved code. This code may seem slightly messy, and indeed it is - often that is the nature of extracting information from a tag soup.+</p>+<p class="rule">+	<b>Rule 3:</b><br/>+	TagSoup is for extracting information where structure has been lost, use more structured information if it is available.+</p>+++<h2>Simon's Papers</h2>++<p>+	Our next very important task is to extract a list of all Simon Peyton Jones' recent research papers off his <a href="http://research.microsoft.com/~simonpj/">home page</a>. The largest change to the previous example is that now we desire a list of papers, rather than just a single result.+</p>+<p>+	As before we first start by writing a simple program that downloads the appropriate page, and look for common patterns. This time we want to look for all patterns which occur every time a paper is mentioned, but no where else. The other difference from last time is that previous we grabbed an automatically generated piece of information - this time the information is entered in a more freeform way by a human.+</p>+<p>+	First we spot that the page helpfully has named anchors, there is a current work anchor, and after that is one for Haskell. We can extract all the information between them with a simple <tt>take</tt>/<tt>drop</tt> pair:+</p>+<pre>+takeWhile (~/= "&lt;a name=haskell&gt;") $+drop 5 $ dropWhile (~/= "&lt;a name=current&gt;") tags+</pre>+<p>+	This code drops until you get to the "current" section, then takes until you get to the "haskell" section, ensuring we only look at the important bit of the page. Next we want to find all hyperlinks within this section:+</p>+<pre>+map f $ sections (~== "&lt;a&gt;") $ ...+</pre>+<p>+	Remember that the function to select all tags with name "a" could have been written as <tt>(~== TagOpen "a" [])</tt>, or alternatively <tt>isTagOpenName "a"</tt>. Afterwards we map each item with an <tt>f</tt> function. This function needs to take the tags starting just after the link, and find the text inside the link.+</p>+<pre>+f = dequote . unwords . words . fromTagText . head . filter isTagText+</pre>+<p>+	Here the complexity of interfacing to human written markup comes through. Some of the links are in italic, some are not - the <tt>filter</tt> drops all those that are not, until we find a pure text node. The <tt>unwords . words</tt> deletes all multiple spaces, replaces tabs and newlines with spaces and trims the front and back - a neat trick when dealing with text which has spacing at the source code but not when displayed. The final thing to take account of is that some papers are given with quotes around the name, some are not - dequote will remove the quotes if they exist.+</p>+<p>+	For completeness, we now present the entire example:+</p>+<pre>+spjPapers :: IO ()+spjPapers = do+        tags <- liftM parseTags $ openURL "http://research.microsoft.com/~simonpj/"+        let links = map f $ sections (~== "&lt;a&gt;") $+                    takeWhile (~/= "&lt;a name=haskell&gt;") $+                    drop 5 $ dropWhile (~/= "&lt;a name=current&gt;") tags+        putStr $ unlines links+    where+        f :: [Tag] -> String+        f = dequote . unwords . words . fromTagText . head . filter isTagText++        dequote ('\"':xs) | last xs == '\"' = init xs+        dequote x = x+</pre>+++<h2>Other Examples</h2>++<p>+	Several more examples are given in the Example file, including obtaining the (short) list of papers from my site, getting the current time and a basic XML validator. All can be invoked using the <tt>tagsoup</tt> executable program. All use very much the same style as presented here - writing screen scrapers follow a standard pattern. We present the code from two for enjoyment only.+</p>++<h3>My Papers</h3>++<pre>+ndmPapers :: IO ()+ndmPapers = do+        tags <- liftM parseTags $ openURL "http://www-users.cs.york.ac.uk/~ndm/downloads/"+        let papers = map f $ sections (~== "&lt;li class=paper&gt;") tags+        putStr $ unlines papers+    where+        f :: [Tag] -> String+        f xs = fromTagText (xs !! 2)+</pre>++<h3>UK Time</h3>++<pre>+currentTime :: IO ()+currentTime = do+        tags <- liftM parseTags $ openURL "http://www.timeanddate.com/worldclock/city.html?n=136"+        let time = fromTagText (dropWhile (~/= "&lt;strong id=ct&gt;") tags !! 1)+        putStrLn time+</pre>+++    </body>+</html>