isoftstone_data_proc_api.py

isoftstone_data_proc_api.py — 软通动力直连

两套 API 响应格式自动适配(社招 code/data/list / 校招 results/count);详情页是动态渲染 SPA,用 Playwright 渲染 + ThreadPoolExecutor 避开事件循环冲突。

代码

import time
import hashlib
import os
import requests
from urllib.parse import urlencode
import sys

sys.path.append('../')
import json
from utils import ner_logger
import re
import asyncio
from playwright.sync_api import sync_playwright
import threading
from concurrent.futures import ThreadPoolExecutor

# 请求头配置,模拟浏览器访问软通动力招聘网站
headers = {
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/json;charset=UTF-8",
    "Host": "career.isoftstone.com",
    "Origin": "https://career.isoftstone.com",
    "Pragma": "no-cache",
    "Referer": "https://career.isoftstone.com/talent/htmls/shehuizhaopin/index.html",
    "Sec-Ch-Ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
    "X-Requested-With": "XMLHttpRequest"
}

# 登录Cookie,保持会话状态
cookie_str = ('Hm_lvt_e5e1889ee1cef86df8447e0c983cb5b5=1760151941; '
              'Hm_lvt_c31aaec3450321c4e3d4fd4f7509f181=1760151941; '
              'dreamer-cms-s=c872e558-050d-45d8-bd3d-c97534fc3757')

headers["Cookie"] = cookie_str


# 请求软通动力招聘接口,获取职位JSON数据
def get_isoftstone_job_json(url, recruitType, curPage):
    # 计算分页偏移量
    skip_count = (curPage - 1) * 50
    # 社招/校招使用不同的请求参数
    if recruitType == "1":
        payload = {
            "workCity": "",
            "jobTypeId": "0",
            "keyWord": "",
            "maxcount": 100,
            "page": curPage,
            "recruitType": 1
        }
    elif recruitType == "2":
        payload = {
            "workCity": "",
            "jobTypeId": "0",
            "keyWord": "",
            "skipCount": skip_count,
            "pageCount": 100,
            "recruitType": 2
        }

    # 发送POST请求
    with requests.Session() as s:
        resp = s.post(url, json=payload, headers=headers, timeout=15)
        print("Status:", resp.status_code)
        # 解析返回的JSON数据
        try:
            if resp.status_code == 200:
                json_data = resp.json()
                # 适配 /job/all 接口格式
                if 'code' in json_data:
                    if json_data['code'] == 0:
                        data = json_data['data']['list']
                        total = int(json_data['data']['count'])
                        return True, data, total
                # 适配 /campus/all 接口格式
                elif 'results' in json_data:
                    data = json_data['results']
                    total = int(json_data['count'])
                    return True, data, total
                else:
                    ner_logger.info("Unknown JSON format: %s", json_data)
                    return False, [], 0
            else:
                ner_logger.info("Request failed with status code: %s, response: %s", resp.status_code, resp.text)
                return False, [], 0
        except Exception as e:
            ner_logger.info("JSON decode error: %s, response: %s", str(e), resp.text)
            return False, [], 0


# 使用Playwright获取动态渲染的职位详情页HTML
def get_isoftstone_job_html(url, tmp_file):
    try:
        # 定义浏览器抓取逻辑
        def fetch_page_content():
            with sync_playwright() as p:
                # 无头模式启动浏览器
                browser = p.chromium.launch(headless=True)
                page = browser.new_page()
                
                # 设置请求头
                page.set_extra_http_headers({
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
                    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                    "Referer": "https://career.isoftstone.com/talent/htmls/shehuizhaopin/index.html"
                })
                
                # 访问页面并等待网络空闲
                page.goto(url)
                page.wait_for_load_state("networkidle")
                page.wait_for_timeout(5000)
                
                # 获取完整页面内容
                full_text = page.content()
                browser.close()
                return full_text
        
        # 在线程池中运行,避免事件循环冲突
        with ThreadPoolExecutor() as executor:
            future = executor.submit(fetch_page_content)
            full_text = future.result()
        
        # 清除JS脚本内容
        full_text = re.sub(r'<script[^>]*?>.*?', '', full_text, flags=re.DOTALL)
        
        # 写入HTML文件
        with open(tmp_file, "w", encoding="utf-8") as f:
            f.write(full_text)
            
    except Exception as e:
        print(f"请求失败:{e}")
        return None


# 将软通动力原始数据转换为统一标准JSON
def transform_job_json(item, recruitType, job_type, channel, target_url, tmp_file, json_file):
    # 社招字段映射
    field_mapping = {
        "announcement_name": "job_name",
        "publish_time": "public_time",
        "hd_dept": "",
        "hd_loc": "work_city",
        "hd_job_num": "count",
        "hd_job_category": ""
    }
    
    # 校招字段映射
    if "name" in item and "address_detail" in item:
        field_mapping = {
            "announcement_name": "name",
            "publish_time": "publish_date",
            "hd_dept": "",
            "hd_loc": "address_detail",
            "hd_job_num": "",
            "hd_job_category": ""
        }
    
    # 固定公共字段
    fixed_fields = {
        "link": target_url,
        "full_url": target_url,
        "last_url": target_url,
        "file_path": tmp_file,
        "parent_url": "https://career.isoftstone.com/talent/htmls/shehuizhaopin/index.html",
        "channel": channel,
        "job_type": job_type
    }
    
    target_json = {}
    # 字段映射赋值
    for target_field, source_field in field_mapping.items():
        if source_field:
            value = item.get(source_field, "")
            if target_field == "hd_job_num" and isinstance(value, int):
                value = str(value)
            target_json[target_field] = value
        else:
            target_json[target_field] = ""
    
    # 写入固定字段
    target_json.update(fixed_fields)
    
    # 保存标准JSON文件
    with open(json_file, 'w', encoding='utf-8') as f:
        json.dump(target_json, f, ensure_ascii=False, indent=4)
        time.sleep(1)


# 软通动力招聘主爬取逻辑
def api_proc_isoftstone(spider_com, _key, com_info, k, url, _stat):
    # 未传入URL则使用默认接口地址
    if not url or url == "":
        url = "https://career.isoftstone.com/job/all"

    # 根据任务类型判断社招/校招
    recruitType = "2"
    job_type = "shezhao"
    
    if k.startswith("shezhao"):
        recruitType = "2"
        job_type = "shezhao"
    elif k.startswith("xiaozhao"):
        recruitType = "1"
        job_type = "xiaozhao"
    
    ner_logger.info("开始处理isoftstone数据, k: %s, url: %s, job_type: %s", k, url, job_type)
    # 获取临时文件目录
    key_tmp_dir = spider_com.get_key_dir(_key)
    ner_logger.info("临时目录: %s", key_tmp_dir)
    
    total_page = 0
    
    # 循环翻页爬取
    for curPage in range(1, 100):
        flag, json_data, totalcount = get_isoftstone_job_json(url, recruitType, curPage)
        if flag:
            # 计算总页数
            if total_page == 0:
                total_page = int(totalcount / 100) + 1
            ner_logger.info("总页数: %s", total_page)
                
            # 终止条件:已到最后一页
            if curPage >= total_page:
                ner_logger.info("已达到总页数,结束分页爬取")
                break
            # 非全量模式只爬5页
            if curPage > 5 and _stat.get('method', '') != "cp_full":
                ner_logger.info("已爬取5页且不是完整模式,结束分页爬取")
                break
                
            # 遍历职位数据
            for i, item in enumerate(json_data):
                job_id = item.get("id")
                # 拼接详情页URL
                if recruitType == "2":
                    _fullurl = f"https://career.isoftstone.com/talent/htmls/shezhaozhiweixiangqing/index.html?id={job_id}&recruitType={recruitType}"
                else:
                    _fullurl = f"https://career.isoftstone.com/talent/htmls/xiaozhaozhiweixiangqing/index.html?id={job_id}&recruitType={recruitType}"
                
                # 生成文件路径
                _hash = hashlib.md5(_fullurl.encode("utf-8")).hexdigest()
                tmp_file = os.path.join(key_tmp_dir, f"detail_{job_id}_{_hash}.html")
                tmp_json_file = os.path.join(key_tmp_dir, f"detail_{job_id}_{_hash}.json")
                
                ner_logger.info("正在处理第 %s 页第 %s 个职位", curPage, i+1)
                
                # 文件已存在则跳过,仅更新时间
                if os.path.exists(tmp_file) and os.path.exists(tmp_json_file):
                    try:
                        current_time = time.time()
                        os.utime(tmp_file, (current_time, current_time))
                        os.utime(tmp_json_file, (current_time, current_time))
                    except Exception as e:
                        ner_logger.error(f"更新文件时间出错:{str(e)}")
                    continue
                    
                # 转换数据并下载详情页
                transform_job_json(item, recruitType, job_type, _key, _fullurl, tmp_file, tmp_json_file)
                time.sleep(1)
                get_isoftstone_job_html(_fullurl, tmp_file)
                time.sleep(1)
        else:
            ner_logger.info("Failed to fetch data for page %s", curPage)
            if curPage == 1:
                ner_logger.error("第一页数据获取失败,终止处理")
                return False
        time.sleep(1)
        
    ner_logger.info("isoftstone数据处理完成")
    return True

项目分区导航baidu_data_proc_api ⬅️ | 02-isoftstone_data_proc_api | ➡️ jd_data_proc_api