diff --git a/.gitignore b/.gitignore index 7a682aa3..f09b9580 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ config.yaml *.iml *.ipr .idea +*.sh +CLAUDE.md +.claude ### Eclipse ### .apt_generated .classpath diff --git a/README.md b/README.md index f692b33e..918ee407 100644 --- a/README.md +++ b/README.md @@ -23,39 +23,37 @@ - [少侠,请立即开始,你精彩的人生吧!【源自L站】](doc/just_do_it.md) - [许愿墙](https://fcv1y6gslc.feishu.cn/sheets/JS45sElqAhKhawtTzsYcftymnFe) - - 如果你有你心仪的工作,请在我这里许下愿望,如果实现了,请记得回来更新状态 - - 许愿墙为飞书文档,你可以编辑好以后,导出为xlsx文档,然后覆盖本项目中resources文件夹中的"**许愿墙.xlsx**"文档 - - 接下来,你就可以把xlsx文档,提交到main分支,然后……你就是本项目的开发者之一了。 - - 你可以在你的简历中加上一条,Github,热门开源项目开发者之一。 - - 但是你需要尽可能的,熟悉本项目的逻辑,去想象某一块功能就是你做的,这样可以更从容,更优雅的"吹牛逼”。 - - 只要你相信你自己,本项目就会帮你,你要记住,是你自己拯救的你自己。 - -> 目前提交pr除了许愿墙pr,会停止所有未经沟通作者沟通过的pr,请知悉,很多的人水平,真的菜的令人发指,请见谅。 + - 如果你有你心仪的工作,请在我这里许下愿望,如果实现了,请记得回来更新状态 + - 许愿墙为飞书文档,你可以编辑好以后,导出为xlsx文档,然后覆盖本项目中resources文件夹中的"**许愿墙.xlsx**"文档 + - 接下来,你就可以把xlsx文档,提交到main分支,然后……你就是本项目的开发者之一了。 + - 你可以在你的简历中加上一条,Github,热门开源项目开发者之一。 + - 但是你需要尽可能的,熟悉本项目的逻辑,去想象某一块功能就是你做的,这样可以更从容,更优雅的"吹牛逼”。 + - 只要你相信你自己,本项目就会帮你,你要记住,是你自己拯救的你自己。 > 需要注意的是,提交pr的commit:请固定使用"✨I can do it!“,然后提交pr即可,剩下的,我全都搞定啦! - 📌 **目前该项目存在的问题** - - 当前招聘市场,有效的软件仅有 Boss 和 猎聘。 - - 如果 Boss 出现掉线等问题,请注意两点: - 1. 修改间隔等待时间; - 2. 当天停止投递,第二天接着投,否则可能会封号。 - - **最重要的事情:不要依赖程序投递 Boss!!!** - 手机上的 Boss,比本程序网页端靠谱得多。当你手机投的很累,又没有投够 100 个,请再使用本程序的 Boss 投递! - - 本项目为 GitHub 热门开源项目,目前已申请 Intelli 的开源支持计划。加入开发组意味着你可以获得 Intelli 编辑器官方的**免费全家桶永久使用权**,欢迎联系我! - - 安卓版本开发中,我有 Auto.js 的资源,我可以无偿分享:只要你对 Auto.js 感兴趣,或者你会、你想学,都可以联系我。如果有谁能开发 iOS 端 Appium,也请**尽快联系我**。 - - 本项目遵循 MIT 协议。是的,你可以商业化,但是——**真心希望你能帮助更多人,团结起来,冲破这片天!** + - 当前招聘市场,有效的软件仅有 Boss 和 猎聘(有少部分岗位)。 + - 如果 Boss 出现掉线等问题,请注意两点: + 1. 当天停止投递,第二天接着投,否则可能会封号。 + - **最重要的事情:不要依赖程序投递 Boss!!!** + 手机上的 Boss,比本程序网页端靠谱得多。当你手机投的很累,又没有投够 100 个,请再使用本程序的 Boss 投递! + - 本项目为 GitHub 热门开源项目,目前已申请 Intelli 的开源支持计划。加入开发组意味着你可以获得 Intelli 编辑器官方的* + *免费全家桶永久使用权**,欢迎联系我! + - 本项目遵循 MIT 协议。是的,你可以商业化,但是——**真心希望你能帮助更多人,团结起来!** + - [【重要】跳转到文末更新日志](#-更新日志) --- - 🚀 **本项目资源** - - **内推链接**:[飞书文档](https://fcv1y6gslc.feishu.cn/sheets/N3wAsfBqhhEWNDtI29AcD7GAnV9?from=from_copylink) - - **简历修改、面试指导、背调跟随、全套服务流程**:如你需要,都可联系。 + - **内推链接**:[飞书文档](https://fcv1y6gslc.feishu.cn/sheets/N3wAsfBqhhEWNDtI29AcD7GAnV9?from=from_copylink) + - **简历修改、面试指导、背调跟随、全套服务流程**:如你需要,都可联系。 --- - 🧠 **最后的心法** - - 我需要你做的,就是**认真准备每一个面试**,去**争取每一个 offer**,去实现你的愿望、目标、梦想。 - - 而不是怕这怕那,去抱怨这个抱怨那个,找借口,逃避,放弃。 + - 我需要你做的,就是**认真准备每一个面试**,去**争取每一个 offer**,去实现你的愿望、目标、梦想。 + - 而不是怕这怕那,去抱怨这个抱怨那个,找借口,逃避,放弃。 > 💥 **“怕输,你就不配赢!”** @@ -90,8 +88,6 @@ > 如果带着不同目的或者没想清楚就进群的 > 一经发现群主会对您的家人及朋友进行亲切(**没有素质**)的问候 > 并将您请出群聊,请珍惜交流的机会,谢谢! -> 我在22年失业玩了命的学AI,如今找到了AI工程师的工作 -> 救了我的,是曾经那个疯狂努力的自己,我希望你也可以 ## 🚀 如何使用? @@ -115,94 +111,47 @@ cd get_jobs - 🔩 通用配置 - - 日志文件在 **target/logs** 目录下,所有日志都会输出在以运行日期结尾的日志文件中 - - **Constant.WAIT_TIME**:超时等待时间,单位秒,用于等待页面加载 - - **cookie登录**: 扫码后会自动保存**cookie.json**文件在代码运行目录下,换号直接删除**cookie.json**即可 - - 每个平台的配置转换码都在平台文件夹下的Enum类里,找到相应的代码添加到类中即可 + - 日志文件在 **target/logs** 目录下,所有日志都会输出在以运行日期结尾的日志文件中 + - **Constant.WAIT_TIME**:超时等待时间,单位秒,用于等待页面加载 + - **cookie登录**: 扫码后会自动保存**cookie.json**文件在代码运行目录下,换号直接删除**cookie.json**即可 + - 每个平台的配置转换码都在平台文件夹下的Enum类里,找到相应的代码添加到类中即可 - 📢 企业微信消息推送设置 - - 把[.env_template](src/main/resources/.env_template)文件重命名为 `.env` - - 在企业微信中创建一个群聊,然后添加机器人,获取到机器人URL,复制到 `.env`文件中的 `HOOK_URL`即可 - - 保持[config.yaml](src/main/resources/config.yaml)文件中 `bot.is_send`为true - - 企业微信推送示例 - `企业微信推送示例` + - 把[.env_template](src/main/resources/.env_template)文件重命名为 `.env` + - 在企业微信中创建一个群聊,然后添加机器人,获取到机器人URL,复制到 `.env`文件中的 `HOOK_URL`即可 + - 保持[config.yaml](src/main/resources/config.yaml)文件中 `bot.is_send`为true + - 企业微信推送示例 + 企业微信推送示例 > 完成以上配置,在每个平台投递结束简历后,便会在企业微信的群聊内,推送岗位的投递情况,无须改动其他代码 - 🤖 AI配置 - - `.env`配制如下: - ``` - HOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key_here - BASE_URL=https://api.openai.com - API_KEY=sk-xxx - MODEL=gpt-4o-mini - ``` - - `HOOK_URL`:企业微信机器人推送的链接 - - `BASE_URL`:直连或中转链接地址,如果是直连需要开梯子 - - `API_KEY`:调用的API KEY - - `MODEL`:需要使用的模型名称 - - > 根据测试,boss直聘在每天所有的岗位投递结束后消耗的额度(gpt-4o-mini)大约在0.06美元(6美分) - > 左右,代理除了在本项目中可用,也可使用客户端(https://github.com/knowlimit/ChatGPT-NextWeb) - > 在日常生活中使用,所以不会浪费,充值额度1刀起,随用随充 + - `.env`配制如下: + ``` + HOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key_here + BASE_URL=https://api.openai.com + API_KEY=sk-xxx + MODEL=gpt-4o-mini + ``` + - `HOOK_URL`:企业微信机器人推送的链接 + - `BASE_URL`:直连或中转链接地址,如果是直连需要开梯子 + - `API_KEY`:调用的API KEY + - `MODEL`:需要使用的模型名称 + + > 根据测试,boss直聘在每天所有的岗位投递结束后消耗的额度(gpt-4o-mini)大约在0.06美元(6美分) + > 左右,代理除了在本项目中可用,也可使用客户端(https://github.com/knowlimit/ChatGPT-NextWeb)进行使用 + > 在日常生活中使用,所以不会浪费,充值额度1刀起,随用随充 > 💥注意!AI代理地址:如云API:https://api.ruyun.fun/ - > ,该网站可自主充值需要的金额,无任何捆绑消费,支持市面上全部大模型,2人民币=1美元,base_url默认使用"https://api.ruyun.fun/" - > 即可 - > + 即可 - - AI生成的打招呼语示例 - `AI生成的打招呼语示例` - -- ⚙️ **主要的配置文件**([config.yaml](src/main/resources/config.yaml)) + - AI生成的打招呼语示例 + AI生成的打招呼语示例 + +- ⚙️ **最重要的配置文件**([💥config.yaml💥](src/main/resources/config.yaml)) + > 因为配置文主要改动较多,所以不放在自述文件中,请自己根据需要修改 - ``` - # 带[ ]括号的,就是多选,不带的就是单选 - boss: - sayHi: "您好,我有7年工作经验,还有AIGC大模型、Java,Python,Golang和运维的相关经验,希望应聘这个岗位,期待可以与您进一步沟通,谢谢!" #必须要关闭boss的自动打招呼 - keywords: [ "大模型工程师", "AI工程师", "Java", "Python", "Golang" ] # 需要搜索的职位,会依次投递 - industry: [ "不限" ] # 公司行业,只能选三个,相关代码枚举的部分,如果需要其他的需要自己找 - cityCode: [ "上海" ] # 只列举了部分,如果没有的需要自己找:目前支持的:全国 北京 上海 广州 深圳 成都 - experience: [ "不限" ] # 工作经验:"应届毕业生", "1年以下", "1-3年", "3-5年", "5-10年", "10年以上" - jobType: "不限" #求职类型:"全职", "兼职" - salary: "不限" # 薪资(单选):"3K以下", "3-5K", "5-10K", "10-20K", "20-50K", "50K以上" - degree: [ "不限" ] # 学历: "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士" - scale: [ "不限" ] # 公司规模:"0-20人", "20-99人", "100-499人", "500-999人", "1000-9999人", "10000人以上" - stage: [ "不限" ] # "未融资", "天使轮", "A轮", "B轮", "C轮", "D轮及以上", "已上市", "不需要融资" - expectedSalary: [ 25,35 ] #期望薪资,单位为K,第一个数字为最低薪资,第二个数字为最高薪资,只填一个数字默认为最低薪水 - filterDeadHR: true # 是否过滤不活跃HR,该选项会过滤半年前活跃的HR - enableAI: true #开启AI检测与自动生成打招呼语 - - job51: - jobArea: [ "上海" ] #工作地区:目前只有【北京 成都 上海 广州 深圳】 - keywords: [ "java", "python", "go", "golang", "大模型", "软件工程师" ] #关键词:依次投递 - salary: [ "不限" ] #薪资范围:只能选5个【"2千以下", "2-3千", "3-4.5千", "4.5-6千", "6-8千", "0.8-1万", "1-1.5万", "1.5-2万", "2-3万", "3-4万", "4-5万", "5万以上"】 - - lagou: - keywords: [ "AI工程师","Java","Golang","Python" ] #搜索关键词 - cityCode: "上海" #拉勾城市名没有限制,直接填写即可 - salary: "不限" #薪资【"不限","2k以下", "2k-5k", "5k-10k", "10k-15k", "15k-25k", "25k-50k", "50k以上"】 - scale: [ "不限" ] #公司规模【"不限","少于15人", "15-50人", "50-150人", "150-500人", "500-2000人", "2000人以上"】 - gj: "在校/应届,3年及以下" - - liepin: - cityCode: "上海" # 目前支持的:全国 北京 上海 广州 深圳 成都 - keywords: [ "Java", "Python", "Golang", "大模型" ] - salary: "不限" # 填 15$30 代表 15k-30k - - zhilian: - cityCode: "上海" - salary: "25001,35000" #薪资区间 - keywords: [ "AI", "Java", "Python", "Golang" ] - - ai: - introduce: "我熟练使用Spring Boot、Spring Cloud、Alibaba Cloud及其生态体系,擅长MySQL、Oracle、PostgreSQL等关系型数据库以及MongoDB、Redis等非关系型数据库。熟悉Docker、Kubernetes等容器化技术,掌握WebSocket、Netty等通信协议,拥有即时通讯系统的开发经验。熟练使用MyBatis-Plus、Spring Data、Django ORM等ORM框架,熟练使用Python、Golang开发,具备机器学习、深度学习及大语言模型的开发与部署经验。此外,我熟悉前端开发,涉及Vue、React、Nginx配置及PHP框架应用" #这是喂给AI的提示词,主要介绍自己的优势 - prompt: "我目前在找工作,%s,我期望的的岗位方向是【%s】,目前我需要投递的岗位名称是【%s】,这个岗位的要求是【%s】,如果这个岗位和我的期望与经历基本符合,注意是基本符合,那么请帮我写一个给HR打招呼的文本发给我,如果这个岗位和我的期望经历完全不相干,直接返回false给我,注意只要返回我需要的内容即可,不要有其他的语气助词,重点要突出我和岗位的匹配度以及我的优势,我自己写的招呼语是:【%s】,你可以参照我自己写的根据岗位情况进行适当调整" #这是AI的提示词,可以自行修改 - - bot: - is_send: true #开启企业微信消息推送 - ``` - boss直聘([Boss.java](src/main/java/boss/Boss.java))【最推荐!每日仅可发起100次新聊天,活跃度还行,但是每日投递次数太少】 @@ -211,14 +160,14 @@ cd get_jobs > 现在找工作是很难,但也别做舔狗,打工人不是牛马! > - - 发送图片简历 + - 发送图片简历 > 在resources文件夹下,将自己的pdf简历转换为resume.jpg,同时配置项sendImgResume为ture,即可自动发送图片简历 > pdf转图片需要wps会员,如果找不到相关工具,可联系群主帮忙转换,5r/次 > - - 目标薪资设置:expectedSalary: [ 25,35 ] - - 单位为K,第一个数字为最低薪资,第二个数字为最高薪资,只填一个数字默认为只要求最低薪水,不要求最高薪水 + - 目标薪资设置:expectedSalary: [ 25,35 ] + - 单位为K,第一个数字为最低薪资,第二个数字为最高薪资,只填一个数字默认为只要求最低薪水,不要求最高薪水 ``` data.json //黑名单数据,在投递结束后会查询聊天记录寻找不合适的公司添加进去 @@ -311,8 +260,8 @@ cd get_jobs > 本项目文档已相对完善,如有运行仍有问题,请添加QQ群联系群主或在群内沟通 - 请注意: - 1. 本项目不支持服务器部署,无须尝试,如招聘网站发现访问者为服务器IP,不会返回任何网站数据。 - 2. 在开发与部署过程有任何问题都可在群内沟通,但群内的同学没有义务必须要解决您的问题,请保持礼貌提问的态度。 + 1. 本项目不支持服务器部署,无须尝试,如招聘网站发现访问者为服务器IP,不会返回任何网站数据。 + 2. 在开发与部署过程有任何问题都可在群内沟通,但群内的同学没有义务必须要解决您的问题,请保持礼貌提问的态度。 > 注:本项目为免费开源项目,非Saas类出售商品,不会考虑任何兼容的设备以及他人的需求,如多位同学有相同的需求可以提出issue,具有一定需求性会考虑开发,其他的问题有能力就自己修改,否则请联系群主,非诚勿扰。 @@ -320,21 +269,31 @@ cd get_jobs ## 📑 更新日志 -- 2024-04-15 01:52:18 - 1. 新增config.yaml,目前仅需修改配置文件即可,已全平台支持 - 2. cookie有效期延长,保持至少一周(拉勾平台除外)【安慰剂】 -- 2024-04-28 15:20:06 - 1. boos:自动更新黑名单公司 -- 2024-06-06 01:49:38 - 1. boos:若公司名小于2个字或4个字母则不会添加进黑名单 - 2. 添加linux系统支持。 -- 2024-06-06 17:41:20 - 1. boss支持多城市投递。 -- 2024-08-11 18:39:56 - 1. 修复智联,猎聘等不能投递的问题。 - 2. 添加定时投递功能 -- 2024-08-12 22:56:20 - 1. 添加企业微信消息推送功能 +--- +* 2025-08-08 04:33:56 + 1. Boss直聘逻辑修正,现已能完整运行所有流程。 + 2. 图片简历不能发送,AI打招呼功能正常。 + 3. 目前boss驱动已从Selenium改为playwright。 + 4. 新增gui分支,该分支为本项目的gui版本,主要为群管理:【凯】提供。 + 5. 砍掉了bark推送,用户地区码,手机端等杂七杂八不重要的功能,返璞归真。 +--- +* 2024-08-12 22:56:20 + 1. 添加企业微信消息推送功能 +* 2024-08-12 22:56:20 + 1. 添加企业微信消息推送功能 +* 2024-08-11 18:39:56 + 1. 修复智联,猎聘等不能投递的问题。 + 2. 添加定时投递功能 +* 2024-06-06 17:41:20 + 1. boss支持多城市投递。 +* 2024-06-06 01:49:38 + 1. boos:若公司名小于2个字或4个字母则不会添加进黑名单 + 2. 添加linux系统支持。 +* 2024-04-28 15:20:06 + 1. boos:自动更新黑名单公司 +* 2024-04-15 01:52:18 + 1. 新增config.yaml,目前仅需修改配置文件即可,已全平台支持 + 2. cookie有效期延长,保持至少一周(拉勾平台除外)【安慰剂】 --- @@ -360,7 +319,8 @@ cd get_jobs 2. 从 `main` 分支新建你的个人开发分支 3. 开发完成后,提交 Pull Request 到 **loks666/get_jobs 的 `dev` 分支** (❗ **注意:不是 main,是 dev!**) -4. 提交 Commit 时,请在信息前加上一个符合提交内容的 **Emoji 表情**([emoji网站](https://www.emojiall.com/zh-hans/all-emojis))自由发挥! +4. 提交 Commit 时,请在信息前加上一个符合提交内容的 **Emoji 表情 + **([emoji网站](https://www.emojiall.com/zh-hans/all-emojis))自由发挥! 5. 等待管理员审核,验证无误后,代码将合并到 `main` 分支 6. 表现优秀者,可提前申请加入开发组,与我们并肩作战! @@ -387,12 +347,12 @@ cd get_jobs - 近日已经有人反馈,有人拿着本项目免费开源的代码,在闲鱼等小红书各处售卖 - 本项目代码完全开源免费,请勿上当受骗,请大家擦亮眼睛 -- 这是一个将本项目免费源码的网站,请避雷:http://yjzd.westpy.cn/ - `骗子网站` - `骗子1` - `骗子2` - `骗子3` - `骗子4` +- 这是一个将本项目免费源码的网站,请避雷:http://yjzd.westpy.cn/ + 骗子网站 + 骗子1 + 骗子2 + 骗子3 + 骗子4 --- diff --git a/src/main/java/StartAll.java b/src/main/java/StartAll.java index ec866a9d..6a41b39d 100644 --- a/src/main/java/StartAll.java +++ b/src/main/java/StartAll.java @@ -1,4 +1,3 @@ -import boss.MobileBossConfig; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutorService; @@ -13,7 +12,6 @@ public class StartAll { // 存储所有子进程的引用 private static final List childProcesses = new ArrayList<>(); - private static final MobileBossConfig mobileBossConfig = MobileBossConfig.init(); public static void main(String[] args) { // Create a ScheduledExecutorService for Boss @@ -30,13 +28,6 @@ public static void main(String[] args) { } }; - // Schedule Boss task to run every 60 minutes - bossScheduler.scheduleAtFixedRate(bossTask, 0, mobileBossConfig.getNextIntervalMinutes(), TimeUnit.MINUTES); - - - - - // 创建一个统一的线程池来执行所有任务 ExecutorService executorService = Executors.newFixedThreadPool(2); diff --git a/src/main/java/ai/AiService.java b/src/main/java/ai/AiService.java index 1a878bc3..58fb469c 100644 --- a/src/main/java/ai/AiService.java +++ b/src/main/java/ai/AiService.java @@ -114,13 +114,57 @@ public static String sendRequest(String content) { public static void main(String[] args) { + System.out.println(cleanBossDesc(".EwyXFHpFfseN{display:inline-block;width:0.1px;height:0.1px;overflow:hidden;visibility: hidden;}.FxpRjMznwNS{display:inline-block;font-size:0!important;width:1em;height:1em;visibility:hidden;line-height:0;}.QTsRdnap{display:inline-block;font-size:0!important;width:1em;height:1em;visibility:hidden;line-height:0;}.spBzTCGii{display:inline-block;font-size:0!important;width:1em;height:1em;visibility:hidden;line-height:0;}.DXpfskbRdfn{display:inline-block;width:0.1px;height:0.1px;overflow:hidden;visibility: hidden;}.snNcSPFFs{font-style:normal;font-weight:normal}.zjziXGAdnjK{font-style:normal;font-weight:normal}.CjmzfkfTmx{font-style:normal;font-weight:normal}.YYTWRZHhrm{font-style:normal;font-weight:normal}.cfAzXEKs{font-style:normal;font-weight:normal}岗位职责:\n" + + "kanzhun一、boss产品+AI的实BOSS直聘施与落地能力\n" + + "1、负责公司产品+AI的实施与落地,熟悉各基础大模型性能,熟练应用大模型关键技术,面向隧道股份各需求,协同规划产品落地路径,具备实施能力;\n" + + "2、负责聚焦场景和产品的大模型相关的训练工作,包括:需求分析、功能设计、数据集构建、模型训练、评估及优化等;\n" + + "3、熟悉包括RAG、指令数据构建、Prompt工程、模型Fine-tuning、Prompt Engineering等环节,实现大模型技术在领域内垂直场景的落地应用;\n" + + "4、熟悉NLP、CV、多模态等领域大模型结构、算法,具备追踪大模型领域内前沿的技术研究成果,包括但不局限预训练、强化学习、知识增强、分布式训练等,同时提出创新思路来推动升级的能力;\n" + + "5、具备优化计算、存储和网络性能的能力,以满足业务系统的资源需求,并设定具体的性能优化目标,如延迟、吞吐量、资源利用率等;在满足性能需求的前提下,优化计算、存储和网络资源的使用,降低总成本;\n" + + "6、对业务系统的效果进行持续调优,通过数据分析和系统改进,提升系统的性能和用户体验。\n" + + "二、项目管理与协作\n" + + "1、参与制定核心业务项目计划和需求分析,确保项目按时交付和达到高质量标准。\n" + + "带领项目成员进行端对端开发,制定项目计划、分配任务并指导项目成员完成开发工作。\n" + + "2、与跨部门团队紧密合作,包括开发人员、测试人员、产品经理等,共同推动项目的顺利进行。\n" + + "四、技术能力提升\n" + + "1、负责相关技术文档的撰写与整理。\n" + + "2、协同团队成员进行技术分享,促进***学习于经验交流。\n" + + "3、建立公司知识库,沉淀技术文档、***实践、案例分享等,方便企业内部日常学习与参考。\n" + + "\n" + + "任职资格:\n" + + "一、教育背景\n" + + "本科及以上学历,电子工程、计算机科学、人工智能等相关领域专业。\n" + + "二、工作经验\n" + + "1、具备3年以上AI相关开发经验或5年以上软件开发经验(优秀者可适当放宽工作年限要求)。\n" + + "2、具备Rerank、Embedding、Langchain、RAG等服务开发及部署经验者优先。\n" + + "具备大模型应用开发经验,在智能问答、代码review、代码续写、测试用例生成等方向有成功经验者优先。\n" + + "4、有大型互联网公司大规模机器学习平台相关研发落地经验者优先。\n" + + "三、专业知识\n" + + "1、熟悉主流大模型如GPT、Gemini、LLaMA、ChatGLM等及其原理,并能进行针对性模型开发工作;\n" + + "2、了解深度学习等技术,熟悉大模型训练、推理、量化和部署者优先;\n" + + "3、了解主流AI应用框架者(如TensorFlow、PyTorch、longchain等)优先;\n" + + "4、熟悉JAVA/C++/Go/Python任一语言,有完整的项目开发经验,具备核心模块设计和维护经验。有一定的算法工程化能力,能够实现算法/模型的工程化与应用部署;具有NLP相关技术经验者更佳;\n" + + "熟悉Agent框架,有优化能力,包括planing、action、tools use、memory等核心Agent能力的提升者优先;、了解深度学习等技术,熟悉大模型训练、推理、量化和部署者优先;\n" + + "四、能力要求\n" + + "1、具备良好的问题解决能力和逻辑思维,能独立分析并解决技术难题。\n" + + "2、具备良好的团队合作精神和沟通能力,能在跨部门协作中发挥积极作用。\n" + + "3、具备良好的学习能力和适应能力,能够快速掌握新技术和新知识。\n" + + "4、具备较强的抗压能力,能够适应一定频率的出差与加班,满足工作中的紧急任务需求。")); try { // 示例:发送请求 String content = "你好"; String response = sendRequest(content); System.out.println("AI回复: " + response); } catch (Exception e) { - e.printStackTrace(); + log.error("AI异常!"); } } + + public static String cleanBossDesc(String raw) { + return raw.replaceAll("kanzhun|BOSS直聘|来自BOSS直聘", "") + .replaceAll("[\\u200b-\\u200d\\uFEFF]", "") + .replaceAll("<[^>]+>", "") // 如果有HTML标签就用 + .replaceAll("\\s+", " ") + .trim(); + } } diff --git a/src/main/java/boss/Boss.java b/src/main/java/boss/Boss.java index e83858d2..f16b4967 100644 --- a/src/main/java/boss/Boss.java +++ b/src/main/java/boss/Boss.java @@ -3,18 +3,10 @@ import ai.AiConfig; import ai.AiFilter; import ai.AiService; -import com.microsoft.playwright.ElementHandle; import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; import lombok.SneakyThrows; import org.json.JSONObject; -import org.openqa.selenium.By; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.Keys; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.devtools.DevTools; -import org.openqa.selenium.devtools.v135.page.Page; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import utils.*; @@ -23,26 +15,25 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; + import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.time.Duration; import java.util.*; import java.util.stream.Collectors; +import java.util.Scanner; -import static boss.BossElementLocators.*; +import static boss.Locators.*; import static utils.Bot.sendMessageByTime; -import static utils.Constant.*; import static utils.JobUtils.formatDuration; -import static utils.SeleniumUtil.*; /** * @author loks666 - * 项目链接: https://github.com/loks666/get_jobs - * Boss直聘自动投递 + * 项目链接: https://github.com/loks666/get_jobs + * Boss直聘自动投递 */ public class Boss { private static final Logger log = LoggerFactory.getLogger(Boss.class); @@ -52,8 +43,8 @@ public class Boss { static Set blackRecruiters; static Set blackJobs; static List resultList = new ArrayList<>(); - static String dataPath = ProjectRootResolver.rootPath + "/src/main/java/boss/data.json"; - static String cookiePath = ProjectRootResolver.rootPath + "/src/main/java/boss/cookie.json"; + static String dataPath = "src/main/java/boss/data.json"; + static String cookiePath = "src/main/java/boss/cookie.json"; static Date startDate; static BossConfig config = BossConfig.init(); @@ -94,12 +85,11 @@ public class Boss { public static void main(String[] args) { loadData(dataPath); - // 暂时使用 PlayWright 获取岗位,后续直接复用原来逻辑,后期优化全面替换 selenium,全部改为PlayWright - SeleniumUtil.initDriver(); + // 使用 PlayWright 获取岗位 PlaywrightUtil.init(); startDate = new Date(); login(); - config.getCityCode().forEach(Boss::postJobByCityByPlaywright); + config.getCityCode().forEach(Boss::postJobByCity); log.info(resultList.isEmpty() ? "未发起新的聊天..." : "新发起聊天公司如下:\n{}", resultList.stream().map(Object::toString).collect(Collectors.joining("\n"))); if (!config.getDebugger()) { @@ -115,144 +105,157 @@ private static void printResult() { saveData(dataPath); resultList.clear(); if (!config.getDebugger()) { - CHROME_DRIVER.close(); - CHROME_DRIVER.quit(); + PlaywrightUtil.close(); } } - private static void postJobByCityByPlaywright(String cityCode) { + private static void postJobByCity(String cityCode) { String searchUrl = getSearchUrl(cityCode); for (String keyword : config.getKeywords()) { + int postCount = 0; // 使用 URLEncoder 对关键词进行编码 String encodedKeyword = URLEncoder.encode(keyword, StandardCharsets.UTF_8); String url = searchUrl + "&query=" + encodedKeyword; - log.info("查询岗位链接:{}", url); + log.info("投递地址:{}", searchUrl + "&query=" + keyword); com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); - PlaywrightUtil.loadCookies(cookiePath); page.navigate(url); - // 记录下拉前后的岗位数量 - int previousJobCount = 0; - int currentJobCount = 0; - int unchangedCount = 0; - - if (isJobsPresent()) { - // 尝试滚动页面加载更多数据 - try { - // 获取岗位列表并下拉加载更多 - log.info("开始获取岗位信息..."); - - while (unchangedCount < 2) { - // 获取所有岗位卡片 - List jobCards = page.querySelectorAll(JOB_LIST_SELECTOR); - currentJobCount = jobCards.size(); - - System.out.println("当前已加载岗位数量: " + currentJobCount); - - // 判断是否有新增岗位 - if (currentJobCount > previousJobCount) { - previousJobCount = currentJobCount; - unchangedCount = 0; - - // 滚动到页面底部加载更多 - PlaywrightUtil.evaluate("window.scrollTo(0, document.body.scrollHeight)"); - log.info("下拉页面加载更多..."); - - // 等待新内容加载 - page.waitForTimeout(2000); - } else { - unchangedCount++; - if (unchangedCount < 2) { - System.out.println("下拉后岗位数量未增加,再次尝试..."); - // 再次尝试滚动 - page.evaluate("window.scrollTo(0, document.body.scrollHeight)"); - page.waitForTimeout(2000); - } else { - break; - } - } - } + // 1. 滚动到底部,加载所有岗位卡片 + int lastCount = -1; + while (true) { + // 滑动到底部 + page.evaluate("window.scrollTo(0, document.body.scrollHeight);"); + PlaywrightUtil.sleep(1); // 等待加载(可根据速度调整) - log.info("已获取所有可加载岗位,共计: " + currentJobCount + " 个"); + // 获取所有卡片数 + Locator cards = page.locator("//ul[contains(@class, 'rec-job-list')]//li[contains(@class, 'job-card-box')]"); + int currentCount = cards.count(); - log.info("继续滚动加载更多岗位"); - } catch (Exception e) { - log.error("滚动加载数据异常: {}", e.getMessage()); - break; + // 判断是否继续滑动 + if (currentCount == lastCount) { + break; // 没有新内容,跳出循环 } + lastCount = currentCount; + } + log.info("【{}】岗位已全部加载,总数:{}", keyword, lastCount); + + // 2. 回到页面顶部 + page.evaluate("window.scrollTo(0, 0);"); + PlaywrightUtil.sleep(1); + + // 3. 逐个遍历所有岗位 + Locator cards = page.locator("//ul[contains(@class, 'rec-job-list')]//li[contains(@class, 'job-card-box')]"); + int count = cards.count(); + for (int i = 0; i < count; i++) { + // 重新获取卡片,避免元素过期 + cards = page.locator("//ul[contains(@class, 'rec-job-list')]//li[contains(@class, 'job-card-box')]"); + cards.nth(i).click(); + PlaywrightUtil.sleep(1); + + // 等待详情内容加载 + page.waitForSelector("div[class*='job-detail-box']", new Page.WaitForSelectorOptions().setTimeout(4000)); + Locator detailBox = page.locator("div[class*='job-detail-box']"); + + // 岗位名称 + String jobName = safeText(detailBox, "span[class*='job-name']"); + if (blackJobs.stream().anyMatch(jobName::contains)) continue; + // 薪资(原始) + String jobSalaryRaw = safeText(detailBox, "span.job-salary"); + String jobSalary = decodeSalary(jobSalaryRaw); + // 城市/经验/学历 + List tags = safeAllText(detailBox, "ul[class*='tag-list'] > li"); + // 标签 (暂时不使用) + // List jobLabels = safeAllText(detailBox, "ul[class*='job-label-list'] > li"); + // 岗位描述 + String jobDesc = safeText(detailBox, "p.desc"); + // Boss姓名、活跃 + String bossNameRaw = safeText(detailBox, "h2[class*='name']"); + String[] bossInfo = splitBossName(bossNameRaw); + String bossName = bossInfo[0]; + String bossActive = bossInfo[1]; + if (config.getDeadStatus().stream().anyMatch(bossActive::contains)) continue; + // Boss公司/职位 + String bossTitleRaw = safeText(detailBox, "div[class*='boss-info-attr']"); + String[] bossTitleInfo = splitBossTitle(bossTitleRaw); + String bossCompany = bossTitleInfo[0]; + if (blackCompanies.stream().anyMatch(bossCompany::contains)) continue; + String bossJobTitle = bossTitleInfo[1]; + if (blackRecruiters.stream().anyMatch(bossJobTitle::contains)) continue; + + // 创建Job对象 + Job job = new Job(); + job.setJobName(jobName); + job.setSalary(jobSalary); + job.setJobArea(String.join(", ", tags)); + job.setCompanyName(bossCompany); + job.setRecruiter(bossName); + job.setJobInfo(jobDesc); + + // 输出 +// log.info("正在投递:第{}条 | 岗位名称:{} | 薪资:{} | 城市/经验/学历:{} | Boss姓名:{} | 活跃状态:{} | 公司:{} | 职位:{}", (i + 1), jobName, jobSalary, tags, bossName, bossActive, bossCompany, bossJobTitle); + resumeSubmission(page, keyword, job); + postCount++; + } + log.info("【{}】岗位已投递完毕!已投递岗位数量:{}", keyword, postCount); + } + } + + public static String decodeSalary(String text) { + Map fontMap = new HashMap<>(); + fontMap.put('', '0'); + fontMap.put('', '1'); + fontMap.put('', '2'); + fontMap.put('', '3'); + fontMap.put('', '4'); + fontMap.put('', '5'); + fontMap.put('', '6'); + fontMap.put('', '7'); + fontMap.put('', '8'); + fontMap.put('', '9'); + StringBuilder result = new StringBuilder(); + for (char c : text.toCharArray()) { + result.append(fontMap.getOrDefault(c, c)); + } + return result.toString(); + } + + // 安全获取单个文本内容 + public static String safeText(Locator root, String selector) { + Locator node = root.locator(selector); + try { + if (node.count() > 0 && node.innerText() != null) { + return node.innerText().trim(); } - - resumeSubmission(keyword); + } catch (Exception e) { + // ignore } + return ""; } - /** - * selenium 默认暴露 navigator.webdriver,boss security-check.html - * 页面在DOM加载后立即运行一系列检测脚本 - * 可能 在 里就执行JS - * 页面有多个连续重定向或iframe跳转 - * 注入的js没赶上运行时机,无法隐藏 navigator.webdriver 属性 - * - * @param cityCode - */ - @Deprecated - private static void postJobByCity(String cityCode) { - String searchUrl = getSearchUrl(cityCode); - WebDriverWait wait = new WebDriverWait(CHROME_DRIVER, Duration.ofSeconds(40)); - for (String keyword : config.getKeywords()) { - // 使用 URLEncoder 对关键词进行编码 - String encodedKeyword = URLEncoder.encode(keyword, StandardCharsets.UTF_8); - - String url = searchUrl + "&query=" + encodedKeyword; - log.info("查询岗位链接:{}", url); - - DevTools devTools = CHROME_DRIVER.getDevTools(); - devTools.createSession(); - setDefaultHeaders(devTools); - injectStealthJs(devTools); - - CHROME_DRIVER.get(url); - while (true) { - log.info("等待页面加载完成"); - - // 确保页面加载完成 - wait.until( - ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[@class='job-list-container']"))); - - // 尝试滚动页面加载更多数据 - try { - JavascriptExecutor js = CHROME_DRIVER; - // 滚动到页面底部 - // js.executeScript("window.scrollTo(0, document.body.scrollHeight)"); - // 等待新内容加载 - SeleniumUtil.sleep(5); - - // 检查是否到底部了(是否有"没有更多了"的提示) - try { - WebElement bottomElement = CHROME_DRIVER.findElement( - By.xpath("//div[contains(text(), '没有更多了') or contains(@class, 'job-list-empty')]")); - if (bottomElement != null && bottomElement.isDisplayed()) { - log.info("已滚动到底部,没有更多数据"); - break; - } - } catch (Exception e) { - // 未找到底部元素,继续滚动 - } - - // 加载一定数量的岗位后可以选择跳出循环 - List jobCards = CHROME_DRIVER.findElements(By.className("job-card-box")); - for (WebElement jobCard : jobCards) { + // 安全获取多个文本内容 + public static List safeAllText(Locator root, String selector) { + try { + return root.locator(selector).allInnerTexts(); + } catch (Exception e) { + return new ArrayList<>(); + } + } - } + // Boss姓名+活跃状态拆分 + public static String[] splitBossName(String raw) { + String[] bossParts = raw.trim().split("\\s+"); + String bossName = bossParts[0]; + String bossActive = bossParts.length > 1 ? String.join(" ", Arrays.copyOfRange(bossParts, 1, bossParts.length)) : ""; + return new String[]{bossName, bossActive}; + } - log.info("继续滚动加载更多岗位"); - } catch (Exception e) { - log.error("滚动加载数据异常: {}", e.getMessage()); - break; - } - } - } + // Boss公司+职位拆分 + public static String[] splitBossTitle(String raw) { + String[] parts = raw.trim().split(" · "); + String company = parts[0]; + String job = parts.length > 1 ? parts[1] : ""; + return new String[]{company, job}; } private static boolean isJobsPresent() { @@ -292,28 +295,31 @@ private static void saveData(String path) { } private static void updateListData() { - CHROME_DRIVER.get("https://www.zhipin.com/web/geek/chat"); - SeleniumUtil.getWait(3); + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate("https://www.zhipin.com/web/geek/chat"); + PlaywrightUtil.sleep(3); - JavascriptExecutor js = CHROME_DRIVER; boolean shouldBreak = false; while (!shouldBreak) { try { - WebElement bottom = CHROME_DRIVER.findElement(By.xpath(FINISHED_TEXT)); - if ("没有更多了".equals(bottom.getText())) { + Locator bottomLocator = page.locator(FINISHED_TEXT); + if (bottomLocator.count() > 0 && "没有更多了".equals(bottomLocator.textContent())) { shouldBreak = true; } } catch (Exception ignore) { } - List items = CHROME_DRIVER.findElements(By.xpath(CHAT_LIST_ITEM)); - for (int i = 0; i < items.size(); i++) { + + Locator items = page.locator(CHAT_LIST_ITEM); + int itemCount = items.count(); + + for (int i = 0; i < itemCount; i++) { try { - WebElement companyElement = CHROME_DRIVER - .findElements(By.xpath(COMPANY_NAME_IN_CHAT)) - .get(i); - WebElement messageElement = CHROME_DRIVER - .findElements(By.xpath(LAST_MESSAGE)) - .get(i); + Locator companyElements = page.locator(COMPANY_NAME_IN_CHAT); + Locator messageElements = page.locator(LAST_MESSAGE); + + if (i >= companyElements.count() || i >= messageElements.count()) { + break; + } String companyName = null; String message = null; @@ -321,36 +327,20 @@ private static void updateListData() { while (retryCount < 2) { try { - companyName = companyElement.getText(); - message = messageElement.getText(); - break; // 成功获取文本,跳出循环 - } catch (org.openqa.selenium.StaleElementReferenceException e) { + companyName = companyElements.nth(i).textContent(); + message = messageElements.nth(i).textContent(); + break; + } catch (Exception e) { retryCount++; if (retryCount >= 2) { log.info("尝试获取元素文本2次失败,放弃本次获取"); break; } log.info("页面元素已变更,正在重试第{}次获取元素文本...", retryCount); - // 重新获取元素 - try { - companyElement = CHROME_DRIVER - .findElements( - By.xpath(COMPANY_NAME_IN_CHAT)) - .get(i); - messageElement = CHROME_DRIVER - .findElements( - By.xpath(LAST_MESSAGE)) - .get(i); - // 等待短暂时间后重试 - SeleniumUtil.sleep(1); - } catch (Exception ex) { - log.info("重新获取元素失败,放弃本次获取"); - break; - } + PlaywrightUtil.sleep(1); } } - // 只有在成功获取文本的情况下才继续处理 if (companyName != null && message != null) { boolean match = message.contains("不") || message.contains("感谢") || message.contains("但") || message.contains("遗憾") || message.contains("需要本") || message.contains("对不"); @@ -370,28 +360,18 @@ private static void updateListData() { log.error("寻找黑名单公司异常...", e); } } - WebElement element; + try { - WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath(SCROLL_LOAD_MORE))); - element = CHROME_DRIVER.findElement(By.xpath(SCROLL_LOAD_MORE)); + Locator scrollElement = page.locator(SCROLL_LOAD_MORE); + if (scrollElement.count() > 0) { + scrollElement.scrollIntoViewIfNeeded(); + } else { + page.evaluate("window.scrollTo(0, document.body.scrollHeight);"); + } } catch (Exception e) { - log.info("没找到滚动条..."); + log.error("滚动元素出错", e); break; } - - if (element != null) { - try { - js.executeScript("arguments[0].scrollIntoView();", element); - } catch (Exception e) { - log.error("滚动到元素出错", e); - } - } else { - try { - js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); - } catch (Exception e) { - log.error("滚动到页面底部出错", e); - } - } } log.info("黑名单公司数量:{}", blackCompanies.size()); } @@ -430,231 +410,122 @@ private static void parseJson(String json) { } @SneakyThrows - private static Integer resumeSubmission(String keyword) { - // 查找所有job卡片元素 - com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); - // 使用page.locator方法获取所有匹配的元素 - Locator jobLocators = BossElementFinder.getPlaywrightLocator(page, BossElementLocators.JOB_CARD_BOX); - // 获取元素总数 - int count = jobLocators.count(); - - List jobs = new ArrayList<>(); - // 遍历所有找到的job卡片 - for (int i = 0; i < count; i++) { - try { - Locator jobCard = jobLocators.nth(i); - String jobName = jobCard.locator(BossElementLocators.JOB_NAME).textContent(); - if (blackJobs.stream().anyMatch(jobName::contains) || !isTargetJob(keyword, jobName)) { - // 排除黑名单岗位 - continue; - } - String companyName = jobCard.locator(BossElementLocators.COMPANY_NAME).textContent(); - if (blackCompanies.stream().anyMatch(companyName::contains)) { - // 排除黑名单公司 - continue; - } + private static void resumeSubmission(com.microsoft.playwright.Page page, String keyword, Job job) { + PlaywrightUtil.sleep(1); - Job job = new Job(); - job.setHref(jobCard.locator(BossElementLocators.JOB_NAME).getAttribute("href")); - job.setCompanyName(companyName); - job.setJobName(jobName); - job.setJobArea(jobCard.locator(BossElementLocators.JOB_AREA).textContent()); - // 获取标签列表 - Locator tagElements = jobCard.locator(BossElementLocators.TAG_LIST); - int tagCount = tagElements.count(); - StringBuilder tag = new StringBuilder(); - for (int j = 0; j < tagCount; j++) { - tag.append(tagElements.nth(j).textContent()).append("·"); - } - if (tag.length() > 0) { - job.setCompanyTag(tag.substring(0, tag.length() - 1)); - } else { - job.setCompanyTag(""); - } - jobs.add(job); - } catch (Exception e) { - log.debug("处理岗位卡片失败: {}", e.getMessage()); - } + // 1. 查找“查看更多信息”按钮(必须存在且新开页) + Locator moreInfoBtn = page.locator("a.more-job-btn"); + if (moreInfoBtn.count() == 0) { + log.warn("未找到“查看更多信息”按钮,跳过..."); + return; } + // 强制用js新开tab + String href = moreInfoBtn.first().getAttribute("href"); + if (href == null || !href.startsWith("/job_detail/")) { + log.warn("未获取到岗位详情链接,跳过..."); + return; + } + String detailUrl = "https://www.zhipin.com" + href; - for (Job job : jobs) { - // 打开新的标签页 - ArrayList tabs = BossPageOperations.openLinkInNewTab(job.getHref()); + // 2. 新开详情页 + com.microsoft.playwright.Page detailPage = page.context().newPage(); + detailPage.navigate(detailUrl); + PlaywrightUtil.sleep(1); // 页面加载 - try { - // 等待聊天按钮出现 - Optional chatButton = BossElementFinder - .waitForElementVisible(BossElementLocators.CHAT_BUTTON); - if (chatButton.isEmpty()) { - Optional errorElement = BossElementFinder - .findElement(BossElementLocators.ERROR_CONTENT); - if (errorElement.isPresent() && errorElement.get().getText().contains("异常访问")) { - return -2; - } - } - } catch (Exception e) { - log.error("无法加载岗位详情页: {}", e.getMessage()); - BossPageOperations.closeCurrentTabAndSwitchTo(tabs, 0); - continue; + // 3. 查找“立即沟通”按钮 + Locator chatBtn = detailPage.locator("a.btn-startchat, a.op-btn-chat"); + boolean foundChatBtn = false; + for (int i = 0; i < 5; i++) { + if (chatBtn.count() > 0 && (chatBtn.first().textContent().contains("立即沟通"))) { + foundChatBtn = true; + break; } + PlaywrightUtil.sleep(1); + } + if (!foundChatBtn) { + log.warn("未找到立即沟通按钮,跳过岗位: {}", job.getJobName()); + detailPage.close(); + return; + } + chatBtn.first().click(); + PlaywrightUtil.sleep(1); - // 过滤不活跃HR - if (isDeadHR()) { - BossPageOperations.closeCurrentTabAndSwitchTo(tabs, 0); - log.info("该HR已过滤"); - SeleniumUtil.sleep(1); - continue; + // 4. 等待聊天输入框 + Locator inputLocator = detailPage.locator("div#chat-input.chat-input[contenteditable='true'], textarea.input-area"); + boolean inputReady = false; + for (int i = 0; i < 10; i++) { + if (inputLocator.count() > 0 && inputLocator.first().isVisible()) { + inputReady = true; + break; } + PlaywrightUtil.sleep(1); + } + if (!inputReady) { + log.warn("聊天输入框未出现,跳过: {}", job.getJobName()); + detailPage.close(); + return; + } - // 获取薪资 - try { - Optional salaryElement = BossElementFinder - .findElement(BossElementLocators.JOB_DETAIL_SALARY); - if (salaryElement.isPresent()) { - String salaryText = salaryElement.get().getText(); - job.setSalary(salaryText); - if (isSalaryNotExpected(salaryText)) { - // 过滤薪资 - log.info("已过滤:【{}】公司【{}】岗位薪资【{}】不符合投递要求", job.getCompanyName(), job.getJobName(), salaryText); - BossPageOperations.closeCurrentTabAndSwitchTo(tabs, 0); - continue; - } - } - } catch (Exception ignore) { - log.info("获取岗位薪资失败:{}", ignore.getMessage()); + // 5. AI智能生成打招呼语 + AiFilter aiResult = null; + if (config.getEnableAI()) { + String jd = job.getJobInfo(); + if (jd != null && !jd.isEmpty()) { + aiResult = checkJob(keyword, job.getJobName(), jd); } + } + String sayHi = config.getSayHi().replaceAll("[\\r\\n]", ""); + String message = (aiResult != null && aiResult.getResult() && isValidString(aiResult.getMessage())) + ? aiResult.getMessage() : sayHi; + + // 6. 输入打招呼语 + Locator input = inputLocator.first(); + input.click(); + if (input.evaluate("el => el.tagName.toLowerCase()") instanceof String tag && tag.equals("textarea")) { + input.fill(message); + } else { + input.evaluate("(el, msg) => el.innerText = msg", message); + } - // 获取招聘人员信息 + // 7. 发送图片简历(可选) + boolean imgResume = false; + if (config.getSendImgResume()) { try { - Optional recruiterElement = BossElementFinder - .findElement(BossElementLocators.RECRUITER_INFO); - if (recruiterElement.isPresent()) { - String recruiterName = recruiterElement.get().getText(); - job.setRecruiter(recruiterName.replaceAll("\\r|\\n", "")); - if (blackRecruiters.stream().anyMatch(recruiterName::contains)) { - // 排除黑名单招聘人员 - BossPageOperations.closeCurrentTabAndSwitchTo(tabs, 0); - continue; + URL resourceUrl = Boss.class.getResource("/resume.jpg"); + if (resourceUrl != null) { + File imageFile = new File(resourceUrl.toURI()); + Locator fileInput = detailPage.locator("//div[@aria-label='发送图片']//input[@type='file']"); + if (fileInput.count() > 0) { + fileInput.setInputFiles(imageFile.toPath()); + imgResume = true; } } - } catch (Exception ignore) { - log.info("获取招聘人员信息失败:{}", ignore.getMessage()); + } catch (Exception e) { + log.error("发送图片简历失败: {}", e.getMessage()); } + } - BossPageOperations.simulateUserBrowsing(); - Optional chatBtn = BossElementFinder.findElement(BossElementLocators.CHAT_BUTTON); - - boolean debug = config.getDebugger(); - - // 休息下,请求太频繁了 - SeleniumUtil.sleep(5); - if (!debug && chatBtn.isPresent() && "立即沟通".equals(chatBtn.get().getText())) { - String waitTime = config.getWaitTime(); - int sleepTime = 10; // 默认等待10秒 - - if (waitTime != null) { - try { - sleepTime = Integer.parseInt(waitTime); - } catch (NumberFormatException e) { - log.error("等待时间转换异常!!"); - } - } - - SeleniumUtil.sleep(sleepTime); - - AiFilter filterResult = null; - if (config.getEnableAI()) { - // AI检测岗位是否匹配 - Optional jdElement = BossElementFinder.findElement(BossElementLocators.JOB_DESCRIPTION); - if (jdElement.isPresent()) { - String jd = jdElement.get().getText(); - filterResult = checkJob(keyword, job.getJobName(), jd); - } - } - - chatBtn.get().click(); - - if (isLimit()) { - SeleniumUtil.sleep(1); - return -1; - } - - try { - try { - Optional dialogTitle = BossElementFinder - .findElement(BossElementLocators.DIALOG_TITLE); - if (dialogTitle.isPresent()) { - Optional closeBtn = BossElementFinder - .findElement(BossElementLocators.DIALOG_CLOSE); - if (closeBtn.isPresent()) { - closeBtn.get().click(); - chatBtn.get().click(); - } - } - } catch (Exception ignore) { - } - - Optional input = BossElementFinder - .waitForElementVisible(BossElementLocators.CHAT_INPUT); - if (input.isPresent()) { - input.get().click(); - Optional dialogElement = BossElementFinder - .findElement(BossElementLocators.DIALOG_CONTAINER); - if (dialogElement.isPresent() && "不匹配".equals(dialogElement.get().getText())) { - BossPageOperations.closeCurrentTabAndSwitchTo(tabs, 0); - continue; - } + // 8. 点击发送按钮(div.send-message 或 button.btn-send) + Locator sendBtn = detailPage.locator("div.send-message, button[type='send'].btn-send, button.btn-send"); + boolean sendSuccess = false; + if (sendBtn.count() > 0) { + sendBtn.first().click(); + PlaywrightUtil.sleep(1); + sendSuccess = true; + } else { + log.warn("未找到发送按钮,自动跳过!岗位:{}", job.getJobName()); + } - input.get().sendKeys( - filterResult != null && filterResult.getResult() - && isValidString(filterResult.getMessage()) - ? filterResult.getMessage() - : config.getSayHi().replaceAll("\\r|\\n", "")); - - Optional sendBtn = BossElementFinder - .waitForElementClickable(BossElementLocators.SEND_BUTTON); - if (sendBtn.isPresent()) { - sendBtn.get().click(); - SeleniumUtil.sleep(5); - - String recruiter = job.getRecruiter(); - String company = job.getCompanyName(); - String position = job.getJobName() + " " + job.getSalary() + " " + job.getJobArea(); - - // 发送简历图片 - Boolean imgResume = false; - if (config.getSendImgResume()) { - try { - // 从类路径加载 resume.jpg - URL resourceUrl = Boss.class.getResource("/resume.jpg"); - if (resourceUrl != null) { - File imageFile = new File(resourceUrl.toURI()); - imgResume = BossPageOperations.sendResumeImage(imageFile.getAbsolutePath()); - } - } catch (Exception e) { - log.error("获取简历图片路径失败: {}", e.getMessage()); - } - } + log.info("投递完成 | 岗位:{} | 招呼语:{} | 图片简历:{}", job.getJobName(), message, imgResume ? "已发送" : "未发送"); - SeleniumUtil.sleep(2); - log.info("正在投递【{}】公司,【{}】职位,招聘官:【{}】{}", company, position, recruiter, - imgResume ? "发送图片简历成功!" : ""); - resultList.add(job); - } - } - } catch (Exception e) { - log.error("发送消息失败:{}", e.getMessage(), e); - } - } + // 9. 关闭详情页,回到主页面 + detailPage.close(); + PlaywrightUtil.sleep(1); - if (!debug) { - BossPageOperations.closeCurrentTabAndSwitchTo(tabs, 0); - } - if (debug) { - break; - } + // 10. 成功投递加入结果 + if (sendSuccess) { + resultList.add(job); } - return resultList.size(); } public static boolean isValidString(String str) { @@ -662,49 +533,18 @@ public static boolean isValidString(String str) { } public static Boolean sendResume(String company) { - // 如果 config.getSendImgResume() 为 true,再去找图片 - if (!config.getSendImgResume()) { - return false; - } - - try { - // 从类路径加载 resume.jpg - URL resourceUrl = Boss.class.getResource("/resume.jpg"); - if (resourceUrl == null) { - log.error("在类路径下未找到 resume.jpg 文件!"); - return false; - } - - // 将 URL 转为 File 对象 - File imageFile = new File(resourceUrl.toURI()); - log.info("简历图片路径:{}", imageFile.getAbsolutePath()); - - if (!imageFile.exists()) { - log.error("简历图片不存在!: {}", imageFile.getAbsolutePath()); - return false; - } - - // 使用 XPath 定位 元素 - WebElement fileInput = CHROME_DRIVER - .findElement(By.xpath("//div[@aria-label='发送图片']//input[@type='file']")); - - // 上传图片 - fileInput.sendKeys(imageFile.getAbsolutePath()); - return true; - } catch (Exception e) { - log.error("发送简历图片时出错:{}", e.getMessage()); - return false; - } + log.warn("sendResume方法已废弃,请直接在主逻辑中使用playwright实现文件上传"); + return false; } /** * 检查岗位薪资是否符合预期 * * @return boolean - * true 不符合预期 - * false 符合预期 - * 期望的最低薪资如果比岗位最高薪资还小,则不符合(薪资给的太少) - * 期望的最高薪资如果比岗位最低薪资还小,则不符合(要求太高满足不了) + * true 不符合预期 + * false 符合预期 + * 期望的最低薪资如果比岗位最高薪资还小,则不符合(薪资给的太少) + * 期望的最高薪资如果比岗位最低薪资还小,则不符合(要求太高满足不了) */ private static boolean isSalaryNotExpected(String salary) { try { @@ -804,7 +644,7 @@ private static String cleanSalaryText(String salaryText) { } private static boolean isSalaryOutOfRange(Integer[] jobSalary, Integer miniSalary, Integer maxSalary, - String jobType) { + String jobType) { if (jobSalary == null) { return true; } @@ -827,36 +667,34 @@ private static boolean isSalaryOutOfRange(Integer[] jobSalary, Integer miniSalar } private static void RandomWait() { - SeleniumUtil.sleep(JobUtils.getRandomNumberInRange(3, 20)); + PlaywrightUtil.sleep(JobUtils.getRandomNumberInRange(3, 20)); } private static void simulateWait() { + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); for (int i = 0; i < 3; i++) { - ACTIONS.sendKeys(" ").perform(); - SeleniumUtil.sleep(1); + page.keyboard().press(" "); + PlaywrightUtil.sleep(1); } - ACTIONS.keyDown(Keys.CONTROL) - .sendKeys(Keys.HOME) - .keyUp(Keys.CONTROL) - .perform(); - SeleniumUtil.sleep(1); + page.keyboard().press("Control+Home"); + PlaywrightUtil.sleep(1); } - private static boolean isDeadHR() { + private static boolean isDeadHR(com.microsoft.playwright.Page page) { if (!config.getFilterDeadHR()) { return false; } try { // 尝试获取 HR 的活跃时间 - Optional activeTimeElement = BossElementFinder.findElement(HR_ACTIVE_TIME); - if (activeTimeElement.isPresent()) { - String activeTimeText = activeTimeElement.get().getText(); - log.info("{}:{}", getCompanyAndHR(), activeTimeText); + Locator activeTimeLocator = page.locator(HR_ACTIVE_TIME); + if (activeTimeLocator.count() > 0) { + String activeTimeText = activeTimeLocator.textContent(); + log.info("{}:{}", getCompanyAndHR(page), activeTimeText); // 如果 HR 活跃状态符合预期,则返回 true return containsDeadStatus(activeTimeText, config.getDeadStatus()); } } catch (Exception e) { - log.info("没有找到【{}】的活跃状态, 默认此岗位将会投递...", getCompanyAndHR()); + log.info("没有找到【{}】的活跃状态, 默认此岗位将会投递...", getCompanyAndHR(page)); } return false; } @@ -870,15 +708,17 @@ public static boolean containsDeadStatus(String activeTimeText, List dea return false;// 如果没有找到,返回 false } - private static String getCompanyAndHR() { - Optional element = BossElementFinder.findElement(RECRUITER_INFO); - return element.map(webElement -> webElement.getText().replaceAll("\n", "")).orElse("未知公司和HR"); + private static String getCompanyAndHR(com.microsoft.playwright.Page page) { + Locator recruiterLocator = page.locator(RECRUITER_INFO); + if (recruiterLocator.count() > 0) { + return recruiterLocator.textContent().replaceAll("\n", ""); + } + return "未知公司和HR"; } private static void closeWindow(ArrayList tabs) { - SeleniumUtil.sleep(1); - CHROME_DRIVER.close(); - CHROME_DRIVER.switchTo().window(tabs.get(0)); + log.warn("closeWindow方法已废弃,请使用playwright的page.close()方法"); + // 该方法已废弃,在playwright中直接使用page.close() } private static AiFilter checkJob(String keyword, String jobName, String jd) { @@ -889,41 +729,6 @@ private static AiFilter checkJob(String keyword, String jobName, String jd) { return result.contains("false") ? new AiFilter(false) : new AiFilter(true, result); } - private static boolean isTargetJob(String keyword, String jobName) { - boolean keywordIsAI = false; - for (String target : new String[] { "大模型", "AI" }) { - if (keyword.contains(target)) { - keywordIsAI = true; - break; - } - } - - boolean jobIsDesign = false; - for (String designOrVision : new String[] { "设计", "视觉", "产品", "运营" }) { - if (jobName.contains(designOrVision)) { - jobIsDesign = true; - break; - } - } - - boolean jobIsAI = false; - for (String target : new String[] { "AI", "人工智能", "大模型", "生成" }) { - if (jobName.contains(target)) { - jobIsAI = true; - break; - } - } - - if (keywordIsAI) { - if (jobIsDesign) { - return false; - } else if (!jobIsAI) { - return true; - } - } - return true; - } - private static Integer[] parseSalaryRange(String salaryText) { try { return Arrays.stream(salaryText.split("-")).map(s -> s.replaceAll("[^0-9]", "")) // 去除非数字字符 @@ -935,11 +740,15 @@ private static Integer[] parseSalaryRange(String salaryText) { return null; } - private static boolean isLimit() { + private static boolean isLimit(com.microsoft.playwright.Page page) { try { - SeleniumUtil.sleep(1); - String text = CHROME_DRIVER.findElement(By.className(DIALOG_CON.substring(1))).getText(); - return text.contains("已达上限"); + PlaywrightUtil.sleep(1); + Locator dialogLocator = page.locator(DIALOG_CON); + if (dialogLocator.count() > 0) { + String text = dialogLocator.textContent(); + return text.contains("已达上限"); + } + return false; } catch (Exception e) { return false; } @@ -949,39 +758,67 @@ private static boolean isLimit() { private static void login() { log.info("打开Boss直聘网站中..."); - CHROME_DRIVER.get(homeUrl); - if (SeleniumUtil.isCookieValid(cookiePath)) { - SeleniumUtil.loadCookie(cookiePath); - CHROME_DRIVER.navigate().refresh(); - SeleniumUtil.sleep(2); - - DevTools devTools = CHROME_DRIVER.getDevTools(); - devTools.createSession(); - // 注入脚本:隐藏 navigator.webdriver - devTools.send( - Page.addScriptToEvaluateOnNewDocument( - "Object.defineProperty(navigator, 'injected', {get: () => 123})", - Optional.empty(), // worldName(可空) - Optional.of(false), // includeCommandLineAPI - Optional.of(true) // runImmediately - )); + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate(homeUrl); + PlaywrightUtil.sleep(1); + // 检查滑块验证 + waitForSliderVerify(page); + + if (PlaywrightUtil.isCookieValid(cookiePath)) { + PlaywrightUtil.loadCookies(cookiePath); + page.reload(); + PlaywrightUtil.sleep(1); + waitForSliderVerify(page); + // 启用反检测模式 + PlaywrightUtil.initStealth(); } + if (isLoginRequired()) { log.error("cookie失效,尝试扫码登录..."); scanLogin(); } } + private static void waitForSliderVerify(com.microsoft.playwright.Page page) { + String SLIDER_URL = "https://www.zhipin.com/web/user/safe/verify-slider"; + // 最多等待5分钟(防呆,防止死循环) + long start = System.currentTimeMillis(); + while (true) { + String url = page.url(); + if (url != null && url.startsWith(SLIDER_URL)) { + System.out.println("\n【滑块验证】请手动完成Boss直聘滑块验证,通过后在控制台回车继续…"); + try { + System.in.read(); + } catch (Exception e) { + log.error("等待滑块验证输入异常: {}", e.getMessage()); + } + PlaywrightUtil.sleep(1); + // 验证通过后页面url会变,循环再检测一次 + continue; + } + if ((System.currentTimeMillis() - start) > 5 * 60 * 1000) { + throw new RuntimeException("滑块验证超时!"); + } + break; + } + } + + private static boolean isLoginRequired() { try { - Optional buttonElement = BossElementFinder.findElement(LOGIN_BTNS); - if (buttonElement.isPresent() && buttonElement.get().getText().contains("登录")) { + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + Locator buttonLocator = page.locator(LOGIN_BTNS); + if (buttonLocator.count() > 0 && buttonLocator.textContent().contains("登录")) { return true; } } catch (Exception e) { try { - BossElementFinder.findElement(PAGE_HEADER); - BossElementFinder.findElement(ERROR_PAGE_LOGIN).ifPresent(WebElement::click); + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.locator(PAGE_HEADER).waitFor(); + Locator errorLoginLocator = page.locator(ERROR_PAGE_LOGIN); + if (errorLoginLocator.count() > 0) { + errorLoginLocator.click(); + } return true; } catch (Exception ex) { log.info("没有出现403访问异常"); @@ -995,13 +832,14 @@ private static boolean isLoginRequired() { @SneakyThrows private static void scanLogin() { // 访问登录页面 - CHROME_DRIVER.get(homeUrl + "/web/user/?ka=header-login"); - SeleniumUtil.sleep(3); + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate(homeUrl + "/web/user/?ka=header-login"); + PlaywrightUtil.sleep(1); // 1. 如果已经登录,则直接返回 try { - Optional element = BossElementFinder.findElement(LOGIN_BTN); - if (element.isPresent() && !Objects.equals(element.get().getText(), "登录")) { + Locator loginBtnLocator = page.locator(LOGIN_BTN); + if (loginBtnLocator.count() > 0 && !Objects.equals(loginBtnLocator.textContent(), "登录")) { log.info("已经登录,直接开始投递..."); return; } @@ -1011,53 +849,46 @@ private static void scanLogin() { log.info("等待登录..."); // 2. 定位二维码登录的切换按钮 - Optional scanButton = BossElementFinder.waitForElementClickable(LOGIN_SCAN_SWITCH, 30); - if (scanButton.isEmpty()) { - log.error("未找到二维码登录按钮,登录失败"); - return; - } - - // 3. 登录逻辑 - boolean login = false; - - // 4. 记录开始时间,用于判断10分钟超时 - long startTime = System.currentTimeMillis(); - final long TIMEOUT = 10 * 60 * 1000; // 10分钟 - - // 5. 用于监听用户是否在控制台回车 - Scanner scanner = new Scanner(System.in); + try { + Locator scanButton = page.locator(LOGIN_SCAN_SWITCH); + scanButton.click(); + + // 3. 登录逻辑 + boolean login = false; + + // 4. 记录开始时间,用于判断10分钟超时 + long startTime = System.currentTimeMillis(); + final long TIMEOUT = 10 * 60 * 1000; // 10分钟 + + while (!login) { + // 判断是否超时 + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed >= TIMEOUT) { + log.error("超过10分钟未完成登录,程序退出..."); + System.exit(1); + } - while (!login) { - // 如果已经超过10分钟,退出程序 - long elapsed = System.currentTimeMillis() - startTime; - if (elapsed >= TIMEOUT) { - log.error("超过10分钟未完成登录,程序退出..."); - System.exit(1); + try { + // 判断页面上是否出现职位列表容器 + Locator jobList = page.locator("div.job-list-container"); + if (jobList.isVisible()) { + login = true; + log.info("用户已登录!"); + // 登录成功,保存Cookie + PlaywrightUtil.saveCookies(cookiePath); + break; + } + } catch (Exception e) { + log.error("检测元素时异常: {}", e.getMessage()); + } + // 每2秒检查一次 + Thread.sleep(2000); } - try { - // 尝试点击二维码按钮并等待页面出现已登录的元素 - scanButton.get().click(); - BossElementFinder.waitForElementVisible(LOGIN_SUCCESS_HEADER, 2); - BossElementFinder.waitForElementVisible(LOGIN_SUCCESS_INDICATOR, 2); - - // 如果上述元素都能找到,说明登录成功 - login = true; - log.info("登录成功!保存cookie..."); - } catch (Exception e) { - // 登录失败 - log.error("登录失败,等待用户操作或者 2 秒后重试..."); - // 每次登录失败后,等待2秒,同时检查用户是否按了回车 - boolean userInput = waitForUserInputOrTimeout(scanner); - if (userInput) { - log.info("检测到用户输入,继续尝试登录..."); - } - } + } catch (Exception e) { + log.error("未找到二维码登录按钮,登录失败", e); } - - // 登录成功后,保存Cookie - SeleniumUtil.saveCookie(cookiePath); } /** @@ -1081,7 +912,7 @@ private static boolean waitForUserInputOrTimeout(Scanner scanner) { } // 小睡一下,避免 CPU 空转 - SeleniumUtil.sleep(1); + PlaywrightUtil.sleep(1); } return false; } diff --git a/src/main/java/boss/BossElementLocators.java b/src/main/java/boss/Locators.java similarity index 98% rename from src/main/java/boss/BossElementLocators.java rename to src/main/java/boss/Locators.java index 9f8f6517..e9f9b7e7 100644 --- a/src/main/java/boss/BossElementLocators.java +++ b/src/main/java/boss/Locators.java @@ -4,7 +4,7 @@ * Boss直聘网页元素定位器 * 集中管理所有页面元素的定位表达式 */ -public class BossElementLocators { +public class Locators { // 主页相关元素 public static final String LOGIN_BTN = "//li[@class='nav-figure']"; public static final String LOGIN_SCAN_SWITCH = "//div[@class='btn-sign-switch ewm-switch']"; diff --git a/src/main/java/boss/MobileBoss.java b/src/main/java/boss/MobileBoss.java deleted file mode 100644 index adc2dd5a..00000000 --- a/src/main/java/boss/MobileBoss.java +++ /dev/null @@ -1,974 +0,0 @@ -package boss; - -import ai.AiConfig; -import ai.AiFilter; -import ai.AiService; -import lombok.SneakyThrows; -import org.json.JSONObject; -import org.openqa.selenium.By; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.Keys; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.StringUtils; -import utils.Job; -import utils.JobUtils; -import utils.ProjectRootResolver; -import utils.SeleniumUtil; - -import java.io.File; -import java.io.IOException; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.*; -import java.util.stream.Collectors; - -import static utils.Bot.sendMessageByTime; -import static utils.Constant.*; -import static utils.Constant.WAIT; -import static utils.JobUtils.formatDuration; - -public class MobileBoss { - private static final Logger log = LoggerFactory.getLogger(Boss.class); - static String homeUrl = "https://www.zhipin.com"; - static String baseUrl = "https://www.zhipin.com"; - static Set blackCompanies; - static Set blackRecruiters; - static Set blackJobs; - static List resultList = new ArrayList<>(); - static String dataPath = ProjectRootResolver.rootPath + "/src/main/java/boss/data.json"; - static String cookiePath = ProjectRootResolver.rootPath + "/src/main/java/boss/cookie.json"; - static Date startDate; - static MobileBossConfig config = MobileBossConfig.init(); - - public static void main(String[] args) { - loadData(dataPath); - SeleniumUtil.initDriver(true); - startDate = new Date(); - login(); - // 最好先填1个,多个城市的情况不确定会不会有什么问题或者导致请求过于频繁出现风险拦截 - config.getCityCode().forEach(MobileBoss::postJobByCity); - log.info(resultList.isEmpty() ? "未发起新的聊天..." : "新发起聊天公司如下:\n{}", resultList.stream().map(Object::toString).collect(Collectors.joining("\n"))); - if(!config.getDebugger()){ - printResult(); - } - - } - - private static void printResult() { - String message = String.format("\nBoss投递完成,共发起%d个聊天,用时%s", resultList.size(), formatDuration(startDate, new Date())); - log.info(message); - sendMessageByTime(message); - saveData(dataPath); - resultList.clear(); - if(!config.getDebugger()){ - CHROME_DRIVER.close(); - CHROME_DRIVER.quit(); - MOBILE_CHROME_DRIVER.close(); - MOBILE_CHROME_DRIVER.quit(); - } - } - - private static void postJobByCity(String cityCode) { - - for (String keyword : config.getKeywords()) { - String searchUrl = getSearchUrl(cityCode,keyword); - log.info("查询url:{}", searchUrl); -// WebDriverWait wait = new WebDriverWait(MOBILE_CHROME_DRIVER, 40); - WebDriverWait wait = new WebDriverWait(MOBILE_CHROME_DRIVER, Duration.ofSeconds(40)); - String url = searchUrl; - log.info("开始投递,页面url:{}", url); - MOBILE_CHROME_DRIVER.get(url); - // 点击立即沟通,建立chat窗口 - if (isMobileJobsPresent(wait)) { - JavascriptExecutor js = MOBILE_CHROME_DRIVER; - - int previousCount = 0; - int retry = 0; - // 向下滚动到底部 - while (true) { - // 当前页面中 class="item" 的 li 元素数量 - List items = MOBILE_CHROME_DRIVER.findElements(By.cssSelector("li.item")); - int currentCount = items.size(); - - // 滚动到底部 - // js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); - // js.executeScript("window.scrollTo(0, document.documentElement.scrollHeight)") - // 滚动到比页面高度更大的值,确保触发加载 - js.executeScript("window.scrollTo(0, document.documentElement.scrollHeight + 100)"); - SeleniumUtil.sleep(10); // 等待数据加载 - - // 检查数量是否变化 - if (currentCount == previousCount) { - retry++; - log.info("第{}次下拉重试" + retry); - if (retry >= 2) { - log.info("尝试2次下拉后无新增岗位,退出"); - break; // 连续两次未加载新数据,认为加载完毕 - } - } else { - retry = 0; // 重置尝试次数 - } - - previousCount = currentCount; - - if(config.getDebugger()){ - break; - } - - } - log.info("已加载全部岗位,总数量: " + previousCount); - } - - // chat页面进行消息沟通 - resumeSubmission(keyword); - } - - } - - /** - * 等待岗位列表元素加载完成 - * - * @param wait - * @return - */ - private static boolean isMobileJobsPresent(WebDriverWait wait) { - try { - // 判断页面是否存在岗位的元素 - WebElement jobList = wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='job-list job-list-new']/ul"))); - List jobCards = jobList.findElements(By.className("item")); - return !jobCards.isEmpty(); - } catch (Exception e) { - log.error("未能找到岗位元素:{}", e.getMessage()); - return false; - } - } - - - private static String getSearchUrl(String cityCode,String keyword) { - // 经验 - List experience = config.getExperience(); - // 学历 - List degree = config.getDegree(); - // 薪资 - String salary = config.getSalary(); - // 规模 - List scale = config.getScale(); - - String searchUrl = baseUrl; - - log.info("cityCode:{}", cityCode); - log.info("experience:{}", experience); - log.info("degree:{}", degree); - log.info("salary:{}", salary); - if (!MobileBossEnum.CityCode.NULL.equals(cityCode)) { - searchUrl = searchUrl + "/" + cityCode + "/"; - } - - Set ydeSet = new LinkedHashSet<>(); - if(!experience.isEmpty()){ - if (!MobileBossEnum.Salary.NULL.equals(salary)) { - ydeSet.add(salary); - } - } - - if(!degree.isEmpty()){ - String degreeStr = degree.stream().findFirst().get(); - if (!MobileBossEnum.Degree.NULL.equals(degreeStr)) { - ydeSet.add(degreeStr); - } - } - if(!experience.isEmpty()){ - String experienceStr = experience.stream().findFirst().get(); - if (!MobileBossEnum.Experience.NULL.equals(experienceStr)) { - ydeSet.add(experienceStr); - } - } - - if(!scale.isEmpty()){ - String scaleStr = scale.stream().findFirst().get(); - if (!MobileBossEnum.Scale.NULL.equals(scaleStr)) { - ydeSet.add(scaleStr); - } - } - - - String yde = ydeSet.stream().collect(Collectors.joining("-")); - log.info("yde:{}", yde); - if (StringUtils.hasLength(yde)) { - if (!searchUrl.endsWith("/")) { - searchUrl = searchUrl + "/" + yde + "/"; - } else { - searchUrl = searchUrl + yde + "/"; - } - } - - searchUrl = searchUrl + "?query=" + keyword; - searchUrl = searchUrl + "&ka=sel-salary-" + salary.split("_")[1]; - return searchUrl; - } - - private static void saveData(String path) { - try { - updateListData(); - Map> data = new HashMap<>(); - data.put("blackCompanies", blackCompanies); - data.put("blackRecruiters", blackRecruiters); - data.put("blackJobs", blackJobs); - String json = customJsonFormat(data); - Files.write(Paths.get(path), json.getBytes()); - } catch (IOException e) { - log.error("保存【{}】数据失败!", path); - } - } - - private static void updateListData() { - CHROME_DRIVER.get("https://www.zhipin.com/web/geek/chat"); - SeleniumUtil.getWait(3); - - JavascriptExecutor js = CHROME_DRIVER; - boolean shouldBreak = false; - while (!shouldBreak) { - try { - WebElement bottom = CHROME_DRIVER.findElement(By.xpath("//div[@class='finished']")); - if ("没有更多了".equals(bottom.getText())) { - shouldBreak = true; - } - } catch (Exception ignore) { - } - List items = CHROME_DRIVER.findElements(By.xpath("//li[@role='listitem']")); - for (int i = 0; i < items.size(); i++) { - try { - WebElement companyElement = CHROME_DRIVER.findElements(By.xpath("//div[@class='title-box']/span[@class='name-box']//span[2]")).get(i); - WebElement messageElement = CHROME_DRIVER.findElements(By.xpath("//div[@class='gray last-msg']/span[@class='last-msg-text']")).get(i); - - String companyName = null; - String message = null; - int retryCount = 0; - - while (retryCount < 2) { - try { - companyName = companyElement.getText(); - message = messageElement.getText(); - break; // 成功获取文本,跳出循环 - } catch (org.openqa.selenium.StaleElementReferenceException e) { - retryCount++; - if (retryCount >= 2) { - log.info("尝试获取元素文本2次失败,放弃本次获取"); - break; - } - log.info("页面元素已变更,正在重试第{}次获取元素文本...", retryCount); - // 重新获取元素 - try { - companyElement = CHROME_DRIVER.findElements(By.xpath("//div[@class='title-box']/span[@class='name-box']//span[2]")).get(i); - messageElement = CHROME_DRIVER.findElements(By.xpath("//div[@class='gray last-msg']/span[@class='last-msg-text']")).get(i); - // 等待短暂时间后重试 - SeleniumUtil.sleep(1); - } catch (Exception ex) { - log.info("重新获取元素失败,放弃本次获取"); - break; - } - } - } - - // 只有在成功获取文本的情况下才继续处理 - if (companyName != null && message != null) { - boolean match = message.contains("不") || message.contains("感谢") || message.contains("但") || message.contains("遗憾") || message.contains("需要本") || message.contains("对不"); - boolean nomatch = message.contains("不是") || message.contains("不生"); - if (match && !nomatch) { - log.info("黑名单公司:【{}】,信息:【{}】", companyName, message); - if (blackCompanies.stream().anyMatch(companyName::contains)) { - continue; - } - companyName = companyName.replaceAll("\\.{3}", ""); - if (companyName.matches(".*(\\p{IsHan}{2,}|[a-zA-Z]{4,}).*")) { - blackCompanies.add(companyName); - } - } - } - } catch (Exception e) { - log.error("寻找黑名单公司异常...",e); - } - } - WebElement element; - try { - WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[contains(text(), '滚动加载更多')]"))); - element = CHROME_DRIVER.findElement(By.xpath("//div[contains(text(), '滚动加载更多')]")); - } catch (Exception e) { - log.info("没找到滚动条..."); - break; - } - - if (element != null) { - try { - js.executeScript("arguments[0].scrollIntoView();", element); - } catch (Exception e) { - log.error("滚动到元素出错", e); - } - } else { - try { - js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); - } catch (Exception e) { - log.error("滚动到页面底部出错", e); - } - } - } - log.info("黑名单公司数量:{}", blackCompanies.size()); - } - - - private static String customJsonFormat(Map> data) { - StringBuilder sb = new StringBuilder(); - sb.append("{\n"); - for (Map.Entry> entry : data.entrySet()) { - sb.append(" \"").append(entry.getKey()).append("\": [\n"); - sb.append(entry.getValue().stream().map(s -> " \"" + s + "\"").collect(Collectors.joining(",\n"))); - - sb.append("\n ],\n"); - } - sb.delete(sb.length() - 2, sb.length()); - sb.append("\n}"); - return sb.toString(); - } - - private static void loadData(String path) { - try { - String json = new String(Files.readAllBytes(Paths.get(path))); - parseJson(json); - } catch (IOException e) { - log.error("读取【{}】数据失败!", path); - } - } - - private static void parseJson(String json) { - JSONObject jsonObject = new JSONObject(json); - blackCompanies = jsonObject.getJSONArray("blackCompanies").toList().stream().map(Object::toString).collect(Collectors.toSet()); - blackRecruiters = jsonObject.getJSONArray("blackRecruiters").toList().stream().map(Object::toString).collect(Collectors.toSet()); - blackJobs = jsonObject.getJSONArray("blackJobs").toList().stream().map(Object::toString).collect(Collectors.toSet()); - } - - - @SneakyThrows - private static Integer resumeSubmission(String keyword) { - List jobCards = MOBILE_CHROME_DRIVER.findElements(By.cssSelector("ul li.item")); - List jobs = new ArrayList<>(); - for (WebElement jobCard : jobCards) { - // 获取完整HTML - String outerHtml = jobCard.getAttribute("outerHTML"); - // 获取招聘者信息 - WebElement recruiterElement = jobCard.findElement(By.cssSelector("div.recruiter div.name")); - String recruiterText = recruiterElement.getText(); - - String salary = jobCard.findElement(By.cssSelector("div.title span.salary")).getText(); - - if (blackRecruiters.stream().anyMatch(recruiterText::contains)) { - // 排除黑名单招聘人员 - continue; - } - String jobName = jobCard.findElement(By.cssSelector("div.title span.title-text")).getText(); - if (blackJobs.stream().anyMatch(jobName::contains) || !isTargetJob(keyword, jobName)) { - // 排除黑名单岗位 - continue; - } - String companyName = jobCard.findElement(By.cssSelector("div.name span.company")).getText(); - if (blackCompanies.stream().anyMatch(companyName::contains)) { - // 排除黑名单公司 - continue; - } - if (isSalaryNotExpected(salary)) { - // 过滤薪资 - log.info("已过滤:【{}】公司【{}】岗位薪资【{}】不符合投递要求", companyName, jobName, salary); - continue; - } - - if(config.getKeyFilter()){ - if(!jobName.toLowerCase().contains(keyword.toLowerCase())){ - log.info("已过滤:岗位【{}】名称不包含关键字【{}】",jobName,keyword); - continue; - } - } - - Job job = new Job(); - // 获取职位链接 - job.setHref(jobCard.findElement(By.cssSelector("a")).getAttribute("href")); - // 获取职位名称 - job.setJobName(jobName); - // 获取工作地点 - job.setJobArea(jobCard.findElement(By.cssSelector("div.name span.workplace")).getText()); - // 获取薪资 - job.setSalary(salary); - // 获取标签 - List tagElements = jobCard.findElements(By.cssSelector("div.labels span")); - StringBuilder tag = new StringBuilder(); - for (WebElement tagElement : tagElements) { - tag.append(tagElement.getText()).append("·"); - } - if (tag.length() > 0) { - job.setCompanyTag(tag.substring(0, tag.length() - 1)); - } else { - job.setCompanyTag(""); - } - // 获取公司名称 - job.setCompanyName(companyName); - // 设置招聘者信息 - job.setRecruiter(recruiterText); - jobs.add(job); - } - - for (Job job : jobs) { - // 打开新的标签页 - JavascriptExecutor jse = CHROME_DRIVER; - // jse.executeScript("window.open(arguments[0], '_blank')", job.getHref()); - // 使用JavaScript控制焦点,避免了每次打开新页签时浏览器窗口自动切换到前台的问题。 - jse.executeScript("var newTab = window.open(arguments[0], '_blank'); newTab.blur(); window.focus();", job.getHref()); - // 切换到新的标签页 - ArrayList tabs = new ArrayList<>(CHROME_DRIVER.getWindowHandles()); - CHROME_DRIVER.switchTo().window(tabs.getLast()); - try { - // 等待聊天按钮出现 - WAIT.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("[class*='btn btn-startchat']"))); - } catch (Exception e) { - Optional element = SeleniumUtil.findElement("//div[@class='error-content']", ""); - if (element.isPresent() && element.get().getText().contains("异常访问")) { - return -2; - } - } - - try{ - WebElement jobSecText = CHROME_DRIVER.findElement(By.xpath("//div[@class='job-sec-text']")); - job.setJobInfo(jobSecText.getText()); - WebElement foldText = CHROME_DRIVER.findElement(By.xpath("//div[@class=' job-sec-text fold-text']")); - job.setCompanyInfo(foldText.getText()); - }catch (Exception e){ - log.info("没有获取到职位/公司描述"); - } - - //过滤不活跃HR - if (isDeadHR()) { - closeWindow(tabs); - log.info("该HR已过滤"); - SeleniumUtil.sleep(1); - continue; - } - simulateWait(); - WebElement btn = null; - try{ - btn = CHROME_DRIVER.findElement(By.cssSelector("[class*='btn btn-startchat']")); - }catch (Exception e){ - log.info("没有获取到立即沟通按钮"); - } - - boolean debug = config.getDebugger(); - - // 休息下,请求太频繁了 - SeleniumUtil.sleep(5); - if (!debug&&Objects.nonNull(btn)&&"立即沟通".equals(btn.getText())) { - String waitTime = config.getWaitTime(); - int sleepTime = 10; // 默认等待10秒 - - if (waitTime != null) { - try { - sleepTime = Integer.parseInt(waitTime); - } catch (NumberFormatException e) { - log.error("等待时间转换异常!!"); - } - } - - SeleniumUtil.sleep(sleepTime); - - AiFilter filterResult = null; - if (config.getEnableAI()) { - // AI检测岗位是否匹配 - String jd = CHROME_DRIVER.findElement(By.xpath("//div[@class='job-sec-text']")).getText(); - filterResult = checkJob(keyword, job.getJobName(), jd); - } - btn.click(); - if (isLimit()) { - SeleniumUtil.sleep(1); - return -1; - } - try { - try { - CHROME_DRIVER.findElement(By.xpath("//div[@class='dialog-title']")); - WebElement close = CHROME_DRIVER.findElement(By.xpath("//i[@class='icon-close']")); - close.click(); - btn.click(); - } catch (Exception ignore) { - } - WebElement input = WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@id='chat-input']"))); - input.click(); - WebElement element = CHROME_DRIVER.findElement(By.xpath("//div[@class='dialog-container']")); - if ("不匹配".equals(element.getText())) { - CHROME_DRIVER.close(); - CHROME_DRIVER.switchTo().window(tabs.getFirst()); - continue; - } - input.sendKeys(filterResult != null && filterResult.getResult() && isValidString(filterResult.getMessage()) ? filterResult.getMessage() : config.getSayHi()); - WebElement send = WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//button[@type='send']"))); - send.click(); - SeleniumUtil.sleep(5); - // 没必要通过聊天窗口再获取了 以名称为例 可能出现 //div[@class='base-info']//span[@class='name-text'] 的情况,boss界面不是固定的结构 -// WebElement recruiterNameElement = CHROME_DRIVER.findElement(By.xpath("//div[@class='base-info']//span[@class='name-text']")); -// WebElement recruiterTitleElement = CHROME_DRIVER.findElement(By.xpath("//div[@class='base-info']/span[@class='base-title']")); -// String recruiter = recruiterNameElement.getText() + " " + recruiterTitleElement.getText(); - String recruiter = job.getRecruiter(); - - String company = job.getCompanyName(); - -// WebElement positionNameElement = CHROME_DRIVER.findElement(By.xpath("//div[@class='position-main']//span[@class='position-name']")); -// WebElement salaryElement = CHROME_DRIVER.findElement(By.xpath("//div[@class='position-main']//span[@class='salary']")); -// WebElement cityElement = CHROME_DRIVER.findElement(By.xpath("//div[@class='position-main']//span[@class='city']")); -// - String position = job.getJobName() + " " + job.getSalary() + " " + job.getJobArea(); - Boolean imgResume = sendResume(company); - SeleniumUtil.sleep(2); - log.info("正在投递【{}】公司,【{}】职位,招聘官:【{}】{}", company, position, recruiter, imgResume ? "发送图片简历成功!" : ""); - resultList.add(job); - } catch (Exception e) { - log.error("发送消息失败:{}", e.getMessage(), e); - } - } - if(!debug){ - closeWindow(tabs); - } - if(debug){ - break; - } - } - return resultList.size(); - } - - - public static boolean isValidString(String str) { - return str != null && !str.isEmpty(); - } - - public static Boolean sendResume(String company) { - // 如果 config.getSendImgResume() 为 true,再去找图片 - if (!config.getSendImgResume()) { - return false; - } - - try { - // 从类路径加载 resume.jpg - URL resourceUrl = Boss.class.getResource("/resume.jpg"); - if (resourceUrl == null) { - log.error("在类路径下未找到 resume.jpg 文件!"); - return false; - } - - // 将 URL 转为 File 对象 - File imageFile = new File(resourceUrl.toURI()); - log.info("简历图片路径:{}", imageFile.getAbsolutePath()); - - if (!imageFile.exists()) { - log.error("简历图片不存在!: {}", imageFile.getAbsolutePath()); - return false; - } - - // 使用 XPath 定位 元素 - WebElement fileInput = CHROME_DRIVER.findElement(By.xpath("//div[@aria-label='发送图片']//input[@type='file']")); - - // 上传图片 - fileInput.sendKeys(imageFile.getAbsolutePath()); - return true; - } catch (Exception e) { - log.error("发送简历图片时出错:{}", e.getMessage()); - return false; - } - } - - /** - * 检查岗位薪资是否符合预期 - * - * @return boolean - * true 不符合预期 - * false 符合预期 - * 期望的最低薪资如果比岗位最高薪资还小,则不符合(薪资给的太少) - * 期望的最高薪资如果比岗位最低薪资还小,则不符合(要求太高满足不了) - */ - private static boolean isSalaryNotExpected(String salary) { - try { - // 1. 如果没有期望薪资范围,直接返回 false,表示"薪资并非不符合预期" - List expectedSalary = config.getExpectedSalary(); - if (!hasExpectedSalary(expectedSalary)) { - return false; - } - - // 2. 清理薪资文本(比如去掉 "·15薪") - salary = removeYearBonusText(salary); - - // 3. 如果薪资格式不符合预期(如缺少 "K" / "k"),直接返回 true,表示"薪资不符合预期" - if (!isSalaryInExpectedFormat(salary)) { - return true; - } - - // 4. 进一步清理薪资文本,比如去除 "K"、"k"、"·" 等 - salary = cleanSalaryText(salary); - - // 5. 判断是 "月薪" 还是 "日薪" - String jobType = detectJobType(salary); - salary = removeDayUnitIfNeeded(salary); // 如果是按天,则去除 "元/天" - - // 6. 解析薪资范围并检查是否超出预期 - Integer[] jobSalaryRange = parseSalaryRange(salary); - return isSalaryOutOfRange(jobSalaryRange, - getMinimumSalary(expectedSalary), - getMaximumSalary(expectedSalary), - jobType); - - } catch (Exception e) { - log.error("岗位薪资获取异常!{}", e.getMessage(), e); - // 出错时,您可根据业务需求决定返回 true 或 false - // 这里假设出错时无法判断,视为不满足预期 => 返回 true - return true; - } - } - - /** - * 是否存在有效的期望薪资范围 - */ - private static boolean hasExpectedSalary(List expectedSalary) { - return expectedSalary != null && !expectedSalary.isEmpty(); - } - - /** - * 去掉年终奖信息,如 "·15薪"、"·13薪"。 - */ - private static String removeYearBonusText(String salary) { - if (salary.contains("薪")) { - // 使用正则去除 "·任意数字薪" - return salary.replaceAll("·\\d+薪", ""); - } - return salary; - } - - /** - * 判断是否是按天计薪,如发现 "元/天" 则认为是日薪 - */ - private static String detectJobType(String salary) { - if (salary.contains("元/天")) { - return "day"; - } - return "mouth"; - } - - /** - * 如果是日薪,则去除 "元/天" - */ - private static String removeDayUnitIfNeeded(String salary) { - if (salary.contains("元/天")) { - return salary.replaceAll("元/天", ""); - } - return salary; - } - - private static Integer getMinimumSalary(List expectedSalary) { - return expectedSalary != null && !expectedSalary.isEmpty() ? expectedSalary.get(0) : null; - } - - private static Integer getMaximumSalary(List expectedSalary) { - return expectedSalary != null && expectedSalary.size() > 1 ? expectedSalary.get(1) : null; - } - - private static boolean isSalaryInExpectedFormat(String salaryText) { - return salaryText.contains("K") || salaryText.contains("k"); - } - - private static String cleanSalaryText(String salaryText) { - salaryText = salaryText.replace("K", "").replace("k", ""); - int dotIndex = salaryText.indexOf('·'); - if (dotIndex != -1) { - salaryText = salaryText.substring(0, dotIndex); - } - return salaryText; - } - - private static boolean isSalaryOutOfRange(Integer[] jobSalary, Integer miniSalary, Integer maxSalary, String jobType) { - if (jobSalary == null) { - return true; - } - if (miniSalary == null) { - return false; - } - if (Objects.equals("day", jobType)) { - // 期望薪资转为平均每日的工资 - maxSalary = BigDecimal.valueOf(maxSalary).multiply(BigDecimal.valueOf(1000)).divide(BigDecimal.valueOf(21.75), 0, RoundingMode.HALF_UP).intValue(); - miniSalary = BigDecimal.valueOf(miniSalary).multiply(BigDecimal.valueOf(1000)).divide(BigDecimal.valueOf(21.75), 0, RoundingMode.HALF_UP).intValue(); - } - // 如果职位薪资下限低于期望的最低薪资,返回不符合 - if (jobSalary[1] < miniSalary) { - return true; - } - // 如果职位薪资上限高于期望的最高薪资,返回不符合 - return maxSalary != null && jobSalary[0] > maxSalary; - } - - private static void RandomWait() { - SeleniumUtil.sleep(JobUtils.getRandomNumberInRange(3, 20)); - } - - private static void simulateWait() { - for (int i = 0; i < 3; i++) { - ACTIONS.sendKeys(" ").perform(); - MOBILE_ACTIONS.sendKeys(" ").perform(); - SeleniumUtil.sleep(1); - } - ACTIONS.keyDown(Keys.CONTROL) - .sendKeys(Keys.HOME) - .keyUp(Keys.CONTROL) - .perform(); - MOBILE_ACTIONS.keyDown(Keys.CONTROL) - .sendKeys(Keys.HOME) - .keyUp(Keys.CONTROL) - .perform(); - SeleniumUtil.sleep(1); - } - - - private static boolean isDeadHR() { - if (!config.getFilterDeadHR()) { - return false; - } - try { - // 尝试获取 HR 的活跃时间 - String activeTimeText = CHROME_DRIVER.findElement(By.xpath("//span[@class='boss-active-time']")).getText(); - log.info("{}:{}", getCompanyAndHR(), activeTimeText); - // 如果 HR 活跃状态符合预期,则返回 true - return containsDeadStatus(activeTimeText, config.getDeadStatus()); - } catch (Exception e) { - log.info("没有找到【{}】的活跃状态, 默认此岗位将会投递...", getCompanyAndHR()); - return false; - } - } - - public static boolean containsDeadStatus(String activeTimeText, List deadStatus) { - for (String status : deadStatus) { - if (activeTimeText.contains(status)) { - return true;// 一旦找到包含的值,立即返回 true - } - } - return false;// 如果没有找到,返回 false - } - - private static String getCompanyAndHR() { - try { - return CHROME_DRIVER.findElement(By.xpath("//div[@class='boss-info-attr']")).getText().replaceAll("\n", ""); - } catch (Exception e) { - log.info("未能获取公司和HR信息"); - return ""; - } - } - - private static void closeWindow(ArrayList tabs) { - SeleniumUtil.sleep(1); - CHROME_DRIVER.close(); - CHROME_DRIVER.switchTo().window(tabs.get(0)); - } - - private static AiFilter checkJob(String keyword, String jobName, String jd) { - AiConfig aiConfig = AiConfig.init(); - String requestMessage = String.format(aiConfig.getPrompt(), aiConfig.getIntroduce(), keyword, jobName, jd, config.getSayHi()); - String result = AiService.sendRequest(requestMessage); - return result.contains("false") ? new AiFilter(false) : new AiFilter(true, result); - } - - private static boolean isTargetJob(String keyword, String jobName) { - boolean keywordIsAI = false; - for (String target : new String[]{"大模型", "AI"}) { - if (keyword.contains(target)) { - keywordIsAI = true; - break; - } - } - - boolean jobIsDesign = false; - for (String designOrVision : new String[]{"设计", "视觉", "产品", "运营"}) { - if (jobName.contains(designOrVision)) { - jobIsDesign = true; - break; - } - } - - boolean jobIsAI = false; - for (String target : new String[]{"AI", "人工智能", "大模型", "生成"}) { - if (jobName.contains(target)) { - jobIsAI = true; - break; - } - } - - if (keywordIsAI) { - if (jobIsDesign) { - return false; - } else if (!jobIsAI) { - return true; - } - } - return true; - } - - private static Integer[] parseSalaryRange(String salaryText) { - try { - return Arrays.stream(salaryText.split("-")).map(s -> s.replaceAll("[^0-9]", "")) // 去除非数字字符 - .map(Integer::parseInt) // 转换为Integer - .toArray(Integer[]::new); // 转换为Integer数组 - } catch (Exception e) { - log.error("薪资解析异常!{}", e.getMessage(), e); - } - return null; - } - - private static boolean isLimit() { - try { - SeleniumUtil.sleep(1); - String text = CHROME_DRIVER.findElement(By.className("dialog-con")).getText(); - return text.contains("已达上限"); - } catch (Exception e) { - return false; - } - } - - @SneakyThrows - private static void login() { - log.info("打开Boss直聘网站中..."); - CHROME_DRIVER.get(homeUrl); - // 避免首次加载请求过于频繁 - SeleniumUtil.sleep(10); - MOBILE_CHROME_DRIVER.get(homeUrl); - if (SeleniumUtil.isCookieValid(cookiePath)) { - SeleniumUtil.loadCookie(cookiePath); - CHROME_DRIVER.navigate().refresh(); - MOBILE_CHROME_DRIVER.navigate().refresh(); - SeleniumUtil.sleep(2); - } - if (isLoginRequired()) { - log.error("cookie失效,尝试扫码登录..."); - scanLogin(); - SeleniumUtil.loadCookie(cookiePath); - MOBILE_CHROME_DRIVER.navigate().refresh(); - SeleniumUtil.sleep(2); - } - } - - - private static boolean isLoginRequired() { - try { - String text = CHROME_DRIVER.findElement(By.className("btns")).getText(); - return text != null && text.contains("登录"); - } catch (Exception e) { - try { - CHROME_DRIVER.findElement(By.xpath("//h1")).getText(); - CHROME_DRIVER.findElement(By.xpath("//a[@ka='403_login']")).click(); - return true; - } catch (Exception ex) { - log.info("没有出现403访问异常"); - } - log.info("cookie有效,已登录..."); - return false; - } - } - - @SneakyThrows - private static void scanLogin() { - log.info("访问登录页面:{}", homeUrl + "/web/user/?ka=header-login"); - // 访问登录页面 - CHROME_DRIVER.get(homeUrl + "/web/user/?ka=header-login"); - SeleniumUtil.sleep(3); - - // 1. 如果已经登录,则直接返回 - try { - String text = CHROME_DRIVER.findElement(By.xpath("//li[@class='nav-figure']")).getText(); - if (!Objects.equals(text, "登录")) { - log.info("已经登录,直接开始投递..."); - return; - } - } catch (Exception ignored) { - } - - log.info("等待登录..."); - - // 2. 定位二维码登录的切换按钮 - WebElement app = WAIT.until(ExpectedConditions.presenceOfElementLocated( - By.xpath("//div[@class='btn-sign-switch ewm-switch']"))); - - // 3. 登录逻辑 - boolean login = false; - - // 4. 记录开始时间,用于判断10分钟超时 - long startTime = System.currentTimeMillis(); - final long TIMEOUT = 10 * 60 * 1000; // 10分钟 - - // 5. 用于监听用户是否在控制台回车 - Scanner scanner = new Scanner(System.in); - - while (!login) { - // 如果已经超过10分钟,退出程序 - long elapsed = System.currentTimeMillis() - startTime; - if (elapsed >= TIMEOUT) { - log.error("超过10分钟未完成登录,程序退出..."); - System.exit(1); - } - - try { - // 尝试点击二维码按钮并等待页面出现已登录的元素 - app.click(); - WAIT.until(ExpectedConditions.presenceOfElementLocated( - By.xpath("//*[@id=\"header\"]/div[1]/div[1]/a"))); - WAIT.until(ExpectedConditions.presenceOfElementLocated( - By.xpath("//*[@id=\"wrap\"]/div[2]/div[1]/div/div[1]/a[2]"))); - - // 如果上述元素都能找到,说明登录成功 - login = true; - log.info("登录成功!保存cookie..."); - } catch (Exception e) { - // 登录失败 - log.error("登录失败,等待用户操作或者 2 秒后重试..."); - - // 每次登录失败后,等待2秒,同时检查用户是否按了回车 - boolean userInput = waitForUserInputOrTimeout(scanner); - if (userInput) { - log.info("检测到用户输入,继续尝试登录..."); - } - } - } - - // 登录成功后,保存Cookie - SeleniumUtil.saveCookie(cookiePath); - } - - /** - * 在指定的毫秒数内等待用户输入回车;若在等待时间内用户按回车则返回 true,否则返回 false。 - * - * @param scanner 用于读取控制台输入 - * @return 用户是否在指定时间内按回车 - */ - private static boolean waitForUserInputOrTimeout(Scanner scanner) { - long end = System.currentTimeMillis() + 2000; - while (System.currentTimeMillis() < end) { - try { - // 判断输入流中是否有可用字节 - if (System.in.available() > 0) { - // 读取一行(用户输入) - scanner.nextLine(); - return true; - } - } catch (IOException e) { - // 读取输入流异常,直接忽略 - } - - // 小睡一下,避免 CPU 空转 - SeleniumUtil.sleep(1); - } - return false; - } - - -} diff --git a/src/main/java/boss/MobileBossConfig.java b/src/main/java/boss/MobileBossConfig.java deleted file mode 100644 index ab637239..00000000 --- a/src/main/java/boss/MobileBossConfig.java +++ /dev/null @@ -1,143 +0,0 @@ -package boss; - -import lombok.Data; -import lombok.SneakyThrows; -import utils.JobUtils; - -import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.stream.Collectors; - -/** - * @author loks666 - * 项目链接: https://github.com/loks666/get_jobs - */ -@Data -public class MobileBossConfig { - /** - * 用于打招呼的语句 - */ - private String sayHi; - - /** - * 开发者模式 - */ - private Boolean debugger; - - /** - * 搜索关键词列表 - */ - private List keywords; - - /** - * 城市编码 - */ - private List cityCode; - - /** - * 自定义城市编码映射 - */ - private Map customCityCode; - - /** - * 行业列表 - */ - private List industry; - - /** - * 工作经验要求 - */ - private List experience; - - /** - * 工作类型 - */ - private String jobType; - - /** - * 薪资范围 - */ - private String salary; - - /** - * 学历要求列表 - */ - private List degree; - - /** - * 公司规模列表 - */ - private List scale; - - /** - * 公司融资阶段列表 - */ - private List stage; - - /** - * 是否开放AI检测 - */ - private Boolean enableAI; - - /** - * 是否过滤不活跃hr - */ - private Boolean filterDeadHR; - - /** - * 是否发送图片简历 - */ - private Boolean sendImgResume; - - /** - * 目标薪资 - */ - private List expectedSalary; - - /** - * 等待时间 - */ - private String waitTime; - - private List deadStatus; - - private Integer nextIntervalMinutes; - - /** - * 是否使用关键词匹配岗位m名称,岗位名称不包含关键字就过滤 - * - */ - private Boolean keyFilter; - - @SneakyThrows - public static MobileBossConfig init() { - MobileBossConfig config = JobUtils.getConfig(MobileBossConfig.class); - - // 转换薪资范围 - config.setSalary(MobileBossEnum.Salary.forValue(config.getSalary()).getCode()); - - // 处理城市编码 - List convertedCityCodes = config.getCityCode().stream() - .map(city -> { - // 优先从自定义映射中获取 - if (config.getCustomCityCode() != null && config.getCustomCityCode().containsKey(city)) { - return config.getCustomCityCode().get(city); - } - // 否则从枚举中获取 - return MobileBossEnum.CityCode.forValue(city).getCode(); - }) - .collect(Collectors.toList()); - config.setCityCode(convertedCityCodes); - - // 转换工作经验要求 - config.setExperience(config.getExperience().stream().map(value -> MobileBossEnum.Experience.forValue(value).getCode()).collect(Collectors.toList())); - // 转换学历要求 - config.setDegree(config.getDegree().stream().map(value -> MobileBossEnum.Degree.forValue(value).getCode()).collect(Collectors.toList())); - // 转换公司规模 - config.setScale(config.getScale().stream().map(value -> MobileBossEnum.Scale.forValue(value).getCode()).collect(Collectors.toList())); - - return config; - } - -} diff --git a/src/main/java/boss/MobileBossEnum.java b/src/main/java/boss/MobileBossEnum.java deleted file mode 100644 index 6fb1c6f2..00000000 --- a/src/main/java/boss/MobileBossEnum.java +++ /dev/null @@ -1,252 +0,0 @@ -package boss; - -import com.fasterxml.jackson.annotation.JsonCreator; -import lombok.Getter; - -import java.util.Arrays; -import java.util.Optional; - -/** - * @author loks666 - * 项目链接: https://github.com/loks666/get_jobs - */ -public class MobileBossEnum { - @Getter - public enum Experience { - NULL("不限", "0"), - STUDENT("在校生", "e_108"), - GRADUATE("应届毕业生", "e_102"), - LESS_THAN_ONE_YEAR("1年以下", "e_103"), - ONE_TO_THREE_YEARS("1-3年", "e_104"), - THREE_TO_FIVE_YEARS("3-5年", "e_105"), - FIVE_TO_TEN_YEARS("5-10年", "e_106"), - MORE_THAN_TEN_YEARS("10年以上", "e_107"); - - private final String name; - private final String code; - - Experience(String name, String code) { - this.name = name; - this.code = code; - } - - public static Optional getCode(String name) { - return Arrays.stream(Experience.values()).filter(experience -> experience.name.equals(name)).findFirst().map(experience -> experience.code); - } - - @JsonCreator - public static Experience forValue(String value) { - for (Experience experience : Experience.values()) { - if (experience.name.equals(value)) { - return experience; - } - } - return NULL; - } - } - - @Getter - public enum CityCode { - NULL("不限", "c0"), - ALL("全国", "c100010000"), - BEIJING("北京", "c101010100"), - SHANGHAI("上海", "c101020100"), - TIANJIN("天津", "c101030100"), - HANGZHOU("杭州", "c101210100"), - GUANGZHOU("广州", "c101280100"), - SHENZHEN("深圳", "c101280600"), - WUHAN("武汉", "c101200100"), - CHENGDU("成都", "c101270100"); - - private final String name; - private final String code; - - CityCode(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static CityCode forValue(String value) { - for (CityCode cityCode : CityCode.values()) { - if (cityCode.name.equals(value)) { - return cityCode; - } - } - return NULL; - } - - } - - @Getter - public enum JobType { - NULL("不限", "0"), - FULL_TIME("全职", "1901"), - PART_TIME("兼职", "1903"); - - private final String name; - private final String code; - - JobType(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static JobType forValue(String value) { - for (JobType jobType : JobType.values()) { - if (jobType.name.equals(value)) { - return jobType; - } - } - return NULL; - } - } - - @Getter - public enum Salary { - NULL("不限", "y_0"), - BELOW_3K("3K以下", "y_1"), - FROM_3K_TO_5K("3-5K", "y_2"), - FROM_5K_TO_10K("5-10K", "y_3"), - FROM_10K_TO_20K("10-20K", "y_4"), - FROM_20K_TO_50K("20-50K", "y_6"), - ABOVE_50K("50K以上", "y_8"); - - private final String name; - private final String code; - - Salary(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static Salary forValue(String value) { - for (Salary salary : Salary.values()) { - if (salary.name.equals(value)) { - return salary; - } - } - return NULL; - } - } - - @Getter - public enum Degree { - NULL("不限", "0"), - BELOW_JUNIOR_HIGH_SCHOOL("初中及以下", "d_209"), - SECONDARY_VOCATIONAL("中专/中技", "d_208"), - HIGH_SCHOOL("高中", "d_206"), - JUNIOR_COLLEGE("大专", "d_202"), - BACHELOR("本科", "d_203"), - MASTER("硕士", "d_204"), - DOCTOR("博士", "d_205"); - - private final String name; - private final String code; - - Degree(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static Degree forValue(String value) { - for (Degree degree : Degree.values()) { - if (degree.name.equals(value)) { - return degree; - } - } - return NULL; - } - } - - @Getter - public enum Scale { - NULL("不限", "0"), - ZERO_TO_TWENTY("0-20人", "s_301"), - TWENTY_TO_NINETY_NINE("20-99人", "s_302"), - ONE_HUNDRED_TO_FOUR_NINETY_NINE("100-499人", "s_303"), - FIVE_HUNDRED_TO_NINE_NINETY_NINE("500-999人", "s_304"), - ONE_THOUSAND_TO_NINE_NINE_NINE_NINE("1000-9999人", "s_305"), - TEN_THOUSAND_ABOVE("10000人以上", "s_306"); - - private final String name; - private final String code; - - Scale(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static Scale forValue(String value) { - for (Scale scale : Scale.values()) { - if (scale.name.equals(value)) { - return scale; - } - } - return NULL; - } - } - - @Getter - public enum Financing { - NULL("不限", "0"), - UNFUNDED("未融资", "801"), - ANGEL_ROUND("天使轮", "802"), - A_ROUND("A轮", "803"), - B_ROUND("B轮", "804"), - C_ROUND("C轮", "805"), - D_AND_ABOVE("D轮及以上", "806"), - LISTED("已上市", "807"), - NO_NEED("不需要融资", "808"); - - private final String name; - private final String code; - - Financing(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static Financing forValue(String value) { - for (Financing financing : Financing.values()) { - if (financing.name.equals(value)) { - return financing; - } - } - return NULL; - } - } - - @Getter - public enum Industry { - NULL("不限", "0"), - INTERNET("互联网", "100020"), - COMPUTER_SOFTWARE("计算机软件", "100021"), - CLOUD_COMPUTING("云计算", "100029"); - - private final String name; - private final String code; - - Industry(String name, String code) { - this.name = name; - this.code = code; - } - - @JsonCreator - public static Industry forValue(String value) { - for (Industry industry : Industry.values()) { - if (industry.name.equals(value)) { - return industry; - } - } - return NULL; - } - } - -} - diff --git a/src/main/java/utils/Bot.java b/src/main/java/utils/Bot.java index a1c232cb..b0bd4bad 100644 --- a/src/main/java/utils/Bot.java +++ b/src/main/java/utils/Bot.java @@ -31,7 +31,7 @@ public class Bot { // 加载环境变量 Dotenv dotenv = Dotenv .configure() - .directory(ProjectRootResolver.rootPath+"/src/main/resources") + .directory("/src/main/resources") .load(); HOOK_URL = dotenv.get("HOOK_URL"); BARK_URL = dotenv.get("BARK_URL"); @@ -39,7 +39,7 @@ public class Bot { // 使用 Jackson 加载 config.yaml 配置 try { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - HashMap config = mapper.readValue(new File(ProjectRootResolver.rootPath+"/src/main/resources/config.yaml"), new TypeReference>() { + HashMap config = mapper.readValue(new File("/src/main/resources/config.yaml"), new TypeReference>() { }); log.info("YAML 配置内容: {}", config); diff --git a/src/main/java/boss/BossElementFinder.java b/src/main/java/utils/Finder.java similarity index 97% rename from src/main/java/boss/BossElementFinder.java rename to src/main/java/utils/Finder.java index 8148a063..7a256233 100644 --- a/src/main/java/boss/BossElementFinder.java +++ b/src/main/java/utils/Finder.java @@ -1,4 +1,4 @@ -package boss; +package utils; import com.microsoft.playwright.ElementHandle; import com.microsoft.playwright.Locator; @@ -11,8 +11,6 @@ import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import utils.Constant; -import utils.SeleniumUtil; import java.time.Duration; import java.util.List; @@ -28,8 +26,8 @@ * - WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("..."))) * - SeleniumUtil.findElement("...", "") */ -public class BossElementFinder { - private static final Logger log = LoggerFactory.getLogger(BossElementFinder.class); +public class Finder { + private static final Logger log = LoggerFactory.getLogger(Finder.class); private static final WebDriver driver = Constant.CHROME_DRIVER; private static final int DEFAULT_TIMEOUT_SECONDS = 10; diff --git a/src/main/java/boss/BossPageOperations.java b/src/main/java/utils/Operate.java similarity index 93% rename from src/main/java/boss/BossPageOperations.java rename to src/main/java/utils/Operate.java index 7123c368..2b3dadf9 100644 --- a/src/main/java/boss/BossPageOperations.java +++ b/src/main/java/utils/Operate.java @@ -1,5 +1,6 @@ -package boss; +package utils; +import boss.Locators; import com.microsoft.playwright.ElementHandle; import com.microsoft.playwright.Page; import org.openqa.selenium.JavascriptExecutor; @@ -7,9 +8,6 @@ import org.openqa.selenium.WebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import utils.JobUtils; -import utils.PlaywrightUtil; -import utils.SeleniumUtil; import java.util.ArrayList; import java.util.List; @@ -30,8 +28,8 @@ * - 滚动页面 * - 发送简历 */ -public class BossPageOperations { - private static final Logger log = LoggerFactory.getLogger(BossPageOperations.class); +public class Operate { + private static final Logger log = LoggerFactory.getLogger(Operate.class); private static final Random random = new Random(); /** @@ -73,7 +71,7 @@ public static int scrollToLoadMoreJobs(Page page, int maxLoadAttempts) { while (unchangedCount < 2 && loadAttempts < maxLoadAttempts) { // 获取所有岗位卡片 - List jobCards = page.querySelectorAll(BossElementLocators.JOB_CARD_BOX); + List jobCards = page.querySelectorAll(Locators.JOB_CARD_BOX); currentJobCount = jobCards.size(); log.info("当前已加载岗位数量: " + currentJobCount); @@ -240,7 +238,7 @@ public static void scrollChatListUntilFinished() { while (!shouldBreak) { try { - Optional finishedElement = BossElementFinder.findElement(BossElementLocators.FINISHED_TEXT); + Optional finishedElement = Finder.findElement(Locators.FINISHED_TEXT); if (finishedElement.isPresent() && "没有更多了".equals(finishedElement.get().getText())) { shouldBreak = true; } @@ -249,7 +247,7 @@ public static void scrollChatListUntilFinished() { } // 尝试查找"滚动加载更多"元素 - Optional loadMoreElement = BossElementFinder.findElement(BossElementLocators.SCROLL_LOAD_MORE); + Optional loadMoreElement = Finder.findElement(Locators.SCROLL_LOAD_MORE); if (loadMoreElement.isPresent()) { try { @@ -271,7 +269,7 @@ public static void scrollChatListUntilFinished() { } // 防止无限循环,给一个额外的检查 - List items = BossElementFinder.findElements(BossElementLocators.CHAT_LIST_ITEM); + List items = Finder.findElements(Locators.CHAT_LIST_ITEM); if (items.isEmpty()) { log.info("没有找到聊天记录项,停止滚动"); break; @@ -313,7 +311,7 @@ public static void scrollChatListUntilFinished() { */ public static boolean sendResumeImage(String imagePath) { try { - Optional fileInput = BossElementFinder.findElement(BossElementLocators.IMAGE_UPLOAD); + Optional fileInput = Finder.findElement(Locators.IMAGE_UPLOAD); if (fileInput.isPresent()) { fileInput.get().sendKeys(imagePath); return true; diff --git a/src/main/java/utils/PlaywrightUtil.java b/src/main/java/utils/PlaywrightUtil.java index 833cf3a5..93b7fa9c 100644 --- a/src/main/java/utils/PlaywrightUtil.java +++ b/src/main/java/utils/PlaywrightUtil.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.Optional; /** * Playwright工具类,提供浏览器自动化相关的功能 @@ -91,29 +92,17 @@ public static void init() { DESKTOP_PAGE = DESKTOP_CONTEXT.newPage(); DESKTOP_PAGE.setDefaultTimeout(DEFAULT_TIMEOUT); - // 创建移动设备页面,暂时用不到mobile端页面,注释不打开 - // MOBILE_PAGE = MOBILE_CONTEXT.newPage(); - // MOBILE_PAGE.setDefaultTimeout(DEFAULT_TIMEOUT); - - // 启用JavaScript捕获控制台日志(用于调试) - DESKTOP_PAGE.onConsoleMessage(message -> { - if (message.type().equals("error")) { - log.error("Browser console error: {}", message.text()); - } - }); - -// MOBILE_PAGE.onConsoleMessage(message -> { +// // 启用JavaScript捕获控制台日志(用于调试) +// DESKTOP_PAGE.onConsoleMessage(message -> { // if (message.type().equals("error")) { -// log.error("Mobile browser console error: {}", message.text()); +// log.error("Browser console error: {}", message.text()); // } // }); - - log.info("Playwright和浏览器实例已初始化完成"); } /** * 设置默认设备类型 - * + * * @param deviceType 设备类型 */ public static void setDefaultDeviceType(DeviceType deviceType) { @@ -123,7 +112,7 @@ public static void setDefaultDeviceType(DeviceType deviceType) { /** * 获取当前页面(基于当前设备类型) - * + * * @param deviceType 设备类型 * @return 对应的Page对象 */ @@ -133,7 +122,7 @@ private static Page getPage(DeviceType deviceType) { /** * 获取当前上下文(基于当前设备类型) - * + * * @param deviceType 设备类型 * @return 对应的BrowserContext对象 */ @@ -163,7 +152,7 @@ public static void close() { /** * 导航到指定URL - * + * * @param url 目标URL * @param deviceType 设备类型 */ @@ -174,7 +163,7 @@ public static void navigate(String url, DeviceType deviceType) { /** * 使用默认设备类型导航到指定URL - * + * * @param url 目标URL */ public static void navigate(String url) { @@ -183,7 +172,7 @@ public static void navigate(String url) { /** * 移动设备导航到指定URL (兼容旧代码) - * + * * @param url 目标URL */ public static void mobileNavigate(String url) { @@ -192,7 +181,7 @@ public static void mobileNavigate(String url) { /** * 等待指定时间(秒) - * + * * @param seconds 等待的秒数 */ public static void sleep(int seconds) { @@ -206,7 +195,7 @@ public static void sleep(int seconds) { /** * 等待指定时间(毫秒) - * + * * @param millis 等待的毫秒数 */ public static void sleepMillis(int millis) { @@ -218,9 +207,18 @@ public static void sleepMillis(int millis) { } } + /** + * 兼容SeleniumUtil的sleepByMilliSeconds方法 + * + * @param milliSeconds 等待的毫秒数 + */ + public static void sleepByMilliSeconds(int milliSeconds) { + sleepMillis(milliSeconds); + } + /** * 查找元素 - * + * * @param selector 元素选择器 * @param deviceType 设备类型 * @return 元素对象,如果未找到则返回null @@ -231,7 +229,7 @@ public static Locator findElement(String selector, DeviceType deviceType) { /** * 使用默认设备类型查找元素 - * + * * @param selector 元素选择器 * @return 元素对象,如果未找到则返回null */ @@ -241,7 +239,7 @@ public static Locator findElement(String selector) { /** * 查找元素并等待直到可见 - * + * * @param selector 元素选择器 * @param timeout 超时时间(毫秒) * @param deviceType 设备类型 @@ -255,7 +253,7 @@ public static Locator waitForElement(String selector, int timeout, DeviceType de /** * 使用默认设备类型查找元素并等待直到可见 - * + * * @param selector 元素选择器 * @param timeout 超时时间(毫秒) * @return 元素对象,如果未找到则返回null @@ -266,7 +264,7 @@ public static Locator waitForElement(String selector, int timeout) { /** * 使用默认超时时间和默认设备类型等待元素 - * + * * @param selector 元素选择器 * @return 元素对象,如果未找到则返回null */ @@ -276,7 +274,7 @@ public static Locator waitForElement(String selector) { /** * 点击元素 - * + * * @param selector 元素选择器 * @param deviceType 设备类型 */ @@ -291,7 +289,7 @@ public static void click(String selector, DeviceType deviceType) { /** * 使用默认设备类型点击元素 - * + * * @param selector 元素选择器 */ public static void click(String selector) { @@ -300,7 +298,7 @@ public static void click(String selector) { /** * 填写表单字段 - * + * * @param selector 元素选择器 * @param text 要输入的文本 * @param deviceType 设备类型 @@ -316,7 +314,7 @@ public static void fill(String selector, String text, DeviceType deviceType) { /** * 使用默认设备类型填写表单字段 - * + * * @param selector 元素选择器 * @param text 要输入的文本 */ @@ -326,7 +324,7 @@ public static void fill(String selector, String text) { /** * 模拟人类输入文本(逐字输入) - * + * * @param selector 元素选择器 * @param text 要输入的文本 * @param minDelay 字符间最小延迟(毫秒) @@ -355,7 +353,7 @@ public static void typeHumanLike(String selector, String text, int minDelay, int /** * 使用默认设备类型模拟人类输入文本 - * + * * @param selector 元素选择器 * @param text 要输入的文本 * @param minDelay 字符间最小延迟(毫秒) @@ -367,7 +365,7 @@ public static void typeHumanLike(String selector, String text, int minDelay, int /** * 获取元素文本 - * + * * @param selector 元素选择器 * @param deviceType 设备类型 * @return 元素文本内容 @@ -383,7 +381,7 @@ public static String getText(String selector, DeviceType deviceType) { /** * 使用默认设备类型获取元素文本 - * + * * @param selector 元素选择器 * @return 元素文本内容 */ @@ -393,7 +391,7 @@ public static String getText(String selector) { /** * 获取元素属性值 - * + * * @param selector 元素选择器 * @param attributeName 属性名 * @param deviceType 设备类型 @@ -410,7 +408,7 @@ public static String getAttribute(String selector, String attributeName, DeviceT /** * 使用默认设备类型获取元素属性值 - * + * * @param selector 元素选择器 * @param attributeName 属性名 * @return 属性值 @@ -421,7 +419,7 @@ public static String getAttribute(String selector, String attributeName) { /** * 截取页面截图并保存 - * + * * @param path 保存路径 * @param deviceType 设备类型 */ @@ -436,7 +434,7 @@ public static void screenshot(String path, DeviceType deviceType) { /** * 使用默认设备类型截取页面截图并保存 - * + * * @param path 保存路径 */ public static void screenshot(String path) { @@ -445,7 +443,7 @@ public static void screenshot(String path) { /** * 截取特定元素的截图 - * + * * @param selector 元素选择器 * @param path 保存路径 * @param deviceType 设备类型 @@ -461,7 +459,7 @@ public static void screenshotElement(String selector, String path, DeviceType de /** * 使用默认设备类型截取特定元素的截图 - * + * * @param selector 元素选择器 * @param path 保存路径 */ @@ -471,7 +469,7 @@ public static void screenshotElement(String selector, String path) { /** * 保存Cookie到文件 - * + * * @param path 保存路径 * @param deviceType 设备类型 */ @@ -505,7 +503,7 @@ public static void saveCookies(String path, DeviceType deviceType) { /** * 使用默认设备类型保存Cookie到文件 - * + * * @param path 保存路径 */ public static void saveCookies(String path) { @@ -514,7 +512,7 @@ public static void saveCookies(String path) { /** * 从文件加载Cookie - * + * * @param path Cookie文件路径 * @param deviceType 设备类型 */ @@ -563,7 +561,7 @@ public static void loadCookies(String path, DeviceType deviceType) { /** * 使用默认设备类型从文件加载Cookie - * + * * @param path Cookie文件路径 */ public static void loadCookies(String path) { @@ -572,63 +570,30 @@ public static void loadCookies(String path) { /** * 执行JavaScript代码 - * + * * @param script JavaScript代码 * @param deviceType 设备类型 - * @return 执行结果 */ - public static Object evaluate(String script, DeviceType deviceType) { + public static void evaluate(String script, DeviceType deviceType) { try { - return getPage(deviceType).evaluate(script); + getPage(deviceType).evaluate(script); } catch (PlaywrightException e) { log.error("执行JavaScript失败 (设备类型: {})", deviceType, e); - return null; } } /** * 使用默认设备类型执行JavaScript代码 - * + * * @param script JavaScript代码 - * @return 执行结果 - */ - public static Object evaluate(String script) { - return evaluate(script, defaultDeviceType); - } - - /** - * 模拟随机用户行为 - * - * @param deviceType 设备类型 - */ - public static void simulateRandomUserBehavior(DeviceType deviceType) { - Random random = new Random(); - - // 随机滚动 - int scrollY = random.nextInt(1001) - 500; // -500到500之间的随机值 - getPage(deviceType).evaluate("window.scrollBy(0," + scrollY + ")"); - - sleepMillis(random.nextInt(500) + 200); // 200-700ms随机延迟 - - // 有5%的概率进行截图 - if (random.nextInt(100) < 5) { - screenshot(ProjectRootResolver.rootPath + "/screenshots/random_" + System.currentTimeMillis() + ".png", - deviceType); - } - - log.debug("已模拟随机用户行为 (设备类型: {})", deviceType); - } - - /** - * 使用默认设备类型模拟随机用户行为 */ - public static void simulateRandomUserBehavior() { - simulateRandomUserBehavior(defaultDeviceType); + public static void evaluate(String script) { + evaluate(script, defaultDeviceType); } /** * 等待页面加载完成 - * + * * @param deviceType 设备类型 */ public static void waitForPageLoad(DeviceType deviceType) { @@ -636,37 +601,9 @@ public static void waitForPageLoad(DeviceType deviceType) { getPage(deviceType).waitForLoadState(LoadState.NETWORKIDLE); } - /** - * 使用默认设备类型等待页面加载完成 - */ - public static void waitForPageLoad() { - waitForPageLoad(defaultDeviceType); - } - - /** - * 检查元素是否存在 - * - * @param selector 元素选择器 - * @param deviceType 设备类型 - * @return 是否存在 - */ - public static boolean elementExists(String selector, DeviceType deviceType) { - return getPage(deviceType).locator(selector).count() > 0; - } - - /** - * 使用默认设备类型检查元素是否存在 - * - * @param selector 元素选择器 - * @return 是否存在 - */ - public static boolean elementExists(String selector) { - return elementExists(selector, defaultDeviceType); - } - /** * 检查元素是否可见 - * + * * @param selector 元素选择器 * @param deviceType 设备类型 * @return 是否可见 @@ -681,7 +618,7 @@ public static boolean elementIsVisible(String selector, DeviceType deviceType) { /** * 使用默认设备类型检查元素是否可见 - * + * * @param selector 元素选择器 * @return 是否可见 */ @@ -691,7 +628,7 @@ public static boolean elementIsVisible(String selector) { /** * 选择下拉列表选项(通过文本) - * + * * @param selector 选择器 * @param optionText 选项文本 * @param deviceType 设备类型 @@ -702,7 +639,7 @@ public static void selectByText(String selector, String optionText, DeviceType d /** * 使用默认设备类型选择下拉列表选项(通过文本) - * + * * @param selector 选择器 * @param optionText 选项文本 */ @@ -712,7 +649,7 @@ public static void selectByText(String selector, String optionText) { /** * 选择下拉列表选项(通过值) - * + * * @param selector 选择器 * @param value 选项值 * @param deviceType 设备类型 @@ -723,7 +660,7 @@ public static void selectByValue(String selector, String value, DeviceType devic /** * 使用默认设备类型选择下拉列表选项(通过值) - * + * * @param selector 选择器 * @param value 选项值 */ @@ -733,7 +670,7 @@ public static void selectByValue(String selector, String value) { /** * 获取当前页面标题 - * + * * @param deviceType 设备类型 * @return 页面标题 */ @@ -743,7 +680,7 @@ public static String getTitle(DeviceType deviceType) { /** * 使用默认设备类型获取当前页面标题 - * + * * @return 页面标题 */ public static String getTitle() { @@ -752,7 +689,7 @@ public static String getTitle() { /** * 获取当前页面URL - * + * * @param deviceType 设备类型 * @return 页面URL */ @@ -762,7 +699,7 @@ public static String getUrl(DeviceType deviceType) { /** * 使用默认设备类型获取当前页面URL - * + * * @return 页面URL */ public static String getUrl() { @@ -771,72 +708,65 @@ public static String getUrl() { /** * 初始化Stealth模式(使浏览器更难被检测为自动化工具) - * + * 增强版本,集成SeleniumUtil的反检测功能 + * * @param deviceType 设备类型 */ public static void initStealth(DeviceType deviceType) { - BrowserContext context; - - // 根据设备类型创建相应的上下文 + // 获取当前页面,不重新创建上下文和页面 + Page page = getPage(deviceType); + + // 为现有上下文设置额外的HTTP头 + BrowserContext context = getContext(deviceType); if (deviceType == DeviceType.DESKTOP) { - // 桌面设备上下文 - context = BROWSER.newContext(new Browser.NewContextOptions() - .setViewportSize(1920, 1080) - .setUserAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36") - .setJavaScriptEnabled(true) - .setBypassCSP(true) - .setExtraHTTPHeaders(Map.of( - "sec-ch-ua", "\"Chromium\";v=\"135\", \"Not A(Brand\";v=\"99\"", - "sec-ch-ua-mobile", "?0", - "sec-ch-ua-platform", "\"Windows\"", - "sec-fetch-dest", "document", - "sec-fetch-mode", "navigate", - "sec-fetch-site", "same-origin", - "accept-language", "zh-CN,zh;q=0.9,en;q=0.8"))); - - // 更新全局上下文 - DESKTOP_CONTEXT = context; - // 创建页面 - DESKTOP_PAGE = DESKTOP_CONTEXT.newPage(); + context.setExtraHTTPHeaders(Map.of( + "sec-ch-ua", "\"Google Chrome\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"", + "sec-ch-ua-mobile", "?0", + "sec-ch-ua-platform", "\"macOS\"", + "accept-language", "zh-CN,zh;q=0.9", + "referer", "https://www.zhipin.com/", + "sec-fetch-dest", "document", + "sec-fetch-mode", "navigate", + "sec-fetch-site", "same-origin")); } else { - // 移动设备上下文 - context = BROWSER.newContext(new Browser.NewContextOptions() - .setViewportSize(375, 812) - .setDeviceScaleFactor(3.0) - .setIsMobile(true) - .setHasTouch(true) - .setUserAgent( - "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1") - .setJavaScriptEnabled(true) - .setBypassCSP(true) - .setExtraHTTPHeaders(Map.of( - "sec-ch-ua", "\"Chromium\";v=\"135\", \"Not A(Brand\";v=\"99\"", - "sec-ch-ua-mobile", "?1", - "sec-ch-ua-platform", "\"iOS\"", - "sec-fetch-dest", "document", - "sec-fetch-mode", "navigate", - "sec-fetch-site", "same-origin", - "accept-language", "zh-CN,zh;q=0.9,en;q=0.8"))); - - // 更新全局上下文 - MOBILE_CONTEXT = context; - // 创建页面 - MOBILE_PAGE = MOBILE_CONTEXT.newPage(); + context.setExtraHTTPHeaders(Map.of( + "sec-ch-ua", "\"Chromium\";v=\"135\", \"Not A(Brand\";v=\"99\"", + "sec-ch-ua-mobile", "?1", + "sec-ch-ua-platform", "\"iOS\"", + "accept-language", "zh-CN,zh;q=0.9", + "sec-fetch-dest", "document", + "sec-fetch-mode", "navigate", + "sec-fetch-site", "same-origin")); } - // 获取当前页面 - Page page = getPage(deviceType); - - // 执行stealth.min.js(需要事先准备此文件) + // 注入反检测脚本(从SeleniumUtil移植) + String stealthScript = """ + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Proxy; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Window; + window.navigator.chrome = { runtime: {} }; + Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); + Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]}); + Object.defineProperty(navigator, 'injected', {get: () => 123}); + """; + + page.addInitScript(stealthScript); + + // 如果有stealth.min.js文件,也尝试加载 try { String stealthJs = new String( - Files.readAllBytes(Paths.get(ProjectRootResolver.rootPath + "/src/main/resources/stealth.min.js"))); + Files.readAllBytes(Paths.get("src/main/resources/stealth.min.js"))); page.addInitScript(stealthJs); - log.info("已启用Stealth模式 (设备类型: {})", deviceType); + log.info("已加载stealth.min.js文件"); } catch (IOException e) { - log.error("启用Stealth模式失败,无法加载stealth.min.js (设备类型: {})", deviceType, e); + log.info("未找到stealth.min.js文件,使用内置反检测脚本"); } + log.info("已启用增强Stealth模式 (设备类型: {})", deviceType); } /** @@ -846,9 +776,39 @@ public static void initStealth() { initStealth(defaultDeviceType); } + /** + * 设置默认请求头(从SeleniumUtil移植) + * + * @param deviceType 设备类型 + */ + public static void setDefaultHeaders(DeviceType deviceType) { + BrowserContext context = getContext(deviceType); + + Map headers = Map.of( + "sec-ch-ua", "\"Google Chrome\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"", + "sec-ch-ua-mobile", deviceType == DeviceType.MOBILE ? "?1" : "?0", + "sec-ch-ua-platform", deviceType == DeviceType.MOBILE ? "\"iOS\"" : "\"macOS\"", + "user-agent", deviceType == DeviceType.MOBILE ? + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" : + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "accept-language", "zh-CN,zh;q=0.9", + "referer", "https://www.zhipin.com/" + ); + + context.setExtraHTTPHeaders(headers); + log.info("已设置默认请求头 (设备类型: {})", deviceType); + } + + /** + * 使用默认设备类型设置默认请求头 + */ + public static void setDefaultHeaders() { + setDefaultHeaders(defaultDeviceType); + } + /** * 获取当前设备类型的Page对象 - * + * * @param deviceType 设备类型 * @return 对应的Page对象 */ @@ -858,7 +818,7 @@ public static Page getPageObject(DeviceType deviceType) { /** * 使用默认设备类型获取Page对象 - * + * * @return 对应的Page对象 */ public static Page getPageObject() { @@ -867,7 +827,7 @@ public static Page getPageObject() { /** * 设置自定义Cookie - * + * * @param name Cookie名称 * @param value Cookie值 * @param domain Cookie域 @@ -878,7 +838,7 @@ public static Page getPageObject() { * @param deviceType 设备类型 */ public static void setCookie(String name, String value, String domain, String path, - Double expires, Boolean secure, Boolean httpOnly, DeviceType deviceType) { + Double expires, Boolean secure, Boolean httpOnly, DeviceType deviceType) { com.microsoft.playwright.options.Cookie cookie = new com.microsoft.playwright.options.Cookie(name, value); cookie.domain = domain; cookie.path = path; @@ -904,7 +864,7 @@ public static void setCookie(String name, String value, String domain, String pa /** * 使用默认设备类型设置自定义Cookie - * + * * @param name Cookie名称 * @param value Cookie值 * @param domain Cookie域 @@ -914,13 +874,13 @@ public static void setCookie(String name, String value, String domain, String pa * @param httpOnly 是否仅HTTP(可选) */ public static void setCookie(String name, String value, String domain, String path, - Double expires, Boolean secure, Boolean httpOnly) { + Double expires, Boolean secure, Boolean httpOnly) { setCookie(name, value, domain, path, expires, secure, httpOnly, defaultDeviceType); } /** * 简化的设置Cookie方法 - * + * * @param name Cookie名称 * @param value Cookie值 * @param domain Cookie域 @@ -933,7 +893,7 @@ public static void setCookie(String name, String value, String domain, String pa /** * 使用默认设备类型的简化设置Cookie方法 - * + * * @param name Cookie名称 * @param value Cookie值 * @param domain Cookie域 @@ -942,4 +902,49 @@ public static void setCookie(String name, String value, String domain, String pa public static void setCookie(String name, String value, String domain, String path) { setCookie(name, value, domain, path, null, null, null, defaultDeviceType); } + + /** + * 检查Cookie文件是否有效(从SeleniumUtil移植) + * + * @param cookiePath Cookie文件路径 + * @return 文件是否存在 + */ + public static boolean isCookieValid(String cookiePath) { + return Files.exists(Paths.get(cookiePath)); + } + + /** + * 带错误消息的元素查找(从SeleniumUtil移植) + * + * @param selector 元素选择器 + * @param message 错误消息 + * @param deviceType 设备类型 + * @return 元素对象的Optional包装 + */ + public static Optional findElementWithMessage(String selector, String message, DeviceType deviceType) { + try { + Locator locator = getPage(deviceType).locator(selector); + // 检查元素是否存在 + if (locator.count() > 0) { + return Optional.of(locator); + } else { + log.error(message); + return Optional.empty(); + } + } catch (Exception e) { + log.error(message + ": " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * 使用默认设备类型的带错误消息的元素查找 + * + * @param selector 元素选择器 + * @param message 错误消息 + * @return 元素对象的Optional包装 + */ + public static Optional findElementWithMessage(String selector, String message) { + return findElementWithMessage(selector, message, defaultDeviceType); + } } \ No newline at end of file diff --git a/src/main/java/utils/ProjectRootResolver.java b/src/main/java/utils/ProjectRootResolver.java deleted file mode 100644 index b7d46eb5..00000000 --- a/src/main/java/utils/ProjectRootResolver.java +++ /dev/null @@ -1,38 +0,0 @@ -package utils; - -import java.io.File; - -public class ProjectRootResolver { - - public static String rootPath = findProjectRoot().getAbsolutePath(); - - public static File findProjectRoot() { - // 获取当前类加载路径 - String classPath = ProjectRootResolver.class.getProtectionDomain() - .getCodeSource() - .getLocation() - .getPath(); - - File dir = new File(classPath).getAbsoluteFile(); - - // 如果是 /target/classes 或 /build/classes/java/main,跳两层 - if (dir.getPath().contains("/target/")) { - dir = dir.getParentFile().getParentFile(); - } else if (dir.getPath().contains("/build/")) { - dir = dir.getParentFile().getParentFile(); - } - - // 向上递归查找标志文件 - while (dir != null) { - if (new File(dir, "pom.xml").exists() || - new File(dir, "build.gradle").exists() || - new File(dir, ".git").exists()) { - return dir; - } - dir = dir.getParentFile(); - } - - return null; - } - -} \ No newline at end of file diff --git a/src/main/java/utils/RandomUserBehaviorSimulator.java b/src/main/java/utils/RandomUserBehaviorSimulator.java deleted file mode 100644 index be4bdee4..00000000 --- a/src/main/java/utils/RandomUserBehaviorSimulator.java +++ /dev/null @@ -1,167 +0,0 @@ -package utils; - -import org.openqa.selenium.By; -import org.openqa.selenium.Keys; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.chrome.ChromeDriver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.concurrent.TimeUnit; - -import static utils.Constant.ACTIONS; - -/** - * @author Summer - * 项目链接: https://github.com/loks666/get_jobs - */ - -public class RandomUserBehaviorSimulator { - private static final ChromeDriver driver = Constant.CHROME_DRIVER; - private static final Random random = new Random(); - private static final Logger log = LoggerFactory.getLogger(RandomUserBehaviorSimulator.class); - private static final String[] randomTexts = { - "您好,我对这个岗位很感兴趣!", - "请问可以详细介绍一下岗位职责吗?", - "工作地点在哪里?", - "这个职位的发展前景如何?", - "我的简历已经投递,请查收。", - "能否提供一下具体的薪资范围?", - "我有相关的工作经验,非常符合要求。", - "期待与贵公司合作。", - "这是一次很棒的求职机会!", - "岗位要求中的技能我都具备。", - "请问这个职位还在招聘吗?", - "非常期待能够加入贵公司。", - "我对贵公司的文化非常认可。", - "请问这个岗位是否支持远程办公?", - "这是我梦寐以求的工作!" - }; - /** - * 模拟随机的用户行为,确保不干扰页面核心自动化行为 - */ - public static void simulateRandomUserBehavior() { - // 模拟随机滚动页面 - simulateRandomScrolling(); - - // 模拟随机输入文本 - simulateRandomInput(); - - // 随机等待时间 - sleepRandom(0, 2); - } - - /** - * 随机模拟页面滚动 - */ - private static void simulateRandomScrolling() { - int numScrolls = random.nextInt(3) + 1; // 随机滚动次数 1 到 3 次 - - for (int i = 0; i < numScrolls; i++) { - // 增加滚动幅度,范围为 -500 到 +500 像素 - int scrollAmount = random.nextInt(1001) - 500; // 随机滚动 -500 到 +500 像素 - String script = String.format("window.scrollBy(0, %d);", scrollAmount); -// driver.executeScript(script); - driver.executeScript(String.format("window.scrollBy(0,%d)", scrollAmount)); - - try { - TimeUnit.MILLISECONDS.sleep(random.nextInt(300) + 100); // 随机延迟 100 到 400 毫秒 - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Sleep was interrupted", e); - } - } - } - - - /** - * 随机输入文本 - */ - private static void simulateRandomInput() { - // 定义所有的 CSS 选择器 - List selectors = List.of( - "div.job-search-wrapper .input-wrap input", - "div.satisfaction-feedback-wrapper textarea", - "div.nav-search .ipt-search", - "div.has-header .boss-search-top .boss-search-container .boss-search-input" - ); - - // 用于存储找到的 WebElement - List inputs = new ArrayList<>(); - - // 遍历每个选择器,查找元素 - for (String selector : selectors) { - try { -// WebElement element = driver.findElementByCssSelector(selector); - WebElement element = driver.findElement(By.cssSelector(selector)); - if (element != null) { - inputs.add(element); - } - } catch (Exception ignored) { - // 忽略找不到元素的异常 - } - } - - // 如果没有找到任何元素,返回 - if (inputs.isEmpty()) { - return; - } - - // 从找到的元素中随机选择一个 - WebElement selectedInput = inputs.get(random.nextInt(inputs.size())); - - // 随机选择一个文本 - String textToInput = randomTexts[random.nextInt(randomTexts.length)]; - - // 对选中的元素进行逐字节输入 - selectedInput.clear(); - for (char c1 : textToInput.toCharArray()) { - selectedInput.sendKeys(String.valueOf(c1)); - try { - // 模拟逐字输入,间隔随机 50-200 毫秒 - TimeUnit.MILLISECONDS.sleep(random.nextInt(150) + 50); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Sleep was interrupted", e); - } - } - - // 逐字节删除 - for (char c2 : textToInput.toCharArray()) { - selectedInput.sendKeys(Keys.BACK_SPACE); - try { - // 模拟逐字输入,间隔随机 50-200 毫秒 - TimeUnit.MILLISECONDS.sleep(random.nextInt(150) + 50); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Sleep was interrupted", e); - } - } - - // 使用 JavaScript 滚动到页面顶部 - driver.executeScript("window.scrollTo(0, 0);"); - - // 等待随机的时间 - sleepRandom(1, 1); - } - - - /** - * 模拟随机的等待时间 - */ - private static void sleepRandom(int minSeconds, int maxSeconds) { - try { - // 生成一个随机等待时间,范围是 minSeconds 到 maxSeconds 之间,包括 0 - int randomSleepTime = random.nextInt((maxSeconds - minSeconds) + 1) + minSeconds; - if (randomSleepTime > 0) { - TimeUnit.SECONDS.sleep(randomSleepTime); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Sleep was interrupted", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/utils/SeleniumUtil.java b/src/main/java/utils/SeleniumUtil.java index 1e66bae1..43c779f2 100644 --- a/src/main/java/utils/SeleniumUtil.java +++ b/src/main/java/utils/SeleniumUtil.java @@ -50,7 +50,7 @@ public static void initDriver() { SeleniumUtil.getWait(WAIT_TIME); } - public static void getChromeDriver(){ + public static void getChromeDriver() { getChromeDriver(false); } @@ -63,16 +63,16 @@ public static void getChromeDriver(Boolean mobile) { String osType = getOSType(osName); switch (osType) { case "windows": - options.setBinary("C:/Program Files/Google/Chrome/Application/chrome.exe");//TODO 注意: 这里需要修改为你的chrome的安装路径,不然启动会报错!!! 右键chrome图标右键,选择属性,复制路径 - System.setProperty("webdriver.chrome.driver", ProjectRootResolver.rootPath+"/src/main/resources/chromedriver.exe"); + options.setBinary("C:/Program Files/Google/Chrome/Application/chrome.exe"); + System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver.exe"); break; case "mac": options.setBinary("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); - System.setProperty("webdriver.chrome.driver", ProjectRootResolver.rootPath+"/src/main/resources/chromedriver"); + System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver"); break; case "linux": options.setBinary("/usr/bin/google-chrome-stable"); - System.setProperty("webdriver.chrome.driver", ProjectRootResolver.rootPath+"/src/main/resources/chromedriver-linux64/chromedriver"); + System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver-linux64/chromedriver"); break; default: log.info("你这什么破系统,没见过,别跑了!"); @@ -80,7 +80,7 @@ public static void getChromeDriver(Boolean mobile) { } BossConfig config = BossConfig.init(); if (config.getDebugger()) { - options.addExtensions(new File(ProjectRootResolver.rootPath+"/src/main/resources/xpathHelper.crx")); + options.addExtensions(new File("src/main/resources/xpathHelper.crx")); } else { options.addArguments("--disable-extensions"); } @@ -95,8 +95,6 @@ public static void getChromeDriver(Boolean mobile) { options.addArguments("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"); - - CHROME_DRIVER = new ChromeDriver(options); CHROME_DRIVER.manage().window().maximize(); @@ -105,7 +103,7 @@ public static void getChromeDriver(Boolean mobile) { ChromeOptions mobileOptions = new ChromeOptions(); addMobileEmulationOptions(mobileOptions); - if(mobile){ + if (mobile) { MOBILE_CHROME_DRIVER = new ChromeDriver(mobileOptions); MOBILE_CHROME_DRIVER.manage().window().maximize(); } @@ -114,6 +112,7 @@ public static void getChromeDriver(Boolean mobile) { /** * 添加移动设备模拟配置到ChromeOptions + * * @param options ChromeOptions对象 */ private static void addMobileEmulationOptions(ChromeOptions options) { @@ -192,7 +191,7 @@ private static void updateCookieFile(JSONArray jsonArray, String path) { public static void loadCookie(String cookiePath) { // 首先清除由于浏览器打开已有的cookies CHROME_DRIVER.manage().deleteAllCookies(); - if(Objects.nonNull(MOBILE_CHROME_DRIVER)){ + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { MOBILE_CHROME_DRIVER.manage().deleteAllCookies(); } // 从文件中读取JSONArray @@ -230,7 +229,7 @@ public static void loadCookie(String cookiePath) { .build(); try { CHROME_DRIVER.manage().addCookie(cookie); - if(Objects.nonNull(MOBILE_CHROME_DRIVER)){ + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { MOBILE_CHROME_DRIVER.manage().addCookie(cookie); } } catch (Exception ignore) { @@ -243,15 +242,15 @@ public static void loadCookie(String cookiePath) { public static void getActions() { ACTIONS = new Actions(Constant.CHROME_DRIVER); - if(Objects.nonNull(MOBILE_CHROME_DRIVER)){ + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { MOBILE_ACTIONS = new Actions(MOBILE_CHROME_DRIVER); } } public static void getWait(long time) { WAIT = new WebDriverWait(Constant.CHROME_DRIVER, Duration.ofSeconds(time)); - if(Objects.nonNull(MOBILE_CHROME_DRIVER)){ - MOBILE_WAIT = new WebDriverWait(MOBILE_CHROME_DRIVER,Duration.ofSeconds(time)); + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { + MOBILE_WAIT = new WebDriverWait(MOBILE_CHROME_DRIVER, Duration.ofSeconds(time)); } } @@ -294,31 +293,24 @@ public static boolean isCookieValid(String cookiePath) { return Files.exists(Paths.get(cookiePath)); } - public static void simulateRandomUserBehavior(boolean condition){ - if(condition){ - RandomUserBehaviorSimulator.simulateRandomUserBehavior(); - } - } - - /** * 注入反自动化检测的脚本,隐藏 webdriver、语言、插件等特征字段。 * 必须在 driver.get(url) 之前调用。 */ public static void injectStealthJs(DevTools devTools) { String script = """ - Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); - delete cdc_adoQpoasnfa76pfcZLmcfl_Array; - delete cdc_adoQpoasnfa76pfcZLmcfl_JSON; - delete cdc_adoQpoasnfa76pfcZLmcfl_Object; - delete cdc_adoQpoasnfa76pfcZLmcfl_Promise; - delete cdc_adoQpoasnfa76pfcZLmcfl_Proxy; - delete cdc_adoQpoasnfa76pfcZLmcfl_Symbol; - delete cdc_adoQpoasnfa76pfcZLmcfl_Window; - window.navigator.chrome = { runtime: {} }; - Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); - Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]}); - """; + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + delete cdc_adoQpoasnfa76pfcZLmcfl_Array; + delete cdc_adoQpoasnfa76pfcZLmcfl_JSON; + delete cdc_adoQpoasnfa76pfcZLmcfl_Object; + delete cdc_adoQpoasnfa76pfcZLmcfl_Promise; + delete cdc_adoQpoasnfa76pfcZLmcfl_Proxy; + delete cdc_adoQpoasnfa76pfcZLmcfl_Symbol; + delete cdc_adoQpoasnfa76pfcZLmcfl_Window; + window.navigator.chrome = { runtime: {} }; + Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); + Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]}); + """; devTools.send(Page.addScriptToEvaluateOnNewDocument( script, @@ -346,6 +338,4 @@ public static void setDefaultHeaders(DevTools devTools) { } - - } diff --git a/src/main/resources/chromedriver.exe b/src/main/resources/chromedriver.exe index 99be610c..1339b31c 100644 Binary files a/src/main/resources/chromedriver.exe and b/src/main/resources/chromedriver.exe differ diff --git a/src/main/resources/config.yaml b/src/main/resources/config.yaml index e7c2d631..05117dad 100644 --- a/src/main/resources/config.yaml +++ b/src/main/resources/config.yaml @@ -2,48 +2,21 @@ boss: debugger: false # 开发者模式,默认为false即可 sayHi: "您好,我有8年工作经验,还有AIGC大模型、Java,Python,Golang和运维的相关经验,希望应聘这个岗位,期待可以与您进一步沟通,谢谢!" #必须要关闭boss的自动打招呼 - keywords: [ "Java" ] # 需要搜索的职位,会依次投递 + keywords: [ "大模型","Python","Golang","Java" ] # 需要搜索的职位,会依次投递 industry: [ "不限" ] # 公司行业,只能选三个,相关代码枚举的部分,如果需要其他的需要自己找 - cityCode: [ "广州" ] # 只列举了部分,如果没有的需要自己找:目前支持的:全国 北京 上海 杭州 广州 深圳 成都 天津 - customCityCode: - "佛山": "101280800" - "珠海": "101280700" + cityCode: [ "上海" ] # 只列举了部分,如果没有的需要自己找:目前支持的:全国 北京 上海 杭州 广州 深圳 成都 天津 experience: [ "5-10年" ] # 工作经验:"应届毕业生", "1年以下", "1-3年", "3-5年", "5-10年", "10年以上" jobType: "不限" #求职类型:"全职", "兼职" - salary: "10-20K" # 薪资(单选):"3K以下", "3-5K", "5-10K", "10-20K", "20-50K", "50K以上" - degree: [ "本科" ] # 学历: "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士" + salary: "20-50K" # 薪资(单选):"3K以下", "3-5K", "5-10K", "10-20K", "20-50K", "50K以上" + degree: [ "不限" ] # 学历: "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士" scale: [ "不限" ] # 公司规模:"0-20人", "20-99人", "100-499人", "500-999人", "1000-9999人", "10000人以上" stage: [ "不限" ] # "未融资", "天使轮", "A轮", "B轮", "C轮", "D轮及以上", "已上市", "不需要融资" expectedSalary: [ 15,25 ] #期望薪资,单位为K,第一个数字为最低薪资,第二个数字为最高薪资,只填一个数字默认为最低薪水 waitTime: 10 #每投递一个岗位,等待几秒 filterDeadHR: true # 是否过滤不活跃HR,该选项会过滤半年前活跃的HR enableAI: false # 开启AI检测与自动生成打招呼语 - sendImgResume: true # 是否发送图片简历 - deadStatus: ["2周内活跃","本月活跃","2月内活跃","半年前活跃"] # 过滤掉HR状态 -mobileboss: - debugger: false # 开发者模式,默认为false即可 - sayHi: "您好,我有8年工作经验,还有AIGC大模型、Java,Python,Golang和运维的相关经验,希望应聘这个岗位,期待可以与您进一步沟通,谢谢!" #必须要关闭boss的自动打招呼 - keywords: [ "Java" ] # 需要搜索的职位,会依次投递 - cityCode: [ "广州" ] # 只列举了部分,如果没有的需要自己找:目前支持的:全国 北京 上海 杭州 广州 深圳 成都 天津 - customCityCode: - "佛山": "c101280800" - "珠海": "c101280700" - experience: [ "5-10年" ] # 工作经验:"应届毕业生", "1年以下", "1-3年", "3-5年", "5-10年", "10年以上" - salary: "10-20K" # 薪资(单选):"3K以下", "3-5K", "5-10K", "10-20K", "20-50K", "50K以上" - degree: [ "本科" ] # 学历: "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士" - scale: [ "不限" ] # 公司规模:"0-20人", "20-99人", "100-499人", "500-999人", "1000-9999人", "10000人以上" - expectedSalary: [ 15,25 ] #期望薪资,单位为K,第一个数字为最低薪资,第二个数字为最高薪资,只填一个数字默认为最低薪水 - waitTime: 10 #每投递一个岗位,等待几秒 - filterDeadHR: true # 是否过滤不活跃HR,该选项会过滤半年前活跃的HR - enableAI: false # 开启AI检测与自动生成打招呼语 - sendImgResume: true # 是否发送图片简历 - deadStatus: ["2周内活跃","本月活跃","2月内活跃","半年前活跃"] # 过滤掉HR状态 - nextIntervalMinutes: 50 - keyFilter: true # 是否使用关键词匹配岗位名称,true岗位名称不包含完整关键字就会过滤,false则不过滤直接使用boss搜索结果 - ### 以下几个参数移动端查岗暂不支持 - jobType: "不限" #求职类型:"全职", "兼职" - industry: [ "不限" ] # 公司行业,只能选三个,相关代码枚举的部分,如果需要其他的需要自己找 - stage: [ "不限" ] # "未融资", "天使轮", "A轮", "B轮", "C轮", "D轮及以上", "已上市", "不需要融资" + sendImgResume: false # 是否发送图片简历 + deadStatus: [ "2周内活跃","本月活跃","2月内活跃","半年前活跃" ] # 过滤掉HR状态 job51: jobArea: [ "上海" ] #工作地区:目前只有【北京 成都 上海 广州 深圳】 @@ -68,9 +41,8 @@ zhilian: keywords: [ "AI", "Java", "Python", "Golang" ] ai: - introduce: "我熟练使用Spring Boot、Spring Cloud、Alibaba Cloud及其生态体系,擅长MySQL、Oracle、PostgreSQL等关系型数据库以及MongoDB、Redis等非关系型数据库。熟悉Docker、Kubernetes等容器化技术,掌握WebSocket、Netty等通信协议,拥有即时通讯系统的开发经验。熟练使用MyBatis-Plus、Spring Data、Django ORM等ORM框架,熟练使用Python、Golang开发,具备机器学习、深度学习及大语言模型的开发与部署经验。此外,我熟悉前端开发,涉及Vue、React、Nginx配置及PHP框架应用" #这是喂给AI的提示词,主要介绍自己的优势 + introduce: "我熟练使用Java Python Golang语言进行开发,目前主要方向为AI开发,Java熟悉Spring Boot、Cloud生态体系,擅长MySQL、Oracle、PostgreSQL等关系型数据库以及MongoDB、Redis等非关系型数据库。熟悉Docker、Kubernetes等容器化技术,掌握WebSocket、Netty等通信协议,拥有即时通讯系统的开发经验。熟练使用MyBatis-Plus、Spring Data、Django ORM等ORM框架,熟练使用Python、Golang开发,具备机器学习、深度学习及大语言模型的开发与部署经验。此外,我熟悉前端开发,涉及Vue、React、Nginx配置及PHP框架应用" #这是喂给AI的提示词,主要介绍自己的优势 prompt: "我目前在找工作,%s,我期望的的岗位方向是【%s】,目前我需要投递的岗位名称是【%s】,这个岗位的要求是【%s】,如果这个岗位和我的期望与经历基本符合,注意是基本符合,那么请帮我写一个给HR打招呼的文本发给我,如果这个岗位和我的期望经历完全不相干,直接返回false给我,注意只要返回我需要的内容即可,不要有其他的语气助词,重点要突出我和岗位的匹配度以及我的优势,我自己写的招呼语是:【%s】,你可以参照我自己写的根据岗位情况进行适当调整" #这是AI的提示词,可以自行修改 bot: - is_send: true #开启企业微信消息推送 - is_bark_send: true #开启Bark消息推送 + is_send: false #开启企业微信消息推送