main.py

main.py — 入口与调度中心

通过 -m 参数控制运行模式,主要流程如下:

参数值 功能
p 爬取学校网站数据
ann 解析爬取结果,调用大模型处理公告
ann_wx 处理微信公众号来源的公告
ann_api 上传处理结果到云端
cp / cp_full 爬取大公司招聘页面
cjob 解析公司职位数据
wxhand 处理手工整理的微信文章
all_ann / all_job 循环自动执行完整流水线

特点: 支持断点续跑(-s 指定单文件)、代理(-p)、分页起始页(-t)、单独处理某公司(-c)。

代码

# -*- coding: utf-8 -*-
"""
学校/企业公告数据爬虫主程序
功能:使用Playwright自动化爬取学校、企业公告数据 → 本地处理 → 上传云端API
支持:定时循环、单文件处理、单企业处理、代理、重试、分页控制
"""

# 导入标准库:时间处理、系统操作、命令行参数解析
import time
import os
import argparse

# 导入Playwright自动化工具(用于模拟浏览器爬取网页)
from playwright.sync_api import sync_playwright

# 导入自定义爬虫模块
from spider_sch import SpiderSch       # 学校爬虫类
from spider_com import SpiderCom       # 企业爬虫类
from spider_data import DEFAULT_WX_SCHOOL, DEFAULT_PCOUNT  # 常量:微信特殊学校、默认处理条数
from spider_data import SpiderData     # 数据处理/上传类

# 导入工具模块:日志、浏览器工具
from utils import set_logger_debug, ner_logger
from utils_playwright import get_browser

# ====================== 命令行参数配置 ======================
# 创建命令行参数解析器,用于接收外部运行指令
parser = argparse.ArgumentParser(description='学校/企业公告数据爬取主程序')

# 运行模式(核心参数)
parser.add_argument('-m', '--method', default="p", type=str, help='启动方式:p=爬学校,cp=爬企业,ann=处理公告等')
# 配置文件编号
parser.add_argument('-f', '--file', default="99", type=str, help='使用的配置文件编号,对应不同爬取任务')
# 版本说明
parser.add_argument('-v', '--version', default="", type=str, help='版本说明')
# 云端API环境(dev/prod)
parser.add_argument('-d', '--dist', default="dev", type=str, help='云端API接口环境:dev=测试,prod=正式')
# 指定只处理单个文件(用于断点续爬/修复)
parser.add_argument('-s', '--processfile', default="", type=str, help='指定需要处理的单个文件ID')
# 指定只处理单个公司
parser.add_argument('-c', '--company', default="", type=str, help='指定单独处理的公司名称/标识')
# 代理设置
parser.add_argument('-p', '--proxy', default="", type=str, help='浏览器代理地址,为空则不使用')
# 重试开关
parser.add_argument('-r', '--retry', default="1", type=str, help='是否允许重试:1=允许,0=不允许')
# 翻页起始页码
parser.add_argument('-t', '--pagestart', default="2", type=str, help='翻页开始页数,默认从第2页开始')

# 解析命令行传入的所有参数
args = parser.parse_args()

# ====================== 核心爬取函数 ======================
def clawler_main(s, _stat={}):
    """
    爬虫主入口:启动浏览器 → 遍历学校/企业节点 → 执行爬取
    :param s: 爬虫实例(SpiderSch / SpiderCom)
    :param _stat: 运行状态字典(记录配置、进度、过滤条件)
    """
    # 从爬虫实例中获取浏览器可执行文件路径
    executable_path = s.get_browser_path()

    # 启动Playwright上下文(管理浏览器生命周期)
    with sync_playwright() as p:
        # 创建浏览器实例(自动处理代理、无头模式、防检测)
        browser = get_browser(p, executable_path, args.proxy)
        # 将浏览器对象绑定到爬虫实例,供内部方法使用
        s.browser = browser

        # 新建浏览器标签页
        page = browser.new_page()

        # ===================== 爬取核心逻辑 =====================
        # 获取所有需要爬取的学校/企业节点(配置文件中定义)
        nodes = s.get_nodes()
        # 获取已爬取进度(用于断点续爬)
        process = s.get_progress()

        # 遍历所有节点(key=学校/企业ID,node=详情配置)
        for _key, _node in nodes.items():
            # 过滤:跳过微信专用特殊学校(单独处理)
            if _key == DEFAULT_WX_SCHOOL:
                continue

            # 过滤:如果指定了单独处理公司,则只爬该公司
            if 'company' in _stat and _stat['company'] != _key:
                continue

            # 遍历该节点下的所有学校/企业信息
            for _sch_info in _node:
                # 如果该节点在进度记录中 → 开始执行爬取
                if _key in process:
                    print(f"开始爬取 {_key}")
                    # 调用实例内部的run方法执行具体爬取逻辑
                    s.run(page, _key, _sch_info, _stat)

        # 爬取完成,等待10秒后关闭浏览器
        time.sleep(10)
        browser.close()

# ====================== 数据处理函数 ======================
def process_main_wx(s, d, _stat={}):
    """
    处理微信渠道数据(单独处理)
    :param s: 学校爬虫实例
    :param d: 数据处理实例
    :param _stat: 状态参数
    """
    nodes = s.get_nodes()
    for _key, _node in nodes.items():
        # 跳过微信特殊学校
        if _key == DEFAULT_WX_SCHOOL:
            continue
        # 遍历并处理公告数据(渠道标记为wx)
        for _sch_info in _node:
            d.process_announcement_data(_key, _sch_info, _stat, "wx")

def process_main_announcement(s, d, _stat={}, proc_type="ann", _uapi="up_api"):
    """
    处理爬取到的公告数据(清洗、解析、入库)
    :param proc_type: 处理类型 ann=普通公告 cjob=企业职位
    :param _uapi: 上传API类型
    """
    nodes = s.get_nodes()
    for _key, _node in nodes.items():
        if _key == DEFAULT_WX_SCHOOL:
            continue

        for _sch_info in _node:
            # 执行数据解析处理
            d.process_announcement_data(_key, _sch_info, _stat, proc_type)

            # 达到默认批量处理条数 → 自动触发上传
            if 'total' in _stat and _stat['total'] >= DEFAULT_PCOUNT:
                ner_logger.info(f"{_stat['total']}条数据已处理完成")
                # 调用上传接口
                d.process_announcement_data(_key, _sch_info, _stat, _uapi)
                ner_logger.info(f"{_stat['total']}条数据已上传完成")
                # 重置计数器
                _stat['total'] = 0

def process_main_wxhand(s, d, _stat={}):
    """
    处理【微信手工录入】的文章数据(专用渠道)
    """
    # 预先初始化微信文章数据
    d.pre_wx_article()
    nodes = s.get_nodes()

    for _key, _node in nodes.items():
        # 只处理微信特殊学校
        if _key != DEFAULT_WX_SCHOOL:
            continue

        ner_logger.info(f"开始处理特殊学校数据:{_key}")
        for _sch_info in _node:
            d.process_announcement_data(_key, _sch_info, _stat, "wx")

def process_main_upapi(s, d, _stat={}, _uapi="up_api"):
    """
    统一上传数据到云端API
    """
    nodes = s.get_nodes()
    for _key, _node in nodes.items():
        if _key == DEFAULT_WX_SCHOOL:
            continue

        for _sch_info in _node:
            d.process_announcement_data(_key, _sch_info, _stat, _uapi)

def process_main_wxhand_api(s, d, _stat={}):
    """
    上传微信手工数据到云端API
    """
    nodes = s.get_nodes()
    for _key, _node in nodes.items():
        # 只处理微信特殊学校
        if _key != DEFAULT_WX_SCHOOL:
            continue

        ner_logger.info(f"开始处理特殊学校数据上传:{_key} {DEFAULT_WX_SCHOOL}")
        for _sch_info in _node:
            d.process_announcement_data(_key, _sch_info, _stat, "up_api")

# ====================== 主调度函数(根据-m参数执行对应流程) ======================
def run_periodically(s, cs, d):
    """
    周期运行调度器:根据命令行参数 -m 执行对应任务
    :param s: 学校爬虫实例
    :param cs: 企业爬虫实例
    :param d: 数据处理实例
    """
    # 初始化状态字典:存储运行配置、过滤条件、计数器
    _stat = {}

    # ===================== 读取命令行参数到状态字典 =====================
    # 单文件处理
    if args.processfile != "":
        _stat['pfile'] = args.processfile
    # 单企业处理
    if args.company != "":
        _stat['company'] = args.company
    # 起始页码
    if args.pagestart != "":
        _stat['page_start'] = int(args.pagestart)
    else:
        _stat['page_start'] = 2
    # 云端环境
    _stat['dist'] = args.dist
    # 是否重试
    _stat['retry'] = args.retry
    # 已处理公告计数
    _stat['p_count'] = 0
    # 已处理列表(去重)
    _stat['all_proc_list'] = []

    # 打印模式:仅输出所有学校列表,不爬取
    if args.method in ["o"]:
        s.print_all_sch()

    # ===================== 企业爬虫流程 =====================
    # 1. 爬取企业原始数据
    if args.method in ["cp", 'cp_full']:
        _stat['method'] = args.method
        clawler_main(cs, _stat)
        time.sleep(1 * 6)
        ner_logger.info("第一步:爬取企业数据完成,休息后继续处理")

    # 2. 处理企业职位数据
    if args.method in ["cjob"]:
        process_main_announcement(cs, d, _stat, "cjob", "up_api_cjob")
        time.sleep(1 * 6)
        ner_logger.info("第二步:处理职位数据完成")

    # 3. 上传企业数据到云
    if args.method in ["cjob_api"]:
        process_main_upapi(cs, d, _stat, "up_api_cjob")
        time.sleep(1 * 6)
        ner_logger.info("第四步:企业数据上传完成")

    # 循环执行企业爬取+处理+上传
    if args.method in ["all_job"]:
        ner_logger.info("启动企业公告循环处理任务")
        _loop_times = 0
        while True:
            _all_total = 0
            _stat['total'] = 0

            # 处理数据
            process_main_announcement(cs, d, _stat, "cjob", "up_api_cjob")
            _all_total += _stat['total']
            time.sleep(6)

            # 上传数据
            _stat['total'] = 0
            process_main_upapi(cs, d, _stat, "up_api_cjob")
            _all_total += _stat['total']
            time.sleep(6)

            # 长期无数据则退出循环
            if _all_total == 0 and _loop_times > 200:
                ner_logger.info("无新数据,结束循环")
                break
            _loop_times += 1

    # ===================== 学校爬虫流程 =====================
    # 1. 爬取学校数据
    if args.method in ["p"]:
        clawler_main(s, _stat)
        time.sleep(6)
        ner_logger.info("第一步:学校数据爬取完成")

    # 循环爬取学校(每8小时一次)
    if args.method in ["all_p"]:
        ner_logger.info("启动学校循环爬取任务(8小时/次)")
        clawler_main(s, _stat)
        time.sleep(60 * 60 * 8)
        ner_logger.info("循环爬取完成,休息8小时")

    # ===================== 学校数据处理流程 =====================
    # 2. 处理学校公告
    if args.method in ["ann"]:
        process_main_announcement(s, d, _stat)
        time.sleep(6)
        ner_logger.info("第二步:公告处理完成")

    # 3. 处理学校微信数据
    if args.method in ["ann_wx"]:
        process_main_wx(s, d, _stat)
        time.sleep(6)
        ner_logger.info("第三步:微信数据处理完成")

    # 4. 上传学校数据到云
    if args.method in ["ann_api"]:
        process_main_upapi(s, d, _stat)
        time.sleep(6)
        ner_logger.info("第四步:数据上传完成")

    # 循环处理学校公告+微信+上传
    if args.method in ["all_ann"]:
        ner_logger.info("启动学校公告循环处理任务")
        _loop_times = 0
        while True:
            _loop_times += 1
            _all_total = 0
            _stat['total'] = 0

            # 上传
            process_main_upapi(s, d, _stat)
            _all_total += _stat['total']
            time.sleep(6)

            # 处理公告
            _stat['total'] = 0
            process_main_announcement(s, d, _stat)
            _all_total += _stat['total']
            time.sleep(6)

            # 处理微信
            _stat['total'] = 0
            process_main_wx(s, d, _stat)
            _all_total += _stat['total']
            time.sleep(6)

            # 无数据则退出
            if _all_total == 0 and _loop_times > 20:
                ner_logger.info("无新数据,结束循环")
                break

    # ===================== 微信手工数据流程 =====================
    # 处理微信手工数据
    if args.method in ["wxhand"]:
        process_main_wxhand(s, d, _stat)
        ner_logger.info("第一步:微信手工数据处理完成")

    # 上传微信手工数据
    if args.method in ["wxhand_api"]:
        process_main_wxhand_api(s, d, _stat)
        ner_logger.info("第二步:微信手工数据上传完成")

    # 循环处理微信手工数据
    if args.method in ["all_wxhand"]:
        ner_logger.info("启动微信循环处理任务")
        while True:
            _stat['total'] = 0
            process_main_wxhand_api(s, d, _stat)
            time.sleep(6)

            _stat['total'] = 0
            process_main_wxhand(s, d, _stat)
            time.sleep(6)

            _stat['total'] = 0
            process_main_wxhand_api(s, d, _stat)
            time.sleep(6)

# ====================== 程序入口 ======================
if __name__ == '__main__':
    # 初始化日志系统
    set_logger_debug(args.file)

    # 初始化三大核心实例
    s = SpiderSch(args.file)       # 学校爬虫
    cs = SpiderCom(args.file)      # 企业爬虫
    d = SpiderData(s)              # 数据处理/上传

    # 如果输入版本参数 → 打印帮助说明
    if args.version != '':
        print("===== 使用说明 =====")
        print("-m / --method : 运行模式")
        print("  p      = 爬取学校数据")
        print("  cp     = 爬取企业数据")
        print("  ann    = 处理学校公告")
        print("  cjob   = 处理企业职位")
        print("  ann_api= 上传学校数据")
        print("  wxhand = 处理微信手工数据")
        print("-f / --file   : 配置文件编号")
        exit()

    # 启动调度器,开始执行任务
    run_periodically(s, cs, d)

项目分区导航全链路流程梳理 ⬅️ | 01-main.py | ➡️ spider_com.py