1. 项目背景与目标每到毕业季或寒暑假大学生们最头疼的问题之一就是找实习。传统方式需要反复浏览各大招聘网站手动记录岗位信息效率低下且容易遗漏关键机会。这时候如果能用技术手段自动采集并分析实习信息就能事半功倍。我去年帮学校就业指导中心开发实习信息分析系统时就用Scrapy搭建了一套高效的采集方案3天就抓取了2万多条结构化数据。Scrapy作为Python生态中最成熟的爬虫框架其异步处理能力和模块化设计特别适合处理这类需求。不同于简单脚本它能自动处理请求队列、去重、异常重试等复杂逻辑。下面我就以国内某知名实习网站为例手把手带你实现从数据采集到分析的完整流程。最终我们将得到包含岗位名称、公司、薪资等字段的结构化数据并分析出热门岗位分布趋势。2. 环境准备与项目创建2.1 安装依赖首先确保你的Python版本在3.7以上推荐3.10。我测试时使用的是Python 3.10和Scrapy 2.6这个组合非常稳定。安装Scrapy只需一行命令pip install scrapy如果遇到权限问题可以加上--user参数。我习惯用虚拟环境管理项目依赖这样可以避免包冲突python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows2.2 创建Scrapy项目执行以下命令初始化项目结构scrapy startproject job_spider cd job_spider这会生成标准的Scrapy项目目录其中几个关键文件需要关注items.py定义要采集的数据结构pipelines.py数据清洗和存储逻辑settings.py爬虫配置spiders/存放爬虫脚本建议用PyCharm或VSCode打开项目后续编写代码会更方便。我习惯先修改settings.py中的基础配置# 设置爬虫间隔避免被封 DOWNLOAD_DELAY 1 # 关闭robots.txt限制 ROBOTSTXT_OBEY False # 设置User-Agent USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.363. 网页分析与爬虫设计3.1 目标网站结构分析我们以实习网的搜索页面为例其URL格式为https://www.shixi.com/search/index?page1通过修改page参数可以翻页。用浏览器开发者工具检查元素时发现每个岗位卡片对应一个div.job-pannel-list岗位名称在div.job-pannel-one dl dt a标签内公司信息包含在div.company-info下的多个span中关键发现是详情页URL藏在岗位名称的href属性里需要二次抓取。这种分页详情页的结构非常常见Scrapy的层级请求处理正好适用。3.2 定义数据模型在items.py中定义要采集的字段import scrapy class JobItem(scrapy.Item): title scrapy.Field() # 岗位名称 company scrapy.Field() # 公司名称 salary scrapy.Field() # 薪资 city scrapy.Field() # 工作城市 education scrapy.Field() # 学历要求 detail_url scrapy.Field() # 详情页链接 description scrapy.Field() # 岗位描述建议按需添加字段太多会增加存储负担。我最初采集了15个字段后来发现实际分析只用到了其中7个。4. 编写核心爬虫逻辑4.1 基础爬虫类在spiders/目录下新建shixi_spider.pyimport scrapy from job_spider.items import JobItem class ShixiSpider(scrapy.Spider): name shixi allowed_domains [www.shixi.com] def start_requests(self): base_url https://www.shixi.com/search/index?page{} for page in range(1, 101): # 抓取前100页 yield scrapy.Request(urlbase_url.format(page), callbackself.parse_list)这里我设置了并发请求数为10Scrapy默认值实际测试发现该网站能承受15左右的并发。可以通过CONCURRENT_REQUESTS参数调整。4.2 列表页解析添加parse_list方法处理列表页def parse_list(self, response): jobs response.css(.job-pannel-list) for job in jobs: item JobItem() item[title] job.css(div.job-pannel-one dl dt a::text).get().strip() item[company] job.css(dd:nth-child(2) div a::text).get().strip() item[city] job.css(.company-info span:nth-child(1) a::text).get() # 获取详情页链接 detail_url job.css(dt a::attr(href)).get() if detail_url: yield response.follow(detail_url, self.parse_detail, meta{item: item})使用CSS选择器时要注意::text获取元素文本::attr(href)获取属性值get()返回单个结果getall()返回列表4.3 详情页解析继续添加详情页处理方法def parse_detail(self, response): item response.meta[item] desc response.css(.job-detail::text).getall() item[description] .join(desc).strip() yield item这里用meta传递部分采集的item对象避免重复解析。我遇到过详情页加载慢的问题解决方案是在settings.py中增加超时设置DOWNLOAD_TIMEOUT 30 # 默认是180秒5. 数据存储与清洗5.1 配置数据管道Scrapy的Pipeline机制可以链式处理数据。在pipelines.py中添加import json from itemadapter import ItemAdapter class JsonWriterPipeline: def open_spider(self, spider): self.file open(jobs.json, w, encodingutf-8) self.file.write([) def close_spider(self, spider): self.file.write(]) self.file.close() def process_item(self, item, spider): line json.dumps(dict(item), ensure_asciiFalse) ,\n self.file.write(line) return item然后在settings.py中启用ITEM_PIPELINES { job_spider.pipelines.JsonWriterPipeline: 300, }5.2 数据去重实际运行中发现约5%的岗位会重复出现。添加去重管道from scrapy.exceptions import DropItem class DuplicatesPipeline: def __init__(self): self.urls_seen set() def process_item(self, item, spider): if item[detail_url] in self.urls_seen: raise DropItem(fDuplicate item found: {item}) self.urls_seen.add(item[detail_url]) return item注意管道执行顺序去重应该放在最前面ITEM_PIPELINES { job_spider.pipelines.DuplicatesPipeline: 100, job_spider.pipelines.JsonWriterPipeline: 300, }6. 数据分析实战6.1 数据加载与清洗采集完成后用Pandas进行简单分析import pandas as pd df pd.read_json(jobs.json, encodingutf-8) # 薪资处理 df[salary] df[salary].str.extract(r(\d)).astype(float) # 过滤无效数据 df df[df[salary] 0]6.2 可视化分析使用Matplotlib绘制热门岗位分布import matplotlib.pyplot as plt top_jobs df[title].value_counts().head(10) plt.figure(figsize(10,6)) top_jobs.plot(kindbarh) plt.title(Top 10 Internship Positions) plt.tight_layout() plt.savefig(jobs_dist.png)从我的采集结果看Java开发和数据分析实习生的需求最大占比超过30%。有趣的是北京和上海提供的实习薪资比其他城市平均高出25%但竞争也更激烈。7. 高级技巧与优化7.1 动态请求头为避免被封可以随机生成请求头# middlewares.py import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ] class RandomUserAgentMiddleware: def process_request(self, request, spider): request.headers[User-Agent] random.choice(USER_AGENTS)7.2 自动限速启用AutoThrottle扩展# settings.py AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 3 AUTOTHROTTLE_MAX_DELAY 60这个功能会根据服务器响应动态调整请求频率实测可以将被封概率降低80%以上。8. 常见问题解决8.1 反爬虫应对如果遇到403错误可以尝试启用cookiesCOOKIES_ENABLED True使用代理IP池添加Referer头我常用的代理中间件配置# middlewares.py class ProxyMiddleware: def process_request(self, request, spider): request.meta[proxy] http://your-proxy-ip:port8.2 数据缺失处理对于可能缺失的字段建议设置默认值item[salary] job.css(.salary::text).get(default面议).strip()在后续分析时可以用Pandas的fillna()方法补充缺失值。这个项目让我深刻体会到Scrapy的强大之处——不仅仅是采集工具更是完整的数据处理流水线。最让我惊喜的是它的扩展性当需要增加新的数据处理逻辑时只需要添加新的Pipeline组件即可。后来我们基于这套系统开发了实时预警功能当出现符合条件的新岗位时会自动推送邮件通知。