This commit is contained in:
lychang
2026-07-19 18:57:30 +08:00
parent 879598585b
commit 34fdb18919
11 changed files with 2267 additions and 1531 deletions

View File

@@ -53,6 +53,14 @@ lazy_static! {
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> = {
@@ -73,48 +81,115 @@ lazy_static! {
// ── 人名提取 ──
/// 从标题中提取模特/人物名称
/// 对应 Python tag_index.extract_person_name_from_title
/// 核心策略:
/// 1. 清理标题(移除[工作室]、编号、N photos等)
/// 2. 尝试用分隔符(–—-)切分,第一部分常含人名
/// 3. 扫描匹配"2-5中文+可选英文"模式的词
/// 4. 结合上下文(后面是否跟"写真/私房/性感"等关键词)
pub fn extract_person_name_from_title(title: &str) -> Option<String> {
let title = title.trim();
if title.is_empty() {
return None;
}
// 清理末尾的 "N photos" 等
let title = RE_PHOTOS_SUFFIX.replace(title, "").trim().to_string();
// 步骤1: 基础清理
let mut t = RE_PHOTOS_SUFFIX.replace(title, "").trim().to_string();
// 1. cosplay 格式: "Name cosplay Character – Series"
if let Some(caps) = RE_COSPLAY_BY.captures(&title) {
return Some(caps[3].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(&title) {
return Some(caps[1].trim().to_string());
if let Some(caps) = RE_COSPLAY.captures(&t) {
return Some(clean_name(caps[1].trim()));
}
// 2. 中文摄影工作室格式
let title = RE_BRACKET_PREFIX.replace(&title, "").trim().to_string();
// 步骤3: 移除 [工作室] 前缀
t = RE_BRACKET_PREFIX.replace(&t, "").trim().to_string();
// 移除模特称呼前缀
let mut title = title;
// 步骤4: 移除模特称呼前缀
for prefix in MODEL_TITLE_PREFIX {
if title.starts_with(prefix) {
title = title[prefix.len()..].trim().to_string();
if t.starts_with(prefix) {
t = t[prefix.len()..].trim().to_string();
break;
}
}
// 移除编号后缀
let title = RE_NUMBER_SUFFIX.replace(&title, "").trim().to_string();
// 步骤5: 移除编号后缀
t = RE_NUMBER_SUFFIX.replace(&t, "").trim().to_string();
// 用 `–—\-` 分割,取第一部分作为人名
let parts: Vec<&str> = RE_SPLIT_DASH.split(&title).collect();
let name = parts[0].trim().to_string();
// 步骤6: 用分隔符切分,检查每一部分
let parts: Vec<&str> = RE_SPLIT_DASH.split(&t).collect();
if name.len() >= 2 {
Some(name)
} else {
None
// 策略A: 检查第一部分(分隔符前的内容最可能包含人名)
if let Some(first) = parts.first() {
if let Some(name) = extract_name_from_segment(first) {
if !is_excluded_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_excluded_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_excluded_name(&name) {
return Some(name);
}
}
None
}
/// 从一个文本片段中提取人名
fn extract_name_from_segment(segment: &str) -> Option<String> {
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
}
/// 提取中文核心(开头的连续中文字符)
@@ -204,6 +279,57 @@ pub fn get_person_key(name: &str) -> String {
.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)
}
// ── 标签提取 ──
/// 从标题中提取标签(排除人名部分)
@@ -282,4 +408,28 @@ mod tests {
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");
}
}