chez-grater 0.0.1 → 0.0.2
raw patch · 6 files changed
+54/−21 lines, 6 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Chez.Grater.Scraper.Site: acceptAll :: Scraper Text Bool
- Chez.Grater.Scraper.Site: allIngredientScrapers :: [IngredientScraper]
- Chez.Grater.Scraper.Site: allStepScrapers :: [StepScraper]
- Chez.Grater.Scraper.Site: allrecipesI :: IngredientScraper
- Chez.Grater.Scraper.Site: allrecipesS :: StepScraper
- Chez.Grater.Scraper.Site: cb101I :: IngredientScraper
- Chez.Grater.Scraper.Site: cb101S :: StepScraper
- Chez.Grater.Scraper.Site: delishI :: IngredientScraper
- Chez.Grater.Scraper.Site: delishS :: StepScraper
- Chez.Grater.Scraper.Site: denyAll :: Scraper Text Bool
- Chez.Grater.Scraper.Site: eatingWellI :: IngredientScraper
- Chez.Grater.Scraper.Site: eatingWellS :: StepScraper
- Chez.Grater.Scraper.Site: eatwell101I :: IngredientScraper
- Chez.Grater.Scraper.Site: food52I :: IngredientScraper
- Chez.Grater.Scraper.Site: food52S :: StepScraper
- Chez.Grater.Scraper.Site: foodNetworkI :: IngredientScraper
- Chez.Grater.Scraper.Site: foodNetworkS :: StepScraper
- Chez.Grater.Scraper.Site: geniusKitchen1I :: IngredientScraper
- Chez.Grater.Scraper.Site: geniusKitchen1S :: StepScraper
- Chez.Grater.Scraper.Site: geniusKitchen2I :: IngredientScraper
- Chez.Grater.Scraper.Site: geniusKitchen2S :: StepScraper
- Chez.Grater.Scraper.Site: ingredientLi1 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi10 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi11 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi12 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi13 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi14 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi15 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi2 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi3 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi4 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi5 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi6 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi7 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi8 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientLi9 :: IngredientScraper
- Chez.Grater.Scraper.Site: ingredientScrapers :: HashMap SiteName IngredientScraper
- Chez.Grater.Scraper.Site: jetpackI :: IngredientScraper
- Chez.Grater.Scraper.Site: jetpackS :: StepScraper
- Chez.Grater.Scraper.Site: mvI :: IngredientScraper
- Chez.Grater.Scraper.Site: mvS :: StepScraper
- Chez.Grater.Scraper.Site: nytimesI :: IngredientScraper
- Chez.Grater.Scraper.Site: nytimesS :: StepScraper
- Chez.Grater.Scraper.Site: setIngredientVersion :: Int -> IngredientScraper -> IngredientScraper
- Chez.Grater.Scraper.Site: setStepVersion :: Int -> StepScraper -> StepScraper
- Chez.Grater.Scraper.Site: simpleIngredientScraper :: Text -> Scraper Text Bool -> Selector -> IngredientScraper
- Chez.Grater.Scraper.Site: simpleStepScraper :: Text -> Scraper Text Bool -> Selector -> StepScraper
- Chez.Grater.Scraper.Site: simplyRecipesI :: IngredientScraper
- Chez.Grater.Scraper.Site: simplyRecipesS :: StepScraper
- Chez.Grater.Scraper.Site: spoonacularI :: IngredientScraper
- Chez.Grater.Scraper.Site: stepLi1 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi10 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi11 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi12 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi13 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi14 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi2 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi3 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi4 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi5 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi6 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi7 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi8 :: StepScraper
- Chez.Grater.Scraper.Site: stepLi9 :: StepScraper
- Chez.Grater.Scraper.Site: stepScrapers :: HashMap SiteName StepScraper
- Chez.Grater.Scraper.Site: tastyI1 :: IngredientScraper
- Chez.Grater.Scraper.Site: tastyI2 :: IngredientScraper
- Chez.Grater.Scraper.Site: tastyI3 :: IngredientScraper
- Chez.Grater.Scraper.Site: tastyS1 :: StepScraper
- Chez.Grater.Scraper.Site: tastyS2 :: StepScraper
- Chez.Grater.Scraper.Site: tastyS3 :: StepScraper
- Chez.Grater.Scraper.Site: tastyS4 :: StepScraper
- Chez.Grater.Scraper.Site: testScrape :: Selector -> Scraper Text Bool
- Chez.Grater.Scraper.Site: thekitchnI :: IngredientScraper
- Chez.Grater.Scraper.Site: thekitchnS :: StepScraper
- Chez.Grater.Scraper.Site: wprmI :: IngredientScraper
- Chez.Grater.Scraper.Site: wprmS :: StepScraper
- Chez.Grater.Scraper.Site: yummlyI :: IngredientScraper
- Chez.Grater.Scraper.Site: yummlyS :: StepScraper
- Chez.Grater.Scraper.Site: zlI :: IngredientScraper
- Chez.Grater.Scraper.Site: zlS :: StepScraper
+ Chez.Grater.Scraper.Site: allScrapers :: Scrapers
+ Chez.Grater.Scraper.Types: Scrapers :: HashMap SiteName IngredientScraper -> [IngredientScraper] -> HashMap SiteName StepScraper -> [StepScraper] -> Scrapers
+ Chez.Grater.Scraper.Types: [scrapersIngredientBySite] :: Scrapers -> HashMap SiteName IngredientScraper
+ Chez.Grater.Scraper.Types: [scrapersIngredients] :: Scrapers -> [IngredientScraper]
+ Chez.Grater.Scraper.Types: [scrapersSites] :: Scrapers -> [StepScraper]
+ Chez.Grater.Scraper.Types: [scrapersStepBySite] :: Scrapers -> HashMap SiteName StepScraper
+ Chez.Grater.Scraper.Types: data Scrapers
- Chez.Grater: scrapeAndParseUrl :: Manager -> URI -> IO (RecipeName, [Ingredient], [Step], ScrapeMetaWrapper)
+ Chez.Grater: scrapeAndParseUrl :: Scrapers -> Manager -> URI -> IO (RecipeName, [Ingredient], [Step], ScrapeMetaWrapper)
- Chez.Grater: scrapeUrl :: Manager -> URI -> IO (ScrapedRecipeName, [ScrapedIngredient], [ScrapedStep], ScrapeMetaWrapper)
+ Chez.Grater: scrapeUrl :: Scrapers -> Manager -> URI -> IO (ScrapedRecipeName, [ScrapedIngredient], [ScrapedStep], ScrapeMetaWrapper)
- Chez.Grater.Scraper: scrape :: (ScrapedRecipeName -> a) -> ([ScrapedIngredient] -> Either Text [b]) -> ([ScrapedStep] -> Either Text [c]) -> Manager -> URI -> IO (a, [b], [c], ScrapeMetaWrapper)
+ Chez.Grater.Scraper: scrape :: (ScrapedRecipeName -> a) -> ([ScrapedIngredient] -> Either Text [b]) -> ([ScrapedStep] -> Either Text [c]) -> Scrapers -> Manager -> URI -> IO (a, [b], [c], ScrapeMetaWrapper)
Files
- chez-grater.cabal +8/−3
- src/Chez/Grater.hs +3/−3
- src/Chez/Grater/Scraper.hs +9/−10
- src/Chez/Grater/Scraper/Site.hs +23/−3
- src/Chez/Grater/Scraper/Types.hs +7/−0
- test/Chez/GraterSpec.hs +4/−2
chez-grater.cabal view
@@ -1,11 +1,16 @@ cabal-version: 3.0 name: chez-grater-version: 0.0.1+version: 0.0.2+author: Dan Fithian maintainer: Dan Fithian copyright: 2022 Dan Fithian build-type: Simple-description: Parse and scrape recipe blogs license: MIT+category: Web+homepage: https://github.com/dfithian/chez-grater+bug-reports: https://github.com/dfithian/chez-grater/issues+synopsis: Parse and scrape recipe blogs+description: Remove the life story! Parse and scrape recipe blogs. Powers https://app.mo-nomz.com/. extra-source-files: test/fixtures/banana-bread-rachelmansfield.txt test/fixtures/chicken-pot-pie-allrecipes.txt@@ -48,7 +53,7 @@ default-language: Haskell2010 common ghc-options- ghc-options: -Wall -Wunused-packages -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog -O0+ ghc-options: -Wall -Wunused-packages -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog library import: options, ghc-options
src/Chez/Grater.hs view
@@ -5,7 +5,7 @@ import Chez.Grater.Parser (parseScrapedIngredients, parseScrapedSteps) import Chez.Grater.Scraper (scrape) import Chez.Grater.Scraper.Types- ( ScrapedRecipeName(..), ScrapeMetaWrapper, ScrapedIngredient, ScrapedStep+ ( ScrapedRecipeName(..), ScrapeMetaWrapper, ScrapedIngredient, ScrapedStep, Scrapers ) import Chez.Grater.Types (RecipeName(..), Ingredient, Step) import Control.Monad ((>=>))@@ -13,11 +13,11 @@ import Network.URI (URI) -- |Scrape a URL without parsing it.-scrapeUrl :: Manager -> URI -> IO (ScrapedRecipeName, [ScrapedIngredient], [ScrapedStep], ScrapeMetaWrapper)+scrapeUrl :: Scrapers -> Manager -> URI -> IO (ScrapedRecipeName, [ScrapedIngredient], [ScrapedStep], ScrapeMetaWrapper) scrapeUrl = scrape id Right Right -- |Scrape a URL and also parse it.-scrapeAndParseUrl :: Manager -> URI -> IO (RecipeName, [Ingredient], [Step], ScrapeMetaWrapper)+scrapeAndParseUrl :: Scrapers -> Manager -> URI -> IO (RecipeName, [Ingredient], [Step], ScrapeMetaWrapper) scrapeAndParseUrl = scrape (RecipeName . unScrapedRecipeName) (nonempty "ingredients" parseScrapedIngredients)
src/Chez/Grater/Scraper.hs view
@@ -2,12 +2,9 @@ import Chez.Grater.Internal.Prelude -import Chez.Grater.Scraper.Site- ( allIngredientScrapers, allStepScrapers, ingredientScrapers, stepScrapers- ) import Chez.Grater.Scraper.Types ( IngredientScraper(..), ScrapeError(..), ScrapeMetaWrapper(..), ScrapedRecipeName(..)- , SiteName(..), StepScraper(..), ScrapedIngredient, ScrapedStep, title+ , Scrapers(..), SiteName(..), StepScraper(..), ScrapedIngredient, ScrapedStep, title ) import Network.HTTP.Client (Manager) import Network.URI (URI, uriAuthority, uriRegName)@@ -19,8 +16,8 @@ :: (ScrapedRecipeName -> a) -> ([ScrapedIngredient] -> Either Text [b]) -> ([ScrapedStep] -> Either Text [c])- -> Manager -> URI -> IO (a, [b], [c], ScrapeMetaWrapper)-scrape mkName runIngredientParser runStepParser manager uri = do+ -> Scrapers -> Manager -> URI -> IO (a, [b], [c], ScrapeMetaWrapper)+scrape mkName runIngredientParser runStepParser scrapers manager uri = do let cfg = Scalpel.Config Scalpel.defaultDecoder (Just manager) tags <- Scalpel.fetchTagsWithConfig cfg (show uri) let domainMay = SiteName . Text.replace "www." "" . Text.pack . uriRegName <$> uriAuthority uri@@ -36,21 +33,23 @@ Just True -> (,stepScraperMeta) <$> runScraper runStepParser stepScraperRun _ -> Nothing - (ingredients, ingredientMeta) <- case flip HashMap.lookup ingredientScrapers =<< domainMay of+ (ingredients, ingredientMeta) <- case flip HashMap.lookup (scrapersIngredientBySite scrapers) =<< domainMay of Just IngredientScraper {..} -> maybe (throwIO $ ScrapeError "Failed to scrape known URL") (pure . (,ingredientScraperMeta)) $ runScraper runIngredientParser ingredientScraperRun Nothing -> maybe (throwIO $ ScrapeError "Failed to scrape URL from defaults") pure . lastMay . sortOn (length . fst) . mapMaybe goIngredient- $ allIngredientScrapers- stepsMay <- case flip HashMap.lookup stepScrapers =<< domainMay of+ . scrapersIngredients+ $ scrapers+ stepsMay <- case flip HashMap.lookup (scrapersStepBySite scrapers) =<< domainMay of Just StepScraper {..} -> pure . fmap (,stepScraperMeta) . runScraper runStepParser $ stepScraperRun Nothing -> pure . lastMay . sortOn (length . fst) . mapMaybe goStep- $ allStepScrapers+ . scrapersSites+ $ scrapers case stepsMay of Just (steps, stepMeta) | not (null steps) -> pure (mkName name, ingredients, steps, ScrapeMetaWrapperIngredientAndStep ingredientMeta stepMeta) _ -> pure (mkName name, ingredients, [], ScrapeMetaWrapperIngredient ingredientMeta)
src/Chez/Grater/Scraper/Site.hs view
@@ -1,11 +1,13 @@ -- |Description: Ingredient and step scrapers for all the websites we know about.-module Chez.Grater.Scraper.Site where+module Chez.Grater.Scraper.Site+ ( allScrapers, debugI, debugS+ ) where import Chez.Grater.Internal.Prelude import Chez.Grater.Scraper.Types ( IngredientScraper(..), ScrapeMeta(..), ScrapeName(..), ScrapeVersion(..), ScrapedIngredient(..)- , ScrapedStep(..), SiteName(..), StepScraper(..), inception+ , ScrapedStep(..), Scrapers(..), SiteName(..), StepScraper(..), inception ) import Data.Function (on) import Text.HTML.Scalpel ((//), (@:), (@=), Scraper, Selector)@@ -13,6 +15,9 @@ import qualified Data.Text as Text import qualified Text.HTML.Scalpel as Scalpel +allScrapers :: Scrapers+allScrapers = Scrapers ingredientScrapers allIngredientScrapers stepScrapers allStepScrapers+ ingredientScrapers :: HashMap SiteName IngredientScraper ingredientScrapers = HashMap.fromList [ ("allrecipes.com", allrecipesI)@@ -113,6 +118,8 @@ , ("thekitchn.com", thekitchnI) , ("eatwell101.com", eatwell101I)++ , ("bbcgoodfood.com", bbcGoodFoodI) ] stepScrapers :: HashMap SiteName StepScraper@@ -209,11 +216,14 @@ , ("food52.com", food52S) , ("thekitchn.com", thekitchnS)++ , ("bbcgoodfood.com", bbcGoodFoodS) ] -- |Get all ingredient scrapers, ordered by most popular first. allIngredientScrapers :: [IngredientScraper]-allIngredientScrapers = fmap head . reverse . sortOn length . groupBy ((==) `on` (scrapeMetaName . ingredientScraperMeta)) . sortOn (scrapeMetaName . ingredientScraperMeta) . HashMap.elems $ ingredientScrapers+allIngredientScrapers = fmap head . reverse . sortOn length . groupBy ((==) `on` (scrapeMetaName . ingredientScraperMeta)) . sortOn (scrapeMetaName . ingredientScraperMeta) $+ HashMap.elems ingredientScrapers <> [ingredientLi4, ingredientLi12] -- |Get all step scrapers, ordered by most popular first. allStepScrapers :: [StepScraper]@@ -608,3 +618,13 @@ eatwell101I = simpleIngredientScraper "eatwell101" denyAll ("div" @: [Scalpel.hasClass "pf-content"] // "li")++bbcGoodFoodI :: IngredientScraper+bbcGoodFoodI = simpleIngredientScraper "bbcgoodfood"+ denyAll+ ("section" @: [Scalpel.hasClass "recipe__ingredients"] // "li")++bbcGoodFoodS :: StepScraper+bbcGoodFoodS = simpleStepScraper "bbcgoodfood"+ denyAll+ ("section" @: [Scalpel.hasClass "recipe__method-steps"] // "li")
src/Chez/Grater/Scraper/Types.hs view
@@ -63,6 +63,13 @@ newtype SiteName = SiteName { unSiteName :: Text } deriving (Eq, Ord, Show, IsString, Hashable) +data Scrapers = Scrapers+ { scrapersIngredientBySite :: HashMap SiteName IngredientScraper+ , scrapersIngredients :: [IngredientScraper]+ , scrapersStepBySite :: HashMap SiteName StepScraper+ , scrapersSites :: [StepScraper]+ }+ data ScrapeError = ScrapeError Text deriving (Eq, Show)
test/Chez/GraterSpec.hs view
@@ -9,6 +9,7 @@ , pillsburySteps, rachelMansfieldIngredients, rachelMansfieldSteps, sallysBakingIngredients , sallysBakingSteps, tasteOfHomeIngredients, tasteOfHomeSteps )+import Chez.Grater.Scraper.Site (allScrapers) import Chez.Grater.Scraper.Types () import Chez.Grater.TestEnv (Env(..)) import Chez.Grater.Types@@ -48,7 +49,7 @@ scrapeAndParse :: Env -> String -> String -> ([Ingredient], [Step]) -> Expectation scrapeAndParse Env {..} url expectedName (expectedIngredients, expectedSteps) = do uri <- maybe (fail "Invalid URL") pure $ parseURI url- (name, ingredients, steps, _) <- scrapeAndParseUrl envManager uri+ (name, ingredients, steps, _) <- scrapeAndParseUrl allScrapers envManager uri name `shouldBe` RecipeName (Text.pack expectedName) ingredients `shouldMatchList` expectedIngredients steps `shouldMatchList` expectedSteps@@ -57,7 +58,7 @@ scrapeAndParseConfig TestCfg {..} url = do let Env {..} = env uri <- maybe (fail "Invalid URL") pure $ parseURI url- (name, ingredients, steps, _) <- scrapeAndParseUrl envManager uri+ (name, ingredients, steps, _) <- scrapeAndParseUrl allScrapers envManager uri unRecipeName name `shouldSatisfy` not . Text.null ingredients `shouldSatisfy` (\xs -> length xs >= requiredIngredients) ingredients `shouldSatisfy` any hasQuantityAndUnit@@ -221,6 +222,7 @@ it "handles chefspencil" $ scrapeAndParseConfig (defCfg { requiredSteps = 0 }) "https://www.chefspencil.com/recipe/carrot-tarte-tatin/" it "handles sweetandsavorymeals" $ scrapeAndParseConfig defCfg "https://sweetandsavorymeals.com/air-fryer-eggplant/" it "handles eatwell101" $ scrapeAndParseConfig (defCfg { requiredSteps = 0 }) "https://www.eatwell101.com/garlic-butter-chicken-bites-asparagus-recipe"+ it "handles bbcgoodfood" $ scrapeAndParseConfig defCfg "https://www.bbcgoodfood.com/recipes/challah/" describe "Implicit" $ do describe "WPRM" $ do