mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-10-05 15:22:59 +08:00
fix: classify visible Amazon ad formats
This commit is contained in:
parent
d42e19f42f
commit
100b4501e6
@ -6,6 +6,8 @@ const html = `
|
||||
<main id="search">
|
||||
<section data-component-type="sponsored-brand">
|
||||
<span aria-label="Sponsored">Sponsored</span>
|
||||
<a href="/stores/example"><span class="brand-logo">Example brand</span></a>
|
||||
<h2>Example brand headline</h2>
|
||||
<article data-asin="B000000001">
|
||||
<h3>Brand product one</h3>
|
||||
<a href="/dp/B000000001"><img src="https://example.com/brand-1.jpg" /></a>
|
||||
@ -51,6 +53,8 @@ const html = `
|
||||
<section class="bottom-sponsored-video-module">
|
||||
<video></video>
|
||||
<span>Sponsored</span>
|
||||
<a href="/stores/video-brand"><span class="brand-logo">Video brand</span></a>
|
||||
<h2>Video brand headline</h2>
|
||||
<div class="puis-card-container">
|
||||
<div class="puisg-row">
|
||||
<article data-asin="B000000008">
|
||||
@ -117,7 +121,7 @@ async function main() {
|
||||
assert.equal(byAsin.get('B000000001')?.placementId, byAsin.get('B000000002')?.placementId);
|
||||
assert.equal(byAsin.get('B000000001')?.adProductIndex, 1);
|
||||
assert.equal(byAsin.get('B000000002')?.adProductIndex, 2);
|
||||
assert.equal(byAsin.get('B000000005')?.adType, 'unknownSponsored');
|
||||
assert.equal(byAsin.get('B000000005')?.adType, 'sponsoredProducts');
|
||||
assert.equal(byAsin.get('B000000005')?.placement, 'bottomOfSearch');
|
||||
assert.equal(byAsin.get('B000000006')?.adType, 'sponsoredDisplay');
|
||||
assert.equal(byAsin.get('B000000006')?.placement, 'sidebar');
|
||||
@ -163,6 +167,55 @@ async function main() {
|
||||
assert.equal(hiddenDisclosureItems.length, 1);
|
||||
assert.equal(hiddenDisclosureItems[0].adType, 'organic');
|
||||
|
||||
const groupedAdsDom = new JSDOM(
|
||||
`
|
||||
<main id="search">
|
||||
<div class="s-main-slot">
|
||||
<section class="shared-sponsored-products">
|
||||
<span aria-label="Sponsored">Sponsored</span>
|
||||
<a href="/stores/reolink" style="display: none">Hidden brand destination</a>
|
||||
<article data-asin="B000000012">
|
||||
<h3>Grouped sponsored product one</h3>
|
||||
<a href="/dp/B000000012"><img src="https://example.com/grouped-1.jpg" /></a>
|
||||
</article>
|
||||
<article data-asin="B000000013">
|
||||
<h3>Grouped sponsored product two</h3>
|
||||
<a href="/dp/B000000013"><img src="https://example.com/grouped-2.jpg" /></a>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
<aside class="left-sponsored-display" data-component-type="sponsored-display">
|
||||
<span aria-label="Sponsored">Sponsored</span>
|
||||
<article>
|
||||
<h3>Left rail display product</h3>
|
||||
<a href="/dp/B000000014"><img src="https://example.com/display-left.jpg" /></a>
|
||||
</article>
|
||||
</aside>
|
||||
</main>
|
||||
`,
|
||||
{ url: 'https://www.amazon.com/s?k=mirror&page=8' },
|
||||
);
|
||||
Object.assign(globalThis, {
|
||||
document: groupedAdsDom.window.document,
|
||||
Node: groupedAdsDom.window.Node,
|
||||
Element: groupedAdsDom.window.Element,
|
||||
HTMLElement: groupedAdsDom.window.HTMLElement,
|
||||
HTMLAnchorElement: groupedAdsDom.window.HTMLAnchorElement,
|
||||
HTMLImageElement: groupedAdsDom.window.HTMLImageElement,
|
||||
});
|
||||
const groupedAds = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||
const groupedAdsByAsin = new Map(groupedAds.map((item) => [item.asin, item]));
|
||||
assert.equal(groupedAdsByAsin.get('B000000012')?.adType, 'sponsoredProducts');
|
||||
assert.equal(groupedAdsByAsin.get('B000000013')?.adType, 'sponsoredProducts');
|
||||
assert.equal(
|
||||
groupedAdsByAsin.get('B000000012')?.placementId,
|
||||
groupedAdsByAsin.get('B000000013')?.placementId,
|
||||
);
|
||||
assert.equal(groupedAdsByAsin.get('B000000012')?.adProductIndex, 1);
|
||||
assert.equal(groupedAdsByAsin.get('B000000013')?.adProductIndex, 2);
|
||||
assert.equal(groupedAdsByAsin.get('B000000014')?.adType, 'sponsoredDisplay');
|
||||
assert.equal(groupedAdsByAsin.get('B000000014')?.placement, 'sidebar');
|
||||
|
||||
console.log('Amazon search parser fixtures passed.');
|
||||
}
|
||||
|
||||
|
||||
@ -104,26 +104,32 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const hasSponsoredDisclosure = (node: Element) => {
|
||||
const candidates = [
|
||||
const sponsoredDisclosureCandidates = (node: Element) =>
|
||||
[
|
||||
...(node.matches('[aria-label], [title], span') ? [node] : []),
|
||||
...Array.from(node.querySelectorAll('[aria-label], [title], span')),
|
||||
].filter((candidate): candidate is HTMLElement => candidate instanceof HTMLElement);
|
||||
return candidates.some((candidate) => {
|
||||
|
||||
const isSponsoredDisclosure = (candidate: HTMLElement) => {
|
||||
if (isVisuallyHiddenText(candidate)) return false;
|
||||
const accessibleLabels = [
|
||||
candidate.getAttribute('aria-label'),
|
||||
candidate.getAttribute('title'),
|
||||
];
|
||||
if (
|
||||
accessibleLabels.some((value) => /(?:^|\b)Sponsored(?:\b|\s|$)|赞助/i.test(value || ''))
|
||||
) {
|
||||
if (accessibleLabels.some((value) => /(?:^|\b)Sponsored(?:\b|\s|$)|赞助/i.test(value || ''))) {
|
||||
return true;
|
||||
}
|
||||
return candidate.matches('span') && /^(Sponsored|赞助)(?:\b|\s|$)/i.test(text(candidate));
|
||||
});
|
||||
};
|
||||
|
||||
const hasSponsoredDisclosure = (node: Element) =>
|
||||
sponsoredDisclosureCandidates(node).some(isSponsoredDisclosure);
|
||||
|
||||
const hasModuleSponsoredDisclosure = (node: Element) =>
|
||||
sponsoredDisclosureCandidates(node).some(
|
||||
(candidate) => !candidate.closest('[data-asin]') && isSponsoredDisclosure(candidate),
|
||||
);
|
||||
|
||||
const metadata = (node: Element) =>
|
||||
[
|
||||
node.getAttribute('data-component-type'),
|
||||
@ -152,15 +158,21 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
const hasExplicitAdMetadata = (node: Element) =>
|
||||
/sponsored.?brand|brand.?ad|display.?ad|sponsored.?display|adholder/.test(metadata(node));
|
||||
|
||||
const hasBrandCreative = (node: Element) =>
|
||||
/sponsored.?brand|brand.?ad/.test(metadata(node)) ||
|
||||
Boolean(node.querySelector('video')) ||
|
||||
(uniqueAsinsIn(node) > 1 &&
|
||||
Boolean(
|
||||
node.querySelector(
|
||||
const hasVisibleElement = (node: Element, selector: string) =>
|
||||
Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible);
|
||||
|
||||
const hasVisibleBrandCreative = (node: Element) => {
|
||||
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/.test(metadata(node));
|
||||
return visibleBrandDestination || (explicitBrandModule && visibleModuleHeadline);
|
||||
};
|
||||
|
||||
const findAdModule = (card: HTMLElement) => {
|
||||
let current: HTMLElement | null = card;
|
||||
@ -168,11 +180,11 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
for (; current; current = current.parentElement) {
|
||||
if (isMainResultsBoundary(current)) break;
|
||||
const explicitModule = hasExplicitAdMetadata(current);
|
||||
const brandedSponsoredModule =
|
||||
current !== card && hasSponsoredDisclosure(current) && hasBrandCreative(current);
|
||||
const singleProductSponsored =
|
||||
current !== card && hasSponsoredDisclosure(current) && uniqueAsinsIn(current) === 1;
|
||||
if (explicitModule || brandedSponsoredModule) return current;
|
||||
const moduleDisclosure = current !== card && hasModuleSponsoredDisclosure(current);
|
||||
const brandedSponsoredModule = moduleDisclosure && hasVisibleBrandCreative(current);
|
||||
const sharedSponsoredProductsModule = moduleDisclosure && uniqueAsinsIn(current) > 1;
|
||||
const singleProductSponsored = moduleDisclosure && uniqueAsinsIn(current) === 1;
|
||||
if (explicitModule || brandedSponsoredModule || sharedSponsoredProductsModule) return current;
|
||||
if (singleProductSponsored && !singleProductModule) singleProductModule = current;
|
||||
}
|
||||
return singleProductModule || card;
|
||||
@ -187,7 +199,8 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
'aside, #rightResultsATF, .s-right-column, [data-cel-widget*="right"], [data-component-type*="sidebar"]',
|
||||
),
|
||||
);
|
||||
const sponsored = hasSponsoredDisclosure(card) || hasSponsoredDisclosure(module);
|
||||
const sponsored =
|
||||
hasSponsoredDisclosure(card) || (module !== card && hasModuleSponsoredDisclosure(module));
|
||||
|
||||
if (!sponsored) {
|
||||
return {
|
||||
@ -203,24 +216,17 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
adFormat: 'standard' as const,
|
||||
};
|
||||
}
|
||||
if (hasBrandCreative(module)) {
|
||||
if (hasVisibleBrandCreative(module)) {
|
||||
return {
|
||||
adType: 'sponsoredBrands' as const,
|
||||
placement: 'middleOfSearch' as const,
|
||||
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const),
|
||||
};
|
||||
}
|
||||
if (hasSponsoredDisclosure(card)) {
|
||||
return {
|
||||
adType: 'sponsoredProducts' as const,
|
||||
placement: 'middleOfSearch' as const,
|
||||
adFormat: 'standard' as const,
|
||||
};
|
||||
}
|
||||
return {
|
||||
adType: 'unknownSponsored' as const,
|
||||
placement: 'unknown' as const,
|
||||
adFormat: 'unknown' as const,
|
||||
adFormat: hasVisibleElement(module, 'video') ? ('video' as const) : ('standard' as const),
|
||||
};
|
||||
};
|
||||
|
||||
@ -253,6 +259,22 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(
|
||||
'aside, #rightResultsATF, .s-right-column, [data-cel-widget*="right"], [data-component-type*="sidebar"], [data-component-type*="sponsored-display"]',
|
||||
)
|
||||
.forEach((sidebarAd) => {
|
||||
if (
|
||||
isVisible(sidebarAd) &&
|
||||
hasSponsoredDisclosure(sidebarAd) &&
|
||||
sidebarAd.querySelector('h2, h3') &&
|
||||
sidebarAd.querySelector('img[src]') &&
|
||||
sidebarAd.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]')
|
||||
) {
|
||||
candidateSet.add(sidebarAd);
|
||||
}
|
||||
});
|
||||
|
||||
const cards = Array.from(candidateSet)
|
||||
.filter(
|
||||
(card, index, all) =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user