webpeel 0.21.68 → 0.21.69
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.
|
@@ -117,6 +117,9 @@ const REGISTRY = [
|
|
|
117
117
|
{ match: (h) => h === 'etsy.com' || h === 'www.etsy.com', extractor: etsyExtractor },
|
|
118
118
|
{ match: (h) => h === 'cars.com' || h === 'www.cars.com', extractor: carsComExtractor },
|
|
119
119
|
{ match: (h) => h === 'ebay.com' || h === 'www.ebay.com', extractor: ebayExtractor },
|
|
120
|
+
// ── Local / Real Estate ────────────────────────────────────────────────────
|
|
121
|
+
{ match: (h) => h === 'yelp.com' || h === 'www.yelp.com', extractor: yelpExtractor },
|
|
122
|
+
{ match: (h) => h === 'zillow.com' || h === 'www.zillow.com', extractor: zillowExtractor },
|
|
120
123
|
];
|
|
121
124
|
/**
|
|
122
125
|
* Returns the domain extractor for a URL, or null if none matches.
|
|
@@ -3199,6 +3202,62 @@ async function bbcExtractor(html, url) {
|
|
|
3199
3202
|
return extractNewsArticle(html, url, 'bbc.com');
|
|
3200
3203
|
}
|
|
3201
3204
|
async function cnnExtractor(html, url) {
|
|
3205
|
+
try {
|
|
3206
|
+
const u = new URL(url);
|
|
3207
|
+
// For homepage — use CNN Lite which has actual headline links
|
|
3208
|
+
if (u.pathname === '/' || u.pathname === '' || u.hostname === 'lite.cnn.com') {
|
|
3209
|
+
const liteResp = await fetch('https://lite.cnn.com', { headers: { 'User-Agent': 'webpeel/0.21' } });
|
|
3210
|
+
if (liteResp.ok) {
|
|
3211
|
+
const liteHtml = await liteResp.text();
|
|
3212
|
+
const headlines = [];
|
|
3213
|
+
const matches = liteHtml.matchAll(/<a[^>]+href="([^"]*)"[^>]*>([^<]+)<\/a>/g);
|
|
3214
|
+
for (const m of matches) {
|
|
3215
|
+
const href = m[1].trim();
|
|
3216
|
+
const text = m[2].trim();
|
|
3217
|
+
// CNN Lite article links contain year patterns like /2026/
|
|
3218
|
+
if (/\/20\d\d\//.test(href) && text.length > 10) {
|
|
3219
|
+
const fullUrl = href.startsWith('http') ? href : `https://www.cnn.com${href}`;
|
|
3220
|
+
headlines.push(`- [${text}](${fullUrl})`);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
if (headlines.length > 5) {
|
|
3224
|
+
return {
|
|
3225
|
+
domain: 'cnn.com',
|
|
3226
|
+
type: 'headlines',
|
|
3227
|
+
structured: { headlines: headlines.length, source: 'cnn-lite' },
|
|
3228
|
+
cleanContent: `# 📰 CNN — Top Headlines\n\n${headlines.slice(0, 20).join('\n')}\n\n---\n*Source: CNN Lite*`,
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
// For article pages — try CNN Lite version of the same URL
|
|
3234
|
+
if (/\/20\d\d\//.test(u.pathname)) {
|
|
3235
|
+
const liteUrl = `https://lite.cnn.com${u.pathname}`;
|
|
3236
|
+
const liteResp = await fetch(liteUrl, { headers: { 'User-Agent': 'webpeel/0.21' } });
|
|
3237
|
+
if (liteResp.ok) {
|
|
3238
|
+
const liteHtml = await liteResp.text();
|
|
3239
|
+
const { load } = await import('cheerio');
|
|
3240
|
+
const $l = load(liteHtml);
|
|
3241
|
+
const title = $l('h1').first().text().trim();
|
|
3242
|
+
const paragraphs = [];
|
|
3243
|
+
$l('p').each((_, el) => {
|
|
3244
|
+
const text = $l(el).text().trim();
|
|
3245
|
+
if (text.length > 20)
|
|
3246
|
+
paragraphs.push(text);
|
|
3247
|
+
});
|
|
3248
|
+
if (title && paragraphs.length > 0) {
|
|
3249
|
+
return {
|
|
3250
|
+
domain: 'cnn.com',
|
|
3251
|
+
type: 'article',
|
|
3252
|
+
structured: { title, paragraphs: paragraphs.length, source: 'cnn-lite' },
|
|
3253
|
+
cleanContent: `# ${title}\n\n${paragraphs.join('\n\n')}\n\n---\n*Source: CNN*`,
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
catch { /* fall through to standard extractor */ }
|
|
3260
|
+
// Fallback to standard news article extractor (works if HTML has content)
|
|
3202
3261
|
return extractNewsArticle(html, url, 'cnn.com');
|
|
3203
3262
|
}
|
|
3204
3263
|
// ---------------------------------------------------------------------------
|
|
@@ -5184,3 +5243,169 @@ async function ebayExtractor(html, url) {
|
|
|
5184
5243
|
return null;
|
|
5185
5244
|
}
|
|
5186
5245
|
}
|
|
5246
|
+
// ---------------------------------------------------------------------------
|
|
5247
|
+
// Yelp extractor — parse JSON-LD + meta from stealth-rendered HTML
|
|
5248
|
+
// ---------------------------------------------------------------------------
|
|
5249
|
+
async function yelpExtractor(html, url) {
|
|
5250
|
+
try {
|
|
5251
|
+
const { load } = await import('cheerio');
|
|
5252
|
+
const $ = load(html);
|
|
5253
|
+
// Try JSON-LD structured data first
|
|
5254
|
+
const jsonLdScripts = $('script[type="application/ld+json"]');
|
|
5255
|
+
let businessData = null;
|
|
5256
|
+
jsonLdScripts.each((_, el) => {
|
|
5257
|
+
const raw = $(el).html() || '';
|
|
5258
|
+
try {
|
|
5259
|
+
const parsed = JSON.parse(raw);
|
|
5260
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
5261
|
+
for (const item of items) {
|
|
5262
|
+
const type = item['@type'];
|
|
5263
|
+
if (type === 'Restaurant' || type === 'LocalBusiness' || type === 'FoodEstablishment' ||
|
|
5264
|
+
type === 'BarOrPub' || type === 'CafeOrCoffeeShop') {
|
|
5265
|
+
businessData = item;
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
catch { /* ignore malformed JSON-LD */ }
|
|
5270
|
+
});
|
|
5271
|
+
// --- Business page ---
|
|
5272
|
+
if (businessData) {
|
|
5273
|
+
const name = businessData.name || '';
|
|
5274
|
+
const rating = businessData.aggregateRating?.ratingValue;
|
|
5275
|
+
const reviewCount = businessData.aggregateRating?.reviewCount;
|
|
5276
|
+
const addr = businessData.address;
|
|
5277
|
+
const address = addr
|
|
5278
|
+
? [addr.streetAddress, addr.addressLocality, addr.addressRegion, addr.postalCode].filter(Boolean).join(', ')
|
|
5279
|
+
: '';
|
|
5280
|
+
const phone = businessData.telephone || '';
|
|
5281
|
+
const cuisine = businessData.servesCuisine || '';
|
|
5282
|
+
const priceRange = businessData.priceRange || '';
|
|
5283
|
+
const description = businessData.description || $('meta[property="og:description"]').attr('content') || '';
|
|
5284
|
+
const hours = businessData.openingHours || '';
|
|
5285
|
+
const lines = [
|
|
5286
|
+
`# ⭐ Yelp: ${name}`,
|
|
5287
|
+
'',
|
|
5288
|
+
rating && `**Rating:** ${rating}/5 (${reviewCount} reviews)`,
|
|
5289
|
+
cuisine && `**Cuisine:** ${cuisine}`,
|
|
5290
|
+
priceRange && `**Price:** ${priceRange}`,
|
|
5291
|
+
address && `**Address:** ${address}`,
|
|
5292
|
+
phone && `**Phone:** ${phone}`,
|
|
5293
|
+
hours && `**Hours:** ${Array.isArray(hours) ? hours.join(', ') : hours}`,
|
|
5294
|
+
description && `\n${description.substring(0, 500)}`,
|
|
5295
|
+
'',
|
|
5296
|
+
`**More info:** [View on Yelp](${url})`,
|
|
5297
|
+
'',
|
|
5298
|
+
'---',
|
|
5299
|
+
'*Source: Yelp*',
|
|
5300
|
+
].filter(Boolean);
|
|
5301
|
+
return {
|
|
5302
|
+
domain: 'yelp.com',
|
|
5303
|
+
type: 'business',
|
|
5304
|
+
structured: { name, rating, reviewCount, address, phone, cuisine, priceRange, description },
|
|
5305
|
+
cleanContent: lines.join('\n'),
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
// --- Search page — parse from meta / og tags ---
|
|
5309
|
+
const ogTitle = $('meta[property="og:title"]').attr('content') || '';
|
|
5310
|
+
const ogDescription = $('meta[property="og:description"]').attr('content') || '';
|
|
5311
|
+
// Try to extract listing names from heading tags
|
|
5312
|
+
const listings = [];
|
|
5313
|
+
$('h3, h4').each((_, el) => {
|
|
5314
|
+
const text = $(el).text().trim();
|
|
5315
|
+
if (text && text.length > 2 && text.length < 100) {
|
|
5316
|
+
const anchor = $(el).find('a').first();
|
|
5317
|
+
const href = anchor.attr('href') || '';
|
|
5318
|
+
const fullHref = href.startsWith('/') ? `https://www.yelp.com${href}` : href;
|
|
5319
|
+
listings.push({ name: text, url: fullHref || undefined });
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
5322
|
+
if (ogTitle || listings.length > 0) {
|
|
5323
|
+
const searchTerm = ogTitle.replace(/\s*-\s*Yelp$/, '').trim();
|
|
5324
|
+
const lines = [
|
|
5325
|
+
`# 🔍 Yelp Search: ${searchTerm || 'Results'}`,
|
|
5326
|
+
ogDescription && `\n${ogDescription}`,
|
|
5327
|
+
listings.length > 0 && `\n**Found ${listings.length} results:**`,
|
|
5328
|
+
...listings.slice(0, 15).map((l, i) => `${i + 1}. ${l.url ? `[${l.name}](${l.url})` : l.name}`),
|
|
5329
|
+
'',
|
|
5330
|
+
`**Search:** [View on Yelp](${url})`,
|
|
5331
|
+
'',
|
|
5332
|
+
'---',
|
|
5333
|
+
'*Source: Yelp*',
|
|
5334
|
+
].filter(Boolean);
|
|
5335
|
+
return {
|
|
5336
|
+
domain: 'yelp.com',
|
|
5337
|
+
type: 'search',
|
|
5338
|
+
structured: { query: searchTerm, count: listings.length, listings },
|
|
5339
|
+
cleanContent: lines.join('\n'),
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
return null;
|
|
5343
|
+
}
|
|
5344
|
+
catch (e) {
|
|
5345
|
+
if (process.env.DEBUG)
|
|
5346
|
+
console.debug('[webpeel]', 'Yelp extractor error:', e instanceof Error ? e.message : e);
|
|
5347
|
+
return null;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
// ---------------------------------------------------------------------------
|
|
5351
|
+
// Zillow extractor — smart fallback with helpful alternatives
|
|
5352
|
+
// ---------------------------------------------------------------------------
|
|
5353
|
+
async function zillowExtractor(_html, url) {
|
|
5354
|
+
try {
|
|
5355
|
+
const u = new URL(url);
|
|
5356
|
+
// Derive location label from the URL path
|
|
5357
|
+
const rawPath = u.pathname.replace(/^\//, '').replace(/\/$/, '');
|
|
5358
|
+
const location = rawPath
|
|
5359
|
+
.replace(/\//g, ' ')
|
|
5360
|
+
.replace(/-/g, ' ')
|
|
5361
|
+
.trim();
|
|
5362
|
+
// Parse city/state for alternative links
|
|
5363
|
+
const pathParts = rawPath.split('/').filter(Boolean);
|
|
5364
|
+
const cityStatePart = pathParts[0] || ''; // e.g. "new-york-ny"
|
|
5365
|
+
const segments = cityStatePart.split('-');
|
|
5366
|
+
const statePart = segments[segments.length - 1] || '';
|
|
5367
|
+
const cityPart = segments.slice(0, -1).join('-');
|
|
5368
|
+
// Redfin city path
|
|
5369
|
+
const cityCapitalized = cityPart.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('_');
|
|
5370
|
+
const stateUpper = statePart.toUpperCase();
|
|
5371
|
+
const redfinCityPath = cityCapitalized && stateUpper
|
|
5372
|
+
? `https://www.redfin.com/city/${cityCapitalized}/${stateUpper}`
|
|
5373
|
+
: 'https://www.redfin.com';
|
|
5374
|
+
const realtorPath = cityStatePart
|
|
5375
|
+
? `https://www.realtor.com/realestateandhomes-search/${cityStatePart}`
|
|
5376
|
+
: 'https://www.realtor.com';
|
|
5377
|
+
const cleanContent = [
|
|
5378
|
+
`# 🏠 Zillow — ${location || 'Real Estate Search'}`,
|
|
5379
|
+
'',
|
|
5380
|
+
'> ⚠️ **Zillow blocks automated access.** WebPeel cannot retrieve live listings directly.',
|
|
5381
|
+
'',
|
|
5382
|
+
'**Try these alternatives that work with WebPeel:**',
|
|
5383
|
+
`- [Redfin](${redfinCityPath}) — similar listings, scrape-friendly`,
|
|
5384
|
+
`- [Realtor.com](${realtorPath}) — MLS-powered, often accessible`,
|
|
5385
|
+
`- [Homes.com](https://www.homes.com) — newer platform, better access`,
|
|
5386
|
+
'',
|
|
5387
|
+
`**Direct Zillow link:** [Open Zillow](${url})`,
|
|
5388
|
+
'',
|
|
5389
|
+
'---',
|
|
5390
|
+
'*Source: Zillow (access blocked — showing alternatives)*',
|
|
5391
|
+
].join('\n');
|
|
5392
|
+
return {
|
|
5393
|
+
domain: 'zillow.com',
|
|
5394
|
+
type: 'real-estate',
|
|
5395
|
+
structured: {
|
|
5396
|
+
location,
|
|
5397
|
+
blocked: true,
|
|
5398
|
+
alternatives: [
|
|
5399
|
+
{ name: 'Redfin', url: redfinCityPath },
|
|
5400
|
+
{ name: 'Realtor.com', url: realtorPath },
|
|
5401
|
+
],
|
|
5402
|
+
},
|
|
5403
|
+
cleanContent,
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
catch (e) {
|
|
5407
|
+
if (process.env.DEBUG)
|
|
5408
|
+
console.debug('[webpeel]', 'Zillow extractor error:', e instanceof Error ? e.message : e);
|
|
5409
|
+
return null;
|
|
5410
|
+
}
|
|
5411
|
+
}
|
package/dist/core/strategies.js
CHANGED
|
@@ -51,6 +51,7 @@ function shouldForceBrowser(url) {
|
|
|
51
51
|
'glassdoor.com',
|
|
52
52
|
'bloomberg.com',
|
|
53
53
|
'indeed.com',
|
|
54
|
+
'yelp.com', // aggressive bot detection
|
|
54
55
|
'amazon.com', // captcha wall on simple/browser fetch
|
|
55
56
|
'zillow.com', // aggressive bot detection
|
|
56
57
|
'ticketmaster.com', // Distil Networks / PerimeterX
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webpeel",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.69",
|
|
4
4
|
"description": "Fast web fetcher for AI agents - stealth mode, crawl mode, page actions, structured extraction, PDF parsing, smart escalation from simple HTTP to headless browser",
|
|
5
5
|
"author": "Jake Liu",
|
|
6
6
|
"license": "AGPL-3.0-only",
|