1
This commit is contained in:
269
message.js
Normal file
269
message.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* type:0普通元素(默认),1列表,2表格(带分页)
|
||||
* condition:{
|
||||
* list:[] 点击条件
|
||||
* time:2000 点击后的等待世界
|
||||
* }
|
||||
* keys:子元素,如果type是0则是普通的键值,否则是数组键值
|
||||
* tableParts:表格专用:兼容多table或分段table的情况
|
||||
* pagination:分页配置,
|
||||
*/
|
||||
|
||||
/**
|
||||
* 数据类型
|
||||
* 1. 纯文字或图片
|
||||
* 2. 列表类型
|
||||
* 3.row布局下的子元素(综合1和2的)
|
||||
* 4. 列表
|
||||
*/
|
||||
|
||||
(async function () {
|
||||
let column = [
|
||||
{
|
||||
label: "低星评论",
|
||||
className: ".border-solid.rounded",
|
||||
condition: {
|
||||
list: [
|
||||
".flex.items-center.mt-6 div:nth-child(3)",
|
||||
".eds-react-checkbox-group label:nth-child(2)",
|
||||
".eds-react-checkbox-group label:nth-child(3)",
|
||||
".eds-react-checkbox-group label:nth-child(4)"
|
||||
],
|
||||
time: 200,
|
||||
},
|
||||
type: 1,
|
||||
keys: [
|
||||
{
|
||||
label: "用户",
|
||||
className: ".flex.items-center.justify-start .ml-2"
|
||||
},
|
||||
{
|
||||
label: "订单编号",
|
||||
className: ".underline.px-1"
|
||||
},
|
||||
{
|
||||
label: "商品名称",
|
||||
className: ".min-w-0.font-medium.break-all"
|
||||
},
|
||||
{
|
||||
label: "规格",
|
||||
className: ".min-w-0.font-medium.break-all + div"
|
||||
},
|
||||
{
|
||||
label: "评价内容",
|
||||
className: ".min-w-0.overflow-hidden",
|
||||
condition: {
|
||||
list: [
|
||||
"span.cursor-pointer"
|
||||
],
|
||||
time: 200,
|
||||
},
|
||||
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
nextBtn: ".eds-react-pagination-pager__button-next",
|
||||
maxPage: 2, // 最大爬取页数
|
||||
delay: 2000 // 翻页后的等待加载时间
|
||||
},
|
||||
|
||||
},
|
||||
]
|
||||
//自定义睡眠
|
||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms || 1500));
|
||||
|
||||
/**
|
||||
* 递归结构
|
||||
* @param {*列表} columns
|
||||
* @param {* 父dom节点} dom
|
||||
* @returns
|
||||
*/
|
||||
async function process(columns, dom) {
|
||||
if (!dom) return null;
|
||||
let result = {}
|
||||
for (const item of columns) {
|
||||
//判断条件,如果存在执行点击
|
||||
await autoClick(item, dom)
|
||||
|
||||
const element = dom.querySelector(item.className);
|
||||
//如果不存在
|
||||
if (!element) {
|
||||
result[item.label] = "没找到该元素"
|
||||
continue;
|
||||
}
|
||||
//如果是普通元素
|
||||
if (!item.type) {
|
||||
//如果是row布局
|
||||
if (item.keys && item.keys.length > 0) {
|
||||
await autoClick(item, element)
|
||||
result[item.label] = await process(item.keys, element);
|
||||
} else {
|
||||
await autoClick(item, element)
|
||||
//正常取值
|
||||
result[item.label] = extractValue(element, item);
|
||||
}
|
||||
} else if (item.type == 1) {
|
||||
result[item.label] = await processList(item, dom)
|
||||
} else if (item.type == 2) {
|
||||
result[item.label] = await processTable(item, element)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发点击事件
|
||||
*/
|
||||
async function autoClick(config, rootDom) {
|
||||
if (config?.condition) {
|
||||
for (const condition of config.condition.list) {
|
||||
let targets = rootDom.querySelectorAll(condition)
|
||||
for (const target of targets) {
|
||||
target.click();
|
||||
await sleep(config?.condition.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取具体值的辅助函数
|
||||
*/
|
||||
function extractValue(el, config) {
|
||||
// 如果指定提取某个属性(如 class, href, src, data-v 等)
|
||||
if (config.attr) {
|
||||
return (el.getAttribute(config.attr) || "").trim();
|
||||
}
|
||||
if (el == null) {
|
||||
return "未找到"
|
||||
}
|
||||
|
||||
const tagName = el.tagName;
|
||||
if (tagName === "IMG") return el.getAttribute("src");
|
||||
if (tagName === "A") {
|
||||
let href = el.getAttribute("href");
|
||||
return href && !href.startsWith("http") ? window.location.origin + href : href;
|
||||
}
|
||||
|
||||
// 默认提取文字,并清洗
|
||||
return el.innerText.replace(/\n/g, "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取列表的数据
|
||||
* @param {*配置} config
|
||||
* @param {*父节点} rootDom
|
||||
*/
|
||||
async function processList(config, rootDom) {
|
||||
let allList = [];
|
||||
let pageCount = 0;
|
||||
while (true) {
|
||||
pageCount++;
|
||||
const allElements = rootDom.querySelectorAll(config.className);
|
||||
|
||||
const elements = Array.from(allElements);
|
||||
for (const element of elements) {
|
||||
let itemData = await process(config.keys, element)
|
||||
allList.push(itemData)
|
||||
}
|
||||
//1.如果没有配置分页,抓一页自动退出
|
||||
if (!config.pagination) {
|
||||
console.log("未配置分页信息,抓取单页后结束。");
|
||||
break;
|
||||
}
|
||||
// 2.如果达到最大页数限制,强制停止
|
||||
if (config.pagination.maxPage && pageCount >= config.pagination.maxPage) {
|
||||
console.log("已达到配置的最大页数,停止。");
|
||||
break;
|
||||
}
|
||||
// 3. 如果找不到下一页按钮,结束
|
||||
const nextBtn = document.querySelector(config.pagination.nextBtn);
|
||||
if (!nextBtn) {
|
||||
console.log("未找到下一页按钮,抓取结束。");
|
||||
break;
|
||||
} else {
|
||||
nextBtn.click();
|
||||
await sleep(config.pagination.delay);
|
||||
}
|
||||
}
|
||||
return allList
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取表格的数据
|
||||
*/
|
||||
async function processTable(config, rootDom) {
|
||||
let allTableData = [];
|
||||
let pageCount = 0;
|
||||
|
||||
while (true) {
|
||||
pageCount++;
|
||||
//锁定所有 Table Parts 的 tr
|
||||
const partsNodes = {};
|
||||
|
||||
config.tableParts.forEach(part => {
|
||||
partsNodes[part.name] = rootDom.querySelectorAll(`${part.select} tr`);
|
||||
});
|
||||
|
||||
|
||||
// //以第一个part的行数为准,进行横向扫描
|
||||
const rowCount = partsNodes[config.tableParts[0].name]?.length || 0
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
let rowData = {};
|
||||
//遍历keys,根据part映射,取对应的里面找
|
||||
for (const keyItem of config.keys) {
|
||||
const targetRowNode = partsNodes[keyItem.part][i];
|
||||
|
||||
if (targetRowNode) {
|
||||
//提取值
|
||||
if (keyItem.keys) {
|
||||
rowData[keyItem.label] = await process(keyItem.keys, targetRowNode)
|
||||
} else {
|
||||
rowData[keyItem.label] = extractValue(targetRowNode.querySelector(keyItem.className), keyItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
allTableData.push(rowData);
|
||||
}
|
||||
//1.如果没有配置分页,抓一页自动退出
|
||||
if (!config.pagination) {
|
||||
console.log("未配置分页信息,抓取单页后结束。");
|
||||
break;
|
||||
}
|
||||
// 2.如果达到最大页数限制,强制停止
|
||||
if (config.pagination.maxPage && pageCount >= config.pagination.maxPage) {
|
||||
console.log("已达到配置的最大页数,停止。");
|
||||
break;
|
||||
}
|
||||
// 3. 如果找不到下一页按钮,结束
|
||||
const nextBtn = document.querySelector(config.pagination.nextBtn);
|
||||
if (!nextBtn) {
|
||||
console.log("未找到下一页按钮,抓取结束。");
|
||||
break;
|
||||
}
|
||||
// 4.检擦按钮是否被禁用
|
||||
const isDisabled = config.pagination.disabledClass ? nextBtn.classList.contains(config.pagination.disabledClass) : nextBtn.disabled;
|
||||
|
||||
if (isDisabled) {
|
||||
console.log("下一页按钮已禁用,抓取结束。");
|
||||
break;
|
||||
}
|
||||
|
||||
//下一页
|
||||
nextBtn.click();
|
||||
await sleep(config.pagination.delay);
|
||||
}
|
||||
return allTableData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let data = await process(column, document.body)
|
||||
|
||||
|
||||
console.log("==== 提取成功 ====");
|
||||
console.log(data);
|
||||
return data
|
||||
})()
|
||||
Reference in New Issue
Block a user