采集背景
- 公司的一些商品通过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的后续数据加工处理形成最终报表给到业务同学。
失败的原因或成功的结果都会通过钉钉机器人的方式发送到告警群,便于快速的得知任务状态或方便进行排错处理。
跑完上述完整流程的一个关键操作:操作完每一步后设置足够长的等待时间来模拟人工操作流程,不然会很容易触发人机验证,如下图: