Scrapy框架实战:从零构建腾讯招聘数据爬虫
1. 项目缘起与目标为什么选择Scrapy来爬取腾讯招聘最近在帮一个做人力资源分析的朋友处理数据他需要一份结构化的、覆盖多个岗位的腾讯招聘数据来做市场薪酬和技能需求分析。一开始我尝试用requestsBeautifulSoup写个脚本但很快就遇到了几个头疼的问题腾讯招聘的页面结构比较复杂分页逻辑需要处理反爬机制比如请求头校验、频率限制也得自己一层层去试更别提数据清洗和存储的麻烦了。写了几百行代码感觉一大半时间都在处理网络请求的异常和解析的边角情况效率很低。这时候就该请出我们的老朋友——Scrapy了。对于腾讯招聘这种数据量大、页面结构规整、需要稳定持续抓取的网站Scrapy框架几乎是量身定做的。它内置的异步请求引擎、健壮的中间件系统、以及清晰的数据流管道Item Pipeline能让我们把精力集中在核心的解析逻辑上而不是陷在繁琐的网络IO和异常处理里。这个项目我们就来手把手搭建一个完整的Scrapy爬虫目标是爬取腾讯招聘官网以社会招聘为例的职位列表页和详情页数据并将结果结构化地保存下来。最终我们希望得到包含职位名称、部门、工作地点、职责要求、发布时间等关键字段的CSV或JSON文件为后续的数据分析打下坚实基础。2. 环境准备与项目初始化搭建你的第一个Scrapy爬虫工欲善其事必先利其器。在开始写代码之前我们需要先把环境搭建好。这个过程看似简单但很多新手容易在这里踩坑尤其是Python环境管理和依赖安装。2.1 Python环境与Scrapy安装首先确保你有一个干净的Python环境。我强烈建议使用conda或venv创建独立的虚拟环境避免包版本冲突。这里以venv为例# 1. 创建项目目录并进入 mkdir tencent_recruitment_spider cd tencent_recruitment_spider # 2. 创建虚拟环境假设使用Python3.8 python -m venv venv # 3. 激活虚拟环境 # 在Windows上 venv\Scripts\activate # 在macOS/Linux上 source venv/bin/activate # 4. 安装Scrapy pip install scrapy -i https://pypi.tuna.tsinghua.edu.cn/simple注意安装Scrapy时它会自动安装一系列依赖如Twisted异步网络框架、lxml和cssselect解析库。如果遇到Twisted安装失败特别是在Windows上可以尝试先安装其预编译的wheel文件或者使用conda install scrapyconda的环境管理对Windows更友好。安装完成后可以验证一下scrapy version # 应该输出类似 Scrapy 2.11.0 的信息2.2 创建Scrapy项目与爬虫Scrapy使用项目Project来组织代码。一个项目下可以包含多个爬虫Spider。我们使用命令行工具来快速搭建骨架# 在当前目录下创建一个名为 tencent 的Scrapy项目 scrapy startproject tencent这个命令会生成一个名为tencent的文件夹里面包含了Scrapy项目的标准结构tencent/ ├── scrapy.cfg # 项目部署配置文件 └── tencent/ # 项目Python模块 ├── __init__.py ├── items.py # 定义要爬取的数据结构 ├── middlewares.py # 自定义中间件处理请求、响应 ├── pipelines.py # 数据管道清洗、验证、存储 ├── settings.py # 项目设置文件非常重要 └── spiders/ # 爬虫代码存放目录 └── __init__.py接下来我们在spiders目录下创建第一个爬虫。假设我们要爬取腾讯社会招聘的列表页其URL模式可能是https://careers.tencent.com/search.html?keywordpython。我们给爬虫起名为recruitmentcd tencent scrapy genspider recruitment careers.tencent.com这个命令会在tencent/spiders/目录下生成一个名为recruitment.py的文件里面有一个爬虫的基本模板。但请注意这个自动生成的模板通常需要大改特别是start_urls和parse方法。我们更倾向于手动创建一个更符合需求的爬虫文件。所以我通常会直接删除自动生成的文件然后在spiders目录下新建一个tencent_spider.py从头开始编写。这样对理解Scrapy的工作流程更有帮助。3. 核心爬虫Spider编写解析列表页与详情页爬虫是Scrapy项目的核心它定义了从哪里开始爬取、如何跟踪链接、以及如何从页面中提取结构化数据。我们的策略是经典的“广度优先”先抓取列表页从中提取详情页的链接再发起请求去抓取详情页数据。3.1 分析目标网站结构与反爬策略在写代码前必须先用浏览器开发者工具F12仔细分析腾讯招聘的页面。列表页分析打开一个搜索结果的URL查看网络请求。重点关注请求URL观察分页参数是如何变化的。例如可能是?page2也可能是通过POST请求携带pageIndex参数。请求头Headers检查是否有特殊的User-Agent、Referer或Cookie要求。腾讯这类大站通常会对请求头做基础校验。数据加载方式页面是服务端渲染SSR还是前端异步加载Ajax查看“网络”选项卡的XHR/Fetch请求看数据是否通过接口返回返回JSON格式。经过实际查看腾讯招聘的社会招聘列表页数据是通过Ajax接口异步加载的这是一个关键点。详情页分析点击一个职位进入详情页同样分析其URL结构和页面内容构成。反爬措施User-Agent检测这是最基本的必须设置一个常见的浏览器UA。请求频率限制在settings.py中必须设置下载延迟DOWNLOAD_DELAY建议设为2-5秒避免对服务器造成压力也降低被封IP的风险。Cookie/Session某些操作可能需要维持会话。基于分析我们发现列表页的真实数据接口可能是类似https://careers.tencent.com/tencentcareer/api/post/Query?pageIndex1pageSize10这样的地址返回JSON数据。这比解析HTML要简单得多3.2 定义数据模型Item在tencent/items.py中我们定义想要爬取的字段。这就像为数据设计一张表结构。import scrapy class TencentItem(scrapy.Item): # 定义字段就像定义数据库表的列 job_id scrapy.Field() # 职位ID job_title scrapy.Field() # 职位名称 department scrapy.Field() # 所属部门 location scrapy.Field() # 工作地点 category scrapy.Field() # 职位类别 responsibility scrapy.Field() # 工作职责 requirement scrapy.Field() # 职位要求 publish_time scrapy.Field() # 发布时间 source_url scrapy.Field() # 详情页URL # 可以根据需要添加更多字段如学历要求、经验要求等3.3 编写爬虫主逻辑现在在tencent/spiders/目录下创建tencent_spider.py。import scrapy import json from tencent.items import TencentItem class TencentRecruitmentSpider(scrapy.Spider): name tencent_recruitment # 爬虫的唯一标识运行爬虫时使用 allowed_domains [careers.tencent.com] # 限制爬虫只爬取该域名下的链接 # 起始URL这里我们直接使用分析得到的API接口 def start_requests(self): # 假设我们要爬取前10页每页10条数据 base_url https://careers.tencent.com/tencentcareer/api/post/Query?pageSize10pageIndex{} for page in range(1, 11): # 页码从1开始 url base_url.format(page) # 发起请求并指定回调函数处理响应 yield scrapy.Request(urlurl, callbackself.parse_list, meta{page: page}) def parse_list(self, response): 解析列表页API返回的JSON数据 page response.meta.get(page, 1) self.logger.info(f正在解析第 {page} 页列表数据) try: # 将响应体解析为JSON data json.loads(response.text) # 根据实际接口返回结构找到职位列表。这里假设数据在 Data - Posts 下 posts data.get(Data, {}).get(Posts, []) if not posts: self.logger.warning(f第 {page} 页未获取到职位数据接口结构可能已变化。) return for post in posts: # 提取详情页的链接或ID。假设详情页URL需要组合或者接口直接提供了Link # 情况1接口直接提供了详情页URL detail_url post.get(PostURL) # 情况2需要自己拼接 (更常见) # post_id post.get(PostId) # detail_url fhttps://careers.tencent.com/jobdesc.html?postId{post_id} if detail_url: # 对详情页发起请求并传递当前已提取的部分信息给详情页解析函数 yield scrapy.Request( urldetail_url, callbackself.parse_detail, meta{post_info: post} # 将列表页信息通过meta传递下去 ) else: self.logger.error(f未找到职位 {post.get(RecruitPostName)} 的详情链接。) except json.JSONDecodeError as e: self.logger.error(f解析第 {page} 页JSON数据失败: {e}, 响应文本: {response.text[:200]}) def parse_detail(self, response): 解析职位详情页HTML post_info response.meta.get(post_info, {}) item TencentItem() # 1. 填充从列表页API获取的信息 item[job_id] post_info.get(PostId) item[job_title] post_info.get(RecruitPostName) item[department] post_info.get(BGName) item[location] post_info.get(LocationName) item[category] post_info.get(CategoryName) item[publish_time] post_info.get(LastUpdateTime) item[source_url] response.url # 2. 从详情页HTML中提取更详细的信息职责和要求 # 使用XPath或CSS选择器。这里需要根据实际页面结构调整。 # 假设职责和要求在特定的div里class为 ‘job-detail’ # 强烈建议先在Scrapy Shell中测试选择器scrapy shell ‘详情页URL’ responsibility response.xpath(//div[contains(class, responsibility)]//text()).getall() requirement response.xpath(//div[contains(class, requirement)]//text()).getall() # 清洗数据合并文本去除空白字符 item[responsibility] .join(.join(responsibility).split()) item[requirement] .join(.join(requirement).split()) # 记录日志 self.logger.info(f成功抓取职位: {item[job_title]}) # 将Item返回给引擎进而交给Item Pipeline处理 yield item关键点解析start_requests方法替代了简单的start_urls列表让我们可以更灵活地构造初始请求比如添加自定义请求头、携带表单数据等。meta参数这是Scrapy中在不同回调函数间传递数据的“瑞士军刀”。我们把列表页提取的post_info通过meta传递给详情页请求这样在parse_detail中就能直接使用避免了重复解析或额外请求。错误处理在parse_list中我们使用try-except捕获JSON解析错误并用logger记录。在生产环境中完善的错误处理和日志记录至关重要。选择器在详情页解析中我们用了XPath。//text()获取所有子孙文本节点getall()返回列表。之后用join和split进行简单的清洗去除多余的空白和换行。务必在写代码前使用scrapy shell ‘url’命令交互式地测试你的XPath或CSS选择器是否准确这是提高开发效率的秘诀。4. 配置与中间件调优让爬虫更稳健、更高效爬虫写好了但直接运行可能会被网站屏蔽或者效率不高。我们需要通过settings.py和中间件进行精细化的配置。4.1 关键配置settings.py打开tencent/settings.py修改以下关键配置# 1. 遵守Robots协议对于正规项目建议开启但测试时如果目标站robots.txt禁止爬取可以暂时关闭 ROBOTSTXT_OBEY False # 根据实际情况调整正式爬取建议先设置为True查看规则 # 2. 并发请求数默认16。对于腾讯这种大站不宜过高避免被封。 CONCURRENT_REQUESTS 8 # 3. 下载延迟秒控制请求频率是应对反爬最重要的设置之一。 DOWNLOAD_DELAY 3 # 随机化延迟让请求间隔更接近人类行为 RANDOMIZE_DOWNLOAD_DELAY True # 4. 请求头特别是User-Agent必须设置成常见的浏览器。 DEFAULT_REQUEST_HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, 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, } # 5. 启用或配置Item Pipeline ITEM_PIPELINES { tencent.pipelines.TencentPipeline: 300, # 数字代表优先级越小越先执行 } # 6. 启用或配置下载器中间件 DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, # 禁用默认的 tencent.middlewares.RotateUserAgentMiddleware: 543, # 使用自定义的随机UA中间件 # tencent.middlewares.ProxyMiddleware: 544, # 如果需要代理可以启用 } # 7. 深度限制和爬取范围可选 DEPTH_LIMIT 0 # 0表示无限制根据需求设置 CLOSESPIDER_ITEMCOUNT 1000 # 爬取1000个Item后自动关闭用于测试4.2 自定义中间件实战随机User-Agent网站经常会检测User-Agent如果一直用一个容易被识别为爬虫。我们需要一个中间件来为每个请求随机分配一个UA。在tencent/middlewares.py中import random class RotateUserAgentMiddleware: 随机User-Agent中间件 # 准备一个常见的浏览器UA列表 user_agent_list [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/120.0, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, ] def process_request(self, request, spider): # 在请求发出前随机选择一个UA并设置 ua random.choice(self.user_agent_list) request.headers[User-Agent] ua # 不需要返回Scrapy会继续处理这个request然后在settings.py中启用它并禁用默认的UserAgentMiddleware如上一节所示。这样你的每个请求都会携带不同的User-Agent大大降低了被简单规则屏蔽的风险。4.3 应对更复杂的反爬IP代理与请求重试如果网站有严格的IP频率限制你可能需要用到代理IP池。同样在middlewares.py中创建一个代理中间件class ProxyMiddleware: 代理IP中间件示例需要你自己实现代理IP的获取和验证逻辑 def process_request(self, request, spider): # 这里假设你有一个方法能返回一个可用的代理IP proxy self.get_proxy() if proxy: request.meta[proxy] proxy # 例如 http://123.45.67.89:8080 spider.logger.debug(fUsing proxy: {proxy}) def get_proxy(self): # 实现从你的代理IP池可能是本地文件、数据库或API获取一个IP # 返回格式应为 http://ip:port 或 https://ip:port # 这是一个复杂的话题涉及IP的付费购买、质量校验和动态更换 # 此处仅作示例 # return http://your-proxy-ip:port return None此外Scrapy内置了重试中间件RetryMiddleware。在settings.py中你可以配置RETRY_ENABLED True RETRY_TIMES 2 # 对失败请求重试2次 RETRY_HTTP_CODES [500, 502, 503, 504, 408, 429] # 对这些状态码重试当遇到网络波动或服务器返回429Too Many Requests时Scrapy会自动重试增加了爬虫的健壮性。5. 数据处理与存储管道Item Pipeline爬取到的Item对象会依次通过ITEM_PIPELINES中定义的各个管道。管道用于进行数据清洗、去重和存储。我们在tencent/pipelines.py中实现一个管道将数据保存到CSV文件。5.1 实现CSV存储管道import csv import os from itemadapter import ItemAdapter from scrapy.exporters import CsvItemExporter # 更推荐使用Scrapy内置的Exporter class TencentPipeline: def open_spider(self, spider): # 爬虫启动时执行 self.file open(tencent_jobs.csv, w, newline, encodingutf-8-sig) # utf-8-sig解决Excel打开乱码 # 定义CSV的列名顺序与Item字段对应 fieldnames [ job_id, job_title, department, location, category, responsibility, requirement, publish_time, source_url ] self.writer csv.DictWriter(self.file, fieldnamesfieldnames) self.writer.writeheader() spider.logger.info(CSV文件已打开准备写入数据。) def process_item(self, item, spider): # 对每个Item进行处理 # 1. 数据清洗示例如果职责为空填充默认值 if not item.get(responsibility): item[responsibility] 暂无描述 # 2. 可以在这里进行数据验证比如检查必填字段 if not item.get(job_title): spider.logger.warning(f发现一个无标题的职位已跳过。URL: {item.get(source_url)}) return item # 即使丢弃也最好返回item或者抛出DropItem异常 # 将Item类字典对象写入CSV self.writer.writerow(ItemAdapter(item).asdict()) return item def close_spider(self, spider): # 爬虫关闭时执行 self.file.close() spider.logger.info(CSV文件已保存爬虫关闭。)5.2 使用Scrapy内置的Exporter更优雅的方式上面的方式需要手动处理文件。Scrapy提供了更强大的Item Exporter支持多种格式JSON, CSV, XML等并且能自动处理字段顺序和文件打开关闭。from scrapy.exporters import CsvItemExporter class CsvExportPipeline: def open_spider(self, spider): self.file open(tencent_jobs_export.csv, wb) # 注意是二进制写入 self.exporter CsvItemExporter(self.file, encodingutf-8-sig) self.exporter.fields_to_export [ # 指定字段顺序 job_id, job_title, department, location, category, responsibility, requirement, publish_time, source_url ] self.exporter.start_exporting() def process_item(self, item, spider): # 在这里进行数据清洗 # ... self.exporter.export_item(item) return item def close_spider(self, spider): self.exporter.finish_exporting() self.file.close()在settings.py中启用这个管道并设置优先级。你可以同时启用多个管道比如一个存CSV一个存数据库。5.3 数据去重管道爬虫可能会爬取到重复的页面比如详情页链接在多个列表页出现。一个简单的基于job_id的内存去重管道可以这样写from scrapy.exceptions import DropItem class DuplicatesPipeline: def __init__(self): self.ids_seen set() def process_item(self, item, spider): adapter ItemAdapter(item) job_id adapter.get(job_id) if job_id is None: return item # 没有ID无法去重 if job_id in self.ids_seen: spider.logger.debug(f重复职位ID: {job_id}已丢弃。) raise DropItem(fDuplicate item found: {item}) # 抛出异常该Item不会被后续管道处理 else: self.ids_seen.add(job_id) return item在settings.py中确保去重管道高优先级如100在存储管道低优先级如300之前执行。6. 运行、调试与数据验证一切就绪让我们运行爬虫并看看成果。6.1 运行爬虫在项目根目录有scrapy.cfg的目录下执行scrapy crawl tencent_recruitment -o output.json -s CLOSESPIDER_ITEMCOUNT50crawl tencent_recruitment运行名为tencent_recruitment的爬虫。-o output.json将爬取的Item直接输出到output.json文件。这是一个非常方便的调试功能Scrapy支持多种格式.json,.jl,.csv,.xml。注意对于大型项目使用-o可能会占用大量内存因为它会收集所有Item再写入。生产环境建议用Item Pipeline。-s CLOSESPIDER_ITEMCOUNT50这是一个设置参数表示爬取50个Item后自动停止非常适合测试。6.2 日志与调试Scrapy有丰富的日志系统。默认是INFO级别。如果你想看到更详细的请求和响应信息可以scrapy crawl tencent_recruitment --loglevelDEBUG在代码中使用self.logger.debug/info/warning/error()来记录不同级别的日志这对于监控爬虫运行状态和排查问题至关重要。最重要的调试工具Scrapy Shell当你的选择器写错了或者不知道页面结构时不要盲目改代码。打开终端scrapy shell ‘https://careers.tencent.com/jobdesc.html?postId123456’这会进入一个交互式环境response对象就是请求的响应。你可以直接测试XPath或CSS选择器response.xpath(//h1/text()).get() response.css(div.job-detail::text).getall()直到找到正确的选择器表达式再复制到你的爬虫代码中。6.3 数据验证与常见问题排查爬虫跑完后打开生成的CSV或JSON文件检查数据。字段为空检查详情页的选择器是否正确。页面结构可能已经更新。回到Scrapy Shell重新分析。数据错乱检查parse_list中从JSON提取字段的键名是否正确。接口返回结构可能因页面社招/校招不同而不同。爬取数量远少于预期检查列表页API是否真的返回了数据。可能是分页参数不对或者有totalPage限制。检查parse_list中的循环逻辑是否因为某个post没有detail_url导致中断我们代码里用了continue是合理的。查看日志中是否有大量的WARNING或ERROR特别是403 Forbidden或429 Too Many Requests。这可能是反爬机制触发了需要调整DOWNLOAD_DELAY或添加更完善的请求头、使用代理。被屏蔽/返回验证码这是最棘手的问题。除了降低频率、使用随机UA和代理可能还需要模拟更完整的浏览器行为通过Selenium或Playwright中间件但这会极大降低速度。识别并处理网站设置的Cookie如acw_tc等。对于验证码通常需要接入打码平台这超出了基础爬虫的范围且需谨慎评估法律和道德风险。7. 项目优化与扩展思考一个能跑起来的爬虫只是开始一个健壮、可维护、可扩展的爬虫才是目标。7.1 将配置参数化不要把页码、关键词等硬编码在爬虫里。Scrapy支持通过-a参数传递参数给爬虫class TencentRecruitmentSpider(scrapy.Spider): name tencent_recruitment def __init__(self, keywordpython, max_pages10, *args, **kwargs): super(TencentRecruitmentSpider, self).__init__(*args, **kwargs) self.keyword keyword self.max_pages int(max_pages) self.base_url f‘https://careers.tencent.com/tencentcareer/api/post/Query?keyword{keyword}pageSize10pageIndex‘运行命令变为scrapy crawl tencent_recruitment -a keywordjava -a max_pages57.2 使用Item Loader与输入/输出处理器当数据清洗逻辑变得复杂时Item直接赋值会显得混乱。Scrapy提供了ItemLoader它通过输入处理器Input Processor和输出处理器Output Processor来标准化数据处理流程比如自动去除空格、连接列表、转换格式等。# 在items.py中 from scrapy.loader import ItemLoader from scrapy.loader.processors import TakeFirst, Join, MapCompose from w3lib.html import remove_tags def strip_whitespace(value): return value.strip() if value else value class TencentItem(scrapy.Item): # ... 字段定义同上 ... class TencentItemLoader(ItemLoader): default_item_class TencentItem # 定义默认的输入输出处理器 default_input_processor MapCompose(strip_whitespace, remove_tags) default_output_processor TakeFirst() # 为特定字段自定义处理器 responsibility_in MapCompose(strip_whitespace, remove_tags) responsibility_out Join(separator‘\n‘) # 用换行符连接在爬虫中使用from tencent.items import TencentItemLoader def parse_detail(self, response): loader TencentItemLoader(itemTencentItem(), responseresponse) loader.add_value(‘job_id‘, post_info.get(‘PostId‘)) loader.add_xpath(‘responsibility‘, ‘//div[classresponsibility]//text()‘) # ... 添加其他字段 ... yield loader.load_item()这样数据清洗的代码更加清晰和可复用。7.3 分布式爬取与增量爬取分布式当数据量极大时可以考虑使用Scrapy-Redis等组件将请求队列和去重指纹存储在Redis中实现多台机器协同爬取。增量爬取只爬取新增或更新的职位。可以在管道中将job_id和publish_time与已存储的数据对比只保存新的或发布时间更晚的记录。更复杂的增量爬取需要设计数据版本或哈希对比。7.4 道德与法律边界最后也是最重要的我们必须时刻牢记爬虫的伦理和法律边界。尊重robots.txt这是网站与爬虫之间的协议。在settings.py中设置ROBOTSTXT_OBEY True并遵守其中的规则。控制爬取速度设置合理的DOWNLOAD_DELAY不要对目标服务器造成DoS攻击式的压力。仅爬取公开数据不要尝试爬取需要登录才能访问的个人信息、商业机密等非公开数据。明确数据用途爬取的数据应用于个人学习、研究或合法的商业分析在用户协议允许范围内切勿用于非法用途或侵犯他人权益。查看网站条款在使用数据前最好查看一下腾讯招聘网站的服务条款明确其对数据抓取的态度。写完这个爬虫我最大的体会是Scrapy框架的强大不在于让你写更少的代码去发起一个请求而在于它提供了一整套工业级的、可扩展的解决方案来处理爬虫生命周期中的所有复杂问题调度、去重、并发、异常处理、数据流。它迫使你以结构化的方式思考爬虫任务这对于开发中型以上的数据采集项目来说带来的可维护性和效率提升是巨大的。下次当你面对一个数据采集需求时如果它超过了几十个页面别犹豫直接用Scrapy吧前期多花半小时搭框架后期能省下几小时甚至几天的调试时间。