Spider.py 基类深度解析
一、整体定位
这是一个爬虫基础设施基类,所有具体爬虫(如猎聘、BOSS直聘等)都继承它。它不负责具体抓取逻辑,而是提供:
Spider (基类)
├── 配置管理(读 ini 文件)
├── 文件/文件夹管理(本地持久化)
├── 代理管理(获取/标记代理)
├── 数据入库(调远程 API)
├── 去重判断(公司/职位是否已存在)
├── 日志记录
└── 工具方法(sleep、日期解析等)
二、初始化 __init__ —— 配置 + 文件夹体系
2.1 读取配置
config = configparser.ConfigParser()
config.read("spider.ini", encoding="utf-8")
self.chromePath = config.get("Common", "chromePath") # 浏览器路径
self.proxies = config.get("Common", 'proxies').split(',') # 代理池
self.savePath = config.get('Common', 'savePath') # 根存储路径
self.timeInterval = int(config.get("Common", 'timeInterval')) # 去重时间窗口
self.maxTryTime = int(config.get("Common", 'maxTryTime')) # 最大重试次数
# ... 各种 API 地址
spider.ini 是全局配置中心,所有爬虫共用。
2.2 文件夹结构(核心!)
# 构造函数中层层创建的文件夹:
self.savePath = "保存路径/liepin" # 以渠道命名
最终生成的目录树:
savePath/
└── liepin/ # self.savePath(按渠道隔离)
├── com/ # self.comPath —— 暂存公司 JSON
├── job/ # self.jobPath —— 暂存职位 JSON
├── success/ # self.successPath —— 入库成功
│ └── 20240703/
│ ├── com/ # 成功的公司文件 + comsuccess.txt 日志
│ └── job/ # 成功的职位文件 + jobsuccess.txt 日志
├── failed/ # self.failedPath —— 入库失败
│ └── 20240703/
│ ├── com/ # 失败的公司文件 + comerror.txt 日志
│ └── job/ # 失败的职位文件 + joberror.txt 日志
├── progress/ # self.progressPath —— 进度记录
├── company/ # self.companyPath —— 按公司组织的职位数据
│ └── {comFileId}/
│ ├── job/ # 待处理的职位
│ ├── success/ # 成功的职位
│ └── failed/ # 失败的职位
└── clean/ # self.cleanPath —— 清理/过期记录
├── last_{suffix}.txt # 翻页进度
└── clean_{suffix}.txt # 已清理的职位记录
2.3 设计思路图
文件即状态机:一个文件从 job/ → success/ 或 failed/,文件的位置就代表了它的处理状态。
三、代理管理
3.1 获取代理
def getProxy(self, tryTimes=0):
'''获取代理'''
if tryTimes >= 3: # 最多重试3次
return ''
params = {"channel": self.channel, "env": 1}
ret = requests.post(self.getProxyUrl, data=params)
# 解析返回的代理地址
proxy = rescontent.get('data').get('proxy')
# 同时获取:
self.limitSpiderCount = ... # 该代理还能抓多少次
self.realProxy = ... # 真实外网IP
流程:
Spider → POST /getProxy → 代理管理服务
│
├── 返回可用代理 IP
├── 返回剩余可用次数 (limitCount)
└── 返回真实外网 IP (realProxy)
3.2 标记代理状态
def setProxyRecord(self, proxy, limitedType, spiderCount=0, tryTimes=0):
'''代理用完/被封后,通知代理管理服务'''
params = {
"channel": self.channel,
"proxy": proxy,
"limitedType": limitedType, # 受限类型(被封、用完等)
"spiderCount": spiderCount # 已抓取数量
}
requests.post(self.setProxyRecordUrl, params=params)
这是一个代理池协作协议:用完/被封 → 通知服务端 → 服务端标记不可用 → 下次分配别的代理。
四、入库逻辑(最核心)
4.1 insert() —— 调远程 API 入库
def insert(self, content, fileName, isCom):
'''
content: JSON 数据(公司或职位信息)
fileName: 文件标识
isCom: 1=公司, 0=职位
'''
data = {
"comFrom": self.comFrom, # 来源渠道编号(如 40001=猎聘)
"fileName": fileName,
"content": json.dumps(content)
}
url = self.comUrl if isCom else self.jobUrl # 公司/职位用不同接口
res = requests.post(url, data=data, timeout=5)
重试策略:
状态码200 + code 200 → 成功,返回 (1, response)
状态码200 + code 501 → 服务端临时错误,重试(最多 maxTryTime 次)
状态码200 + 其他code → 业务错误,直接失败 (0, response)
状态码非200 → 网络错误,重试
异常 → 直接失败,重置计数器
4.2 enterDatabase() —— 入库 + 文件归档(一条龙)
这是最关键的方法,串联了入库和文件管理:
def enterDatabase(self, jsonData, fileId, filePath, type):
# type = 'com' 或 'job'
完整流程图:
关键细节:
# 成功时
successFolder = os.path.join(self.successPath, date.today().strftime("%Y%m%d"), type)
shutil.move(filePath, successFolder + '/' + fileId) # 文件移走
self.writeLog(successFolder, type+'success.txt', fileId, rs) # 记日志
# 如果职位关联了公司,同步更新公司文件夹下的状态
if type == 'job' and jsonData.get('comFileId'):
comFolder = os.path.join(self.companyPath, jsonData['comFileId'])
shutil.move(comFolder+'/job/'+fileId, comFolder+'/success/'+fileId)
**双重归档:**职位数据同时在「全局维度」和「公司维度」两个地方做状态流转。
五、去重机制
5.1 远程去重(数据库级别)
def comExist(self, comname):
'''公司是否已在数据库中'''
# 返回: 1=存在, -1=黑名单, 0=不存在
def jobExist(self, comName, jobTitle, city):
'''职位是否已在数据库中'''
# 返回: 1=存在, 0=不存在
5.2 本地去重(文件级别)
def isSpiderToday(self, folder, fileId, type):
'''递归检查 success 文件夹下是否已有该文件'''
# 遍历文件夹 → 找到 type 子目录 → 检查 fileId 是否存在
def isSpiderCompanyJob(self, comFileId, jobFileId):
'''检查公司目录下 success/ 中是否已有该职位'''
jobFilePath = os.path.join(self.companyPath, comFileId, 'success', jobFileId)
return os.path.exists(jobFilePath)
5.3 时间窗口去重
def traverseFolder(self, curPath, compareFileName):
'''遍历历史文件夹,只检查 timeInterval 内的'''
# 文件夹名是日期(如 20240703)
# 转时间戳,与当前时间比较
if int(time.time()) - timestamp < self.timeInterval:
# 在时间窗口内 → 继续检查
else:
# 超出时间窗口 → 跳过(允许重新抓取)
三级去重:数据库查 → 本地今日文件查 → 历史时间窗口查。
六、职位清理(过期处理)
6.1 获取待检查的职位链接
def jobLinks(self, suffix, tryTimes=0):
'''分页获取需要检查是否过期的职位链接'''
# 用 lastId 实现分页游标
# lastId 持久化到 clean/last_{suffix}.txt
# hasNext 控制是否继续翻页
翻页进度持久化:
clean/
├── last_beijing.txt # 存储上次翻到的 lastId
└── clean_beijing.txt # 已处理的过期职位记录
6.2 标记职位过期
def jobToExpired(self, jobId, url, suffix, tryTimes=0):
'''调 API 将职位标记为过期'''
requests.post(self.jobToExpiredApi, params={"jobid": jobId})
self.writeCleanId(...) # 记录到 clean 日志
七、工具方法
7.1 日期解析(非常灵活)
def getEndDate(self, string):
'''支持多种格式的截止时间解析'''
输入 '2024-07-03 19:31' → 精确时间戳
输入 '2024-07-03' → 当天零点时间戳
输入 '3' → 3天前零点的时间戳
输入 异常值 → 今天零点的时间戳(兜底)
7.2 获取抓取条件/链接
def getContions(self, type):
'''从服务端获取抓取条件(关键词、城市等)'''
def getSpiderUrl(self, index):
'''从服务端获取要抓取的具体链接'''
7.3 随机休眠
def randomSleep(self, min=1, max=3, msg=''):
sleepTime = random.randint(min, max)
time.sleep(sleepTime)
八、整体架构图
九、设计亮点与可改进点
设计亮点
| 亮点 | 说明 |
|---|---|
| 文件即状态 | 用文件位置(com→success/failed)表示处理状态,简单可靠,宕机可恢复 |
| 双维度归档 | 全局维度 + 公司维度,方便按公司追踪所有职位状态 |
| 三级去重 | 数据库 + 本地今日 + 历史时间窗口,避免重复抓取 |
| 代理池协作 | 获取-使用-反馈 闭环,自动淘汰坏代理 |
| 游标分页 | 用 lastId 持久化到文件,中断后可续跑 |
| 灵活的日期解析 | 支持多种格式,容错性好 |
可改进点
| 问题 | 建议 |
|---|---|
裸 except: 太多 |
应至少 except Exception as e:,避免吞掉 KeyboardInterrupt |
| 文件操作非原子性 | shutil.move 在跨磁盘时可能出问题,可考虑先 copy 再 delete |
| 无并发控制 | 多进程同时操作同一文件夹可能冲突,可加文件锁 |
getSpiderUrl 中有 bug |
失败时调的是 self.getContions(type) 而不是 self.getSpiderUrl(index) |
| 硬编码路径拼接 | comFolder+'/job/'+fileId 建议统一用 os.path.join() |
| 缺少类型注解 | 加上 type hints 会大幅提升可读性 |
项目分区导航:学习顺序 ⬅️ | 01-Spider.py基类深度解析 | ➡️ getProxy-setProxyRecord
💬 评论