fix: ignore hidden Amazon sponsored products

This commit is contained in:
syrup 2026-09-02 10:45:14 +08:00
parent 3f34ed8341
commit 2ad58b6f9c
2 changed files with 33 additions and 24 deletions

View File

@ -78,6 +78,19 @@ const html = `
<a href="/dp/B000000010"><img src="https://example.com/hidden-aria.jpg" /></a> <a href="/dp/B000000010"><img src="https://example.com/hidden-aria.jpg" /></a>
</article> </article>
</section> </section>
<section class="featured-from-amazon-brands">
<span>Featured from Amazon brands</span>
<article data-asin="B000000011">
<h3
aria-label="Sponsored Ad - hidden brand-banner product"
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
>Hidden brand-banner product</h3>
<a href="/sspa/click?url=%2Fdp%2FB000000011">
<img src="https://example.com/visible-brand-hero.jpg" />
</a>
</article>
</section>
</div> </div>
<aside id="rightResultsATF" data-component-type="sponsored-display"> <aside id="rightResultsATF" data-component-type="sponsored-display">
@ -127,6 +140,7 @@ async function main() {
assert.equal(items.filter((item) => item.asin === 'B000000008').length, 1); assert.equal(items.filter((item) => item.asin === 'B000000008').length, 1);
assert.equal(byAsin.has('B000000009'), false); assert.equal(byAsin.has('B000000009'), false);
assert.equal(byAsin.has('B000000010'), false); assert.equal(byAsin.has('B000000010'), false);
assert.equal(byAsin.has('B000000011'), false);
assert.deepEqual( assert.deepEqual(
items.map((item) => item.pageRank), items.map((item) => item.pageRank),
[1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8],

View File

@ -42,7 +42,12 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
style.visibility === 'hidden' || style.visibility === 'hidden' ||
style.visibility === 'collapse' || style.visibility === 'collapse' ||
style.opacity === '0' || style.opacity === '0' ||
style.getPropertyValue('content-visibility') === 'hidden') style.getPropertyValue('content-visibility') === 'hidden' ||
(style.clip !== 'auto' && style.clip !== '') ||
(style.clipPath !== 'none' && style.clipPath !== '') ||
(style.overflow === 'hidden' &&
Number.parseFloat(style.width) <= 1 &&
Number.parseFloat(style.height) <= 1))
) { ) {
return false; return false;
} }
@ -85,10 +90,11 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const hasSponsoredDisclosure = (node: Element) => { const hasSponsoredDisclosure = (node: Element) => {
const candidates = [ const candidates = [
node, ...(node.matches('[aria-label], [title], span') ? [node] : []),
...Array.from(node.querySelectorAll('[aria-label], [title], span, a')), ...Array.from(node.querySelectorAll('[aria-label], [title], span')),
]; ].filter((candidate): candidate is HTMLElement => candidate instanceof HTMLElement);
return candidates.some((candidate) => { return candidates.some((candidate) => {
if (!isVisible(candidate)) return false;
const accessibleLabels = [ const accessibleLabels = [
candidate.getAttribute('aria-label'), candidate.getAttribute('aria-label'),
candidate.getAttribute('title'), candidate.getAttribute('title'),
@ -98,21 +104,10 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
) { ) {
return true; return true;
} }
return /^(Sponsored|赞助)(?:\b|\s|$)/i.test(text(candidate)); return candidate.matches('span') && /^(Sponsored|赞助)(?:\b|\s|$)/i.test(text(candidate));
}); });
}; };
const hasSponsoredLink = (node: Element) =>
Array.from(node.querySelectorAll<HTMLAnchorElement>('a[href]')).some((anchor) => {
const href = decodeRepeatedly(anchor.href).toLowerCase();
return (
href.includes('/sspa/click') ||
href.includes('/gp/slredirect') ||
href.includes('adid=') ||
href.includes('qualifier=')
);
});
const metadata = (node: Element) => const metadata = (node: Element) =>
[ [
node.getAttribute('data-component-type'), node.getAttribute('data-component-type'),
@ -176,13 +171,7 @@ 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 = const sponsored = hasSponsoredDisclosure(card) || hasSponsoredDisclosure(module);
hasSponsoredDisclosure(card) ||
hasSponsoredDisclosure(module) ||
hasSponsoredLink(card) ||
hasSponsoredLink(module) ||
hasExplicitAdMetadata(card) ||
hasExplicitAdMetadata(module);
if (!sponsored) { if (!sponsored) {
return { return {
@ -205,7 +194,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const), adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const),
}; };
} }
if (hasSponsoredDisclosure(card) || hasSponsoredLink(card)) { if (hasSponsoredDisclosure(card)) {
return { return {
adType: 'sponsoredProducts' as const, adType: 'sponsoredProducts' as const,
placement: 'middleOfSearch' as const, placement: 'middleOfSearch' as const,
@ -249,6 +238,11 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
}); });
const cards = Array.from(candidateSet) const cards = Array.from(candidateSet)
.filter((card) => {
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3');
const imageNode = card.querySelector<HTMLImageElement>('img.s-image, img[src]');
return Boolean(titleNode && imageNode && isVisible(titleNode) && isVisible(imageNode));
})
.filter( .filter(
(card, index, all) => (card, index, all) =>
!all.some( !all.some(
@ -284,6 +278,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3'); 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]');
if (!titleNode || !imageNode || !isVisible(titleNode) || !isVisible(imageNode)) return items;
const title = const title =
titleNode?.getAttribute('aria-label')?.trim() || titleNode?.getAttribute('aria-label')?.trim() ||
text(titleNode) || text(titleNode) ||