crawlchain 0.1.1.7 → 0.1.2.0
raw patch · 6 files changed
+124/−93 lines, 6 filesdep +crawlchaindep +http-streamsdep +textdep −HTTPdep ~base
Dependencies added: crawlchain, http-streams, text
Dependencies removed: HTTP
Dependency ranges changed: base
Files
- crawlchain.cabal +18/−3
- src/Network/CrawlChain/CrawlChain.hs +1/−6
- src/Network/CrawlChain/Crawling.hs +68/−71
- src/Network/CrawlChain/Downloading.hs +4/−9
- src/Text/HTML/CrawlChain/HtmlFiltering.hs +8/−4
- test/src/HtmlFilteringTests.hs +25/−0
crawlchain.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: crawlchain-version: 0.1.1.7+version: 0.1.2.0 synopsis: Simulation user crawl paths description: Library for simulating user crawl paths (trees) with selectors - takes an initial action and a chain of processing actions to crawl a tree (lazy, depth first) searching for a matching branch. stability: experimental@@ -35,13 +35,28 @@ , Network.CrawlChain.Downloading , Network.CrawlChain.Report , Network.CrawlChain.Util- build-depends: base < 4.9+ build-depends: base < 4.10 , bytestring , directory- , HTTP+ , http-streams , network-uri , split , tagsoup+ , text , time+ default-language: Haskell2010+ ghc-options: -Wall+++Test-Suite html-tests+ type: exitcode-stdio-1.0+ main-is: HtmlFilteringTests.hs+ other-modules: Text.HTML.CrawlChain.HtmlFiltering+ , Network.CrawlChain.CrawlAction+ hs-source-dirs: src, test/src+ build-depends: base+ , crawlchain+ , split+ , tagsoup default-language: Haskell2010 ghc-options: -Wall
src/Network/CrawlChain/CrawlChain.hs view
@@ -1,13 +1,10 @@ module Network.CrawlChain.CrawlChain (- crawlChain, crawlChains, -- primary interface+ crawlChain, crawlChains, -- primary interface executeActions, crawlForUrl, -- legacy, deprecated executeCrawlChain -- visible for tests ) where import Data.List (intersperse)-import Data.List.Split (splitOn)-import System.Environment (getArgs, getProgName)-import System.Exit (exitFailure) import Network.CrawlChain.CrawlAction import Network.CrawlChain.CrawlingParameters@@ -15,8 +12,6 @@ import Network.CrawlChain.CrawlingContext (defaultContext, storingContext) import Network.CrawlChain.DirectiveChainResult (extractFirstResult) import Network.CrawlChain.Downloading-import Network.CrawlChain.Storing-import Network.CrawlChain.BasicTemplates executeActions :: CrawlingParameters -> String -> String -> IO () executeActions args dir fName = do
src/Network/CrawlChain/Crawling.hs view
@@ -1,25 +1,35 @@+{-# LANGUAGE OverloadedStrings #-} module Network.CrawlChain.Crawling ( crawl, crawlAndStore, CrawlActionDescriber, Crawler ) where -import Network.HTTP-import Network.Stream (Result)-import Network.URI +import Control.Exception (bracket)+import qualified Data.ByteString.Char8 as BC+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Network.Http.Client as C+import Network.URI (URI (..), parseURI)+ import Network.CrawlChain.CrawlAction import Network.CrawlChain.CrawlResult import Network.CrawlChain.Util-import Network.URI.Util -type RequestType = Request_String type Crawler = CrawlAction -> IO CrawlResult type CrawlActionDescriber = CrawlAction -> String +{-|+ Processes one step of a crawl chain: does the actual loading.+-} crawl :: Crawler-crawl action = delaySeconds 1 >> crawl' action 3 (toRequest action)+crawl action = delaySeconds 1 >> crawlInternal action +{-|+ Used for preparation of integration tests: additionally stores the crawl result+ using the given file name strategy.+-} crawlAndStore :: CrawlActionDescriber -> Crawler crawlAndStore describer = (>>= store) . crawl where@@ -37,71 +47,58 @@ putStrLn $ "writing to " ++ n writeFile n c -toRequest :: CrawlAction -> RequestType-toRequest (GetRequest url) = addStandardHeader $ mkRequest GET (toURI url)-toRequest (PostRequest url params postType) =- plainPost {rqBody = formParams,- rqHeaders = makePostHeaders postType formParams- }- where- plainPost :: RequestType- plainPost = addStandardHeader $ mkRequest POST (toURI url)- formParams = urlEncodeVars params--addStandardHeader :: (HasHeaders h) => h -> h-addStandardHeader = insertHeaders [- Header HdrUserAgent "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0"- ]--makePostHeaders :: PostType -> String -> [Header]-makePostHeaders PostForm formParams =- [- mkHeader HdrContentType "application/x-www-form-urlencoded",- mkHeader HdrContentLength (show $ length formParams)- ]-makePostHeaders PostAJAX formParams = ajaxHeader:(makePostHeaders PostForm formParams) where- ajaxHeader = mkHeader (HdrCustom "X-Requested-With") "XMLHttpRequest"-makePostHeaders _ _ = []--crawl' :: CrawlAction -> Int -> RequestType -> IO CrawlResult-crawl' originalAction maxRedirects request = do+crawlInternal :: CrawlAction -> IO CrawlResult+crawlInternal action = do -- print request- response <- simpleHTTP request--- print response- body <- getResponseBody response--- print body- code <- getResponseCode response- logMsg $ "Crawled " ++ (showRequest request) ++ " with result: " ++ (show code)- checkRedirect maxRedirects request (crawlResult response body code)- where- crawlResult :: (HasHeaders a) => Result a -> String -> ResponseCode -> CrawlResult- crawlResult response body code = CrawlResult originalAction body (parseResonseCode code (locationHeaders response))- where- locationHeaders :: (HasHeaders a) => Result a -> [Header]- locationHeaders = either (\_ -> []) (retrieveHeaders HdrLocation)---- this reinvents the wheel and should be switched to using http-client if problems occur-checkRedirect :: Int -> RequestType -> CrawlResult -> IO CrawlResult-checkRedirect 0 _ result = return result-checkRedirect maxRedirects previousRequest result =- maybe (return result) (crawl' (crawlingAction result) (maxRedirects -1)) (extractRedirectAction $ crawlingResultStatus result)+ response <- doRequest action+-- print response+ logMsg $ "Crawled " ++ (show action)+ return $ CrawlResult action (BC.unpack response) CrawlingOk where- extractRedirectAction :: CrawlingResultStatus -> Maybe (Request String)- -- unclean: converts PostRequest to Get, should do something more sensible- extractRedirectAction (CrawlingRedirect url) = Just $ previousRequest { rqURI = toURI url }- extractRedirectAction _ = Nothing--parseResonseCode :: ResponseCode -> [Header] -> CrawlingResultStatus-parseResonseCode (2, _, _) _ = CrawlingOk-parseResonseCode code@(3, _, _) hdrLoc = maybe (CrawlingFailed (show code)) CrawlingRedirect (extractRedirectUrl hdrLoc)-parseResonseCode code _ = CrawlingFailed (show code)--extractRedirectUrl :: [Header] -> Maybe String-extractRedirectUrl [] = Nothing-extractRedirectUrl ((Header _ value):xs) =- let parsedHeader = (parseURIReference value)- in maybe (extractRedirectUrl xs) (Just . show) parsedHeader--showRequest :: RequestType -> String-showRequest r = (show $ rqURI r) ++ " - " ++ (show $ rqMethod r) ++ ": " ++ (rqBody r)+ doRequest :: CrawlAction -> IO (BC.ByteString)+ doRequest (GetRequest url) = C.get (BC.pack url) C.concatHandler -- TODO check exceptions with concatHandler'+ doRequest (PostRequest urlString ps pType) = doPost (BC.pack urlString) formParams pType where+ formParams = map (\(a, b) -> (BC.pack a, BC.pack b)) ps+ doPost :: BC.ByteString -> [(BC.ByteString, BC.ByteString)] -> PostType -> IO BC.ByteString+ doPost url params postType = doPost' postType where+ doPost' :: PostType -> IO BC.ByteString+ doPost' Undefined = doPost' PostForm+ doPost' PostForm = C.postForm url params C.concatHandler+ doPost' PostAJAX = ajaxRequest url params +ajaxRequest :: BC.ByteString -> [(BC.ByteString, BC.ByteString)] -> IO BC.ByteString+ajaxRequest = postRequest C.concatHandler ajaxRequestChanges where+ ajaxRequestChanges = do+ C.setContentType "application/x-www-form-urlencoded; charset=UTF-8"+ C.setAccept "application/json, text/javascript, */*"+ C.setHeader "X-Requested-With" "XMLHttpRequest"+ -- I am not terribly enthusiastic about the http-streams interface when changing headers+ postRequest handler requestChanges url formParams = do+ bracket+ (C.establishConnection url)+ (C.closeConnection)+ (process)+ where+ u = parseURL url where+ parseURL :: C.URL -> URI+ parseURL r' =+ case parseURI r of+ Just u' -> u'+ Nothing -> error ("Can't parse URI " ++ r)+ where r = T.unpack $ T.decodeUtf8 r'+ q = C.buildRequest1 $ do+ C.http C.POST (path u)+ C.setAccept $ BC.pack "*/*"+ C.setContentType $ BC.pack "application/x-www-form-urlencoded"+ requestChanges where+ path :: URI -> BC.ByteString+ path u' =+ case url' of+ "" -> "/"+ _ -> url'+ where+ url' = T.encodeUtf8 $! T.pack $! concat [uriPath u', uriQuery u', uriFragment u']+ process c = do+ _ <- C.sendRequest c q (C.encodedFormBody formParams)+ x <- C.receiveResponse c handler+ return x
src/Network/CrawlChain/Downloading.hs view
@@ -1,11 +1,7 @@ module Network.CrawlChain.Downloading (downloadTo, storeDownloadAction) where -import Data.ByteString as B-import Network.HTTP---import Network.Stream---import Pipes---import Pipes.HTTP---import qualified Pipes.ByteString as PB -- from `pipes-bytestring`+import qualified Data.ByteString.Char8 as BC+import qualified Network.Http.Client as C import Network.CrawlChain.CrawlAction import Network.URI.Util@@ -14,9 +10,8 @@ downloadTo :: Maybe String -> String -> CrawlAction -> IO () downloadTo dir destination (GetRequest url) = buildAndCreateTargetDir True dir destination >>= \fulldestination -> do Prelude.putStrLn $ "Downloading from "++url++" to "++fulldestination- downloadResult <- simpleHTTP (defaultGETRequest_ (toURI url))- responseBody <- getResponseBody downloadResult- B.writeFile fulldestination responseBody+ downloadResult <- C.get (BC.pack url) C.concatHandler+ BC.writeFile fulldestination downloadResult Prelude.putStrLn $ "Download finished" downloadTo _ _ req = Prelude.putStrLn $ "POST Requests not supported: " ++ show req
src/Text/HTML/CrawlChain/HtmlFiltering.hs view
@@ -1,5 +1,5 @@ module Text.HTML.CrawlChain.HtmlFiltering (- extractLinks, extractLinksMatching, extractLinksWithAttributes, extractLinksFilteringUrlAttrs, extractLinksFilteringAll,+ extractLinks, extractLinksMatching, extractLinksWithAttributes, extractLinksFilteringUrlAttrs, extractLinksFilteringAll, unevaluated, findAllUrlsEndingWith, findFirstLinkAfter, extractFirstForm, Method(..),@@ -66,7 +66,8 @@ parseTags where filterAndGroupLinks :: [TagS] -> [[TagS]]- filterAndGroupLinks = map cleanupLinkGroup . splitWhen' (isTagOpenName "a")+ filterAndGroupLinks =+ map cleanupLinkGroup . splitWhen' (\t -> isTagOpenName "a" t || isTagOpenName "iframe" t) where splitWhen' :: (a -> Bool) -> [a] -> [[a]] splitWhen' f = splitWhen'' []@@ -74,7 +75,10 @@ splitWhen'' col [] = [col] splitWhen'' col (x:rest) = if f x then col:(splitWhen'' [x] rest) else splitWhen'' (col++[x]) rest cleanupLinkGroup :: [TagS] -> [TagS]- cleanupLinkGroup = takeWhile (not . (isTagCloseName "a")) . dropWhile (not . (isTagOpenName "a"))+ cleanupLinkGroup = takeWhile notEndSrcTag . dropWhile notStartSrcTag where+ notStartSrcTag t = not $ any (flip isTagOpenName t) supportedTags -- (not . (isTagOpenName "a"))+ notEndSrcTag t = not $ any (flip isTagCloseName t) supportedTags+ supportedTags = ["a", "iframe"] findFirstLinkAfter :: String -> [(String, String)] -> String -> [CrawlAction] findFirstLinkAfter tagName tagAttrs =@@ -92,7 +96,7 @@ isLink _ = False getSrc :: Tag String -> String-getSrc (TagOpen _ as) = fromMaybe "" (lookup "href" as)+getSrc (TagOpen _ attributes) = fromMaybe (fromMaybe "" (lookup "src" attributes)) (lookup "href" attributes) getSrc _ = [] getTagAttrs :: Tag String -> [(String, String)]
+ test/src/HtmlFilteringTests.hs view
@@ -0,0 +1,25 @@+module Main (main) where++import System.Exit (exitFailure)++import Network.CrawlChain.CrawlAction+import Text.HTML.CrawlChain.HtmlFiltering++main :: IO ()+main = do+ match extractLinks "<a href=\"link\">text</a>" ["link"]+ match extractLinks "<a href=\"link\" />" ["link"]+ match extractLinks "<a href=\"link\">sdf</a><a href=\"link2\">sdf</a>" ["link", "link2"]+ match (extractLinksFilteringAll unevaluated unevaluated unevaluated) "<a href=\"link\">text<span>bla</span></a>" ["link"]+ match (extractLinksFilteringAll unevaluated unevaluated (any (=="bla"))) "<a href=\"link\">text<span>bla</span></a>" ["link"]+ match (extractLinksFilteringAll unevaluated unevaluated (not . any (=="bla"))) "<a href=\"link\">text<span>bla</span></a>" []+ match extractLinks "<iframe src=\"link\"></iframe>" ["link"]+ match extractLinks "<iframe src=\"link\"/>" ["link"]+ where+ match method input expected = + if actual /= map GetRequest expected+ then putStrLn ("could not find " ++ (show expected) ++ " in; " ++ input) >> exitFailure+ else pure ()+ where+ actual = method input+