recipe_scrapers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +20 -0
- data/README.md +54 -0
- data/lib/recipe_scrapers/configuration.rb +119 -0
- data/lib/recipe_scrapers/declaration.rb +85 -0
- data/lib/recipe_scrapers/error_tracker.rb +14 -0
- data/lib/recipe_scrapers/errors.rb +16 -0
- data/lib/recipe_scrapers/http/adapter.rb +50 -0
- data/lib/recipe_scrapers/http/address_guard.rb +90 -0
- data/lib/recipe_scrapers/http/body_limit.rb +45 -0
- data/lib/recipe_scrapers/http/encoding.rb +48 -0
- data/lib/recipe_scrapers/http/follow_redirects.rb +75 -0
- data/lib/recipe_scrapers/models/ingredient.rb +20 -0
- data/lib/recipe_scrapers/models/ingredient_group.rb +20 -0
- data/lib/recipe_scrapers/models/nutrient.rb +19 -0
- data/lib/recipe_scrapers/models/recipe.rb +125 -0
- data/lib/recipe_scrapers/parsers/chain.rb +52 -0
- data/lib/recipe_scrapers/parsers/durations.rb +70 -0
- data/lib/recipe_scrapers/parsers/ingredients.rb +144 -0
- data/lib/recipe_scrapers/parsers/nutrients.rb +47 -0
- data/lib/recipe_scrapers/parsers/quantity.rb +46 -0
- data/lib/recipe_scrapers/parsers/ratings.rb +26 -0
- data/lib/recipe_scrapers/parsers/vocabulary/common.yml +7 -0
- data/lib/recipe_scrapers/parsers/vocabulary/de.yml +5 -0
- data/lib/recipe_scrapers/parsers/vocabulary/en.yml +26 -0
- data/lib/recipe_scrapers/parsers/vocabulary/es.yml +4 -0
- data/lib/recipe_scrapers/parsers/vocabulary/fr.yml +5 -0
- data/lib/recipe_scrapers/parsers/vocabulary/hu.yml +3 -0
- data/lib/recipe_scrapers/parsers/vocabulary/it.yml +4 -0
- data/lib/recipe_scrapers/parsers/vocabulary/ja.yml +2 -0
- data/lib/recipe_scrapers/parsers/vocabulary/nl.yml +4 -0
- data/lib/recipe_scrapers/parsers/vocabulary/pt.yml +8 -0
- data/lib/recipe_scrapers/parsers/vocabulary/ru.yml +7 -0
- data/lib/recipe_scrapers/parsers/vocabulary/scandinavian.yml +3 -0
- data/lib/recipe_scrapers/parsers/vocabulary/sk.yml +3 -0
- data/lib/recipe_scrapers/parsers/vocabulary/tr.yml +2 -0
- data/lib/recipe_scrapers/parsers/vocabulary.rb +120 -0
- data/lib/recipe_scrapers/parsers/yields.rb +30 -0
- data/lib/recipe_scrapers/registry.rb +88 -0
- data/lib/recipe_scrapers/scraper/parsed_fields.rb +69 -0
- data/lib/recipe_scrapers/scraper.rb +199 -0
- data/lib/recipe_scrapers/site_path.rb +21 -0
- data/lib/recipe_scrapers/sites/au/bestrecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/be/15gram.rb +3 -0
- data/lib/recipe_scrapers/sites/br/panelinha.rb +3 -0
- data/lib/recipe_scrapers/sites/br/receitasnestle.rb +3 -0
- data/lib/recipe_scrapers/sites/br/receiteria.rb +3 -0
- data/lib/recipe_scrapers/sites/br/tudogostoso.rb +3 -0
- data/lib/recipe_scrapers/sites/ch/bettybossi.rb +3 -0
- data/lib/recipe_scrapers/sites/ch/migusto.rb +3 -0
- data/lib/recipe_scrapers/sites/ch/swissmilk.rb +3 -0
- data/lib/recipe_scrapers/sites/co/tasty.rb +3 -0
- data/lib/recipe_scrapers/sites/com/365daysofbakingandmore.rb +3 -0
- data/lib/recipe_scrapers/sites/com/40aprons.rb +3 -0
- data/lib/recipe_scrapers/sites/com/750g.rb +3 -0
- data/lib/recipe_scrapers/sites/com/abeautifulmess.rb +3 -0
- data/lib/recipe_scrapers/sites/com/abuelascounter.rb +3 -0
- data/lib/recipe_scrapers/sites/com/acozykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/addapinch.rb +3 -0
- data/lib/recipe_scrapers/sites/com/adozensundays.rb +3 -0
- data/lib/recipe_scrapers/sites/com/aflavorjournal.rb +3 -0
- data/lib/recipe_scrapers/sites/com/afullliving.rb +3 -0
- data/lib/recipe_scrapers/sites/com/ahealthysliceoflife.rb +5 -0
- data/lib/recipe_scrapers/sites/com/alexandracooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/allnutritious.rb +3 -0
- data/lib/recipe_scrapers/sites/com/allthehealthythings.rb +3 -0
- data/lib/recipe_scrapers/sites/com/altonbrown.rb +3 -0
- data/lib/recipe_scrapers/sites/com/amazingribs.rb +3 -0
- data/lib/recipe_scrapers/sites/com/amberskitchencooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/andy_cooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/anitalianinmykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/app.rb +27 -0
- data/lib/recipe_scrapers/sites/com/asweetpeachef.rb +3 -0
- data/lib/recipe_scrapers/sites/com/aubreyskitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/averiecooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bake_eat_repeat.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bakerbynature.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bakewithzoha.rb +3 -0
- data/lib/recipe_scrapers/sites/com/baking_sense.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bakingmischief.rb +3 -0
- data/lib/recipe_scrapers/sites/com/barefeetinthekitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/barefootinthepines.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bbcgoodfood.rb +3 -0
- data/lib/recipe_scrapers/sites/com/belleofthekitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/betterfoodguru.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bettycrocker.rb +3 -0
- data/lib/recipe_scrapers/sites/com/beyondfrosting.rb +3 -0
- data/lib/recipe_scrapers/sites/com/biancazapatka.rb +3 -0
- data/lib/recipe_scrapers/sites/com/biggerbolderbaking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/billyparisi.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bitsofcarey.rb +3 -0
- data/lib/recipe_scrapers/sites/com/blogghetti.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bluejeanchef.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bonappetit.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bonviveur.rb +3 -0
- data/lib/recipe_scrapers/sites/com/bowlofdelicious.rb +3 -0
- data/lib/recipe_scrapers/sites/com/breadtopia.rb +3 -0
- data/lib/recipe_scrapers/sites/com/brokenovenbaking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/budgetbytes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cafedelites.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cakemehometonight.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cakewhiz.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cambreabakes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/carlsbadcravings.rb +3 -0
- data/lib/recipe_scrapers/sites/com/castironskilletcooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/celebratingsweets.rb +3 -0
- data/lib/recipe_scrapers/sites/com/chefjackovens.rb +3 -0
- data/lib/recipe_scrapers/sites/com/chefsavvy.rb +3 -0
- data/lib/recipe_scrapers/sites/com/chewoutloud.rb +3 -0
- data/lib/recipe_scrapers/sites/com/chocolatewithgrace.rb +3 -0
- data/lib/recipe_scrapers/sites/com/ciaoflorentina.rb +3 -0
- data/lib/recipe_scrapers/sites/com/closetcooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cloudykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/coleycooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookedandloved.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookfastrecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookieandkate.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookiesandcups.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookingclassy.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookinglsl.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookingwithjanica.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookpad.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookwell.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cookwithdana.rb +3 -0
- data/lib/recipe_scrapers/sites/com/copykat.rb +3 -0
- data/lib/recipe_scrapers/sites/com/countryliving.rb +3 -0
- data/lib/recipe_scrapers/sites/com/crazyforcrust.rb +3 -0
- data/lib/recipe_scrapers/sites/com/creativecanning.rb +3 -0
- data/lib/recipe_scrapers/sites/com/cuisineaz.rb +3 -0
- data/lib/recipe_scrapers/sites/com/daringgourmet.rb +3 -0
- data/lib/recipe_scrapers/sites/com/dashfordinner.rb +3 -0
- data/lib/recipe_scrapers/sites/com/davidlebovitz.rb +3 -0
- data/lib/recipe_scrapers/sites/com/deliciouslyella.rb +3 -0
- data/lib/recipe_scrapers/sites/com/deliciouslysprinkled.rb +3 -0
- data/lib/recipe_scrapers/sites/com/delish.rb +3 -0
- data/lib/recipe_scrapers/sites/com/delscookingtwist.rb +3 -0
- data/lib/recipe_scrapers/sites/com/dinneratthezoo.rb +3 -0
- data/lib/recipe_scrapers/sites/com/directoalpaladar.rb +3 -0
- data/lib/recipe_scrapers/sites/com/domesticate_me.rb +3 -0
- data/lib/recipe_scrapers/sites/com/downshiftology.rb +3 -0
- data/lib/recipe_scrapers/sites/com/eatingbirdfood.rb +3 -0
- data/lib/recipe_scrapers/sites/com/eatingeuropean.rb +3 -0
- data/lib/recipe_scrapers/sites/com/eatingonadime.rb +3 -0
- data/lib/recipe_scrapers/sites/com/eatliverun.rb +3 -0
- data/lib/recipe_scrapers/sites/com/eatthismuch.rb +3 -0
- data/lib/recipe_scrapers/sites/com/elavegan.rb +3 -0
- data/lib/recipe_scrapers/sites/com/emilybites.rb +3 -0
- data/lib/recipe_scrapers/sites/com/erinliveswhole.rb +3 -0
- data/lib/recipe_scrapers/sites/com/erinscozykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/errenskitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/everyday_delicious.rb +3 -0
- data/lib/recipe_scrapers/sites/com/everydaypie.rb +3 -0
- data/lib/recipe_scrapers/sites/com/evolvingtable.rb +3 -0
- data/lib/recipe_scrapers/sites/com/familyfoodonthetable.rb +3 -0
- data/lib/recipe_scrapers/sites/com/familyspice.rb +3 -0
- data/lib/recipe_scrapers/sites/com/fantabulosity.rb +3 -0
- data/lib/recipe_scrapers/sites/com/farmhouseonboone.rb +3 -0
- data/lib/recipe_scrapers/sites/com/farmtojar.rb +3 -0
- data/lib/recipe_scrapers/sites/com/feastingathome.rb +3 -0
- data/lib/recipe_scrapers/sites/com/fifteenspatulas.rb +3 -0
- data/lib/recipe_scrapers/sites/com/figjar.rb +3 -0
- data/lib/recipe_scrapers/sites/com/finedininglovers.rb +3 -0
- data/lib/recipe_scrapers/sites/com/fithealthymacros.rb +3 -0
- data/lib/recipe_scrapers/sites/com/fitslowcookerqueen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/food.rb +3 -0
- data/lib/recipe_scrapers/sites/com/foodbymaria.rb +3 -0
- data/lib/recipe_scrapers/sites/com/foodfidelity.rb +3 -0
- data/lib/recipe_scrapers/sites/com/foodrepublic.rb +3 -0
- data/lib/recipe_scrapers/sites/com/forksoverknives.rb +3 -0
- data/lib/recipe_scrapers/sites/com/garlicandzest.rb +3 -0
- data/lib/recipe_scrapers/sites/com/garnishandglaze.rb +3 -0
- data/lib/recipe_scrapers/sites/com/gimmesomeoven.rb +3 -0
- data/lib/recipe_scrapers/sites/com/girlgonegourmet.rb +3 -0
- data/lib/recipe_scrapers/sites/com/girlversusdough.rb +3 -0
- data/lib/recipe_scrapers/sites/com/gloriousrecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/glutenfreeonashoestring.rb +3 -0
- data/lib/recipe_scrapers/sites/com/gonnawantseconds.rb +3 -0
- data/lib/recipe_scrapers/sites/com/goodfooddiscoveries.rb +3 -0
- data/lib/recipe_scrapers/sites/com/goodhousekeeping.rb +3 -0
- data/lib/recipe_scrapers/sites/com/grandbaby_cakes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/greatbritishchefs.rb +3 -0
- data/lib/recipe_scrapers/sites/com/halfbakedharvest.rb +3 -0
- data/lib/recipe_scrapers/sites/com/hassanchef.rb +3 -0
- data/lib/recipe_scrapers/sites/com/headbangerskitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/healthywithachanceofsprinkles.rb +3 -0
- data/lib/recipe_scrapers/sites/com/heatherchristo.rb +3 -0
- data/lib/recipe_scrapers/sites/com/hilahcooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/hogarmania.rb +3 -0
- data/lib/recipe_scrapers/sites/com/homeandplate.rb +3 -0
- data/lib/recipe_scrapers/sites/com/hostthetoast.rb +3 -0
- data/lib/recipe_scrapers/sites/com/houseofnasheats.rb +3 -0
- data/lib/recipe_scrapers/sites/com/houseofyumm.rb +3 -0
- data/lib/recipe_scrapers/sites/com/iamafoodblog.rb +3 -0
- data/lib/recipe_scrapers/sites/com/im_worthy.rb +3 -0
- data/lib/recipe_scrapers/sites/com/inbloombakery.rb +3 -0
- data/lib/recipe_scrapers/sites/com/indianhealthyrecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/ingoodflavor.rb +3 -0
- data/lib/recipe_scrapers/sites/com/inspiralized.rb +3 -0
- data/lib/recipe_scrapers/sites/com/itdoesnttastelikechicken.rb +3 -0
- data/lib/recipe_scrapers/sites/com/itsnotaboutnutrition.rb +3 -0
- data/lib/recipe_scrapers/sites/com/izzycooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/jennycancook.rb +3 -0
- data/lib/recipe_scrapers/sites/com/jimcooksfoodgood.rb +3 -0
- data/lib/recipe_scrapers/sites/com/jocooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/joyfullymad.rb +3 -0
- data/lib/recipe_scrapers/sites/com/joythebaker.rb +3 -0
- data/lib/recipe_scrapers/sites/com/juliasalbum.rb +3 -0
- data/lib/recipe_scrapers/sites/com/justataste.rb +3 -0
- data/lib/recipe_scrapers/sites/com/justinesnacks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/kennethtemple.rb +3 -0
- data/lib/recipe_scrapers/sites/com/keytomylime.rb +3 -0
- data/lib/recipe_scrapers/sites/com/kitchendreaming.rb +3 -0
- data/lib/recipe_scrapers/sites/com/kitchenstories.rb +3 -0
- data/lib/recipe_scrapers/sites/com/kristineskitchenblog.rb +3 -0
- data/lib/recipe_scrapers/sites/com/krollskorner.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lanascooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/laurenslatest.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lazycatkitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lecremedelacrumb.rb +3 -0
- data/lib/recipe_scrapers/sites/com/leitesculinaria.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lekkerensimpel.rb +3 -0
- data/lib/recipe_scrapers/sites/com/letscampsmore.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lifestyleofafoodie.rb +3 -0
- data/lib/recipe_scrapers/sites/com/littleferrarokitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/littlespicejar.rb +3 -0
- data/lib/recipe_scrapers/sites/com/littlespoonfarm.rb +3 -0
- data/lib/recipe_scrapers/sites/com/littlesunnykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/livelytable.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lolascocina.rb +3 -0
- data/lib/recipe_scrapers/sites/com/lovingitvegan.rb +3 -0
- data/lib/recipe_scrapers/sites/com/madsvin.rb +3 -0
- data/lib/recipe_scrapers/sites/com/makeitdairyfree.rb +3 -0
- data/lib/recipe_scrapers/sites/com/mealprepmanual.rb +3 -0
- data/lib/recipe_scrapers/sites/com/meganvskitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/melissaknorris.rb +3 -0
- data/lib/recipe_scrapers/sites/com/melskitchencafe.rb +3 -0
- data/lib/recipe_scrapers/sites/com/mexicanplease.rb +3 -0
- data/lib/recipe_scrapers/sites/com/minimalistbaker.rb +3 -0
- data/lib/recipe_scrapers/sites/com/ministryofcurry.rb +3 -0
- data/lib/recipe_scrapers/sites/com/modernhoney.rb +3 -0
- data/lib/recipe_scrapers/sites/com/momontimeout.rb +3 -0
- data/lib/recipe_scrapers/sites/com/momswithcrockpots.rb +3 -0
- data/lib/recipe_scrapers/sites/com/motherthyme.rb +3 -0
- data/lib/recipe_scrapers/sites/com/mybakingaddiction.rb +3 -0
- data/lib/recipe_scrapers/sites/com/myjewishlearning.rb +3 -0
- data/lib/recipe_scrapers/sites/com/mykidslickthebowl.rb +3 -0
- data/lib/recipe_scrapers/sites/com/mykoreankitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/myriadrecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/myvegetarianroots.rb +3 -0
- data/lib/recipe_scrapers/sites/com/natashaskitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/naturallyella.rb +3 -0
- data/lib/recipe_scrapers/sites/com/noracooks.rb +3 -0
- data/lib/recipe_scrapers/sites/com/norecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/notenoughcinnamon.rb +3 -0
- data/lib/recipe_scrapers/sites/com/nourishedbynutrition.rb +3 -0
- data/lib/recipe_scrapers/sites/com/number_2_pencil.rb +3 -0
- data/lib/recipe_scrapers/sites/com/onceuponachef.rb +3 -0
- data/lib/recipe_scrapers/sites/com/onesweetappetite.rb +3 -0
- data/lib/recipe_scrapers/sites/com/organicallyaddison.rb +3 -0
- data/lib/recipe_scrapers/sites/com/ourbestbites.rb +3 -0
- data/lib/recipe_scrapers/sites/com/paleorunningmomma.rb +3 -0
- data/lib/recipe_scrapers/sites/com/panlasangpinoy.rb +3 -0
- data/lib/recipe_scrapers/sites/com/peelwithzeal.rb +3 -0
- data/lib/recipe_scrapers/sites/com/pequerecetas.rb +3 -0
- data/lib/recipe_scrapers/sites/com/pilipinasrecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/pinkowlkitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/plantyou.rb +3 -0
- data/lib/recipe_scrapers/sites/com/platingpixels.rb +3 -0
- data/lib/recipe_scrapers/sites/com/platingsandpairings.rb +3 -0
- data/lib/recipe_scrapers/sites/com/plowingthroughlife.rb +3 -0
- data/lib/recipe_scrapers/sites/com/polishfoodies.rb +3 -0
- data/lib/recipe_scrapers/sites/com/practicalselfreliance.rb +3 -0
- data/lib/recipe_scrapers/sites/com/preppykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/pressureluckcooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/purelypope.rb +3 -0
- data/lib/recipe_scrapers/sites/com/rachlmansfield.rb +3 -0
- data/lib/recipe_scrapers/sites/com/rainbowplantlife.rb +3 -0
- data/lib/recipe_scrapers/sites/com/realfoodwell.rb +3 -0
- data/lib/recipe_scrapers/sites/com/realmomnutrition.rb +3 -0
- data/lib/recipe_scrapers/sites/com/receitas.rb +3 -0
- data/lib/recipe_scrapers/sites/com/recipeforperfection.rb +3 -0
- data/lib/recipe_scrapers/sites/com/recipegirl.rb +3 -0
- data/lib/recipe_scrapers/sites/com/reciperunner.rb +3 -0
- data/lib/recipe_scrapers/sites/com/recipetineats.rb +3 -0
- data/lib/recipe_scrapers/sites/com/redhousespice.rb +3 -0
- data/lib/recipe_scrapers/sites/com/ricardocuisine.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sallysbakingaddiction.rb +3 -0
- data/lib/recipe_scrapers/sites/com/saltpepperskillet.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sandwichtribunal.rb +3 -0
- data/lib/recipe_scrapers/sites/com/savoringthegood.rb +3 -0
- data/lib/recipe_scrapers/sites/com/savorynothings.rb +3 -0
- data/lib/recipe_scrapers/sites/com/savorythoughts.rb +3 -0
- data/lib/recipe_scrapers/sites/com/scrummylane.rb +3 -0
- data/lib/recipe_scrapers/sites/com/shelikesfood.rb +3 -0
- data/lib/recipe_scrapers/sites/com/simple_veganista.rb +3 -0
- data/lib/recipe_scrapers/sites/com/simplehomeedit.rb +3 -0
- data/lib/recipe_scrapers/sites/com/simplyquinoa.rb +3 -0
- data/lib/recipe_scrapers/sites/com/simplywhisked.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sipandfeast.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sizzlefish.rb +3 -0
- data/lib/recipe_scrapers/sites/com/skinnytaste.rb +3 -0
- data/lib/recipe_scrapers/sites/com/smalltownwoman.rb +3 -0
- data/lib/recipe_scrapers/sites/com/somuchfoodblog.rb +3 -0
- data/lib/recipe_scrapers/sites/com/southernbite.rb +3 -0
- data/lib/recipe_scrapers/sites/com/spainonafork.rb +3 -0
- data/lib/recipe_scrapers/sites/com/spendwithpennies.rb +3 -0
- data/lib/recipe_scrapers/sites/com/spicysouthernkitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/stacyling.rb +3 -0
- data/lib/recipe_scrapers/sites/com/staysnatched.rb +3 -0
- data/lib/recipe_scrapers/sites/com/steamykitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/com/strongrfastr.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sudachirecipes.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sugarhero.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sugarmaplefarmhouse.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sugarspunrun.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sunset.rb +3 -0
- data/lib/recipe_scrapers/sites/com/sweetcsdesigns.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tableanddish.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tasteandtellblog.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tasteofhome.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tastesbetterfromscratch.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tastesoflizzyt.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tastyoven.rb +3 -0
- data/lib/recipe_scrapers/sites/com/tatyanaseverydayfood.rb +3 -0
- data/lib/recipe_scrapers/sites/com/teakandthyme.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thealmondeater.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thebigmansworld.rb +3 -0
- data/lib/recipe_scrapers/sites/com/theclevercarrot.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thecookierookie.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thefoodcharlatan.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thefoodietakesflight.rb +3 -0
- data/lib/recipe_scrapers/sites/com/theglutenfreeaustrian.rb +3 -0
- data/lib/recipe_scrapers/sites/com/theguardian.rb +3 -0
- data/lib/recipe_scrapers/sites/com/theicecreamconfectionals.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thekitchenmagpie.rb +3 -0
- data/lib/recipe_scrapers/sites/com/themagicalslowcooker.rb +3 -0
- data/lib/recipe_scrapers/sites/com/themediterraneandish.rb +3 -0
- data/lib/recipe_scrapers/sites/com/themodernproper.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thepalatablelife.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thepioneerwoman.rb +3 -0
- data/lib/recipe_scrapers/sites/com/theppk.rb +3 -0
- data/lib/recipe_scrapers/sites/com/therecipecritic.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thesaltymarshmallow.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thespicetrain.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thesuburbansoapbox.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thevintagemixer.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thewoodenskillet.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thinlicious.rb +3 -0
- data/lib/recipe_scrapers/sites/com/thishealthytable.rb +3 -0
- data/lib/recipe_scrapers/sites/com/toriavey.rb +3 -0
- data/lib/recipe_scrapers/sites/com/twopeasandtheirpod.rb +3 -0
- data/lib/recipe_scrapers/sites/com/unsophisticook.rb +3 -0
- data/lib/recipe_scrapers/sites/com/valentinascorner.rb +3 -0
- data/lib/recipe_scrapers/sites/com/vanillaandbean.rb +3 -0
- data/lib/recipe_scrapers/sites/com/veganricha.rb +3 -0
- data/lib/recipe_scrapers/sites/com/vegrecipesofindia.rb +3 -0
- data/lib/recipe_scrapers/sites/com/wearenotmartha.rb +3 -0
- data/lib/recipe_scrapers/sites/com/wedishitup.rb +3 -0
- data/lib/recipe_scrapers/sites/com/wellplated.rb +3 -0
- data/lib/recipe_scrapers/sites/com/whatsgabycooking.rb +3 -0
- data/lib/recipe_scrapers/sites/com/wyseguide.rb +3 -0
- data/lib/recipe_scrapers/sites/com/yemek.rb +3 -0
- data/lib/recipe_scrapers/sites/com/zenbelly.rb +3 -0
- data/lib/recipe_scrapers/sites/com/zestfulkitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/de/bofrost.rb +3 -0
- data/lib/recipe_scrapers/sites/de/chefkoch.rb +26 -0
- data/lib/recipe_scrapers/sites/de/eattolerant.rb +3 -0
- data/lib/recipe_scrapers/sites/de/emmikochteinfach.rb +3 -0
- data/lib/recipe_scrapers/sites/de/kochbar.rb +3 -0
- data/lib/recipe_scrapers/sites/de/springlane.rb +3 -0
- data/lib/recipe_scrapers/sites/dk/madensverden.rb +3 -0
- data/lib/recipe_scrapers/sites/dk/sundpaabudget.rb +3 -0
- data/lib/recipe_scrapers/sites/fr/jow.rb +3 -0
- data/lib/recipe_scrapers/sites/fr/madame.rb +35 -0
- data/lib/recipe_scrapers/sites/hu/nosalty.rb +3 -0
- data/lib/recipe_scrapers/sites/hu/sobors.rb +3 -0
- data/lib/recipe_scrapers/sites/info/misya.rb +3 -0
- data/lib/recipe_scrapers/sites/it/cucchiaio.rb +3 -0
- data/lib/recipe_scrapers/sites/it/ricette.rb +3 -0
- data/lib/recipe_scrapers/sites/it/ricetteperbimby.rb +3 -0
- data/lib/recipe_scrapers/sites/me/leckerschmecker.rb +3 -0
- data/lib/recipe_scrapers/sites/menu/1000.rb +3 -0
- data/lib/recipe_scrapers/sites/mx/cocinafacil.rb +3 -0
- data/lib/recipe_scrapers/sites/mx/directoalpaladar.rb +3 -0
- data/lib/recipe_scrapers/sites/mx/recetasnestle.rb +3 -0
- data/lib/recipe_scrapers/sites/net/bellyfull.rb +3 -0
- data/lib/recipe_scrapers/sites/net/castironketo.rb +3 -0
- data/lib/recipe_scrapers/sites/net/feelgoodfoodie.rb +3 -0
- data/lib/recipe_scrapers/sites/net/hungryhappens.rb +3 -0
- data/lib/recipe_scrapers/sites/net/iambaker.rb +3 -0
- data/lib/recipe_scrapers/sites/net/inspiredtaste.rb +3 -0
- data/lib/recipe_scrapers/sites/net/piesandplots.rb +3 -0
- data/lib/recipe_scrapers/sites/net/tidymom.rb +3 -0
- data/lib/recipe_scrapers/sites/nl/leukerecepten.rb +3 -0
- data/lib/recipe_scrapers/sites/nl/projectgezond.rb +3 -0
- data/lib/recipe_scrapers/sites/no/godt.rb +3 -0
- data/lib/recipe_scrapers/sites/no/matprat.rb +3 -0
- data/lib/recipe_scrapers/sites/no/nrk.rb +3 -0
- data/lib/recipe_scrapers/sites/no/tine.rb +3 -0
- data/lib/recipe_scrapers/sites/org/lmld.rb +3 -0
- data/lib/recipe_scrapers/sites/org/marmiton.rb +3 -0
- data/lib/recipe_scrapers/sites/org/nutritionfacts.rb +3 -0
- data/lib/recipe_scrapers/sites/org/thekitchencommunity.rb +3 -0
- data/lib/recipe_scrapers/sites/plus/recette.rb +3 -0
- data/lib/recipe_scrapers/sites/pt/pingodoce.rb +3 -0
- data/lib/recipe_scrapers/sites/pt/teleculinaria.rb +3 -0
- data/lib/recipe_scrapers/sites/recipes/goodstuff.rb +3 -0
- data/lib/recipe_scrapers/sites/recipes/howtocook.rb +3 -0
- data/lib/recipe_scrapers/sites/ru/eda.rb +3 -0
- data/lib/recipe_scrapers/sites/ru/edimdoma.rb +3 -0
- data/lib/recipe_scrapers/sites/ru/gastronom.rb +3 -0
- data/lib/recipe_scrapers/sites/ru/iamcook.rb +6 -0
- data/lib/recipe_scrapers/sites/ru/povar.rb +3 -0
- data/lib/recipe_scrapers/sites/ru/russianfood.rb +8 -0
- data/lib/recipe_scrapers/sites/se/alltommat.rb +3 -0
- data/lib/recipe_scrapers/sites/se/ica.rb +3 -0
- data/lib/recipe_scrapers/sites/se/kiddokitchen.rb +3 -0
- data/lib/recipe_scrapers/sites/se/koket.rb +3 -0
- data/lib/recipe_scrapers/sites/se/recept.rb +3 -0
- data/lib/recipe_scrapers/sites/sk/receptyprevas.rb +3 -0
- data/lib/recipe_scrapers/sources/declared.rb +38 -0
- data/lib/recipe_scrapers/sources/ingredient_groups.rb +74 -0
- data/lib/recipe_scrapers/sources/json_ld.rb +93 -0
- data/lib/recipe_scrapers/sources/microdata.rb +113 -0
- data/lib/recipe_scrapers/sources/open_graph.rb +26 -0
- data/lib/recipe_scrapers/sources/schema_org/ingredient_list.rb +36 -0
- data/lib/recipe_scrapers/sources/schema_org/nutrition_facts.rb +21 -0
- data/lib/recipe_scrapers/sources/schema_org.rb +160 -0
- data/lib/recipe_scrapers/text.rb +59 -0
- data/lib/recipe_scrapers/version.rb +6 -0
- data/lib/recipe_scrapers.rb +137 -0
- metadata +560 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Models
|
|
5
|
+
# One nutrition fact split into its parts. {Models::Recipe#parsed_nutrients} holds these.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] name
|
|
8
|
+
# @return [String, nil] the key the page uses, such as "calories" or "fatContent"
|
|
9
|
+
# @!attribute [r] unit
|
|
10
|
+
# @return [String, nil] "g", "mg", "µg", "kcal" or "kJ" for mass and energy, any other unit as written
|
|
11
|
+
# @!attribute [r] amount
|
|
12
|
+
# @return [Float, nil] the number, with every decimal the page writes
|
|
13
|
+
Nutrient = Data.define(:name, :unit, :amount) do
|
|
14
|
+
def initialize(name: nil, unit: nil, amount: nil)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Models
|
|
5
|
+
# The recipe a page holds, with every field already read. {RecipeScrapers.scrape} and
|
|
6
|
+
# {RecipeScrapers.parse} return one. It is a Ruby Data object: frozen, compared by value, and free
|
|
7
|
+
# of the page it came from. A field the page does not publish is nil.
|
|
8
|
+
#
|
|
9
|
+
# {#to_h} converts the nested value objects too, so the result holds only strings, numbers,
|
|
10
|
+
# arrays and hashes, ready for JSON.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# recipe = RecipeScrapers.scrape("https://www.recipetineats.com/crispy-potato-straws-pommes-paille/")
|
|
14
|
+
# recipe.title # => "Crispy potato straws (Pommes Paille)"
|
|
15
|
+
# recipe.to_h[:parsed_ingredients].first
|
|
16
|
+
# # => { amount: 1.0, unit: nil, name: "potato, or other starchy or all-rounder potato" }
|
|
17
|
+
#
|
|
18
|
+
# @!attribute [r] url
|
|
19
|
+
# @return [String] the address the recipe was read from
|
|
20
|
+
# @!attribute [r] title
|
|
21
|
+
# @return [String, nil] the name of the recipe
|
|
22
|
+
# @!attribute [r] author
|
|
23
|
+
# @return [String, nil] the author, several joined with ", "
|
|
24
|
+
# @!attribute [r] site_name
|
|
25
|
+
# @return [String, nil] the name of the website
|
|
26
|
+
# @!attribute [r] language
|
|
27
|
+
# @return [String, nil] the language tag of the recipe, such as "en-US"
|
|
28
|
+
# @!attribute [r] canonical_url
|
|
29
|
+
# @return [String] the canonical link of the page resolved against {Models::Recipe#url}, or the
|
|
30
|
+
# url itself when the page has none
|
|
31
|
+
# @!attribute [r] host
|
|
32
|
+
# @return [String] the host of {Models::Recipe#url}, without "www."
|
|
33
|
+
# @!attribute [r] description
|
|
34
|
+
# @return [String, nil] a sentence or short paragraph about the recipe
|
|
35
|
+
# @!attribute [r] image
|
|
36
|
+
# @return [String, nil] the address of the main image, resolved against {Models::Recipe#url}
|
|
37
|
+
# @!attribute [r] ingredients
|
|
38
|
+
# The ingredient lines as the page writes them. A line with no letter and no digit, such as
|
|
39
|
+
# "*", is a separator and left out. A line that ends in a colon and has no digit, such as
|
|
40
|
+
# "For the sauce:", is a group heading and goes to {Models::Recipe#ingredient_groups}
|
|
41
|
+
# instead. See {Models::Recipe#parsed_ingredients}.
|
|
42
|
+
#
|
|
43
|
+
# @return [Array<String>, nil]
|
|
44
|
+
# @!attribute [r] parsed_ingredients
|
|
45
|
+
# Every line of {Models::Recipe#ingredients}, in the same order, split into an amount, a unit and a name.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# recipe.parsed_ingredients.first.to_h # => { amount: 1.5, unit: "cups", name: "vegetable oil" }
|
|
49
|
+
# @return [Array<Models::Ingredient>, nil]
|
|
50
|
+
# @!attribute [r] ingredient_groups
|
|
51
|
+
# The ingredients in the groups the page shows, such as "For the sauce". The groups come from
|
|
52
|
+
# the HTML when the page marks them up, otherwise from the heading lines of the ingredient
|
|
53
|
+
# list. A page without groups gives one group with a nil purpose.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<Models::IngredientGroup>, nil] nil when the page has no ingredients
|
|
56
|
+
# @!attribute [r] instructions_list
|
|
57
|
+
# @return [Array<String>, nil] the steps, with section headings as their own entries
|
|
58
|
+
# @!attribute [r] instructions
|
|
59
|
+
# @return [String, nil] the steps joined with newlines
|
|
60
|
+
# @!attribute [r] category
|
|
61
|
+
# @return [String, nil] the course, such as "Dessert"
|
|
62
|
+
# @!attribute [r] cuisine
|
|
63
|
+
# @return [String, nil] the cuisine, such as "Italian"
|
|
64
|
+
# @!attribute [r] cooking_method
|
|
65
|
+
# @return [String, nil] how it is cooked, such as "Baking"
|
|
66
|
+
# @!attribute [r] keywords
|
|
67
|
+
# @return [Array<String>, nil] the keywords, without facet entries such as "diet: vegan"
|
|
68
|
+
# @!attribute [r] dietary_restrictions
|
|
69
|
+
# @return [Array<String>, nil] the diets the recipe suits, such as "VeganDiet"
|
|
70
|
+
# @!attribute [r] equipment
|
|
71
|
+
# @return [Array<String>, nil] the tools a site declares, schema.org has none
|
|
72
|
+
# @!attribute [r] yields
|
|
73
|
+
# @return [String, nil] how much it makes, as "4 servings" or "12 items"
|
|
74
|
+
# @!attribute [r] total_time
|
|
75
|
+
# @return [Integer, nil] the total time in minutes
|
|
76
|
+
# @!attribute [r] prep_time
|
|
77
|
+
# @return [Integer, nil] the preparation time in minutes
|
|
78
|
+
# @!attribute [r] cook_time
|
|
79
|
+
# @return [Integer, nil] the cooking time in minutes
|
|
80
|
+
# @!attribute [r] nutrients
|
|
81
|
+
# The nutrition facts as the page publishes them, keyed by the schema.org property name.
|
|
82
|
+
# Nothing is parsed here. See {Models::Recipe#parsed_nutrients}.
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# recipe.nutrients # => { "calories" => "219 kcal", "fatContent" => "7 g" }
|
|
86
|
+
# @return [Hash{String => String}, nil]
|
|
87
|
+
# @!attribute [r] parsed_nutrients
|
|
88
|
+
# Every entry of {Models::Recipe#nutrients}, in the order the page lists them, split into a name, a unit
|
|
89
|
+
# and an amount. The name is the key the page uses. A value with no number is left out.
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# recipe.parsed_nutrients.first.to_h # => { name: "calories", unit: "kcal", amount: 219.0 }
|
|
93
|
+
# @return [Array<Models::Nutrient>, nil]
|
|
94
|
+
# @!attribute [r] ratings
|
|
95
|
+
# @return [Float, nil] the average rating
|
|
96
|
+
# @!attribute [r] ratings_count
|
|
97
|
+
# @return [Integer, nil] how many ratings the average is made of
|
|
98
|
+
# @!attribute [r] links
|
|
99
|
+
# @return [Array<String>] the href of every link on the page, as written
|
|
100
|
+
Recipe = Data.define(
|
|
101
|
+
:url, :title, :author, :site_name, :language, :canonical_url, :host, :description, :image, :ingredients,
|
|
102
|
+
:parsed_ingredients, :ingredient_groups, :instructions_list, :instructions, :category, :cuisine,
|
|
103
|
+
:cooking_method, :keywords, :dietary_restrictions, :equipment, :yields, :total_time, :prep_time, :cook_time,
|
|
104
|
+
:nutrients, :parsed_nutrients, :ratings, :ratings_count, :links
|
|
105
|
+
) do
|
|
106
|
+
# The recipe as plain data. Every nested value object becomes a hash as well.
|
|
107
|
+
#
|
|
108
|
+
# @return [Hash{Symbol => Object}]
|
|
109
|
+
def to_h(&)
|
|
110
|
+
super.transform_values { |value| plain(value) }.to_h(&)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def plain(value)
|
|
116
|
+
case value
|
|
117
|
+
when Data then plain(value.to_h)
|
|
118
|
+
when Hash then value.transform_values { |item| plain(item) }
|
|
119
|
+
when Array then value.map { |item| plain(item) }
|
|
120
|
+
else value
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
# Turn text into values.
|
|
5
|
+
module Parsers
|
|
6
|
+
# Runs the parsers of a field in order until one returns something. A parser returns a hash of
|
|
7
|
+
# the model's attributes, which becomes the model, or nil. A parser that raises, or returns a
|
|
8
|
+
# hash with an unknown key, goes to {Configuration#error_tracker}, and the next one is tried.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Chain
|
|
12
|
+
FIELDS = %i[ingredients nutrients].freeze
|
|
13
|
+
MODELS = { ingredients: Models::Ingredient, nutrients: Models::Nutrient }.freeze
|
|
14
|
+
private_constant :MODELS
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def defaults
|
|
18
|
+
{ ingredients: [Ingredients], nutrients: [Nutrients] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def field!(field)
|
|
22
|
+
return field if FIELDS.include?(field)
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, "no parser can be set for #{field.inspect}, only for #{FIELDS.join(", ")}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(field, parsers, text, language:)
|
|
28
|
+
parsers.each do |parser|
|
|
29
|
+
result = attempt(field, parser, text, language)
|
|
30
|
+
return result unless result.nil?
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def attempt(field, parser, text, language)
|
|
38
|
+
model(field, parser.call(text, language: language))
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
RecipeScrapers.config.error_tracker.call(e)
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def model(field, result)
|
|
45
|
+
return result if result.nil? || result.is_a?(Data)
|
|
46
|
+
|
|
47
|
+
MODELS.fetch(field).new(**result)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Parsers
|
|
5
|
+
module Durations
|
|
6
|
+
LANGUAGES = {
|
|
7
|
+
english: { hours: %w[h hr hrs hour hours], minutes: %w[m min mins minute minutes],
|
|
8
|
+
seconds: %w[s sec secs second seconds] },
|
|
9
|
+
german: { hours: %w[std stunde stunden], minutes: %w[minuten],
|
|
10
|
+
seconds: %w[sek sekunde sekunden] },
|
|
11
|
+
french: { hours: %w[heure heures], minutes: %w[minute minutes], seconds: %w[seconde secondes] },
|
|
12
|
+
italian: { hours: %w[ora ore], minutes: %w[minuto minuti], seconds: %w[secondo secondi] },
|
|
13
|
+
dutch: { hours: %w[uur], minutes: %w[minuut minuten], seconds: %w[seconde seconden] },
|
|
14
|
+
swedish: { hours: %w[tim timme timmar], minutes: %w[minut minuter],
|
|
15
|
+
seconds: %w[sekund sekunder] },
|
|
16
|
+
norwegian: { hours: %w[time timer], minutes: %w[minutt minutter], seconds: %w[sekund sekunder] },
|
|
17
|
+
hungarian: { hours: %w[óra órát], minutes: %w[perc percet], seconds: %w[másodperc] },
|
|
18
|
+
russian: { hours: %w[ч час часа часов], minutes: %w[м мин минута минуты минут минуту],
|
|
19
|
+
seconds: %w[с сек секунда секунды секунд] }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
HOURS, MINUTES, SECONDS = %i[hours minutes seconds].map do |unit|
|
|
23
|
+
words = LANGUAGES.values.flat_map { |language| language.fetch(unit) }.uniq
|
|
24
|
+
/(\d+)\s*(?:#{words.sort_by { |word| -word.length }.join("|")})\b/i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
ISO = /\AP(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?)?\z/
|
|
28
|
+
ISO_UNIT_MINUTES = [10_080, 1440, 60, 1, 1 / 60.0].freeze
|
|
29
|
+
NUMBER = /\d+/
|
|
30
|
+
RANGE = /(\d+)\s*(?:-|–|—|to|до)\s*\d+/
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def minutes(value)
|
|
34
|
+
return nil if value.nil?
|
|
35
|
+
return value if value.is_a?(Integer)
|
|
36
|
+
return minutes(value["@value"]) if value.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
text = Text.normalize(value)
|
|
39
|
+
return nil if text.nil?
|
|
40
|
+
|
|
41
|
+
from_iso(text) || from_words(text.gsub(RANGE, '\\1'))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def from_iso(text)
|
|
47
|
+
match = ISO.match(text)
|
|
48
|
+
return nil unless match&.captures&.any?
|
|
49
|
+
|
|
50
|
+
total = match.captures.zip(ISO_UNIT_MINUTES).sum { |count, minutes| count.to_f * minutes }.round
|
|
51
|
+
total.positive? ? total : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def from_words(text)
|
|
55
|
+
hours, mins, secs = [HOURS, MINUTES, SECONDS].map { |unit| text[unit, 1] }
|
|
56
|
+
return sole_number(text) if [hours, mins, secs].compact.empty?
|
|
57
|
+
return nil unless [hours, mins, secs].compact.size == text.scan(NUMBER).size
|
|
58
|
+
|
|
59
|
+
total = (hours.to_i * 60) + mins.to_i
|
|
60
|
+
total.positive? ? total : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sole_number(text)
|
|
64
|
+
numbers = text.scan(NUMBER)
|
|
65
|
+
numbers.size == 1 ? numbers.first.to_i : nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Parsers
|
|
5
|
+
# The bundled parser of ingredient lines. It reads the amount, the unit and the name with the
|
|
6
|
+
# words of the recipe language. Put it after your own parser to keep it as the fallback.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# RecipeScrapers::Parsers::Ingredients.call("1 1/2 cups flour", language: "en").to_h
|
|
10
|
+
# # => { amount: 1.5, unit: "cups", name: "flour" }
|
|
11
|
+
class Ingredients
|
|
12
|
+
QUANTITY = Quantity::QUANTITY
|
|
13
|
+
SUM = Quantity::SUM
|
|
14
|
+
UPPER_BOUND = /(?:(?:\s*[-–—]\s*|\s+(?:to|or|o|a|à|bis|до|или)\s+)#{QUANTITY})?/o
|
|
15
|
+
WORD_END = /(?![\p{L}\d'’-])/
|
|
16
|
+
PREPARATION = /\p{Ll}[\p{L}-]*(?:\s+\p{Ll}[\p{L}-]*)?/
|
|
17
|
+
CONNECTOR = /\A(?:(?:of|de|di)\s+)?(?:an?\s+)?/i
|
|
18
|
+
PARENTHESES = /\([^()]*\)/
|
|
19
|
+
SPACE_BEFORE_PUNCTUATION = /\s+([,.;:])/
|
|
20
|
+
EDGE_PUNCTUATION = /\A[\s,;:.]+|[\s,;:]+\z/
|
|
21
|
+
BULLET = /\A[♥♡**•·]+\s*/
|
|
22
|
+
ARTICLES = %w[a an].freeze
|
|
23
|
+
LOCK = Mutex.new
|
|
24
|
+
private_constant :QUANTITY, :SUM, :UPPER_BOUND, :WORD_END, :PREPARATION, :CONNECTOR, :PARENTHESES,
|
|
25
|
+
:SPACE_BEFORE_PUNCTUATION, :EDGE_PUNCTUATION, :BULLET, :ARTICLES, :LOCK
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# @param line [String] one ingredient line
|
|
29
|
+
# @param language [String, nil] the language tag of the recipe
|
|
30
|
+
# @return [Models::Ingredient]
|
|
31
|
+
def call(line, language: nil)
|
|
32
|
+
for_language(language).call(line)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param language [String, nil]
|
|
36
|
+
# @return [Ingredients] a parser for the vocabulary of the language, built once per vocabulary
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
def for_language(language)
|
|
40
|
+
names = Vocabulary.names_for(language)
|
|
41
|
+
LOCK.synchronize { instances[names] ||= new(Vocabulary.combine(names)) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def instances
|
|
47
|
+
@instances ||= {}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Vocabulary] the words this parser reads
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
attr_reader :vocabulary
|
|
55
|
+
|
|
56
|
+
# @param vocabulary [Vocabulary]
|
|
57
|
+
#
|
|
58
|
+
# @api private
|
|
59
|
+
def initialize(vocabulary)
|
|
60
|
+
@vocabulary = vocabulary
|
|
61
|
+
@patterns = build_patterns
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param line [String] one ingredient line
|
|
65
|
+
# @return [Models::Ingredient]
|
|
66
|
+
#
|
|
67
|
+
# @api private
|
|
68
|
+
def call(line, **)
|
|
69
|
+
parsed = parse_text(without_parentheses(line.to_s))
|
|
70
|
+
return parsed unless parsed.name.nil? && line.to_s.include?("(")
|
|
71
|
+
|
|
72
|
+
parse_text(tidy(line.to_s.gsub(PARENTHESES, " ").delete("()")))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def build_patterns
|
|
78
|
+
[
|
|
79
|
+
/\A#{qualifier}#{measure}\s*(?<ingredient>.*)\z/,
|
|
80
|
+
/\A(?:#{adjective})?(?<unit>#{pinch_word})\s+(?:of\s+)?(?<ingredient>.+)\z/i,
|
|
81
|
+
/\A(?<note>#{PREPARATION})\s+(?<amount>#{SUM})#{UPPER_BOUND}\s*#{unit}\s+(?<ingredient>.+)\z/,
|
|
82
|
+
/\A(?<ingredient>[^:]+?)\s*:\s*#{qualifier}#{numeric_measure}(?:\.?\s+(?<note>\D+?))?\.?\z/,
|
|
83
|
+
/\A(?<ingredient>.+?)(?:\s*[,\-–—]\s*|\s+)#{qualifier}#{numeric_measure}\z/,
|
|
84
|
+
/\A(?<ingredient>.+?)\s*[:,\-–—]\s*(?<unit>#{pinch_word})\z/i,
|
|
85
|
+
/\A(?<ingredient>.+?)\s+(?<unit>#{unit_word})\s*(?<amount>#{QUANTITY})\z/
|
|
86
|
+
].freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def words(list) = Vocabulary.alternation(list)
|
|
90
|
+
def unit_word = @unit_word ||= /(?:#{words(vocabulary.units)})/i
|
|
91
|
+
def pinch_word = @pinch_word ||= /(?:#{words(vocabulary.pinches)})#{WORD_END}/i
|
|
92
|
+
def adjective = @adjective ||= /(?:#{words(vocabulary.sizes)})\s+/i
|
|
93
|
+
def qualifier = @qualifier ||= /(?:(?:#{words(vocabulary.qualifiers)})\s+|~\s*)*/i
|
|
94
|
+
def size = @size ||= /#{QUANTITY}#{UPPER_BOUND}\s*-?\s*#{unit_word}\.?(?:-|\s)\s*/
|
|
95
|
+
def unit_lead = @unit_lead ||= /(?:#{size})?(?:#{adjective})?/
|
|
96
|
+
def alternative = @alternative ||= %r{(?:\s*[/+]\s*|\s+)#{QUANTITY}#{UPPER_BOUND}\s*#{unit_word}\.?#{WORD_END}}
|
|
97
|
+
def unit = @unit ||= /#{unit_lead}(?<unit>#{unit_word})\.?#{WORD_END}(?:#{alternative})*/
|
|
98
|
+
def counted_word = /(?:#{words(vocabulary.number_words.keys - ARTICLES)})(?=\s)/i
|
|
99
|
+
def article = /an?(?=\s+#{unit_lead}#{unit_word}#{WORD_END})/i
|
|
100
|
+
def numeric_measure = /(?<amount>#{SUM})#{UPPER_BOUND}\s*(?:#{unit})?/
|
|
101
|
+
def measure = /(?<amount>#{SUM}|#{counted_word}|#{article})#{UPPER_BOUND}\s*(?:#{unit})?/
|
|
102
|
+
|
|
103
|
+
def parse_text(text)
|
|
104
|
+
@patterns.each do |pattern|
|
|
105
|
+
match = text.match(pattern)
|
|
106
|
+
return measured(match) if match && !match[:ingredient].start_with?("%")
|
|
107
|
+
end
|
|
108
|
+
row(nil, nil, text)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def measured(match)
|
|
112
|
+
names = match.names
|
|
113
|
+
unit = match[:unit]&.squeeze(" ")
|
|
114
|
+
ingredient = match[:ingredient]
|
|
115
|
+
ingredient = ingredient.sub(CONNECTOR, "") if match.begin(:ingredient).positive?
|
|
116
|
+
ingredient = [ingredient, match[:note]].compact.join(", ") if names.include?("note")
|
|
117
|
+
row(names.include?("amount") ? amount(match[:amount]) : 1.0, unit, ingredient)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def row(amount, unit, ingredient)
|
|
121
|
+
cleaned = ingredient.gsub(EDGE_PUNCTUATION, "")
|
|
122
|
+
Models::Ingredient.new(amount: amount, unit: unit, name: cleaned.empty? ? nil : cleaned)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def amount(quantity)
|
|
126
|
+
word = vocabulary.number_words[quantity.downcase]
|
|
127
|
+
return word.to_f if word
|
|
128
|
+
|
|
129
|
+
Quantity.value(quantity)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def without_parentheses(text)
|
|
133
|
+
reduced = text.gsub(PARENTHESES, " ")
|
|
134
|
+
return tidy(reduced) if reduced == text
|
|
135
|
+
|
|
136
|
+
without_parentheses(reduced)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def tidy(text)
|
|
140
|
+
text.gsub(SPACE_BEFORE_PUNCTUATION, "\\1").squeeze(" ").strip.sub(BULLET, "")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Parsers
|
|
5
|
+
# The bundled parser of nutrient values. It reads the first number and the word after it, and
|
|
6
|
+
# drops text in parentheses. Mass and energy units are written as symbols, so "grams" and "г" become
|
|
7
|
+
# "g" and "calories" becomes "kcal".
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# RecipeScrapers::Parsers::Nutrients.call("12.5 grams").to_h # => { name: nil, unit: "g", amount: 12.5 }
|
|
11
|
+
module Nutrients
|
|
12
|
+
PARENTHESES = /\([^()]*\)/
|
|
13
|
+
CONNECTORS = %w[of de di].freeze
|
|
14
|
+
MEASURE = /(?<amount>#{Quantity::QUANTITY})\s*(?<unit>[\p{L}µ][\p{L}-]*)?/o
|
|
15
|
+
CANONICAL_UNITS = {
|
|
16
|
+
"g" => %w[g gr grs gram grams gramm gramos gramas grammi gramme grammes г гр грамм грамма граммов],
|
|
17
|
+
"mg" => %w[mg milligram milligrams milligramm мг],
|
|
18
|
+
"µg" => %w[µg μg mcg microgram micrograms мкг],
|
|
19
|
+
"kcal" => %w[
|
|
20
|
+
kcal kcals kkal cal cals calorie calories kalorien kalorier calorías calorias ккал калорий калории
|
|
21
|
+
],
|
|
22
|
+
"kJ" => %w[kj кдж]
|
|
23
|
+
}.flat_map { |canonical, spellings| spellings.map { |spelling| [spelling, canonical] } }.to_h.freeze
|
|
24
|
+
private_constant :PARENTHESES, :CONNECTORS, :MEASURE, :CANONICAL_UNITS
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @param text [String] one nutrient value
|
|
28
|
+
# @return [Models::Nutrient, nil] without a name, which {Models::Recipe#parsed_nutrients} fills in. Nil
|
|
29
|
+
# when the value has no number
|
|
30
|
+
def call(text, **)
|
|
31
|
+
match = text.to_s.gsub(PARENTHESES, " ").match(MEASURE)
|
|
32
|
+
return nil if match.nil?
|
|
33
|
+
|
|
34
|
+
Models::Nutrient.new(amount: Quantity.total(match[:amount]), unit: unit(match[:unit]))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def unit(written)
|
|
40
|
+
return nil if written.nil? || CONNECTORS.include?(written.downcase)
|
|
41
|
+
|
|
42
|
+
CANONICAL_UNITS.fetch(written.downcase, written)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Parsers
|
|
5
|
+
module Quantity
|
|
6
|
+
NUMBER = /\d+(?:[.,]\d+)?/
|
|
7
|
+
SLASH_FRACTION = %r{\d+\s*[/⁄]\s*\d+}
|
|
8
|
+
VULGAR = /[½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅐⅛⅜⅝⅞⅑⅒]/
|
|
9
|
+
JOINER = /\s+(?:and|und|y|e)\s+|\s*と\s*/
|
|
10
|
+
QUANTITY = /
|
|
11
|
+
\d+(?:#{JOINER}|\s*)#{VULGAR}|\d+(?:#{JOINER}|\s+)#{SLASH_FRACTION}|#{SLASH_FRACTION}|#{NUMBER}|#{VULGAR}
|
|
12
|
+
/xo
|
|
13
|
+
PLUS = /\s*\+\s*/
|
|
14
|
+
SUM = /#{QUANTITY}(?:#{PLUS}#{QUANTITY})*/o
|
|
15
|
+
SLASH_SPACING = %r{\s*([/⁄])\s*}
|
|
16
|
+
WHOLE_AND_FRACTION = /#{JOINER}|\s+|(?=#{VULGAR})/o
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def value(sum)
|
|
20
|
+
amount = total(sum).round(2)
|
|
21
|
+
amount.zero? ? nil : amount
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def total(sum)
|
|
25
|
+
sum.split(PLUS).sum { |term| term_value(term) }.to_f
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def term_value(term)
|
|
31
|
+
whole, fraction = term.gsub(SLASH_SPACING, "\\1").split(WHOLE_AND_FRACTION, 2)
|
|
32
|
+
part_value(whole) + part_value(fraction)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def part_value(part)
|
|
36
|
+
return 0 if part.nil?
|
|
37
|
+
|
|
38
|
+
numerator, denominator = part.unicode_normalize(:nfkd).split(%r{[/⁄]})
|
|
39
|
+
return Rational(numerator.to_i, denominator.to_i) if denominator.to_i.positive?
|
|
40
|
+
|
|
41
|
+
Rational(numerator.tr(",", "."))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecipeScrapers
|
|
4
|
+
module Parsers
|
|
5
|
+
module Ratings
|
|
6
|
+
DECIMAL = /\d+(?:[.,]\d+)?/
|
|
7
|
+
DIGIT_GROUPING = /[,. ]/
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def average(node)
|
|
11
|
+
return nil if node.nil?
|
|
12
|
+
|
|
13
|
+
value = node["ratingValue"].to_s[DECIMAL]&.tr(",", ".")&.to_f
|
|
14
|
+
value&.positive? ? value : nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def count(node)
|
|
18
|
+
return nil if node.nil?
|
|
19
|
+
|
|
20
|
+
total = (node["ratingCount"] || node["reviewCount"]).to_s.gsub(DIGIT_GROUPING, "")[/\A\d+/]&.to_i
|
|
21
|
+
total&.positive? ? total : nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
units: [g, gr, grs, gram, grams, gramm, gramos, gramas, grammi, kg, kilo, kilos, kilogram, kilograms, quilo,
|
|
2
|
+
quilos, mg, ml, milliliter, milliliters, millilitre, millilitres, l, liter, liters, litre, litres, litro,
|
|
3
|
+
litros, dl, cl, krm, cm, mm, count]
|
|
4
|
+
qualifiers: [ca., circa, "null"]
|
|
5
|
+
number_words:
|
|
6
|
+
a: 1
|
|
7
|
+
an: 1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
languages: [en]
|
|
2
|
+
units: [cup, cups, c, tsp, tsps, teaspoon, teaspoons, t, tbsp, tbsps, tbs, tbl, tablespoon, tablespoons, oz,
|
|
3
|
+
ounce, ounces, lb, lbs, pound, pounds, quart, quarts, qt, pint, pints, pt, gallon, gallons, clove, cloves,
|
|
4
|
+
can, cans, tin, tins, jar, jars, package, packages, packet, packets, pkg, bag, bags, box, boxes, bunch,
|
|
5
|
+
bunches, sprig, sprigs, slice, slices, stick, sticks, head, heads, stalk, stalks, pinch, pinches, dash,
|
|
6
|
+
dashes, handful, handfuls, drizzle, splash, sprinkle, knob, piece, pieces, pc, pcs, sheet, sheets, scoop,
|
|
7
|
+
scoops, container, containers, ear, ears, drop, drops, bottle, bottles, envelope, envelopes, block, blocks,
|
|
8
|
+
shot, shots, each, count, serving, servings, portion, portions, ptn, ptns, thumb, thumbs, batch, foot, feet,
|
|
9
|
+
leaf, leaves, bar, bars, loaf, loaves, spear, spears, inch, inches, fl oz, fluid ounce, fluid ounces]
|
|
10
|
+
pinches: [pinch, pinches, dash, dashes, handful, handfuls, drizzle, splash, sprinkle, knob]
|
|
11
|
+
sizes: [large, small, medium, big, small-to-medium, heaping, heaped, level, scant, rounded, generous]
|
|
12
|
+
qualifiers: [about, approx., approx, approximately, around, scant, generous, heaping, heaped, "optional:"]
|
|
13
|
+
number_words:
|
|
14
|
+
one: 1
|
|
15
|
+
two: 2
|
|
16
|
+
three: 3
|
|
17
|
+
four: 4
|
|
18
|
+
five: 5
|
|
19
|
+
six: 6
|
|
20
|
+
seven: 7
|
|
21
|
+
eight: 8
|
|
22
|
+
nine: 9
|
|
23
|
+
ten: 10
|
|
24
|
+
eleven: 11
|
|
25
|
+
twelve: 12
|
|
26
|
+
half: 0.5
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
languages: [fr]
|
|
2
|
+
units: [cs, cc, pincée, pincées, poignée, poignées, gousse, gousses, brin, brins, feuille, feuilles, tranche,
|
|
3
|
+
tranches, tran, sachet, sachets, cuillère, cuillères, c. à soupe, c. à café, cuillère à soupe,
|
|
4
|
+
cuillères à soupe, cuillère à café, cuillères à café]
|
|
5
|
+
pinches: [pincée, poignée]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
languages: [pt]
|
|
2
|
+
units: [xícara, xícaras, colher, colheres, dente, dentes, pitada, pitadas, fatia, fatias, rodela, rodelas,
|
|
3
|
+
rolo, rolos, medida, medidas, lata, latas, tablete, tabletes, cháv, chávena, chávenas, unid, c. de sopa,
|
|
4
|
+
c. de café, c. de chá, xícara de chá, xícaras de chá, colher de sopa, colheres de sopa, colher de chá,
|
|
5
|
+
colheres de chá]
|
|
6
|
+
number_words:
|
|
7
|
+
meia: 0.5
|
|
8
|
+
meio: 0.5
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
languages: [ru]
|
|
2
|
+
units: [г, гр, грамм, грамма, граммов, кг, мл, л, стакан, стакана, стаканов, шт, зубчик, зубчика, зубчиков,
|
|
3
|
+
щепотка, щепотки, пучок, пучка, штука, штуки, штук, стебель, стебля, головка, головки, веточка, веточки,
|
|
4
|
+
веточек, ст. л, стол. л, ч. л, ст. ложка, ст. ложки, ст. ложек, ч. ложка, ч. ложки, ч. ложек, дес. ложка,
|
|
5
|
+
дес. ложки, ложка, ложки, ложек, столовая ложка, столовые ложки, столовых ложки, столовых ложек,
|
|
6
|
+
чайная ложка, чайные ложки, чайных ложки, чайных ложек]
|
|
7
|
+
pinches: [щепотка]
|