crawlforge-extractors 1.6.0 → 1.6.1
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.
- package/package.json +1 -1
- package/src/templates.js +39 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-extractors",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Extraction logic shared by the CrawlForge MCP server and REST API — scrape templates, charset-correct capped body reading, structural fingerprinting, and embedded-state extraction. One implementation, so the two surfaces cannot drift apart.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
package/src/templates.js
CHANGED
|
@@ -274,6 +274,35 @@ function amazonByline($) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
/** "4.7 out of 5 stars" → 4.7 */
|
|
277
|
+
/**
|
|
278
|
+
* ISO 4217 code for an Amazon price string. The add-to-cart form's hidden
|
|
279
|
+
* currencyCode input is absent on pages Amazon serves without a buy box
|
|
280
|
+
* (amazon.de and amazon.in book pages, R14 2026-09-03), so the code is read
|
|
281
|
+
* off the price itself: an explicit code ("52,02USD"), else the symbol. A
|
|
282
|
+
* bare "$" belongs to several marketplaces, told apart by the canonical host.
|
|
283
|
+
*/
|
|
284
|
+
function amazonCurrency($, price) {
|
|
285
|
+
if (!price) return null;
|
|
286
|
+
const code = price.match(/(USD|EUR|GBP|INR|JPY|CAD|AUD|MXN|BRL|SGD|AED|SAR|PLN|TRY)(?![A-Z])/);
|
|
287
|
+
if (code) return code[1];
|
|
288
|
+
if (price.includes('₹')) return 'INR';
|
|
289
|
+
if (price.includes('€')) return 'EUR';
|
|
290
|
+
if (price.includes('£')) return 'GBP';
|
|
291
|
+
if (price.includes('¥') || price.includes('¥')) return 'JPY';
|
|
292
|
+
if (price.includes('R$')) return 'BRL';
|
|
293
|
+
if (price.includes('zł')) return 'PLN';
|
|
294
|
+
if (price.includes('₺')) return 'TRY';
|
|
295
|
+
if (price.includes('$')) {
|
|
296
|
+
const host = (attr($, 'link[rel="canonical"]', 'href') || '').match(/^https?:\/\/([^/]+)/)?.[1] || '';
|
|
297
|
+
if (/\.ca$/.test(host)) return 'CAD';
|
|
298
|
+
if (/\.com\.au$/.test(host)) return 'AUD';
|
|
299
|
+
if (/\.com\.mx$/.test(host)) return 'MXN';
|
|
300
|
+
if (/\.sg$/.test(host)) return 'SGD';
|
|
301
|
+
return 'USD';
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
277
306
|
function amazonRating(value) {
|
|
278
307
|
const match = tidy(value)?.match(/([\d.]+)/);
|
|
279
308
|
return match ? Number.parseFloat(match[1]) : null;
|
|
@@ -562,12 +591,17 @@ export const TEMPLATES = [
|
|
|
562
591
|
.map(fullSizeImage)
|
|
563
592
|
.filter(Boolean);
|
|
564
593
|
|
|
594
|
+
const price = text($, '.a-price .a-offscreen') || text($, '#priceblock_ourprice') || text($, '#priceblock_dealprice');
|
|
595
|
+
|
|
565
596
|
return {
|
|
566
597
|
title: tidy(text($, '#productTitle')),
|
|
567
|
-
price
|
|
598
|
+
price,
|
|
568
599
|
// Amazon ships no priceCurrency meta tag — the ISO code is a hidden
|
|
569
|
-
// field on the add-to-cart form.
|
|
570
|
-
currency:
|
|
600
|
+
// field on the add-to-cart form, and off the price where there is none.
|
|
601
|
+
currency:
|
|
602
|
+
attr($, 'input[name*="currencyCode"]', 'value') ||
|
|
603
|
+
attr($, 'meta[itemprop="priceCurrency"]', 'content') ||
|
|
604
|
+
amazonCurrency($, price),
|
|
571
605
|
rating: amazonRating(attr($, '#acrPopover', 'title') || text($, '#averageCustomerReviews .a-icon-alt')),
|
|
572
606
|
review_count: amazonCount(text($, '#acrCustomerReviewText') || text($, '[data-hook="total-review-count"]')),
|
|
573
607
|
asin: text($, 'input#ASIN') || attr($, 'input[name="ASIN"]', 'value'),
|
|
@@ -729,7 +763,8 @@ export const TEMPLATES = [
|
|
|
729
763
|
title: $titleLink.text().trim(),
|
|
730
764
|
url: safeHref($titleLink.attr('href')),
|
|
731
765
|
site: $row.find('.sitebit a').text().trim() || null,
|
|
732
|
-
|
|
766
|
+
// "1 point" on a fresh story and "3 points" on the rest — strip both.
|
|
767
|
+
score: $score.text().replace(/\s*points?$/, '').trim() || null,
|
|
733
768
|
author: $subtext.find('.hnuser').text().trim() || null,
|
|
734
769
|
// ".age a" wraps the relative age string ("3 hours ago"); its href is the item permalink.
|
|
735
770
|
posted: $subtext.find('.age a').text().trim() || null,
|