//! 标题解析、人名提取、标签提取 //! 对应 Python 的 tag_index.py + clean_person_names.py 的核心逻辑 use regex::Regex; use std::collections::HashSet; use lazy_static::lazy_static; use jieba_rs::Jieba; // 全局 jieba 分词器 lazy_static! { static ref JIEBA: Jieba = Jieba::new(); } // ── 常量 ── /// 模特称呼前缀(移除用) const MODEL_TITLE_PREFIX: &[&str] = &[ "美女模特", "性感女神", "清纯美女", "气质美女", "可爱女孩", "甜美少女", "时尚达人", "女神", "美女", "模特", "小姐姐", "小仙女", "御姐", "萝莉", "辣妹", "潮人", ]; /// 停用词(标签过滤用) const STOP_WORDS: &[&str] = &[ "写真", "套图", "图片", "照片", "摄影", "拍摄", "风格", "系列", "作品", "专辑", "主题", "人像", "艺术", "唯美", "诱惑", "性感", "迷人", "魅力", "时尚", "潮流", "高清", "原版", "无水印", "私房", "内衣", "比基尼", "泳装", "旗袍", "制服", "丝袜", "美腿", "美胸", "翘臀", "cosplay", "cos", "角色扮演", "动漫", "游戏", "classic", "exclusive", "premium", "collection", "photo", "photos", "video", "videos", "set", "vol", "no", "番号", "编号", "期号", ]; /// 保留词性(仅用于 jieba 分词场景,这里做占位符) const _KEEP_POS: &[&str] = &["n", "nr", "ns", "nz", "v", "vn", "a", "an", "eng", "x"]; /// 工作室/平台排除名(大幅扩充) const EXCLUDED_NAMES: &[&str] = &[ // 工作室/平台 "Pure Media", "XiuRen秀人网", "尤蜜", "魅妍社", "花漾", "瑞丝馆", "撸图", "情欲萝莉", "蜜桃社", "tgod", "MiStar", "BLUECAKE", "HuaYang", "YouMi", "秀人", "XIUREN", "YouMi尤蜜", "IMiss爱蜜社", "MiiTao蜜桃社", "MFStar模范学院", "TGOD推女神", "DKGirl御女郎", "XINGYAN星颜社", "XIAOYU语画界", "YOUMI尤蜜荟", "FAIRY CLUB", "FAIRY-CLUB", "FEILIN嗲囡囡", "CANDY糖果画报", "YouWu尤物馆", "UXING优星馆", "108TV", "108酱", "RuiSG瑞丝馆", "SIW斯文传媒", "SLADY猎女神", "SSA丝社", "SJA佳爷", "TASHA系列", "Ugirls爱尤物", "V Girls", "VGirl", "WingS影私荟", "YALAYI雅拉伊", "ISHOW爱秀", "ISHOW", "写真", "私房", "套图", "系列", "合集", "作品", // 国家/地区 "日本", "韩国", "中国", "台湾", "香港", "泰国", "越南", "新加坡", "马来西亚", "菲律宾", "印度尼西亚", "美国", "俄罗斯", "东京", "首尔", "北京", "上海", "广州", "深圳", "成都", "重庆", "曼谷", "胡志明", "新加坡市", "马尼拉", "雅加达", // 非人名常见词 "模特", "美女", "女神", "小姐姐", "小仙女", "御姐", "萝莉", "性感", "诱惑", "清纯", "甜美", "可爱", "迷人", "魅力", "时尚", "潮流", "高清", "原版", "无水印", "完整版", "比基尼", "泳装", "内衣", "丝袜", "制服", "旗袍", "睡衣", "浴室", "浴缸", "床上", "沙发", "厨房", "阳台", "室内", "户外", "街拍", "旅拍", "海边", "沙滩", "泳池", "cosplay", "角色扮演", "动漫", "游戏", "超短裙", "包臀裙", "紧身裙", "连衣裙", "牛仔裤", "热裤", "高跟鞋", "运动鞋", "凉鞋", "美腿", "美胸", "翘臀", "玉足", "美背", "香肩", "蛮腰", "白皙", "肌肤", "纹身", "日系", "韩系", "森系", "复古", "冷艳", "优雅", "知性", "俏皮", "温柔", "妩媚", "野性", "热情", "慵懒", "迷离", "唯美", "清新", "冷傲", // 工作室前缀 "PureMedia", "BlueCake", "HuaYang", "XiuRen", "MiStar", "YouMi", "MFStar", "TGOD", "DKGirl", "XINGYAN", "XIAOYU", "YOUMI", "FAIRYCLUB", "FEILIN", "CANDY", "YouWu", "UXING", "RuiSG", "SIW", "SLADY", "SSA", "SJA", "TASHA", "Ugirls", "VGIRLS", "VGIRL", "WING", "YALAYI", "ISHOW", ]; lazy_static! { /// 编译后的正则 static ref RE_COSPLAY: Regex = Regex::new( r"(?i)^(.+?)\s+cos(play)?\s+(.+?)(?:\s*[–—\-]\s*(.+))?$" ).unwrap(); static ref RE_COSPLAY_BY: Regex = Regex::new( r"(?i)^(.+?)\s+cos(play)?\s+by\s+(.+?)(?:\s*[–—\-]\s*(.+))?$" ).unwrap(); static ref RE_BRACKET_PREFIX: Regex = Regex::new(r"^\[([^\]]+)\]\s*").unwrap(); static ref RE_NUMBER_SUFFIX: Regex = Regex::new(r"[\s#]*(?:no\.?\s*|vol\.?\s*|#)?\d+[\s]*$").unwrap(); static ref RE_PAREN_CONTENT: Regex = Regex::new(r"[((]([^))]*)[))]").unwrap(); static ref RE_SPLIT_DASH: Regex = Regex::new(r"\s*[–—\-]\s*").unwrap(); static ref RE_PHOTOS_SUFFIX: Regex = Regex::new(r"(?i)\s*\d+\s*photos?\s*(?:and\s*\d+\s*videos?)?\s*$").unwrap(); static ref RE_CHINESE: Regex = Regex::new(r"[\u{4e00}-\u{9fff}]").unwrap(); static ref RE_ENGLISH_SUFFIX: Regex = Regex::new(r"^([\u{4e00}-\u{9fff}]+)([A-Za-z].*)$").unwrap(); static ref RE_TRAILING_PUNCT: Regex = Regex::new(r"[\s.,;:,。;:\-–—]+$").unwrap(); /// 人名模式:2-5个中文字符 + 可选英文后缀 static ref RE_CN_NAME: Regex = Regex::new( r"([\u{4e00}-\u{9fff}]{2,5}(?:[A-Za-z][A-Za-z0-9_]*)?)" ).unwrap(); /// 常见人名后缀(表明前面是人名) static ref RE_NAME_CONTEXT: Regex = Regex::new( r"(?i)(私房|写真|内衣|比基尼|泳装|旗袍|制服|街拍|室内|户外|浴室|浴室|浴缸|床上|床照|内衣|透视|包臀裙|紧身裙|超短裙|丝袜|美腿|美臀|美胸|翘臀|性感|清纯|甜美|气质|可爱|女神|嫩模|模特)" ).unwrap(); /// 停用词集合 static ref STOP_WORDS_SET: HashSet<&'static str> = { let mut s = HashSet::new(); for w in STOP_WORDS { s.insert(*w); } s }; /// 排除名集合(小写) static ref EXCLUDED_LOWER: HashSet = { let mut s = HashSet::new(); for n in EXCLUDED_NAMES { s.insert(n.to_lowercase()); } s.extend(["youmi", "huayang", "bluecake", "mistar", "xiuren", "秀人"].map(|s| s.to_string())); s }; } // ── 人名提取 ── /// 从标题中提取模特/人物名称 /// 核心策略(按优先级): /// 1. cosplay 格式匹配(Name cosplay Character) /// 2. jieba 分词 + 词性标注(优先提取 nr 人名) /// 3. 清理标题(移除[工作室]、编号、N photos等) /// 4. 用分隔符(–—-)切分,第一部分常含人名 /// 5. 扫描匹配"2-5中文+可选英文"模式的词 /// 6. 结合上下文(后面是否跟"写真/私房/性感"等关键词) pub fn extract_person_name_from_title(title: &str) -> Option { let title = title.trim(); if title.is_empty() { return None; } // 步骤1: 基础清理 let mut t = RE_PHOTOS_SUFFIX.replace(title, "").trim().to_string(); // 步骤2: cosplay 格式优先匹配 if let Some(caps) = RE_COSPLAY_BY.captures(&t) { return Some(clean_name(caps[3].trim())); } if let Some(caps) = RE_COSPLAY.captures(&t) { return Some(clean_name(caps[1].trim())); } // 步骤2.5: jieba 分词 + 词性标注(最高优先级的中文人名提取) // 只保留词性为 nr(人名)、n(名词)、nz(其它专名) 的词 if let Some(name) = extract_person_with_jieba(&t) { return Some(name); } // 步骤3: 移除 [工作室] 前缀 t = RE_BRACKET_PREFIX.replace(&t, "").trim().to_string(); // 步骤4: 移除模特称呼前缀 for prefix in MODEL_TITLE_PREFIX { if t.starts_with(prefix) { t = t[prefix.len()..].trim().to_string(); break; } } // 步骤5: 移除编号后缀 t = RE_NUMBER_SUFFIX.replace(&t, "").trim().to_string(); // 步骤6: 用分隔符切分,检查每一部分 let parts: Vec<&str> = RE_SPLIT_DASH.split(&t).collect(); // 策略A: 检查第一部分(分隔符前的内容最可能包含人名) if let Some(first) = parts.first() { if let Some(name) = extract_name_from_segment(first) { if is_likely_person_name(&name) { return Some(name); } } } // 策略B: 如果第一部分没找到,扫描所有部分 // 优先选择后面跟有关键词的部分 for i in 0..parts.len() { let part = parts[i]; // 检查这一部分后面是否跟有"写真/私房"等关键词 let has_context_after = if i + 1 < parts.len() { RE_NAME_CONTEXT.is_match(parts[i + 1]) } else { false }; if let Some(name) = extract_name_from_segment(part) { if is_likely_person_name(&name) { if has_context_after { return Some(name); // 有关键词上下文,优先返回 } } } } // 策略C: 直接在整个标题中搜索人名模式 for cap in RE_CN_NAME.captures_iter(&t) { let name = clean_name(&cap[1]); if is_likely_person_name(&name) { return Some(name); } } None } /// 用 jieba 分词提取人名(最高优先级策略) /// 只保留词性为 nr(人名)、n(名词)、nz(其它专名) 的词 /// 同时结合二次验证过滤非人名 fn extract_person_with_jieba(title: &str) -> Option { // 对标题做词性标注分词(true = 使用 HMM 新词发现) let tags = JIEBA.tag(title, true); // 按优先级收集候选词:nr > nz > n let mut candidates: Vec<(String, usize)> = Vec::new(); // (词, 优先级) for tag in &tags { let word = tag.word.trim(); if word.len() < 2 { continue; } let priority = match tag.tag { "nr" => 0, // 人名(最高优先级) "nrfg" => 1, // 古体人名 "nrt" => 2, // 音译人名 "nz" => 3, // 其它专名 "n" => 4, // 名词 _ => continue, // 其他词性跳过 }; if is_likely_person_name(word) { candidates.push((word.to_string(), priority)); } } // 按优先级排序,取第一个 candidates.sort_by(|a, b| a.1.cmp(&b.1)); candidates.into_iter().next().map(|(name, _)| clean_name(&name)) } /// 从一个文本片段中提取人名 fn extract_name_from_segment(segment: &str) -> Option { let seg = segment.trim(); if seg.is_empty() { return None; } // 尝试直接匹配人名模式 if let Some(cap) = RE_CN_NAME.captures(seg) { let name = clean_name(&cap[1]); if name.len() >= 2 { return Some(name); } } // 如果整个片段是纯中文(2-5字),也认为是人名 let is_pure_cn = seg.chars().all(|c| RE_CHINESE.is_match(&c.to_string())); if is_pure_cn && seg.len() >= 2 && seg.len() <= 6 { return Some(clean_name(seg)); } None } /// 清理人名:移除前后空白和标点 fn clean_name(name: &str) -> String { let name = name.trim(); // 移除末尾标点 let name = RE_TRAILING_PUNCT.replace(name, "").trim().to_string(); name } /// 提取中文核心(开头的连续中文字符) /// 对应 Python merge_persons_by_core.extract_cn pub fn extract_cn(name: &str) -> Option { let re = Regex::new(r"^([\u{4e00}-\u{9fff}]+)").unwrap(); re.captures(name).map(|c| c[1].to_string()) } /// 判断后缀是否有效(不含中文=有效英文后缀) /// 对应 Python merge_persons_by_core.is_valid_suffix pub fn is_valid_suffix(suffix: &str) -> bool { if suffix.is_empty() { return true; } !RE_CHINESE.is_match(suffix) } /// 判断是否为工作室/平台名(需排除) pub fn is_excluded_name(name: &str) -> bool { EXCLUDED_LOWER.contains(&name.to_lowercase()) } /// 二次验证:判断一个词是否"看起来像人名" /// 返回 true = 像人名,可以保留;false = 不像,应该排除 pub fn is_likely_person_name(name: &str) -> bool { let name = name.trim(); if name.is_empty() { return false; } if name.len() < 2 { return false; } // 先排除已知的非人名 if is_excluded_name(name) { return false; } let name_lower = name.to_lowercase(); // 纯数字排除 if name.chars().all(|c| c.is_ascii_digit()) { return false; } // 含数字太多的排除(超过一半是数字) let digit_count = name.chars().filter(|c| c.is_ascii_digit()).count(); if digit_count * 2 > name.len() { return false; } // 纯英文且长度 > 15 的排除(太长不像人名) let has_cn = RE_CHINESE.is_match(name); if !has_cn && name.len() > 15 { return false; } // 以数字开头的排除(除非是纯数字的英文名) if name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { return false; } // 含特殊字符过多的排除 let special_count = name.chars().filter(|c| { !c.is_alphanumeric() && !RE_CHINESE.is_match(&c.to_string()) }).count(); if special_count > 2 { return false; } // 已知后缀排除(如 "photos", "series", "collection" 等) let known_suffix = ["photos", "series", "collection", "edition", "set", "vol", "album"]; for suffix in known_suffix { if name_lower.ends_with(suffix) { return false; } } // 全部是大写字母且长度 > 6 的排除(通常是缩写/工作室名) if name.chars().all(|c| c.is_ascii_uppercase()) && name.len() > 6 { return false; } true } // ── 人名清洗 ── /// 清洗人名:移除前缀、括号内容、末尾标点 /// 返回 (cleaned_name, aliases) /// 对应 Python clean_person_names.clean_person_name pub fn clean_person_name(raw_name: &str) -> (String, Vec<(String, String)>) { let mut aliases = Vec::new(); let mut name = raw_name.to_string(); // 1. 移除前缀 for prefix in MODEL_TITLE_PREFIX { if name.starts_with(prefix) { aliases.push(("prefix".to_string(), prefix.to_string())); name = name[prefix.len()..].trim().to_string(); break; } } // 2. 移除末尾括号内容 let mut cleaned = String::new(); let mut last_end = 0; for cap in RE_PAREN_CONTENT.captures_iter(&name) { let m = cap.get(0).unwrap(); let content = cap.get(1).map_or("", |c| c.as_str()); cleaned.push_str(&name[last_end..m.start()]); last_end = m.end(); if !content.is_empty() { if RE_CHINESE.is_match(content) { aliases.push(("nickname".to_string(), content.to_string())); } else { aliases.push(("english".to_string(), content.to_string())); } } } cleaned.push_str(&name[last_end..]); let name = cleaned.trim().to_string(); // 3. 清理末尾标点 let name = RE_TRAILING_PUNCT.replace(&name, "").trim().to_string(); // 4. 从清洗后名字提取英文名别名 if let Some(caps) = RE_ENGLISH_SUFFIX.captures(&name) { let chinese_part = caps[1].to_string(); let english_part = caps[2].to_string(); if !english_part.is_empty() { aliases.push(("chinese".to_string(), chinese_part)); aliases.push(("english".to_string(), english_part)); } } if name.is_empty() { (raw_name.to_string(), aliases) } else { (name, aliases) } } /// 获取归并键(小写化+移除空格和标点) pub fn get_person_key(name: &str) -> String { name.to_lowercase() .chars() .filter(|c| !c.is_whitespace() && !c.is_ascii_punctuation()) .collect() } /// 拆分人名为「中文主名 + 别名列表」 /// 规则: /// - 如果有中文部分:主名 = 中文部分,别名 = 英文后缀 + 原始全名 + 括号内容 /// - 如果只有英文:主名 = 英文全名,别名 = 原始全名 /// /// 示例: /// - 杨晨晨Yome -> 主名"杨晨晨", 别名["Yome", "杨晨晨Yome"] /// - 杨晨晨Sugar -> 主名"杨晨晨", 别名["Sugar", "杨晨晨Sugar"] /// - Yome -> 主名"Yome", 别名["Yome"] pub fn split_person_name_with_aliases(raw_name: &str) -> (String, Vec<(String, String)>) { let (cleaned, mut aliases) = clean_person_name(raw_name); let mut primary_name = cleaned.clone(); // 尝试提取中文主名 if let Some(cn_part) = extract_cn(&cleaned) { if !cn_part.is_empty() && cn_part != cleaned { primary_name = cn_part.clone(); // 提取英文后缀作为别名 if let Some(rest) = cleaned.strip_prefix(&cn_part) { let english_suffix = rest.trim().to_string(); if !english_suffix.is_empty() { // 避免重复 let already_has = aliases.iter().any(|(_, a)| a == &english_suffix); if !already_has { aliases.push(("english".to_string(), english_suffix)); } } } } } // 原始完整名也作为别名(用于搜索匹配) if !raw_name.trim().is_empty() && raw_name.trim() != primary_name { let already_has = aliases.iter().any(|(_, a)| a == raw_name.trim()); if !already_has { aliases.push(("original".to_string(), raw_name.trim().to_string())); } } // cleaned 名也作为别名(如果和主名不同) if cleaned != primary_name { let already_has = aliases.iter().any(|(_, a)| a == &cleaned); if !already_has { aliases.push(("cleaned".to_string(), cleaned)); } } (primary_name, aliases) } // ── 一键提取:人名 + 标签(单次 jieba 分词) ── /// 一次 jieba 分词,同时提取人名和标签 /// 返回 (人名Option, 标签Vec) /// 优势:只做一次分词,省去重复开销 pub fn extract_person_and_tags(title: &str) -> (Option, Vec<(String, String)>) { let title = title.trim(); if title.is_empty() { return (None, Vec::new()); } // 做一次词性标注分词 let tags = JIEBA.tag(title, true); // ── 第一步:提取人名(优先级最高)── let person_name = extract_person_from_tags(&tags); // ── 第二步:提取名词标签(排除人名)── let tag_list = extract_tags_from_jieba_result(&tags, person_name.as_deref()); (person_name, tag_list) } /// 从 jieba 分词结果中提取人名 fn extract_person_from_tags(tags: &[jieba_rs::Tag]) -> Option { let mut candidates: Vec<(String, usize)> = Vec::new(); for tag in tags { let word = tag.word.trim(); if word.len() < 2 { continue; } let priority = match tag.tag { "nr" => 0, // 人名(最高优先级) "nrfg" => 1, // 古体人名 "nrt" => 2, // 音译人名 "nz" => 3, // 其它专名 "n" => 4, // 名词 _ => continue, }; if is_likely_person_name(word) { candidates.push((word.to_string(), priority)); } } candidates.sort_by(|a, b| a.1.cmp(&b.1)); candidates.into_iter().next().map(|(name, _)| clean_name(&name)) } /// 从 jieba 分词结果中提取标签(排除人名) fn extract_tags_from_jieba_result( tags: &[jieba_rs::Tag], person_name: Option<&str>, ) -> Vec<(String, String)> { use std::collections::HashSet; let mut result = Vec::new(); let mut seen = HashSet::new(); // 停用词 let stop_words: HashSet<&str> = STOP_WORDS.iter().copied().collect(); // 人名排除 let person_exclude: Vec = person_name .map(|n| { let mut v = vec![n.to_lowercase()]; for part in n.split_whitespace() { v.push(part.to_lowercase()); } if let Some(cn) = extract_cn(n) { v.push(cn.to_lowercase()); } v }) .unwrap_or_default(); for tag in tags { let word = tag.word.trim(); if word.len() < 2 { continue; } let word_lower = word.to_lowercase(); // 纯数字跳过 if word.chars().all(|c| c.is_ascii_digit()) { continue; } // 停用词跳过 if stop_words.contains(word_lower.as_str()) { continue; } // 人名跳过 if person_exclude.iter().any(|p| word_lower.contains(p)) { continue; } // 去重 if !seen.insert(word_lower.clone()) { continue; } // 按词性分类 let pos = tag.tag; let category = match pos { "n" | "ng" => "noun", "nz" => "proper", "ns" => "location", "nt" => "organization", "vn" => "noun_verb", "a" | "ad" | "an" => "adj", "v" | "vd" | "vg" => "verb", "eng" => "english", "x" => "other", // 排除的词性 "nr" | "nrfg" | "nrt" => continue, // 人名 "m" | "mq" => continue, "q" | "qv" | "qt" => continue, "r" | "rr" | "rz" => continue, "c" => continue, "p" => continue, "u" | "ul" | "uv" | "ud" | "ug" | "uj" => continue, "d" => continue, "e" | "y" | "o" => continue, "w" => continue, "t" | "tg" => continue, "s" => continue, "f" => continue, "b" | "bl" => continue, "z" => continue, _ => continue, }; if category == "english" && word.len() < 3 { continue; } result.push((word.to_string(), category.to_string())); } result } // ── 标签提取 ── /// 从标题中提取标签(排除人名部分) /// 返回 Vec<(tag, pos)> /// 对应 Python tag_index.extract_tags_from_title #[allow(dead_code)] pub fn extract_tags_from_title_legacy(title: &str, person_name: Option<&str>) -> Vec<(String, String)> { let mut title = title.to_string(); // 清理末尾的 "N photos" 等 title = RE_PHOTOS_SUFFIX.replace(&title, "").to_string(); // 移除人名 if let Some(name) = person_name { title = title.replace(name, " "); for part in name.split_whitespace() { title = title.replace(part, " "); } } // 移除 [工作室] 前缀 title = RE_BRACKET_PREFIX.replace(&title, "").to_string(); // 按空白分割,过滤停用词和短词 let mut tags = Vec::new(); let mut seen = HashSet::new(); for word in title.split_whitespace() { let word = word.trim().to_lowercase(); if word.len() < 2 { continue; } if STOP_WORDS_SET.contains(word.as_str()) { continue; } if word.chars().all(|c| c.is_ascii_digit() || c == '.') { continue; } if seen.contains(&word) { continue; } seen.insert(word.clone()); tags.push((word, "x".to_string())); } tags } /// 标签归一化 pub fn normalize_tag(tag: &str) -> String { tag.trim().to_lowercase() } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_person_name() { assert_eq!(extract_person_name_from_title("杨晨晨Yome 私房写真").unwrap(), "杨晨晨Yome"); assert_eq!(extract_person_name_from_title("王馨瑶yanni 比基尼").unwrap(), "王馨瑶yanni"); assert_eq!(extract_person_name_from_title("[XiuRen] 杨晨晨 性感").unwrap(), "杨晨晨"); assert_eq!(extract_person_name_from_title("美女模特 杨晨晨 私房").unwrap(), "杨晨晨"); } #[test] fn test_extract_cn() { assert_eq!(extract_cn("杨晨晨Yome").unwrap(), "杨晨晨"); assert_eq!(extract_cn("王馨瑶yanni").unwrap(), "王馨瑶"); assert!(extract_cn("Yome").is_none()); } #[test] fn test_clean_person_name() { let (cleaned, aliases) = clean_person_name("美女模特 杨晨晨Yome"); assert_eq!(cleaned, "美女模特 杨晨晨Yome"); assert!(!aliases.is_empty()); } #[test] fn test_split_person_name_with_aliases() { // 中文 + 英文后缀 let (name, aliases) = split_person_name_with_aliases("杨晨晨Yome"); assert_eq!(name, "杨晨晨"); let alias_names: Vec<&str> = aliases.iter().map(|(_, a)| a.as_str()).collect(); assert!(alias_names.contains(&"Yome")); assert!(alias_names.contains(&"杨晨晨Yome")); // 同一人,不同英文名 let (name2, aliases2) = split_person_name_with_aliases("杨晨晨Sugar"); assert_eq!(name2, "杨晨晨"); let alias_names2: Vec<&str> = aliases2.iter().map(|(_, a)| a.as_str()).collect(); assert!(alias_names2.contains(&"Sugar")); // 只有中文 let (name3, _) = split_person_name_with_aliases("杨晨晨"); assert_eq!(name3, "杨晨晨"); // 只有英文 let (name4, _) = split_person_name_with_aliases("Yome"); assert_eq!(name4, "Yome"); } }