油猴 1688列表自动爬取数据
此代码为油猴脚本,用于1688商品搜索列表和店铺商品列表爬取自动数据,请在商品两处地方补充自己的代码,将商品信息上传到自己的数据库。
代码原理是,hook 1688的xhr请求,请求中途拦截响应,取出请求内容处理。但是有些请求是通过JSONP的方式,就是通过script标签动态插入body,从而让浏览器加载,所以同样监听了script加载内容回调,符合的响应内容则返回。对于商品详情的数据,则通过iframe,父页面开启监听,子页面进行推送。
// ==UserScript==
// @name 1688列表自动爬取数据
// @namespace http://tampermonkey.net/
// @version 0.2
// @description 1688列表自动爬取数据
// @author 我亏一点
// @match *.1688.com/page/offerlist.htm*
// @match *.1688.com/offer/*.html*
// @match *.1688.com/selloffer/offer_search.htm*
// @icon https://s1.ax1x.com/2022/10/14/xwsJYT.png
// @grant none
// ==/UserScript==
(function () {
"use strict";
const config = {
lock: false, //是否自动爬取 true为是,false为否
run_status: false, //是否正在拦截爬取
sleep: 5, // 爬取间隔 (秒)
iframe_timeout: 3600, // 最大等待时间(秒),防止卡死
debug: false, // 是否开启测试,开启只会爬一个数据
data: [], //数据集
is_iframe: window.self !== window.top,
type: "", // 页面类型 shop_list为店铺商品列表,goods_list为搜索商品列表,detail为商品详情
type_match: {
shop_list: "mtop.alibaba.alisite.cbu.server.moduleasyncservice", // 店铺商品列表
goods_list: "mtop.relationrecommend.wirelessrecommend.recommend", // 搜索商品列表
},
};
//================创建面板开始====================
// 创建一个 div 元素用于显示日志
const logDiv = document.createElement("div");
logDiv.id = "log-div-1688";
logDiv.style.position = "fixed";
logDiv.style.bottom = "10px";
logDiv.style.left = "10px";
logDiv.style.width = "400px";
logDiv.style.height = "300px";
logDiv.style.overflowY = "auto";
logDiv.style.border = "10px solid black";
logDiv.style.backgroundColor = "white";
logDiv.style.zIndex = 9999;
logDiv.style.padding = "20px";
logDiv.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.1)";
logDiv.style.wordWrap = "break-word";
logDiv.style.overflowWrap = "break-word";
logDiv.style.fontFamily = "monospace";
logDiv.style.fontSize = "12px";
// 日志容器 - 使用单独的容器存放日志条目
const logContainer = document.createElement("div");
logContainer.id = "log-container-1688";
logContainer.style.maxHeight = "240px";
logContainer.style.overflowY = "auto";
logDiv.appendChild(logContainer);
// 日志头部
const logHeader = document.createElement("p");
logHeader.style.color = "green";
logHeader.style.fontWeight = "bold";
logHeader.style.margin = "0 0 10px 0";
logHeader.textContent = "插件初始化——我亏一点";
logDiv.insertBefore(logHeader, logContainer);
document.body.appendChild(logDiv);
// 日志配置
const logConfig = {
maxLines: 200, // 最大日志行数
batchUpdate: true, // 批量更新
autoScroll: true, // 自动滚动
};
// 日志缓冲队列
let logBuffer = [];
let logUpdateTimer = null;
let pendingScroll = false; // 标记是否需要滚动
// 优化后的日志添加函数
function addLog(html_code, lock = true) {
logBuffer.push(html_code);
// 标记需要滚动
if (lock && logConfig.autoScroll) {
pendingScroll = true;
}
// 批量更新,避免频繁 DOM 操作
if (!logUpdateTimer) {
logUpdateTimer = setTimeout(flushLogBuffer, 50);
}
}
// 刷新日志缓冲
function flushLogBuffer() {
if (logBuffer.length === 0) {
logUpdateTimer = null;
return;
}
// 使用 DocumentFragment 批量插入
const fragment = document.createDocumentFragment();
logBuffer.forEach((html) => {
const p = document.createElement("p");
p.innerHTML = html;
p.style.margin = "2px 0";
fragment.appendChild(p);
});
logContainer.appendChild(fragment);
// 限制日志行数,自动清理旧日志
while (logContainer.children.length > logConfig.maxLines) {
logContainer.removeChild(logContainer.firstChild);
}
logBuffer = [];
logUpdateTimer = null;
// DOM 更新后执行滚动
if (pendingScroll) {
pendingScroll = false;
scrollToBottom();
}
}
// 滚动到底部函数
function scrollToBottom() {
requestAnimationFrame(() => {
setTimeout(() => {
logContainer.scrollTop = logContainer.scrollHeight;
}, 10);
});
}
// 清空日志功能
function clearLog() {
logContainer.innerHTML = "";
logBuffer = [];
pendingScroll = false;
}
// 添加清空按钮到日志头部
const clearBtn = document.createElement("button");
clearBtn.textContent = "清空日志";
clearBtn.style.marginLeft = "10px";
clearBtn.style.fontSize = "10px";
clearBtn.style.cursor = "pointer";
clearBtn.onclick = clearLog;
logHeader.appendChild(clearBtn);
// 拖动相关变量
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
logDiv.addEventListener("mousedown", dragStart);
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragEnd);
function dragStart(e) {
if (e.target.tagName === "BUTTON" || e.target.id === "start_run_button") {
return;
}
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === logDiv) {
isDragging = true;
logDiv.style.cursor = "grabbing";
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, logDiv);
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
logDiv.style.cursor = "grab";
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
//================创建面板结束====================
//================页面检测开始==================
// 如果跳转登录,提示请登录
if (window.location.href.includes("login.taobao.com")) {
addLog("<p style='color: red'>请自行登录账号</p>");
return;
}
// 如果在商品详情页面
if (window.location.href.includes("detail.1688.com/offer")) {
config.type = "detail";
addLog("<p style='color:green'>已进入商品详情页面</p>");
const item_detail = window.context.result.global.globalData.model;
addLog(
`<p style='color:green'>商品编号:${item_detail.offerDetail.offerId}</p>`,
);
addLog(
`<p style='color:green'>商品名称:${item_detail.offerDetail.subject}</p>`,
);
addLog(
`<p style='color:green'>商品价格:${item_detail.tradeModel.priceDisplay}</p>`,
);
addLog(
`<p style='color:green'>商品首图:${item_detail.offerDetail.mainImageList[0].fullPathImageURI}</p>`,
);
if (config.is_iframe) {
addLog(`<p style='color:green'>检测到子页面,自动推送数据</p>`);
window.parent.postMessage(
{
type: "item_detail",
data: item_detail,
},
"*",
);
}
return;
}
// 如果在店铺商品列表或者搜索商品列表页面
else if (
window.location.href.includes("1688.com/page/offerlist.htm") ||
window.location.href.includes("1688.com/selloffer/offer_search.htm")
) {
// 如果是店铺商品列表页面
if (window.location.href.includes("1688.com/page/offerlist.htm")) {
config.type = "shop_list";
addLog("<p style='color:green'>已进入店铺商品列表页面</p>");
}
// 如果是搜索商品列表页面
if (window.location.href.includes("1688.com/selloffer/offer_search.htm")) {
config.type = "goods_list";
addLog("<p style='color:green'>已进入搜索商品列表页面</p>");
}
// 如果不在子页面,则监听消息
if (!config.is_iframe) {
addLog("<p style='color:green'>自动监听消息</p>");
window.addEventListener("message", function (e) {
if (e.data.type === "item_detail") {
const item_detail = e.data.data;
addLog(
`<p style='color:green'>商品编号:${item_detail.offerDetail.offerId}</p>`,
);
addLog(
`<p style='color:green'>商品名称:${item_detail.offerDetail.subject}</p>`,
);
addLog(
`<p style='color:green'>商品价格:${item_detail.tradeModel.priceDisplay}</p>`,
);
addLog(
`<p style='color:green'>商品首图:${item_detail.offerDetail.mainImageList[0].fullPathImageURI}</p>`,
);
if (typeof currentItemResolver === "function") {
currentItemResolver(item_detail);
}
// 可以在这里写把数据保存到数据库的逻辑
}
});
}
// 如果配置锁开启,则自动开始爬取
if (config.lock) {
addLog(
"<p style='color: blue'>自动爬取:" +
(config.lock ? "启动" : "关闭") +
"</p>",
);
window.onload = () => {
run();
};
} else {
// 否则添加开始按钮
const startBtn = document.createElement("button");
startBtn.textContent = "开始爬取";
startBtn.style.marginLeft = "10px";
startBtn.style.fontSize = "10px";
startBtn.style.cursor = "pointer";
startBtn.onclick = run;
logHeader.appendChild(startBtn);
}
}
// 如果不是商品列表页面,则提示请进入商品列表页面
else {
addLog("<p style='color: red'>请进入商品列表页面</p>");
return;
}
//================页面检测结束==================
//================拦截请求开始==================
// hook请求方法
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function () {
originalOpen.apply(this, arguments);
this.addEventListener("load", function () {
switch (config.type) {
case "shop_list":
if (
this.responseURL
.toLocaleLowerCase()
.includes(config.type_match.shop_list.toLocaleLowerCase()) &&
config.run_status
) {
try {
const list = JSON.parse(this.responseText).data.content.offerList;
if (list) {
const pageNum =
document.querySelector(
'div[style*="background: rgb(255, 64, 0)"]',
)?.textContent || "未知";
addLog("<p>开始爬取第 " + pageNum + " 页</p>");
processAllItemsSequentially(list).then(() => {
setTimeout(() => {
if (
document.querySelectorAll("button")[1] &&
!config.debug
) {
document.querySelectorAll("button")[1].click();
}
}, config.sleep * 1000);
});
}
} catch (e) {
addLog("<p style='color: red'>列表数据解析失败</p>");
}
}
break;
default:
break;
}
});
};
XMLHttpRequest.prototype.send = function () {
originalSend.apply(this, arguments);
};
//================拦截请求结束==================
//================拦截jsonp请求开始==================
// 保存原生 src 的 setter
const srcDescriptor = Object.getOwnPropertyDescriptor(
HTMLScriptElement.prototype,
"src",
);
const originalSrcSetter = srcDescriptor.set;
// 重写 src setter,拦截所有 script.src 赋值
Object.defineProperty(HTMLScriptElement.prototype, "src", {
// 重写 setter
set(url) {
// 如果不是 1688/淘宝的 mtop JSONP,直接走原生逻辑
if (typeof url !== "string" || !isMtopJsonpUrl(url)) {
return originalSrcSetter.call(this, url);
}
// 从 URL 中取出 callback 参数
const callbackName = extractCallbackName(url);
if (!callbackName) {
return originalSrcSetter.call(this, url);
}
// 在真正设置 src 之前,先 hook 这个全局回调函数
hookMtopJsonpCallback(callbackName, url);
// 再让浏览器正常去加载这个 script
return originalSrcSetter.call(this, url);
},
get() {
return srcDescriptor.get.call(this);
},
configurable: true,
});
// 判断是否是 1688/淘宝 mtop JSONP 请求
function isMtopJsonpUrl(url) {
if (!url) return false;
const lower = url.toLowerCase();
return (
(lower.includes("h5api.m.1688.com/h5/mtop.") ||
lower.includes("h5api.m.taobao.com/h5/mtop.")) &&
/[?&]callback=mtopjsonp/i.test(url)
);
}
// 从 URL 中解析 callback 参数名
function extractCallbackName(url) {
try {
const search = url.split("?")[1];
if (!search) return null;
const params = new URLSearchParams(search);
const callback = params.get("callback");
return callback || null;
} catch (e) {
return null;
}
}
/**
* 动态替换全局 JSONP 回调函数
* @param {string} callbackName - 如 mtopjsonp5
* @param {string} url - 原始 JSONP URL(用于日志、匹配接口)
*/
function hookMtopJsonpCallback(callbackName, url) {
const win = window;
// 如果回调已经是函数,则直接劫持覆盖
if (typeof win[callbackName] === "function") {
// 如果已经被 hook 过,就不重复,防止同页面请求爬取多次
if (win["__" + callbackName + "__hooked__"]) return;
win["__" + callbackName + "__hooked__"] = true;
const raw = win[callbackName];
win[callbackName] = function (data) {
handleMtopJsonResponse(url, data);
return raw.apply(this, arguments);
};
return;
}
// 如果没有回调函数,则劫持函数赋值,等待赋值再进行劫持
Object.defineProperty(win, callbackName, {
configurable: true, // 允许后续重新定义
enumerable: true,
get: function () {
// 返回 undefined 或者你缓存的函数,通常不影响逻辑
return win["__proxy_" + callbackName];
},
set: function (realCallback) {
// 此时页面正在把真正的回调函数赋值给这个变量
// 我们把这个属性"篡改"成我们的函数,并保存真正的回调
Object.defineProperty(win, callbackName, {
value: function (data) {
// console.log("[JSONP HOOK] 拿到数据:", data);
// 拦截数据处理
handleMtopJsonResponse(url, data);
// 继续运行原函数
if (typeof realCallback === "function") {
return realCallback.apply(this, arguments);
}
},
writable: true, // 允许被删除或修改
configurable: true, // 允许被删除
enumerable: true,
});
},
});
}
/**
* 根据接口类型分发处理
* @param {string} url
* @param {any} data
*/
function handleMtopJsonResponse(url, data) {
const lowerUrl = url.toLowerCase();
if (
lowerUrl.includes("mtop.relationrecommend.wirelessrecommend.recommend")
) {
handleGoodsListJsonp(data);
}
}
/**
* 处理搜索列表 goods_list 的 JSONP 数据
* @param {any} data
*/
function handleGoodsListJsonp(data) {
if (!config.run_status) return;
try {
const list =
data &&
data.data &&
data.data.data &&
data.data.data.OFFER &&
data.data.data.OFFER.items;
if (!list) {
addLog("<p style='color: red'>JSONP 列表数据格式异常</p>");
return;
}
const pageNum =
document.querySelector(".fui-current")?.textContent || "未知";
addLog("<p>开始爬取第 " + pageNum + " 页(JSONP)</p>");
processAllItemsSequentially(list).then(() => {
setTimeout(() => {
if (document.querySelector(".fui-next") && !config.debug) {
document.querySelector(".fui-next").click();
}
}, config.sleep * 1000);
});
} catch (e) {
console.error(e);
addLog("<p style='color: red'>JSONP 列表数据解析失败</p>");
}
}
//================拦截jsonp请求结束==================
//=================处理商品详情开始==================
// 用于在拦截器和处理函数之间通信
let currentItemResolver = null;
function run() {
if (config.run_status) {
addLog("<p style='color: blue'>已开始自动爬取</p>");
return;
}
config.run_status = true;
addLog("<p style='color: blue'>开始自动爬取</p>");
let firstPageBtn = null;
if (config.type === "goods_list") {
firstPageBtn = document.querySelector(".fui-current");
}
if (config.type === "shop_list") {
firstPageBtn = document.querySelector(
'div[style*="background: rgb(255, 64, 0)"]',
);
}
if (firstPageBtn) {
firstPageBtn.click();
} else {
addLog("<p style='color: red'>未找到分页按钮,请确保在商品列表页</p>");
}
}
async function processItemDetail(item) {
return new Promise((resolve) => {
addLog(`<p style='color: blue'>开始处理商品: ${item.subject}</p>`);
const iframe = document.createElement("iframe");
iframe.style.width = "800px";
iframe.style.height = "600px";
iframe.style.border = "1px solid #ccc";
iframe.style.position = "fixed";
iframe.style.top = "0";
iframe.style.right = "0";
iframe.style.zIndex = "9999";
iframe.src = `https://detail.1688.com/offer/${item.id}.html`;
// 定义清理和关闭的逻辑
const cleanup = (data = null) => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
addLog(
`<p style='color: green'>商品: ${item.subject} 处理完成${data ? " (数据已保存)" : " (超时或失败)"}</p>`,
);
}
resolve();
};
// 将 resolver 暴露给全局拦截器
currentItemResolver = cleanup;
// 设置一个最大超时时间,防止某些商品加载不出数据导致脚本卡死
const timeoutId = setTimeout(() => {
if (currentItemResolver === cleanup) {
addLog(
`<p style='color: red'>商品: ${item.subject} 捕获超时,自动跳过</p>`,
);
currentItemResolver = null;
cleanup();
}
}, config.iframe_timeout * 1000);
iframe.onload = function () {};
iframe.onerror = function () {
addLog(
`<p style='color: red'>商品: ${item.subject} iframe加载失败</p>`,
);
clearTimeout(timeoutId);
cleanup();
};
document.body.appendChild(iframe);
});
}
async function processAllItemsSequentially(list) {
for (const item of list) {
if (config.type == "goods_list") {
item.subject = item.data.title;
item.id = item.data.offerId;
}
if (item && item.id) {
// 可以对接数据库校验是否已存在
await processItemDetail(item);
if (config.debug) {
break;
}
// 休眠
addLog(`<p style='color: blue'>等待${config.sleep}秒</p>`);
await new Promise((r) => setTimeout(r, config.sleep * 1000));
}
}
addLog("<p style='color: green'>本页处理完成</p>");
}
//=================处理商品详情结束==================
})();
需补充的两处地方,请搜索 数据库
- 位置1 自动入库
// 如果不在子页面,则监听消息
if (!config.is_iframe) {
addLog("<p style='color:green'>自动监听消息</p>");
window.addEventListener("message", function (e) {
if (e.data.type === "item_detail") {
const item_detail = e.data.data;
addLog(
`<p style='color:green'>商品编号:${item_detail.offerDetail.offerId}</p>`,
);
addLog(
`<p style='color:green'>商品名称:${item_detail.offerDetail.subject}</p>`,
);
addLog(
`<p style='color:green'>商品价格:${item_detail.tradeModel.priceDisplay}</p>`,
);
addLog(
`<p style='color:green'>商品首图:${item_detail.offerDetail.mainImageList[0].fullPathImageURI}</p>`,
);
if (typeof currentItemResolver === "function") {
currentItemResolver(item_detail);
}
// 可以在这里写把数据保存到数据库的逻辑
}
});
}- 位置2 校验数据库是否存在
async function processAllItemsSequentially(list) {
for (const item of list) {
if (config.type == "goods_list") {
item.subject = item.data.title;
item.id = item.data.offerId;
}
if (item && item.id) {
// 可以对接数据库校验是否已存在
await processItemDetail(item);
if (config.debug) {
break;
}
// 休眠
addLog(`<p style='color: blue'>等待${config.sleep}秒</p>`);
await new Promise((r) => setTimeout(r, config.sleep * 1000));
}
}
addLog("<p style='color: green'>本页处理完成</p>");
}
评论 (0)