BOOS岗位下载器

【2025】【长期维护】一键批量抓取并导出BOSS直聘职位信息,让求职数据分析变得更简单!反馈问题或建议请加QQ群:685904930

// ==UserScript==
// @name         BOOS岗位下载器
// @namespace    bossXiaZaiGongZuo
// @version      1.0.0
// @author       TING软件科技
// @description  【2025】【长期维护】一键批量抓取并导出BOSS直聘职位信息,让求职数据分析变得更简单!反馈问题或建议请加QQ群:685904930
// @license      AGPL v3
// @icon         https://u2233.vip/favicon.ico
// @match        https://www.zhipin.com/web/geek/jobs*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// @require      data:application/javascript,%3Bwindow.Vue%20%3D%20Vue%3B
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/index.full.min.js
// @connect      wan.baidu.com
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @noframes
// ==/UserScript==

(t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const e=document.createElement("style");e.textContent=t,document.head.append(e)})(" #AppBtn[data-v-6221d9fc]{position:fixed;bottom:100px;right:100px}#tingApp[data-v-5ec82447]{position:fixed;z-index:99999} ");

(function (vue, ElementPlus) {
  'use strict';

  const _export_sfc = (sfc, props) => {
    const target = sfc.__vccOpts || sfc;
    for (const [key, val] of props) {
      target[key] = val;
    }
    return target;
  };
  const _sfc_main$1 = {
    __name: "AppBtn",
    setup(__props) {
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.unref(ElementPlus.ElButton), {
          type: "primary",
          id: "AppBtn",
          size: "large"
        }, {
          default: vue.withCtx(() => [
            vue.renderSlot(_ctx.$slots, "default", {}, void 0, true)
          ]),
          _: 3
        });
      };
    }
  };
  const AppBtn = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-6221d9fc"]]);
  const _hoisted_1 = { style: { "margin-bottom": "20px" } };
  const _hoisted_2 = { key: 0 };
  const _hoisted_3 = { style: { "display": "flex", "flex-wrap": "wrap", "gap": "4px" } };
  const _hoisted_4 = { style: { "display": "flex", "flex-wrap": "wrap", "gap": "4px" } };
  const _hoisted_5 = { style: { "margin-top": "20px", "display": "flex", "justify-content": "space-between", "align-items": "center" } };
  const _hoisted_6 = { key: 1 };
  const _sfc_main = {
    __name: "App",
    setup(__props) {
      let drawer = vue.ref(false);
      const jobs = vue.ref([]);
      const currentPage = vue.ref(1);
      const pageSize = vue.ref(50);
      const downloading = vue.ref(false);
      const autoDetect = vue.ref(false);
      let autoDetectTimer = null;
      const totalPages = vue.computed(() => {
        return Math.ceil(jobs.value.length / pageSize.value);
      });
      const paginatedJobs = vue.computed(() => {
        const start = (currentPage.value - 1) * pageSize.value;
        const end = start + pageSize.value;
        return jobs.value.slice(start, end);
      });
      const toggleAutoDetect = () => {
        autoDetect.value = !autoDetect.value;
        if (autoDetect.value) {
          ElementPlus.ElMessage.success("已开启自动爬取岗位功能");
          getJobs();
          autoDetectTimer = setInterval(() => {
            getJobs();
          }, 3e3);
        } else {
          stopInterval();
          ElementPlus.ElMessage.info("已暂停爬取岗位功能");
        }
      };
      const stopInterval = () => {
        if (autoDetectTimer) {
          clearInterval(autoDetectTimer);
          autoDetectTimer = null;
        }
      };
      const getJobs = () => {
        try {
          const jobContainer = document.querySelector("#wrap > div.page-jobs-main");
          if (!jobContainer) {
            if (autoDetect.value) {
              console.log("未找到岗位列表容器");
            } else {
              ElementPlus.ElMessage.error("未找到岗位列表容器,请确保在BOSS直聘岗位搜索页面使用");
            }
            return;
          }
          const vueInstance = jobContainer.__vue__;
          if (!vueInstance || !vueInstance.jobList) {
            if (autoDetect.value) {
              console.log("无法爬取岗位数据");
            } else {
              ElementPlus.ElMessage.error("无法爬取岗位数据,请刷新页面后重试");
            }
            return;
          }
          const rawJobs = vueInstance.jobList || [];
          console.log("原始岗位数据:", rawJobs);
          const formattedJobs = rawJobs.map((job) => {
            return {
              jobId: job.jobId,
              encryptJobId: job.encryptJobId,
              jobName: job.jobName,
              brandName: job.brandName,
              salaryDesc: job.salaryDesc,
              jobLabels: job.jobLabels || [],
              skills: job.skills || [],
              areaDistrict: job.areaDistrict,
              jobExperience: job.jobExperience,
              jobDegree: job.jobDegree,
              cityName: job.cityName,
              brandLogo: job.brandLogo,
              brandStageName: job.brandStageName,
              brandIndustry: job.brandIndustry,
              brandScaleName: job.brandScaleName,
              bossName: job.bossName,
              bossTitle: job.bossTitle,
              bossAvatar: job.bossAvatar,
              goldHunter: job.goldHunter,
              jobUrl: `https://www.zhipin.com/job_detail/${job.encryptJobId}.html`,
              securityId: job.securityId,
              lid: job.lid
            };
          });
          const uniqueJobs = [];
          const jobIds = /* @__PURE__ */ new Set();
          formattedJobs.forEach((job) => {
            if (!jobIds.has(job.encryptJobId)) {
              jobIds.add(job.encryptJobId);
              uniqueJobs.push(job);
            }
          });
          if (jobs.value.length === uniqueJobs.length) {
            ElementPlus.ElNotification({
              title: "当前页面没有数据了~",
              type: "success",
              duration: 5e3
            });
            autoDetect.value = false;
            stopInterval();
            return;
          }
          jobs.value = uniqueJobs;
          if (autoDetect.value) {
            console.log(`自动检测: 爬取到 ${uniqueJobs.length} 个岗位信息`);
            window.scrollTo({
              top: document.body.scrollHeight,
              behavior: "smooth"
            });
          } else {
            ElementPlus.ElNotification({
              title: "爬取成功",
              message: `成功爬取到 ${uniqueJobs.length} 个岗位信息`,
              type: "success"
            });
          }
        } catch (error) {
          console.error("爬取岗位信息失败:", error);
          if (!autoDetect.value) {
            ElementPlus.ElMessage.error("爬取岗位信息失败: " + error.message);
          }
        }
      };
      const downloadAll = async () => {
        if (jobs.value.length === 0) {
          ElementPlus.ElMessage.warning("没有岗位信息可下载");
          return;
        }
        downloading.value = true;
        try {
          exportToCSV(jobs.value);
          ElementPlus.ElNotification({
            title: "下载成功",
            message: `成功下载 ${jobs.value.length} 个岗位信息`,
            type: "success"
          });
        } catch (error) {
          console.error("下载失败:", error);
          ElementPlus.ElMessage.error("下载失败: " + error.message);
        } finally {
          downloading.value = false;
        }
      };
      const exportToCSV = (jobsData) => {
        const headers = [
          "职位名称",
          "公司名称",
          "薪资",
          "城市",
          "地区",
          "经验要求",
          "学历要求",
          "技能要求",
          "标签",
          "公司行业",
          "公司规模",
          "HR姓名",
          "HR职位",
          "职位链接"
        ];
        const chunkSize = 1e3;
        const chunks = [];
        for (let i = 0; i < jobsData.length; i += chunkSize) {
          chunks.push(jobsData.slice(i, i + chunkSize));
        }
        const csvChunks = chunks.map((chunk) => {
          return chunk.map(
            (job) => [
              `"${escapeCSVField(job.jobName || "")}"`,
              `"${escapeCSVField(job.brandName || "")}"`,
              `"${escapeCSVField(job.salaryDesc || "")}"`,
              `"${escapeCSVField(job.cityName || "")}"`,
              `"${escapeCSVField(job.areaDistrict || "")}"`,
              `"${escapeCSVField(job.jobExperience || "")}"`,
              `"${escapeCSVField(job.jobDegree || "")}"`,
              `"${escapeCSVField((job.skills || []).join("|"))}"`,
              `"${escapeCSVField((job.jobLabels || []).join("|"))}"`,
              `"${escapeCSVField(job.brandIndustry || "")}"`,
              `"${escapeCSVField(job.brandScaleName || "")}"`,
              `"${escapeCSVField(job.bossName || "")}"`,
              `"${escapeCSVField(job.bossTitle || "")}"`,
              `"${escapeCSVField(job.jobUrl || "")}"`
            ].join(",")
          );
        });
        const csvContent = [headers.join(","), ...csvChunks.flat()].join("\n");
        const blob = new Blob(["\uFEFF" + csvContent], {
          type: "text/csv;charset=utf-8;"
        });
        const url = URL.createObjectURL(blob);
        const link2 = document.createElement("a");
        link2.setAttribute("href", url);
        link2.setAttribute(
          "download",
          `BOSS岗位信息_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`
        );
        link2.style.visibility = "hidden";
        document.body.appendChild(link2);
        link2.click();
        document.body.removeChild(link2);
      };
      const escapeCSVField = (field) => {
        if (typeof field !== "string") return field;
        return field.replace(/"/g, '""');
      };
      vue.onUnmounted(() => {
        if (autoDetectTimer) {
          clearInterval(autoDetectTimer);
        }
      });
      return (_ctx, _cache) => {
        const _component_el_button = vue.resolveComponent("el-button");
        const _component_el_alert = vue.resolveComponent("el-alert");
        const _component_el_table_column = vue.resolveComponent("el-table-column");
        const _component_el_link = vue.resolveComponent("el-link");
        const _component_el_tag = vue.resolveComponent("el-tag");
        const _component_el_table = vue.resolveComponent("el-table");
        const _component_el_pagination = vue.resolveComponent("el-pagination");
        const _component_el_drawer = vue.resolveComponent("el-drawer");
        return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [
          vue.createVNode(AppBtn, {
            onClick: _cache[0] || (_cache[0] = ($event) => vue.isRef(drawer) ? drawer.value = !vue.unref(drawer) : drawer = !vue.unref(drawer))
          }, {
            default: vue.withCtx(() => _cache[3] || (_cache[3] = [
              vue.createTextVNode("打 开 插 件", -1)
            ])),
            _: 1,
            __: [3]
          }),
          vue.createVNode(_component_el_drawer, {
            modelValue: vue.unref(drawer),
            "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => vue.isRef(drawer) ? drawer.value = $event : drawer = $event),
            title: "BOSS岗位信息下载工具",
            size: "80%"
          }, {
            default: vue.withCtx(() => [
              vue.createElementVNode("div", _hoisted_1, [
                vue.createVNode(_component_el_button, {
                  onClick: toggleAutoDetect,
                  type: autoDetect.value ? "danger" : "primary"
                }, {
                  default: vue.withCtx(() => [
                    vue.createTextVNode(vue.toDisplayString(autoDetect.value ? "暂停爬取" : "爬取【当前页】岗位"), 1)
                  ]),
                  _: 1
                }, 8, ["type"]),
                vue.createVNode(_component_el_button, {
                  onClick: downloadAll,
                  type: "success",
                  loading: downloading.value,
                  disabled: jobs.value.length === 0,
                  style: { "margin-left": "20px" }
                }, {
                  default: vue.withCtx(() => [
                    vue.createTextVNode(vue.toDisplayString(downloading.value ? "下载中..." : `下载全部岗位 (${jobs.value.length}个)`), 1)
                  ]),
                  _: 1
                }, 8, ["loading", "disabled"])
              ]),
              vue.createVNode(_component_el_alert, {
                title: "免责声明",
                type: "error",
                description: "本插件仅用于学习和技术研究目的,不得用于任何商业用途。使用本插件产生的任何后果均由使用者自行承担,开发者不承担任何责任。",
                "show-icon": "",
                style: { "margin-bottom": "20px" },
                closable: ""
              }),
              jobs.value.length > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2, [
                vue.createVNode(_component_el_alert, {
                  title: `共爬取到 ${jobs.value.length} 个岗位信息,当前显示第 ${currentPage.value} 页,共 ${totalPages.value} 页`,
                  type: "success",
                  "show-icon": "",
                  style: { "margin-bottom": "20px" }
                }, null, 8, ["title"]),
                vue.createVNode(_component_el_table, {
                  data: paginatedJobs.value,
                  height: "500",
                  style: { "width": "100%" },
                  border: ""
                }, {
                  default: vue.withCtx(() => [
                    vue.createVNode(_component_el_table_column, {
                      type: "index",
                      label: "#",
                      width: "60"
                    }, {
                      default: vue.withCtx((scope) => [
                        vue.createTextVNode(vue.toDisplayString((currentPage.value - 1) * pageSize.value + scope.$index + 1), 1)
                      ]),
                      _: 1
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "jobName",
                      label: "职位名称",
                      "min-width": "150"
                    }, {
                      default: vue.withCtx((scope) => [
                        vue.createVNode(_component_el_link, {
                          href: scope.row.jobUrl,
                          target: "_blank",
                          type: "primary"
                        }, {
                          default: vue.withCtx(() => [
                            vue.createTextVNode(vue.toDisplayString(scope.row.jobName), 1)
                          ]),
                          _: 2
                        }, 1032, ["href"])
                      ]),
                      _: 1
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "brandName",
                      label: "公司名称",
                      "min-width": "120"
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "salaryDesc",
                      label: "薪资",
                      width: "100"
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "skills",
                      label: "技能要求",
                      "min-width": "200"
                    }, {
                      default: vue.withCtx((scope) => [
                        vue.createElementVNode("div", _hoisted_3, [
                          (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(scope.row.skills, (skill) => {
                            return vue.openBlock(), vue.createBlock(_component_el_tag, {
                              key: skill,
                              size: "small"
                            }, {
                              default: vue.withCtx(() => [
                                vue.createTextVNode(vue.toDisplayString(skill), 1)
                              ]),
                              _: 2
                            }, 1024);
                          }), 128))
                        ])
                      ]),
                      _: 1
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "jobLabels",
                      label: "标签",
                      "min-width": "150"
                    }, {
                      default: vue.withCtx((scope) => [
                        vue.createElementVNode("div", _hoisted_4, [
                          (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(scope.row.jobLabels, (tag) => {
                            return vue.openBlock(), vue.createBlock(_component_el_tag, {
                              key: tag,
                              size: "small"
                            }, {
                              default: vue.withCtx(() => [
                                vue.createTextVNode(vue.toDisplayString(tag), 1)
                              ]),
                              _: 2
                            }, 1024);
                          }), 128))
                        ])
                      ]),
                      _: 1
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "areaDistrict",
                      label: "地区",
                      width: "100"
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "jobExperience",
                      label: "经验要求",
                      width: "100"
                    }),
                    vue.createVNode(_component_el_table_column, {
                      prop: "jobDegree",
                      label: "学历要求",
                      width: "100"
                    })
                  ]),
                  _: 1
                }, 8, ["data"]),
                vue.createElementVNode("div", _hoisted_5, [
                  vue.createVNode(_component_el_pagination, {
                    "current-page": currentPage.value,
                    "onUpdate:currentPage": _cache[1] || (_cache[1] = ($event) => currentPage.value = $event),
                    "page-size": pageSize.value,
                    total: jobs.value.length,
                    layout: "prev, pager, next, jumper",
                    background: "",
                    style: { "margin": "0 auto" }
                  }, null, 8, ["current-page", "page-size", "total"])
                ])
              ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_6, [
                vue.createVNode(_component_el_alert, {
                  title: "暂无岗位信息,点击上方按钮开始爬取岗位信息",
                  type: "info",
                  "show-icon": ""
                })
              ]))
            ]),
            _: 1
          }, 8, ["modelValue"])
        ], 64);
      };
    }
  };
  const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5ec82447"]]);
  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  window.GM_xmlhttpRequest = _GM_xmlhttpRequest;
  window.GM_setValue = _GM_setValue;
  window.GM_getValue = _GM_getValue;
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = "https://cdn.jsdelivr.net/npm/[email protected]/dist/index.css";
  document.head.appendChild(link);
  const appContainer = document.createElement("div");
  const appId = `tingApp`;
  appContainer.id = appId;
  document.body.appendChild(appContainer);
  vue.createApp(App).use(ElementPlus).mount(appContainer);

})(Vue, ElementPlus);