GPT自動(dòng)投簡(jiǎn)歷,一周斬獲三offer,開(kāi)源分享!
共 16430字,需瀏覽 33分鐘
·
2024-04-02 18:33
作者:不要禿頭啊
https://juejin.cn/post/7320949203542409231
一、前言
最近在 GitHub 上發(fā)現(xiàn)了一個(gè)非常有意思的項(xiàng)目:GitHub鏈接1。
該作者巧妙地結(jié)合 GPT 和 RPA 技術(shù),打造了一個(gè)自動(dòng)投簡(jiǎn)歷助手。這是原作者分享的效果展示視頻:B站視頻鏈接2。
然而,由于原項(xiàng)目存在以下問(wèn)題:
代碼使用 Python 編寫(xiě),對(duì)于前端開(kāi)發(fā)者不夠友好。
運(yùn)行該項(xiàng)目需要充值 OpenAI 賬戶,而且只支持使用國(guó)外的信用卡,國(guó)內(nèi)用戶想充錢都沒(méi)地。
運(yùn)行該項(xiàng)目還需要配置代理,對(duì)一些用戶而言可能不太友好。
折騰無(wú)果,遂決定使用 Node.js 重新實(shí)現(xiàn)該項(xiàng)目,并且完全免費(fèi)、一鍵運(yùn)行,無(wú)需設(shè)置代理:GitHub項(xiàng)目地址3。
在這個(gè)寒冷的招聘季,這個(gè)腳本能為您提供一些幫助,為您帶來(lái)一些溫暖。如果您覺(jué)得這個(gè)項(xiàng)目有價(jià)值,希望您能幫忙點(diǎn)個(gè) star4,將不勝感激。
二、整體思路
首先,我們會(huì)使用 selenium-webdriver5 來(lái)模擬用戶行為,該庫(kù)是一個(gè)強(qiáng)大的自動(dòng)化測(cè)試工具。它能夠通過(guò)編程方式控制瀏覽器交互,通常用于自動(dòng)化測(cè)試、網(wǎng)頁(yè)抓取以及模擬用戶交互等任務(wù)。
用 selenium-webdriver5 模擬用戶打開(kāi)瀏覽器窗口,并導(dǎo)航至直聘網(wǎng)的主頁(yè)。
等待頁(yè)面加載完成,找到登錄按鈕的
DOM節(jié)點(diǎn),模擬用戶點(diǎn)擊觸發(fā)登錄,等待用戶掃碼操作。在用戶成功掃碼登錄后,進(jìn)入招聘信息列表頁(yè)面。
遍歷招聘信息列表,對(duì)每一項(xiàng)進(jìn)行以下操作:
點(diǎn)擊招聘信息,找到該項(xiàng)招聘信息的職位描述信息
結(jié)合上傳的簡(jiǎn)歷信息與招聘信息傳遞給
GPT,等待GPT的響應(yīng)在
GPT響應(yīng)后,點(diǎn)擊“立即溝通”按鈕,進(jìn)入溝通聊天界面在聊天界面中找到輸入框,將
GPT返回的信息填入聊天框,并觸發(fā)發(fā)送事件返回招聘信息列表頁(yè)面,點(diǎn)擊下一項(xiàng)招聘信息
重復(fù)上述步驟,遍歷下一項(xiàng)招聘信息的職位描述信息
三、具體實(shí)現(xiàn)
3.1、獲取免費(fèi)的 API Key 并初始化 OpenAI 客戶端
做過(guò) GPT 開(kāi)發(fā)的應(yīng)該知道,調(diào)用 GPT 的接口是要付費(fèi)的,而且充值過(guò)程異常繁瑣,需要使用境外銀行卡。
為了簡(jiǎn)化這個(gè)過(guò)程,我在 GitCode 上找到了一個(gè)提供免費(fèi) API_KEY 的項(xiàng)目6,只需使用 GitHub 賬戶登錄即可輕松領(lǐng)取。
這樣你就可以用免費(fèi)的 API_KEY 來(lái)初始化 OpenAI 客戶端。
// 初始化OpenAI客戶端
const openai = new OpenAI({
// 代理地址,這樣國(guó)內(nèi)用戶就可以訪問(wèn)了
baseURL: "https://api.chatanywhere.com.cn",
apiKey: "你的apiKey",
});
3.2、模擬用戶打開(kāi)瀏覽器并前往主頁(yè)
在這一步中,我們要實(shí)現(xiàn)的是打開(kāi)瀏覽器并導(dǎo)航至指定的 URL。具體操作就是調(diào)用 selenium-webdriver5 的 API,直接上代碼:
const { Builder, By, until } = require("selenium-webdriver");
const chrome = require("selenium-webdriver/chrome");
// 全局 WebDriver 實(shí)例
let driver;
// 使用指定的選項(xiàng)打開(kāi)瀏覽器
async function openBrowserWithOptions(url, browser) {
const options = new chrome.Options();
options.addArguments("--detach");
if (browser === "chrome") {
// 初始化一個(gè)谷歌瀏覽器客戶端
driver = await new Builder()
.forBrowser("chrome")
.setChromeOptions(options)
.build();
// 全屏打開(kāi)瀏覽器
await driver.manage().window().maximize();
} else {
throw new Error("不支持的瀏覽器類型");
}
await driver.get(url);
// 等待直到頁(yè)面包含登錄按鈕dom
const loginDom = By.xpath("http://*[@id='header']/section[1]/section[3]/section/a");
await driver.wait(until.elementLocated(loginDom), 10000);
}
// 主函數(shù)
async function main(url, browserType) {
try {
// 打開(kāi)瀏覽器
await openBrowserWithOptions(url, browserType);
} catch (error) {
console.error(`發(fā)生錯(cuò)誤: ${error}`);
}
}
const url =
"https://www.zhipin.com/web/geek/job-recommend?ka=header-job-recommend";
const browserType = "chrome";
main(url, browserType);
3.3、找到登錄按鈕的DOM節(jié)點(diǎn)并點(diǎn)擊
這一步中我們需要找到 登錄按鈕 的 DOM 節(jié)點(diǎn),然后模擬點(diǎn)擊登錄。
// 省略上一步的代碼
// 點(diǎn)擊登錄按鈕,并等待登錄成功
async function logIn() {
// 點(diǎn)擊登錄
const loginButton = await driver.findElement(
By.xpath("http://*[@id='header']/section[1]/section[3]/section/a")
);
await loginButton.click();
// 等待微信登錄按鈕出現(xiàn)
const xpathLocatorWechatLogin =
"http://*[@id='wrap']/section/section[2]/section[2]/section[2]/section[1]/section[4]/a";
await driver.wait(
until.elementLocated(By.xpath(xpathLocatorWechatLogin)),
10000
);
const wechatButton = await driver.findElement(
By.xpath("http://*[@id='wrap']/section/section[2]/section[2]/section[2]/section[1]/section[4]/a")
);
// 選擇微信掃碼登錄
await wechatButton.click();
const xpathLocatorWechatLogo =
"http://*[@id='wrap']/section/section[2]/section[2]/section[1]/section[2]/section[1]/img";
await driver.wait(
until.elementLocated(By.xpath(xpathLocatorWechatLogo)),
10000
);
// 等待用戶掃碼,登錄成功
const xpathLocatorLoginSuccess = "http://*[@id='header']/section[1]/section[3]/ul/li[2]/a";
await driver.wait(
until.elementLocated(By.xpath(xpathLocatorLoginSuccess)),
60000
);
}
// 主函數(shù)
async function main(url, browserType) {
try {
// 打開(kāi)瀏覽器
// 點(diǎn)擊登錄按鈕,并等待登錄成功
+ await logIn();
} catch (error) {
console.error(`發(fā)生錯(cuò)誤: ${error}`);
}
}
3.4、遍歷招聘信息列表
登錄成功后進(jìn)入到招聘信息列表頁(yè)面,這一步中我們需要遍歷招聘信息并依次點(diǎn)擊,找到每一項(xiàng)招聘信息的職位描述信息,如圖所示:
// 省略上一步的代碼
// 根據(jù)索引獲取職位描述
async function getJobDescriptionByIndex(index) {
try {
const jobSelector = `//*[@id='wrap']/section[2]/section[2]/section/section/section[1]/ul/li[${index}]`;
const jobElement = await driver.findElement(By.xpath(jobSelector));
// 點(diǎn)擊招聘信息列表中的項(xiàng)
await jobElement.click();
// 找到描述信息節(jié)點(diǎn)并獲取文字
const descriptionSelector =
"http://*[@id='wrap']/section[2]/section[2]/section/section/section[2]/section/section[2]/p";
await driver.wait(
until.elementLocated(By.xpath(descriptionSelector)),
10000
);
const jobDescriptionElement = await driver.findElement(
By.xpath(descriptionSelector)
);
return jobDescriptionElement.getText();
} catch (error) {
console.log(`在索引 ${index} 處找不到工作。`);
return null;
}
}
// 主函數(shù)
async function main(url, browserType) {
try {
// 打開(kāi)瀏覽器
// 點(diǎn)擊登錄按鈕,并等待登錄成功
// 開(kāi)始的索引
+ let jobIndex = 1;
+ while (true) {
+ // 獲取對(duì)應(yīng)下標(biāo)的職位描述
+ const jobDescription = await getJobDescriptionByIndex(jobIndex);
+ console.log(`職位描述信息/n:${jobDescription}`);
+ if (jobDescription) {
+ //
+ }
+ jobIndex += 1;
}
} catch (error) {
console.error(`發(fā)生錯(cuò)誤: ${error}`);
}
}
接著結(jié)合上傳的簡(jiǎn)歷信息與招聘信息傳遞給 GPT,等待 GPT 的響應(yīng):
// 省略上一步的代碼
// 讀取簡(jiǎn)歷信息
const getResumeInfo = () => {
fs.readFile("./簡(jiǎn)歷基本信息.txt", "utf8", (err, data) => {
if (err) {
console.error("讀取文件時(shí)出錯(cuò):", err);
return;
}
// 輸出文件內(nèi)容
return data;
});
};
// 與GPT進(jìn)行聊天的函數(shù)
async function chat(jobDescription) {
// 獲取簡(jiǎn)歷信息
const resumeInfo = getResumeInfo();
const askMessage = `你好,這是我的簡(jiǎn)歷:${resumeInfo},這是我所應(yīng)聘公司的要求:${jobDescription}。我希望您能幫我直接給HR寫(xiě)一個(gè)禮貌專業(yè)的求職新消息,要求能夠用專業(yè)的語(yǔ)言將簡(jiǎn)歷中的技能結(jié)合應(yīng)聘工作的描述,來(lái)闡述自己的優(yōu)勢(shì),盡最大可能打動(dòng)招聘者。并且請(qǐng)您始終使用中文來(lái)進(jìn)行消息的編寫(xiě),開(kāi)頭是招聘負(fù)責(zé)人。這是一封完整的求職信,不要包含求職信內(nèi)容以外的東西,例如“根據(jù)您上傳的求職要求和個(gè)人簡(jiǎn)歷,我來(lái)幫您起草一封求職郵件:”這一類的內(nèi)容,以便于我直接自動(dòng)化復(fù)制粘貼發(fā)送,字?jǐn)?shù)控制在80字左右為宜`;
try {
const completion = await openai.chat.completions.create({
messages: [
{
role: "system",
content: askMessage,
},
],
model: "gpt-3.5-turbo",
});
// 獲取gpt返回的信息
const formattedMessage = completion.choices[0].message.content.replace(
/\n/g,
" "
);
return formattedMessage;
} catch (error) {
console.error(`gpt返回時(shí)發(fā)生錯(cuò)誤: ${error}`);
const errorResponse = JSON.stringify({ error: String(error) });
return errorResponse;
}
}
// 主函數(shù)
async function main(url, browserType) {
try {
// 打開(kāi)瀏覽器
// 點(diǎn)擊登錄按鈕,并等待登錄成功
// 開(kāi)始的索引
while (true) {
// 獲取對(duì)應(yīng)下標(biāo)的職位描述
if (jobDescription) {
// 發(fā)送描述到聊天并打印響應(yīng)
+ const response = await chat(jobDescription);
+ console.log("gpt給的回復(fù)", response);
}
jobIndex += 1;
}
} catch (error) {
console.error(`發(fā)生錯(cuò)誤: ${error}`);
}
}
GPT 響應(yīng)完成后,找到 立即溝通按鈕 并模擬點(diǎn)擊,此時(shí)進(jìn)入溝通聊天界面,如圖所示:
// 省略上一步的代碼
// 主函數(shù)
async function main(url, browserType) {
try {
// 打開(kāi)瀏覽器
// 點(diǎn)擊登錄按鈕,并等待登錄成功
// 開(kāi)始的索引
while (true) {
// 獲取對(duì)應(yīng)下標(biāo)的職位描述
if (jobDescription) {
// 發(fā)送描述到聊天并打印響應(yīng)
// 點(diǎn)擊溝通按鈕
+ const contactButton = await driver.findElement(
+ By.xpath(
+ "http://*[@id='wrap']/section[2]/section[2]/section/section/section[2]/section/section[1]/section[2]/a[2]"
+ )
+ );
+ await contactButton.click();
}
jobIndex += 1;
}
} catch (error) {
console.error(`發(fā)生錯(cuò)誤: ${error}`);
}
}
此時(shí)進(jìn)入到聊天界面,將 GPT 的返回信息填入到輸入框中,觸發(fā)發(fā)送事件。
// 省略上一步的代碼
// 發(fā)送響應(yīng)到聊天框
async function sendResponseToChatBox(driver, response) {
try {
// 請(qǐng)找到聊天輸入框
const chatBox = await driver.findElement(By.xpath("http://*[@id='chat-input']"));
// 清除輸入框中可能存在的任何文本
await chatBox.clear();
// 將響應(yīng)粘貼到輸入框
await chatBox.sendKeys(response);
await sleep(1000);
// 模擬按下回車鍵來(lái)發(fā)送消息
await chatBox.sendKeys(Key.RETURN);
await sleep(2000); // 模擬等待2秒
} catch (error) {
console.error(`發(fā)送響應(yīng)到聊天框時(shí)發(fā)生錯(cuò)誤: ${error}`);
}
}
// 主函數(shù)
async function main(url, browserType) {
try {
// 打開(kāi)瀏覽器
// 點(diǎn)擊登錄按鈕,并等待登錄成功
// 開(kāi)始的索引
while (true) {
// 獲取對(duì)應(yīng)下標(biāo)的職位描述
if (jobDescription) {
// 發(fā)送描述到聊天并打印響應(yīng)
// 點(diǎn)擊溝通按鈕
// 等待回復(fù)框出現(xiàn)
+ const chatBox = await driver.wait(
+ until.elementLocated(By.xpath("http://*[@id='chat-input']")),
+ 10000
+ );
+ // 調(diào)用函數(shù)發(fā)送響應(yīng)
+ await sendResponseToChatBox(driver, response);
+ // 返回到上一個(gè)頁(yè)面
+ await driver.navigate().back();
+ await sleep(2000); // 模擬等待3秒
}
jobIndex += 1;
}
} catch (error) {
console.error(`發(fā)生錯(cuò)誤: ${error}`);
}
}
發(fā)送完成后返回招聘列表頁(yè)面,以此往復(fù)。
該項(xiàng)目只是簡(jiǎn)單的將簡(jiǎn)歷信息結(jié)合職位信息發(fā)送給 GPT,然后用 GPT 的回復(fù)發(fā)送給招聘者,實(shí)際上并沒(méi)有什么難度,意在拋磚引玉。
這里其實(shí)還有更優(yōu)雅的做法,比如將個(gè)人簡(jiǎn)歷傳給 GPT,讓 GPT 去提煉有效信息(原作者就是這么做的)。但由于 GPT-API-free 項(xiàng)目7 并沒(méi)有提供 assistant8 服務(wù),實(shí)現(xiàn)這一點(diǎn)需要付費(fèi),有充值渠道的朋友可以嘗試一下。
此外,對(duì)于有興趣的朋友,還可以進(jìn)一步深挖,例如:
根據(jù)職位詳情進(jìn)行分詞權(quán)重分析,生成崗位熱點(diǎn)詞匯云圖,幫助分析簡(jiǎn)歷匹配度
自動(dòng)過(guò)濾掉最近未活躍的 Boss 發(fā)布的信息,以免浪費(fèi)每天的 100 次機(jī)會(huì)
設(shè)置過(guò)濾薪資范圍,防止無(wú)效投遞
自動(dòng)檢測(cè)上下文,排除【外包、外派、駐場(chǎng)】等字眼的職位信息
...
最后,這里重申原作者的觀點(diǎn):
希望不要有人拿著我的腳本割韭菜,都已經(jīng)被逼到用這種腳本投簡(jiǎn)歷的地步了,還有啥油水可去榨。
向下滑動(dòng)查看
