GitHub Actions Bot commited on
Commit ·
7668e81
1
Parent(s): 2cff0cd
🚀 Auto-deploy: 27b0c30bc71c932c4699cdd45f05255ae06d4b3e
Browse files- server.ts +8 -8
- src/config.ts +9 -24
- src/otto-crawler.ts +9 -1
server.ts
CHANGED
|
@@ -38,10 +38,10 @@ app.get('/health', (req, res) => {
|
|
| 38 |
// 2. 执行爬取任务接口
|
| 39 |
app.post('/crawl', async (req, res) => {
|
| 40 |
try {
|
| 41 |
-
const {
|
| 42 |
|
| 43 |
// 参数验证
|
| 44 |
-
if (!
|
| 45 |
return res.status(400).json({
|
| 46 |
success: false,
|
| 47 |
error: '请提供有效的 platform 参数 (例如: "Otto")'
|
|
@@ -56,22 +56,22 @@ app.post('/crawl', async (req, res) => {
|
|
| 56 |
}
|
| 57 |
|
| 58 |
// 检查是否支持该源
|
| 59 |
-
if (!(
|
| 60 |
return res.status(400).json({
|
| 61 |
success: false,
|
| 62 |
-
error: `不支持的爬虫源: ${
|
| 63 |
});
|
| 64 |
}
|
| 65 |
|
| 66 |
-
log.info(`[API] 收到爬取请求:
|
| 67 |
|
| 68 |
-
// 根据
|
| 69 |
-
const crawler = crawlerMap[
|
| 70 |
const results: ProductData[] = await crawler(urls, { preset });
|
| 71 |
|
| 72 |
res.json({
|
| 73 |
success: true,
|
| 74 |
-
|
| 75 |
count: results.length,
|
| 76 |
data: results
|
| 77 |
});
|
|
|
|
| 38 |
// 2. 执行爬取任务接口
|
| 39 |
app.post('/crawl', async (req, res) => {
|
| 40 |
try {
|
| 41 |
+
const { platform, urls, preset = 'stable' } = req.body;
|
| 42 |
|
| 43 |
// 参数验证
|
| 44 |
+
if (!platform || typeof platform !== 'string') {
|
| 45 |
return res.status(400).json({
|
| 46 |
success: false,
|
| 47 |
error: '请提供有效的 platform 参数 (例如: "Otto")'
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
// 检查是否支持该源
|
| 59 |
+
if (!(platform in crawlerMap)) {
|
| 60 |
return res.status(400).json({
|
| 61 |
success: false,
|
| 62 |
+
error: `不支持的爬虫源: ${platform}。支持的源: ${Object.keys(crawlerMap).join(', ')}`
|
| 63 |
});
|
| 64 |
}
|
| 65 |
|
| 66 |
+
log.info(`[API] 收到爬取请求: platform=${platform}, ${urls.length} 个 URL, preset=${preset}`);
|
| 67 |
|
| 68 |
+
// 根据 platform 选择对应的爬虫执行
|
| 69 |
+
const crawler = crawlerMap[platform as CrawlerPlatform];
|
| 70 |
const results: ProductData[] = await crawler(urls, { preset });
|
| 71 |
|
| 72 |
res.json({
|
| 73 |
success: true,
|
| 74 |
+
platform: platform,
|
| 75 |
count: results.length,
|
| 76 |
data: results
|
| 77 |
});
|
src/config.ts
CHANGED
|
@@ -14,11 +14,11 @@ export interface CrawlerConfig {
|
|
| 14 |
args?: string[];
|
| 15 |
};
|
| 16 |
selectors: {
|
|
|
|
| 17 |
price: string[];
|
|
|
|
| 18 |
rating: string[];
|
| 19 |
reviews: string[];
|
| 20 |
-
title?: string[];
|
| 21 |
-
imageUrl?: string[];
|
| 22 |
};
|
| 23 |
}
|
| 24 |
|
|
@@ -107,35 +107,20 @@ const PRESETS: Record<string, Partial<CrawlerConfig>> = {
|
|
| 107 |
|
| 108 |
export const DEFAULT_SELECTORS = {
|
| 109 |
// 标题选择器 - 用于智能等待判断页面是否加载完成
|
| 110 |
-
|
| 111 |
-
'
|
| 112 |
-
'h1[data-test="product-title"]',
|
| 113 |
-
'.product-title h1',
|
| 114 |
-
'div.pdp_short-info__main-name'
|
| 115 |
],
|
| 116 |
price: [
|
| 117 |
-
'span[data-test="price-current"]',
|
| 118 |
-
'span.js_pdp_price__current-price',
|
| 119 |
-
'.price--current',
|
| 120 |
-
'[data-test="product-price"]',
|
| 121 |
'span.js_pdp_price__retail-price__value_original'
|
| 122 |
],
|
|
|
|
|
|
|
|
|
|
| 123 |
rating: [
|
| 124 |
-
'div
|
| 125 |
-
'div.pdp_cr-rating-score span',
|
| 126 |
-
'.rating-stars',
|
| 127 |
-
'[data-test="average-rating"]'
|
| 128 |
],
|
| 129 |
reviews: [
|
| 130 |
-
'span
|
| 131 |
-
'span.js_pdp_cr-rating--review-count',
|
| 132 |
-
'.review-count-text',
|
| 133 |
-
'[data-test="total-reviews"]'
|
| 134 |
-
],
|
| 135 |
-
imageUrl: [
|
| 136 |
-
'img[data-test="product-image"]',
|
| 137 |
-
'.pdp-gallery__active img',
|
| 138 |
-
'meta[property="og:image"]'
|
| 139 |
]
|
| 140 |
};
|
| 141 |
|
|
|
|
| 14 |
args?: string[];
|
| 15 |
};
|
| 16 |
selectors: {
|
| 17 |
+
brand: string[];
|
| 18 |
price: string[];
|
| 19 |
+
originalPrice: string[];
|
| 20 |
rating: string[];
|
| 21 |
reviews: string[];
|
|
|
|
|
|
|
| 22 |
};
|
| 23 |
}
|
| 24 |
|
|
|
|
| 107 |
|
| 108 |
export const DEFAULT_SELECTORS = {
|
| 109 |
// 标题选择器 - 用于智能等待判断页面是否加载完成
|
| 110 |
+
brand: [
|
| 111 |
+
'.js_pdp_short-info__brand-link'
|
|
|
|
|
|
|
|
|
|
| 112 |
],
|
| 113 |
price: [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
'span.js_pdp_price__retail-price__value_original'
|
| 115 |
],
|
| 116 |
+
originalPrice: [
|
| 117 |
+
'span.pdp_price__strike-through-price'
|
| 118 |
+
],
|
| 119 |
rating: [
|
| 120 |
+
'div.pdp_cr-rating-score span'
|
|
|
|
|
|
|
|
|
|
| 121 |
],
|
| 122 |
reviews: [
|
| 123 |
+
'span.js_pdp_cr-rating--review-count'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
]
|
| 125 |
};
|
| 126 |
|
src/otto-crawler.ts
CHANGED
|
@@ -8,6 +8,7 @@ let currentConfig: CrawlerConfig = getCrawlerConfig();
|
|
| 8 |
export interface ProductData {
|
| 9 |
url: string;
|
| 10 |
crawledAt: string;
|
|
|
|
| 11 |
price?: string | null;
|
| 12 |
rating?: string | null;
|
| 13 |
reviews?: string | null;
|
|
@@ -44,10 +45,17 @@ async function extractText(page: Page, ...selectors: string[]): Promise<string |
|
|
| 44 |
async function extractOttoData(page: Page, product: ProductData = {} as ProductData): Promise<ProductData> {
|
| 45 |
try {
|
| 46 |
const selectors = currentConfig.selectors;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
// 提取价格
|
| 49 |
const price = await extractText(page, ...selectors.price);
|
| 50 |
if (price) product.price = price;
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
// 提取评分
|
| 53 |
const rating = await extractText(page, ...selectors.rating);
|
|
@@ -58,7 +66,7 @@ async function extractOttoData(page: Page, product: ProductData = {} as ProductD
|
|
| 58 |
if (reviews) product.reviews = reviews;
|
| 59 |
|
| 60 |
log.info(`✓ 成功提取商品数据:`, {
|
| 61 |
-
|
| 62 |
price: product.price || 'N/A',
|
| 63 |
rating: product.rating || 'N/A',
|
| 64 |
reviews: product.reviews || 'N/A'
|
|
|
|
| 8 |
export interface ProductData {
|
| 9 |
url: string;
|
| 10 |
crawledAt: string;
|
| 11 |
+
brand?: string | null;
|
| 12 |
price?: string | null;
|
| 13 |
rating?: string | null;
|
| 14 |
reviews?: string | null;
|
|
|
|
| 45 |
async function extractOttoData(page: Page, product: ProductData = {} as ProductData): Promise<ProductData> {
|
| 46 |
try {
|
| 47 |
const selectors = currentConfig.selectors;
|
| 48 |
+
|
| 49 |
+
// 提取品牌
|
| 50 |
+
const brand = await extractText(page, ...selectors.brand);
|
| 51 |
+
if (brand) product.brand = brand;
|
| 52 |
|
| 53 |
// 提取价格
|
| 54 |
const price = await extractText(page, ...selectors.price);
|
| 55 |
if (price) product.price = price;
|
| 56 |
+
|
| 57 |
+
const originalPrice = await extractText(page, ...selectors.originalPrice);
|
| 58 |
+
if (price) product.originalPrice = originalPrice;
|
| 59 |
|
| 60 |
// 提取评分
|
| 61 |
const rating = await extractText(page, ...selectors.rating);
|
|
|
|
| 66 |
if (reviews) product.reviews = reviews;
|
| 67 |
|
| 68 |
log.info(`✓ 成功提取商品数据:`, {
|
| 69 |
+
brand: product.brand || 'N/A',
|
| 70 |
price: product.price || 'N/A',
|
| 71 |
rating: product.rating || 'N/A',
|
| 72 |
reviews: product.reviews || 'N/A'
|