UNPKG

scrape-it-core

Version:

The core scraping functionality of scrape-it.

209 lines (187 loc) 6.87 kB
"use strict"; var typpy = require("typpy"), iterateObj = require("iterate-object"), Err = require("err"), objDef = require("obj-def"), emptyObj = require("is-empty-obj"), cheerio = require("cheerio"); /** * scrapeItCore * The core scraping functionality of `scrape-it`. * * Scrapes the data in the provided element. * * For the format of the selector, please refer to the [Selectors section of the Cheerio library](https://github.com/cheeriojs/cheerio#-selector-context-root-) * * @name scrapeIt.scrapeHTML * @function * @param {Cheerio|String} $ The input element or the HTML as a string. * @param {Object} opts An object containing the scraping information. * * If you want to scrape a list, you have to use the `listItem` selector: * * - `listItem` (String): The list item selector. * - `data` (Object): The fields to include in the list objects: * - `<fieldName>` (Object|String): The selector or an object containing: * - `selector` (String): The selector. * - `convert` (Function): An optional function to change the value. * - `how` (Function|String): A function or function name to access the * value. * - `attr` (String): If provided, the value will be taken based on * the attribute name. * - `trimValue` (Boolean): If `false`, the value will *not* be trimmed * (default: `true`). * - `closest` (String): If provided, returns the first ancestor of * the given element. * - `eq` (Number): If provided, it will select the *nth* element. * - `texteq` (Number): If provided, it will select the *nth* direct text child. * Deep text child selection is not possible yet. * Overwrites the `how` key. * - `listItem` (Object): An object, keeping the recursive schema of * the `listItem` object. This can be used to create nested lists. * * **Example**: * ```js * { * articles: { * listItem: ".article" * , data: { * createdAt: { * selector: ".date" * , convert: x => new Date(x) * } * , title: "a.article-title" * , tags: { * listItem: ".tags > span" * } * , content: { * selector: ".article-content" * , how: "html" * } * , traverseOtherNode: { * selector: ".upperNode" * , closest: "div" * , convert: x => x.length * } * } * } * } * ``` * * If you want to collect specific data from the page, just use the same * schema used for the `data` field. * * **Example**: * ```js * { * title: ".header h1" * , desc: ".header h2" * , avatar: { * selector: ".header img" * , attr: "src" * } * } * ``` * * * @name scrapeItCore * @function * @returns {Object} The scraped data. */ module.exports = function ($, opts) { if (typeof $ === "string") { $ = cheerio.load($); } // Normalizes the option var normalizeOpt = function normalizeOpt(v) { if (typpy(v, String)) { v = { selector: v }; } objDef(v, "data", {}); objDef(v, "how", "text", true); if (v.attr) { v.how = function ($elm) { return $elm.attr(v.attr); }; } objDef(v, "trimValue", true); objDef(v, "closest", ""); return v; }; // Recursively handles the data var handleDataObj = function handleDataObj(data, $context) { var pageData = {}; iterateObj(data, function (cOpt, optName) { cOpt = normalizeOpt(cOpt); cOpt.name = optName; var $cContext = $context === $ ? undefined : $context; if (!$cContext && !cOpt.selector && !cOpt.listItem) { throw new Err("There is no element selected for the '<option.name>' field. Please provide a selector, list item or use nested object structure.", { option: cOpt, code: "NO_ELEMENT_SELECTED" }); } var $elm = cOpt.selector ? $(cOpt.selector, $cContext) : $cContext; // Handle lists if (cOpt.listItem) { var docs = pageData[cOpt.name] = [], $items = $(cOpt.listItem, $cContext), isEmpty = emptyObj(cOpt.data); if (isEmpty) { cOpt.data.___raw = {}; } for (var i = 0; i < $items.length; ++i) { var cDoc = handleDataObj(cOpt.data, $items.eq(i)); var convert = cOpt.convert || function (x) { return x; }; docs.push(convert(cDoc.___raw || cDoc)); } } else { if (typpy(cOpt.eq, Number)) { $elm = $elm.eq(cOpt.eq); } if (typpy(cOpt.texteq, Number)) { var children = $elm.contents(), textCounter = 0, found = false; for (var _i = 0, child; child = children[_i]; _i++) { if (child.type === "text") { if (textCounter == cOpt.texteq) { $elm = child; found = true; break; } textCounter++; } } if (!found) { $elm = cheerio.load(""); } cOpt.how = function (elm) { return elm.data; }; } // Handle closest if (cOpt.closest) { $elm = $elm.closest(cOpt.closest); } if (!emptyObj(cOpt.data)) { pageData[cOpt.name] = handleDataObj(cOpt.data, $elm); return pageData; } var value = typpy(cOpt.how, Function) ? cOpt.how($elm) : $elm[cOpt.how](); value = value === undefined ? "" : value; if (cOpt.trimValue && typpy(value, String)) { value = value.trim(); } if (cOpt.convert) { value = cOpt.convert(value, $elm); } pageData[cOpt.name] = value; } }); return pageData; }; return handleDataObj(opts); };