fix: classify bottom Sponsored Brands video placements

This commit is contained in:
syrup 2026-09-02 09:58:23 +08:00
parent 793c343fb6
commit 6b4aa2306a
2 changed files with 34 additions and 9 deletions

View File

@ -47,6 +47,19 @@ const html = `
<a href="/dp/B000000005"><img src="https://example.com/unknown.jpg" /></a>
</article>
</section>
<section class="bottom-sponsored-video-module">
<video></video>
<span>Sponsored</span>
<div class="puis-card-container">
<div class="puisg-row">
<article data-asin="B000000008">
<h3>Sponsored Brands video product</h3>
<a href="/dp/B000000008"><img src="https://example.com/brand-video.jpg" /></a>
</article>
</div>
</div>
</section>
</div>
<aside id="rightResultsATF" data-component-type="sponsored-display">
@ -71,7 +84,7 @@ Object.assign(globalThis, {
async function main() {
const items = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
assert.equal(items.length, 7);
assert.equal(items.length, 8);
const byAsin = new Map(items.map((item) => [item.asin, item]));
assert.equal(byAsin.get('B000000003')?.adType, 'organic');
@ -90,9 +103,13 @@ async function main() {
assert.equal(byAsin.get('B000000005')?.placement, 'bottomOfSearch');
assert.equal(byAsin.get('B000000006')?.adType, 'sponsoredDisplay');
assert.equal(byAsin.get('B000000006')?.placement, 'sidebar');
assert.equal(byAsin.get('B000000008')?.adType, 'sponsoredBrands');
assert.equal(byAsin.get('B000000008')?.adFormat, 'video');
assert.equal(byAsin.get('B000000008')?.placement, 'bottomOfSearch');
assert.equal(items.filter((item) => item.asin === 'B000000008').length, 1);
assert.deepEqual(
items.map((item) => item.pageRank),
[1, 2, 3, 4, 5, 6, 7],
[1, 2, 3, 4, 5, 6, 7, 8],
);
console.log('Amazon search parser fixtures passed.');

View File

@ -134,17 +134,18 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const findAdModule = (card: HTMLElement) => {
let current: HTMLElement | null = card;
for (let depth = 0; current && depth < 6; depth++, current = current.parentElement) {
let singleProductModule: HTMLElement | undefined;
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 || singleProductSponsored)
return current;
if (explicitModule || brandedSponsoredModule) return current;
if (singleProductSponsored && !singleProductModule) singleProductModule = current;
}
return card;
return singleProductModule || card;
};
const classifyAd = (card: HTMLElement, module: HTMLElement) => {
@ -235,7 +236,8 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
(other, otherIndex) =>
index !== otherIndex &&
card.contains(other) &&
card.dataset.asin === other.dataset.asin,
parseAsin(card, card.querySelector<HTMLAnchorElement>('a[href]')?.href) ===
parseAsin(other, other.querySelector<HTMLAnchorElement>('a[href]')?.href),
),
)
.sort((left, right) => {
@ -252,6 +254,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const placementRanks = new Map<HTMLElement, number>();
const adRanks = new Map<AmazonAdType, number>();
const moduleProductIndexes = new Map<HTMLElement, number>();
const moduleAsins = new Map<HTMLElement, Set<string>>();
let nextPlacementRank = 1;
return cards.reduce<CollectedItem[]>((items, card) => {
@ -283,6 +286,12 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const module = findAdModule(card);
const classification = classifyAd(card, module);
if (classification.adType !== 'organic') {
const seenAsins = moduleAsins.get(module) || new Set<string>();
if (seenAsins.has(asin)) return items;
seenAsins.add(asin);
moduleAsins.set(module, seenAsins);
}
let placementRank: number | undefined;
let placementId: string | undefined;
let adRank: number | undefined;
@ -308,8 +317,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const afterLastOrganic =
lastOrganicCandidate &&
Boolean(
module.compareDocumentPosition(lastOrganicCandidate) &
Node.DOCUMENT_POSITION_PRECEDING,
module.compareDocumentPosition(lastOrganicCandidate) & Node.DOCUMENT_POSITION_PRECEDING,
);
placement = beforeFirstOrganic
? 'topOfSearch'