当前位置: 首页 > article >正文

Python爬虫实战:构建智能职位信息聚合工具JobClaw

1. 项目概述一个面向开发者的智能职位信息聚合与解析工具最近在帮团队招聘和看机会的朋友聊天发现一个挺普遍的问题大家找技术岗位要么在几个主流招聘App上反复刷信息分散且格式不一要么就是盯着几个大厂的官网效率很低。尤其是对于需要精准匹配技术栈和薪资范围的开发者来说这个过程既耗时又容易遗漏好机会。我自己也经历过这个阶段所以一直想能不能有个工具能像爬虫一样把散落在各处的职位信息“抓”到一起然后按照我们开发者习惯的方式——比如按技术栈、薪资、经验要求——重新组织和呈现这就是JobClaw诞生的背景。简单说它是一个用 Python 编写的、高度可定制的职位信息爬取与聚合工具。它的核心目标不是替代招聘平台而是为开发者、技术招聘者或任何对技术岗位市场动态感兴趣的人提供一个数据抓手。你可以把它看作一个为你私人订制的、7x24小时不间断的“岗位雷达”。它能够从多个预设的招聘数据源如主流招聘网站、技术社区招聘版块等自动抓取职位信息经过清洗、去重和关键信息如职位名称、公司、薪资、地点、技能要求、发布时间提取后结构化地存储起来方便你进行筛选、分析和长期追踪。这个工具特别适合以下几类人主动求职的开发者可以设置自己关心的技术关键词如“Go”、“Rust”、“分布式存储”、薪资范围和地点让 JobClaw 帮你监控新出现的岗位不错过任何机会。技术团队负责人或HR用于做市场薪酬调研了解特定技术栈在市场上的需求热度、薪资水位为招聘定价和人才画像提供数据支持。自由职业者或技术顾问用来发现短期的项目制岗位或合约机会。对技术趋势感兴趣的人通过长期爬取和分析职位描述中的技术关键词可以直观地看到哪些技术在崛起哪些在逐渐淡出成为一个有趣的技术风向标。接下来我会详细拆解 JobClaw 的设计思路、核心实现以及我在搭建和使用过程中积累的一些实战经验。你会发现它不仅仅是一个爬虫脚本的集合更涉及到了任务调度、数据清洗、反爬策略应对和简易数据服务等多个工程化层面的思考。2. 核心架构与设计思路拆解在动手写第一行代码之前明确架构和设计思路至关重要。JobClaw 的目标是稳定、可扩展、易维护而不是一个跑一次就扔的脚本。我的核心设计原则是模块化、配置化、松耦合。2.1 为什么选择模块化设计一个常见的误区是把针对不同网站比如A平台、B平台的爬虫逻辑全部写在一个巨大的、面条式的代码文件里。这会导致几个问题1代码难以阅读和维护2某个网站的页面结构变动会波及整个脚本3无法灵活地启用或禁用某个数据源。因此JobClaw 采用了“爬虫插件”的设计模式。整个系统由几个核心模块组成调度中心 (Scheduler)负责任务的定时触发与协调。它决定什么时候启动爬取任务调用哪个爬虫插件以及如何处理爬取结果。我选择了APScheduler这个库来实现它轻量且功能强大支持基于日期、固定时间间隔以及Cron风格的定时任务非常适合这种后台定时作业的场景。爬虫插件 (Spider Plugins)这是核心。每个招聘数据源如拉勾、BOSS直聘、某技术论坛都对应一个独立的爬虫类。它们继承自一个基础的BaseSpider抽象类必须实现fetch_jobs()等方法。这样新增一个数据源只需要新建一个插件文件实现具体逻辑即可完全不影响其他部分。数据管道 (Data Pipeline)爬虫插件抓取到的原始数据通常是HTML或JSON是杂乱无章的。数据管道负责解析、清洗和结构化。例如从HTML中提取职位标题、公司名、薪资字符串将“15k-30k”这样的字符串解析为min_salary和max_salary两个整数字段统一城市名称的格式如“北京”和“北京市”统一为“北京”。存储层 (Storage)结构化后的数据需要持久化。为了灵活性和快速原型开发我同时支持了两种方式SQLite 数据库轻便无需安装额外服务适合个人单机使用。用于存储最终的职位信息。JSON 文件作为中间缓存或备份便于调试和手动检查原始数据。配置与日志 (Config Logging)所有可配置项如定时任务周期、目标网站列表、关键词过滤规则、数据库路径等都集中在一个配置文件中如config.yaml。日志系统则详细记录运行状态、错误信息是排查问题的第一手资料。注意在设计之初就考虑好日志的级别INFO, WARNING, ERROR和输出格式文件控制台这在后期排查一些偶发的、与网站反爬策略相关的问题时能救命。2.2 技术栈选型背后的考量编程语言Python这几乎是爬虫领域的“官方语言”。生态丰富拥有requests,BeautifulSoup4,lxml,Scrapy等无数成熟的库开发效率极高。对于 JobClaw 这种重IO、轻计算的任务Python 非常合适。HTTP 客户端Requests 会话维持Requests库简单易用。关键在于使用Session对象来维持会话可以自动处理 Cookies模拟更真实的浏览器行为对于需要登录或有多步跳转的网站很有帮助。HTML 解析BeautifulSoup4 为主lxml 为辅BeautifulSoup4的 API 非常友好适合快速开发和解析结构相对规范的页面。对于超大型页面或极致性能要求可以切换到lxml作为解析后端两者可以无缝结合。任务调度APScheduler相比自己用time.sleep和循环来实现定时APScheduler提供了更专业、更可靠的任务管理支持持久化存储任务这样重启程序后任务还在并且有丰富的触发策略。数据存储SQLite SQLAlchemy ORMSQLite 是单文件数据库部署简单。使用SQLAlchemy这个 ORM对象关系映射工具可以用 Python 类来定义数据表用面向对象的方式操作数据库大大提升了代码的可读性和可维护性。未来如果数据量激增迁移到 PostgreSQL 或 MySQL 也相对容易。反爬应对基础随机User-Agent、请求延迟这是伦理和可持续性的关键。必须在代码中模拟人类行为每次请求使用随机的浏览器 User-Agent 头在连续请求之间插入随机的、合理的延迟如 3-10 秒尊重网站的robots.txt规则。这些是基本的“礼貌爬虫”守则。3. 核心模块实现细节与实操要点理解了整体架构我们深入看看几个核心模块的具体实现和需要注意的“坑”。3.1 基础爬虫类 (BaseSpider) 的设计定义一个所有具体爬虫都必须遵守的“契约”这是保证系统扩展性的关键。# base_spider.py import abc import logging from typing import List, Dict, Any import requests from fake_useragent import UserAgent class BaseSpider(abc.ABC): 所有具体爬虫的基类 def __init__(self, name: str, start_urls: List[str]): self.name name # 爬虫名称如 lagou_spider self.start_urls start_urls self.session requests.Session() self.ua UserAgent() self.logger logging.getLogger(self.name) # 设置会话的默认请求头 self.session.headers.update({ Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) def _get_random_headers(self) - Dict[str, str]: 生成随机的请求头 return { User-Agent: self.ua.random, Referer: https://www.google.com/, # 可以随机或设置为目标网站的域名 } def fetch_page(self, url: str, **kwargs) - str: 获取页面内容内置基础的反爬策略和错误处理 headers self._get_random_headers() try: # 重要添加随机延迟避免请求过快 time.sleep(random.uniform(2, 5)) resp self.session.get(url, headersheaders, timeout10, **kwargs) resp.raise_for_status() # 如果状态码不是200抛出HTTPError异常 # 检查编码避免乱码 resp.encoding resp.apparent_encoding or utf-8 return resp.text except requests.exceptions.RequestException as e: self.logger.error(f请求 {url} 失败: {e}) return abc.abstractmethod def parse_jobs(self, html: str) - List[Dict[str, Any]]: 解析HTML提取职位信息列表。子类必须实现此方法。 pass def run(self) - List[Dict[str, Any]]: 运行爬虫的主流程 all_jobs [] for url in self.start_urls: self.logger.info(f开始抓取: {url}) html self.fetch_page(url) if not html: continue jobs self.parse_jobs(html) all_jobs.extend(jobs) self.logger.info(f从 {url} 解析到 {len(jobs)} 个职位) return all_jobs实操要点异常处理要细致网络请求可能因为超时、连接错误、对方服务器错误等失败。fetch_page方法必须捕获requests.exceptions.RequestException及其子类异常并记录日志而不是让整个程序崩溃。编码问题网页编码千奇百怪。使用resp.apparent_encoding由requests库推测的编码作为首选回退到utf-8能解决大部分乱码问题。抽象方法parse_jobs这是每个网站解析逻辑不同的地方所以设计为抽象方法强制子类实现。返回的是一个字典列表每个字典代表一个职位。3.2 具体爬虫实现示例以解析静态列表页为例假设我们要抓取一个技术博客的招聘板块其列表页结构相对简单。# simple_blog_spider.py from bs4 import BeautifulSoup from base_spider import BaseSpider import re class SimpleBlogSpider(BaseSpider): def __init__(self): # 假设博客招聘版块有分页构造起始URL列表 start_urls [fhttps://example-tech-blog.com/jobs/page/{i} for i in range(1, 6)] super().__init__(namesimple_blog, start_urlsstart_urls) def parse_jobs(self, html: str) - List[Dict[str, Any]]: jobs [] soup BeautifulSoup(html, html.parser) # 假设每个职位信息在一个 class 为 job-item 的 div 中 job_elements soup.find_all(div, class_job-item) for elem in job_elements: try: # 提取标题和链接 title_elem elem.find(h2).find(a) job_title title_elem.get_text(stripTrue) job_url title_elem[href] # 处理相对链接 if job_url.startswith(/): job_url https://example-tech-blog.com job_url # 提取公司名 - 假设在 class 为 company 的 span 里 company elem.find(span, class_company).get_text(stripTrue) # 提取薪资 - 通常是一个字符串需要后续清洗 salary_text elem.find(span, class_salary).get_text(stripTrue) # 提取地点和标签 location elem.find(span, class_location).get_text(stripTrue) tags [tag.get_text(stripTrue) for tag in elem.find_all(span, class_tag)] # 构造职位信息字典 job_info { title: job_title, company: company, salary_raw: salary_text, # 保存原始字符串供管道清洗 location: location, tags: ,.join(tags), # 将列表转为字符串存储 url: job_url, source: self.name, crawled_time: datetime.now().isoformat() # 抓取时间 } jobs.append(job_info) except (AttributeError, KeyError) as e: # 如果某个元素找不到记录警告并跳过该条目不影响其他职位 self.logger.warning(f解析职位元素时出错跳过: {e}) continue return jobs注意事项页面结构变化这是爬虫最大的敌人。今天classjob-item明天可能就变成了classjob-listing。因此解析逻辑要健壮使用try...except包裹可能出错的提取部分。数据缺失处理不是每个职位都有薪资或地点信息。代码中如果elem.find(span, class_salary)返回None再调用.get_text()就会抛出AttributeError。要做好防御性编程或者提供默认值。相对链接转绝对链接这是一个非常容易忽略的细节。href属性可能是/jobs/123这样的相对路径需要拼接上网站的根域名才能形成有效的完整URL。3.3 数据管道从原始文本到结构化数据爬虫返回的salary_raw字段可能是“面议”、“15k-30k·13薪”、“20k以上”等多种格式。数据管道的任务就是将这些“脏数据”清洗干净。# pipeline.py import re class SalaryParser: 专门用于解析薪资字符串的类 staticmethod def parse(salary_text: str) - Dict[str, Any]: 将薪资字符串解析为最小、最大月薪和薪资单位。 支持格式: ‘15k-30k‘, ‘20k以上‘, ‘面议‘, ‘15k-30k·13薪‘ salary_text salary_text.strip().lower() result {min_monthly: None, max_monthly: None, unit: 月, notes: salary_text} if 面议 in salary_text: return result # 提取数字和单位处理 ‘k‘ 代表千 # 正则匹配类似 15k, 15.5k, 20-30k, 20k以上 pattern r(\d(?:\.\d)?)\s*(?:k|千)? numbers re.findall(pattern, salary_text.replace(,, )) nums [float(n) for n in numbers] if len(nums) 1: # 类似 ‘20k以上‘ if 以上 in salary_text or up in salary_text: result[min_monthly] nums[0] * 1000 result[max_monthly] None # 表示上不封顶 else: # 可能是固定薪资如 ‘25k‘ result[min_monthly] result[max_monthly] nums[0] * 1000 elif len(nums) 2: # 通常第一个是最小第二个是最大 result[min_monthly] nums[0] * 1000 result[max_monthly] nums[1] * 1000 # 判断是否是年薪 if 年 in salary_text or 年薪 in salary_text: result[unit] 年 # 如果之前是按月解析的需要转换假设按12个月算实际情况更复杂 if result[min_monthly]: result[min_monthly] * 12 if result[max_monthly]: result[max_monthly] * 12 # 处理13薪、14薪等 elif 薪 in salary_text: # 例如 ‘15k-30k·13薪‘ 这里的13薪是额外信息可以在notes里或单独字段 result[notes] salary_text return result class DataPipeline: 数据处理管道 def __init__(self): self.salary_parser SalaryParser() def process_item(self, job_item: Dict[str, Any]) - Dict[str, Any]: 清洗和增强单个职位信息 # 1. 清洗公司名去除多余空格和特殊字符 job_item[company] job_item[company].strip() # 2. 解析薪资 salary_info self.salary_parser.parse(job_item.pop(salary_raw, )) job_item.update(salary_info) # 3. 统一地点格式例如将 ‘北京市‘ 转为 ‘北京‘ location job_item.get(location, ) if location.endswith(市): job_item[location] location.rstrip(市) # 4. 从标签或描述中提取技术关键词简化示例 tags job_item.get(tags, ) description job_item.get(description, ) full_text tags description # 这里可以定义一个技术关键词列表进行匹配 tech_keywords self._extract_tech_keywords(full_text) job_item[tech_keywords] ,.join(tech_keywords) # 5. 生成一个唯一ID用于去重例如基于标题、公司、地点的哈希 unique_str f{job_item[title]}_{job_item[company]}_{job_item[location]} job_item[job_id] hashlib.md5(unique_str.encode()).hexdigest() return job_item def _extract_tech_keywords(self, text: str) - List[str]: # 这里可以维护一个大的技术栈词典进行匹配 predefined_techs [python, java, golang, react, vue, docker, kubernetes, aws, mysql, redis] found [] for tech in predefined_techs: if re.search(rf\b{tech}\b, text, re.IGNORECASE): found.append(tech) return found实操心得薪资解析是难点现实中的薪资格式五花八门还有年薪、时薪、面议、薪资范围带奖金说明等。SalaryParser类不可能覆盖所有情况但可以处理80%的常见格式。对于无法解析的保留原始文本在notes字段供人工查看。去重策略基于title,company,location生成哈希ID是一个简单有效的去重方法。但更高级的去重可能需要考虑语义相似度比如同一家公司发布的“高级后端工程师”和“后端开发专家”可能是同一个职位这需要引入NLP技术复杂度会大大增加。初期用简单规则即可。技术关键词提取这是一个非常有价值的增强功能。简单的字符串匹配如上面的示例容易误判比如“Java”和“JavaScript”。更准确的做法可以使用正则表达式确保单词边界或者引入更专业的技能实体识别模型。初期可以从一个精心维护的、分门别类的技术关键词列表开始。4. 任务调度、存储与简易查询服务数据抓取和清洗好了如何让它自动运行并把数据存起来、用起来呢4.1 使用 APScheduler 实现自动化爬取# scheduler.py from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger import logging from spiders import SimpleBlogSpider, AnotherSpider # 导入你的爬虫 from pipeline import DataPipeline from storage import DatabaseManager def job_crawler(): 定时任务要执行的函数 logger logging.getLogger(scheduler) logger.info(开始执行定时爬取任务...) # 1. 初始化组件 spiders [SimpleBlogSpider(), AnotherSpider()] pipeline DataPipeline() db_manager DatabaseManager() all_processed_jobs [] # 2. 运行所有爬虫 for spider in spiders: try: raw_jobs spider.run() for job in raw_jobs: processed_job pipeline.process_item(job) all_processed_jobs.append(processed_job) except Exception as e: logger.error(f爬虫 {spider.name} 执行失败: {e}, exc_infoTrue) # 3. 存储到数据库 if all_processed_jobs: new_count db_manager.bulk_save_jobs(all_processed_jobs) logger.info(f本次爬取完成新增 {new_count} 个职位记录。) else: logger.warning(本次爬取未获取到任何职位信息。) def main(): # 配置日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(jobclaw.log), logging.StreamHandler()]) scheduler BlockingScheduler() # 添加定时任务每天上午9点和下午5点各运行一次 scheduler.add_job(job_crawler, CronTrigger(hour9,17, minute0)) # 也可以添加间隔任务每4小时运行一次 # scheduler.add_job(job_crawler, interval, hours4) logger.info(JobClaw 调度器已启动按 CtrlC 退出。) try: scheduler.start() except (KeyboardInterrupt, SystemExit): logger.info(JobClaw 调度器已停止。) if __name__ __main__: main()关键点BlockingScheduler这是一个阻塞式的调度器适合将整个脚本作为一个独立的守护进程来运行。CronTrigger使用Cron表达式可以非常灵活地设定任务时间比如“工作日早上9点”、“每小时的第30分钟”等。异常处理每个爬虫的执行要用try...except包裹避免一个爬虫出错导致整个任务中断。exc_infoTrue会将完整的异常堆栈信息记录到日志便于调试。日志将日志同时输出到文件和控制台方便后续追踪。4.2 使用 SQLAlchemy 定义数据模型并存储# models.py from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base declarative_base() class JobPosition(Base): 职位信息数据表模型 __tablename__ job_positions id Column(Integer, primary_keyTrue, autoincrementTrue) job_id Column(String(64), uniqueTrue, nullableFalse, indexTrue) # 我们生成的唯一ID用于去重 title Column(String(255), nullableFalse) company Column(String(255), nullableFalse) min_monthly Column(Float) # 最低月薪元 max_monthly Column(Float) # 最高月薪元 salary_unit Column(String(10), default月) # 薪资单位月/年 salary_notes Column(Text) # 薪资原始说明 location Column(String(100)) tech_keywords Column(Text) # 逗号分隔的技术关键词 url Column(String(500), uniqueTrue) # 职位链接 source Column(String(50)) # 数据来源如 ‘simple_blog‘ published_date Column(String(50)) # 原网页上的发布日期 crawled_time Column(DateTime, defaultdatetime.now) # 我们抓取的时间 def __repr__(self): return fJobPosition(title{self.title}, company{self.company}) # storage.py from sqlalchemy.exc import IntegrityError import logging class DatabaseManager: def __init__(self, db_pathjobs.db): self.engine create_engine(fsqlite:///{db_path}) # 如果表不存在则创建 Base.metadata.create_all(self.engine) self.Session sessionmaker(bindself.engine) self.logger logging.getLogger(storage) def bulk_save_jobs(self, jobs_data: List[Dict]) - int: 批量保存职位信息返回新增记录数 session self.Session() new_count 0 try: for job_data in jobs_data: # 根据唯一ID判断是否已存在 existing session.query(JobPosition).filter_by(job_idjob_data[job_id]).first() if existing: # 可以在这里选择更新已有记录例如更新crawled_time existing.crawled_time datetime.now() continue # 创建新记录 job JobPosition(**job_data) session.add(job) new_count 1 session.commit() self.logger.info(f成功保存 {new_count} 条新记录。) except IntegrityError as e: session.rollback() self.logger.error(f数据库完整性错误可能重复: {e}) except Exception as e: session.rollback() self.logger.error(f保存数据时发生未知错误: {e}, exc_infoTrue) finally: session.close() return new_count def query_jobs(self, keywordNone, locationNone, techNone, days7): 查询职位信息 session self.Session() query session.query(JobPosition) # 按抓取时间过滤最近N天的 from datetime import timedelta time_threshold datetime.now() - timedelta(daysdays) query query.filter(JobPosition.crawled_time time_threshold) if keyword: # 在标题或公司名中搜索 query query.filter( (JobPosition.title.contains(keyword)) | (JobPosition.company.contains(keyword)) ) if location: query query.filter(JobPosition.location.contains(location)) if tech: # 在技术关键词中搜索 query query.filter(JobPosition.tech_keywords.contains(tech)) # 按抓取时间倒序排列 results query.order_by(JobPosition.crawled_time.desc()).all() session.close() return results经验之谈使用 ORM 的好处SQLAlchemy让你用 Python 对象操作数据库避免了手写 SQL 字符串的繁琐和潜在的安全风险如 SQL 注入。JobPosition类清晰定义了表结构。去重逻辑在bulk_save_jobs中我们通过查询job_id来判断是否重复。这是“插入前检查”的模式。对于大规模数据也可以考虑使用数据库的ON CONFLICT DO UPDATEUPSERT语句效率更高但SQLAlchemy对 SQLite 的该语法支持需要特定版本。索引的重要性在job_id和crawled_time上建立索引indexTrue能极大提升查询速度尤其是当数据量积累到数万条以后。4.3 提供一个简易的 CLI 或 Web 查询界面数据存好了最后一步是让它能被方便地查询。一个轻量级的命令行界面CLI是快速验证数据的好方法。# cli.py import argparse from storage import DatabaseManager def main(): parser argparse.ArgumentParser(descriptionJobClaw 命令行查询工具) parser.add_argument(--keyword, -k, help搜索职位标题或公司名) parser.add_argument(--location, -l, help搜索工作地点) parser.add_argument(--tech, -t, help搜索技术关键词) parser.add_argument(--days, -d, typeint, default7, help查看最近几天的数据默认7天) parser.add_argument(--limit, -n, typeint, default20, help显示结果数量限制) args parser.parse_args() db DatabaseManager() jobs db.query_jobs(keywordargs.keyword, locationargs.location, techargs.tech, daysargs.days) print(f\n找到 {len(jobs)} 个相关职位显示前 {min(args.limit, len(jobs))} 个:\n) print(- * 100) for i, job in enumerate(jobs[:args.limit]): salary_display f{job.min_monthly/1000:.1f}k-{job.max_monthly/1000:.1f}k if job.min_monthly and job.max_monthly else job.salary_notes or 面议 print(f{i1}. [{job.source}] {job.title} {job.company}) print(f 地点: {job.location} | 薪资: {salary_display} | 技术: {job.tech_keywords}) print(f 链接: {job.url}) print(f 抓取于: {job.crawled_time.strftime(%Y-%m-%d %H:%M)}) print(- * 100) if __name__ __main__: main()这样你就可以在终端里使用类似python cli.py -k “后端” -l “上海” -t “golang” -d 3的命令来查询最近3天上海地区需要 Go 技术的后端岗位了。如果想更直观可以用Flask或FastAPI花一个小时搭建一个极简的 Web 查询页面通过浏览器访问和筛选。5. 部署、维护与高级话题5.1 部署到服务器为了让 JobClaw 持续运行你需要将它部署到一台稳定的服务器上比如云服务器或家里的树莓派。环境准备在服务器上安装 Python 3.8 和项目依赖 (pip install -r requirements.txt)。进程管理使用Systemd或Supervisor来管理你的爬虫调度进程。这样可以在进程崩溃后自动重启并且方便地查看日志、启动、停止服务。Systemd 服务文件示例 (/etc/systemd/system/jobclaw.service)[Unit] DescriptionJobClaw Job Crawler Scheduler Afternetwork.target [Service] Typesimple Useryour_username WorkingDirectory/path/to/your/jobclaw ExecStart/usr/bin/python3 /path/to/your/jobclaw/scheduler.py Restarton-failure RestartSec10 StandardOutputsyslog StandardErrorsyslog SyslogIdentifierjobclaw [Install] WantedBymulti-user.target然后使用sudo systemctl start jobclaw启动sudo systemctl enable jobclaw设置开机自启。日志管理日志文件会不断增长需要定期清理或使用日志轮转工具如logrotate进行管理。5.2 常见问题与排查技巧在运行 JobClaw 的过程中你肯定会遇到各种问题。以下是一些典型场景和解决思路问题1爬虫突然抓不到数据了返回空列表或错误页面。可能原因1网站页面结构改版。这是最常见的原因。需要更新对应爬虫的parse_jobs方法中的 CSS 选择器或解析逻辑。排查手动访问目标URL用浏览器的开发者工具检查元素对比之前的HTML结构。更新解析代码后单独运行该爬虫进行测试。可能原因2触发了反爬机制。你的 IP 或请求频率被识别为爬虫。排查检查日志中是否有 403、429 状态码或返回的HTML中包含“验证”、“访问过于频繁”等字样。应对显著增加请求延迟如time.sleep(random.uniform(10, 20))。检查并更新User-Agent池确保使用的是最新的、常见的浏览器标识。考虑使用更复杂的请求头模拟更真实的浏览器如Accept-Encoding,Accept-Language,Connection等。重要对于商业网站务必仔细阅读其robots.txt文件和服务条款遵守规则。过度爬取可能导致法律风险。问题2数据库文件越来越大查询变慢。处理定期清理旧数据。可以在DatabaseManager中增加一个方法定期删除比如3个月前的数据session.query(JobPosition).filter(JobPosition.crawled_time cutoff_date).delete()。确保在常用查询字段如job_id,crawled_time,location,tech_keywords上建立了索引。如果数据量真的非常大数十万条以上考虑迁移到 PostgreSQL 或 MySQL。问题3薪资解析错误率高。处理薪资格式太多样。除了不断优化SalaryParser的正则表达式一个更务实的方法是增加容错和人工复核通道。将解析失败的薪资原始文本完整保存下来并标记一个salary_parsed_successfullyFalse的字段。在查询界面可以优先显示解析成功的同时提供查看原始文本的选项。也可以定期检查解析失败的样本来改进解析器。问题4如何扩展新的数据源步骤在spiders/目录下新建一个 Python 文件例如new_site_spider.py。创建一个继承自BaseSpider的类。实现parse_jobs方法编写针对新网站的解析逻辑。在调度器scheduler.py的job_crawler函数中将这个新的爬虫类实例添加到spiders列表中。重启调度服务即可。5.3 进阶可能性JobClaw 的基础框架搭建完成后有很多方向可以深入和扩展动态渲染页面支持很多现代网站使用 JavaScript 动态加载内容。requests只能获取初始HTML。这时需要引入Selenium或Playwright这样的浏览器自动化工具来模拟用户操作获取完整页面。但这会大幅增加资源消耗和复杂度。代理IP池对于反爬严格的网站需要使用代理IP来分散请求。你需要维护一个代理IP池并在请求时随机选用。这涉及到代理IP的获取、验证和轮换。数据可视化将数据库中的数据进行聚合分析用matplotlib或pyecharts生成图表。例如各城市技术岗位数量分布、热门技术栈趋势变化、薪资区间统计等。邮件或消息通知实现一个订阅功能当出现符合特定条件如技术栈、薪资、地点的新职位时自动发送邮件或通过 Telegram/Bark 等工具推送通知。容器化部署使用 Docker 将 JobClaw 及其依赖打包成镜像实现环境隔离和一键部署。构建 JobClaw 的过程远不止是写几个爬虫脚本。它涉及了软件工程的设计模式、数据清洗的复杂性、生产环境部署的可靠性以及对网络伦理的考量。它从一个具体的需求帮开发者更高效地找岗位出发最终演变成一个可扩展、可维护的数据管道项目。无论你是用来服务自己还是作为一个练手项目来深入学习 Python 生态这个过程带来的收获都是实实在在的。最重要的是开始动手做在遇到问题、解决问题的循环中你的能力会得到最快的提升。

相关文章:

Python爬虫实战:构建智能职位信息聚合工具JobClaw

1. 项目概述:一个面向开发者的智能职位信息聚合与解析工具最近在帮团队招聘和看机会的朋友聊天,发现一个挺普遍的问题:大家找技术岗位,要么在几个主流招聘App上反复刷,信息分散且格式不一;要么就是盯着几个…...

5分钟搞定Mac Boot Camp驱动部署:Brigadier全攻略

5分钟搞定Mac Boot Camp驱动部署:Brigadier全攻略 【免费下载链接】brigadier Fetch and install Boot Camp ESDs with ease. 项目地址: https://gitcode.com/gh_mirrors/bri/brigadier 还在为Mac安装Windows系统时繁琐的驱动匹配而烦恼吗?每次重…...

Python 爬虫数据处理:特殊格式文档爬虫解析处理

前言 在 Python 爬虫规模化采集业务中,除常规 HTML 网页与 JSON 接口数据外,经常会遇到各类非网页型特殊格式文档资源,常见包含 PDF、Word、Excel、CSV、TXT、压缩包内嵌文档、Base64 加密文档、富文本混合格式文档等。这类文档无法通过常规…...

终极指南:如何让淘宝淘金币任务全自动完成,每天节省20分钟

终极指南:如何让淘宝淘金币任务全自动完成,每天节省20分钟 【免费下载链接】taojinbi 淘宝淘金币自动执行脚本,包含蚂蚁森林收取能量,芭芭农场全任务,解放你的双手 项目地址: https://gitcode.com/gh_mirrors/ta/tao…...

Notero终极指南:打通Zotero与Notion的学术工作流桥梁

Notero终极指南:打通Zotero与Notion的学术工作流桥梁 【免费下载链接】notero A Zotero plugin for syncing items and notes into Notion 项目地址: https://gitcode.com/gh_mirrors/no/notero 当你在Zotero中积累了数百篇文献,却发现整理和引用它…...

Python 爬虫高级实战:爬虫接口限流自适应调节

前言 网络目标站点普遍具备严格的接口访问限流、频率校验、IP 频次风控、接口令牌校验等防护机制,常规固定延时、固定并发的爬虫模式极易触发封禁、接口 429 限流、会话失效、IP 拉黑等问题。人工配置延时、手动调整并发阈值的传统方式,无法适配站点动态…...

libhv实战:300行构建C++异步RPC框架,集成Protobuf与evpp

1. 为什么需要C异步RPC框架 在微服务架构盛行的今天,服务间的通信效率直接决定了系统整体性能。传统同步RPC调用就像打电话,必须等对方接听才能开始对话,而异步RPC更像是发微信,发完消息就可以去做其他事情,等对方回复…...

下行周期生存之道 = 低风险试错 × 即时反馈 × 长期复购

总结公式: 下行周期赚钱 低风险试错 即时反馈 长期复购 日本用30年验证了这套逻辑。 普通人现在能不能赚到钱,不在于胆子够不够大,而在于你能不能在大家焦虑的时候,给他一点确定感。 先收藏,慢慢找自己的切入口。...

图解人工智能(12)自动做化学实验的机器

近年来,人工智能和传统科学的结合备受瞩目。2019年,英国利物浦大学在《自然》杂志发表论文,介绍了一种可以自动做化学实验的机器人。查找相关资料,并讨论一下类似的工作能给人类社会带来怎样的变革。首先,实验人员的培…...

图解人工智能(11)让人惊讶的AI

人工智能已经融入到我们的生活之中,如便捷的刷脸支付,帮我们扫地的机器人。想一想,你身边还有哪些有趣的人工智能设备?以一种设备为例,搜索它的相关信息,看它为我们的生活带来了哪些便利。开放讨论题&#…...

图解人工智能(10)人工智能的发展历程

人工智能自20世纪50年代发展至今,经历了若干次高潮和低谷。每到陷入困境的时候,总有一些科学家勇敢地打破传统思想的束缚,创造出新理论、新方法,使人工智能重现生机。例如,在符号主义陷入危机的时候,费根鲍…...

ESP32音频播放终极指南:从SD卡播放MP3到网络流媒体的完整解决方案

ESP32音频播放终极指南:从SD卡播放MP3到网络流媒体的完整解决方案 【免费下载链接】ESP32-audioI2S Play mp3 files from SD via I2S 项目地址: https://gitcode.com/gh_mirrors/es/ESP32-audioI2S 想要在ESP32上构建专业的音频播放系统吗?ESP32-…...

如何解锁数字化制造的数据瓶颈:stltostp的轻量级STL转STEP解决方案

如何解锁数字化制造的数据瓶颈:stltostp的轻量级STL转STEP解决方案 【免费下载链接】stltostp Convert stl files to STEP brep files 项目地址: https://gitcode.com/gh_mirrors/st/stltostp 在数字化制造与工业4.0转型的浪潮中,数据格式的互操作…...

2026届学术党必备的六大降重复率平台推荐榜单

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 令AI精确执行任务的基础,是下达精准的指令,此即降AI指令。降AI指令专…...

LayerDivider终极指南:5分钟掌握智能插画分层技术

LayerDivider终极指南:5分钟掌握智能插画分层技术 【免费下载链接】layerdivider A tool to divide a single illustration into a layered structure. 项目地址: https://gitcode.com/gh_mirrors/la/layerdivider 你是否曾经面对一张复杂的插画作品&#xf…...

3步免费获取公式识别神器:img2latex-mathpix本地部署终极指南

3步免费获取公式识别神器:img2latex-mathpix本地部署终极指南 【免费下载链接】img2latex-mathpix Mathpix has changed their billing policy and no longer has free monthly API requests. This repo is now archived and will not receive any updates for the …...

在株洲如何选择护脊透气的床垫?

引言在现代社会,随着生活节奏的加快和工作压力的增加,越来越多的人开始关注睡眠质量。而床垫作为影响睡眠质量的重要因素之一,其选择显得尤为重要。特别是对于需要护脊和透气功能的床垫,如何选择成为了一个关键问题。本文将结合德…...

在株洲如何根据个人需求选择合适的床垫?

如何根据个人需求选择合适的床垫?在快节奏的现代生活中,一张舒适的床垫对于保证良好的睡眠质量至关重要。然而,面对市场上琳琅满目的床垫产品,如何根据个人需求选择一款合适的床垫呢?本文将从多个维度出发,…...

告别底噪与失真:手把手教你用STM32 I2C驱动WM8988音频Codec(附完整寄存器配置代码)

嵌入式音频开发实战:WM8988音质优化全攻略 在嵌入式音频系统开发中,WM8988作为一款高性能低功耗的音频编解码芯片,因其出色的音质表现和灵活的配置选项,成为众多开发者的首选。然而,很多工程师在完成基础驱动后&#x…...

告别单调仪表盘:用LVGL Gauge控件打造一个智能家居温湿度监控界面(ESP32实战)

智能家居温湿度监控实战:用LVGL打造动态仪表盘 在智能家居系统中,实时监控环境参数是基础但关键的功能。传统数字显示虽然精确,但缺乏直观性;而精心设计的仪表盘不仅能提升用户体验,还能通过视觉反馈快速传达环境状态。…...

ClawGuard:为Clawdbot AI智能体打造的安全监控与熔断防护系统

1. 项目概述:ClawGuard 是什么,以及为什么你需要它如果你正在使用或开发基于 Clawdbot 框架的 AI 智能体,那么“安全”和“可控”这两个词,大概率已经在你脑海里盘旋过无数次了。我接触过不少团队,从最初的兴奋于 AI 智…...

DeepSeek(V3为主、兼顾V2/R1)算力优化细节详解

DeepSeek(V3为主、兼顾V2/R1)算力优化细节详解以下是针对核心优化模块的深入技术拆解,包含MLA数学原理、FP8精准实现、无辅助损失负载均衡、R1-GRPO算法核心,内容基于DeepSeek-V3官方技术报告及2026年5月公开权威分析。DeepSeek系…...

黄仁勋CMU演讲:取代你的是会AI的人,所有人同一起跑线,奔跑吧

老黄又当博士了。这是他的第7个荣誉博士学位,而且英特尔CEO陈立武亲自为其授袍。卡内基梅隆大学(CMU)最新一届毕业典礼上,黄仁勋向5800多名毕业生发表演讲。面对AI浪潮的冲击,所有人都在焦虑、都在担心会不会被AI取代&…...

【开盘预测】2026年5月13日(周三)

生成时间:2026-05-12 20:30 | 数据来源:金融市场数据 核心预测:市场震荡整理,关注4200-4250区间,量能变化是关键一、今日收盘总结指数收盘点涨跌幅关键技术位上证指数4214.49-0.25%失守4220,守在4200上方深…...

喜马拉雅音频离线收藏:这款跨平台下载器如何帮你永久保存付费内容?

喜马拉雅音频离线收藏:这款跨平台下载器如何帮你永久保存付费内容? 【免费下载链接】xmly-downloader-qt5 喜马拉雅FM专辑下载器. 支持VIP与付费专辑. 使用GoQt5编写(Not Qt Binding). 项目地址: https://gitcode.com/gh_mirrors/xm/xmly-downloader-q…...

Docker 的了解和使用

1. 虚拟化全虚拟化:虚拟机的操作系统可以和宿主机的操作系统完全不同。os层虚拟化:操作内核相同,软件虚拟化。2. docker安装 Docker容器本质上是Linux容器,它需要Linux内核环境才能运行。在Windows上直接运行Docker,需…...

Python 爬虫反爬突破:CDN 防护节点穿透采集

前言 当下大型互联网站点、电商平台资讯门户、行业数据网站均全面接入 CDN 内容分发网络,借助全球节点缓存、流量调度、智能分流、节点 IP 隐身、区域访问限制等机制构建底层防护体系。传统爬虫直接请求源站 IP 的方式会被 CDN 节点拦截、跳转、限速、IP 封禁、节点…...

AI Agent 的难点,不在搭 Demo,而在让人敢交任务

Agent难在让人敢托付 很多团队做 Agent 的误会,是把跑通一次当成好用。 现在搭一个 Demo 确实不难。一个大模型,几段提示词,接几个搜索、表格、浏览器或数据库工具,很快就能演示一个会拆任务、会调用工具、会输出结果的流程。看起…...

通过Taotoken官方价折扣与活动价降低大模型API使用门槛

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 通过Taotoken官方折扣与活动价降低大模型API使用门槛 对于开发者而言,大模型API的成本是项目落地和持续迭代中必须考量…...

六、Ext系列文件系统(2)

...