mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-10-05 07:13:06 +08:00
fix: collect Shop by type thematic SP ads via redirect chains without data-asin
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7d446d8f54
commit
4c26b108a7
@ -931,6 +931,202 @@ async function main() {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shopByTypeDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGCAM01">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGCAM01"><img class="s-image" src="https://example.com/org-cam.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<video></video>
|
||||||
|
<a href="/stores/stealthcam"><span class="brand-logo">STEALTH CAM</span></a>
|
||||||
|
<div class="sb-headline">STEALTH CAM</div>
|
||||||
|
<div data-asin="B0STEALTH1" class="sbv-product">
|
||||||
|
<h2>Stealth Cam trail camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0STEALTH1">
|
||||||
|
<img src="https://example.com/stealth.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="s-widget-container celwidget slot=MAIN template=SHOP_BY_TYPE widget=shop-by-type-desktop"
|
||||||
|
cel_widget_id="MAIN-SHOP_BY_TYPE-8"
|
||||||
|
>
|
||||||
|
<h2>Shop cameras by type</h2>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>security system</h3>
|
||||||
|
<div>
|
||||||
|
<h2>Night Owl 2-Way Audio</h2>
|
||||||
|
<a href="https://redirect.amazon.com/x/c/abc123?url=%2Fdp%2FB0NIGHTOWL">
|
||||||
|
<img
|
||||||
|
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
|
||||||
|
data-src="https://example.com/nightowl-type.jpg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<span class="a-price"><span class="a-offscreen">$549.99</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>webcam</h3>
|
||||||
|
<div>
|
||||||
|
<h2>Prisual PTZ Camera</h2>
|
||||||
|
<a href="https://redirect.amazon.com/x/c/def456?url=%2Fdp%2FB0PRISUAL1">
|
||||||
|
<img
|
||||||
|
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
|
||||||
|
data-src="https://example.com/prisual-type.jpg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<span class="a-price"><span class="a-offscreen">$494.09</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>instant print</h3>
|
||||||
|
<div>
|
||||||
|
<h2>Kids Instant Print Camera</h2>
|
||||||
|
<a href="https://redirect.amazon.com/x/c/ghi789?url=%2Fdp%2FB0KIDINST1">
|
||||||
|
<img data-src="https://example.com/instant-type.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>point and shoot</h3>
|
||||||
|
<div>
|
||||||
|
<h2>Digital Camera 4K</h2>
|
||||||
|
<a href="https://redirect.amazon.com/x/c/jkl012?url=%2Fdp%2FB0POINTSH1">
|
||||||
|
<img data-src="https://example.com/pointshoot-type.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>film camera</h3>
|
||||||
|
<div>
|
||||||
|
<h2>Kodak EKTAR H35N</h2>
|
||||||
|
<a href="https://direct.amazon.com/x/c/QmToken/https://www.amazon.com/dp/B0KODAKH35">
|
||||||
|
<img data-src="https://example.com/kodak-type.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=camera' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: shopByTypeDom.window.document,
|
||||||
|
Node: shopByTypeDom.window.Node,
|
||||||
|
Element: shopByTypeDom.window.Element,
|
||||||
|
HTMLElement: shopByTypeDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: shopByTypeDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: shopByTypeDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const shopByType = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const shopByTypeByAsin = new Map(shopByType.map((item) => [item.asin, item]));
|
||||||
|
const shopByTypeAsins = ['B0NIGHTOWL', 'B0PRISUAL1', 'B0KIDINST1', 'B0POINTSH1', 'B0KODAKH35'];
|
||||||
|
const shopByTypeItems = shopByTypeAsins.map((asin) => shopByTypeByAsin.get(asin));
|
||||||
|
assert.ok(shopByTypeItems.filter(Boolean).length > 0, 'Shop by type cards must be collected');
|
||||||
|
assert.equal(shopByTypeItems.filter(Boolean).length, 5);
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0NIGHTOWL')?.asin, 'B0NIGHTOWL');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0NIGHTOWL')?.title, 'Night Owl 2-Way Audio');
|
||||||
|
assert.equal(
|
||||||
|
shopByTypeByAsin.get('B0NIGHTOWL')?.imageSrc,
|
||||||
|
'https://example.com/nightowl-type.jpg',
|
||||||
|
);
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0NIGHTOWL')?.price, '$549.99');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0PRISUAL1')?.asin, 'B0PRISUAL1');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0PRISUAL1')?.title, 'Prisual PTZ Camera');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0KIDINST1')?.asin, 'B0KIDINST1');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0KIDINST1')?.title, 'Kids Instant Print Camera');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0POINTSH1')?.asin, 'B0POINTSH1');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0POINTSH1')?.title, 'Digital Camera 4K');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0KODAKH35')?.asin, 'B0KODAKH35');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0KODAKH35')?.title, 'Kodak EKTAR H35N');
|
||||||
|
assert.ok(shopByTypeItems.every((item) => item?.asin && item.title && item.imageSrc));
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0ORGCAM01')?.adType, 'organic');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0STEALTH1')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0STEALTH1')?.adFormat, 'video');
|
||||||
|
assert.ok(shopByTypeItems.every((item) => item?.adType === 'sponsoredProducts'));
|
||||||
|
assert.equal(
|
||||||
|
shopByTypeByAsin.get('B0NIGHTOWL')?.placementId,
|
||||||
|
shopByTypeByAsin.get('B0PRISUAL1')?.placementId,
|
||||||
|
);
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0NIGHTOWL')?.adProductIndex, 1);
|
||||||
|
assert.equal(shopByTypeByAsin.get('B0PRISUAL1')?.adProductIndex, 2);
|
||||||
|
assert.equal(
|
||||||
|
shopByType.some((item) =>
|
||||||
|
/^(security system|webcam|instant print|point and shoot|film camera)$/i.test(item.title),
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const organicShopByTypeDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGTYPE0">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGTYPE0"><img class="s-image" src="https://example.com/org-type0.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="s-widget-container celwidget slot=MAIN template=SHOP_BY_TYPE widget=shop-by-type-desktop"
|
||||||
|
cel_widget_id="MAIN-SHOP_BY_TYPE-9"
|
||||||
|
>
|
||||||
|
<h2>Shop cameras by type</h2>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>webcam</h3>
|
||||||
|
<div data-asin="B0ORGTYPE1">
|
||||||
|
<h2>Organic type webcam</h2>
|
||||||
|
<a href="/dp/B0ORGTYPE1">
|
||||||
|
<img class="s-image" src="https://example.com/org-type1.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shop-by-type-column">
|
||||||
|
<h3>film camera</h3>
|
||||||
|
<div>
|
||||||
|
<h2>Organic type film camera</h2>
|
||||||
|
<a href="https://redirect.amazon.com/x/c/org?url=%2Fdp%2FB0ORGTYPE2">
|
||||||
|
<img data-src="https://example.com/org-type2.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=camera' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: organicShopByTypeDom.window.document,
|
||||||
|
Node: organicShopByTypeDom.window.Node,
|
||||||
|
Element: organicShopByTypeDom.window.Element,
|
||||||
|
HTMLElement: organicShopByTypeDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: organicShopByTypeDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: organicShopByTypeDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const organicShopByType = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const organicShopByTypeByAsin = new Map(organicShopByType.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE1')?.asin, 'B0ORGTYPE1');
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE1')?.title, 'Organic type webcam');
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE2')?.asin, 'B0ORGTYPE2');
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE2')?.title, 'Organic type film camera');
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE0')?.adType, 'organic');
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE1')?.adType, 'organic');
|
||||||
|
assert.equal(organicShopByTypeByAsin.get('B0ORGTYPE2')?.adType, 'organic');
|
||||||
|
assert.equal(
|
||||||
|
organicShopByType.some(
|
||||||
|
(item) => item.adType === 'sponsoredProducts' || item.adType === 'sponsoredBrands',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
console.log('Amazon search parser fixtures passed.');
|
console.log('Amazon search parser fixtures passed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,13 +68,15 @@ export async function waitForAmazonSearchPageReady() {
|
|||||||
.map((node) => node.dataset.asin || '')
|
.map((node) => node.dataset.asin || '')
|
||||||
.filter((asin) => /^[A-Z0-9]{10}$/.test(asin));
|
.filter((asin) => /^[A-Z0-9]{10}$/.test(asin));
|
||||||
const productLinks = Array.from(
|
const productLinks = Array.from(
|
||||||
root.querySelectorAll<HTMLAnchorElement>('a[href*="/dp/"], a[href*="%2Fdp%2F"]'),
|
root.querySelectorAll<HTMLAnchorElement>(
|
||||||
|
'a[href*="/dp/"], a[href*="%2Fdp%2F"], a[href*="redirect.amazon.com"], a[href*="direct.amazon.com"], a[href*="/x/c/"]',
|
||||||
|
),
|
||||||
).map((anchor) => anchor.href);
|
).map((anchor) => anchor.href);
|
||||||
const sponsoredLabels = root.querySelectorAll(
|
const sponsoredLabels = root.querySelectorAll(
|
||||||
'[aria-label*="Sponsored" i], [title*="Sponsored" i]',
|
'[aria-label*="Sponsored" i], [title*="Sponsored" i]',
|
||||||
).length;
|
).length;
|
||||||
const cards = root.querySelectorAll(
|
const cards = root.querySelectorAll(
|
||||||
'[data-component-type="s-search-result"], .puis-card-container, .puisg-row',
|
'[data-component-type="s-search-result"], .puis-card-container, .puisg-row, [class*="shop-by-type"]',
|
||||||
).length;
|
).length;
|
||||||
return [
|
return [
|
||||||
Array.from(new Set(asins)).sort().join(','),
|
Array.from(new Set(asins)).sort().join(','),
|
||||||
|
|||||||
@ -231,6 +231,14 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasShopByTypeCopy = (node: Element) =>
|
||||||
|
Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3, span, a, [class*="headline"]')).some(
|
||||||
|
(candidate) => {
|
||||||
|
if (closestAsinHost(candidate) || isVisuallyHiddenText(candidate)) return false;
|
||||||
|
return /\bshop\b.+\bby type\b/i.test(text(candidate));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const hasThematicSpSignal = (node: Element) => {
|
const hasThematicSpSignal = (node: Element) => {
|
||||||
const blob = [
|
const blob = [
|
||||||
metadata(node),
|
metadata(node),
|
||||||
@ -244,13 +252,13 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
/featured.?asins.?list|sponsoredproductsloom|sp_search_thematic|loom-desktop|influencer/.test(
|
/featured.?asins.?list|sponsoredproductsloom|sp_search_thematic|loom-desktop|influencer|shop.?by.?type|shop_by_type/.test(
|
||||||
blob,
|
blob,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return hasInfluencerThematicCopy(node);
|
return hasInfluencerThematicCopy(node) || hasShopByTypeCopy(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSponsoredProductsThematic = (node: Element) => {
|
const isSponsoredProductsThematic = (node: Element) => {
|
||||||
@ -364,7 +372,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
const hasCollectableProductLink = (node: Element) =>
|
const hasCollectableProductLink = (node: Element) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
node.querySelector(
|
node.querySelector(
|
||||||
'a[href*="/dp/"], a[href*="%2Fdp%2F"], a[href*="/gp/product"], a[href*="%2Fgp%2Fproduct%2F"], a[href*="sspa/click"], a[href*="lp_asins="]',
|
'a[href*="/dp/"], a[href*="%2Fdp%2F"], a[href*="/gp/product"], a[href*="%2Fgp%2Fproduct%2F"], a[href*="sspa/click"], a[href*="lp_asins="], a[href*="redirect.amazon.com"], a[href*="direct.amazon.com"], a[href*="/x/c/"]',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -379,6 +387,20 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProductCardFromAnchor = (anchor: HTMLAnchorElement, module: HTMLElement) => {
|
||||||
|
const asinHost = closestAsinHost(anchor);
|
||||||
|
if (asinHost && module.contains(asinHost) && asinHost !== module) return asinHost;
|
||||||
|
|
||||||
|
let current: HTMLElement | null = anchor.parentElement;
|
||||||
|
while (current && current !== module) {
|
||||||
|
if (current.querySelector('img') && current.querySelector('h2, h3')) return current;
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return anchor.parentElement instanceof HTMLElement && anchor.parentElement !== module
|
||||||
|
? anchor.parentElement
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
const candidateSet = new Set<HTMLElement>();
|
const candidateSet = new Set<HTMLElement>();
|
||||||
document
|
document
|
||||||
.querySelectorAll<HTMLElement>(
|
.querySelectorAll<HTMLElement>(
|
||||||
@ -434,6 +456,50 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
addCandidate(module);
|
addCandidate(module);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const collectThematicModuleCards = (module: HTMLElement) => {
|
||||||
|
module.querySelectorAll<HTMLElement>('[data-asin]').forEach(addCandidate);
|
||||||
|
module.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((anchor) => {
|
||||||
|
const href = anchor.getAttribute('href') || anchor.href;
|
||||||
|
if (!parseAsinFromText(anchor.href) && !parseAsinFromText(href)) return;
|
||||||
|
const card = findProductCardFromAnchor(anchor, module);
|
||||||
|
if (card) addCandidate(card);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLElement>(
|
||||||
|
'.s-widget-container, [class*="celwidget"], [class*="s-widget"], [class*="shop-by-type"], [cel_widget_id*="SHOP_BY_TYPE" i], [cel_widget_id*="shop-by-type" i]',
|
||||||
|
)
|
||||||
|
.forEach((module) => {
|
||||||
|
if (!isVisible(module) || !isSponsoredProductsThematic(module)) return;
|
||||||
|
collectThematicModuleCards(module);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll<HTMLElement>('h1, h2, h3, [class*="headline"]').forEach((heading) => {
|
||||||
|
if (closestAsinHost(heading) || !/\bshop\b.+\bby type\b/i.test(text(heading))) return;
|
||||||
|
const module =
|
||||||
|
heading.closest<HTMLElement>(
|
||||||
|
'.s-widget-container, [class*="celwidget"], [class*="s-widget"], section',
|
||||||
|
) || heading.parentElement;
|
||||||
|
if (module instanceof HTMLElement && isVisible(module)) collectThematicModuleCards(module);
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLAnchorElement>(
|
||||||
|
'a[href*="redirect.amazon.com"], a[href*="direct.amazon.com"], a[href*="/x/c/"]',
|
||||||
|
)
|
||||||
|
.forEach((anchor) => {
|
||||||
|
if (!isVisible(anchor)) return;
|
||||||
|
const href = anchor.getAttribute('href') || anchor.href;
|
||||||
|
if (!parseAsinFromText(anchor.href) && !parseAsinFromText(href)) return;
|
||||||
|
const module =
|
||||||
|
anchor.closest<HTMLElement>(
|
||||||
|
'.s-widget-container, [class*="celwidget"], [class*="s-widget"], [class*="shop-by-type"]',
|
||||||
|
) || document.body;
|
||||||
|
const card = findProductCardFromAnchor(anchor, module);
|
||||||
|
if (card) addCandidate(card);
|
||||||
|
});
|
||||||
|
|
||||||
const cards = Array.from(candidateSet)
|
const cards = Array.from(candidateSet)
|
||||||
.filter(
|
.filter(
|
||||||
(card, index, all) =>
|
(card, index, all) =>
|
||||||
@ -468,7 +534,9 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
const asin = parseAsin(card, productAnchor?.href);
|
const asin = parseAsin(card, productAnchor?.href);
|
||||||
if (!asin) return items;
|
if (!asin) return items;
|
||||||
|
|
||||||
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3');
|
const titleNode =
|
||||||
|
card.querySelector<HTMLElement>('h2[aria-label], h2') ||
|
||||||
|
card.querySelector<HTMLElement>('h3[aria-label], h3');
|
||||||
const imageNode = card.querySelector<HTMLImageElement>(
|
const imageNode = card.querySelector<HTMLImageElement>(
|
||||||
'img.s-image, img[src], img[data-src], img[srcset]',
|
'img.s-image, img[src], img[data-src], img[srcset]',
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user