Boss直聘职位数据采集

采集Boss直聘上的职位数据

// ==UserScript==
// @name         Boss直聘职位数据采集
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  采集Boss直聘上的职位数据
// @author       You
// @match        https://www.zhipin.com/*
// @grant        GM_addStyle
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
  'use strict';

  // 存储token和数据
  let token = GM_getValue('zp_token', '');
  let zp_token = GM_getValue('zp_zp_token', '');
  let cachedData = GM_getValue('zp_cached_data', null);
  let lastFetchTime = GM_getValue('zp_last_fetch', 0);

  // 添加样式
  GM_addStyle(`
    .zp-data-btn {
      position: fixed;
      bottom: 100px;
      right: 20px;
      z-index: 9999;
      background: linear-gradient(135deg, #3a7bd5, #00d2ff);
      color: white;
      border: none;
      border-radius: 50px;
      padding: 12px 24px;
      font-size: 16px;
      font-weight: bold;
      cursor: pointer;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
      transition: all 0.3s ease;
    }

    .zp-data-btn:hover {
      transform: translateY(-3px);
      box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
    }

    .zp-modal {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 10000;
    }

    .zp-modal-content {
      background: white;
      padding: 30px;
      border-radius: 10px;
      width: 400px;
      max-width: 90%;
      box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
    }

    .zp-modal-title {
      font-size: 20px;
      margin-bottom: 20px;
      color: #333;
    }

    .zp-input-group {
      margin-bottom: 20px;
    }

    .zp-input-group label {
      display: block;
      margin-bottom: 8px;
      font-weight: bold;
      color: #555;
    }

    .zp-input-group input {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
      font-size: 16px;
    }

    .zp-modal-actions {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
    }

    .zp-btn {
      padding: 10px 20px;
      border-radius: 5px;
      border: none;
      cursor: pointer;
      font-weight: bold;
    }

    .zp-btn-primary {
      background: linear-gradient(135deg, #3a7bd5, #00d2ff);
      color: white;
    }

    .zp-btn-secondary {
      background: #f0f0f0;
      color: #333;
    }

    /* 数据展示浮窗样式 */
    .zp-data-container {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 80%;
      height: 80%;
      max-width: 1000px;
      background: white;
      border-radius: 10px;
      box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
      z-index: 10001;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      transition: all 0.3s ease;
    }

    .zp-data-container.minimized {
      height: 50px;
      width: 200px;
      overflow: hidden;
    }

    .zp-data-header {
      padding: 15px 20px;
      background: linear-gradient(135deg, #3a7bd5, #00d2ff);
      color: white;
      display: flex;
      justify-content: space-between;
      align-items: center;
      cursor: pointer;
    }

    .zp-data-close {
      background: none;
      border: none;
      color: white;
      font-size: 20px;
      cursor: pointer;
      margin-left: 10px;
    }

    .zp-data-toolbar {
      padding: 15px 20px;
      background: #f5f5f5;
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }

    .zp-data-search {
      flex: 1;
      min-width: 200px;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }

    .zp-data-filter {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
      min-width: 150px;
    }

    .zp-data-date-filter {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }

    .zp-data-content {
      flex: 1;
      overflow-y: auto;
      padding: 20px;
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
      gap: 20px;
    }

    .zp-data-item {
      padding: 15px;
      border: 1px solid #eee;
      border-radius: 8px;
      transition: all 0.2s ease;
      display: flex;
      flex-direction: column;
      height: fit-content;
    }

    .zp-data-item:hover {
      background: #f9f9f9;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }

    .zp-data-item-header {
      display: flex;
      align-items: center;
      margin-bottom: 10px;
    }

    .zp-data-logo {
      width: 40px;
      height: 40px;
      border-radius: 4px;
      margin-right: 10px;
      object-fit: cover;
      background: #f5f5f5;
    }

    .zp-data-company-wrapper {
      flex: 1;
      min-width: 0;
    }

    .zp-data-company {
      font-weight: bold;
      font-size: 16px;
      margin-bottom: 5px;
      color: #3a7bd5;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .zp-data-job {
      font-size: 15px;
      margin-bottom: 8px;
      display: flex;
      justify-content: space-between;
    }

    .zp-data-salary {
      color: #ff6b6b;
      font-weight: bold;
    }

    .zp-data-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      font-size: 13px;
      color: #666;
      margin-bottom: 8px;
    }

    .zp-data-time {
      font-size: 12px;
      color: #999;
      margin-top: auto;
    }

    .zp-data-link {
      display: inline-block;
      margin-top: 8px;
      color: #3a7bd5;
      text-decoration: none;
      font-size: 13px;
    }

    .zp-data-link:hover {
      text-decoration: underline;
    }

    .zp-data-refresh {
      background: none;
      border: none;
      color: white;
      font-size: 16px;
      cursor: pointer;
      margin-left: 10px;
    }

    .zp-data-count {
      font-size: 14px;
      opacity: 0.9;
    }
  `);

  // 创建按钮
  function createButton() {
    const btn = document.createElement('button');
    btn.className = 'zp-data-btn';
    btn.textContent = '获取职位数据';
    btn.addEventListener('click', handleButtonClick);
    document.body.appendChild(btn);
  }

  // 显示token输入弹窗
  function showTokenModal() {
    const modal = document.createElement('div');
    modal.className = 'zp-modal';

    modal.innerHTML = `
      <div class="zp-modal-content">
        <h3 class="zp-modal-title">请输入Token信息</h3>
        <div class="zp-input-group">
          <label for="token">Token</label>
          <input type="text" id="token" placeholder="输入token" value="${token}">
        </div>
        <div class="zp-input-group">
          <label for="zp_token">Zp Token</label>
          <input type="text" id="zp_token" placeholder="输入zp_token" value="${zp_token}">
        </div>
        <div class="zp-modal-actions">
          <button class="zp-btn zp-btn-secondary" id="cancel-token">取消</button>
          <button class="zp-btn zp-btn-primary" id="save-token">确认</button>
        </div>
      </div>
    `;

    document.body.appendChild(modal);

    const cancelBtn = modal.querySelector('#cancel-token');
    const saveBtn = modal.querySelector('#save-token');

    cancelBtn.addEventListener('click', () => {
      document.body.removeChild(modal);
    });

    saveBtn.addEventListener('click', () => {
      const tokenInput = modal.querySelector('#token');
      const zpTokenInput = modal.querySelector('#zp_token');

      token = tokenInput.value.trim();
      zp_token = zpTokenInput.value.trim();

      if (!token || !zp_token) {
        alert('请输入完整的token信息');
        return;
      }

      // 保存token
      GM_setValue('zp_token', token);
      GM_setValue('zp_zp_token', zp_token);

      document.body.removeChild(modal);
      fetchAndDisplayData();
    });
  }

  // 格式化时间
  function formatTime(timestamp) {
    const date = new Date(timestamp);
    return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  }

  // 生成岗位链接
  function generateJobLink(encryptJobId) {
    return `https://www.zhipin.com/job_detail/${encryptJobId}.html?ka=personal_sawme_job_${encryptJobId}`;
  }

  // 获取职位数据
  async function fetchJobData(page) {
    const timestamp = Date.now();
    const url = `https://www.zhipin.com/wapi/zprelation/interaction/geekGetJob?page=${page}&tag=2&_=${timestamp}`;

    const response = await fetch(url, {
      headers: {
        "accept": "application/json, text/plain, */*",
        "token": token,
        "zp_token": zp_token
      },
      method: "GET",
      credentials: "include"
    });

    if (!response.ok) throw new Error(`请求第 ${page} 页失败`);
    return response.json();
  }

  // 处理并显示数据
  async function fetchAndDisplayData(forceRefresh = false) {
    try {
      // 检查缓存是否有效(1小时内)
      const now = Date.now();
      const cacheValid = now - lastFetchTime < 3600000; // 1小时缓存

      if (cachedData && cacheValid && !forceRefresh) {
        console.log('使用缓存数据');
        showData(processData(cachedData));
        return;
      }

      console.log('开始获取数据...');

      // 显示加载状态
      const loading = document.createElement('div');
      loading.className = 'zp-modal';
      loading.innerHTML = `
        <div class="zp-modal-content" style="text-align: center;">
          <h3>正在获取数据,请稍候...</h3>
        </div>
      `;
      document.body.appendChild(loading);

      // 获取25页数据
      const allData = [];
      for (let page = 1; page <= 25; page++) {
        console.log(`正在获取第 ${page} 页...`);
        const data = await fetchJobData(page);
        if(data.zpData && data.zpData.cardList) {
          allData.push(...data.zpData.cardList);
        }
        // 添加延迟避免请求过于频繁
        await new Promise(resolve => setTimeout(resolve, 500));
      }

      // 保存数据和获取时间
      cachedData = allData;
      lastFetchTime = now;
      GM_setValue('zp_cached_data', allData);
      GM_setValue('zp_last_fetch', now);

      // 移除加载状态
      document.body.removeChild(loading);

      // 处理数据
      const processedData = processData(allData);
      showData(processedData);

    } catch (error) {
      console.error('获取数据失败:', error);
      GM_notification({
        title: '获取数据失败',
        text: error.message,
        timeout: 3000
      });
    }
  }

  // 处理数据
  function processData(data) {
    // 按时间排序
    const sortedData = data.sort((a, b) => {
      return new Date(b.happenTime) - new Date(a.happenTime);
    });

    // 按公司名分组
    const companyMap = {};
    sortedData.forEach(item => {
      if (!item.brandName) return;
      if (!companyMap[item.brandName]) {
        companyMap[item.brandName] = [];
      }
      companyMap[item.brandName].push(item);
    });

    return { sortedData, companyMap };
  }

  // 显示数据浮窗
  function showData(data) {
    // 如果已经存在容器,则更新内容
    let container = document.querySelector('.zp-data-container');
    if (!container) {
      container = document.createElement('div');
      container.className = 'zp-data-container';
      document.body.appendChild(container);
    } else {
      // 如果已经最小化,则展开
      container.classList.remove('minimized');
    }

    container.innerHTML = `
      <div class="zp-data-header">
        <div>
          <span>职位数据</span>
          <span class="zp-data-count">(共 ${data.sortedData.length} 条)</span>
        </div>
        <div>
          <button class="zp-data-refresh" title="刷新数据">↻</button>
          <button class="zp-data-close" title="最小化">−</button>
          <button class="zp-data-close" title="关闭">&times;</button>
        </div>
      </div>
      <div class="zp-data-toolbar">
        <input type="text" class="zp-data-search" placeholder="搜索公司/职位...">
        <select class="zp-data-filter">
          <option value="all">全部公司</option>
          ${Object.keys(data.companyMap).map(company =>
        `<option value="${company}">${company}</option>`
    ).join('')}
        </select>
        <input type="date" class="zp-data-date-filter" id="date-from" placeholder="开始日期">
        <input type="date" class="zp-data-date-filter" id="date-to" placeholder="结束日期">
      </div>
      <div class="zp-data-content" id="data-content">
        ${renderDataItems(data.sortedData)}
      </div>
    `;

    // 关闭按钮
    const closeBtns = container.querySelectorAll('.zp-data-close');
    closeBtns[0].addEventListener('click', () => {
      container.classList.add('minimized');
    });
    closeBtns[1].addEventListener('click', () => {
      document.body.removeChild(container);
    });

    // 刷新按钮
    const refreshBtn = container.querySelector('.zp-data-refresh');
    refreshBtn.addEventListener('click', () => {
      fetchAndDisplayData(true);
    });

    // 搜索功能
    const searchInput = container.querySelector('.zp-data-search');
    searchInput.addEventListener('input', () => {
      filterData();
    });

    // 筛选功能
    const filterSelect = container.querySelector('.zp-data-filter');
    filterSelect.addEventListener('change', () => {
      filterData();
    });

    // 日期筛选功能
    const dateFrom = container.querySelector('#date-from');
    const dateTo = container.querySelector('#date-to');
    dateFrom.addEventListener('change', filterData);
    dateTo.addEventListener('change', filterData);

    // 综合筛选函数
    function filterData() {
      const searchTerm = searchInput.value.toLowerCase();
      const selectedCompany = filterSelect.value;
      const fromDate = dateFrom.value ? new Date(dateFrom.value).getTime() : 0;
      const toDate = dateTo.value ? new Date(dateTo.value).getTime() + 86400000 : Infinity;

      let filtered = data.sortedData;

      // 公司筛选
      if (selectedCompany !== 'all') {
        filtered = data.companyMap[selectedCompany] || [];
      }


      // 搜索筛选
      filtered = filtered.filter(item => {
        const company = item.brandName?.toLowerCase() || '';
        const job = item.jobName?.toLowerCase() || '';
        return company.includes(searchTerm) || job.includes(searchTerm);
      });

      // 日期筛选
      filtered = filtered.filter(item => {
        const itemTime = new Date(item.happenTime).getTime();
        return itemTime >= fromDate && itemTime <= toDate;
      });

      updateDataContent(filtered);
    }
  }



  function renderDataItems(items) {
    return items.map(item => {
      if (!item.encryptJobId) return '';

      const jobLink = generateJobLink(item.encryptJobId);
      const logoUrl = item.brandLogo || 'https://www.zhipin.com/favicon.ico';
      const defaultLogo = 'https://www.zhipin.com/favicon.ico';

      return `
      <div class="zp-data-item">
        <div class="zp-data-item-header">
          <img class="zp-data-logo"
               src="${logoUrl}"
               alt="${item.brandName || '公司logo'}"
               onerror="this.src='${defaultLogo}'">
          <div class="zp-data-company-wrapper">
            <div class="zp-data-company" title="${item.brandName || '未知公司'}">
              ${item.brandName || '未知公司'}
            </div>
            <div class="zp-data-job">
              <span>${item.jobName || '未知岗位'}</span>
              <span class="zp-data-salary">${item.jobSalary || '薪资面议'}</span>
            </div>
          </div>
        </div>
        <div class="zp-data-meta">
          <span>${item.bossName || '未知'}</span>
          <span>${item.bossTitle || '未知职位'}</span>
          <span style="color: blue">${item.itemSource || ''}</>
        </div>
        <div class="zp-data-meta">
          <span>${item.scaleName || '未知'}</span>
          <span>${item.jobLabels[0] || '未知'}</span>
        </div>
        <div class="zp-data-time">${formatTime(item.happenTime)}</div>
        <a href="${jobLink}" target="_blank" class="zp-data-link">
          查看职位详情
        </a>
      </div>
    `;
    }).join('');
  }
  // 更新数据内容
  function updateDataContent(items) {
    const content = document.getElementById('data-content');
    if (content) {
      content.innerHTML = renderDataItems(items);
      // 更新计数
      const countElement = document.querySelector('.zp-data-count');
      if (countElement) {
        countElement.textContent = `(共 ${items.length} 条)`;
      }
    }
  }

// 处理按钮点击
  async function handleButtonClick() {
    try {
      // 如果有缓存且token存在,直接使用缓存
      if (cachedData && token && zp_token) {
        const processedData = processData(cachedData);
        showData(processedData);
        return;
      }

      // 尝试获取第一页数据测试token是否有效
      await fetchJobData(1);
      fetchAndDisplayData();
    } catch (error) {
      console.log('需要输入token:', error);
      showTokenModal();
    }
  }

// 初始化
  function init() {
    createButton();

    // 如果有缓存数据,直接显示
    if (cachedData && token && zp_token) {
      const processedData = processData(cachedData);
      showData(processedData);
    }
  }

// 页面加载完成后执行
  window.addEventListener('load', init);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址