update
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user