【RPA】前端实现分国家自动化采集Google商品数据写入excel

采集背景

  • 公司的一些商品通过Google Shopping进行广告推广,约占Google渠道流量60%。
  • Google在广告界面展示商品的评分情况,影响客户决策,需定期对该评分情况进行监测,业务会评估监测结果,由编辑部门编撰、补充高分评论,维护重点商品评分;
  • Google Shopping评论信息来源-独立站广告系统上传给Google,Google经过评估过滤校验后,形成最终评分;
  • 爬取频次要求:1个月/次

由于涉及到公司的隐私信息,下面我用Amazon平台举例子给大家展示了。

采集步骤及要求

需要采集的国家如下:

国家 站点
United States US
Australia AU
Canada CA
Germany DE
Spain ES
Italy IT
United Kingdom UK
France FR
Ireland EUR
Netherlands NL
Poland PL
Mexico MX

STEP1 : Google设置到对应的国家,如下图所示:

STEP2:指定seller,并排列组合来切换价格、评分信息以命中更多商品;

价格区间(示例为按USD币种呈现的价格区间,其他国家需进行换算): 10-30、30.01-60、60.01-100、100-200、200-300、300-500、500-1000、1000+

技术实现

由于整体逻辑是在 egg 服务里实现的,这里就不牵扯更多的东西了,把核心的技术要点展示出来。

STEP1:我们先把要用到的数据常量准备好,

  • currencyRatio是美元换算成其他国家币种的比例,做价格区间时切换成对应国家币种的价格区间;
  • site字段在勾选指定卖家时候会用到;
  • _site是需要传给后端的
js 复制代码
const countries = [
  { name: "美国", site: "Amazon.com", _site: "US", currencyRatio: 1 },
  { name: "法国", site: "Amazon.fr - Seller", _site: "FR", currencyRatio: 0.92 },
  { name: "澳大利亚", site: "Amazon AU", _site: "AU", currencyRatio: 1.52 },
  { name: "加拿大", site: "Amazon.ca", _site: "CA", currencyRatio: 1.32 },
  { name: "德国", site: "Amazon.de", _site: "DE", currencyRatio: 0.92 },
  { name: "西班牙", site: "Amazon.es", _site: "ES", currencyRatio: 0.92 },
  { name: "意大利", site: "Amazon.it", _site: "IT", currencyRatio: 0.92 },
  { name: "英国", site: "uk.Amazon.com", _site: "UK", currencyRatio: 0.79 },
  { name: "爱尔兰", site: "Amazon.eur", _site: "EUR", currencyRatio: 0.92 },
  { name: "荷兰", site: "Amazon.nl", _site: "NL", currencyRatio: 1.79 },
  { name: "波兰", site: "Amazon.pl", _site: "PL", currencyRatio: 4 },
  { name: "墨西哥", site: "Amazon.mx", _site: "MX", currencyRatio: 17.53 },
];

// 美国的价格区间分段
const usPriceRanges = [
  { min: 10, max: 30 },
  { min: 30.01, max: 60 },
  { min: 60.01, max: 100 },
  { min: 100.01, max: 200 },
  { min: 200.01, max: 300 },
  { min: 300.01, max: 500 },
  { min: 500.01, max: 1000 },
  { min: 1000.01, max: "" },
];

// 生成excel的表头字段
const excelHeader = {
  skuName: { text: "商品标题", width: 200 },
  shopName: { text: "店铺名", width: 15 },
  mark: { text: "评分", width: 15 },
  commentCount: { text: "评论数量", width: 15 },
  link: { text: "商品链接", width: 250 },
  page: { text: "当前页", width: 15 },
  min: { text: "最小价格", width: 15 },
  max: { text: "最大价格", width: 15 },
  rating: { text: "星级", width: 15 },
  totalPage: { text: "总页数", width: 15 },
};

STEP2 :使用 Puppeteer 启动一个浏览器实例

js 复制代码
const puppeteer = require("puppeteer");

const createBrowserInstance = async () => {
  let browser = await puppeteer.launch({
    headless: false,
    defaultViewport: {
      width: 1920,
      height: 1080,
    },
  });
  const page = await browser.newPage();
  
  return page;
};

STEP3:切换国家逻辑,这里在实际项目里会对文本元素的查找支持中英文的兼容逻辑,这里先使用中文模式进行查找,如:搜索结果区域和一些国家切换的特殊逻辑处理就会用到

js 复制代码
const switchLanguagePage =
  "https://www.google.com/preferences?hl=&prev=&lang=1&prev=https://www.google.com/preferences?hl%3D%26prev%3D";

async function clickTargetElement({
  page,
  selector,
  callback,
  searchText,
  fullEqual = false,
}) {
  const elements = await page.$$(selector);

  for (const element of elements) {
    const elementText = await page.evaluate(callback, element);

    if (
      fullEqual ? elementText === searchText : elementText.includes(searchText)
    ) {
      await element.click();
      break;
    }
  }
  return elements.length;
}

async function switchCountryBySearchText(page, country) {
  await page.goto(switchLanguagePage); // 先切换语言

  // 打开切换国家的弹窗
  await clickTargetElement({
    page,
    selector: "span.sjVJQd.RqT6ge",
    callback: (element) => element.textContent,
    searchText: "搜索结果区域",
  });

  await page.waitForTimeout(1000 * 3);

  await page.type("input[type=text]", country.name, { delay: 100 });

  await page.waitForTimeout(1000 * 3);

  // 点击搜索的国家 对某些国家需要特殊处理
  await clickTargetElement({
    page,
    selector: "g-menu-item",
    callback: (element) => element.textContent,
    searchText: country.name,
    fullEqual: ["西班牙", "意大利", "爱尔兰", "荷兰", "波兰"].includes(
      country.name
    ),
  });

  await page.waitForTimeout(1000 * 3);

  // 点击确认
  const confirmEl =
    "div.qk7LXc.TUOsUe.Fb1AKc.PnNX7d.ivkdbf > div:last-child > span:last-child";
  await page.waitForSelector(confirmEl, { visible: true });
  await page.click(confirmEl);
}

STEP4:搜索 amazon关键词,并点击到对应的购物tab,这里会有多种dom形态,所以做了特殊处理,然后,卖家栏设置到对应的关键词上,也就是上面的site字段;同理,这里也只是做了中文模式的逻辑

js 复制代码
async function openAmazonShopPage(page) {
  await page.goto("https://www.google.com");

  await page.waitForTimeout(3000);

  await page.type("textarea[name=q]", "amazon");

  await page.keyboard.press("Enter");
}
async function clickShoppingTab(page, sellerKeyword) {
  // 点击购物tab
  const hasEl = await clickTargetElement({
    page,
    selector: "span.FMKtTb",
    callback: (element) => element.textContent,
    searchText: "购物",
  });

  // 如果没有购物tab,那就是其它类型的dom结构
  if (!hasEl) {
    await clickTargetElement({
      page,
      selector: "div.hdtb-mitem",
      callback: (element) => element.textContent,
      searchText: "购物",
    });
  }

  await page.waitForTimeout(1000 * 10);

  // 卖家栏勾选带amazon关键字的
  await clickTargetElement({
    page,
    selector: "span.lg3aE > span",
    callback: (element) => element.textContent,
    searchText: sellerKeyword,
  });
}

STEP5:生成评分和价格区间的排列组合,结构如下图所示,后面我们循环这个数组来完成单个国家的数据采集

js 复制代码
// 获取价格和评分的组合
function getRatingsAndPrices(ratio, usPriceRanges) {
  const priceRanges = usPriceRanges.map((item) => ({
    min: (item.min * ratio).toFixed(2),
    max: item.max ? (item.max * ratio).toFixed(2) + "" : "",
  }));

  return Array.from({ length: 4 }, (_, index) => index + 1).map(
    (ratingRange) => ({
      priceRanges,
      rating: `${ratingRange} 颗星及以上`,
    })
  );
}
// 循环评分
for (const item of ratingsAndPrices) {
  // 步骤6,见下面
}

STEP6 :先处理集 1 颗星及以上,在单个评分里面再循环内部的 priceRanges,去采集每个价格区间期的数据

js 复制代码
// 切换评分
async function switchRating(page, rating) {
  // 商品评分 勾选星级
  await page.evaluate((rating) => {
    const allMarkEl = document.querySelectorAll(".lg3aE.aIuqNd .cNaB2e");
    for (const el of allMarkEl) {
      if (el.textContent.includes(rating)) {
        el.click();
        break;
      }
    }
  }, rating);
}

async processRating(page, item, country, app, filePath) {
  // 点击评分
  await switchRating(page, item.rating);
  await page.waitForTimeout(1000 * 20);

  // 循环价格区间
  for (const range of item.priceRanges) {
    // 步骤7,见下面
  }
}

STEP7:切换价格区间,然后判断有多少页,依次循环每一页对数据做采集,并将当页的数据写入到excel文件中,单个国家采集完的部分数据如下图所示

js 复制代码
// 切换价格区间
async function switchPriceRange(page, range) {
  await setInputValue(page, "input[name=lower]", range.min);
  if (range.max) {
    await setInputValue(page, "input[name=upper]", range.max);
  }
  await page.waitForTimeout(1000 * 10);
  await page.waitForSelector("button.sh-dr__prs", { visible: true });
  await page.click("button.sh-dr__prs");
}

async processPriceRange(page, app, { range, item, country, filePath }) {
  await switchPriceRange(page, range);
  await page.waitForTimeout(1000 * 20);

  let currentPage = 1;
  let totalPage = await getPageCount(page);

  while (currentPage <= totalPage) {
    const currentParams = {
      totalPage,
      page: currentPage,
      rating: item.rating,
      ...range,
    };
   
    const result = await getItemDetails(page, currentParams);

    await writeToExcel(result, filePath, excelHeader);

    if (currentPage > 1) await clickTargetPage(page, currentPage);
    await page.waitForTimeout(1000 * 20);

    currentPage++;
  }
}

STEP8:将文件通过接口形式传给后端同学写入数据库,经过BI的后续数据加工处理形成最终报表给到业务同学。

失败的原因或成功的结果都会通过钉钉机器人的方式发送到告警群,便于快速的得知任务状态或方便进行排错处理。

跑完上述完整流程的一个关键操作:操作完每一步后设置足够长的等待时间来模拟人工操作流程,不然会很容易触发人机验证,如下图:

相关推荐
cnsxjean1 小时前
SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传
java·前端·spring boot·分布式·后端·中间件·架构
ZL_5672 小时前
uniapp中使用uni-forms实现表单管理,验证表单
前端·javascript·uni-app
沉浮yu大海2 小时前
Vue.js 组件开发:构建可重用且高效的 UI 块
前端·vue.js·ui
代码欢乐豆2 小时前
软件工程第13章小测
服务器·前端·数据库·软件工程
给自己做减法2 小时前
解决登录Google账号遇到手机上Google账号无法验证的问题
google
莘薪3 小时前
JQuery -- 第九课
前端·javascript·jquery
好青崧3 小时前
CSS 样式入门:属性全知晓
前端·css·tensorflow
光头程序员3 小时前
工程化开发谷歌插件到底有爽
前端·react·工程化·前端工程化·谷歌插件
蒜蓉大猩猩3 小时前
Vue.js --- Vue3中其他组合式API
前端·javascript·vue.js·前端框架·node.js·html
铅华尽4 小时前
前端---HTML(一)
前端