feat(edit): 采集插件、编辑页SKU 属性动态展示、颜色展示开发
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 将采集到的描述规范为可阅读的纯文本。保留原有段落和列表边界;只有来源
|
||||
* 已把结构压成一行时,才依据常见商品段落标题补出最小必要的换行。
|
||||
*/
|
||||
export function formatCollectedDescription(value: string): string {
|
||||
const withoutMarkup = value
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
||||
.replace(/<\s*\/\s*(?:p|div|section|article|li|h[1-6])\s*>/gi, '\n')
|
||||
.replace(/<\s*li\b[^>]*>/gi, '\n- ')
|
||||
.replace(/<[^>]*>/g, '');
|
||||
const formatted = withoutMarkup
|
||||
.replace(
|
||||
/\s*(Описание(?:\s+товара)?|Характеристики|Преимущества|Особенности|Комплектация|Технические\s+характеристики|Specifications?|Features?|Package\s+contents?|商品描述|产品描述|商品信息|规格参数|产品参数|产品特点|包装清单)\s*([::])\s*/gi,
|
||||
'\n\n$1$2\n',
|
||||
)
|
||||
.replace(/([^\n])\s*[-•·]\s+(?=[\p{L}\p{N}])/gu, '$1\n- ')
|
||||
.replace(/[\t\f\v ]*\n[\t\f\v ]*/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
return formatted;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CollectedProductPayload } from '@atlas/contracts';
|
||||
import { dedupeImageUrl, normaliseImageUrl } from './product-media.js';
|
||||
import { formatCollectedDescription } from './description-format.js';
|
||||
|
||||
export type OzonPageJsonRequest = {
|
||||
kind: 'modal-aspects' | 'product' | 'product-detail';
|
||||
@@ -341,7 +342,7 @@ export async function collectOzonProduct(
|
||||
itemKey: currentSku,
|
||||
canonicalUrl: canonical,
|
||||
collectedAt: new Date().toISOString(),
|
||||
parserVersion: 'ozon-public-2.3.0',
|
||||
parserVersion: 'ozon-public-2.3.1',
|
||||
},
|
||||
common: {
|
||||
...(title
|
||||
@@ -650,10 +651,7 @@ export function extractOzonDescription(
|
||||
} catch {
|
||||
// Some older page responses already contain plain text here.
|
||||
}
|
||||
value = value
|
||||
.replace(/\s+\n/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
value = formatCollectedDescription(value);
|
||||
if (!value) continue;
|
||||
return {
|
||||
value: value.slice(0, 100_000),
|
||||
@@ -678,7 +676,7 @@ export function extractOzonJsonLdDescription(
|
||||
: '';
|
||||
if (!raw) continue;
|
||||
return {
|
||||
value: raw.replace(/\s+\n/g, '\n').replace(/\n{3,}/g, '\n\n').slice(0, 100_000),
|
||||
value: formatCollectedDescription(raw).slice(0, 100_000),
|
||||
rawValue: raw.slice(0, 120_000),
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type OzonPageJsonFetcher,
|
||||
} from './ozon-parser.js';
|
||||
import { collectPlatformDomAssets } from './product-media.js';
|
||||
import { formatCollectedDescription } from './description-format.js';
|
||||
|
||||
type SourcePlatform = '1688' | 'taobao' | 'tmall' | 'ozon';
|
||||
type Json = Record<string, unknown>;
|
||||
@@ -112,7 +113,7 @@ function collect1688(root: Document, pageUrl: string, bridgedState?: unknown): C
|
||||
axes,
|
||||
uniqueVariants(variants),
|
||||
evidence,
|
||||
'1688-structured-2.0.0',
|
||||
'1688-structured-2.0.1',
|
||||
state,
|
||||
);
|
||||
}
|
||||
@@ -187,7 +188,7 @@ function collectTaobao(
|
||||
axes,
|
||||
uniqueVariants(variants),
|
||||
evidence,
|
||||
`${platform}-structured-2.0.0`,
|
||||
`${platform}-structured-2.0.1`,
|
||||
state,
|
||||
);
|
||||
}
|
||||
@@ -357,7 +358,11 @@ function extractMarketplaceFacts(root: Document, states: unknown[], platform: Ex
|
||||
return {
|
||||
title: (isRecord(item) ? stringAt(item, ['title', 'name']) : undefined) ?? (isRecord(temp) ? stringAt(temp, ['offerTitle']) : undefined) ?? (product ? stringAt(product, ['name']) : undefined),
|
||||
price,
|
||||
description: (product ? stringAt(product, ['description']) : undefined) ?? scopedDescription(root, platform),
|
||||
description: formatCollectedDescription(
|
||||
(product ? stringAt(product, ['description']) : undefined) ??
|
||||
scopedDescription(root, platform) ??
|
||||
'',
|
||||
) || undefined,
|
||||
attributes,
|
||||
categories: [...new Set(categoryNames)].map((name) => ({ key: `${platform}:category:${name.toLowerCase()}`.slice(0, 300), name })),
|
||||
};
|
||||
@@ -368,7 +373,10 @@ function scopedDescription(root: Document, platform: Exclude<SourcePlatform, 'oz
|
||||
? ['.de-description-detail', '#detailContentContainer', '.html-description']
|
||||
: ['[class*="tabDetailWrap--"]', '[class*="detailInfo--"]'];
|
||||
for (const selector of selectors) {
|
||||
const value = root.querySelector(selector)?.textContent?.replace(/\s+/g, ' ').trim();
|
||||
const element = root.querySelector<HTMLElement>(selector);
|
||||
const value = formatCollectedDescription(
|
||||
element?.innerText ?? element?.textContent ?? '',
|
||||
);
|
||||
if (value && value.length >= 10) return value.slice(0, 100_000);
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
extractOzonGalleryImageUrls,
|
||||
extractOzonGalleryVideos,
|
||||
} from '../lib/ozon-parser.ts';
|
||||
import { formatCollectedDescription } from '../lib/description-format.ts';
|
||||
import { dedupeImageUrl, normaliseImageUrl } from '../lib/product-media.ts';
|
||||
|
||||
test('Ozon gallery fixture only returns product-gallery images', async () => {
|
||||
@@ -93,6 +94,15 @@ test('Ozon JSON-LD Product description is the fallback when rich content is abse
|
||||
);
|
||||
});
|
||||
|
||||
test('采集描述保留段落和列表,并为压平的 Ozon 结构标题补出换行', () => {
|
||||
assert.equal(
|
||||
formatCollectedDescription(
|
||||
'Описание товара:适合户外使用。Характеристики:- 容量:25 L- 材质:尼龙Преимущества:- 防水\n\nКомплектация:<br>背包',
|
||||
),
|
||||
'Описание товара:\n适合户外使用。\n\nХарактеристики:\n- 容量:25 L\n- 材质:尼龙\n\nПреимущества:\n- 防水\n\nКомплектация:\n背包',
|
||||
);
|
||||
});
|
||||
|
||||
test('Ozon full characteristics merge with the five short header characteristics', () => {
|
||||
const attributes = extractOzonCharacteristics([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user