1. 项目概述一个求职者的“信息雷达”如果你正在找工作或者曾经找过工作那你一定体会过那种每天在各个招聘网站、公司官网、社交媒体上反复刷新、生怕错过任何一个机会的焦虑感。手动搜索不仅效率低下还容易因为信息过载而遗漏关键岗位。NissonCX/offercatcher这个开源项目就是为了解决这个痛点而生的。你可以把它理解为一个为你私人定制的、24小时不间断工作的“求职信息雷达”。它的核心功能非常直接自动化地监控你指定的目标公司或职位关键词一旦有新的招聘信息发布就立即通过你预设的渠道比如邮件、Telegram机器人、钉钉等推送给你让你成为第一批看到机会的人。这不仅仅是简单的信息聚合更是一种主动式的求职策略。在竞争激烈的求职市场中时间就是机会早一步投递简历可能就意味着更高的面试概率。这个项目适合所有正在积极寻找工作机会的求职者无论是应届生、寻求跳槽的资深工程师还是希望探索新机会的自由职业者。它尤其适合那些目标明确例如只关注某几家心仪公司或特定技术栈岗位的求职者。通过自动化工具解放双手和注意力你可以将更多精力投入到简历优化、面试准备等更有价值的事情上。2. 核心设计思路与架构拆解2.1 从需求到方案为什么选择自建爬虫市面已有一些招聘信息聚合平台或RSS订阅服务那为什么还要自己搭建一个offercatcher呢这背后有几个关键的考量点。首先可控性与定制化。第三方平台的信息源、更新频率、筛选逻辑都是黑盒你无法干预。而offercatcher允许你完全自定义监控的目标。比如你可以精确地监控“A公司官网招聘页中所有包含‘后端开发’和‘Go’关键词的职位”或者“B招聘网站上‘机器学习’类别下薪资范围在30K以上的职位”。这种颗粒度的控制是通用平台难以提供的。其次实时性与可靠性。自建服务的更新周期完全由你决定可以设置为每分钟检查一次实现近乎实时的监控。而且数据直接来自源头公司官网、招聘平台避免了第三方平台可能存在的信息延迟或遗漏。最后隐私与数据安全。你的求职意向、关注的公司列表是高度敏感的个人信息。使用自建服务所有这些配置和数据都运行在你自己的服务器或电脑上无需担心信息泄露给第三方。offercatcher的技术栈选择也体现了其实用主义导向。它主要基于 Python利用requests、BeautifulSoup、Selenium等库来抓取和解析网页即“爬虫”部分再通过smtplib邮件、requests调用各类Webhook等方式进行通知。整个架构是模块化的核心流程可以概括为配置加载 - 定时触发 - 多平台抓取 - 数据解析去重 - 条件过滤 - 多渠道通知。2.2 核心模块功能解析一个完整的offercatcher实例通常包含以下几个核心模块配置管理器这是项目的大脑。它负责读取你的配置文件通常是config.yaml或config.json里面定义了所有监控任务。每个任务会包含目标URL需要监控的招聘列表页面地址。抓取规则如何从页面HTML中提取职位名称、公司、链接、发布日期等关键信息通常使用CSS选择器或XPath。过滤条件对抓取到的职位进行筛选例如关键词匹配、排除特定公司等。通知渠道设置邮件、Telegram Bot Token、钉钉Webhook地址等。爬虫调度器这是项目的心脏。它通常基于schedule或apscheduler这样的库实现定时任务。调度器按照配置的时间间隔例如每30分钟依次触发各个监控任务的抓取流程。网页抓取与解析器这是项目的手和眼睛。针对不同的网站可能需要不同的抓取策略。对于静态页面直接使用requests获取HTML再用BeautifulSoup解析即可。对于动态加载大量使用JavaScript的页面则需要启动Selenium模拟浏览器行为等待页面元素加载完成后再进行解析。这部分是开发中最复杂、最需要针对不同网站单独适配的地方。数据处理器负责对抓取到的原始数据进行清洗、去重和过滤。去重是关键它通过比对本次抓取和历史上次抓取的职位ID或链接确保不会重复通知同一个职位。过滤则根据配置的关键词进行匹配只保留你感兴趣的职位。通知发送器这是项目的嘴巴。将处理后的、符合条件的新职位信息按照配置的格式如Markdown、纯文本通过不同的渠道发送出去。模块化设计使得新增一个通知渠道如飞书、企业微信变得相对容易。注意在设计和运行爬虫时必须严格遵守目标网站的robots.txt协议并设置合理的请求间隔如每次请求间隔2-5秒避免对目标网站服务器造成压力这既是法律和道德要求也能保证你的爬虫长期稳定运行。3. 从零开始搭建你的第一个职位监控器3.1 环境准备与项目初始化假设你已经在本地或一台云服务器上准备好了 Python 环境建议使用 Python 3.8接下来我们一步步搭建。首先克隆项目代码并安装依赖。通常这类项目的依赖会放在requirements.txt文件中。# 克隆项目仓库 git clone https://github.com/NissonCX/offercatcher.git cd offercatcher # 创建并激活虚拟环境推荐避免污染系统环境 python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装依赖 pip install -r requirements.txt典型的requirements.txt会包含以下核心库requests2.25.1 beautifulsoup44.9.3 schedule1.1.0 python-dotenv0.19.0 PyYAML5.4.1 # 如果需要抓取动态页面 selenium4.0.0如果项目没有提供requirements.txt你也可以根据其源码中import的语句手动安装这些库。3.2 核心配置文件详解配置是整个项目的灵魂。我们需要创建一个配置文件比如config.yaml来告诉offercatcher监控什么、如何通知。# config.yaml 示例 global: check_interval_minutes: 30 # 全局检查间隔单位分钟 storage_file: ./data/jobs.json # 用于存储历史职位数据实现去重 notifiers: email: enabled: true smtp_server: smtp.gmail.com # SMTP服务器地址 smtp_port: 587 username: your_emailgmail.com # 发送邮件的邮箱 password: your_app_specific_password # 注意使用应用专用密码非邮箱登录密码 sender: your_emailgmail.com receivers: - your_target_emailexample.com # 接收通知的邮箱 telegram: enabled: false # 按需开启 bot_token: YOUR_BOT_TOKEN chat_id: YOUR_CHAT_ID targets: - name: A公司官网-技术岗位 url: https://careers.example-a.com/jobs parser: css # 使用CSS选择器解析 selectors: job_item: div.job-listing # 职位列表项的CSS选择器 title: h3.job-title a # 职位标题 link: h3.job-title ahref # 职位链接href表示取href属性 company: div.company-name::text # 公司名 publish_date: span.date::text filters: include_keywords: [后端, 开发, 工程师, Python, Java] exclude_keywords: [前端, 实习] notifiers: [email] # 使用哪个通知器 - name: B招聘平台-机器学习 url: https://www.example-b.com/search?keyword机器学习 parser: selenium # 动态页面使用Selenium selectors: # ... 类似上述选择器需要根据实际页面结构调整 wait_condition: # Selenium等待条件 type: presence_of_element_located selector: div.job-list filters: min_salary: 30000 # 可选过滤最低薪资 notifiers: [email]配置关键点解析selectors这是配置中最核心也最易出错的部分。你需要使用浏览器的开发者工具F12仔细分析目标网页的HTML结构找到能唯一标识一个职位条目以及其内部各个字段标题、链接等的CSS选择器或XPath。这个过程需要一些耐心和反复测试。应用专用密码对于Gmail等邮箱如果开启了两步验证不能直接使用登录密码需要在邮箱设置中生成“应用专用密码”来填写。去重存储storage_file指定的JSON文件会保存所有抓取到的职位ID/链接。每次运行时会先读取历史数据只对新出现的职位进行通知。3.3 编写与调试爬虫解析规则对于静态页面我们可以写一个简单的测试脚本来验证选择器是否正确。# test_parser.py import requests from bs4 import BeautifulSoup url https://careers.example-a.com/jobs headers {User-Agent: Mozilla/5.0} # 模拟浏览器请求头 resp requests.get(url, headersheaders) soup BeautifulSoup(resp.content, html.parser) # 使用配置中的选择器进行测试 job_items soup.select(div.job-listing) print(f找到 {len(job_items)} 个职位条目) for item in job_items[:2]: # 只看前两个 title_elem item.select_one(h3.job-title a) title title_elem.text.strip() if title_elem else N/A link title_elem[href] if title_elem else # company_elem item.select_one(div.company-name) company company_elem.text.strip() if company_elem else N/A print(f标题: {title}) print(f链接: {link}) print(f公司: {company}) print(- * 30)运行这个脚本如果它能正确打印出职位信息说明你的选择器是有效的。如果返回空列表你需要重新检查网页HTML结构可能页面是动态加载的或者选择器写错了。对于动态页面调试会更复杂一些。你可能需要启动一个Selenium浏览器实例在交互环境中逐步执行代码查看页面加载后的实际DOM结构。实操心得在编写选择器时尽量选择那些具有唯一性和稳定性的属性比如>import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session(): session requests.Session() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } session.headers.update(headers) # 配置重试策略应对网络波动 retries Retry(total3, backoff_factor1, status_forcelist[500, 502, 503, 504]) session.mount(http://, HTTPAdapter(max_retriesretries)) session.mount(https://, HTTPAdapter(max_retriesretries)) return session # 使用会话进行请求 session create_session() try: response session.get(target_url, timeout10) response.raise_for_status() # 如果状态码不是200抛出异常 except requests.exceptions.RequestException as e: print(f请求失败: {e}) # 这里可以加入重试或通知管理员逻辑2. 智能去重策略简单的基于链接去重可能不够因为有些网站会为同一个职位生成不同的追踪链接。更健壮的去重策略是结合职位标题、公司名称和职位唯一ID如果存在生成一个哈希值作为唯一标识。import hashlib import json def get_job_signature(job): 根据职位信息生成唯一签名 # 优先使用网站提供的唯一ID unique_id job.get(job_id) or job.get(id) if unique_id: return unique_id # 否则使用标题公司生成哈希 content f{job[title]}_{job[company]} return hashlib.md5(content.encode(utf-8)).hexdigest() def filter_new_jobs(fetched_jobs, history_file./data/jobs.json): 过滤出新的职位 try: with open(history_file, r, encodingutf-8) as f: history json.load(f) except (FileNotFoundError, json.JSONDecodeError): history [] history_signatures {job[signature] for job in history} new_jobs [] for job in fetched_jobs: job[signature] get_job_signature(job) if job[signature] not in history_signatures: new_jobs.append(job) history.append({signature: job[signature], fetched_at: 2023-10-01}) # 记录获取时间 # 保存更新后的历史记录可限制最大保存数量避免文件过大 with open(history_file, w, encodingutf-8) as f: json.dump(history[-1000:], f, ensure_asciiFalse, indent2) # 只保留最近1000条 return new_jobs4.2 构建灵活的通知系统一个实用的通知系统应该支持多种渠道并且消息模板要清晰易读。1. 邮件通知增强除了发送纯文本我们可以发送格式更友好的HTML邮件并支持附件例如将本次所有新职位生成一个CSV文件附上。import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication import pandas as pd from datetime import datetime def send_email_with_html_and_attachment(smtp_config, new_jobs): 发送带HTML内容和附件的邮件 msg MIMEMultipart() msg[From] smtp_config[sender] msg[To] , .join(smtp_config[receivers]) msg[Subject] f【职位监控】发现 {len(new_jobs)} 个新职位 - {datetime.now().strftime(%Y-%m-%d %H:%M)} # 1. 构建HTML正文 html_content html body h2发现的新职位如下/h2 table border1 cellpadding5 styleborder-collapse: collapse; trth公司/thth职位/thth链接/thth发布时间/th/tr for job in new_jobs: html_content f tr td{job.get(company, N/A)}/td tdstrong{job.get(title, N/A)}/strong/td tda href{job.get(link, #)}查看详情/a/td td{job.get(publish_date, N/A)}/td /tr html_content /table p详情请查看附件CSV文件。/p /body /html msg.attach(MIMEText(html_content, html)) # 2. 构建并附加CSV文件 if new_jobs: df pd.DataFrame(new_jobs) csv_buffer df.to_csv(indexFalse).encode(utf-8-sig) # 处理中文编码 attachment MIMEApplication(csv_buffer) attachment.add_header(Content-Disposition, attachment, filenamefnew_jobs_{datetime.now().strftime(%Y%m%d_%H%M)}.csv) msg.attach(attachment) # 3. 发送邮件 try: with smtplib.SMTP(smtp_config[smtp_server], smtp_config[smtp_port]) as server: server.starttls() # 安全连接 server.login(smtp_config[username], smtp_config[password]) server.send_message(msg) print(邮件发送成功) except Exception as e: print(f邮件发送失败: {e})2. 集成即时通讯工具对于Telegram、钉钉、企业微信等通常通过调用它们的Webhook或Bot API来实现。以钉钉群机器人为例import requests import json def send_dingtalk_message(webhook_url, new_jobs): 发送钉钉群机器人通知Markdown格式 if not new_jobs: return title f 发现 {len(new_jobs)} 个新职位 text f### {title}\n\n for idx, job in enumerate(new_jobs[:10], 1): # 最多显示10条避免消息过长 text f{idx}. **{job.get(company)}** - [{job.get(title)}]({job.get(link)})\n if len(new_jobs) 10: text f\n 还有 {len(new_jobs) - 10} 个职位未显示请查看完整通知。 data { msgtype: markdown, markdown: { title: title, text: text } } headers {Content-Type: application/json} try: resp requests.post(webhook_url, datajson.dumps(data), headersheaders, timeout5) if resp.json().get(errcode) ! 0: print(f钉钉消息发送失败: {resp.text}) except Exception as e: print(f钉钉请求异常: {e})4.3 部署与长期运行方案让offercatcher在后台稳定运行有以下几种常见方案方案一云服务器 Systemd/Cron推荐这是最稳定、最像生产环境的方式。购买一台低配的云服务器如腾讯云、阿里云的轻量应用服务器将项目部署上去。使用 Systemd 服务Linux 创建一个服务文件/etc/systemd/system/offercatcher.service[Unit] DescriptionOffercatcher Job Monitor Afternetwork.target [Service] Typesimple Useryour_username WorkingDirectory/path/to/offercatcher EnvironmentPATH/usr/local/bin:/usr/bin ExecStart/path/to/venv/bin/python /path/to/offercatcher/main.py Restartalways # 崩溃后自动重启 RestartSec10 [Install] WantedBymulti-user.target然后使用sudo systemctl start offercatcher启动sudo systemctl enable offercatcher设置开机自启。使用 Crontab 如果项目是单次运行脚本可以用Cron定时触发。# 编辑当前用户的crontab crontab -e # 添加一行每30分钟运行一次并重定向日志 */30 * * * * cd /path/to/offercatcher /path/to/venv/bin/python main.py /tmp/offercatcher.log 21方案二本地电脑 计划任务Windows/ LaunchdMac适合不想购买服务器的用户但需要保证电脑长期开机。Windows使用“任务计划程序”创建一个定时触发的基本任务操作设置为启动你的Python脚本。Mac使用launchd创建守护进程类似于Linux的Systemd。方案三使用云函数/Serverless对于有一定技术背景的用户可以将抓取逻辑打包成云函数如阿里云函数计算、腾讯云SCF并配置定时触发器。这种方式无需管理服务器按量计费成本极低但调试和依赖管理相对复杂一些。实操心得无论选择哪种部署方式日志记录都至关重要。确保你的脚本将运行状态、抓取到的职位数量、错误信息等都输出到日志文件或日志服务中方便后期排查问题。例如可以使用Python内置的logging模块配置同时输出到控制台和文件。5. 常见问题排查与优化实录即使代码写得再完善在实际运行中也会遇到各种问题。下面是我在长期使用和调试offercatcher这类工具中积累的一些典型问题与解决方案。5.1 抓取失败问题排查表问题现象可能原因排查步骤与解决方案请求返回403/404错误1. 网站反爬虫检测到非常规UA或请求频率过高2. URL已失效或需要登录3. 请求头不完整1.检查并完善请求头添加User-Agent,Referer,Accept-Language等模拟浏览器。使用session保持会话。2.降低请求频率在请求间增加随机延时如time.sleep(random.uniform(2, 5))。3.手动浏览器访问确认URL在浏览器中可正常打开无需登录。解析器返回空数据但页面在浏览器有内容1. 页面内容为JavaScript动态加载SPA2. CSS选择器/XPath写错或已过时3. 需要处理分页1.判断是否为动态页面查看网页源代码CtrlU搜索职位信息关键词。若源码中没有则是动态加载。2.使用Selenium换用selenium配合WebDriverWait等待元素加载。3.重新审查选择器使用浏览器开发者工具F12的“检查”功能重新定位元素并更新选择器。能抓到数据但重复通知同一个职位去重逻辑失效1.检查去重标识确认用于生成signature的字段是否稳定唯一。优先使用职位ID其次是“标题公司”的组合。2.检查历史存储文件查看storage_file指定的JSON文件是否被正确读写内容是否正常。3.清理历史文件有时文件损坏会导致去重失败可以尝试备份后删除该文件让系统重新生成。邮件/通知发送失败1. 邮箱SMTP配置错误密码、端口2. 网络问题或发送频率被限制3. Webhook地址或Token错误1.测试SMTP连接写一个独立的测试脚本只测试邮件发送功能。2.查看错误日志Python的smtplib或requests库会抛出具体的异常信息根据提示排查。3.检查接收端确认Telegram Bot已添加到群组并获取了正确的chat_id钉钉机器人Webhook地址无误。程序运行一段时间后崩溃1. 内存泄漏如Selenium驱动未关闭2. 未处理的异常导致进程退出3. 依赖库版本冲突1.完善异常捕获在任务执行的最外层添加try...except记录错误并继续下一个任务避免整个程序崩溃。2.资源清理确保Selenium WebDriver在每次使用后调用driver.quit()。3.查看系统日志使用journalctl -u offercatcherSystemd服务或直接查看程序输出的日志文件定位崩溃前的最后一条错误信息。5.2 性能与稳定性优化技巧异步抓取提升效率如果你的监控目标很多顺序抓取会非常慢。可以考虑使用asyncio和aiohttp库进行异步HTTP请求能极大缩短整体抓取时间。但需要注意目标网站的并发压力适当控制并发数。引入代理IP池对于反爬策略严格的网站单一IP频繁请求容易被封。可以考虑使用付费或免费的代理IP服务在请求时随机切换IP。实现时可以构建一个代理IP列表并在请求失败时自动切换。配置热重载每次修改配置文件都需要重启服务可以实现一个信号监听或定期检查配置文件最后修改时间的功能当发现配置变更时自动重新加载配置而无需中断正在运行的监控任务。实现健康检查与告警除了监控职位你的监控程序本身也需要被监控。可以添加一个简单的“心跳”功能定期向一个健康检查接口报告状态或者发送“我还活着”的定时通知。如果长时间没有收到心跳则说明程序可能已挂掉需要触发更高级别的告警如短信。数据持久化与备份历史职位数据文件 (jobs.json) 是去重的关键。定期备份这个文件或者考虑使用更轻量的数据库如SQLite来存储可以提高读写效率和可靠性。5.3 应对网站改版招聘网站的前端改版是爬虫最大的天敌。应对策略如下监控与告警在抓取逻辑中加入对“空结果”的检测。如果连续几次抓取某个目标都返回空数据而你的选择器又没改过那很可能是网站改版了。此时应触发一个紧急通知如发送邮件到你的个人邮箱提醒你手动检查。选择器冗余设计为关键字段如职位标题、链接准备多个备选选择器。解析时按顺序尝试直到有一个成功为止。这能在网站进行小幅调整时提供一定的容错能力。将解析规则外部化不要将CSS选择器硬编码在代码里。可以将每个网站的解析规则选择器、等待条件等独立成一个配置文件如parsers/company_a.yaml。当网站改版时你只需要更新这个配置文件而无需修改主程序代码甚至可以实现规则的动态加载。搭建和维护一个属于自己的offercatcher初期需要投入一些时间进行配置和调试但一旦稳定运行它将成为你求职路上一个无声却强大的助手。它能帮你从被动的信息搜寻者转变为主动的机会捕捉者。最重要的是这个过程本身也是对Python自动化、网络请求、数据处理等技能的一次绝佳实践。当你收到第一封由自己编写的程序发来的职位推荐邮件时那种成就感是无可替代的。开始动手定制你的信息雷达吧。