mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-10-05 23:33:21 +08:00
fix: classify sponsored ads across new Amazon templates
This commit is contained in:
parent
4c26b108a7
commit
3c0e0e4c33
@ -1127,6 +1127,80 @@ async function main() {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unknownTemplatesDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0GENORG01">
|
||||||
|
<h2>Generic organic product</h2>
|
||||||
|
<a href="/dp/B0GENORG01"><img src="https://example.com/generic-organic.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section class="future-layout-alpha" data-ad-details='{"adType":"sponsoredProducts"}'>
|
||||||
|
<h2>A heading that is not Shop by type</h2>
|
||||||
|
<span aria-label="Sponsored">Sponsored</span>
|
||||||
|
<div class="future-card-one">
|
||||||
|
<h3>Future SP one</h3>
|
||||||
|
<a href="https://redirect.amazon.com/x/c/new1?url=%2Fdp%2FB0GENSP001">
|
||||||
|
<img src="https://example.com/generic-sp-1.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="future-card-two">
|
||||||
|
<h3>Future SP two</h3>
|
||||||
|
<a href="https://direct.amazon.com/x/c/new2/https://www.amazon.com/dp/B0GENSP002">
|
||||||
|
<img src="https://example.com/generic-sp-2.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="future-layout-beta" data-component-type="sponsored-brand-vNext">
|
||||||
|
<span title="Sponsored">Sponsored</span>
|
||||||
|
<a href="/stores/future-brand"><span class="brand-logo">Future Brand</span></a>
|
||||||
|
<h2 class="future-copy">Future Brand collection</h2>
|
||||||
|
<div data-asin="B0GENSB001">
|
||||||
|
<h3>Future SB product</h3>
|
||||||
|
<a href="/dp/B0GENSB001"><img src="https://example.com/generic-sb.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="future-layout-gamma" data-ad-details='{"adType":"displayAd"}'>
|
||||||
|
<span aria-label="Sponsored">Sponsored</span>
|
||||||
|
<div data-asin="B0GENSD001">
|
||||||
|
<h3>Future SD product</h3>
|
||||||
|
<a href="/dp/B0GENSD001"><img src="https://example.com/generic-sd.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=future-layout' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: unknownTemplatesDom.window.document,
|
||||||
|
Node: unknownTemplatesDom.window.Node,
|
||||||
|
Element: unknownTemplatesDom.window.Element,
|
||||||
|
HTMLElement: unknownTemplatesDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: unknownTemplatesDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: unknownTemplatesDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const unknownTemplates = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const unknownTemplatesByAsin = new Map(unknownTemplates.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(unknownTemplatesByAsin.get('B0GENORG01')?.adType, 'organic');
|
||||||
|
assert.equal(unknownTemplatesByAsin.get('B0GENSP001')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(unknownTemplatesByAsin.get('B0GENSP002')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(
|
||||||
|
unknownTemplatesByAsin.get('B0GENSP001')?.placementId,
|
||||||
|
unknownTemplatesByAsin.get('B0GENSP002')?.placementId,
|
||||||
|
);
|
||||||
|
assert.equal(unknownTemplatesByAsin.get('B0GENSB001')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(unknownTemplatesByAsin.get('B0GENSD001')?.adType, 'sponsoredDisplay');
|
||||||
|
assert.ok(
|
||||||
|
['B0GENSP001', 'B0GENSP002', 'B0GENSB001', 'B0GENSD001'].every((asin) =>
|
||||||
|
unknownTemplatesByAsin.has(asin),
|
||||||
|
),
|
||||||
|
'Visible SP/SB/SD products must be collected even when their wrapper classes are unknown',
|
||||||
|
);
|
||||||
|
|
||||||
console.log('Amazon search parser fixtures passed.');
|
console.log('Amazon search parser fixtures passed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -239,8 +239,8 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasThematicSpSignal = (node: Element) => {
|
const thematicMetadata = (node: Element) =>
|
||||||
const blob = [
|
[
|
||||||
metadata(node),
|
metadata(node),
|
||||||
node.getAttribute('cel_widget_id') || '',
|
node.getAttribute('cel_widget_id') || '',
|
||||||
node.getAttribute('data-multi-ad-feedback-form-trigger') || '',
|
node.getAttribute('data-multi-ad-feedback-form-trigger') || '',
|
||||||
@ -248,16 +248,19 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
]
|
]
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
|
const hasDirectThematicSpSignal = (node: Element) => {
|
||||||
|
const blob = thematicMetadata(node);
|
||||||
if (/brand-footer|multi-brand-creative|brands-related|desktop-hsa-3psl/.test(blob)) {
|
if (/brand-footer|multi-brand-creative|brands-related|desktop-hsa-3psl/.test(blob)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
return /featured.?asins.?list|sponsored.?products|sp_search_thematic|loom-desktop|influencer|shop.?by.?type|shop_by_type/.test(
|
||||||
/featured.?asins.?list|sponsoredproductsloom|sp_search_thematic|loom-desktop|influencer|shop.?by.?type|shop_by_type/.test(
|
blob,
|
||||||
blob,
|
);
|
||||||
)
|
};
|
||||||
) {
|
|
||||||
return true;
|
const hasThematicSpSignal = (node: Element) => {
|
||||||
}
|
if (hasDirectThematicSpSignal(node)) return true;
|
||||||
return hasInfluencerThematicCopy(node) || hasShopByTypeCopy(node);
|
return hasInfluencerThematicCopy(node) || hasShopByTypeCopy(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -348,7 +351,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
adFormat: 'standard' as const,
|
adFormat: 'standard' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isSearchResultCard(card)) {
|
if (isSearchResultCard(card) || isSponsoredProductsThematic(module)) {
|
||||||
return {
|
return {
|
||||||
adType: 'sponsoredProducts' as const,
|
adType: 'sponsoredProducts' as const,
|
||||||
placement: 'middleOfSearch' as const,
|
placement: 'middleOfSearch' as const,
|
||||||
@ -466,6 +469,58 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amazon frequently changes the wrapper/template names used for ads. Discover a
|
||||||
|
* sponsored unit from its visible disclosure first, then find the closest
|
||||||
|
* ancestor that actually owns one or more product links. This keeps template
|
||||||
|
* names and marketing headings out of the SP/SB/SD classification contract.
|
||||||
|
*/
|
||||||
|
const findSponsoredProductScope = (disclosure: HTMLElement) => {
|
||||||
|
for (
|
||||||
|
let current = disclosure.parentElement;
|
||||||
|
current && !isMainResultsBoundary(current);
|
||||||
|
current = current.parentElement
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
isVisible(current) &&
|
||||||
|
(hasExplicitAdMetadata(current) || hasDirectThematicSpSignal(current)) &&
|
||||||
|
hasCollectableProductLink(current) &&
|
||||||
|
current.querySelector('img[src], img[data-src], img[srcset]')
|
||||||
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResult = disclosure.closest<HTMLElement>('[data-component-type="s-search-result"]');
|
||||||
|
if (searchResult) return searchResult;
|
||||||
|
|
||||||
|
const asinHost = closestAsinHost(disclosure);
|
||||||
|
if (asinHost) return asinHost;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let current = disclosure.parentElement;
|
||||||
|
current && !isMainResultsBoundary(current);
|
||||||
|
current = current.parentElement
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
isVisible(current) &&
|
||||||
|
hasCollectableProductLink(current) &&
|
||||||
|
current.querySelector('img[src], img[data-src], img[srcset]')
|
||||||
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
sponsoredDisclosureCandidates(document.body).forEach((disclosure) => {
|
||||||
|
if (!isSponsoredDisclosure(disclosure)) return;
|
||||||
|
const scope = findSponsoredProductScope(disclosure);
|
||||||
|
if (!scope) return;
|
||||||
|
collectThematicModuleCards(scope);
|
||||||
|
addCandidate(scope);
|
||||||
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
.querySelectorAll<HTMLElement>(
|
.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]',
|
'.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]',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user