// ==UserScript==
// @name 求职助手
// @namespace job_seeking_helper
// @author Gloduck
// @license MIT
// @version 1.1.1
// @description 为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。
// @match https://www.zhipin.com/*
// @match https://m.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'),
MOBILE_SEARCH: Symbol('MobileSearch'),
MOBILE_RECOMMEND: Symbol('MobileRecommend')
}
const JobFilterType = {
BLACKLIST: Symbol('Blacklist'), VIEWED: Symbol('Viewed'), MISMATCH_CONDITION: Symbol('MismatchCondition'),
}
let curPageHash = null;
// 默认设置
const defaults = {
blacklist: [],
minSalary: 0,
maxSalary: Infinity,
salaryFilterType: 'include',
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),
salaryFilterType: GM_getValue('salaryFilterType', defaults.salaryFilterType),
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('salaryFilterType', ['include', 'overlap'], settings.salaryFilterType, {
include: '包含范围',
overlap: '存在交集'
})}
处理方式设置
${createRadioGroup('blacklistAction', ['delete', 'hide', 'mark'], settings.blacklistAction, {
delete: '删除',
hide: '屏蔽',
mark: '标识'
})}
${createRadioGroup('viewedAction', ['delete', 'hide', 'mark'], settings.viewedAction, {
delete: '删除',
hide: '屏蔽',
mark: '标识'
})}
${createRadioGroup('conditionAction', ['delete', 'hide', 'mark'], settings.conditionAction, {
delete: '删除',
hide: '屏蔽',
mark: '标识'
})}
将重置当前平台所有已经查看的职位
`;
// 添加样式
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, values, selected, labels) {
return `
${values.map(value => `
`).join('')}
`;
}
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.salaryFilterType = document.querySelector('input[name="salaryFilterType"]:checked').value;
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('salaryFilterType', settings.salaryFilterType);
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;
} else if (document.querySelector('.rec-job-list') != null) {
return JobPageType.RECOMMEND;
} else if (document.querySelector('.job-recommend .job-list') != null) {
return JobPageType.MOBILE_RECOMMEND;
} else if (document.querySelector('#main .job-list') != null) {
return JobPageType.MOBILE_SEARCH;
}
return null;
}
fetchJobElements(jobPageType) {
if (jobPageType === JobPageType.SEARCH) {
return document.querySelectorAll('ul.job-list-box > li.job-card-wrapper');
} else if (jobPageType === JobPageType.RECOMMEND) {
return document.querySelectorAll('ul.rec-job-list > div > li.job-card-box');
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
return document.querySelectorAll('.job-list > ul > li');
} 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 if (jobPageType === JobPageType.RECOMMEND) {
const element = jobElement.querySelector('.job-name');
if (element == null) {
return null;
}
const url = element.href;
if (url == null) {
return null;
}
return url.split('/job_detail/')[1].split('.html')[0];
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
const element = jobElement.querySelector('a');
if (element == null) {
return null;
}
if(element.classList.contains('delete')){
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 if (jobPageType === JobPageType.RECOMMEND) {
const salary = this.convertSalaryField(jobElement.querySelector('.job-salary').textContent);
return parseSlaryToMontyly(salary);
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
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 if (jobPageType === JobPageType.RECOMMEND) {
return jobElement.querySelector('.boss-name').textContent
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
return jobElement.querySelector('.company').textContent
} else {
throw new Error('Not a job element')
}
}
// document.querySelectorAll('ul.rec-job-list > div > li.job-card-box')[0].querySelector('.job-name').textContent
parseJobName(jobElement, jobPageType) {
if (jobPageType === JobPageType.SEARCH) {
return jobElement.querySelector('.job-name').textContent
} else if (jobPageType === JobPageType.RECOMMEND) {
return jobElement.querySelector('.job-name').textContent
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
return jobElement.querySelector('.title-text').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 || jobPageType === JobPageType.RECOMMEND) {
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';
markSpan.style.float = 'left';
titleElement.insertBefore(markSpan, titleElement.firstChild);
}
markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
this.changeJobElementColor(jobElement, jobPageType);
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
const titleElement = jobElement.querySelector('.title');
let markSpan = titleElement.querySelector('.mark');
if (markSpan === null) {
markSpan = document.createElement('span');
markSpan.classList.add('mark');
markSpan.style.color = 'red';
markSpan.style.float = 'left';
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 if (jobPageType === JobPageType.RECOMMEND) {
const cardBody = jobElement.querySelector('.job-info');
cardBody.innerHTML = `
已屏蔽
`;
const cardFooter = jobElement.querySelector('.job-card-footer');
cardFooter.innerHTML = `
${message}
`;
this.changeJobElementColor(jobElement, jobPageType);
} else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
const cardBody = jobElement.querySelector('a');
cardBody.classList.add('delete');
cardBody.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 || jobPageType === JobPageType.RECOMMEND || jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
jobElement.style.backgroundColor = '#e1e1e1';
} else {
throw new Error('Not a job element')
}
}
/**
*
* @param {String} salary
*/
convertSalaryField(salary) {
// 恶心的BOSS添加了特殊字符,需要转换
let res = '';
for (let i = 0; i < salary.length; i++) {
let charCode = salary.charCodeAt(i);
if (charCode >= 57393 && charCode <= 57402) {
charCode = charCode - 57393 + 48;
}
res += String.fromCharCode(charCode);
}
return res;
}
}
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 (settings.salaryFilterType === 'include') {
if (companySalary.min < settings.minSalary || companySalary.max > settings.maxSalary) {
filterTypes.push(JobFilterType.MISMATCH_CONDITION);
}
} else if (settings.salaryFilterType === 'overlap') {
if (!(companySalary.max >= settings.minSalary && settings.maxSalary >= companySalary.min)) {
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;
}
})();