collect-amazon.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env node
  2. /**
  3. * Amazon (Sorftime) 深度采集模块
  4. *
  5. * 对单个产品:
  6. * 1) 多关键词 × 多页 ProductQuery → 去重合并产品池
  7. * 2) 对 Top N 产品:ProductRequest 取详情(BSR、评分分布、Buybox、上线日、Photos)
  8. * 3) 对 Top M 产品:ProductReviewsQuery 取真实评论
  9. *
  10. * 导出:collectAmazonForProduct(product) → {by_keyword, product_details, reviews_by_asin}
  11. */
  12. const https = require('https');
  13. function httpRequest(options, bodyData = null, maxAttempts = 3) {
  14. return new Promise(async (resolve, reject) => {
  15. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  16. try {
  17. const result = await new Promise((res, rej) => {
  18. const req = https.request(options, (response) => {
  19. const chunks = [];
  20. response.on('data', (c) => chunks.push(c));
  21. response.on('end', () => res({ status: response.statusCode, body: Buffer.concat(chunks).toString('utf-8') }));
  22. });
  23. req.on('error', rej);
  24. req.on('timeout', () => { req.destroy(); rej(new Error('timeout')); });
  25. if (bodyData) req.write(bodyData);
  26. req.end();
  27. });
  28. if (result.status === 200) return resolve(result);
  29. if ([400, 429, 500, 502, 503, 504].includes(result.status) && attempt < maxAttempts) {
  30. await new Promise((r) => setTimeout(r, 1500 * Math.pow(1.8, attempt - 1)));
  31. continue;
  32. }
  33. return resolve(result);
  34. } catch (e) {
  35. if (attempt === maxAttempts) return reject(e);
  36. await new Promise((r) => setTimeout(r, 2000 * attempt));
  37. }
  38. }
  39. });
  40. }
  41. async function sorftimeCall(apiPath, body = {}, domain = 1) {
  42. const bodyStr = JSON.stringify({ path: apiPath, method: 'POST', body, query: { domain } });
  43. const { status, body: respBody } = await httpRequest({
  44. hostname: 'server-msq.fmode.cn',
  45. path: '/api/sorftime/forward',
  46. method: 'POST',
  47. headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
  48. timeout: 60000,
  49. }, bodyStr);
  50. if (status !== 200) return { _error: { status, body: respBody.slice(0, 300) } };
  51. try {
  52. const j = JSON.parse(respBody);
  53. return j?.Data ?? j?.data ?? j;
  54. } catch (e) {
  55. return { _error: { parse: e.message } };
  56. }
  57. }
  58. const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  59. // ============================================================
  60. // 1) 多页关键词产品查询(Sorftime ProductQuery)
  61. // ============================================================
  62. async function fetchProductsForKeyword(kw, pages = 3) {
  63. console.log(` [amz] 🔎 "${kw}" · ${pages}页 ProductQuery`);
  64. const all = [];
  65. for (let page = 1; page <= pages; page++) {
  66. const r = await sorftimeCall('/api/ProductQuery', {
  67. Page: page, Query: '1', QueryType: '7', Pattern: kw,
  68. });
  69. if (r?._error) {
  70. console.log(` ↻ p${page} 失败: ${JSON.stringify(r._error).slice(0, 120)}`);
  71. break;
  72. }
  73. const products = r?.Products || [];
  74. console.log(` · p${page}: ${products.length} products (total pages=${r?.PageCount || '?'})`);
  75. all.push(...products);
  76. if (!products.length || page >= (r?.PageCount || 0)) break;
  77. await sleep(500);
  78. }
  79. return all;
  80. }
  81. // ============================================================
  82. // 2) 产品详情(ProductRequest)— 含 BSR/评分数/Buybox/上线日期/Photo 等
  83. // ============================================================
  84. async function fetchProductDetail(asin) {
  85. const r = await sorftimeCall('/api/ProductRequest', {
  86. ASIN: asin, Trend: 1, QueryTrendStartDt: '', QueryTrendEndDt: '',
  87. });
  88. if (r?._error) return { _error: r._error };
  89. return r;
  90. }
  91. // ============================================================
  92. // 3) 产品评论(ProductReviewsQuery)
  93. // ============================================================
  94. async function fetchProductReviews(asin) {
  95. const r = await sorftimeCall('/api/ProductReviewsQuery', { ASIN: asin });
  96. if (r?._error) return { _error: r._error };
  97. return r;
  98. }
  99. // ============================================================
  100. // 主入口
  101. // ============================================================
  102. async function collectAmazonForProduct(product) {
  103. const {
  104. keywords_en: keywords,
  105. sorftime_pages = 3,
  106. sorftime_top_detail = 6,
  107. sorftime_top_reviews = 3,
  108. } = product;
  109. console.log(`\n ━━━ Amazon for "${product.name}" (${keywords.length} keywords) ━━━`);
  110. const by_keyword = {};
  111. const asinPool = new Map(); // asin → product summary (取 SalesVolume 最高的)
  112. for (const kw of keywords) {
  113. try {
  114. const products = await fetchProductsForKeyword(kw, sorftime_pages);
  115. by_keyword[kw] = {
  116. keyword: kw,
  117. top_products: { Products: products },
  118. total: products.length,
  119. };
  120. products.forEach((p) => {
  121. if (!p.Asin) return;
  122. const cur = asinPool.get(p.Asin);
  123. if (!cur || (p.ListingSalesVolumeOfMonth || 0) > (cur.ListingSalesVolumeOfMonth || 0)) {
  124. asinPool.set(p.Asin, { ...p, _from_keyword: kw });
  125. }
  126. });
  127. await sleep(800);
  128. } catch (e) {
  129. console.log(` [amz:${kw}] ❌ ${e.message}`);
  130. by_keyword[kw] = { _error: e.message };
  131. }
  132. }
  133. // 按月销量排序得到 top 产品
  134. const topByVolume = Array.from(asinPool.values())
  135. .sort((a, b) => (b.ListingSalesVolumeOfMonth || 0) - (a.ListingSalesVolumeOfMonth || 0));
  136. console.log(`\n [amz] 合并去重后 ${topByVolume.length} 款 · 抓 Top ${sorftime_top_detail} 详情 + Top ${sorftime_top_reviews} 评论`);
  137. // 抓详情
  138. const product_details = {};
  139. for (const p of topByVolume.slice(0, sorftime_top_detail)) {
  140. console.log(` [amz/detail] ${p.Asin} · ${(p.Brand || '').slice(0, 20)} · $${((p.SalesPrice || p.Price || 0) / 100).toFixed(2)}`);
  141. const detail = await fetchProductDetail(p.Asin);
  142. if (!detail?._error) product_details[p.Asin] = detail;
  143. else console.log(` ❌ ${JSON.stringify(detail._error).slice(0, 80)}`);
  144. await sleep(500);
  145. }
  146. // 抓评论
  147. const reviews_by_asin = {};
  148. for (const p of topByVolume.slice(0, sorftime_top_reviews)) {
  149. console.log(` [amz/reviews] ${p.Asin}`);
  150. const reviews = await fetchProductReviews(p.Asin);
  151. if (!reviews?._error) reviews_by_asin[p.Asin] = reviews;
  152. else console.log(` ❌ ${JSON.stringify(reviews._error).slice(0, 80)}`);
  153. await sleep(500);
  154. }
  155. return {
  156. by_keyword,
  157. product_details,
  158. reviews_by_asin,
  159. merged_products: topByVolume,
  160. };
  161. }
  162. module.exports = { collectAmazonForProduct, fetchProductsForKeyword, fetchProductDetail, fetchProductReviews };