油猴 1688列表自动爬取数据

1585364631
2026-03-14 / 0 评论 / 2 阅读 / 正在检测是否收录...

油猴 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

评论 (0)

取消