// ==UserScript== // @name 求职助手 // @namespace job_seeking_helper // @author Gloduck // @license MIT // @version 1.0 // @description 为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。 // @match https://www.zhipin.com/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 策略枚举 const JobPlatform = { BOSS_ZHIPIN: Symbol('Boss'), LAGOU: Symbol('Lagou'), UNKNOWN: Symbol('未知网站') }; const JobPageType = { SEARCH: Symbol('Search'), RECOMMEND: Symbol('Recommend') } const JobFilterType = { BLACKLIST: Symbol('Blacklist'), VIEWED: Symbol('Viewed'), MISMATCH_CONDITION: Symbol('MismatchCondition'), } let curPageHash = null; // 默认设置 const defaults = { blacklist: [], minSalary: 0, maxSalary: Infinity, maxDailyHours: 8, maxMonthlyDays: 22, maxWeeklyCount: 4, viewedAction: 'mark', blacklistAction: 'hide', conditionAction: 'hide' }; // 加载用户设置 const settings = { blacklist: GM_getValue('blacklist', defaults.blacklist), minSalary: GM_getValue('minSalary', defaults.minSalary), maxSalary: GM_getValue('maxSalary', defaults.maxSalary), maxDailyHours: GM_getValue('maxDailyHours', defaults.maxDailyHours), maxMonthlyDays: GM_getValue('maxMonthlyDays', defaults.maxMonthlyDays), maxWeeklyCount: GM_getValue('maxWeeklyCount', defaults.maxWeeklyCount), viewedAction: GM_getValue('viewedAction', defaults.viewedAction), blacklistAction: GM_getValue('blacklistAction', defaults.blacklistAction), conditionAction: GM_getValue('conditionAction', defaults.conditionAction) }; // 注册设置菜单 GM_registerMenuCommand('职位过滤设置', showSettings); function showSettings() { const dialog = document.createElement('div'); dialog.style = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffffff; padding: 25px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 9999; min-width: 380px; font-family: 'Segoe UI', sans-serif; `; dialog.innerHTML = `

职位过滤设置

-

工作时间限制

处理方式设置

${createRadioGroup('blacklistAction', ['delete', 'hide', 'mark'], settings.blacklistAction)}
${createRadioGroup('viewedAction', ['delete', 'hide', 'mark'], settings.viewedAction)}
${createRadioGroup('conditionAction', ['delete', 'hide', 'mark'], settings.conditionAction)}
将重置当前平台所有已经查看的职位
`; // 添加样式 const style = document.createElement('style'); style.textContent = ` .dialog-btn { padding: 8px 20px; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 14px; } .primary { background: #3498db; color: white; } .secondary { background: #f0f0f0; color: #666; } .radio-group { display: flex; gap: 15px; margin-top: 5px; } .radio-item label { display: flex; align-items: center; gap: 6px; cursor: pointer; } `; dialog.appendChild(style); document.body.appendChild(dialog); // 事件绑定 dialog.querySelector('#saveBtn').addEventListener('click', saveSettings); dialog.querySelector('#cancelBtn').addEventListener('click', () => dialog.remove()); dialog.querySelector('#clearCacheBtn').addEventListener('click', () => { if (confirm('确定要清理已查看职位吗?\n这将重置当前平台所有已经查看的职位!')) { clearViewedJob(choosePlatForm()); alert('清理完成!'); location.reload(); } }); } // 生成单选按钮组 function createRadioGroup(name, options, selected) { return `
${options.map(opt => `
`).join('')}
`; } // 获取操作标签 function getActionLabel(action) { const labels = { delete: '删除', hide: '屏蔽', mark: '标识' }; return labels[action] || action; } function saveSettings() { settings.blacklist = document.querySelector('#blacklist').value .split(',') .map(s => s.trim().toLowerCase()) .filter(Boolean); settings.minSalary = parseInt(document.querySelector('#minSalary').value) || 0; settings.maxSalary = parseInt(document.querySelector('#maxSalary').value) || Infinity; settings.viewedAction = document.querySelector('input[name="viewedAction"]:checked').value; settings.blacklistAction = document.querySelector('input[name="blacklistAction"]:checked').value; settings.conditionAction = document.querySelector('input[name="conditionAction"]:checked').value; settings.maxDailyHours = parseFloat(document.querySelector('#maxDailyHours').value) || 0; settings.maxMonthlyDays = parseInt(document.querySelector('#maxMonthlyDays').value) || 0; settings.maxWeeklyCount = parseFloat(document.querySelector('#maxWeeklyCount').value) || 0; // 保存设置 GM_setValue('blacklist', settings.blacklist); GM_setValue('minSalary', settings.minSalary); GM_setValue('maxSalary', settings.maxSalary); GM_setValue('viewedAction', settings.viewedAction); GM_setValue('blacklistAction', settings.blacklistAction); GM_setValue('conditionAction', settings.conditionAction); GM_setValue('maxDailyHours', settings.maxDailyHours); GM_setValue('maxMonthlyDays', settings.maxMonthlyDays); GM_setValue('maxWeeklyCount', settings.maxWeeklyCount); document.querySelector('div').remove(); alert('设置已保存!'); location.reload(); } /** * * @return {symbol} */ function choosePlatForm() { const href = window.location.href; if (href.includes('zhipin.com')) { return JobPlatform.BOSS_ZHIPIN; } else { return JobPlatform.UNKNOWN; } } /** * * @returns {PlatFormStrategy} */ function getStrategy() { switch (choosePlatForm()) { case JobPlatform.BOSS_ZHIPIN: return new BossStrategy(); default: throw new Error('Unsupported platform') } } /** * * @param {String} salaryRange */ function parseSlaryToMontyly(salaryRange) { const regex = /(\d+(\.\d+)?)\s*-\s*(\d+(\.\d+)?)/; const match = salaryRange.match(regex); if (!match) { throw new Error('Invalid salary range format'); } // 提取最小值和最大值 let minSalary = parseFloat(match[1]); let maxSalary = parseFloat(match[3]); if (salaryRange.includes('K') || salaryRange.includes('k')) { minSalary = minSalary * 1000; maxSalary = maxSalary * 1000; } if (salaryRange.includes('周')) { minSalary = minSalary * settings.maxWeeklyCount; maxSalary = maxSalary * settings.maxWeeklyCount; } else if (salaryRange.includes('天')) { minSalary = minSalary * settings.maxMonthlyDays; maxSalary = maxSalary * settings.maxMonthlyDays; } else if (salaryRange.includes('时')) { minSalary = minSalary * settings.maxMonthlyDays * settings.maxDailyHours; maxSalary = maxSalary * settings.maxMonthlyDays * settings.maxDailyHours; } return { min: minSalary, max: maxSalary, } } /** * * @param {Symbol} jobPlatform */ function clearViewedJob(jobPlatform) { let jobViewKey = getJobViewKey(jobPlatform); GM_setValue(jobViewKey, []); } /** * * @param {Symbol} jobPlatform * @param {String} uniqueKey */ function setJobViewed(jobPlatform, uniqueKey) { let jobViewKey = getJobViewKey(jobPlatform); const jobViewedSet = getJobViewedSet(jobPlatform); if (jobViewedSet.has(uniqueKey)) { return; } jobViewedSet.add(uniqueKey); GM_setValue(jobViewKey, [...jobViewedSet]); } /** * * @param {Symbol} jobPlatform * @return {Set} */ function getJobViewedSet(jobPlatform) { let jobViewKey = getJobViewKey(jobPlatform); return new Set(GM_getValue(jobViewKey, [])); } /** * * @param {Symbol} jobPlatform * @return {string} */ function getJobViewKey(jobPlatform) { return jobPlatform.description + "ViewHistory"; } class PlatFormStrategy { /** * @returns {JobPageType} */ fetchJobPageType() { throw new Error('Method not implemented') } /** * @param {JobPageType} jobPageType * @returns {NodeListOf} */ fetchJobElements(jobPageType) { throw new Error('Method not implemented') } /** * @param {Element} jobElement * @param {JobPageType} jobPageType * @returns {String|null} */ fetchJobUniqueKey(jobElement, jobPageType) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @returns {{min: number, max: number}} */ parseSalary(jobElement, jobPageType) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @returns {String} */ parseJobName(jobElement, jobPageType) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @returns {String} */ parseCompanyName(jobElement, jobPageType) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @param {JobFilterType[]} jobFilterTypes * @returns {void} */ markCurJobElement(jobElement, jobPageType, jobFilterTypes) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @param {JobFilterType[]} jobFilterTypes * @returns {void} */ blockCurJobElement(jobElement, jobPageType, jobFilterTypes) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @returns {void} */ removeCurJobElement(jobElement, jobPageType) { throw new Error('Method not implemented') } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType * @param {Function} eventCallback * @returns {void} */ addViewedCallback(jobElement, jobPageType, eventCallback) { throw new Error('Method not implemented') } /** * * @param {Element} element */ addDeleteLine(element) { const delElement = document.createElement('del'); while (element.firstChild) { delElement.appendChild(element.firstChild); } element.appendChild(delElement); } /** * @param {JobFilterType} jobFilterType * @returns {String} */ convertFilterTypeToMessage(jobFilterType) { if (jobFilterType === JobFilterType.BLACKLIST) { return '黑名单'; } else if (jobFilterType === JobFilterType.VIEWED) { return '已查看'; } else if (jobFilterType === JobFilterType.MISMATCH_CONDITION) { return '条件不符'; } else { return '未知'; } } } class BossStrategy extends PlatFormStrategy { fetchJobPageType() { if (document.querySelector('.search-job-result') != null) { return JobPageType.SEARCH; } return null; } fetchJobElements(jobPageType) { if (jobPageType === JobPageType.SEARCH) { return document.querySelectorAll('ul.job-list-box > li.job-card-wrapper'); } else { throw new Error('Not a job element') } } fetchJobUniqueKey(jobElement, jobPageType) { if (jobPageType === JobPageType.SEARCH) { const element = jobElement.querySelector('.job-card-left'); if (element == null) { return null; } const url = element.href; if (url == null) { return null; } return url.split('/job_detail/')[1].split('.html')[0]; } else { throw new Error('Not a job element') } } parseSalary(jobElement, jobPageType) { if (jobPageType === JobPageType.SEARCH) { const salary = jobElement.querySelector('.salary').textContent; return parseSlaryToMontyly(salary); } else { throw new Error('Not a job element') } } parseCompanyName(jobElement, jobPageType) { if (jobPageType === JobPageType.SEARCH) { return jobElement.querySelector('.company-name > a').textContent } else { throw new Error('Not a job element') } } parseJobName(jobElement, jobPageType) { if (jobPageType === JobPageType.SEARCH) { return jobElement.querySelector('.job-name').textContent } else { throw new Error('Not a job element') } } addViewedCallback(jobElement, jobPageType, eventCallback) { jobElement.addEventListener('click', eventCallback, true); } markCurJobElement(jobElement, jobPageType, jobFilterTypes) { if (jobPageType === JobPageType.SEARCH) { const titleElement = jobElement.querySelector('.job-title'); let markSpan = titleElement.querySelector('.mark'); if (markSpan === null) { markSpan = document.createElement('span'); markSpan.classList.add('mark'); markSpan.style.color = 'red'; titleElement.insertBefore(markSpan, titleElement.firstChild); } markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')'; this.changeJobElementColor(jobElement, jobPageType); } else { throw new Error('Not a job element') } } blockCurJobElement(jobElement, jobPageType, jobFilterTypes) { const message = jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|'); if (jobPageType === JobPageType.SEARCH) { const cardBody = jobElement.querySelector('.job-card-body'); cardBody.innerHTML = `
已屏蔽
`; const cardFooter = jobElement.querySelector('.job-card-footer'); cardFooter.innerHTML = `
${message}
`; this.changeJobElementColor(jobElement, jobPageType); } else { throw new Error('Not a job element') } } removeCurJobElement(jobElement, jobPageType) { jobElement.parentElement.removeChild(jobElement); // jobElement.style.display = 'none'; } /** * * @param {Element} jobElement * @param {JobPageType} jobPageType */ changeJobElementColor(jobElement, jobPageType) { if (jobPageType === JobPageType.SEARCH) { jobElement.style.backgroundColor = '#e1e1e1'; } else { throw new Error('Not a job element') } } } setInterval(() => { const strategy = getStrategy(); if (strategy == null) { return; } let jobPageType = strategy.fetchJobPageType(); if (jobPageType == null) { return; } const pageHash = getPageHash(); if (pageHash !== curPageHash) { const jobPlatform = choosePlatForm(); const viewedJobIds = getJobViewedSet(jobPlatform); const elements = strategy.fetchJobElements(jobPageType); for (let i = 0; i < elements.length; i++) { const job = elements[i]; const id = strategy.fetchJobUniqueKey(job, jobPageType); if (id == null) { continue; } initEventHandler(jobPlatform, strategy, jobPageType, job); const salary = strategy.parseSalary(job, jobPageType); const companyName = strategy.parseCompanyName(job, jobPageType); let jobName = strategy.parseJobName(job, jobPageType); console.log(`Id:${id},公司:${companyName},岗位:${jobName},月薪: ${salary.min} - ${salary.max}`); const jobFilterType = filterElement(strategy, job, jobPageType, viewedJobIds); handleElement(strategy, job, jobPageType, jobFilterType); } // 元素可能变动了,重新计算Hash curPageHash = getPageHash(); } }, 1000) /** * @param {PlatFormStrategy} strategy * @param {Element} jobElement * @param {JobPageType} jobPageType * @param {Set} viewedJobs * @returns {JobFilterType[]} */ function filterElement(strategy, jobElement, jobPageType, viewedJobs) { const filterTypes = []; const companyName = strategy.parseCompanyName(jobElement, jobPageType).toLowerCase(); for (let i = 0; i < settings.blacklist.length; i++) { if (companyName.includes(settings.blacklist[i])) { filterTypes.push(JobFilterType.BLACKLIST); break } } const jobId = strategy.fetchJobUniqueKey(jobElement, jobPageType); if (viewedJobs.has(jobId)) { filterTypes.push(JobFilterType.VIEWED); } const companySalary = strategy.parseSalary(jobElement, jobPageType); if (companySalary.min < settings.minSalary || companySalary.max > settings.maxSalary) { filterTypes.push(JobFilterType.MISMATCH_CONDITION); } return filterTypes; } /** * * @param {PlatFormStrategy} strategy * @param {Element} jobElement * @param {JobPageType} jobPageType * @param {JobFilterType[]} jobFilterTypes */ function handleElement(strategy, jobElement, jobPageType, jobFilterTypes) { if (jobFilterTypes.length === 0) { return; } let filter = jobFilterTypes.map(filterType => { if (filterType === JobFilterType.BLACKLIST) { return settings.blacklistAction; } else if (filterType === JobFilterType.VIEWED) { return settings.viewedAction; } else if (filterType === JobFilterType.MISMATCH_CONDITION) { return settings.conditionAction; } else { return null; } }).filter(action => action != null && typeof action === 'string'); if (filter.includes('delete')) { strategy.removeCurJobElement(jobElement, jobPageType); } else if (filter.includes('hide')) { strategy.blockCurJobElement(jobElement, jobPageType, jobFilterTypes); } else if (filter.includes('mark')) { strategy.markCurJobElement(jobElement, jobPageType, jobFilterTypes); } } function getPageHash() { const strategy = getStrategy(); let jobPageType = strategy.fetchJobPageType(); const elements = strategy.fetchJobElements(jobPageType); let keys = ''; for (let i = 0; i < elements.length; i++) { const id = strategy.fetchJobUniqueKey(elements[i], jobPageType); if (id === null) { continue; } keys += id; } return hashCode(keys); } /** *@param {Symbol} jobPlatform * @param {PlatFormStrategy} strategy * @param {JobPageType} jobPageType * @param {Element} element */ function initEventHandler(jobPlatform, strategy, jobPageType, element) { const callBack = () => { const id = strategy.fetchJobUniqueKey(element, jobPageType); setJobViewed(jobPlatform, id); // 重置PageHash,来刷新 curPageHash = null; }; strategy.addViewedCallback(element, jobPageType, callBack); } /** * * @param {String} str * @returns {number} */ function hashCode(str) { let hash = 0; if (str.length === 0) return hash; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return hash; } })();