recipe-scraper
Version:
A JS package for scraping recipes from the web.
136 lines (116 loc) • 3.15 kB
JavaScript
;
const fetch = require("node-fetch");
const cheerio = require("cheerio");
const {validate} = require("jsonschema");
const Recipe = require("./Recipe");
const recipeSchema = require("./RecipeSchema.json");
/**
* Abstract Class which all scrapers inherit from
*/
class BaseScraper {
constructor(url, subUrl = "") {
this.url = url;
this.subUrl = subUrl;
}
async checkServerResponse() {
try {
const res = await fetch(this.url);
return res.ok; // res.status >= 200 && res.status < 300
} catch (e) {
console.log(e)
return false;
}
}
/**
* Checks if the url has the required sub url
*/
checkUrl() {
if (!this.url.includes(this.subUrl)) {
throw new Error(`url provided must include '${this.subUrl}'`);
}
}
/**
* Builds a new instance of Recipe
*/
createRecipeObject() {
this.recipe = new Recipe();
}
defaultError() {
throw new Error("No recipe found on page");
}
/**
* @param {object} $ - a cheerio object representing a DOM
* @returns {string|null} - if found, an image url
*/
defaultSetImage($) {
this.recipe.image =
$("meta[property='og:image']").attr("content") ||
$("meta[name='og:image']").attr("content") ||
$("meta[itemprop='image']").attr("content");
}
/**
* @param {object} $ - a cheerio object representing a DOM
* if found, set recipe description
*/
defaultSetDescription($) {
const description =
$("meta[name='description']").attr("content") ||
$("meta[property='og:description']").attr("content") ||
$("meta[name='twitter:description']").attr("content");
this.recipe.description = description ? description.replace(/\n/g, " ").trim() : '';
}
/**
* Fetches html from url
* @returns {object} - Cheerio instance
*/
async fetchDOMModel() {
try {
const res = await fetch(this.url);
const html = await res.text();
return cheerio.load(html);
} catch (err) {
this.defaultError();
}
}
/**
* Handles the workflow for fetching a recipe
* @returns {object} - an object representing the recipe
*/
async fetchRecipe() {
this.checkUrl();
const $ = await this.fetchDOMModel();
this.createRecipeObject();
this.scrape($);
return this.validateRecipe();
}
/**
* Abstract method
* @param {object} $ - cheerio instance
* @returns {object} - an object representing the recipe
*/
scrape($) {
throw new Error("scrape is not defined in BaseScraper");
}
textTrim(el) {
return el.text().trim();
}
/**
* Validates scraped recipes against defined recipe schema
* @returns {object} - an object representing the recipe
*/
validateRecipe() {
let res = validate(this.recipe, recipeSchema);
if (!res.valid) {
this.defaultError();
}
return this.recipe;
}
static parsePTTime(ptTime) {
ptTime = ptTime.replace('PT', '');
ptTime = ptTime.replace('H', ' hours');
ptTime = ptTime.replace('M', ' minutes');
ptTime = ptTime.replace('S', ' seconds');
return ptTime;
}
}
module.exports = BaseScraper;