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 设计思路图

image-3317d1ef

文件即状态机:一个文件从 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)

这是一个代理池协作协议:用完/被封 → 通知服务端 → 服务端标记不可用 → 下次分配别的代理。

getProxy-setProxyRecord

四、入库逻辑(最核心)

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'

完整流程图:

image-ab7ec507

关键细节:

# 成功时
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)

八、整体架构图

diagram-1775039106802-7427e5db

九、设计亮点与可改进点

设计亮点

亮点 说明
文件即状态 用文件位置(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