Python Scrapy实战:抓取腾讯招聘数据,掌握反爬与数据存储

1. 项目概述与核心价值

最近在整理数据分析项目时,发现招聘市场数据是个非常有意思的观察窗口,尤其是像腾讯这样的大厂,其招聘需求的变化往往能反映出技术栈的演进和行业的动向。直接去官网一页页翻看效率太低,而且很难做横向对比和历史分析。于是,我决定用 Python 的 Scrapy 框架写一个爬虫,系统性地抓取腾讯招聘官网的职位数据。这不仅仅是一个简单的数据采集练习,更是一个完整的、涉及反爬策略、数据清洗、结构化存储的实战项目,对于想深入理解 Scrapy 框架和应对复杂网站数据抓取的朋友来说,会是一次很好的演练。

这个项目能帮你解决几个实际问题:一是自动化获取海量、实时的招聘信息,为你的求职或市场分析提供数据基础;二是深入掌握 Scrapy 从请求调度、数据解析到管道处理的完整工作流;三是学习如何处理动态加载内容、应对常见的反爬机制(如请求头校验、频率限制)。无论你是刚学完 Scrapy 基础想找个项目练手,还是需要为某个分析任务采集特定数据,这个案例都能提供一条清晰的实现路径。接下来,我会把整个从环境搭建、爬虫编写、到数据存储和问题排查的过程拆开揉碎了讲清楚。

2. 项目整体设计与环境准备

在动手写代码之前,合理的项目规划和环境搭建是成功的一半。对于爬取腾讯招聘这类具有一定反爬措施的商业网站,我们不能上来就蛮干,需要先分析网站结构,设计好爬取策略,并准备好相应的工具链。

2.1 目标网站分析与爬取策略

首先,我们得明确要爬什么、怎么爬。打开腾讯招聘官网,我们可以看到职位通常按类别、地点、关键词等进行筛选和分页展示。我们的爬虫核心任务就是模拟这些筛选和翻页操作,遍历所有职位列表页,提取每个职位详情页的链接,最后进入详情页抓取结构化的职位信息。

这里有几个关键点需要提前规划:

  1. 入口与翻页:确定一个合理的起始URL,例如某个技术类职位的列表页。翻页逻辑需要观察URL规律或分析页面上的“下一页”按钮。
  2. 数据解析:列表页需要解析出职位名称、详情页链接、大概地点等基础信息;详情页则需要解析职位描述、要求、部门、发布时间等完整字段。
  3. 反爬应对:像腾讯这样的网站,很可能会有请求频率检测、请求头验证等基础反爬措施。我们需要在爬虫中设置合理的下载延迟(DOWNLOAD_DELAY),并构造完整的请求头(User-Agent,Referer等)。
  4. 数据存储:规划好抓取下来的数据如何存储。为了便于后续分析,我们选择将数据存储为结构化的CSV文件,同时也会介绍如何存入MySQL数据库,以适应不同规模的需求。

注意:在开始任何爬取动作前,务必仔细阅读目标网站的robots.txt文件(通常在网站根目录,如https://careers.tencent.com/robots.txt),并尊重其中定义的爬取规则。同时,应控制爬取频率,避免对目标网站服务器造成过大压力,这是编写负责任爬虫的基本素养。

2.2 开发环境搭建与依赖安装

工欲善其事,必先利其器。我们使用Python 3.8+版本和Scrapy 2.5+框架。推荐使用condavenv创建独立的虚拟环境,避免包依赖冲突。

# 1. 创建并激活虚拟环境 (以conda为例) conda create -n tencent_spider python=3.9 conda activate tencent_spider # 2. 安装Scrapy框架 pip install scrapy # 3. 安装可能用到的辅助库 # 用于处理可能遇到的JSONP或复杂JSON数据 pip install jsonpath # 如果需要更复杂的请求,可以安装requests,但Scrapy内置的足够强大 # pip install requests

除了核心的 Scrapy,我们还会用到 Python 内置的csv模块进行文件写入。如果计划使用数据库,则需要安装对应的驱动,例如pymysql用于连接 MySQL。

# 安装MySQL驱动 pip install pymysql

环境准备好后,我们就可以初始化 Scrapy 项目了。在合适的目录下执行以下命令:

scrapy startproject tencent_recruitment cd tencent_recruitment scrapy genspider recruitment careers.tencent.com

这条命令创建了一个名为tencent_recruitment的项目,并在其中生成了一个针对careers.tencent.com域名的爬虫文件recruitment.py。项目的骨架已经搭好,接下来就是填充血肉了。

3. 爬虫核心逻辑与代码实现

这一部分是整个项目的核心,我们将一步步构建爬虫的请求、解析和数据处理逻辑。我会详细解释每一步的意图和代码细节,确保你能理解为什么这么做,而不仅仅是复制代码。

3.1 定义数据模型(Items)

首先,我们需要明确要抓取哪些数据字段。在 Scrapy 中,这通过定义Item类来完成。它类似于一个字典,但提供了字段定义,使得数据更结构化,也便于后续的管道处理。

打开项目中的items.py文件,修改如下:

import scrapy class TencentRecruitmentItem(scrapy.Item): # 定义抓取的字段 job_id = scrapy.Field() # 职位ID,通常从URL或数据中提取,用于去重 job_name = scrapy.Field() # 职位名称 job_category = scrapy.Field() # 职位类别(如技术、产品、设计) job_location = scrapy.Field() # 工作地点 job_department = scrapy.Field() # 所属部门 job_responsibility = scrapy.Field() # 工作职责 job_requirement = scrapy.Field() # 职位要求 publish_time = scrapy.Field() # 发布时间 apply_link = scrapy.Field() # 申请链接(通常是详情页URL本身) source_url = scrapy.Field() # 数据来源页面URL,便于追踪和调试

定义好Item后,在爬虫中解析到数据就可以填充到这个容器里,然后交给后续的管道(Pipeline)处理。

3.2 编写爬虫主程序(Spider)

接下来是重头戏,编写recruitment.py爬虫文件。Scrapy 的爬虫核心是继承scrapy.Spider并重写几个关键方法。

import scrapy from tencent_recruitment.items import TencentRecruitmentItem import json from urllib.parse import urljoin class RecruitmentSpider(scrapy.Spider): name = 'recruitment' # 爬虫名称,用于运行命令 allowed_domains = ['careers.tencent.com'] # 允许爬取的域名 # 起始URL,这里以技术类职位为例,实际可根据需要调整 start_urls = ['https://careers.tencent.com/search.html'] def start_requests(self): """ 重写start_requests方法,可以在这里为初始请求添加自定义参数或头信息。 腾讯招聘的列表数据很可能是通过AJAX接口获取的,我们需要找到真正的数据接口。 """ # 首先访问搜索页面,可能只是为了获取Cookie或初始状态 yield scrapy.Request(url=self.start_urls[0], callback=self.parse_list_page) def parse_list_page(self, response): """ 解析列表页,提取职位详情页链接,并处理翻页。 经过分析,腾讯招聘的职位数据是通过一个POST接口动态加载的。 """ # 这里需要分析网页网络请求,找到加载职位列表的API接口 # 假设我们通过分析发现接口是:https://careers.tencent.com/tencentcareer/api/post/Query # 并且需要携带特定的表单数据(如页码、岗位类别等) api_url = 'https://careers.tencent.com/tencentcareer/api/post/Query' # 构造POST请求的表单数据,这里以第一页,查询所有职位为例 form_data = { 'language': 'zh-cn', 'area': 'cn', 'keyword': '', # 关键词留空表示查询所有 'pageIndex': '1', # 页码 'pageSize': '10', # 每页数量 'timestamp': str(int(time.time() * 1000)) # 时间戳,有些接口需要 } # 使用scrapy.FormRequest发送POST请求,并指定回调函数处理返回的JSON数据 yield scrapy.FormRequest( url=api_url, formdata=form_data, callback=self.parse_job_list_json, # 回调函数用于解析JSON meta={'page_index': 1} # 将当前页码传递给回调函数,用于翻页 ) def parse_job_list_json(self, response): """ 处理列表接口返回的JSON数据,提取职位信息,并构造详情页请求。 """ data = json.loads(response.text) # 根据实际接口返回的JSON结构解析,这里是一个示例结构 posts = data.get('Data', {}).get('Posts', []) for post in posts: item = TencentRecruitmentItem() # 从列表数据中提取部分信息 item['job_id'] = post.get('PostId') item['job_name'] = post.get('RecruitPostName') item['job_category'] = post.get('CategoryName') item['job_location'] = post.get('LocationName') # 详情页的URL可能需要拼接 detail_path = post.get('PostURL') if detail_path: detail_url = urljoin('https://careers.tencent.com', detail_path) item['apply_link'] = detail_url # 发起对详情页的请求,将初步填充的item传递过去,以便补充更多字段 yield scrapy.Request( url=detail_url, callback=self.parse_job_detail, meta={'item': item} ) else: # 如果没有详情页链接,暂时保存已获取的信息 yield item # 翻页逻辑:判断是否还有下一页 current_page = response.meta['page_index'] total_page = data.get('Data', {}).get('Count', 0) // 10 + 1 # 假设每页10条,计算总页数 if current_page < total_page: next_page = current_page + 1 next_form_data = { 'language': 'zh-cn', 'area': 'cn', 'keyword': '', 'pageIndex': str(next_page), 'pageSize': '10', 'timestamp': str(int(time.time() * 1000)) } api_url = 'https://careers.tencent.com/tencentcareer/api/post/Query' yield scrapy.FormRequest( url=api_url, formdata=next_form_data, callback=self.parse_job_list_json, meta={'page_index': next_page} ) def parse_job_detail(self, response): """ 解析职位详情页,补充Item的剩余字段。 详情页可能是静态HTML,也可能有额外的数据接口,需要具体分析。 """ # 从meta中获取之前传递过来的item item = response.meta['item'] # 使用XPath或CSS选择器从详情页HTML中提取数据 # 这里需要根据实际页面结构编写选择器,以下为示例 item['job_department'] = response.xpath('//div[@class="job-detail"]//span[contains(text(),"部门")]/following-sibling::text()').get(default='').strip() # 职责和要求可能在一个文本块里,需要仔细处理 responsibility_text = response.xpath('//div[contains(@class, "responsibility")]//text()').getall() item['job_responsibility'] = '\n'.join([text.strip() for text in responsibility_text if text.strip()]) requirement_text = response.xpath('//div[contains(@class, "requirement")]//text()').getall() item['job_requirement'] = '\n'.join([text.strip() for text in requirement_text if text.strip()]) item['publish_time'] = response.xpath('//span[contains(@class, "publish-time")]/text()').get(default='').strip() item['source_url'] = response.url # 记录详情页URL # 至此,item已填充完整,yield给Pipeline处理 yield item

代码要点解析

  1. 寻找数据接口:现代网站大量使用 AJAX,直接解析 HTML 可能拿不到数据。关键步骤是打开浏览器开发者工具(F12),切换到 Network(网络)选项卡,刷新页面或进行筛选操作,观察 XHR/Fetch 请求,找到返回职位列表数据的那个请求,复制其 URL 和请求参数(通常是 POST 的 Form Data)。这是我们爬虫的“生命线”。
  2. 模拟请求:使用scrapy.FormRequest来模拟 POST 请求,并携带必要的参数。注意timestamp这类动态参数可能需要生成。
  3. 翻页逻辑:在解析列表 JSON 的回调函数中,需要根据返回数据中的总条数和当前页码,计算出总页数,然后递归地构造下一页的请求。这是爬虫能够遍历所有数据的关键。
  4. 数据传递:通过Requestmeta参数,可以将初步的item从列表页传递到详情页请求的回调函数中,实现数据的累加填充。
  5. 选择器编写:详情页的解析依赖于稳定的 XPath 或 CSS 选择器。编写后务必在scrapy shell中测试其准确性。页面结构可能会变,所以选择器要尽量健壮,使用contains等函数减少对精确结构的依赖。

3.3 配置爬虫设置(Settings)

Scrapy 项目的行为主要由settings.py文件控制。为了爬虫能稳定、友好地运行,我们需要进行一些关键配置。

# settings.py BOT_NAME = 'tencent_recruitment' SPIDER_MODULES = ['tencent_recruitment.spiders'] NEWSPIDER_MODULE = 'tencent_recruitment.spiders' # 模拟真实浏览器的User-Agent USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' # 遵守robots.txt协议,对于学习项目建议设为True,生产环境视情况而定 ROBOTSTXT_OBEY = True # 配置并发请求数,不要设置太高,避免对目标服务器造成压力 CONCURRENT_REQUESTS = 16 # 下载延迟,单位秒。设置一个合理的延迟(如1-3秒)是礼貌爬虫的基本要求,也能有效避免被禁 DOWNLOAD_DELAY = 2 # 启用或禁用Cookie COOKIES_ENABLED = True # 默认请求头,可以在这里添加一些通用头信息 DEFAULT_REQUEST_HEADERS = { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://careers.tencent.com/search.html', } # 启用并配置Item Pipeline ITEM_PIPELINES = { 'tencent_recruitment.pipelines.TencentRecruitmentPipeline': 300, # 'tencent_recruitment.pipelines.MysqlPipeline': 400, # 如果需要存入MySQL,可以启用这个管道 } # 启用AutoThrottle扩展,自动调整爬取速度,更友好 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 5 AUTOTHROTTLE_MAX_DELAY = 60 AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

设置项解读

  • DOWNLOAD_DELAYCONCURRENT_REQUESTS是控制爬取速度的核心,两者共同决定了每秒的请求数。对于腾讯这类网站,建议保守一些。
  • AUTOTHROTTLE扩展非常有用,它能根据服务器的响应时间自动调整延迟,在服务器负载高时自动慢下来。
  • DEFAULT_REQUEST_HEADERS中的RefererAccept字段对于通过 AJAX 接口获取数据有时是必须的,模拟浏览器行为。

4. 数据处理与存储管道(Pipeline)

爬虫解析出来的Item会依次通过ITEM_PIPELINES中定义的管道进行处理。我们主要实现两个管道:一个用于将数据保存到 CSV 文件,另一个用于存入 MySQL 数据库。

4.1 CSV文件存储管道

这是最简单直接的存储方式,适合数据量不大或快速分析场景。

# pipelines.py import csv import os from itemadapter import ItemAdapter class TencentRecruitmentPipeline: def open_spider(self, spider): """ 爬虫启动时执行,用于打开文件、初始化资源。 """ # 确保output目录存在 if not os.path.exists('output'): os.makedirs('output') # 打开CSV文件,并写入表头 self.file = open('output/tencent_jobs.csv', 'w', newline='', encoding='utf-8-sig') # 定义CSV的字段顺序,与Item中定义的字段对应 fieldnames = [ 'job_id', 'job_name', 'job_category', 'job_location', 'job_department', 'job_responsibility', 'job_requirement', 'publish_time', 'apply_link', 'source_url' ] self.writer = csv.DictWriter(self.file, fieldnames=fieldnames) self.writer.writeheader() def process_item(self, item, spider): """ 处理每一个Item。将Item(字典形式)写入CSV文件。 """ # 使用ItemAdapter确保兼容性 adapter = ItemAdapter(item) # 确保所有字段都存在,避免KeyError row = {field: adapter.get(field, '') for field in self.writer.fieldnames} self.writer.writerow(row) # 返回item,以便后续的Pipeline(如果有)继续处理 return item def close_spider(self, spider): """ 爬虫关闭时执行,用于关闭文件、释放资源。 """ self.file.close() spider.logger.info(f'CSV文件已保存至 output/tencent_jobs.csv')

4.2 MySQL数据库存储管道

对于需要持久化、查询或大规模数据的情况,存入数据库是更好的选择。

# pipelines.py (续) import pymysql class MysqlPipeline: def __init__(self, mysql_settings): # 从settings.py中传入的配置初始化 self.mysql_settings = mysql_settings self.conn = None self.cursor = None @classmethod def from_crawler(cls, crawler): # 从crawler.settings中读取MySQL配置 mysql_settings = { 'host': crawler.settings.get('MYSQL_HOST', 'localhost'), 'port': crawler.settings.get('MYSQL_PORT', 3306), 'user': crawler.settings.get('MYSQL_USER', 'root'), 'password': crawler.settings.get('MYSQL_PASSWORD', ''), 'database': crawler.settings.get('MYSQL_DATABASE', 'spider_data'), 'charset': crawler.settings.get('MYSQL_CHARSET', 'utf8mb4'), } return cls(mysql_settings) def open_spider(self, spider): """建立数据库连接,并创建表(如果不存在)。""" try: self.conn = pymysql.connect(**self.mysql_settings) self.cursor = self.conn.cursor() spider.logger.info("成功连接到MySQL数据库") # 创建表的SQL语句 create_table_sql = """ CREATE TABLE IF NOT EXISTS tencent_jobs ( id INT AUTO_INCREMENT PRIMARY KEY, job_id VARCHAR(100), job_name VARCHAR(255), job_category VARCHAR(100), job_location VARCHAR(100), job_department VARCHAR(255), job_responsibility TEXT, job_requirement TEXT, publish_time VARCHAR(50), apply_link TEXT, source_url TEXT, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_job (job_id) -- 根据job_id去重 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; """ self.cursor.execute(create_table_sql) self.conn.commit() except Exception as e: spider.logger.error(f"连接或创建表失败: {e}") raise def process_item(self, item, spider): """将Item数据插入或更新到数据库。""" adapter = ItemAdapter(item) # 构造插入或更新的SQL语句(使用ON DUPLICATE KEY UPDATE实现去重更新) sql = """ INSERT INTO tencent_jobs (job_id, job_name, job_category, job_location, job_department, job_responsibility, job_requirement, publish_time, apply_link, source_url) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE job_name=VALUES(job_name), job_category=VALUES(job_category), job_location=VALUES(job_location), job_department=VALUES(job_department), job_responsibility=VALUES(job_responsibility), job_requirement=VALUES(job_requirement), publish_time=VALUES(publish_time), apply_link=VALUES(apply_link), source_url=VALUES(source_url); """ data = ( adapter.get('job_id'), adapter.get('job_name'), adapter.get('job_category'), adapter.get('job_location'), adapter.get('job_department'), adapter.get('job_responsibility'), adapter.get('job_requirement'), adapter.get('publish_time'), adapter.get('apply_link'), adapter.get('source_url'), ) try: self.cursor.execute(sql, data) self.conn.commit() spider.logger.debug(f"成功插入/更新职位: {adapter.get('job_name')}") except Exception as e: spider.logger.error(f"插入数据失败: {e}, 数据: {data}") self.conn.rollback() return item def close_spider(self, spider): """关闭数据库连接。""" if self.cursor: self.cursor.close() if self.conn: self.conn.close() spider.logger.info("MySQL连接已关闭")

管道使用要点

  1. 去重策略:在数据库表中,我们为job_id字段设置了唯一键约束,并在插入时使用ON DUPLICATE KEY UPDATE语句。这意味着如果爬虫因中断重启后抓取到相同job_id的职位,数据库会自动更新该记录,而不是重复插入。这是一种简单有效的增量爬取去重方法。
  2. 错误处理:数据库操作必须进行异常捕获(try...except),并在出错时回滚事务(rollback),同时记录日志,避免因单条数据错误导致整个爬虫任务失败。
  3. 配置分离:数据库连接参数(主机、端口、用户名、密码等)应写在settings.py中,通过from_crawler类方法读取,而不是硬编码在管道里,提高安全性和可移植性。

需要在settings.py中添加 MySQL 配置:

# settings.py (追加) MYSQL_HOST = 'localhost' MYSQL_PORT = 3306 MYSQL_USER = 'your_username' MYSQL_PASSWORD = 'your_password' MYSQL_DATABASE = 'spider_data' MYSQL_CHARSET = 'utf8mb4'

5. 运行爬虫与实战技巧

代码写好了,配置也完成了,现在是时候让爬虫跑起来了。但直接运行可能会遇到各种问题,掌握正确的运行方法和调试技巧至关重要。

5.1 运行爬虫与数据验证

在项目根目录下,运行以下命令启动爬虫:

# 最基本的运行方式 scrapy crawl recruitment # 如果想将日志输出到文件,方便查看 scrapy crawl recruitment -s LOG_FILE=scrapy.log # 如果想限制爬取的页数进行测试(例如只爬2页) scrapy crawl recruitment -s CLOSESPIDER_PAGECOUNT=2

爬虫运行后,会在控制台看到大量的日志输出。重点关注INFODEBUG级别的日志,它们会告诉你请求发送、响应接收、Item 抓取和管道处理的情况。

数据验证

  • CSV文件:爬虫结束后,检查output/tencent_jobs.csv文件。用 Excel 或文本编辑器打开,查看数据是否完整,字段是否正确,有无乱码(使用utf-8-sig编码可避免 Excel 打开乱码)。
  • 数据库:连接到你的 MySQL 数据库,执行SELECT * FROM tencent_jobs LIMIT 10;,查看数据是否成功入库。

5.2 核心调试技巧与避坑指南

在实际操作中,你几乎一定会遇到页面结构变化、接口参数调整、IP被封等问题。下面是我总结的几个核心调试技巧和避坑点:

  1. 善用 Scrapy Shell:这是最强大的调试工具。当你的选择器写错了或者不确定接口返回什么时,不要盲目改代码。在命令行输入:

    scrapy shell 'https://careers.tencent.com/tencentcareer/api/post/Query'

    这会进入一个交互式环境,response对象就是请求的响应。你可以直接在这里测试 XPath (response.xpath(...).get()) 或 CSS 选择器 (response.css(...).get()),也可以查看response.textresponse.json(),快速验证你的解析逻辑。

  2. 处理动态参数:如果接口需要像timestamp_signature这样的动态参数,你需要分析这些参数是如何生成的。通常有两种情况:

    • 前端生成:查看网页的 JavaScript 源代码,找到生成参数的函数,尝试用 Python 模拟(可能需要用到execjs库)。这通常比较复杂。
    • 服务器返回:有时第一个请求会返回一个 token 或 key,后续请求需要带上。这时你的爬虫需要先发起一个“种子请求”来获取这个动态值。
  3. 应对反爬策略

    • 请求头:确保你的请求头(User-Agent,Referer,Accept,Accept-Language等)和浏览器一致。有些网站会校验OriginHost头。
    • Cookie 与 Session:如果网站需要登录或依赖会话,可能需要处理 Cookie。Scrapy 默认是启用 Cookie 的。对于更复杂的情况,可以使用scrapy.downloadermiddlewares.cookies.CookiesMiddleware或手动管理。
    • IP 限制:如果频繁请求导致 IP 被封,需要考虑使用代理 IP 池。可以在settings.py中配置下载中间件,或者使用scrapy-rotating-proxies这类第三方库。切记,使用代理必须合法合规。
    • 验证码:如果遇到验证码,对于简单图形验证码可以尝试 OCR 库(如ddddocrtesseract),对于复杂验证码(如滑块、点选)则可能需要人工打码平台或更复杂的识别方案,这通常会显著增加项目复杂度。
  4. 数据清洗:抓取下来的原始数据往往很“脏”,包含多余的空格、换行、不可见字符等。在 Pipeline 的process_item方法中,最好加入清洗步骤:

    def process_item(self, item, spider): for field in item.fields: value = item.get(field) if isinstance(value, str): # 去除首尾空白字符,并将多个连续空白(包括换行)替换为单个空格 item[field] = ' '.join(value.split()) return item
  5. 增量爬取与去重:我们已经在数据库层面通过唯一键做了去重。另一种更通用的 Scrapy 方式是使用scrapy.dupefilters.RFPDupeFilter并结合Requestdont_filter参数和自定义的请求指纹(fingerprint)来实现。对于需要定期全量更新的场景,可以在 Item 或数据库记录中增加update_time字段,定期清理旧数据。

6. 常见问题排查与优化建议

即使按照上述步骤操作,你可能还是会遇到一些棘手的问题。这里我整理了一份常见问题速查表,附上我的排查思路和解决方案。

问题现象可能原因排查步骤与解决方案
爬虫不发送请求或请求数极少1.start_urlsstart_requests逻辑有误。
2.ROBOTSTXT_OBEY=True且目标网站的robots.txt禁止爬取。
3. 下载中间件或扩展配置错误导致请求被过滤。
1. 检查parse或初始回调函数是否正确yield了新的RequestFormRequest
2. 暂时将ROBOTSTXT_OBEY设为False进行测试(仅用于测试,最终需尊重规则)。
3. 查看日志中是否有Filtered offsite requestFiltered duplicate request等提示。
返回 HTTP 403/404 错误1. 请求头不完整或被识别为爬虫。
2. URL 已失效或参数错误。
3. 需要登录或具有特定权限。
1. 在settings.pyRequest中完善headers,特别是User-AgentReferer
2. 在浏览器中手动访问该 URL,确认其有效性。检查请求参数(尤其是动态参数)是否正确。
3. 检查是否需要处理 Cookie 或 Session。
解析不到数据(返回空列表)1. 页面结构已更新,XPath/CSS 选择器失效。
2. 数据是通过 JavaScript 动态渲染的,初始 HTML 中没有。
3. 接口返回的数据结构发生变化。
1. 使用scrapy shell重新分析页面,更新选择器。尽量使用相对路径和容错性高的函数如contains
2. 寻找真正的数据接口(XHR/Fetch请求)。这是现代爬虫的必备技能。
3. 打印response.textresponse.json(),仔细比对与代码中解析路径的差异。
数据存入 CSV 出现乱码文件编码问题。Windows 下 Excel 直接打开 UTF-8 编码的 CSV 会乱码。open()函数中指定encoding='utf-8-sig'utf-8-sig会在文件开头添加 BOM 头,帮助 Excel 正确识别编码。
数据存入数据库失败1. 数据库连接参数错误。
2. 表结构不匹配(字段长度、类型)。
3. 网络问题或数据库权限不足。
1. 确认settings.py中的 MySQL 配置正确,数据库服务已启动。
2. 检查CREATE TABLE语句定义的字段类型和长度是否能容纳爬取的数据(特别是 TEXT 类型字段)。
3. 在 Pipeline 的open_spiderprocess_item中加强异常捕获和日志记录,明确失败原因。
爬取速度慢1.DOWNLOAD_DELAY设置过大。
2. 目标网站响应慢。
3. 管道处理(如数据库写入)耗时过长。
1. 在遵守网站规则的前提下,适当降低DOWNLOAD_DELAY,或启用AUTOTHROTTLE
2. 考虑使用异步数据库驱动(如aiomysql)或批量插入来优化管道性能。

项目优化建议

  • 分布式爬取:如果数据量巨大,可以考虑使用Scrapy-Redis组件将爬虫改造成分布式,利用多台机器同时爬取。
  • 数据监控与告警:可以编写简单的脚本,定时运行爬虫,并检查数据量是否在正常范围内。如果连续多次抓取数据量锐减或为零,可能意味着爬虫失效(如被反爬或接口变更),需要发送邮件或钉钉告警。
  • 容器化部署:使用 Docker 将整个爬虫项目容器化,可以方便地在任何支持 Docker 的服务器上部署和运行,环境一致,迁移方便。

这个项目从分析到实现,涵盖了 Scrapy 爬虫开发的大部分核心环节。最关键的收获不是代码本身,而是遇到问题时的那套分析方法:查看网络请求、分析数据来源、模拟请求、处理反爬、清洗存储。每个网站都是独特的,但解决问题的思路是相通的。在实际操作中,腾讯招聘的接口地址和参数可能会变,页面结构也可能调整,这时就需要你灵活运用上述的调试技巧去重新分析和适配。爬虫开发是一个“道高一尺,魔高一丈”的持续对抗过程,保持耐心,勤于分析,你的爬虫技能就会在解决一个又一个的实际问题中不断提升。