--- title: "01-Spider.py基类深度解析" created: 2026-04-01 tags: - 项目 aliases: - Spider.py基类深度解析 --- # Spider.py 基类深度解析 ## **一、整体定位** 这是一个**爬虫基础设施基类**,所有具体爬虫(如猎聘、BOSS直聘等)都继承它。它**不负责具体抓取逻辑**,而是提供: ```text Spider (基类) ├── 配置管理(读 ini 文件) ├── 文件/文件夹管理(本地持久化) ├── 代理管理(获取/标记代理) ├── 数据入库(调远程 API) ├── 去重判断(公司/职位是否已存在) ├── 日志记录 └── 工具方法(sleep、日期解析等) ``` ## **二、初始化** `__init__` **—— 配置 + 文件夹体系** ### **2.1 读取配置** ```python 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 文件夹结构(核心!)** ```text # 构造函数中层层创建的文件夹: self.savePath = "保存路径/liepin" # 以渠道命名 ``` 最终生成的目录树: ```text 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.png]] > 文件即状态机:一个文件从 job/ → success/ 或 failed/,文件的位置就代表了它的处理状态。 ## **三、代理管理** ### **3.1 获取代理** ```python 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 ``` **流程:** ```text Spider → POST /getProxy → 代理管理服务 │ ├── 返回可用代理 IP ├── 返回剩余可用次数 (limitCount) └── 返回真实外网 IP (realProxy) ``` ### **3.2 标记代理状态** ```python 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) ``` > 这是一个代理池协作协议:用完/被封 → 通知服务端 → 服务端标记不可用 → 下次分配别的代理。 [[02-getProxy-setProxyRecord|getProxy-setProxyRecord]] ## **四、入库逻辑(最核心)** ### **4.1** `insert()` **—— 调远程 API 入库** ```python 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) ``` **重试策略:** ```text 状态码200 + code 200 → 成功,返回 (1, response) 状态码200 + code 501 → 服务端临时错误,重试(最多 maxTryTime 次) 状态码200 + 其他code → 业务错误,直接失败 (0, response) 状态码非200 → 网络错误,重试 异常 → 直接失败,重置计数器 ``` ### **4.2** `enterDatabase()` **—— 入库 + 文件归档(一条龙)** 这是**最关键的方法**,串联了入库和文件管理: ```python def enterDatabase(self, jsonData, fileId, filePath, type): # type = 'com' 或 'job' ``` **完整流程图:** ![[image-ab7ec507.png]] **关键细节:** ```python # 成功时 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 远程去重(数据库级别)** ```python def comExist(self, comname): '''公司是否已在数据库中''' # 返回: 1=存在, -1=黑名单, 0=不存在 def jobExist(self, comName, jobTitle, city): '''职位是否已在数据库中''' # 返回: 1=存在, 0=不存在 ``` ### **5.2 本地去重(文件级别)** ```python 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 时间窗口去重** ```python def traverseFolder(self, curPath, compareFileName): '''遍历历史文件夹,只检查 timeInterval 内的''' # 文件夹名是日期(如 20240703) # 转时间戳,与当前时间比较 if int(time.time()) - timestamp < self.timeInterval: # 在时间窗口内 → 继续检查 else: # 超出时间窗口 → 跳过(允许重新抓取) ``` > ***三级去重***:数据库查 → 本地今日文件查 → 历史时间窗口查。 ## **六、职位清理(过期处理)** ### **6.1 获取待检查的职位链接** ```python def jobLinks(self, suffix, tryTimes=0): '''分页获取需要检查是否过期的职位链接''' # 用 lastId 实现分页游标 # lastId 持久化到 clean/last_{suffix}.txt # hasNext 控制是否继续翻页 ``` **翻页进度持久化:** ```text clean/ ├── last_beijing.txt # 存储上次翻到的 lastId └── clean_beijing.txt # 已处理的过期职位记录 ``` ### **6.2 标记职位过期** ```python def jobToExpired(self, jobId, url, suffix, tryTimes=0): '''调 API 将职位标记为过期''' requests.post(self.jobToExpiredApi, params={"jobid": jobId}) self.writeCleanId(...) # 记录到 clean 日志 ``` ## **七、工具方法** ### **7.1 日期解析(非常灵活)** ```python def getEndDate(self, string): '''支持多种格式的截止时间解析''' ``` ```text 输入 '2024-07-03 19:31' → 精确时间戳 输入 '2024-07-03' → 当天零点时间戳 输入 '3' → 3天前零点的时间戳 输入 异常值 → 今天零点的时间戳(兜底) ``` ### **7.2 获取抓取条件/链接** ```python def getContions(self, type): '''从服务端获取抓取条件(关键词、城市等)''' def getSpiderUrl(self, index): '''从服务端获取要抓取的具体链接''' ``` ### **7.3 随机休眠** ```python def randomSleep(self, min=1, max=3, msg=''): sleepTime = random.randint(min, max) time.sleep(sleepTime) ``` ## **八、整体架构图** ![[diagram-1775039106802-7427e5db.png]] ## **九、设计亮点与可改进点** ### **设计亮点** | **亮点** | **说明** | | --- | --- | | **文件即状态** | 用文件位置(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 会大幅提升可读性 | --- **项目分区导航**:[[00-学习顺序|学习顺序]] ⬅️ | 01-Spider.py基类深度解析 | ➡️ [[02-getProxy-setProxyRecord|getProxy-setProxyRecord]]