// ==UserScript==
// @name boss直聘自动打招呼
// @namespace https://github.com/18023785187/auto-job
// @version 0.1.0
// @description boss直聘自动打招呼油猴脚本
// @author hym20000418
// @match *://www.zhipin.com/*
// @icon none
// @grant none
// @license MIT
// ==/UserScript==
/*
修改该配置即可限定打招呼对象
*/
const config = {
/**
* 设置职位入口,可选值 0, 1, 2
* 0 为以 搜索框搜索 作为职位入口
* 1 为以 推荐职位——精选职位 作为职位入口
* 2 为以 推荐职位——最新职位 作为职位入口
*
* 设置三个入口是原因三个入口的职位都不太一样,避免漏了一些职位可以尝试切换入口
*/
mode: 2,
/**
* 目标城市,
* mode 为 1 或 2 时需要事先设置求职意向为目标城市,否则不生效
*/
city: '深圳',
/**
* 职位关键词
* mode 为 0 时作为搜索框的关键词键入
* mode 为 1 或 2 时作为职位列表项中的职位名称匹配(这是因为推荐的职位不是很准确,比如偶尔会出现 “安卓工程师” 之类的职位,这时就需要通过关键词去过滤不匹配的职位)
*/
keyword: '前端',
/**
* 职位名称要排除的关键字,比如 '高级前端工程师' 将会比排除在外
*/
excludeKeywords: ['高级', '资深', '驻场', '外派', '安卓'],
/**
* 是否接受外地职位(职位列表有时会出现外地职位)
*/
otherPlace: false,
/**
* 活跃度,匹配中的才会打招呼
*/
liveness: ['在线', '刚刚活跃', '今日活跃', '3日内活跃'],
/**
* 要排除的公司名
*/
excludes: ['中软国际', '德科', '睿服'],
/**
* 每次访问的最小间隔,防止操作过快被系统判定为机器人,单位秒
*/
min: 3,
/**
* 每次访问的最大间隔,防止操作过快被系统判定为机器人,单位秒
*/
max: 6,
/**
* 打招呼语
*/
message: '您好,我正在找前端开发的工作,希望有机会与贵司进一步交流',
/**
* 工作年限,可多选
* 经验不限 101
* 应届生 102
* 1年以内 103
* 1-3年 104
* 3-5年 105
* 5-10年 106
* 10年以上 107
* 在校生 108
*/
experience: [101, 103, 104, 105],
/**
* 薪资待遇,数字类型
* 3K 以下 402
* 3-5K 403
* 5-10K 404
* 10-20K 405
* 20-50K 406
* 50K以上 407
*
* 自定义 [min, max] min表示最小值,max表示最大值,单位 K,如 salary: [13, 15] 表示 13-15K
*/
salary: [],
/**
* 公司规模
* 0-20人 301
* 20-99人 302
* 100-499人 303
* 500-999人 304
* 1000-9999人 305
* 10000人以上 306
*/
scale: [],
/**
* 学历要求
* 大专 202
* 本科 203
* 硕士 204
* 博士 205
* 高中 206
* 中专 208
* 初中 209
*/
degree: [],
}
var AutoJob = (function () {
'use strict';
/**
* 监听元素是否生成
*/
async function monitorElementGeneration(selector) {
return new Promise(resolve => {
let el;
let timer = setInterval(() => {
el = document.querySelector(selector);
if(el) {
resolve(el);
clearInterval(timer);
}
}, 100);
})
}
async function monitorElementsGeneration(selector) {
return new Promise(resolve => {
let el;
let timer = setInterval(() => {
el = document.querySelectorAll(selector);
if(el.length) {
resolve(el);
clearInterval(timer);
}
}, 100);
})
}
/**
* 输入最大和最小正整数,在该范围内取随机数
* @param {*} min
* @param {*} max
* @returns
*/
function random(min, max) {
return (
min + (Math.random() * (max - min))
)
}
const cityCodeMap = {
"北京": 101010100,
"上海": 101020100,
"天津": 101030100,
"重庆": 101040100,
"哈尔滨": 101050100,
"齐齐哈尔": 101050200,
"牡丹江": 101050300,
"佳木斯": 101050400,
"绥化": 101050500,
"黑河": 101050600,
"伊春": 101050700,
"大庆": 101050800,
"七台河": 101050900,
"鸡西": 101051000,
"鹤岗": 101051100,
"双鸭山": 101051200,
"大兴安岭地区": 101051300,
"长春": 101060100,
"吉林": 101060200,
"四平": 101060300,
"通化": 101060400,
"白城": 101060500,
"辽源": 101060600,
"松原": 101060700,
"白山": 101060800,
"延边朝鲜族自治州": 101060900,
"沈阳": 101070100,
"大连": 101070200,
"鞍山": 101070300,
"抚顺": 101070400,
"本溪": 101070500,
"丹东": 101070600,
"锦州": 101070700,
"营口": 101070800,
"阜新": 101070900,
"辽阳": 101071000,
"铁岭": 101071100,
"朝阳": 101071200,
"盘锦": 101071300,
"葫芦岛": 101071400,
"呼和浩特": 101080100,
"包头": 101080200,
"乌海": 101080300,
"通辽": 101080400,
"赤峰": 101080500,
"鄂尔多斯": 101080600,
"呼伦贝尔": 101080700,
"巴彦淖尔": 101080800,
"乌兰察布": 101080900,
"锡林郭勒盟": 101081000,
"兴安盟": 101081100,
"阿拉善盟": 101081200,
"石家庄": 101090100,
"保定": 101090200,
"张家口": 101090300,
"承德": 101090400,
"唐山": 101090500,
"廊坊": 101090600,
"沧州": 101090700,
"衡水": 101090800,
"邢台": 101090900,
"邯郸": 101091000,
"秦皇岛": 101091100,
"太原": 101100100,
"大同": 101100200,
"阳泉": 101100300,
"晋中": 101100400,
"长治": 101100500,
"晋城": 101100600,
"临汾": 101100700,
"运城": 101100800,
"朔州": 101100900,
"忻州": 101101000,
"吕梁": 101101100,
"西安": 101110100,
"咸阳": 101110200,
"延安": 101110300,
"榆林": 101110400,
"渭南": 101110500,
"商洛": 101110600,
"安康": 101110700,
"汉中": 101110800,
"宝鸡": 101110900,
"铜川": 101111000,
"济南": 101120100,
"青岛": 101120200,
"淄博": 101120300,
"德州": 101120400,
"烟台": 101120500,
"潍坊": 101120600,
"济宁": 101120700,
"泰安": 101120800,
"临沂": 101120900,
"菏泽": 101121000,
"滨州": 101121100,
"东营": 101121200,
"威海": 101121300,
"枣庄": 101121400,
"日照": 101121500,
"聊城": 101121700,
"乌鲁木齐": 101130100,
"克拉玛依": 101130200,
"昌吉回族自治州": 101130300,
"巴音郭楞蒙古自治州": 101130400,
"博尔塔拉蒙古自治州": 101130500,
"伊犁哈萨克自治州": 101130600,
"吐鲁番": 101130800,
"哈密": 101130900,
"阿克苏地区": 101131000,
"克孜勒苏柯尔克孜自治州": 101131100,
"喀什地区": 101131200,
"和田地区": 101131300,
"塔城地区": 101131400,
"阿勒泰地区": 101131500,
"石河子": 101131600,
"阿拉尔": 101131700,
"图木舒克": 101131800,
"五家渠": 101131900,
"铁门关": 101132000,
"北屯市": 101132100,
"可克达拉市": 101132200,
"昆玉市": 101132300,
"双河市": 101132400,
"新星市": 101132500,
"胡杨河市": 101132600,
"拉萨": 101140100,
"日喀则": 101140200,
"昌都": 101140300,
"林芝": 101140400,
"山南": 101140500,
"那曲": 101140600,
"阿里地区": 101140700,
"西宁": 101150100,
"海东": 101150200,
"海北藏族自治州": 101150300,
"黄南藏族自治州": 101150400,
"海南藏族自治州": 101150500,
"果洛藏族自治州": 101150600,
"玉树藏族自治州": 101150700,
"海西蒙古族藏族自治州": 101150800,
"兰州": 101160100,
"定西": 101160200,
"平凉": 101160300,
"庆阳": 101160400,
"武威": 101160500,
"金昌": 101160600,
"张掖": 101160700,
"酒泉": 101160800,
"天水": 101160900,
"白银": 101161000,
"陇南": 101161100,
"嘉峪关": 101161200,
"临夏回族自治州": 101161300,
"甘南藏族自治州": 101161400,
"银川": 101170100,
"石嘴山": 101170200,
"吴忠": 101170300,
"固原": 101170400,
"中卫": 101170500,
"郑州": 101180100,
"安阳": 101180200,
"新乡": 101180300,
"许昌": 101180400,
"平顶山": 101180500,
"信阳": 101180600,
"南阳": 101180700,
"开封": 101180800,
"洛阳": 101180900,
"商丘": 101181000,
"焦作": 101181100,
"鹤壁": 101181200,
"濮阳": 101181300,
"周口": 101181400,
"漯河": 101181500,
"驻马店": 101181600,
"三门峡": 101181700,
"济源": 101181800,
"南京": 101190100,
"无锡": 101190200,
"镇江": 101190300,
"苏州": 101190400,
"南通": 101190500,
"扬州": 101190600,
"盐城": 101190700,
"徐州": 101190800,
"淮安": 101190900,
"连云港": 101191000,
"常州": 101191100,
"泰州": 101191200,
"宿迁": 101191300,
"武汉": 101200100,
"襄阳": 101200200,
"鄂州": 101200300,
"孝感": 101200400,
"黄冈": 101200500,
"黄石": 101200600,
"咸宁": 101200700,
"荆州": 101200800,
"宜昌": 101200900,
"十堰": 101201000,
"随州": 101201100,
"荆门": 101201200,
"恩施土家族苗族自治州": 101201300,
"仙桃": 101201400,
"潜江": 101201500,
"天门": 101201600,
"神农架": 101201700,
"杭州": 101210100,
"湖州": 101210200,
"嘉兴": 101210300,
"宁波": 101210400,
"绍兴": 101210500,
"台州": 101210600,
"温州": 101210700,
"丽水": 101210800,
"金华": 101210900,
"衢州": 101211000,
"舟山": 101211100,
"合肥": 101220100,
"蚌埠": 101220200,
"芜湖": 101220300,
"淮南": 101220400,
"马鞍山": 101220500,
"安庆": 101220600,
"宿州": 101220700,
"阜阳": 101220800,
"亳州": 101220900,
"滁州": 101221000,
"淮北": 101221100,
"铜陵": 101221200,
"宣城": 101221300,
"六安": 101221400,
"池州": 101221500,
"黄山": 101221600,
"福州": 101230100,
"厦门": 101230200,
"宁德": 101230300,
"莆田": 101230400,
"泉州": 101230500,
"漳州": 101230600,
"龙岩": 101230700,
"三明": 101230800,
"南平": 101230900,
"南昌": 101240100,
"九江": 101240200,
"上饶": 101240300,
"抚州": 101240400,
"宜春": 101240500,
"吉安": 101240600,
"赣州": 101240700,
"景德镇": 101240800,
"萍乡": 101240900,
"新余": 101241000,
"鹰潭": 101241100,
"长沙": 101250100,
"湘潭": 101250200,
"株洲": 101250300,
"衡阳": 101250400,
"郴州": 101250500,
"常德": 101250600,
"益阳": 101250700,
"娄底": 101250800,
"邵阳": 101250900,
"岳阳": 101251000,
"张家界": 101251100,
"怀化": 101251200,
"永州": 101251300,
"湘西土家族苗族自治州": 101251400,
"贵阳": 101260100,
"遵义": 101260200,
"安顺": 101260300,
"铜仁": 101260400,
"毕节": 101260500,
"六盘水": 101260600,
"黔东南苗族侗族自治州": 101260700,
"黔南布依族苗族自治州": 101260800,
"黔西南布依族苗族自治州": 101260900,
"成都": 101270100,
"攀枝花": 101270200,
"自贡": 101270300,
"绵阳": 101270400,
"南充": 101270500,
"达州": 101270600,
"遂宁": 101270700,
"广安": 101270800,
"巴中": 101270900,
"泸州": 101271000,
"宜宾": 101271100,
"内江": 101271200,
"资阳": 101271300,
"乐山": 101271400,
"眉山": 101271500,
"雅安": 101271600,
"德阳": 101271700,
"广元": 101271800,
"阿坝藏族羌族自治州": 101271900,
"凉山彝族自治州": 101272000,
"甘孜藏族自治州": 101272100,
"广州": 101280100,
"韶关": 101280200,
"惠州": 101280300,
"梅州": 101280400,
"汕头": 101280500,
"深圳": 101280600,
"珠海": 101280700,
"佛山": 101280800,
"肇庆": 101280900,
"湛江": 101281000,
"江门": 101281100,
"河源": 101281200,
"清远": 101281300,
"云浮": 101281400,
"潮州": 101281500,
"东莞": 101281600,
"中山": 101281700,
"阳江": 101281800,
"揭阳": 101281900,
"茂名": 101282000,
"汕尾": 101282100,
"东沙群岛": 101282200,
"昆明": 101290100,
"曲靖": 101290200,
"保山": 101290300,
"玉溪": 101290400,
"普洱": 101290500,
"昭通": 101290700,
"临沧": 101290800,
"丽江": 101290900,
"西双版纳傣族自治州": 101291000,
"文山壮族苗族自治州": 101291100,
"红河哈尼族彝族自治州": 101291200,
"德宏傣族景颇族自治州": 101291300,
"怒江傈僳族自治州": 101291400,
"迪庆藏族自治州": 101291500,
"大理白族自治州": 101291600,
"楚雄彝族自治州": 101291700,
"南宁": 101300100,
"崇左": 101300200,
"柳州": 101300300,
"来宾": 101300400,
"桂林": 101300500,
"梧州": 101300600,
"贺州": 101300700,
"贵港": 101300800,
"玉林": 101300900,
"百色": 101301000,
"钦州": 101301100,
"河池": 101301200,
"北海": 101301300,
"防城港": 101301400,
"海口": 101310100,
"三亚": 101310200,
"三沙": 101310300,
"儋州": 101310400,
"五指山": 101310500,
"琼海": 101310600,
"文昌": 101310700,
"万宁": 101310800,
"东方": 101310900,
"定安": 101311000,
"屯昌": 101311100,
"澄迈": 101311200,
"临高": 101311300,
"白沙黎族自治县": 101311400,
"昌江黎族自治县": 101311500,
"乐东黎族自治县": 101311600,
"陵水黎族自治县": 101311700,
"保亭黎族苗族自治县": 101311800,
"琼中黎族苗族自治县": 101311900,
"香港": 101320300,
"澳门": 101330100,
"台湾": 101341100
};
class AutoJob {
constructor(config) {
this.config = this._formatConfig(config);
}
_formatConfig(config) {
const newConfig = {};
if (![0, 1, 2].includes(config.mode)) {
throw new TypeError('mode 的值必须是 0, 1, 2')
}
if (typeof config.city !== 'string') {
throw new TypeError('city 类型必须是 string')
}
if (typeof config.keyword !== 'string') {
throw new TypeError('keyword 类型必须是 string')
}
if (typeof config.message !== 'string') {
throw new TypeError('message 类型必须是 string')
}
if (!config.message.length) {
throw new TypeError('message 不能为空')
}
if (config.salary !== undefined) {
if (typeof config.salary !== 'number' && !Array.isArray(config.salary)) {
throw new TypeError('salary 类型必须是 number 或 array')
}
if (
Array.isArray(config.salary) &&
config.salary.length &&
(typeof config.salary[0] !== 'number' || typeof config.salary[1] !== 'number')
) {
throw new TypeError('salary 类型为数组时前两项必须是 number 类型')
}
}
newConfig.mode = config.mode;
newConfig.city = config.city;
newConfig.keyword = config.keyword;
newConfig.otherPlace = !!config.otherPlace;
newConfig.excludeKeywords = Array.isArray(config.excludeKeywords) ? config.excludeKeywords : [];
newConfig.experience = Array.isArray(config.experience) ? config.experience : [];
newConfig.liveness = Array.isArray(config.liveness) ? config.liveness : [];
newConfig.excludes = Array.isArray(config.excludes) ? config.excludes : [];
newConfig.scale = Array.isArray(config.scale) ? config.scale : [];
newConfig.degree = Array.isArray(config.degree) ? config.degree : [];
newConfig.min = (typeof newConfig.min === 'number' ? newConfig.min : 3) * 1000;
newConfig.max = (typeof newConfig.max === 'number' ? newConfig.max : 6) * 1000;
newConfig.message = config.message;
newConfig.salary = config.salary ?? [];
return newConfig
}
start() {
if (window.location.pathname === '/web/geek/job') { // 通过搜索框打开的 jobs
this._traverseJob();
} else if (window.location.pathname === '/web/geek/recommend') { // 推荐职位的 jobs
this._traverseRecommend();
}
else if (window.location.pathname.indexOf('/job_detail') === 0) { // 详情页
this._checkValidJob();
} else if (window.location.pathname === '/web/geek/chat') { // 聊天页
this._sayHello();
}
else {
this._toJobs();
}
}
/**
* 首页操作
* 1、打开推荐职位
* 2、选择城市并所搜职位关键词
*/
async _toJobs() {
const { config } = this;
// 选择城市并所搜职位关键词
if (config.mode === 0) {
const nav = document.querySelector('.nav-city-box');
const selected = nav.querySelector('.nav-city-selected');
if (selected.innerText !== config.city) {
nav.click();
const section = await monitorElementGeneration('.city-group-section');
const citys = section.querySelectorAll('a');
const targetCity = Array.from(citys).find(city => city.innerText === config.city);
if (window.location.pathname !== targetCity.pathname) {
targetCity.click();
}
return
}
// 填写职位关键词
const form = document.querySelector('.search-form');
const search = form.querySelector('.search-form-con > .ipt-wrap > input');
search.value = config.keyword;
const button = form.querySelector('.btn-search');
button.click();
} else {
// 打开推荐职位
const recommend = await monitorElementGeneration('.merge-city-job-recommend');
const moreBtn = recommend.querySelector('.common-tab-more > a');
moreBtn.click();
}
}
/**
* 修正获取的 jobs 并逐个访问
*/
async _traverseJob() {
const { config } = this;
const url = new URL(window.location.href);
const { searchParams } = url;
if (!searchParams.has('page')) {
searchParams.append('page', 1);
}
const page = ~~searchParams.get('page');
// 限制最多 10 页
if (page > 10) return
let isModify = false;
setSearchParams('city', cityCodeMap[config.city]);
setSearchParams('query', config.keyword);
setSearchParams('experience', config.experience);
setSearchParams('scale', config.scale);
setSearchParams('degree', config.degree);
if (Array.isArray(config.salary) && config.salary.length) {
setSearchParams('salary', -40001);
setSearchParams('lowSalary', config.salary[0]);
setSearchParams('highSalary', config.salary[1]);
} else {
setSearchParams('salary', config.salary);
}
searchParams.set('page', page);
if (isModify) {
window.location.search = searchParams.toString();
return
}
await this._traverse();
searchParams.set('page', page + 1);
window.location.search = searchParams.toString();
function setSearchParams(key, value) {
const oldValue = searchParams.get(key);
if (oldValue == null || oldValue.toString() !== value.toString()) {
searchParams.set(key, value);
isModify = true;
}
}
}
/**
* 修正获取的 jobs 并逐个访问
*/
async _traverseRecommend() {
const { config } = this;
const url = new URL(window.location.href);
const { searchParams } = url;
if (!searchParams.has('page')) {
searchParams.append('page', 1);
}
const page = ~~searchParams.get('page');
// 限制最多 30 页
if (page > 30) return
const cities = await monitorElementsGeneration('.system-search-condition .expect-list > .expect-item');
const city = Array.from(cities).find(city => city.innerText.includes(config.city));
if (!city) return
city.click();
const jobTabs = await monitorElementsGeneration('.user-jobs-area .job-tab > span');
Array.from(jobTabs).find(tab => tab.innerText === (config.mode === 2 ? '最新职位' : '精选职位')).click();
let isModify = false;
setSearchParams('scale', config.scale);
setSearchParams('degree', config.degree);
setSearchParams('experience', config.experience);
const newUrl = new URL(window.location.href);
const { searchParams: newSearchParams } = newUrl;
searchParams.set('expectId', newSearchParams.get('expectId'));
searchParams.set('sortType', newSearchParams.get('sortType'));
if (Array.isArray(config.salary) && config.salary.length) {
setSearchParams('salary', -40001);
setSearchParams('lowSalary', config.salary[0]);
setSearchParams('highSalary', config.salary[1]);
} else {
setSearchParams('salary', config.salary);
}
if (isModify) {
window.location.search = searchParams.toString();
return
}
await this._traverse();
searchParams.set('page', page + 1);
window.location.search = searchParams.toString();
function setSearchParams(key, value) {
const oldValue = searchParams.get(key);
if (oldValue == null || oldValue.toString() !== value.toString()) {
searchParams.set(key, value);
isModify = true;
}
}
}
/**
* 遍历 jobs
*/
async _traverse() {
const { config } = this;
const box = await monitorElementGeneration('.job-list-box');
const handlers = Array.from(box.querySelectorAll('.job-card-wrapper'))
.filter(dom => {
const isfriend = dom.querySelector('.job-card-left > .job-info > .start-chat-btn');
const name = dom.querySelector('.job-card-right .company-name > a');
const jobName = dom.querySelector('.job-card-left .job-name');
// 过滤已沟通的职位
return isfriend.innerText === '立即沟通' &&
// 排除的公司
!config.excludes.some(exclude => name.innerText.includes(exclude)) &&
// 是否接受外地职位
(config.otherPlace || !dom.querySelector('.job-card-left > .icon-other-place')) &&
// 职位名称匹配
jobName.innerText.includes(config.keyword) &&
// 职位名称排除关键词
!config.excludeKeywords.find(keyword => jobName.innerText.includes(keyword))
})
.map(dom => {
return () => new Promise(resolve => {
setTimeout(() => {
dom.click();
resolve();
}, random(config.min, config.max));
})
});
for (const handler of handlers) {
await handler();
}
}
/**
* 检查 job 是否符合,符合则打招呼
*/
async _checkValidJob() {
const { config } = this;
const info = await monitorElementGeneration('.job-boss-info>.name');
const liveness = info.querySelector('span');
if (!liveness || (config.liveness.length && !config.liveness.includes(liveness.innerText))) {
this._close();
return
}
const commentBtn = await monitorElementGeneration('.job-banner .btn-container :nth-child(2)');
if (commentBtn.dataset.isfriend === 'true') {
this._close();
return
}
commentBtn.click();
const message = await monitorElementGeneration('.dialog-container>.dialog-con>.startchat-content .edit-area');
const input = message.querySelector('.input-area');
const send = message.querySelector('.send-message');
const inputEv = new Event('input', { bubbles: true });
inputEv.simulated = true;
input.value = config.message;
input.dispatchEvent(inputEv);
setTimeout(() => {
send.click();
this._close();
}, 1000);
}
/**
* 点击立即沟通后有可能直接打开沟通页面,此时需要在当前页发招呼语
*/
async _sayHello() {
const content = await monitorElementGeneration('.chat-conversation>.message-content');
const controls = await monitorElementGeneration('.chat-conversation>.message-controls');
const mySelf = content.querySelectorAll('.chat-message .item-myself');
// 如果有,说明该对话框发送过消息,视为已打过招呼,直接返回
if (mySelf.length) {
this._close();
return
}
const input = controls.querySelector('.chat-editor #chat-input');
const send = controls.querySelector('.chat-editor .btn-send');
const inputEv = new Event('input', { bubbles: true });
inputEv.simulated = true;
input.innerText = config.message;
input.dispatchEvent(inputEv);
setTimeout(() => {
send.click();
this._close();
}, 1000);
}
_close() {
setTimeout(() => {
window.close();
}, 3000);
}
}
return AutoJob;
})();
new AutoJob(config).start()