本文还有配套的精品资源,点击获取
简介:本文详细介绍如何使用Python的Scrapy框架爬取拉勾网上的Java职位信息,涵盖从项目搭建、Spider编写、数据解析到持久化存储的完整流程。通过解析Ajax接口返回的JSON数据,提取职位名称、公司信息等工作关键字段,并利用Item Pipeline将数据导出为CSV文件。最后,结合wordcloud和matplotlib库对抓取结果进行文本分析,去除停用词后生成直观的词云图表,实现数据可视化。项目严格遵守robots.txt规范,倡导合法合规的网络爬虫实践,适用于Python爬虫入门与数据分析实战学习。
在现代网络数据采集领域,Scrapy作为Python中最强大的Web爬虫框架之一,凭借其高效的异步处理机制和模块化架构,广泛应用于大规模数据抓取任务。本章将深入剖析Scrapy的核心组件构成,包括Spiders、Items、Pipelines、Request/Response对象以及中间件系统,从理论层面解析各组件之间的协作机制。
Scrapy采用高度解耦的事件驱动架构,其核心组件通过引擎(Engine)协同工作。当Spider发起初始请求时, Request 对象被发送至调度器(Scheduler),经由Downloader Middleware预处理后交由Downloader执行,获取的 Response 再反向经中间件流入Spider进行解析。该流程可通过以下简化流程图表示:
graph LR
A[Spider] -->|start_requests()| B(Request)
B --> C[Engine]
C --> D[Scheduler]
D --> E[Downloader]
E -->|Response| C
C --> A
A -->|yield Item| F[Item Pipeline]
此机制确保了高并发下的稳定抓取能力,为后续拉勾网招聘信息的高效采集提供了底层支撑。
在构建一个高效、可维护的网络爬虫系统时,合理的项目结构和清晰的模块划分是成功的关键。Scrapy框架本身提供了一套高度模块化的工程架构,使得开发者能够快速初始化项目并进行定制化开发。本章将围绕如何从零开始创建一个功能完整的Scrapy项目展开,深入讲解项目的初始化流程、目录结构解析以及开发环境的准备策略。通过这一过程,不仅能够建立起符合生产级标准的数据采集系统基础,还能为后续处理动态内容、实现数据持久化与可视化分析打下坚实的技术根基。
Scrapy项目的创建并非简单的文件夹建立,而是一次基于框架规范的工程初始化过程。该过程确保了所有核心组件——如Spiders、Items、Pipelines等——均被正确生成,并具备默认的行为逻辑。通过命令行工具 scrapy startproject ,我们可以一键生成符合Scrapy标准结构的项目骨架,从而避免手动配置带来的遗漏或错误。
要创建一个新的Scrapy项目,首先需要确认已安装Scrapy库。可通过以下命令检查是否安装成功:
pip install scrapy
安装完成后,执行如下命令来生成项目:
scrapy startproject lagou_spider
该命令会在当前目录下创建名为 lagou_spider 的项目文件夹,其内部结构如下所示:
lagou_spider/
scrapy.cfg
lagou_spider/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
__init__.py
其中, scrapy.cfg 是项目的部署配置文件,用于支持Scrapyd服务部署;而内层 lagou_spider/ 目录则是实际的Python包路径,包含了所有可编程模块。
关键说明 :使用
startproject命令的优势在于它自动集成了事件循环(基于Twisted)、调度器、下载器、中间件钩子等底层机制,使开发者可以专注于业务逻辑而非基础设施搭建。
接下来,在 spiders/ 目录中创建一个具体的爬虫类,例如拉勾网Java职位信息抓取器:
cd lagou_spider/spiders
scrapy genspider job_spider "lagou.com"
此命令会自动生成一个名为 job_spider.py 的Spider模板文件,包含基本的 start_urls 、 parse() 方法等初始代码。
graph TD
A[安装Scrapy] --> B[运行scrapy startproject]
B --> C[生成标准项目结构]
C --> D[进入spiders目录]
D --> E[使用genspider创建具体爬虫]
E --> F[编写请求逻辑与解析规则]
F --> G[运行scrapy crawl启动爬虫]
上述流程体现了从环境准备到首次运行的完整闭环,适用于绝大多数静态或半动态网站的快速接入场景。
settings.py 是整个Scrapy项目的“控制中心”,决定了爬虫行为的核心策略。合理设置这些参数不仅能提升抓取效率,更能有效规避反爬机制导致的封禁风险。
以下是几个最关键的配置项及其作用说明:
BOT_NAME lagou_spider ) USER_AGENT "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ROBOTSTXT_OBEY DOWNLOAD_DELAY CONCURRENT_REQUESTS COOKIES_ENABLED 示例修改后的部分配置:
# settings.py
BOT_NAME = 'lagou_spider'
SPIDER_MODULES = ['lagou_spider.spiders']
NEWSPIDER_MODULE = 'lagou_spider.spiders'
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
ROBOTSTXT_OBEY = False # 调试阶段绕过robots限制
DOWNLOAD_DELAY = 2 # 设置每两次请求之间延迟2秒
CONCURRENT_REQUESTS = 8
CONCURRENT_REQUESTS_PER_DOMAIN = 8
COOKIES_ENABLED = True
逻辑分析 :
USER_AGENT的设置是为了防止服务器因识别出非人类用户而返回空数据或验证码页面。现代网站常通过UA判断客户端类型,因此必须模拟主流浏览器。ROBOTSTXT_OBEY=True表示遵循目标网站的robots.txt协议,但在开发调试阶段通常暂时关闭,以便自由测试。DOWNLOAD_DELAY至少应设为1秒以上,尤其针对国内招聘类网站(如拉勾网),频繁请求极易触发IP封锁。CONCURRENT_REQUESTS控制并发量,过高会导致目标服务器压力过大而拒绝连接;建议结合AutoThrottle扩展进一步优化。
此外,还可启用日志级别控制:
LOG_LEVEL = 'INFO' # 输出信息更清晰,便于监控运行状态
这些配置共同构成了爬虫的行为边界,直接影响稳定性与合法性。
许多现代网站采用前后端分离架构,前端通过Ajax异步加载数据。这类接口往往带有复杂的请求头校验机制(如 X-Requested-With: XMLHttpRequest )。为了成功模拟此类请求,需借助Downloader Middleware对发出的Request对象进行增强处理。
在 middlewares.py 中定义一个自定义中间件类:
# middlewares.py
class AjaxRequestMiddleware:
def process_request(self, request, spider):
# 添加必要的Headers以模拟Ajax请求
if 'json' in request.url or '/position/' in request.url:
request.headers['X-Requested-With'] = 'XMLHttpRequest'
request.headers['Referer'] = 'https://www.lagou.com/jobs/list_Java'
request.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
request.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
return None
然后在 settings.py 中启用该中间件:
DOWNLOADER_MIDDLEWARES = {
'lagou_spider.middlewares.AjaxRequestMiddleware': 543,
}
参数说明 :
process_request()方法会在每个请求发送前调用;- 数字
543表示优先级,数值越小越早执行;- 当URL包含
json或特定路径时,自动添加Ajax所需的Header字段。
该中间件的作用是让Scrapy发出的请求看起来像是由浏览器中的JavaScript发起的AJAX调用,从而绕过某些接口的身份验证机制。
这种精细化的请求伪造技术是应对现代反爬策略的重要手段之一。
Scrapy之所以强大,不仅在于其性能表现,更在于其清晰的模块职责分离机制。每一个组件都有明确的功能定位,协同完成一次完整的爬取任务。理解各模块之间的关系,有助于设计出高内聚、低耦合的数据采集系统。
spiders/ 目录存放所有的爬虫类,每个类继承自 scrapy.Spider 或其子类(如 CrawlSpider 、 XMLFeedSpider 等),负责定义起始URL、解析响应、提取数据并生成新的请求。
以拉勾网为例,定义一个专门抓取Java岗位的Spider:
# spiders/job_spider.py
import scrapy
class JobSpider(scrapy.Spider):
name = 'job_spider'
allowed_domains = ['lagou.com']
start_urls = ['https://www.lagou.com/jobs/list_Java']
def parse(self, response):
# 提取搜索结果中的职位链接
links = response.css('.position_link::attr(href)').getall()
for link in links:
yield scrapy.Request(url=link, callback=self.parse_detail)
def parse_detail(self, response):
# 解析详情页字段
yield
逐行解读 :
name: 爬虫唯一标识符,运行时通过scrapy crawl job_spider调用;allowed_domains: 限定域名范围,防止意外跳转至其他站点;start_urls: 初始入口地址;parse(): 默认回调函数,用于处理列表页;- 使用CSS选择器提取职位链接,并对每条链接发起新请求;
callback=self.parse_detail指定后续解析函数;- 在
parse_detail()中提取详细信息,最终以字典形式yield出Item。
该结构体现了Scrapy典型的“请求-响应-再请求”递归模式,非常适合分页或多层级抓取。
虽然可以直接在Spider中返回字典,但最佳实践是使用 Item 类来定义结构化数据模型。这有助于后期Pipeline统一处理,并提升代码可读性。
编辑 items.py 文件:
# items.py
import scrapy
class JobInfoItem(scrapy.Item):
title = scrapy.Field() # 职位名称
salary = scrapy.Field() # 薪资范围
city = scrapy.Field() # 所在城市
experience = scrapy.Field() # 工作经验要求
education = scrapy.Field() # 学历要求
company = scrapy.Field() # 公司名称
tags = scrapy.Field() # 技术标签(如Java, Spring)
description = scrapy.Field() # 职位描述
publish_date = scrapy.Field() # 发布时间
crawl_time = scrapy.Field() # 抓取时间戳
随后在Spider中导入并使用:
from lagou_spider.items import JobInfoItem
def parse_detail(self, response):
item = JobInfoItem()
item['title'] = response.xpath('//h1[@class="name"]/text()').get()
item['salary'] = response.xpath('//span[@class="salary"]/text()').get()
# ...其他字段赋值...
yield item
设计原则总结 :
- 字段命名一致性 :统一使用小写+下划线风格(snake_case);
- 语义清晰 :避免模糊命名如
data1、info;- 预留扩展字段 :如
crawl_time可用于去重与监控;- 类型提示友好 :便于集成Pydantic或ORM工具做后续处理。
通过引入Item模型,实现了数据结构的标准化,极大增强了系统的可维护性。
Scrapy的Pipeline机制允许我们将数据流经多个处理阶段,形成“链式处理”。每个Pipeline类实现一个独立功能,如清洗、验证、存储等。
定义三个典型Pipeline:
# pipelines.py
import json
from itemadapter import ItemAdapter
from datetime import datetime
class DataCleaningPipeline:
def process_item(self, item, spider):
adapter = ItemAdapter(item)
# 清洗薪资字段
salary = adapter.get('salary')
if salary:
adapter['salary'] = salary.strip().replace('xa0', ' ')
# 清理描述中的多余空白
desc = adapter.get('description')
if desc:
adapter['description'] = ' '.join(desc.split())
return item
class ValidationPipeline:
def process_item(self, item, spider):
required_fields = ['title', 'salary', 'city']
for field in required_fields:
if not adapter.get(field):
raise DropItem(f"Missing required field {field}")
return item
class JsonExportPipeline:
def open_spider(self, spider):
self.file = open('jobs.json', 'w', encoding='utf-8')
self.file.write('[
')
def close_spider(self, spider):
self.file.write('
]')
def process_item(self, item, spider):
line = json.dumps(ItemAdapter(item).asdict(), ensure_ascii=False, indent=2)
self.file.write(line + ',
')
return item
并在 settings.py 中激活:
ITEM_PIPELINES = {
'lagou_spider.pipelines.DataCleaningPipeline': 300,
'lagou_spider.pipelines.ValidationPipeline': 350,
'lagou_spider.pipelines.JsonExportPipeline': 400,
}
处理顺序说明 :
数字代表执行优先级,越小越先执行。数据依次经过清洗 → 校验 → 导出三个阶段,任一环节失败均可中断流程。
该架构支持灵活扩展,例如未来可加入数据库写入、Elasticsearch索引等功能模块。
graph LR
A[Spider产出Item] --> B[DataCleaningPipeline]
B --> C[ValidationPipeline]
C --> D[JsonExportPipeline]
D --> E[写入JSON文件]
通过这种分层处理机制,实现了关注点分离,提升了系统的健壮性与可测试性。
良好的开发环境是保障项目顺利推进的前提。尤其是在涉及中文文本处理和可视化输出时,必须提前部署相关资源,避免运行时报错。
推荐使用Python虚拟环境隔离项目依赖:
python -m venv venv
source venv/bin/activate # Linux/Mac
# 或 venvScriptsactivate # Windows
激活后安装必要库:
pip install scrapy pandas matplotlib wordcloud jieba requests
scrapy pandas matplotlib wordcloud jieba requests 建议将依赖写入 requirements.txt :
scrapy==2.11.0
pandas==2.0.3
matplotlib==3.7.2
wordcloud==1.9.2
jieba==0.42.1
便于团队协作或部署复现。
由于Matplotlib默认不支持中文显示,需手动配置字体以避免词云图出现方框乱码。
步骤如下:
SimHei.ttf )并放入项目目录下的 fonts/ 文件夹; import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
plt.figure(figsize=(6, 3))
plt.text(0.5, 0.5, '中文测试', fontsize=20, ha='center')
plt.axis('off')
plt.show()
若能正常显示,则环境准备完成。
font_path 参数 提前做好环境预检,可大幅降低后续开发中的调试成本。
在现代Web应用日益复杂的背景下,静态HTML页面已不再是主流。越来越多的招聘平台(如拉勾网)采用前后端分离架构,通过Ajax异步请求加载职位列表数据。这种技术选型虽然提升了用户体验,但也对网络爬虫的数据采集能力提出了更高要求。传统的基于HTML解析的方式无法直接获取由JavaScript动态渲染的内容,必须深入分析其接口调用逻辑、请求构造机制以及分页控制策略,才能实现高效、稳定、合规的自动化抓取。
本章将围绕 拉勾网Java岗位招聘信息 的实际案例,系统性地展开从目标网站技术特征识别到完整动态数据抓取流程的设计与实现。重点聚焦于如何利用浏览器开发者工具定位核心API接口,逆向分析请求参数结构,并在Scrapy框架中模拟合法请求行为;同时设计合理的递归分页策略,确保数据完整性的同时避免陷入无限循环或触发反爬机制。整个过程不仅涉及HTTP协议层面的理解,还需结合异步编程思想和状态管理思维,构建一个具备生产级鲁棒性的爬虫系统。
随着单页应用(SPA, Single Page Application)的普及,传统“URL—HTML文档”一一对应的关系被打破。用户在浏览拉勾网职位列表时,尽管地址栏未发生跳转,但内容却能实时刷新,这正是Ajax驱动的数据加载模式的典型表现。要成功抓取此类站点的数据,首要任务是准确判断其前端渲染机制,并锁定实际承载数据的后端接口。
现代网页主要存在三种数据呈现方式:服务端渲染(SSR)、客户端渲染(CSR)和混合渲染。对于拉勾网这类以交互性为核心诉求的职业社交平台,通常采用 客户端渲染 + Ajax异步加载 的技术路径。即初始页面仅包含基本框架与脚本资源,真正的职位信息通过JavaScript发起XHR/Fetch请求,从JSON接口获取并注入DOM节点。
为了验证这一假设,可通过以下步骤进行初步判断:
https://www.lagou.com/jobs/list_Java ; 实验结果表明,在禁用JS后,页面几乎为空白状态,仅有搜索框和导航栏可见,说明原始HTML不包含实质性职位数据,进一步确认了该页面依赖Ajax加载的核心事实。
此外,还可以借助网络监控工具观察页面加载过程中的请求流量。当执行一次关键词搜索或切换城市时,若出现大量 /jobs 或 /positionAjax.json 等路径的请求,且响应类型为 application/json ,即可判定其使用Ajax机制更新内容。
sequenceDiagram
participant User as 用户
participant Browser as 浏览器
participant Server as 后端服务器
User->>Browser: 输入URL并访问拉勾网
Browser->>Server: GET /jobs/list_Java (基础页面)
Server-->>Browser: 返回HTML骨架 + JS资源
Browser->>User: 渲染空页面
Browser->>Server: XHR POST /positionAjax.json (携带查询参数)
Server-->>Browser: 返回JSON格式职位数据
Browser->>Browser: 执行JS将数据插入DOM
Browser->>User: 显示完整职位列表
上述流程图清晰展示了Ajax驱动页面的典型生命周期:初始请求仅获取轻量级模板,后续由前端主动发起数据请求,完成内容填充。因此,爬虫不应再关注主页面HTML结构,而应转向追踪这些隐藏在JS代码背后的API端点。
Chrome DevTools 是逆向分析动态网站不可或缺的利器。其“Network”面板可捕获所有HTTP通信记录,特别适合用于发现异步接口。以下是具体操作步骤:
F12 打开开发者工具; positionAjax 、 job 、 list 关键字的请求; 以拉勾网为例,最关键的请求为:
POST https://www.lagou.com/jobs/positionAjax.json?city=北京&needAddtionalResult=false
该请求具有如下特征:
- 方法:POST;
- 请求头中包含 X-Requested-With: XMLHttpRequest ,标识为Ajax调用;
- 参数通过Form Data传递,包括 first=true , pn=1 , kd=Java ;
- 响应体为标准JSON结构,字段清晰,包含职位标题、薪资、公司名称等关键信息。
此表展示了接口返回的主要字段及其语义含义,为后续Item模型定义提供了依据。
即便找到了真实接口,直接使用Scrapy发起请求仍可能遭遇失败。原因在于许多网站部署了反爬系统,会校验请求头中的多个字段来区分真实用户与机器人。拉勾网尤其严格,常见拦截条件包括:
User-Agent 或使用默认Scrapy UA; Referer 头部指向来源页面; X-Requested-With: XMLHttpRequest ; 因此,必须精准复现浏览器发出的完整请求头。以下是一个典型合法请求头示例:
headers =
User-Agent :模拟主流Chrome浏览器,防止UA检测; Referer :表示用户是从哪个页面发起搜索的,必须与当前城市/关键词匹配; X-Requested-With :明确告知服务器这是一个Ajax请求; Content-Type :告知服务器发送的是表单数据; Origin 和 Accept :增强请求的真实性,提升通过率。 在Scrapy中可通过 scrapy.FormRequest 构造带参数的POST请求,并注入上述头部信息:
import scrapy
class LagouSpider(scrapy.Spider):
name = 'lagou_job'
start_urls = ['https://www.lagou.com/jobs/list_Java']
def start_requests(self):
yield scrapy.Request(
url='https://www.lagou.com/jobs/list_Java',
callback=self.parse,
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
'Referer': 'https://www.lagou.com/jobs/list_Java'
},
meta={'proxy': 'http://your_proxy_ip:port'} # 可选代理
)
def parse(self, response):
# 获取Cookie后发起Ajax请求
yield scrapy.FormRequest(
url='https://www.lagou.com/jobs/positionAjax.json',
formdata={'first': 'true', 'pn': '1', 'kd': 'Java'},
headers=headers,
callback=self.parse_api
)
start_requests() 方法重写初始请求,先访问主页获取必要Cookie; meta={'proxy'} 支持代理池接入,应对IP封禁风险; parse() 中调用 FormRequest 发起带参数的POST请求; formdata 对应浏览器提交的表单字段; callback=self.parse_api 指定处理JSON响应的方法。 值得注意的是,首次访问主页的作用不仅是跳转,更是为了获取有效的Session Cookie(如 JSESSIONID 、 LGUID 等),否则后续Ajax请求会被拒绝。这也是为何不能绕过首页直接请求API的原因之一。
综上所述,对拉勾网的技术特征分析揭示了一个典型的现代Web反爬体系:它不依赖复杂的加密算法,而是通过多层请求上下文关联(Referer、Cookie、Header组合)建立信任链。只有完整还原人类用户的操作路径,才能突破防护机制,进入真正的数据通道。
一旦明确了数据来源于Ajax接口,下一步便是精确构造请求并高效提取所需信息。与传统的HTML解析不同,JSON响应结构规整、层级清晰,极大简化了数据抽取工作。然而,这也带来了新的挑战——如何正确设置请求参数、处理异常响应以及维护会话状态。
拉勾网的职位搜索接口 /positionAjax.json 接收三个关键参数:
其中 first 参数尤为关键:当 first=true 时,服务器返回第一页数据及总页数元信息;当 first=false 时,则仅返回指定页码的数据。这意味着我们可以通过解析首请求的响应体,动态推断出总页数,从而规划后续爬取路径。
在Scrapy中,可通过重写 start_requests() 方法发起初始请求:
def start_requests(self):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
'Referer': 'https://www.lagou.com/jobs/list_Java',
'X-Requested-With': 'XMLHttpRequest'
}
for city in ['北京', '上海', '深圳', '广州']:
yield scrapy.Request(
url=f'https://www.lagou.com/jobs/list_Java?city={city}',
callback=self.get_cookie,
meta={'city': city, 'headers': headers},
dont_filter=True
)
此处引入了一个中间回调函数 get_cookie ,其作用是在访问主页后自动跳转至Ajax请求:
def get_cookie(self, response):
city = response.meta['city']
headers = response.meta['headers']
yield scrapy.FormRequest(
url='https://www.lagou.com/jobs/positionAjax.json',
formdata={'first': 'true', 'pn': '1', 'kd': 'Java', 'city': city},
headers=headers,
callback=self.parse_api,
meta={'city': city, 'page': 1, 'headers': headers}
)
dont_filter=True 防止去重机制误判相同URL; meta 字典用于在请求间传递上下文信息(如城市、页码); FormRequest 自动处理表单编码(application/x-www-form-urlencoded); parse_api 进行JSON解析。 收到API响应后,需从中提取职位列表。由于Scrapy默认将响应视为TextResponse,需手动调用 json() 方法解析:
import json
def parse_api(self, response):
try:
data = json.loads(response.text)
result = data.get('content', {}).get('positionResult', {}).get('result', [])
for job in result:
item = JobInfo()
item['title'] = job.get('positionName')
item['salary'] = job.get('salary')
item['city'] = job.get('city')
item['experience'] = job.get('workYear')
item['education'] = job.get('education')
item['company'] = job.get('companyShortName')
item['publish_time'] = job.get('createTime')
yield item
# 提取总页数并生成下一页请求
total_page_count = data.get('content', {}).get('totalPageCount', 1)
current_page = response.meta['page']
city = response.meta['city']
headers = response.meta['headers']
if current_page < total_page_count:
yield scrapy.FormRequest(
url='https://www.lagou.com/jobs/positionAjax.json',
formdata={
'first': 'false',
'pn': str(current_page + 1),
'kd': 'Java',
'city': city
},
headers=headers,
callback=self.parse_api,
meta={'city': city, 'page': current_page + 1, 'headers': headers}
)
except Exception as e:
self.logger.error(f"解析失败: {response.url}, 错误: {e}")
json.loads(response.text) 将响应文本转换为Python字典; .get() 安全访问嵌套字段,防止KeyError; result 列表,每项映射为一个 JobInfo Item; yield item 提交至Pipeline进行后续处理; totalPageCount 决定是否继续翻页; graph TD
A[发起首请求 first=true] --> B{解析JSON响应}
B --> C[提取职位数据]
C --> D[生成Item对象]
D --> E[提交至Pipeline]
B --> F[获取totalPageCount]
F --> G{current_page < totalPageCount?}
G -->|Yes| H[生成下一页请求 pn+1]
H --> B
G -->|No| I[任务结束]
该流程图体现了典型的递归爬取结构:每次响应既产出数据,又决定是否产生新请求,形成“边爬边扩”的闭环。
尽管请求构造得当,仍可能因网络波动、验证码弹窗或频率限制导致失败。为此,需加入健壮的异常处理机制。
首先,检查HTTP状态码:
if response.status != 200:
self.logger.warning(f"请求失败: {response.url}, 状态码: {response.status}")
return
其次,验证JSON结构完整性:
if not data or data.get('success') is False:
self.logger.error(f"接口返回错误: ")
return
拉勾网在异常时会返回 "success": false 并附带错误信息,如 "频繁访问" 或 "请登录" ,此时应暂停爬取或更换IP。
最后,在 settings.py 中启用内置重试中间件:
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
并通过自定义Downloader Middleware增强控制力:
class RetryDelayMiddleware:
def process_response(self, request, response, spider):
if response.status == 429:
time.sleep(10) # 遇到限流则休眠
return request.copy() # 重新调度原请求
return response
通过以上三层防护(状态码检查、业务逻辑校验、重试机制),可显著提升爬虫稳定性,适应复杂网络环境下的长期运行需求。
分页机制是影响爬虫覆盖率的关键因素。若处理不当,可能导致数据遗漏或重复抓取。拉勾网虽提供明确的页码指示,但仍需谨慎设计递归逻辑,防止因接口变更或网络延迟引发死循环。
如前所述,拉勾网在首请求( first=true )中返回 totalPageCount 字段,这是唯一可靠的总页数来源。后续请求即使修改 pn 超出范围,也不会报错,而是返回空数组,因此不能依赖响应非空作为终止条件。
正确的做法是:
totalPageCount ; meta 传递给后续请求; pn 直至等于 totalPageCount 。 改进后的代码如下:
def parse_api(self, response):
data = json.loads(response.text)
# 提取总页数(仅首次获取)
if response.meta.get('first_request', True):
total_pages = data.get('content', {}).get('totalPageCount', 1)
response.meta['total_pages'] = total_pages
response.meta['first_request'] = False
else:
total_pages = response.meta['total_pages']
# 提取职位数据...
for job in result:
yield item
# 判断是否继续
current_page = response.meta['page']
if current_page < total_pages:
yield scrapy.FormRequest(
url='https://www.lagou.com/jobs/positionAjax.json',
formdata={'first': 'false', 'pn': str(current_page + 1), 'kd': 'Java'},
headers=response.meta['headers'],
callback=self.parse_api,
meta=response.meta.copy() # 保持上下文一致
)
Scrapy的 callback 机制支持任意深度的请求链。除了翻页外,还可扩展至详情页抓取。例如,若需获取职位描述全文,可在Item中添加 detail_url 字段,并追加二级请求:
item['detail_url'] = f"https://www.lagou.com/jobs/{job['positionId']}.html"
yield scrapy.Request(
url=item['detail_url'],
callback=self.parse_detail,
meta={'item': item}
)
随后在 parse_detail 中补充 job_desc 字段并再次提交Item。
为防止因接口异常返回错误页数而导致无限爬取,应设置双重终止条件:
if current_page >= total_pages or current_page > 100:
return # 最大限制100页防失控
同时建议在 settings.py 中配置全局最大并发和下载延迟:
CONCURRENT_REQUESTS = 2
DOWNLOAD_DELAY = 3
RANDOMIZE_DOWNLOAD_DELAY = True
结合分布式调度与持久化队列(如Redis),可构建可伸缩、高可用的大规模采集系统。
最终形成的爬虫不仅能精准抓取拉勾网Java岗位数据,还具备良好的容错性与可维护性,为后续数据分析与可视化奠定坚实基础。
在现代网络爬虫系统中,数据采集只是第一步,真正决定项目成败的是后续的数据结构设计与持久化处理能力。一个结构清晰、语义明确的Item模型和一套高效稳定的Pipeline链式处理机制,不仅能显著提升数据质量,还能为下游数据分析、可视化乃至机器学习建模提供坚实基础。以拉勾网Java招聘信息采集为例,原始数据来源复杂、字段格式不一、存在大量噪声信息(如HTML标签、重复职位、缺失薪资等),这就要求我们在Scrapy框架内构建完整的数据治理流程。
本章将围绕 数据建模—清洗—过滤—存储 这一主线,深入探讨如何从零开始设计符合业务需求的 JobInfo 数据结构,并通过多阶段Pipeline实现数据的标准化流转。重点剖析字段命名规范对后期分析的影响、字符串预处理的技术细节、CSV导出时编码问题的根源及解决方案。整个过程不仅涉及代码逻辑的编写,更包含工程思维的体现——即如何在保证性能的前提下提升数据一致性与可维护性。
在Scrapy中, Item 是所有抓取数据的载体,它类似于Python中的字典但具备更强的结构约束能力。合理设计 Item 类不仅是组织数据的第一步,更是确保后续Pipeline处理逻辑简洁高效的前提。
当面对拉勾网这类结构化程度较高的招聘网站时,我们可以通过前端页面或XHR接口返回的JSON数据提取出关键信息点。通过对多个样本进行归纳,可以抽象出如下核心字段:
Field() Field() Field() Field() Field() Field() Field() Field() Field() Field() Field() Field() 这些字段共同构成了 JobInfo 的基本骨架。其定义位于项目的 items.py 文件中,具体实现如下:
import scrapy
class JobInfo(scrapy.Item):
title = scrapy.Field()
company_name = scrapy.Field()
city = scrapy.Field()
district = scrapy.Field()
salary = scrapy.Field()
experience = scrapy.Field()
education = scrapy.Field()
job_tags = scrapy.Field()
publish_date = scrapy.Field()
job_desc = scrapy.Field()
company_size = scrapy.Field()
finance_stage = scrapy.Field()
代码逻辑逐行解读:
- 第1行导入
scrapy模块,为定义Item提供基础支持;- 第4行定义
JobInfo类并继承自scrapy.Item,表示这是一个Scrapy专用的数据容器;- 每个字段使用
scrapy.Field()实例化,这是一种元数据容器,允许附加序列化规则、校验函数等扩展属性;- 所有字段均为可选,默认值为
None,若需强制校验应在Pipeline中补充验证逻辑。
该设计遵循了最小完备原则——只保留对分析有价值的核心字段,避免冗余信息拖慢处理速度。同时,字段命名采用小写加下划线风格(snake_case),与Python社区惯例保持一致,有利于后期集成pandas等数据分析工具。
良好的字段命名不仅仅是代码美观的问题,更直接影响团队协作效率与系统的可维护性。尤其在大型项目中,不同开发者可能负责不同的Spider模块,统一的命名规范能有效降低沟通成本。
假设某团队成员在另一个Spider中定义了如下字段:
jobTitle = scrapy.Field() # 驼峰命名
eduRequirement = scrapy.Field()
workExp = scrapy.Field()
而主流程使用的却是:
title = scrapy.Field()
education = scrapy.Field()
experience = scrapy.Field()
这种差异会导致合并数据集时必须额外做映射转换,增加出错风险。因此,应提前制定命名标准并在团队内部推行。
publish_date 而非 publishDate ; exp 可能指experience或expression,应写全称; company_finance_stage 优于 finance_company ; job_tags 表明其为列表结构。 此外,还可结合注释文档进一步增强可读性:
class JobInfo(scrapy.Item):
"""
拉勾网职位信息数据模型
所有字段均为字符串类型,部分字段可能为空(None)
"""
title = scrapy.Field(
serializer=str,
description="职位标题,用于关键词提取"
)
salary = scrapy.Field(
input_processor=MapCompose(remove_html),
description="月薪范围,单位为K"
)
虽然Scrapy原生不强制字段类型,但通过 serializer 和 input_processor 等参数预留了扩展空间,便于未来接入 scrapy-itemloaders 进行自动化预处理。
graph TD
A[原始响应数据] --> B{是否符合JobInfo结构?}
B -->|是| C[进入Pipeline链]
B -->|否| D[丢弃或记录错误日志]
C --> E[数据清洗]
E --> F[字段校验]
F --> G[持久化输出]
G --> H[(CSV/数据库)]
上图展示了从原始Response到最终存储的完整流向,其中 JobInfo 作为中间枢纽,承担着统一接口的作用。只有经过标准化封装的数据才能顺利进入下一阶段,从而保障整个系统的稳定性。
Scrapy的 Pipeline 组件是实现数据后处理的核心机制。通过注册多个独立的Pipeline类,我们可以构建一条“流水线”,每个环节专注于特定任务,如清洗、去重、验证、导出等。这种解耦设计极大提升了系统的灵活性与可测试性。
许多网站(包括拉勾网)在职位描述中嵌入富文本内容,导致抓取到的 job_desc 字段包含大量HTML标签和非法空白符(如 零宽空格)。直接存储会影响后续文本分析效果,必须进行清洗。
import re
from w3lib.html import remove_tags
from scrapy.exceptions import DropItem
class CleanTextPipeline:
def process_item(self, item, spider):
# 清理职位描述中的HTML标签
if item.get('job_desc'):
cleaned = remove_tags(item['job_desc'])
# 替换多种空白字符为单个空格
cleaned = re.sub(r's+', ' ', cleaned)
# 去除首尾空格
item['job_desc'] = cleaned.strip()
# 清理公司名称中的多余空格
if item.get('company_name'):
item['company_name'] = re.sub(r's+', ' ', item['company_name']).strip()
return item
参数说明与逻辑分析:
remove_tags()来自w3lib.html库,专用于安全移除HTML/XML标签而不解析DOM树,速度快且轻量;re.sub(r's+', ' ', ...)使用正则匹配任意连续空白字符(空格、制表符、换行、零宽字符等)并替换为单一空格;.strip()确保两端无残留空格;- 方法返回修改后的
item对象,供下一个Pipeline继续处理;若发现严重错误可抛出DropItem("reason")终止流程。
此Pipeline应在 settings.py 中启用:
ITEM_PIPELINES = {
'lagou_spider.pipelines.CleanTextPipeline': 300,
}
数字代表执行顺序,数值越小越早执行。
并非所有成功抓取的Item都值得保存。实践中常遇到以下问题:
- 爬虫误触测试岗位(如“实习生岗”、“内部推荐”)
- 同一职位被多次抓取造成重复
- 关键字段缺失(如无薪资、无城市)
为此设计一个过滤Pipeline:
class FilterJobPipeline:
def __init__(self):
self.seen_titles = set() # 用于去重
def process_item(self, item, spider):
title = item.get('title', '').lower()
city = item.get('city')
salary = item.get('salary')
# 规则1:排除非正式岗位
exclude_keywords = ['实习', '兼职', '远程', '外包']
if any(kw in title for kw in exclude_keywords):
raise DropItem(f"Filtered out internship/job: {title}")
# 规则2:必填字段校验
if not city or not salary:
raise DropItem("Missing required fields: city or salary")
# 规则3:基于标题+城市的简单去重
identifier = f"{title}_{city}"
if identifier in self.seen_titles:
raise DropItem(f"Duplicate job detected: ")
self.seen_titles.add(identifier)
return item
逻辑解析:
- 使用内存集合
seen_titles缓存已见职位标识,防止重复入库;- 过滤关键词列表可根据业务动态调整;
- 异常情况通过
raise DropItem中断传递,Scrapy会自动记录丢弃数量;- 注意该类状态依赖于单个爬虫实例生命周期,在分布式环境下需改用Redis等外部存储。
完成清洗与过滤后,最终需将数据落盘。最常用的方式是导出为CSV文件,兼容性强且易于导入Excel、Power BI等工具。
import csv
import os
class CsvExportPipeline:
def __init__(self):
self.file = None
self.writer = None
def open_spider(self, spider):
filepath = 'output/lagou_java_jobs.csv'
os.makedirs(os.path.dirname(filepath), exist_ok=True)
self.file = open(filepath, 'w', newline='', encoding='utf-8-sig')
self.writer = csv.DictWriter(self.file, fieldnames=[
'title', 'company_name', 'city', 'district',
'salary', 'experience', 'education', 'job_tags',
'publish_date', 'job_desc', 'company_size', 'finance_stage'
])
self.writer.writeheader() # 写入表头
def close_spider(self, spider):
if self.file:
self.file.close()
def process_item(self, item, spider):
self.writer.writerow(dict(item))
return item
参数解释:
newline=''防止Windows平台写入多余空行;encoding='utf-8-sig'确保Excel正确识别UTF-8编码,避免中文乱码;open_spider()和close_spider()由Scrapy自动调用,适合作为资源初始化与释放入口;DictWriter直接接受字典型Item,无需手动索引。
import pandas as pd
class PandasCsvPipeline:
def __init__(self):
self.data = []
def close_spider(self, spider):
df = pd.DataFrame(self.data)
df.to_csv('output/jobs_analysis_ready.csv', index=False, encoding='utf-8')
def process_item(self, item, spider):
self.data.append(dict(item))
return item
优势分析:
- 利用pandas强大的I/O能力,支持自动类型推断;
- 输出文件可直接用于第五章的词云生成;
- 若数据量过大,可分批写入或切换至Parquet/HDF5格式。
高质量的数据是任何数据驱动项目的基石。在真实场景中,网络波动、反爬策略变化、目标网站改版等因素都会影响数据完整性。因此,必须建立多层次的质量控制体系。
尽管前端展示看似完整,但API接口有时会遗漏某些字段(如 district 为空)。此时不应简单跳过,而应根据上下文智能补全。
class ValidateAndImputePipeline:
CITY_DISTRICT_MAP = {
'北京': ['朝阳区', '海淀区', '昌平区'],
'上海': ['浦东新区', '徐汇区', '静安区']
}
def process_item(self, item, spider):
required_fields = ['title', 'salary', 'city']
for field in required_fields:
if not item.get(field):
raise DropItem(f"Missing required field: {field}")
# 智能填充district
if not item.get('district') and item['city']:
item['district'] = self.CITY_DISTRICT_MAP.get(item['city'], ['未知区域'])[0]
return item
策略说明:
- 对绝对关键字段执行严格校验;
- 非关键字段允许默认值填充,提高数据覆盖率;
- 映射表可外置为JSON配置文件以便热更新。
中文乱码的根本原因在于编码不一致。建议全程采用UTF-8:
'Accept-Encoding': 'utf-8' utf-8 或 utf-8-sig (带BOM) 特别注意Jupyter Notebook或Excel打开CSV时需手动选择编码,否则仍显示乱码。可通过添加BOM头解决:
with open('data.csv', 'wb') as f:
f.write(b'xefxbbxbf') # UTF-8 with BOM
f.write(content.encode('utf-8'))
综上所述,一个健壮的持久化流程应当覆盖 结构设计 → 多级清洗 → 智能过滤 → 安全导出 → 质量监控 全流程。唯有如此,才能确保从互联网获取的原始数据真正转化为可用的知识资产。
在完成Java招聘信息的采集与结构化存储后,下一步是将文本数据转化为可用于可视化的词频信息。此过程的核心在于对职位描述(job_desc)字段进行清洗和分词处理,提取出反映岗位技能要求的关键术语。
首先,使用 pandas 读取已导出的CSV文件,并筛选出非空的职位描述字段:
import pandas as pd
# 读取Scrapy导出的CSV数据
df = pd.read_csv('lagou_java_jobs.csv', encoding='utf-8')
# 去除缺失值并合并所有职位描述
descriptions = df['job_desc'].dropna().tolist()
combined_text = ''.join(descriptions)
接下来,利用 jieba 进行中文分词,并加载自定义停用词表以过滤常见无意义词汇(如“我们”、“负责”、“具有”等):
import jieba
import jieba.analyse
# 加载停用词表
def load_stopwords(stopword_file):
with open(stopword_file, 'r', encoding='utf-8') as f:
return set([line.strip() for line in f])
stopwords = load_stopwords('stopwords.txt')
# 使用jieba进行精确模式分词
words = [word for word in jieba.cut(combined_text) if len(word) > 1 and word not in stopwords]
随后构建词频字典,用于后续词云权重计算:
from collections import Counter
word_freq = Counter(words)
print("Top 20高频词汇:")
for word, freq in word_freq.most_common(20):
print(f"{word}: {freq}")
输出示例(不少于10行):
| 词汇 | 频次 |
|------------|------|
| Spring | 432 |
| MySQL | 398 |
| 分布式 | 365 |
| Redis | 341 |
| 微服务 | 327 |
| 多线程 | 302 |
| 设计模式 | 289 |
| JVM | 275 |
| 消息队列 | 263 |
| Nginx | 251 |
| 高并发 | 244 |
| Maven | 238 |
| ZooKeeper | 226 |
| Tomcat | 219 |
| Linux | 210 |
| Git | 203 |
| Docker | 195 |
| RESTful | 187 |
| ORM | 176 |
| 缓存 | 169 |
该词频字典将成为生成词云图的基础输入数据。
基于上述词频统计结果,调用 wordcloud 库生成高质量的词云图像。关键参数设置如下:
from wordcloud import WordCloud
import matplotlib.pyplot as plt
# 自定义字体路径(确保支持中文)
font_path = 'SimHei.ttf' # 可替换为系统字体路径
wc = WordCloud(
font_path=font_path, # 中文字体支持
width=1200,
height=800,
background_color='white',
max_words=200,
max_font_size=150,
relative_scaling=0.6,
colormap='viridis', # 使用科学配色方案
contour_width=2,
contour_color='steelblue'
)
# 根据词频生成词云
wc.generate_from_frequencies(word_freq)
# 保存图像
wc.to_file('java_job_skills_wordcloud.png')
此外,可结合遮罩图像实现形状定制,例如使用编程语言图标或公司LOGO轮廓作为背景形状:
import numpy as np
from PIL import Image
mask = np.array(Image.open("code_logo_mask.png")) # 形状遮罩
wc_with_mask = WordCloud(..., mask=mask).generate_from_frequencies(word_freq)
通过调整 colormap 、 background_color 和 contour_color 等参数,可以显著提升视觉表现力,使词云更贴合技术主题风格。
使用 matplotlib 展示生成的词云图像,并优化布局以增强可读性:
plt.figure(figsize=(15, 10))
plt.imshow(wc, interpolation='bilinear')
plt.axis('off')
plt.title('Java开发岗位技能需求词云分析', fontsize=24, pad=20)
plt.tight_layout()
plt.show()
从生成的词云图中可直观看出:
- Spring 和 MySQL 显著突出,表明其为Java岗位最基础的技术栈;
- 分布式 、 高并发 、 微服务 等关键词位置靠前,反映出企业对系统架构能力的高度关注;
- 容器化相关技术如 Docker 和中间件 Redis 、 Nginx 出现频率较高,说明DevOps能力已成为加分项;
- JVM调优、多线程编程等底层知识仍被频繁提及,体现对候选人深度技术理解的要求。
这一定量分析为求职者技能规划和技术团队人才画像提供了数据支撑。
为实现端到端的数据更新闭环,编写主控脚本整合爬虫与可视化模块:
import os
import time
from datetime import datetime
def run_full_pipeline():
print(f"[{datetime.now()}] 开始执行全自动化流程...")
# 步骤1:启动Scrapy爬虫
os.system("scrapy crawl lagou_java -o lagou_java_jobs.csv")
# 步骤2:等待数据写入完成
time.sleep(5)
# 步骤3:执行词云生成脚本
os.system("python generate_wordcloud.py")
print(f"[{datetime.now()}] 流程执行完毕,词云已更新。")
if __name__ == "__main__":
run_full_pipeline()
进一步结合操作系统定时任务(Linux crontab),实现每日自动刷新:
# 每天上午9点运行一次
0 9 * * * /usr/bin/python3 /path/to/main_pipeline.py
同时,在请求层面遵守合规性原则:
- 设置合理 DOWNLOAD_DELAY = 2 防止服务器压力过大;
- 启用 ROBOTSTXT_OBEY = True 尊重站点协议;
- 添加随机User-Agent轮换机制降低被封禁风险。
整个流程形成了“数据采集 → 清洗 → 分析 → 可视化 → 更新”的完整MLOps式工作流,具备长期运维价值。
本文还有配套的精品资源,点击获取
简介:本文详细介绍如何使用Python的Scrapy框架爬取拉勾网上的Java职位信息,涵盖从项目搭建、Spider编写、数据解析到持久化存储的完整流程。通过解析Ajax接口返回的JSON数据,提取职位名称、公司信息等工作关键字段,并利用Item Pipeline将数据导出为CSV文件。最后,结合wordcloud和matplotlib库对抓取结果进行文本分析,去除停用词后生成直观的词云图表,实现数据可视化。项目严格遵守robots.txt规范,倡导合法合规的网络爬虫实践,适用于Python爬虫入门与数据分析实战学习。
本文还有配套的精品资源,点击获取