大规模数据清洗 Pipeline 技术介绍

基于 1600 万行岗位数据清洗项目的实战经验总结


一、项目概述

1.1 项目背景

在 AI 人才分析场景中,需要从各大招聘平台抓取海量岗位数据,但原始数据存在大量噪声:岗位名称不规范、职责描述包含广告、重复数据泛滥、非 AI 岗位混入等问题。本项目设计了一套 6 阶段流水线,将 1600 万行原始数据清洗为高质量的 AI 岗位池。

1.2 核心挑战

挑战具体表现
数据量大1600 万行,单机处理需优化内存和速度
噪声多包含福利、联系方式、公司介绍等无关文本
分类模糊"运营"、"测试"等岗位需结合上下文判断
重复泛滥同一公司同一岗位多次发布,内容高度相似
语义鸿沟传统关键词匹配无法理解岗位语义

1.3 整体架构

原始数据 (1600w)
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│  Stage 1: 文本清洗 (first_cleaned.py)                        │
│  - 正则表达式清洗噪声文本                                      │
│  - 提取技能/领域/动作/场景/角色标签                              │
│  - 初步分类:accept_direct / reject_direct / to_embedding     │
└─────────────────────────────────────────────────────────────┘
    │
    ├──────────────────┬──────────────────┐
    ▼                  ▼                  ▼
 岗位字典匹配       模糊岗位           直接拒绝
 (精筛)            (需进一步判断)
    │                  │
    ▼                  ▼
┌─────────────────┐  ┌─────────────────┐
│ Stage 2: 字典精筛 │  │ Stage 3: 技能筛选 │
│ (second.py)      │  │ (third.py)       │
│ - 加权评分       │  │ - 技能标签匹配    │
│ - 去重           │  │ - 英文假阳性过滤  │
│ - 后置规则覆盖   │  │ - 全局去重       │
└─────────────────┘  └─────────────────┘
    │                  │
    ▼                  ▼
┌─────────────────────────────────────────────────────────────┐
│  Stage 4: 语义聚类 (fourth.py)                               │
│  - SentenceTransformer 向量化                                │
│  - UMAP 降维                                                 │
│  - HDBSCAN 密度聚类                                          │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│  Stage 5: 池合并与技能恢复 (fifth.py)                         │
│  - 合并字典池和模糊池                                          │
│  - 从 reject 中恢复有技能标签的记录                             │
│  - 最终去重                                                   │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│  Stage 6: AI 行业特征富化 (final.py)                          │
│  - 三通道评分:标签优先 / 文本规则 / 兜底                       │
│  - AI 专业映射                                                │
│  - 广东省重点产业赛道匹配                                      │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
最终输出 (高质量 AI 岗位池)

二、Stage 1:文本清洗与标签提取

文件first_cleaned.py

2.1 核心思路

原始 JD(Job Description)文本充满噪声,第一步是用正则表达式清洗无关内容,同时提取结构化标签。

2.2 噪声清洗规则

# 各类噪声的正则模式
RE_WELFARE = re.compile(
    r"(五险一金|带薪年假|节日福利|补充医疗保险|交通补贴|餐饮补贴|住房补贴|"
    r"定期体检|员工旅游|年终奖金|绩效奖金|股票期权|弹性工作|免费班车|免费工作餐)"
)

RE_CONTACT = re.compile(
    r"(联系人|联系电话|联系方式|Tel|Phone|邮箱|Email|地址|工作地点)[::]\s*\S+"
)

RE_WORKTIME = re.compile(
    r"(工作时间|上班时间|工作制度)[::]\s*\S+"
)

RE_SOFTSKILL = re.compile(
    r"(沟通能力|团队合作|责任心强|抗压能力|学习能力|执行力|积极主动|"
    r"良好的沟通|较强的责任心|吃苦耐劳)"
)

RE_COMPANY_DESC = re.compile(
    r"(公司简介|公司介绍|关于我们|企业介绍|公司成立于|公司规模|注册资本)"
)

RE_GARBAGE_SECTION = re.compile(
    r"(岗位职责|任职要求|职位描述|职位要求|岗位要求|工作职责|岗位信息|"
    r"职位信息|薪资福利|福利待遇|晋升空间|发展空间|培训机会)"
)

2.3 标签提取系统

项目定义了 5 个维度的标签提取:

# 1. 技能标签(30+ 技术)
SKILL_PATTERNS = {
    "Python": re.compile(r"\bpython\b", re.I),
    "Java": re.compile(r"\bjava\b", re.I),
    "PyTorch": re.compile(r"\bpytorch\b", re.I),
    "TensorFlow": re.compile(r"\btensorflow\b", re.I),
    "OpenCV": re.compile(r"\bopencv\b", re.I),
    "Transformer": re.compile(r"\btransformer\b", re.I),
    "BERT": re.compile(r"\bbert\b", re.I),
    "GPT": re.compile(r"\bgpt\b", re.I),
    # ... 更多技能
}

# 2. 领域标签
DOMAIN_PATTERNS = {
    "自然语言处理": re.compile(r"(NLP|自然语言处理|文本处理|语义理解)"),
    "计算机视觉": re.compile(r"(CV|计算机视觉|图像识别|目标检测)"),
    "大模型": re.compile(r"(大模型|LLM|大语言模型|GPT)"),
    "机器学习": re.compile(r"(机器学习|深度学习|神经网络)"),
    "自动驾驶": re.compile(r"(自动驾驶|无人驾驶|ADAS)"),
    # ... 更多领域
}

# 3. 动作标签
ACTION_PATTERNS = {
    "训练": re.compile(r"(训练|train|training)"),
    "部署": re.compile(r"(部署|deploy|deployment)"),
    "优化": re.compile(r"(优化|optimize|optimization)"),
    "评测": re.compile(r"(评测|评估|benchmark)"),
}

# 4. 场景标签
SCENE_PATTERNS = {
    "搜索": re.compile(r"(搜索|search|query)"),
    "推荐": re.compile(r"(推荐|recommend)"),
    "风控": re.compile(r"(风控|反欺诈|风险控制)"),
}

# 5. 角色标签
ROLE_PATTERNS = {
    "算法": re.compile(r"(算法|algorithm)"),
    "标注": re.compile(r"(标注|label|annotation)"),
    "测试": re.compile(r"(测试|test|testing)"),
}

2.4 标签置信度评分

def compute_tag_confidence(record):
    """根据标签数量和质量计算置信度"""
    skill_count = len(split_tags(record.get("jd_skill_tags", "")))
    domain_count = len(split_tags(record.get("jd_domain_tags", "")))

    if skill_count >= 3 and domain_count >= 2:
        return "high"
    elif skill_count >= 2 or domain_count >= 2:
        return "medium"
    elif skill_count >= 1 or domain_count >= 1:
        return "low"
    else:
        return "very_low"

2.5 路由决策

def make_routing_decision(record):
    """决定记录去向"""
    bucket = classify_job_title(record.get("name", ""))
    confidence = record.get("tag_confidence", "very_low")

    # 直接接受:正向岗位 + 高置信度
    if bucket == "positive" and confidence in ("high", "medium"):
        return "accept_direct"

    # 直接拒绝:负向岗位
    if bucket == "negative":
        return "reject_direct"

    # 其他进入 embedding 阶段
    return "to_embedding"

三、Stage 2:岗位字典精筛

文件second.py

3.1 加权评分系统

对于通过字典匹配的岗位,使用加权评分判断是否保留:

# 权重定义
STRONG_DOMAIN_HIGH = {
    "自然语言处理": 3.2,
    "计算机视觉": 3.2,
    "大模型": 3.0,
    "机器学习": 3.0,
    "自动驾驶": 3.0,
}

MEDIUM_SKILL_WEIGHTS = {
    "Python": 1.5,
    "Java": 1.2,
    "C++": 1.2,
    "PyTorch": 2.0,
    "TensorFlow": 2.0,
    "OpenCV": 1.8,
    "Transformer": 1.8,
}

# 评分逻辑
def calculate_score(record):
    score = 0.0

    # 领域加分
    domain_tags = split_tags(record.get("jd_domain_tags", ""))
    for tag in domain_tags:
        score += STRONG_DOMAIN_HIGH.get(tag, 0)

    # 技能加分
    skill_tags = split_tags(record.get("jd_skill_tags", ""))
    for tag in skill_tags:
        score += MEDIUM_SKILL_WEIGHTS.get(tag, 0)

    # 动作加分
    action_tags = split_tags(record.get("jd_action_tags", ""))
    if "训练" in action_tags:
        score += 2.0
    if "部署" in action_tags:
        score += 1.5

    return score

3.2 去重策略

def build_duplicate_signature(record):
    """构建去重签名:公司 + 关键词 + 职责"""
    company = normalize_text_key(record.get("company", ""))
    keyword = normalize_text_key(record.get("keyword", ""))
    responsibility = normalize_text_key(record.get("responsibility_compact", ""))
    base = f"{company}|{keyword}|{responsibility}"
    return hashlib.md5(base.encode("utf-8")).hexdigest()

3.3 后置规则覆盖

某些特殊情况需要覆盖评分结果:

def apply_post_rules_override(record):
    """后置规则:强制拒绝某些情况"""
    name = str(record.get("name", ""))

    # 硬负向标题
    if contains_any_term(name, HARD_NEGATIVE_TITLE_TERMS):
        record["final_decision"] = "reject"
        record["final_reason"] = "hard_negative_title_override"
        return record

    # 运营类岗位(除非有强AI特征)
    if should_reject_operation_case(record):
        record["final_decision"] = "reject"
        record["final_reason"] = "operation_title_override"
        return record

    # 弱搜索案例(只有搜索标签,无NLP/LLM技能)
    if is_weak_search_case(record):
        record["final_decision"] = "reject"
        record["final_reason"] = "weak_search_case_override"
        return record

    return record

四、Stage 3:模糊岗位技能筛选

文件third.py

4.1 技能直留规则

对于无法通过字典匹配的"模糊"岗位,使用技能标签判断:

# 强技能标签(命中1个即保留)
STRONG_SKILL_TAGS = {
    "PyTorch", "TensorFlow", "OpenCV", "PCL", "CUDA",
    "Transformer", "BERT", "GPT", "LLaMA", "LangChain",
    "HuggingFace", "RAG", "LoRA", "CNN", "RNN", "GAN",
    "XGBoost", "LightGBM", "Scikit-learn",
}

# 中等技能标签(命中2个即保留)
MEDIUM_SKILL_TAGS = {
    "Python", "Java", "C++", "C#", "Go", "MATLAB",
    "SQL", "MySQL", "PostgreSQL", "MongoDB", "Redis",
    "ElasticSearch", "Linux", "Shell", "Docker", "Kubernetes",
    "Spark", "Hadoop", "Hive", "Pandas", "NumPy", "Matplotlib",
}

def should_direct_keep_by_skill(record):
    """技能直留判断"""
    name = str(record.get("name", ""))

    # 硬负向标题直接拒绝
    if contains_any_term(name, HARD_NEGATIVE_TITLE_TERMS):
        return False

    # 英文假阳性过滤
    if is_english_false_positive(record):
        return False

    # 强技能命中
    if has_strong_skill(record):
        return True

    # 中等技能命中2个
    if has_medium_skill_pair(record):
        return True

    return False

4.2 英文假阳性检测

某些岗位虽然包含技术关键词,但实际是纯英文环境的非技术岗:

def is_english_false_positive(record):
    """检测英文假阳性"""
    resp = str(record.get("responsibility_compact", ""))
    req = str(record.get("requirement_compact", ""))
    merged = f"{resp} {req}".strip()

    ratio = english_ratio(merged)  # 英文字符占比
    has_domain = has_core_ai_domain(record)

    # 英文占比过高且无AI领域标签 → 假阳性
    if ratio >= 0.55 and not has_domain:
        return True

    # 英文占比过高且技能标签弱 → 假阳性
    if ratio >= 0.55 and strong_skill_count == 0 and medium_skill_count <= 2:
        return True

    return False

4.3 全局去重

所有保留的记录进入候选池,按时间保留最新:

# 候选池:签名 -> (时间, 记录)
candidate_pool = {}

for row in accepted_rows:
    sig = build_duplicate_signature(row)
    time_val = str(row.get("time", "") or "")

    if sig not in candidate_pool or time_val > candidate_pool[sig][0]:
        candidate_pool[sig] = (time_val, row)

五、Stage 4:语义向量聚类

文件fourth.py

5.1 向量化

使用 SentenceTransformer 将岗位文本转换为向量:

from sentence_transformers import SentenceTransformer

# 加载中文 BERT 模型
MODEL_NAME = "D:/models/bge-large-zh-v1.5"
model = SentenceTransformer(MODEL_NAME, device="cuda")

def build_embedding_text(record):
    """构建用于向量化的文本"""
    parts = []
    if record.get("name"):
        parts.append(f"岗位名称: {record['name']}")
    if record.get("keyword"):
        parts.append(f"关键词: {record['keyword']}")
    if record.get("jd_domain_tags"):
        parts.append(f"领域标签: {record['jd_domain_tags']}")
    if record.get("jd_skill_tags"):
        parts.append(f"技能标签: {record['jd_skill_tags']}")
    if record.get("responsibility_compact"):
        parts.append(f"职责: {record['responsibility_compact']}")
    if record.get("requirement_compact"):
        parts.append(f"要求: {record['requirement_compact']}")
    return "\n".join(parts)

# 批量编码
embeddings = model.encode(
    texts,
    batch_size=256,
    normalize_embeddings=True,
    show_progress_bar=True
)

5.2 原型分类

定义 8 个类别原型,用余弦相似度分类:

CATEGORY_PROTOTYPES = {
    "nlp_llm": "自然语言处理、大语言模型、LLM、文本生成、信息抽取...",
    "cv_vision": "计算机视觉、CV、图像识别、目标检测、图像分割...",
    "autonomous_robotics": "自动驾驶、无人驾驶、ADAS、SLAM、机器人...",
    "search_recommend_risk": "搜索算法、推荐系统、风控、反欺诈...",
    "data_labeling_training": "数据标注、模型评测、数据清洗、AI训练师...",
    "mlops_deployment_eval": "模型部署、推理服务、MLOps、性能优化...",
    "ai_product_solution": "AI产品经理、智能体、Agent、解决方案...",
    "other_ai": "人工智能、AI、机器学习、深度学习、算法研发...",
}

def classify_one(embedding, prototype_embeddings):
    """基于余弦相似度分类"""
    scores = []
    for category, proto_vec in prototype_embeddings.items():
        sim = cosine_similarity(embedding, proto_vec)
        scores.append((category, sim))

    scores.sort(key=lambda x: x[1], reverse=True)
    top1_cat, top1_score = scores[0]
    top2_cat, top2_score = scores[1]

    return {
        "embedding_category_top1": top1_cat,
        "embedding_category_score_top1": f"{top1_score:.4f}",
        "embedding_category_confidence": judge_confidence(top1_score, top2_score),
    }

def judge_confidence(top1_score, top2_score):
    """判断分类置信度"""
    if top1_score >= 0.75 and (top1_score - top2_score) >= 0.08:
        return "high"
    if top1_score >= 0.65 and (top1_score - top2_score) >= 0.03:
        return "medium"
    return "low"

5.3 UMAP 降维

将高维向量降到低维,便于聚类:

import umap

umap_reducer = umap.UMAP(
    n_neighbors=15,        # 邻居数量
    n_components=10,       # 降维后的维度
    min_dist=0.0,          # 最小距离(0表示允许紧密聚类)
    metric='cosine',       # 使用余弦距离
    random_state=42
)

reduced = umap_reducer.fit_transform(vectors)

参数说明

  • n_neighbors=15:控制局部与全局结构的平衡,值越大越关注全局
  • n_components=10:降到 10 维,保留主要结构信息
  • min_dist=0.0:允许点紧密聚集,有利于发现密集簇

5.4 HDBSCAN 密度聚类

import hdbscan

clusterer = hdbscan.HDBSCAN(
    min_cluster_size=10,           # 最小簇大小
    min_samples=5,                 # 核心点最小邻居数
    metric='euclidean',            # 距离度量
    cluster_selection_method='eom' # 簇选择方法
)

labels = clusterer.fit_predict(reduced)

# 结果解读
# -1 表示噪声点(不属于任何簇)
# 0, 1, 2... 表示簇编号

参数说明

  • min_cluster_size=10:簇至少包含 10 个样本,小于此数的被视为噪声
  • min_samples=5:核心点需要至少 5 个邻居,控制簇的密度
  • cluster_selection_method='eom':Excess of Mass 方法,倾向于选择更紧凑的簇

5.5 类内聚类

对每个类别分别进行聚类,发现子类:

# 按类别分组
cat_to_indices = {}
for idx, row in enumerate(rows):
    cat = row["embedding_category_top1"]
    cat_to_indices.setdefault(cat, []).append(idx)

# 对每个类别单独聚类
for cat, indices in cat_to_indices.items():
    if len(indices) < HDBSCAN_MIN_CLUSTER_SIZE * 2:
        # 样本太少,跳过聚类
        for idx in indices:
            rows[idx]["sub_cluster_id"] = f"{cat}_0"
        continue

    # 降维
    cat_vectors = vectors_array[indices]
    reduced = umap_reducer.fit_transform(cat_vectors)

    # 聚类
    labels = hdbscan_clusterer.fit_predict(reduced)

    # 标记结果
    for j, idx in enumerate(indices):
        if labels[j] == -1:
            rows[idx]["sub_cluster_id"] = f"{cat}_noise"
        else:
            rows[idx]["sub_cluster_id"] = f"{cat}_{labels[j]}"

六、Stage 5:池合并与技能恢复

文件fifth.py

6.1 池合并

将多个来源的岗位合并:

# 四个来源
dict_final_rows = read_csv_rows("岗位字典_final.csv")
fuzzy_final_rows = read_csv_rows("模糊岗位_final.csv")
dict_recovered_rows = read_csv_rows("岗位字典_reject_skill_recovered.csv")
fuzzy_recovered_rows = read_csv_rows("模糊岗位_reject_skill_recovered.csv")

# 标记来源
for row in dict_final_rows:
    row["pool_source"] = "岗位字典_final"

for row in fuzzy_final_rows:
    row["pool_source"] = "模糊岗位_final"

# 合并
final_pool = dict_final_rows + fuzzy_final_rows + dict_recovered_rows + fuzzy_recovered_rows

6.2 技能恢复

从 reject 池中恢复有技能标签的记录:

def recover_skill_rows(rows, source_name):
    """从 reject 中恢复有技能的记录"""
    recovered = []

    for row in rows:
        strong_skill_count = safe_int(row.get("strong_skill_count", 0))
        medium_skill_count = safe_int(row.get("medium_skill_count", 0))

        # 强技能 >= 1 或 中等技能 >= 2
        if strong_skill_count >= 1 or medium_skill_count >= 2:
            # 英文假阳性过滤
            if is_english_false_positive(row):
                continue

            row["recovered_from_reject"] = 1
            row["recovered_source"] = source_name
            row["recovered_reason"] = (
                "strong_skill_recovered"
                if strong_skill_count >= 1
                else "two_medium_skills_recovered"
            )
            recovered.append(row)

    return recovered

6.3 最终去重

def deduplicate_keep_latest(rows):
    """去重,保留最新记录"""
    rows_sorted = sorted(rows, key=lambda r: str(r.get("time", "")), reverse=True)

    seen = set()
    deduped = []

    for row in rows_sorted:
        sig = build_duplicate_signature(row)
        if sig not in seen:
            seen.add(sig)
            deduped.append(row)

    return deduped

七、Stage 6:AI 行业特征富化

文件final.py

7.1 三通道评分

def compute_ai_relevance_score(record):
    """三通道评分"""

    # Channel A: 标签优先
    domain_tags = split_tags(record.get("jd_domain_tags", ""))
    skill_tags = split_tags(record.get("jd_skill_tags", ""))

    strong_ai_domains = {"自然语言处理", "计算机视觉", "大模型", "机器学习", "自动驾驶"}
    strong_ai_skills = {"PyTorch", "TensorFlow", "OpenCV", "Transformer", "BERT", "GPT", "LLaMA"}

    domain_hit = len(domain_tags & strong_ai_domains)
    skill_hit = len(skill_tags & strong_ai_skills)

    if domain_hit >= 2:
        return 0.95, "high"
    if domain_hit >= 1 and skill_hit >= 1:
        return 0.90, "high"
    if skill_hit >= 2:
        return 0.85, "high"

    # Channel B: 文本规则
    text = f"{record.get('responsibility_compact', '')} {record.get('requirement_compact', '')}"
    ai_keywords = ["人工智能", "深度学习", "神经网络", "算法", "模型训练"]
    keyword_hits = sum(1 for kw in ai_keywords if kw in text)

    if keyword_hits >= 3:
        return 0.80, "medium"
    if keyword_hits >= 2:
        return 0.70, "medium"

    # Channel C: 兜底
    return 0.50, "low"

7.2 AI 专业映射

AI_MAJOR_MAP = {
    "计算机科学与技术": {
        "segments": ["nlp_llm", "cv_vision", "mlops_deployment_eval"],
        "clusters": ["算法工程师", "深度学习工程师"],
        "courses": ["机器学习", "深度学习", "计算机视觉"],
    },
    "人工智能": {
        "segments": ["nlp_llm", "cv_vision", "autonomous_robotics"],
        "clusters": ["AI算法工程师", "机器学习工程师"],
        "courses": ["自然语言处理", "计算机视觉", "强化学习"],
    },
    "数据科学与大数据技术": {
        "segments": ["data_labeling_training", "search_recommend_risk"],
        "clusters": ["数据分析师", "数据工程师"],
        "courses": ["数据挖掘", "大数据处理", "统计学习"],
    },
    # ... 更多专业
}

7.3 广东省重点产业赛道

GD_AI_INDUSTRY_TRACK_MAP = {
    "智能机器人": {
        "priority": "P0",
        "keywords": ["机器人", "ROS", "机械臂", "运动控制"],
    },
    "自动驾驶": {
        "priority": "P0",
        "keywords": ["自动驾驶", "ADAS", "SLAM", "路径规划"],
    },
    "智能语音": {
        "priority": "P1",
        "keywords": ["语音识别", "语音合成", "ASR", "TTS"],
    },
    "计算机视觉": {
        "priority": "P0",
        "keywords": ["图像识别", "目标检测", "OCR", "人脸识别"],
    },
    "自然语言处理": {
        "priority": "P0",
        "keywords": ["NLP", "文本分析", "机器翻译", "情感分析"],
    },
    # ... 更多赛道
}

八、关键技术总结

8.1 正则表达式

  • 预编译:大量匹配时使用 re.compile() 提升性能
  • IGNORECASE:使用 re.IGNORECASE 忽略大小写
  • 非捕获分组:使用 (?:...) 避免不必要的捕获
  • 批量替换:将多个正则模式编译后依次替换

8.2 加权评分

  • 分层权重:强领域 > 中等技能 > 弱信号
  • 阈值判断:根据总分决定保留/拒绝
  • 后置覆盖:特殊规则可以覆盖评分结果

8.3 去重策略

  • 签名构建:公司 + 关键词 + 职责的 MD5 哈希
  • 时间排序:相同签名保留最新记录
  • 两阶段去重:阶段内去重 + 全局去重

8.4 向量化与聚类

  • SentenceTransformer:将文本转换为高维向量
  • UMAP:降维保留局部和全局结构
  • HDBSCAN:密度聚类,自动发现簇数量,识别噪声点
  • 类内聚类:对每个类别单独聚类,发现子类

8.5 工程技巧

  • MultiCSVWriter:延迟写入表头,支持多输出文件
  • 进度打印:每 50000 行打印进度
  • 内存优化:使用生成器和流式处理
  • 编码兼容:统一使用 utf-8-sig 编码

九、数据流转图

┌──────────────────────────────────────────────────────────────────┐
│                         原始数据 (1600w)                          │
└──────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                    Stage 1: 文本清洗与标签提取                      │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │ accept_direct│  │reject_direct│  │to_embedding │              │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘              │
└─────────┼────────────────┼────────────────┼──────────────────────┘
          │                │                │
          │                ▼                │
          │         直接拒绝 (噪声)          │
          │                                 │
          ▼                                 ▼
┌─────────────────────┐          ┌─────────────────────┐
│  Stage 2: 字典精筛    │          │  Stage 3: 技能筛选    │
│  ┌─────────────────┐│          │  ┌─────────────────┐│
│  │岗位字典_final    ││          │  │模糊岗位_final    ││
│  │岗位字典_reject   ││          │  │模糊岗位_reject   ││
│  └────────┬────────┘│          │  └────────┬────────┘│
└───────────┼─────────┘          └───────────┼─────────┘
            │                                │
            ▼                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                    Stage 4: 语义向量聚类                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │ SentenceTrans│  │    UMAP     │  │  HDBSCAN    │              │
│  │  former      │  │   降维      │  │  密度聚类    │              │
│  └──────────────┘  └─────────────┘  └─────────────┘              │
└──────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                    Stage 5: 池合并与技能恢复                        │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ 岗位字典_final + 模糊岗位_final + reject_skill_recovered     │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                          │                                       │
│                          ▼                                       │
│                    全局去重 (MD5签名)                              │
└──────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                    Stage 6: AI 行业特征富化                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │ AI相关度评分  │  │ 专业映射     │  │ 产业赛道匹配 │              │
│  └──────────────┘  └─────────────┘  └─────────────┘              │
└──────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                      最终输出 (高质量AI岗位池)                      │
│  - embedding_category_top1: 岗位类别                              │
│  - sub_cluster_id: 子类编号                                       │
│  - ai_relevance_score: AI相关度评分                                │
│  - ai_mapped_majors: 匹配专业                                     │
│  - ai_gd_key_industry_track: 产业赛道                             │
└──────────────────────────────────────────────────────────────────┘

十、性能优化建议

10.1 内存优化

# 1. 使用生成器替代列表
def process_rows(filepath):
    with open(filepath, "r", encoding="utf-8-sig") as f:
        for row in csv.DictReader(f):
            yield clean_row(row)

# 2. 分批处理
BATCH_SIZE = 10000
batch = []
for row in process_rows("huge_file.csv"):
    batch.append(row)
    if len(batch) >= BATCH_SIZE:
        process_batch(batch)
        batch.clear()

# 3. 及时释放内存
import gc
del large_list
gc.collect()

10.2 计算优化

# 1. 正则预编译
PATTERN = re.compile(r"complex_pattern")  # 编译一次
PATTERN.search(text)  # 多次使用

# 2. 集合查找替代列表查找
VALID_TAGS = {"Python", "Java", "C++"}  # 集合 O(1)
tag in VALID_TAGS  # 快速判断

# 3. 批量向量化
embeddings = model.encode(texts, batch_size=256)  # 批量处理

10.3 I/O 优化

# 1. 使用 utf-8-sig 编码(兼容 Excel)
with open("output.csv", "w", encoding="utf-8-sig", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(rows)

# 2. 延迟写入表头
class MultiCSVWriter:
    def write_row(self, row):
        if not self._header_written:
            self.fieldnames = list(row.keys())
            self.writer = csv.DictWriter(self.file, fieldnames=self.fieldnames)
            self.writer.writeheader()
            self._header_written = True
        self.writer.writerow(row)

附录:核心数据结构

岗位记录字段

字段名说明示例
name岗位名称"算法工程师"
company公司名称"某科技公司"
keyword搜索关键词"NLP"
responsibility_compact职责描述(清洗后)"负责NLP模型训练..."
requirement_compact要求描述(清洗后)"熟悉PyTorch..."
jd_skill_tags技能标签"Python;PyTorch;Transformer"
jd_domain_tags领域标签"自然语言处理;大模型"
jd_action_tags动作标签"训练;部署"
jd_scene_tags场景标签"搜索;推荐"
jd_role_tags角色标签"算法"
job_title_bucket岗位分类"positive" / "boundary" / "negative"
final_decision最终决策"accept_direct" / "reject"
final_reason决策原因"skill_direct_keep"
embedding_category_top1向量分类结果"nlp_llm"
sub_cluster_id子类编号"nlp_llm_3"
ai_relevance_scoreAI相关度评分0.85
ai_relevance_levelAI相关度等级"high"

这套 Pipeline 的核心设计思想是:分层过滤 + 多信号融合 + 语义增强。通过规则层快速过滤明显噪声,通过技能标签层保留有价值信号,通过语义向量层发现隐含关联,最终实现从 1600 万行原始数据中精准提取高质量 AI 岗位。