mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-10-05 07:13:06 +08:00
fix: classify Amazon sponsored brand and video search ads
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
72b0c348a9
commit
7d446d8f54
@ -216,6 +216,721 @@ async function main() {
|
||||
assert.equal(groupedAdsByAsin.get('B000000014')?.adType, 'sponsoredDisplay');
|
||||
assert.equal(groupedAdsByAsin.get('B000000014')?.placement, 'sidebar');
|
||||
|
||||
const liveSponsoredBrandsDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
|
||||
<div class="sb-headline">Arlo Security for any Home</div>
|
||||
<span>Sponsored</span>
|
||||
<div data-asin="B08AR2S6MC">
|
||||
<span>Video Doorbell</span>
|
||||
<a href="/sspa/click?ie=UTF8&url=%2Fdp%2FB08AR2S6MC">
|
||||
<img data-src="https://example.com/arlo-1.jpg" src="https://example.com/arlo-1.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
<div data-asin="B0ARLO0002">
|
||||
<span>Outdoor Camera</span>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0ARLO0002">
|
||||
<img src="https://example.com/arlo-2.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
<div data-asin="B0ARLO0003">
|
||||
<span>Bundle</span>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0ARLO0003">
|
||||
<img src="https://example.com/arlo-3.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<article data-component-type="s-search-result" data-asin="B09RING001">
|
||||
<span>Sponsored</span>
|
||||
<h2>Ring Spotlight Cam Plus, Battery</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB09RING001"><img class="s-image" src="https://example.com/ring.jpg" /></a>
|
||||
</article>
|
||||
<article data-component-type="s-search-result" data-asin="B09ORG0001">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B09ORG0001"><img class="s-image" src="https://example.com/org.jpg" /></a>
|
||||
</article>
|
||||
<div class="sbv-container AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<video></video>
|
||||
<div class="sbv-copy">PROTECT YOUR EQUIPMENT & FACILITIES</div>
|
||||
<div data-asin="B0VOSKER01" class="sbv-product">
|
||||
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0VOSKER01"><img src="https://example.com/vosker.jpg" /></a>
|
||||
<span class="a-price"><span class="a-offscreen">$379.99</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<section class="brands-related-to-your-search AdHolder">
|
||||
<h2>Brands related to your search</h2>
|
||||
<span>Sponsored</span>
|
||||
<div class="brand-card" data-asin="B0BRAND001">
|
||||
<img src="https://example.com/vosker-brand.jpg" alt="VOSKER" />
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0BRAND001">VOSKER</a>
|
||||
</div>
|
||||
<div class="brand-card">
|
||||
<img src="https://example.com/reolink-brand.jpg" alt="Reolink" />
|
||||
<a href="/sspa/click?url=%2Fgp%2Fproduct%2FB0BRAND002">Reolink</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: liveSponsoredBrandsDom.window.document,
|
||||
Node: liveSponsoredBrandsDom.window.Node,
|
||||
Element: liveSponsoredBrandsDom.window.Element,
|
||||
HTMLElement: liveSponsoredBrandsDom.window.HTMLElement,
|
||||
HTMLAnchorElement: liveSponsoredBrandsDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: liveSponsoredBrandsDom.window.HTMLImageElement,
|
||||
});
|
||||
const liveSponsoredBrands = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const liveByAsin = new Map(liveSponsoredBrands.map((item) => [item.asin, item]));
|
||||
assert.equal(liveByAsin.get('B08AR2S6MC')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveByAsin.get('B0ARLO0002')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveByAsin.get('B0ARLO0003')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveByAsin.get('B08AR2S6MC')?.placement, 'topOfSearch');
|
||||
assert.equal(liveByAsin.get('B09RING001')?.adType, 'sponsoredProducts');
|
||||
assert.equal(liveByAsin.get('B09ORG0001')?.adType, 'organic');
|
||||
assert.equal(liveByAsin.get('B0VOSKER01')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveByAsin.get('B0VOSKER01')?.adFormat, 'video');
|
||||
assert.equal(liveByAsin.get('B0VOSKER01')?.placement, 'bottomOfSearch');
|
||||
assert.equal(liveByAsin.get('B0BRAND001')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveByAsin.get('B0BRAND002')?.adType, 'sponsoredBrands');
|
||||
|
||||
const lazyImageDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<div class="s-widget AdHolder">
|
||||
<div class="sb-headline">Arlo Security for any Home</div>
|
||||
<span>Sponsored</span>
|
||||
<div data-asin="B08AR2LAZY">
|
||||
<h3>Video Doorbell</h3>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB08AR2LAZY">
|
||||
<img data-src="https://example.com/arlo-lazy.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: lazyImageDom.window.document,
|
||||
Node: lazyImageDom.window.Node,
|
||||
Element: lazyImageDom.window.Element,
|
||||
HTMLElement: lazyImageDom.window.HTMLElement,
|
||||
HTMLAnchorElement: lazyImageDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: lazyImageDom.window.HTMLImageElement,
|
||||
});
|
||||
const lazyImageItems = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
assert.equal(lazyImageItems.length, 1);
|
||||
assert.equal(lazyImageItems[0]?.asin, 'B08AR2LAZY');
|
||||
assert.equal(lazyImageItems[0]?.adType, 'sponsoredBrands');
|
||||
assert.equal(lazyImageItems[0]?.imageSrc, 'https://example.com/arlo-lazy.jpg');
|
||||
|
||||
const classificationBoundaryDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
|
||||
<div class="sb-headline">Arlo Security for any Home</div>
|
||||
<span>Sponsored</span>
|
||||
<div data-asin="B08ARLOSB1">
|
||||
<span>Video Doorbell</span>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB08ARLOSB1">
|
||||
<img src="https://example.com/arlo-banner-1.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
<div data-asin="B08ARLOSB2">
|
||||
<span>Outdoor Camera</span>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB08ARLOSB2">
|
||||
<img src="https://example.com/arlo-banner-2.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-widget AdHolder" data-cel-widget="search-grid-wrap">
|
||||
<article data-component-type="s-search-result" data-asin="B0ARLOGRID" class="AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<h2>Arlo Video Doorbell Wired</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0ARLOGRID">
|
||||
<img class="s-image" src="https://example.com/arlo-grid.jpg" />
|
||||
</a>
|
||||
</article>
|
||||
<article data-component-type="s-search-result" data-asin="B09RINGSP1" class="AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<h2>Ring Spotlight Cam Plus, Battery</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB09RINGSP1">
|
||||
<img class="s-image" src="https://example.com/ring-grid.jpg" />
|
||||
</a>
|
||||
</article>
|
||||
<article data-component-type="s-search-result" data-asin="B09ORGKEEP">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B09ORGKEEP"><img class="s-image" src="https://example.com/org-keep.jpg" /></a>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article data-component-type="s-search-result" data-asin="B0TRACKSP1" class="AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<h2>Arlo Go 2 LTE Security Camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0TRACKSP1">
|
||||
<img class="s-image" src="https://example.com/arlo-track.jpg" />
|
||||
</a>
|
||||
<a href="/gp/product/B0TRACKXX2">related tracking</a>
|
||||
</article>
|
||||
|
||||
<article data-component-type="s-search-result" data-asin="B0DEMOSP01" class="AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<h2>Ring Indoor Cam with demo</h2>
|
||||
<video></video>
|
||||
<a href="/dp/B0DEMOSP01">
|
||||
<img class="s-image" src="https://example.com/ring-demo.jpg" />
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<div class="sbv-container AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<video></video>
|
||||
<div class="sbv-copy">PROTECT YOUR EQUIPMENT & FACILITIES</div>
|
||||
<div data-asin="B0VOSKER02" class="sbv-product">
|
||||
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0VOSKER02">
|
||||
<img src="https://example.com/vosker-2.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article data-component-type="s-search-result" data-asin="B0NOADVID1">
|
||||
<h2>Organic with product video</h2>
|
||||
<video></video>
|
||||
<a href="/dp/B0NOADVID1">
|
||||
<img class="s-image" src="https://example.com/organic-video.jpg" />
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<section class="brands-related-to-your-search AdHolder">
|
||||
<h2>Brands related to your search</h2>
|
||||
<span>Sponsored</span>
|
||||
<div class="brand-card" data-asin="B0BRANDWL1">
|
||||
<img src="https://example.com/vosker-wall.jpg" alt="VOSKER" />
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0BRANDWL1">VOSKER</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<aside id="rightResultsATF" data-component-type="sponsored-display">
|
||||
<span>Sponsored</span>
|
||||
<article data-asin="B0SIDEBAR1">
|
||||
<h3>Sidebar display product</h3>
|
||||
<a href="/dp/B0SIDEBAR1"><img src="https://example.com/sidebar.jpg" /></a>
|
||||
</article>
|
||||
</aside>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: classificationBoundaryDom.window.document,
|
||||
Node: classificationBoundaryDom.window.Node,
|
||||
Element: classificationBoundaryDom.window.Element,
|
||||
HTMLElement: classificationBoundaryDom.window.HTMLElement,
|
||||
HTMLAnchorElement: classificationBoundaryDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: classificationBoundaryDom.window.HTMLImageElement,
|
||||
});
|
||||
const classificationBoundary = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const boundaryByAsin = new Map(classificationBoundary.map((item) => [item.asin, item]));
|
||||
assert.equal(boundaryByAsin.get('B08ARLOSB1')?.adType, 'sponsoredBrands');
|
||||
assert.equal(boundaryByAsin.get('B08ARLOSB2')?.adType, 'sponsoredBrands');
|
||||
assert.equal(boundaryByAsin.get('B0ARLOGRID')?.adType, 'sponsoredProducts');
|
||||
assert.equal(boundaryByAsin.get('B09RINGSP1')?.adType, 'sponsoredProducts');
|
||||
assert.equal(boundaryByAsin.get('B09ORGKEEP')?.adType, 'organic');
|
||||
assert.equal(boundaryByAsin.get('B0TRACKSP1')?.adType, 'sponsoredProducts');
|
||||
assert.equal(boundaryByAsin.has('B0TRACKXX2'), false);
|
||||
assert.equal(boundaryByAsin.get('B0DEMOSP01')?.adType, 'sponsoredProducts');
|
||||
assert.equal(boundaryByAsin.get('B0VOSKER02')?.adType, 'sponsoredBrands');
|
||||
assert.equal(boundaryByAsin.get('B0VOSKER02')?.adFormat, 'video');
|
||||
assert.equal(boundaryByAsin.get('B0NOADVID1')?.adType, 'organic');
|
||||
assert.equal(boundaryByAsin.get('B0BRANDWL1')?.adType, 'sponsoredBrands');
|
||||
assert.equal(boundaryByAsin.get('B0SIDEBAR1')?.adType, 'sponsoredDisplay');
|
||||
assert.equal(boundaryByAsin.get('B0SIDEBAR1')?.placement, 'sidebar');
|
||||
|
||||
const thematicLoomDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<article data-component-type="s-search-result" data-asin="B0ORGLOOM1">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B0ORGLOOM1"><img class="s-image" src="https://example.com/org-loom.jpg" /></a>
|
||||
</article>
|
||||
|
||||
<div
|
||||
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-trending-hardlines-bottom-na-page2plus-t1"
|
||||
cel_widget_id="MAIN-FEATURED_ASINS_LIST-22"
|
||||
>
|
||||
<div class="a-section s-include-content-margin">
|
||||
<h2 id="loom-desktop-bottom-slot_featuredasins-heading">Trending now</h2>
|
||||
<span
|
||||
data-action="multi-ad-feedback-form-trigger"
|
||||
data-multi-ad-feedback-form-trigger='{"adPlacementMetaData":{"slotName":"sp_search_thematic_btf-trending-hardlines","feedbackType":"sponsoredProductsLoom"}}'
|
||||
>
|
||||
<a aria-label="View Sponsored information or leave ad feedback" class="s-widget-sponsored-label-text">Sponsored</a>
|
||||
</span>
|
||||
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
|
||||
<ol class="a-carousel">
|
||||
<li class="a-carousel-card" aria-hidden="false">
|
||||
<div data-asin="B0B2MPYRKP" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>iulock Keypad Smart Door Lock</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0B2MPYRKP&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/iulock.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="a-carousel-card" aria-hidden="false">
|
||||
<div data-asin="B0F8HS3RRT" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>Richelock fingerprint lock</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0F8HS3RRT&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/richelock.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="a-carousel-card" aria-hidden="false">
|
||||
<div data-asin="B0GQX6GHK2" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>Swann MaxRanger4K</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0GQX6GHK2&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/swann.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="a-carousel-card" aria-hidden="false" style="visibility: hidden;">
|
||||
<div data-asin="B0CN95HR9V" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>Solar Fence Lights</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0CN95HR9V&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/solar.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="a-carousel-card" aria-hidden="true" style="visibility: hidden;">
|
||||
<div data-asin="B07M68S376" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>SECRUI Wireless Doorbell</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB07M68S376&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/secrui.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sbv-container AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<video></video>
|
||||
<div class="sbv-copy">PROTECT YOUR EQUIPMENT & FACILITIES</div>
|
||||
<div data-asin="B0VOSKERLM" class="sbv-product">
|
||||
<h2>Vosker VKX Solar Powered camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERLM"><img src="https://example.com/vosker-loom.jpg" /></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
|
||||
<div class="sb-headline">Arlo Security for any Home</div>
|
||||
<span>Sponsored</span>
|
||||
<div data-asin="B08ARLOLM1">
|
||||
<span>Video Doorbell</span>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB08ARLOLM1">
|
||||
<img src="https://example.com/arlo-loom.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: thematicLoomDom.window.document,
|
||||
Node: thematicLoomDom.window.Node,
|
||||
Element: thematicLoomDom.window.Element,
|
||||
HTMLElement: thematicLoomDom.window.HTMLElement,
|
||||
HTMLAnchorElement: thematicLoomDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: thematicLoomDom.window.HTMLImageElement,
|
||||
});
|
||||
const thematicLoom = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const thematicByAsin = new Map(thematicLoom.map((item) => [item.asin, item]));
|
||||
assert.equal(thematicByAsin.get('B0ORGLOOM1')?.adType, 'organic');
|
||||
assert.equal(thematicByAsin.get('B0B2MPYRKP')?.adType, 'sponsoredProducts');
|
||||
assert.equal(thematicByAsin.get('B0F8HS3RRT')?.adType, 'sponsoredProducts');
|
||||
assert.equal(thematicByAsin.get('B0GQX6GHK2')?.adType, 'sponsoredProducts');
|
||||
assert.equal(
|
||||
thematicByAsin.get('B0B2MPYRKP')?.placementId,
|
||||
thematicByAsin.get('B0F8HS3RRT')?.placementId,
|
||||
);
|
||||
assert.equal(thematicByAsin.get('B0B2MPYRKP')?.adProductIndex, 1);
|
||||
assert.equal(thematicByAsin.get('B0F8HS3RRT')?.adProductIndex, 2);
|
||||
assert.equal(thematicByAsin.get('B0GQX6GHK2')?.adProductIndex, 3);
|
||||
assert.equal(thematicByAsin.has('B0CN95HR9V'), false);
|
||||
assert.equal(thematicByAsin.has('B07M68S376'), false);
|
||||
assert.equal(thematicByAsin.get('B0VOSKERLM')?.adType, 'sponsoredBrands');
|
||||
assert.equal(thematicByAsin.get('B0VOSKERLM')?.adFormat, 'video');
|
||||
assert.equal(thematicByAsin.get('B08ARLOLM1')?.adType, 'sponsoredBrands');
|
||||
|
||||
const organicLoomDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<article data-component-type="s-search-result" data-asin="B0ORGCFV01">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B0ORGCFV01"><img class="s-image" src="https://example.com/org-cfv.jpg" /></a>
|
||||
</article>
|
||||
|
||||
<div
|
||||
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-frequently-viewed-bottom-na"
|
||||
cel_widget_id="MAIN-FEATURED_ASINS_LIST-23"
|
||||
>
|
||||
<div class="a-section s-include-content-margin">
|
||||
<h2 id="loom-desktop-bottom-slot_featuredasins-heading">Customers frequently viewed</h2>
|
||||
<span
|
||||
data-action="multi-ad-feedback-form-trigger"
|
||||
data-multi-ad-feedback-form-trigger='{"adPlacementMetaData":{"slotName":"sp_search_thematic_btf-frequently-viewed","feedbackType":"sponsoredProductsLoom"}}'
|
||||
></span>
|
||||
<span
|
||||
aria-label="Sponsored"
|
||||
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
|
||||
>Sponsored</span>
|
||||
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
|
||||
<ol class="a-carousel">
|
||||
<li class="a-carousel-card" aria-hidden="false">
|
||||
<div data-asin="B0CFV00001" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>ANNKE security camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0CFV00001&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/annke.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="a-carousel-card" aria-hidden="false">
|
||||
<div data-asin="B0CFV00002" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>Night Owl camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0CFV00002&widgetName=sp_search_thematic_btf">
|
||||
<img class="s-image" src="https://example.com/nightowl.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-recently-bought-bottom-na"
|
||||
cel_widget_id="MAIN-FEATURED_ASINS_LIST-24"
|
||||
>
|
||||
<h2>Recently bought and rated</h2>
|
||||
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
|
||||
<ol class="a-carousel">
|
||||
<li class="a-carousel-card" aria-hidden="false">
|
||||
<div data-asin="B0RBR00001" class="s-result-item">
|
||||
<div class="puis-card-container">
|
||||
<h2>Rated doorbell</h2>
|
||||
<a href="/dp/B0RBR00001">
|
||||
<img class="s-image" src="https://example.com/rated.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: organicLoomDom.window.document,
|
||||
Node: organicLoomDom.window.Node,
|
||||
Element: organicLoomDom.window.Element,
|
||||
HTMLElement: organicLoomDom.window.HTMLElement,
|
||||
HTMLAnchorElement: organicLoomDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: organicLoomDom.window.HTMLImageElement,
|
||||
});
|
||||
const organicLoom = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const organicLoomByAsin = new Map(organicLoom.map((item) => [item.asin, item]));
|
||||
assert.equal(organicLoomByAsin.get('B0ORGCFV01')?.adType, 'organic');
|
||||
assert.equal(organicLoomByAsin.get('B0CFV00001')?.adType, 'organic');
|
||||
assert.equal(organicLoomByAsin.get('B0CFV00002')?.adType, 'organic');
|
||||
assert.equal(organicLoomByAsin.get('B0RBR00001')?.adType, 'organic');
|
||||
assert.equal(
|
||||
organicLoom.some((item) => item.adType === 'sponsoredProducts'),
|
||||
false,
|
||||
);
|
||||
|
||||
const organicVideoStripDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<article data-component-type="s-search-result" data-asin="B0ORGVID01">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B0ORGVID01"><img class="s-image" src="https://example.com/org-vid.jpg" /></a>
|
||||
</article>
|
||||
|
||||
<div class="sbv-container AdHolder">
|
||||
<video></video>
|
||||
<a href="/stores/chamberlain"><span class="brand-logo">Chamberlain</span></a>
|
||||
<div class="sb-headline">Chamberlain myQ</div>
|
||||
<div class="sbv-copy">Control your garage from anywhere</div>
|
||||
<div data-asin="B0NOSBV001" class="sbv-product">
|
||||
<h2>Chamberlain garage opener</h2>
|
||||
<a href="/dp/B0NOSBV001"><img src="https://example.com/chamberlain-nosponsored.jpg" /></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sbv-container AdHolder">
|
||||
<span
|
||||
aria-label="Sponsored"
|
||||
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
|
||||
>Sponsored</span>
|
||||
<video></video>
|
||||
<a href="/stores/vosker"><span class="brand-logo">Vosker</span></a>
|
||||
<div class="sbv-copy">PROTECT YOUR EQUIPMENT</div>
|
||||
<div data-asin="B0HIDDENSB" class="sbv-product">
|
||||
<h2>Vosker solar camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0HIDDENSB"><img src="https://example.com/vosker-hidden.jpg" /></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sbv-container AdHolder">
|
||||
<span>Sponsored</span>
|
||||
<video></video>
|
||||
<a href="/stores/vosker"><span class="brand-logo">Vosker</span></a>
|
||||
<div class="sbv-copy">PROTECT YOUR EQUIPMENT</div>
|
||||
<div data-asin="B0YESBV001" class="sbv-product">
|
||||
<h2>Vosker with visible sponsored</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0YESBV001"><img src="https://example.com/vosker-yes.jpg" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: organicVideoStripDom.window.document,
|
||||
Node: organicVideoStripDom.window.Node,
|
||||
Element: organicVideoStripDom.window.Element,
|
||||
HTMLElement: organicVideoStripDom.window.HTMLElement,
|
||||
HTMLAnchorElement: organicVideoStripDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: organicVideoStripDom.window.HTMLImageElement,
|
||||
});
|
||||
const organicVideoStrip = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const videoStripByAsin = new Map(organicVideoStrip.map((item) => [item.asin, item]));
|
||||
assert.equal(videoStripByAsin.get('B0ORGVID01')?.adType, 'organic');
|
||||
assert.equal(videoStripByAsin.get('B0NOSBV001')?.adType, 'organic');
|
||||
assert.equal(videoStripByAsin.get('B0HIDDENSB')?.adType, 'organic');
|
||||
assert.equal(videoStripByAsin.get('B0YESBV001')?.adType, 'sponsoredBrands');
|
||||
assert.equal(videoStripByAsin.get('B0YESBV001')?.adFormat, 'video');
|
||||
assert.deepEqual(
|
||||
organicVideoStrip.filter((item) => item.adType === 'sponsoredBrands').map((item) => item.asin),
|
||||
['B0YESBV001'],
|
||||
);
|
||||
|
||||
const liveBrandWallDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<article data-component-type="s-search-result" data-asin="B0ORGGRID1">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B0ORGGRID1"><img class="s-image" src="https://example.com/org-grid.jpg" /></a>
|
||||
</article>
|
||||
|
||||
<div
|
||||
class="s-result-item s-widget AdHolder s-flex-full-width"
|
||||
data-asin=""
|
||||
cel_widget_id="multi-brand-creative-desktop_loom-desktop-brand-footer-slot_46"
|
||||
>
|
||||
<span aria-label="Brands related to your search" role="heading">Brands related to your search</span>
|
||||
<span aria-label="Leave feedback on Sponsored ad" tabindex="0" role="button">Sponsored</span>
|
||||
<div class="sbx-desktop" data-aid="301209426575402" data-slot="desktop-hsa-3psl">
|
||||
<a href="/stores/page/VOSKER?lp_asins=B0D5Z3V464%2CB0FY2QCXRJ%2CB0FY2MRK56&lp_query=home%20security&lp_slot=desktop-hsa-3psl">
|
||||
<img
|
||||
alt="Vosker"
|
||||
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
|
||||
data-src="https://example.com/vosker-wall.jpg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sbx-desktop" data-aid="301252396855702" data-slot="desktop-hsa-3psl">
|
||||
<a href="/stores/page/REOLINK?lp_asins=B0C2PY2KTJ%2CB0FM45MSS8&lp_slot=desktop-hsa-3psl">
|
||||
<img
|
||||
alt="REOLINK"
|
||||
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
|
||||
data-src="https://example.com/reolink-wall.jpg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: liveBrandWallDom.window.document,
|
||||
Node: liveBrandWallDom.window.Node,
|
||||
Element: liveBrandWallDom.window.Element,
|
||||
HTMLElement: liveBrandWallDom.window.HTMLElement,
|
||||
HTMLAnchorElement: liveBrandWallDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: liveBrandWallDom.window.HTMLImageElement,
|
||||
});
|
||||
const liveBrandWall = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const liveBrandWallByAsin = new Map(liveBrandWall.map((item) => [item.asin, item]));
|
||||
assert.equal(liveBrandWallByAsin.get('B0ORGGRID1')?.adType, 'organic');
|
||||
assert.equal(liveBrandWallByAsin.get('B0D5Z3V464')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveBrandWallByAsin.get('B0C2PY2KTJ')?.adType, 'sponsoredBrands');
|
||||
assert.equal(liveBrandWallByAsin.get('B0D5Z3V464')?.title, 'Vosker');
|
||||
assert.equal(
|
||||
liveBrandWallByAsin.get('B0D5Z3V464')?.imageSrc,
|
||||
'https://example.com/vosker-wall.jpg',
|
||||
);
|
||||
assert.equal(
|
||||
liveBrandWallByAsin.get('B0C2PY2KTJ')?.imageSrc,
|
||||
'https://example.com/reolink-wall.jpg',
|
||||
);
|
||||
assert.equal(liveBrandWallByAsin.has('B0FY2QCXRJ'), false);
|
||||
|
||||
const influencerPicksDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<article data-component-type="s-search-result" data-asin="B0ORGINF01">
|
||||
<h2>Organic camera</h2>
|
||||
<a href="/dp/B0ORGINF01"><img class="s-image" src="https://example.com/org-inf.jpg" /></a>
|
||||
</article>
|
||||
|
||||
<section class="s-widget-container celwidget">
|
||||
<h2>Picks from Amazon Influencers</h2>
|
||||
<span>Sponsored</span>
|
||||
<span>From Amazon Influencer storefronts</span>
|
||||
<div data-asin="B0ANNKE001" class="puis-card-container">
|
||||
<h2>ANNKE 4K PoE Security Camera System</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0ANNKE001"><img src="https://example.com/annke.jpg" /></a>
|
||||
</div>
|
||||
<div data-asin="B0NIGHTO01" class="puis-card-container">
|
||||
<h2>Night Owl DVR</h2>
|
||||
<a href="/shop/nightowlstorefront"><span>storefront</span></a>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0NIGHTO01"><img src="https://example.com/nightowl.jpg" /></a>
|
||||
</div>
|
||||
<div data-asin="B0SOLAR001" class="puis-card-container">
|
||||
<h2>Solar Street Lights</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0SOLAR001"><img src="https://example.com/solar.jpg" /></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="s-widget AdHolder">
|
||||
<div class="creative-collage">
|
||||
<img src="https://example.com/jobsite.jpg" alt="jobsite" />
|
||||
<img src="https://example.com/camera-closeup.jpg" alt="camera" />
|
||||
<button aria-label="Pause"></button>
|
||||
<img src="https://example.com/packaging.jpg" alt="packaging" />
|
||||
</div>
|
||||
<div data-asin="B0VOSKERVK">
|
||||
<span>Sponsored</span>
|
||||
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERVK">
|
||||
<img src="https://example.com/vosker-vkx.jpg" />
|
||||
</a>
|
||||
<span class="a-price"><span class="a-offscreen">$379.99</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sbv-container AdHolder">
|
||||
<div class="sbv-creative">
|
||||
<img src="https://example.com/jobsite2.jpg" alt="PROTECT YOUR INVESTMENT" />
|
||||
<img src="https://example.com/closeup2.jpg" alt="CELLULAR SECURITY CAMERA" />
|
||||
<button class="sbv-pause" aria-label="Pause"></button>
|
||||
</div>
|
||||
<div data-asin="B0VOSKERSB" class="sbv-product">
|
||||
<span>Sponsored</span>
|
||||
<h2>Vosker VKX Solar Powered camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERSB">
|
||||
<img src="https://example.com/vosker-sb.jpg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article data-component-type="s-search-result" data-asin="B0VIEWVOSK">
|
||||
<span>Sponsored</span>
|
||||
<span class="previously-viewed-badge">Previously viewed</span>
|
||||
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0VIEWVOSK">
|
||||
<img class="s-image" src="https://example.com/vosker-grid.jpg" />
|
||||
</a>
|
||||
<span class="a-price"><span class="a-offscreen">$149.99</span></span>
|
||||
</article>
|
||||
<article data-component-type="s-search-result" data-asin="B0OWLLABS1">
|
||||
<span>Sponsored</span>
|
||||
<h2>Owl Labs Meeting Owl 3</h2>
|
||||
<a href="/sspa/click?url=%2Fdp%2FB0OWLLABS1">
|
||||
<img class="s-image" src="https://example.com/owl.jpg" />
|
||||
</a>
|
||||
<span class="a-price"><span class="a-offscreen">$1,099.00</span></span>
|
||||
</article>
|
||||
|
||||
<div class="s-related-searches AdHolder">
|
||||
<h2>Related searches</h2>
|
||||
<a href="/s?k=home+security+camera">home security camera</a>
|
||||
<a href="/s?k=outdoor+camera">outdoor camera</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: influencerPicksDom.window.document,
|
||||
Node: influencerPicksDom.window.Node,
|
||||
Element: influencerPicksDom.window.Element,
|
||||
HTMLElement: influencerPicksDom.window.HTMLElement,
|
||||
HTMLAnchorElement: influencerPicksDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: influencerPicksDom.window.HTMLImageElement,
|
||||
});
|
||||
const influencerPicks = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const influencerByAsin = new Map(influencerPicks.map((item) => [item.asin, item]));
|
||||
assert.equal(influencerByAsin.get('B0ORGINF01')?.adType, 'organic');
|
||||
assert.equal(influencerByAsin.get('B0ANNKE001')?.adType, 'sponsoredProducts');
|
||||
assert.equal(influencerByAsin.get('B0NIGHTO01')?.adType, 'sponsoredProducts');
|
||||
assert.equal(influencerByAsin.get('B0SOLAR001')?.adType, 'sponsoredProducts');
|
||||
assert.equal(
|
||||
influencerByAsin.get('B0ANNKE001')?.placementId,
|
||||
influencerByAsin.get('B0NIGHTO01')?.placementId,
|
||||
);
|
||||
assert.equal(influencerByAsin.get('B0VOSKERVK')?.adType, 'sponsoredBrands');
|
||||
assert.equal(influencerByAsin.get('B0VOSKERVK')?.adFormat, 'standard');
|
||||
assert.equal(influencerByAsin.get('B0VOSKERSB')?.adType, 'sponsoredBrands');
|
||||
assert.equal(influencerByAsin.get('B0VIEWVOSK')?.adType, 'sponsoredProducts');
|
||||
assert.equal(influencerByAsin.get('B0OWLLABS1')?.adType, 'sponsoredProducts');
|
||||
assert.equal(
|
||||
influencerPicks.some((item) => /related searches/i.test(item.title)),
|
||||
false,
|
||||
);
|
||||
|
||||
console.log('Amazon search parser fixtures passed.');
|
||||
}
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ export async function waitForAmazonSearchPageReady() {
|
||||
|
||||
// Visit the full result stream instead of jumping straight to pagination. Amazon loads
|
||||
// recommendation and sponsored modules when their region approaches the viewport.
|
||||
for (const fraction of [0.2, 0.55, 0.92]) {
|
||||
for (const fraction of [0.15, 0.35, 0.55, 0.75, 0.92]) {
|
||||
const documentHeight = Math.max(
|
||||
document.documentElement.scrollHeight,
|
||||
document.body?.scrollHeight || 0,
|
||||
|
||||
@ -92,14 +92,29 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
return decoded;
|
||||
};
|
||||
|
||||
const parseAsinFromText = (value: string) => {
|
||||
const decoded = decodeRepeatedly(value);
|
||||
const patterns = [
|
||||
/\/(?:dp|gp\/product|gp\/aw\/d)\/([A-Z0-9]{10})(?:[/?]|$)/i,
|
||||
/[?&#]asin=([A-Z0-9]{10})(?:[&]|$)/i,
|
||||
/[?&#]lp_asins=([A-Z0-9]{10})/i,
|
||||
/\/dp\/([A-Z0-9]{10})/i,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = pattern.exec(decoded);
|
||||
if (match) return match[1].toUpperCase();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const parseAsin = (node: HTMLElement, href?: string) => {
|
||||
const dataAsin = node.closest<HTMLElement>('[data-asin]')?.dataset.asin || node.dataset.asin;
|
||||
if (dataAsin && /^[A-Z0-9]{10}$/.test(dataAsin)) return dataAsin;
|
||||
|
||||
const candidates = [href || '', decodeRepeatedly(href || '')];
|
||||
for (const candidate of candidates) {
|
||||
const match = /\/(?:dp|gp\/product)\/([A-Z0-9]{10})(?:[/?]|$)/i.exec(candidate);
|
||||
if (match) return match[1].toUpperCase();
|
||||
const asin = parseAsinFromText(candidate);
|
||||
if (asin) return asin;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@ -125,9 +140,14 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
const hasSponsoredDisclosure = (node: Element) =>
|
||||
sponsoredDisclosureCandidates(node).some(isSponsoredDisclosure);
|
||||
|
||||
const closestAsinHost = (node: Element) => {
|
||||
const host = node.closest<HTMLElement>('[data-asin]');
|
||||
return host && /^[A-Z0-9]{10}$/.test(host.dataset.asin || '') ? host : null;
|
||||
};
|
||||
|
||||
const hasModuleSponsoredDisclosure = (node: Element) =>
|
||||
sponsoredDisclosureCandidates(node).some(
|
||||
(candidate) => !candidate.closest('[data-asin]') && isSponsoredDisclosure(candidate),
|
||||
(candidate) => !closestAsinHost(candidate) && isSponsoredDisclosure(candidate),
|
||||
);
|
||||
|
||||
const metadata = (node: Element) =>
|
||||
@ -152,6 +172,10 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
const asin = candidate.dataset.asin;
|
||||
if (asin && /^[A-Z0-9]{10}$/.test(asin)) asins.add(asin);
|
||||
});
|
||||
node.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((anchor) => {
|
||||
const asin = parseAsinFromText(anchor.href);
|
||||
if (asin) asins.add(asin);
|
||||
});
|
||||
return asins.size;
|
||||
};
|
||||
|
||||
@ -161,17 +185,117 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
const hasVisibleElement = (node: Element, selector: string) =>
|
||||
Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible);
|
||||
|
||||
const isSearchResultCard = (node: Element) =>
|
||||
node.matches('[data-component-type="s-search-result"]');
|
||||
|
||||
const hasModuleHeadline = (node: Element) =>
|
||||
Array.from(
|
||||
node.querySelectorAll<HTMLElement>(
|
||||
'h1, h2, h3, [class*="headline"], [class*="sb-headline"], [class*="sbv-"]',
|
||||
),
|
||||
).some(
|
||||
(heading) =>
|
||||
!heading.closest('[data-asin]') && isVisible(heading) && text(heading).length > 0,
|
||||
);
|
||||
|
||||
const isOutsideProductCard = (node: Element, root: Element) => {
|
||||
if (node.closest('[data-component-type="s-search-result"]')) return false;
|
||||
const asinHost = node.closest<HTMLElement>('[data-asin]');
|
||||
return !asinHost || asinHost === root;
|
||||
};
|
||||
|
||||
const hasModuleLevelVideo = (node: Element) =>
|
||||
Array.from(node.querySelectorAll('video')).some((video) => isOutsideProductCard(video, node));
|
||||
|
||||
const hasModuleLevelBrandVisual = (node: Element) => {
|
||||
if (hasModuleLevelVideo(node)) return true;
|
||||
const playPause = Array.from(
|
||||
node.querySelectorAll<HTMLElement>(
|
||||
'[aria-label*="Pause" i], [aria-label*="Play" i], [title*="Pause" i], [title*="Play" i]',
|
||||
),
|
||||
).some((candidate) => isOutsideProductCard(candidate, node) && isVisible(candidate));
|
||||
if (playPause) return true;
|
||||
const outsideImages = Array.from(node.querySelectorAll<HTMLElement>('img')).filter(
|
||||
(image) => isOutsideProductCard(image, node) && isVisible(image),
|
||||
);
|
||||
return outsideImages.length >= 2;
|
||||
};
|
||||
|
||||
const hasInfluencerThematicCopy = (node: Element) =>
|
||||
Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3, span, a, [class*="headline"]')).some(
|
||||
(candidate) => {
|
||||
if (closestAsinHost(candidate) || isVisuallyHiddenText(candidate)) return false;
|
||||
return /picks from amazon influencers|influencer storefronts|amazon influencer/i.test(
|
||||
text(candidate),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const hasThematicSpSignal = (node: Element) => {
|
||||
const blob = [
|
||||
metadata(node),
|
||||
node.getAttribute('cel_widget_id') || '',
|
||||
node.getAttribute('data-multi-ad-feedback-form-trigger') || '',
|
||||
node.getAttribute('data-component-props') || '',
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
if (/brand-footer|multi-brand-creative|brands-related|desktop-hsa-3psl/.test(blob)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
/featured.?asins.?list|sponsoredproductsloom|sp_search_thematic|loom-desktop|influencer/.test(
|
||||
blob,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return hasInfluencerThematicCopy(node);
|
||||
};
|
||||
|
||||
const isSponsoredProductsThematic = (node: Element) => {
|
||||
if (hasThematicSpSignal(node)) return true;
|
||||
if (
|
||||
Array.from(
|
||||
node.querySelectorAll(
|
||||
'[cel_widget_id], [data-multi-ad-feedback-form-trigger], [id*="loom"], [class*="loom"]',
|
||||
),
|
||||
).some(hasThematicSpSignal)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
for (
|
||||
let current = node.parentElement;
|
||||
current && !isMainResultsBoundary(current);
|
||||
current = current.parentElement
|
||||
) {
|
||||
if (hasThematicSpSignal(current)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasVisibleBrandCreative = (node: Element) => {
|
||||
if (isSearchResultCard(node)) return false;
|
||||
if (isSponsoredProductsThematic(node)) return false;
|
||||
|
||||
const visibleBrandDestination = hasVisibleElement(
|
||||
node,
|
||||
'a[href*="/stores/"], a[href*="/brands/"], [class*="brand-logo"], [class*="brandLogo"]',
|
||||
);
|
||||
const visibleModuleHeadline = Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3')).some(
|
||||
(heading) =>
|
||||
!heading.closest('[data-asin]') && isVisible(heading) && text(heading).length > 0,
|
||||
const explicitBrandModule = /sponsored.?brand|brand.?ad|brands-related|brand-card|\bsbv-/.test(
|
||||
metadata(node),
|
||||
);
|
||||
const brandsRelatedCopy = /brands related to your search/i.test(text(node));
|
||||
const moduleSponsored = hasModuleSponsoredDisclosure(node) || hasSponsoredDisclosure(node);
|
||||
const videoStrip = hasModuleLevelBrandVisual(node) && moduleSponsored;
|
||||
const headlineUnit = hasModuleHeadline(node) && moduleSponsored;
|
||||
return (
|
||||
visibleBrandDestination ||
|
||||
explicitBrandModule ||
|
||||
brandsRelatedCopy ||
|
||||
videoStrip ||
|
||||
headlineUnit
|
||||
);
|
||||
const explicitBrandModule = /sponsored.?brand|brand.?ad/.test(metadata(node));
|
||||
return visibleBrandDestination || (explicitBrandModule && visibleModuleHeadline);
|
||||
};
|
||||
|
||||
const findAdModule = (card: HTMLElement) => {
|
||||
@ -216,11 +340,18 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
adFormat: 'standard' as const,
|
||||
};
|
||||
}
|
||||
if (isSearchResultCard(card)) {
|
||||
return {
|
||||
adType: 'sponsoredProducts' as const,
|
||||
placement: 'middleOfSearch' as const,
|
||||
adFormat: hasVisibleElement(card, 'video') ? ('video' as const) : ('standard' as const),
|
||||
};
|
||||
}
|
||||
if (hasVisibleBrandCreative(module)) {
|
||||
return {
|
||||
adType: 'sponsoredBrands' as const,
|
||||
placement: 'middleOfSearch' as const,
|
||||
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const),
|
||||
adFormat: hasModuleLevelVideo(module) ? ('video' as const) : ('standard' as const),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@ -230,6 +361,24 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
};
|
||||
};
|
||||
|
||||
const hasCollectableProductLink = (node: Element) =>
|
||||
Boolean(
|
||||
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="]',
|
||||
),
|
||||
);
|
||||
|
||||
const addCandidate = (node: HTMLElement) => {
|
||||
if (
|
||||
isVisible(node) &&
|
||||
(node.dataset.asin ||
|
||||
hasCollectableProductLink(node) ||
|
||||
parseAsin(node, node.querySelector<HTMLAnchorElement>('a[href]')?.href))
|
||||
) {
|
||||
candidateSet.add(node);
|
||||
}
|
||||
};
|
||||
|
||||
const candidateSet = new Set<HTMLElement>();
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(
|
||||
@ -238,13 +387,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
.forEach((node) => {
|
||||
const stableCard =
|
||||
node.closest<HTMLElement>('[data-component-type="s-search-result"]') || node;
|
||||
if (
|
||||
isVisible(stableCard) &&
|
||||
(stableCard.dataset.asin ||
|
||||
stableCard.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]'))
|
||||
) {
|
||||
candidateSet.add(stableCard);
|
||||
}
|
||||
addCandidate(stableCard);
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLElement>('[data-asin]').forEach((node) => {
|
||||
@ -268,13 +411,29 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
isVisible(sidebarAd) &&
|
||||
hasSponsoredDisclosure(sidebarAd) &&
|
||||
sidebarAd.querySelector('h2, h3') &&
|
||||
sidebarAd.querySelector('img[src]') &&
|
||||
sidebarAd.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]')
|
||||
sidebarAd.querySelector('img[src], img[data-src]') &&
|
||||
hasCollectableProductLink(sidebarAd)
|
||||
) {
|
||||
candidateSet.add(sidebarAd);
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(
|
||||
'.AdHolder, [class*="brands-related"], [class*="brand-card"], [data-component-type="sponsored-brand"]',
|
||||
)
|
||||
.forEach((module) => {
|
||||
if (!isVisible(module)) return;
|
||||
const nestedCards = module.querySelectorAll<HTMLElement>(
|
||||
'[data-asin], [class*="brand-card"], .sbv-product, [data-slot*="3psl"], [data-slot*="hsa"]',
|
||||
);
|
||||
if (nestedCards.length > 0) {
|
||||
nestedCards.forEach(addCandidate);
|
||||
return;
|
||||
}
|
||||
addCandidate(module);
|
||||
});
|
||||
|
||||
const cards = Array.from(candidateSet)
|
||||
.filter(
|
||||
(card, index, all) =>
|
||||
@ -310,13 +469,22 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
if (!asin) return items;
|
||||
|
||||
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3');
|
||||
const imageNode = card.querySelector<HTMLImageElement>('img.s-image, img[src]');
|
||||
const imageNode = card.querySelector<HTMLImageElement>(
|
||||
'img.s-image, img[src], img[data-src], img[srcset]',
|
||||
);
|
||||
const title =
|
||||
titleNode?.getAttribute('aria-label')?.trim() ||
|
||||
text(titleNode) ||
|
||||
imageNode?.alt?.trim() ||
|
||||
asin;
|
||||
const imageSrc = imageNode?.src;
|
||||
const dataSrc = imageNode?.getAttribute('data-src') || imageNode?.dataset.src;
|
||||
const srcsetUrl = (imageNode?.getAttribute('srcset') || '')
|
||||
.split(',')[0]
|
||||
?.trim()
|
||||
.split(/\s+/)[0];
|
||||
const rawSrc = imageNode?.src && !imageNode.src.startsWith('data:') ? imageNode.src : undefined;
|
||||
const placeholderSrc = /grey-pixel|transparent-pixel|\/1x1(?:\.|$)/i.test(rawSrc || '');
|
||||
const imageSrc = (placeholderSrc ? undefined : rawSrc) || dataSrc || srcsetUrl;
|
||||
if (!imageSrc) return items;
|
||||
|
||||
const currentPriceNode = card.querySelector<HTMLElement>(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user