mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-10-05 07:13:06 +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">
|
<main id="search">
|
||||||
<section data-component-type="sponsored-brand">
|
<section data-component-type="sponsored-brand">
|
||||||
<span aria-label="Sponsored">Sponsored</span>
|
<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">
|
<article data-asin="B000000001">
|
||||||
<h3>Brand product one</h3>
|
<h3>Brand product one</h3>
|
||||||
<a href="/dp/B000000001"><img src="https://example.com/brand-1.jpg" /></a>
|
<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">
|
<section class="bottom-sponsored-video-module">
|
||||||
<video></video>
|
<video></video>
|
||||||
<span>Sponsored</span>
|
<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="puis-card-container">
|
||||||
<div class="puisg-row">
|
<div class="puisg-row">
|
||||||
<article data-asin="B000000008">
|
<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')?.placementId, byAsin.get('B000000002')?.placementId);
|
||||||
assert.equal(byAsin.get('B000000001')?.adProductIndex, 1);
|
assert.equal(byAsin.get('B000000001')?.adProductIndex, 1);
|
||||||
assert.equal(byAsin.get('B000000002')?.adProductIndex, 2);
|
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('B000000005')?.placement, 'bottomOfSearch');
|
||||||
assert.equal(byAsin.get('B000000006')?.adType, 'sponsoredDisplay');
|
assert.equal(byAsin.get('B000000006')?.adType, 'sponsoredDisplay');
|
||||||
assert.equal(byAsin.get('B000000006')?.placement, 'sidebar');
|
assert.equal(byAsin.get('B000000006')?.placement, 'sidebar');
|
||||||
@ -163,6 +167,55 @@ async function main() {
|
|||||||
assert.equal(hiddenDisclosureItems.length, 1);
|
assert.equal(hiddenDisclosureItems.length, 1);
|
||||||
assert.equal(hiddenDisclosureItems[0].adType, 'organic');
|
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.');
|
console.log('Amazon search parser fixtures passed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,26 +104,32 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasSponsoredDisclosure = (node: Element) => {
|
const sponsoredDisclosureCandidates = (node: Element) =>
|
||||||
const candidates = [
|
[
|
||||||
...(node.matches('[aria-label], [title], span') ? [node] : []),
|
...(node.matches('[aria-label], [title], span') ? [node] : []),
|
||||||
...Array.from(node.querySelectorAll('[aria-label], [title], span')),
|
...Array.from(node.querySelectorAll('[aria-label], [title], span')),
|
||||||
].filter((candidate): candidate is HTMLElement => candidate instanceof HTMLElement);
|
].filter((candidate): candidate is HTMLElement => candidate instanceof HTMLElement);
|
||||||
return candidates.some((candidate) => {
|
|
||||||
|
const isSponsoredDisclosure = (candidate: HTMLElement) => {
|
||||||
if (isVisuallyHiddenText(candidate)) return false;
|
if (isVisuallyHiddenText(candidate)) return false;
|
||||||
const accessibleLabels = [
|
const accessibleLabels = [
|
||||||
candidate.getAttribute('aria-label'),
|
candidate.getAttribute('aria-label'),
|
||||||
candidate.getAttribute('title'),
|
candidate.getAttribute('title'),
|
||||||
];
|
];
|
||||||
if (
|
if (accessibleLabels.some((value) => /(?:^|\b)Sponsored(?:\b|\s|$)|赞助/i.test(value || ''))) {
|
||||||
accessibleLabels.some((value) => /(?:^|\b)Sponsored(?:\b|\s|$)|赞助/i.test(value || ''))
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return candidate.matches('span') && /^(Sponsored|赞助)(?:\b|\s|$)/i.test(text(candidate));
|
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) =>
|
const metadata = (node: Element) =>
|
||||||
[
|
[
|
||||||
node.getAttribute('data-component-type'),
|
node.getAttribute('data-component-type'),
|
||||||
@ -152,15 +158,21 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
const hasExplicitAdMetadata = (node: Element) =>
|
const hasExplicitAdMetadata = (node: Element) =>
|
||||||
/sponsored.?brand|brand.?ad|display.?ad|sponsored.?display|adholder/.test(metadata(node));
|
/sponsored.?brand|brand.?ad|display.?ad|sponsored.?display|adholder/.test(metadata(node));
|
||||||
|
|
||||||
const hasBrandCreative = (node: Element) =>
|
const hasVisibleElement = (node: Element, selector: string) =>
|
||||||
/sponsored.?brand|brand.?ad/.test(metadata(node)) ||
|
Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible);
|
||||||
Boolean(node.querySelector('video')) ||
|
|
||||||
(uniqueAsinsIn(node) > 1 &&
|
const hasVisibleBrandCreative = (node: Element) => {
|
||||||
Boolean(
|
const visibleBrandDestination = hasVisibleElement(
|
||||||
node.querySelector(
|
node,
|
||||||
'a[href*="/stores/"], a[href*="/brands/"], [class*="brand-logo"], [class*="brandLogo"]',
|
'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) => {
|
const findAdModule = (card: HTMLElement) => {
|
||||||
let current: HTMLElement | null = card;
|
let current: HTMLElement | null = card;
|
||||||
@ -168,11 +180,11 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
for (; current; current = current.parentElement) {
|
for (; current; current = current.parentElement) {
|
||||||
if (isMainResultsBoundary(current)) break;
|
if (isMainResultsBoundary(current)) break;
|
||||||
const explicitModule = hasExplicitAdMetadata(current);
|
const explicitModule = hasExplicitAdMetadata(current);
|
||||||
const brandedSponsoredModule =
|
const moduleDisclosure = current !== card && hasModuleSponsoredDisclosure(current);
|
||||||
current !== card && hasSponsoredDisclosure(current) && hasBrandCreative(current);
|
const brandedSponsoredModule = moduleDisclosure && hasVisibleBrandCreative(current);
|
||||||
const singleProductSponsored =
|
const sharedSponsoredProductsModule = moduleDisclosure && uniqueAsinsIn(current) > 1;
|
||||||
current !== card && hasSponsoredDisclosure(current) && uniqueAsinsIn(current) === 1;
|
const singleProductSponsored = moduleDisclosure && uniqueAsinsIn(current) === 1;
|
||||||
if (explicitModule || brandedSponsoredModule) return current;
|
if (explicitModule || brandedSponsoredModule || sharedSponsoredProductsModule) return current;
|
||||||
if (singleProductSponsored && !singleProductModule) singleProductModule = current;
|
if (singleProductSponsored && !singleProductModule) singleProductModule = current;
|
||||||
}
|
}
|
||||||
return singleProductModule || card;
|
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"]',
|
'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) {
|
if (!sponsored) {
|
||||||
return {
|
return {
|
||||||
@ -203,24 +216,17 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
adFormat: 'standard' as const,
|
adFormat: 'standard' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasBrandCreative(module)) {
|
if (hasVisibleBrandCreative(module)) {
|
||||||
return {
|
return {
|
||||||
adType: 'sponsoredBrands' as const,
|
adType: 'sponsoredBrands' as const,
|
||||||
placement: 'middleOfSearch' as const,
|
placement: 'middleOfSearch' as const,
|
||||||
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const),
|
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasSponsoredDisclosure(card)) {
|
|
||||||
return {
|
return {
|
||||||
adType: 'sponsoredProducts' as const,
|
adType: 'sponsoredProducts' as const,
|
||||||
placement: 'middleOfSearch' as const,
|
placement: 'middleOfSearch' as const,
|
||||||
adFormat: 'standard' as const,
|
adFormat: hasVisibleElement(module, 'video') ? ('video' as const) : ('standard' as const),
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
adType: 'unknownSponsored' as const,
|
|
||||||
placement: 'unknown' as const,
|
|
||||||
adFormat: 'unknown' 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)
|
const cards = Array.from(candidateSet)
|
||||||
.filter(
|
.filter(
|
||||||
(card, index, all) =>
|
(card, index, all) =>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user