fix: preserve base Amazon product collection

This commit is contained in:
syrup 2026-09-02 10:51:02 +08:00
parent 2ad58b6f9c
commit 6fbf8b7bcd
2 changed files with 54 additions and 27 deletions

View File

@ -78,19 +78,6 @@ 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">
@ -140,12 +127,42 @@ 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],
); );
const hiddenDisclosureDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<article data-component-type="s-search-result" data-asin="B000000011">
<h2>Visible organic product</h2>
<span
aria-label="Sponsored Ad - hidden disclosure"
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
>Sponsored</span>
<a href="/sspa/click?url=%2Fdp%2FB000000011">
<img class="s-image" src="https://example.com/visible-product.jpg" />
</a>
</article>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=mirror' },
);
Object.assign(globalThis, {
document: hiddenDisclosureDom.window.document,
Node: hiddenDisclosureDom.window.Node,
Element: hiddenDisclosureDom.window.Element,
HTMLElement: hiddenDisclosureDom.window.HTMLElement,
HTMLAnchorElement: hiddenDisclosureDom.window.HTMLAnchorElement,
HTMLImageElement: hiddenDisclosureDom.window.HTMLImageElement,
});
const hiddenDisclosureItems = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
assert.equal(hiddenDisclosureItems.length, 1);
assert.equal(hiddenDisclosureItems[0].adType, 'organic');
console.log('Amazon search parser fixtures passed.'); console.log('Amazon search parser fixtures passed.');
} }

View File

@ -42,12 +42,7 @@ 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;
} }
@ -62,6 +57,27 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
); );
}; };
const isVisuallyHiddenText = (node: HTMLElement) => {
if (!isVisible(node)) return true;
const style = node.ownerDocument.defaultView?.getComputedStyle(node);
if (!style) return false;
const width = Number.parseFloat(style.width);
const height = Number.parseFloat(style.height);
const screenReaderOnlyBox =
(style.position === 'absolute' || style.position === 'fixed') &&
style.overflow === 'hidden' &&
Number.isFinite(width) &&
Number.isFinite(height) &&
width <= 1 &&
height <= 1;
const zeroClip = /rect\(\s*0(?:px)?[, ]+\s*0(?:px)?[, ]+\s*0(?:px)?[, ]+\s*0(?:px)?\s*\)/i.test(
style.clip || '',
);
const insetClip = /inset\(\s*(?:50%|100%)/i.test(style.clipPath || '');
return screenReaderOnlyBox || zeroClip || insetClip;
};
const decodeRepeatedly = (value: string) => { const decodeRepeatedly = (value: string) => {
let decoded = value; let decoded = value;
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
@ -94,7 +110,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
...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) => { return candidates.some((candidate) => {
if (!isVisible(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'),
@ -238,11 +254,6 @@ 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(
@ -278,7 +289,6 @@ 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) ||