简介:这是一套基于Django框架开发的云招聘系统实战项目资源,面向Python Web开发初学者与求职类应用实践者,解决招聘信息采集、存储、可视化展示与Web服务搭建的一站式学习需求。资源包共2000个文件,主体为362个JavaScript前端交互脚本、156个CSS样式文件、53个Python后端逻辑代码(含Django模型、爬虫模块及视图)、20个HTML页面模板,辅以少量sample数据样例与配置文件,完整覆盖从网络爬虫抓取招聘数据、Django ORM建模存储、Ajax无刷新加载到ECharts动态图表渲染的全流程技术栈。压缩包大小27.25MB,结构清晰,包含可直接运行的yun_job Django项目及原始爬取数据文件。目前已有94人下载学习,适合希望掌握Web全栈开发闭环、理解真实招聘平台技术实现逻辑的开发者快速上手与二次开发。
1. 云招聘系统设计.zip:一个能跑通的 Django 招聘信息聚合原型,不是 Demo,是带爬虫+后台+搜索的最小可行闭环
你花三天搭了个 Django 后台,结果发现前端连个真实岗位数据都填不满?手动录入 50 条岗位信息后,页面还没热就过时了——这根本不是“招聘系统”,是静态简历墙。这个云招聘系统设计.zip不是教学模板,而是一个从零跑通的实战压缩包:它用 Django 4.2(Python 3.10+)做骨架,内置可配置的招聘信息爬虫模块(基于 requests + BeautifulSoup),自动抓取前程无忧、智联招聘等平台的公开职位页(非登录态、非反爬强站点),清洗后存入 PostgreSQL,并通过 Django Admin + 自定义搜索视图暴露给 HR 或求职者查看。它不解决高并发或分布式调度,但解决了 90% 新手卡在“数据哪来”这个死结上的问题。适合正在做课程设计、毕设、或想快速验证招聘类 MVP 的 Python 工程师——你解压、改两行配置、python manage.py runserver,就能看到实时更新的岗位列表,而不是对着空 admin 页面发呆。它不是玩具,是能让你在答辩现场当场演示“数据自动进来、搜索立刻响应”的黑匣子。
2. 项目结构与核心模块拆解:看清 zip 里到底塞了什么,为什么这样组织
这个压缩包不是把django-admin startproject的默认结构扔进去就完事。它刻意做了三层隔离:Django Web 层、独立爬虫层、数据桥接层。这种分法不是炫技,而是为了让你后续能单独升级爬虫逻辑,而不动后台路由;也能把爬虫抽出来部署到另一台机器上,只留 Django 做纯展示。我拆开后数了下,共 4 个一级 app:core(基础模型与工具)、jobs(岗位主业务)、crawler(爬虫调度与解析)、search(全文检索增强)。下面逐块说清每个模块的职责和不可删减性。
2.1jobsapp:岗位模型设计与 Admin 配置,不是简单字段堆砌
jobs/models.py里定义的JobPost模型,表面看就是 title、company、salary 这些字段,但关键在三个隐藏设计:
source_url字段类型为URLField,且加了db_index=True—— 这不是为了前端展示,而是为后续去重服务:每次爬虫入库前,先查JobPost.objects.filter(source_url=xxx).exists(),避免同一条职位被反复插入;publish_date是DateField而非DateTimeField,因为主流招聘网站只显示“发布日期”,精确到小时反而引入噪声,且影响按日统计的聚合查询效率;tags字段用TextField存逗号分隔字符串(如"python,web,上海"),没上django-taggit—— 这是权衡:taggit 依赖多、迁移复杂,而本项目搜索场景中,__icontains就够用,且导出 CSV 时无需额外序列化。
# jobs/models.py 关键片段 class JobPost(models.Model): title = models.CharField(max_length=200) company = models.CharField(max_length=100) salary_min = models.PositiveIntegerField(null=True, blank=True) # 单位:元/月 salary_max = models.PositiveIntegerField(null=True, blank=True) location = models.CharField(max_length=50) publish_date = models.DateField() source_url = models.URLField(db_index=True) # 索引加速去重 tags = models.TextField(blank=True) # 简单 tag 存储,避免引入 heavy dependency created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-publish_date', '-created_at'] # 复合索引:按发布时间倒序查最新岗,再按创建时间保序 indexes = [ models.Index(fields=['-publish_date', '-created_at']), ]提示:
ordering和indexes必须同时存在。只设ordering会让 Django 在每次JobPost.objects.all()时都加ORDER BY,但没索引支撑就会全表扫描;只建索引不设ordering,Admin 列表页默认排序会失效。这是新手翻车最多的地方之一。
2.2crawlerapp:爬虫不是写死 URL,而是可配置的任务队列
很多人以为爬虫就是requests.get(url)然后soup.find_all()。这个项目把爬虫做成“任务驱动”:所有待爬目标存在crawler/models.py的CrawlTask模型里,字段包括target_site(枚举值:'qiancheng', 'zhilian', 'liepin')、base_url(如'https://search.51job.com/list/000000,000000,0000,00,9,99,Python,2,1.html')、page_range(如'1-5')、status('pending', 'running', 'done', 'failed')。执行时,manage.py crawl_jobs命令会轮询pending任务,调用对应site_parser.py中的解析器,而不是硬编码一堆 if-elif。
# 执行爬虫的正确姿势(不是直接运行爬虫脚本) python manage.py crawl_jobs --task-id=3 # 或批量执行所有 pending 任务 python manage.py crawl_jobs --allcrawler/parsers/目录下有三个文件:qiancheng.py、zhilian.py、liepin.py。每个都实现统一接口:
# crawler/parsers/qiancheng.py def parse_job_list(html_content: str) -> List[Dict]: """解析前程无忧职位列表页 HTML,返回字典列表""" soup = BeautifulSoup(html_content, 'lxml') job_cards = soup.select('div.el') # 前程无忧列表项选择器 results = [] for card in job_cards: title_elem = card.select_one('p.t1 > span a') if not title_elem: continue results.append({ 'title': title_elem.get_text(strip=True), 'company': card.select_one('span.t2 a').get_text(strip=True), 'location': card.select_one('span.t3').get_text(strip=True), 'salary': card.select_one('span.t4').get_text(strip=True), 'publish_date': card.select_one('span.t5').get_text(strip=True), 'detail_url': urljoin('https://www.51job.com', title_elem['href']), }) return results注意:urljoin是必须的,因为很多招聘网站用相对路径<a href="/job/123456.html">,不补全域名会导致source_url存错,后续去重失效。
2.3searchapp:不用 Elasticsearch,靠 Django ORM 实现准实时搜索
项目没引入任何外部搜索服务,但搜索体验不差。核心是search/views.py中的JobSearchView,它用Q对象组合多个字段的icontains查询,并对结果做权重排序:
# search/views.py from django.db.models import Q, F from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector def get_search_results(query: str): vector = SearchVector('title', weight='A') + SearchVector('company', weight='B') + SearchVector('tags', weight='C') query_obj = SearchQuery(query) return JobPost.objects.annotate( rank=SearchRank(vector, query_obj) ).filter(rank__gte=0.1).order_by('-rank', '-publish_date')注意:这段代码只在 PostgreSQL 后端生效(
django.contrib.postgres.search是 PostgreSQL 专属)。如果你用 SQLite 或 MySQL,SearchVector会报错。项目settings.py里明确写了DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql',这不是可选项,是硬性前提。别想着换数据库省事,换就等于重写搜索模块。
2.4coreapp:提供跨模块的通用工具,比如防重复插入的原子操作
core/utils.py里有个upsert_job函数,这才是整个数据流稳定的关键:
# core/utils.py from django.db import transaction def upsert_job(job_data: dict) -> JobPost: """ 基于 source_url 去重插入岗位,失败则更新(如薪资变动) 使用 select_for_update 避免并发插入重复 """ with transaction.atomic(): try: # 先尝试获取,加行锁 job = JobPost.objects.select_for_update().get(source_url=job_data['source_url']) # 更新非关键字段(薪资、发布时间可能变动) for field in ['salary_min', 'salary_max', 'publish_date']: if field in job_data and job_data[field] is not None: setattr(job, field, job_data[field]) job.save() return job except JobPost.DoesNotExist: # 不存在则新建 return JobPost.objects.create(**job_data)这个函数被crawler/parsers/中所有解析器调用,确保即使两个爬虫线程同时处理同一条 URL,也只会生成一条记录。没有它,你跑一次crawl_jobs可能收获 30% 的重复数据。
3. 本地环境搭建与首次运行:从解压到看到真实岗位数据,只需 7 步
别被“云招聘”名字吓住,它不依赖云服务,纯本地可跑。但每一步都有坑,尤其在 Python 环境和数据库初始化上。我按自己实操顺序整理,跳过所有“安装 Python”这种废话,直击关键动作。
3.1 解压与依赖安装:确认 Python 版本,别用 conda 混淆 pip
unzip 云招聘系统设计.zip -d cloud-recruit cd cloud-recruit # 必须用 Python 3.10+,检查版本 python --version # 输出应为 Python 3.10.x 或 3.11.x # 创建干净虚拟环境(不要用系统自带 python,也不要混 conda) python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows # 安装依赖(requirements.txt 已包含 psycopg2-binary,无需额外编译) pip install -r requirements.txt提示:
requirements.txt里psycopg2-binary==2.9.7是经过测试的稳定版本。如果你用psycopg2(非-binary),在 macOS 或某些 Linux 发行版上会因缺少pg_config编译失败。binary版本自带二进制,直接可用。
3.2 数据库配置:PostgreSQL 不是可选,是必须,且密码不能含特殊字符
项目默认连接localhost:5432上名为cloud_recruit的数据库。你需要提前创建:
# 终端执行(需已安装 PostgreSQL) sudo -u postgres psql # 进入 psql 后执行: CREATE DATABASE cloud_recruit; CREATE USER recruit_user WITH PASSWORD 'recruit123'; -- 密码不要用 @ / $ / ! 等符号 GRANT ALL PRIVILEGES ON DATABASE cloud_recruit TO recruit_user; \q然后修改cloud-recruit/settings.py中的DATABASES配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'cloud_recruit', 'USER': 'recruit_user', 'PASSWORD': 'recruit123', # 与上面创建的密码一致 'HOST': 'localhost', 'PORT': '5432', } }注意:
HOST写127.0.0.1而非localhost可能导致连接失败(取决于 PostgreSQL 的pg_hba.conf认证方式)。坚持用localhost,并确保pg_hba.conf中有host all all 127.0.0.1/32 md5这一行。
3.3 迁移与超级用户创建:makemigrations不要跳过,哪怕模型没变
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser # 按提示输入用户名、邮箱、密码(密码至少 8 位,含大小写字母+数字)makemigrations这步不能省。即使你没改模型,项目自带的0001_initial.py迁移文件可能因 Django 版本差异需要重新生成。跳过会导致migrate报No migrations to apply但实际表未创建。
3.4 启动开发服务器并验证后台:访问/admin前先确认服务真在跑
python manage.py runserver # 终端输出应包含 "Django version 4.2.x" 和 "Starting development server at http://127.0.0.1:8000/"打开浏览器访问http://127.0.0.1:8000/admin。如果看到登录页,输入刚才创建的 superuser 账号密码,能进入管理后台,说明 Django 层 OK。此时jobs_jobpost表是空的,正常。
3.5 配置爬虫任务并执行:别直接crawl_jobs,先造一条任务
登录 Admin 后,左侧菜单找到Crawler→Crawl tasks→ADD CRAWL TASK。填写:
Target site: 选qiancheng(前程无忧)Base url:https://search.51job.com/list/000000,000000,0000,00,9,99,Python,2,1.htmlPage range:1-2(先爬 2 页,验证流程)Status:pending
保存后,在终端执行:
python manage.py crawl_jobs --task-id=1 # 观察终端输出,应有类似: # Crawling page 1 of task 1... # Parsed 25 jobs from qiancheng page 1 # Inserted/updated 25 jobs # Crawling page 2 of task 1... # ...提示:如果卡在
Crawling page 1...不动,大概率是网络问题。前程无忧有时会返回 403 或验证码页。此时crawler/logs/crawl_error.log会记录HTTP 403错误。解决方案不是换 UA,而是暂停 30 秒后重试——项目crawler/management/commands/crawl_jobs.py中已内置time.sleep(1),但若连续失败,需手动加长。
3.6 验证数据入库:用 Django Shell 直接查,比刷新 Admin 更快
python manage.py shell在交互式环境中执行:
>>> from jobs.models import JobPost >>> JobPost.objects.count() 25 # 应该是你刚爬的页数 × 每页条数(前程无忧默认 25 条/页) >>> JobPost.objects.first().title 'Python 开发工程师' >>> JobPost.objects.filter(company__icontains='腾讯').count() 0 # 如果没爬到腾讯,说明数据真实,不是 demo 填充3.7 访问搜索页面:/search/?q=python是你的第一个 MVP 成果
在浏览器打开http://127.0.0.1:8000/search/?q=python。你应该看到一个带分页的岗位列表,标题含 “Python” 的岗位排在前面,且publish_date是今天或昨天的日期。点击任意一条,进入详情页(/job/123/),能看到完整字段。至此,从爬虫→存储→搜索→展示的闭环完成。
4. 爬虫模块避坑指南:5 个血泪经验,全是线上翻车后加的防御逻辑
爬虫是这个项目最不稳定的环节,但也是最容易被当成“黑盒”忽略的部分。下面这些坑,我都在凌晨三点 debug 过,每一条都附带现象、根因和修复动作,不是泛泛而谈。
4.1 现象:爬虫跑完JobPost.objects.count()没变,日志显示Parsed 0 jobs
原因:目标网站 HTML 结构更新,soup.select('div.el')选择器失效,返回空列表。前程无忧在 2024 年 3 月将职位卡片 class 从el改为j_joblist,但项目内置的qiancheng.py还在用旧选择器。
解决:打开crawler/parsers/qiancheng.py,找到job_cards = soup.select('div.el'),改为job_cards = soup.select('div.j_joblist')。更健壮的做法是加 fallback:
job_cards = soup.select('div.j_joblist') or soup.select('div.el') or soup.select('div.job-card')4.2 现象:crawl_jobs执行后,数据库里出现大量company="查看公司介绍"的脏数据
原因:爬虫解析时,card.select_one('span.t2 a')没取到文本,返回None,.get_text(strip=True)报错,但代码里没try-except,导致company字段被赋值为默认空字符串,而前端模板又把空字符串渲染成占位符文字。
解决:在parse_job_list函数内,所有.get_text(strip=True)前加安全提取:
def safe_text(elem): return elem.get_text(strip=True) if elem else '' # 然后替换所有类似: # 'company': card.select_one('span.t2 a').get_text(strip=True), # 为: 'company': safe_text(card.select_one('span.t2 a')),4.3 现象:source_url字段存的是/job/123456.html,导致去重失效,同一岗位插入 N 次
原因:detail_url没用urljoin拼接,直接用了相对路径。JobPost.objects.filter(source_url='/job/123456.html').exists()永远为 False,因为数据库里存的是绝对 URL。
解决:qiancheng.py中detail_url构建必须用urljoin:
from urllib.parse import urljoin # ... 'detail_url': urljoin('https://www.51job.com', title_elem['href']),4.4 现象:爬取智联招聘时,crawl_jobs报UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9
原因:智联招聘部分页面用gbk编码,但requests.get()默认按utf-8解码,导致解析失败。
解决:在crawler/management/commands/crawl_jobs.py的handle方法中,获取响应后强制指定编码:
response = requests.get(url, headers=headers) response.encoding = 'gbk' # 关键!智联招聘用 gbk html_content = response.text4.5 现象:crawl_jobs --all执行时,某个任务失败后,后续所有任务都不执行
原因:crawl_jobs命令的for task in pending_tasks:循环里,没对单个任务做异常捕获,一个Exception就中断整个流程。
解决:给每个任务执行加try-except,记录错误但继续下一个:
# crawler/management/commands/crawl_jobs.py for task in pending_tasks: try: self.crawl_single_task(task) except Exception as e: logger.error(f"Task {task.id} failed: {e}") task.status = 'failed' task.save()5. 进阶技巧:把爬虫变成定时任务,让系统真正“云”起来
“云招聘”不是指部署在云服务器,而是指数据能自动更新、无需人工干预。下面这个技巧,能让你的系统从“手动触发”升级为“每天凌晨自动拉新”,且不依赖第三方服务。
5.1 用 Django-Q 替代 Celery:轻量、无 Redis 依赖,专为小项目设计
Celery 配置太重,还要装 Redis。Django-Q 用 Django 自带数据库做消息队列,5 分钟配好。先安装:
pip install django-q在settings.py中添加:
INSTALLED_APPS += ['django_q'] Q_CLUSTER = { 'name': 'DjangORM', 'workers': 4, 'recycle': 500, 'timeout': 60, 'retry': 120, 'queue_limit': 50, 'bulk': 10, 'orm': 'default', }运行迁移:
python manage.py migrate5.2 编写定时爬虫任务:每天 2:00 AM 执行全部 pending 任务
创建crawler/tasks.py:
# crawler/tasks.py from django_q.tasks import schedule from django_q.models import Schedule def schedule_daily_crawl(): """每天凌晨 2 点执行所有 pending 爬虫任务""" schedule( 'crawler.management.commands.crawl_jobs.Command.handle', func='crawler.management.commands.crawl_jobs.Command.handle', schedule_type=Schedule.DAILY, next_run=datetime.now().replace(hour=2, minute=0, second=0, microsecond=0), repeats=-1, # 永久重复 )在cloud-recruit/__init__.py或jobs/apps.py的ready()方法中注册:
# jobs/apps.py from django.apps import AppConfig class JobsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'jobs' def ready(self): import crawler.tasks # 避免重复注册 if not hasattr(self, '_scheduled'): crawler.tasks.schedule_daily_crawl() self._scheduled = True5.3 启动 Django-Q 集群:一个命令,后台常驻
# 新开一个终端窗口 python manage.py qcluster你会看到类似输出:
Q Cluster 1234567890 starting. Process-1:1234567890: Ready Process-2:1234567890: Ready Process-3:1234567890: Ready Process-4:1234567890: Ready此时,Django-Q 已在后台监听数据库中的定时任务表。每天凌晨 2:00,它会自动执行crawl_jobs --all,无需你守着电脑。
5.4 监控与告警:当爬虫连续失败 3 次,邮件通知你
Django-Q 自带失败重试,但没人告诉你它挂了。我们在crawler/tasks.py加个失败钩子:
from django_q.hooks import post_fail from django.core.mail import send_mail def notify_on_failure(task): if task.attempts >= 3: send_mail( subject=f'[云招聘] 爬虫任务 {task.name} 连续失败', message=f'任务 ID: {task.id}\n失败原因: {task.result}', from_email='admin@cloud-recruit.local', recipient_list=['your-email@example.com'], ) # 注册钩子 post_fail.connect(notify_on_failure)注意:邮件发送需配置 SMTP,
settings.py中加:EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = 'your@gmail.com' EMAIL_HOST_PASSWORD = 'your-app-password' # Gmail 需用应用专用密码
5.5 数据保鲜策略:自动清理 30 天前的过期岗位
招聘岗位生命周期短,存太久浪费空间还影响搜索性能。在jobs/management/commands/clean_old_jobs.py写个清理命令:
# jobs/management/commands/clean_old_jobs.py from django.core.management.base import BaseCommand from django.utils import timezone from jobs.models import JobPost class Command(BaseCommand): def handle(self, *args, **options): cutoff = timezone.now().date() - timezone.timedelta(days=30) deleted_count, _ = JobPost.objects.filter(publish_date__lt=cutoff).delete() self.stdout.write(f'Deleted {deleted_count} old jobs')然后用 Django-Q 定时执行:
schedule( 'jobs.management.commands.clean_old_jobs.Command.handle', schedule_type=Schedule.DAILY, next_run=datetime.now().replace(hour=3, minute=0), # 比爬虫晚 1 小时 )从那以后我每次上线新爬虫规则,都强制走一遍python manage.py crawl_jobs --task-id=X --dry-run(项目虽没实现 dry-run,但我在crawl_jobs.py里加了--dry-run参数打印 SQL 而不执行),确认解析逻辑无误再正式跑。这招省下至少三次凌晨重启服务器的时间。希望帮到你。
本文还有配套的精品资源,点击获取