scrape.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import RequestPromise from "request-promise";
  2. import Cheerio from "cheerio";
  3. import { Recipe, RecipeDetails, Ingredient, ComposedIngredients } from './index';
  4. import { parse } from "recipe-ingredient-parser";
  5. type RegexMutator = (s: RegExpMatchArray) => number;
  6. const matchInt = (data: string, reg: RegExp, logic?: RegexMutator): number => {
  7. let result = data.match(reg);
  8. return logic ?
  9. (result ? logic(result) : 0) :
  10. (result ? parseInt(result[0]) : 0)
  11. }
  12. const parseTime = (data: string) => {
  13. let parsedHoursAsMin = matchInt(data, /(\d+) hours?/, result => parseInt(result[0])*60);
  14. let parsedMinutes = matchInt(data, /(\d+) minutes?/);
  15. return parsedHoursAsMin + parsedMinutes;
  16. };
  17. const LAURA_MAP: {
  18. [index:string] : {
  19. key: string,
  20. transform: (data: string) => number,
  21. }
  22. } = {
  23. preparationTime: {
  24. key: 'Preparation',
  25. transform: parseTime,
  26. },
  27. cookingTime: {
  28. key: 'Cook time',
  29. transform: parseTime,
  30. },
  31. servings: {
  32. key: 'Servings',
  33. transform: (data: string) => matchInt(data, /\d+/),
  34. }
  35. };
  36. function parseIngredients(rawIngredient: string): Ingredient {
  37. let parsed = parse(rawIngredient);
  38. return {
  39. name: parsed.ingredient,
  40. quantity: Number(parsed.quantity) || 0,
  41. unit: parsed.unit || '',
  42. _original: rawIngredient,
  43. };
  44. }
  45. function getIngredients($:CheerioSelector): ComposedIngredients[] {
  46. const ingredientsScrape = $('.cs-ingredients-check-list > ul').children();
  47. const isNewIngredientList = (tagName: string): boolean => tagName === 'span';
  48. let ingredients: ComposedIngredients[] = [];
  49. return ingredientsScrape.toArray().reduce((list, rawIngredient)=>{
  50. let last = list.length - 1;
  51. let text = $(rawIngredient).text();
  52. if (isNewIngredientList(rawIngredient.tagName)) {
  53. return list.concat([{name: text, ingredients: []}]);
  54. } else {
  55. list[last].ingredients = list[last].ingredients.concat([parseIngredients(text)]);
  56. return list;
  57. }
  58. }, ingredients);
  59. }
  60. function getRecipeDetails($:CheerioSelector):RecipeDetails {
  61. let details = new RecipeDetails();
  62. const detailsKey = Object.keys(details);
  63. const data = $('.cs-recipe-details').find('div');
  64. let x: any = {};
  65. data.each((i, el) => {
  66. let key: string = $(el).find('span').text();
  67. x[key] = $(el).contents().last().text();
  68. });
  69. return detailsKey.reduce((recipeDetails, key) => {
  70. let _key = LAURA_MAP[key];
  71. return {
  72. ...recipeDetails,
  73. [key]: _key.transform(x[_key.key])
  74. }
  75. }, details);
  76. }
  77. function getRecipeName($:CheerioSelector): string {
  78. return $('.cs-page-title>h1').text().trim();
  79. }
  80. function getInstructions($:CheerioSelector): string[] {
  81. const data = $('#recipe-process').find('ul').text();
  82. return data.split('\n').filter(s => s !== '').map(s => s.trim().replace(/^\d\)\s*/, ''));
  83. }
  84. function lauraInTheKitchen($:CheerioSelector):Recipe {
  85. let recipe = new Recipe(
  86. getRecipeName($),
  87. getRecipeDetails($),
  88. getIngredients($),
  89. getInstructions($),
  90. );
  91. return recipe;
  92. }
  93. function scrape(url:string) {
  94. const options = {
  95. uri: url,
  96. transform: (body: any) => Cheerio.load(body, {
  97. normalizeWhitespace: true
  98. }),
  99. };
  100. return RequestPromise(options).then(($) => {
  101. return lauraInTheKitchen($);
  102. });
  103. }
  104. export default scrape;