tests/test_javdb_cn_names.py
Rama master · réplica de lectura del repositorio original.
"""javdb 资料页取中文名:现名与旧名必须分开,中文写法只认一个。
配错一位就是把另一个人的名字写进真相字段。这里守的是「什么才算证据」。
"""
import importlib.util
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path
from urllib.parse import quote
from peach import javdb
from peach.entities import normalize_entity_name
REPO = Path(__file__).resolve().parents[1]
#: 资料页顶部的真实结构(2026-09-04 实测)。现名一栏中日并列,紧随的是旧艺名。
PAGE = """
<div class="column section-title">
<h2 class="title is-4">
<span class="actor-section-name">{current}</span>
<br />
<span class="section-meta">{former}</span>
<br>
<span class="section-meta">324 部影片</span>
</h2>
</div>
<a id="button-collect-actor" href="/actors/{actor}/collect"></a>
"""
def page(current: str, former: str = "", actor: str = "RJM8") -> str:
return PAGE.format(current=current, former=former, actor=actor)
def load_script(name: str):
"""按文件路径加载脚本。
要先登记进 `sys.modules` 再执行:`localize_performer_names` 里有 dataclass,
而 `dataclasses` 会去 `sys.modules[cls.__module__]` 取模块字典,没登记就是
`'NoneType' object has no attribute '__dict__'`——报错指向 dataclass,
真正的原因在这一行。
"""
sys.path.insert(0, str(REPO / "src"))
spec = importlib.util.spec_from_file_location(name, REPO / "scripts" / f"{name}.py")
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
def load_module():
return load_script("harvest_javdb_cn_names")
class ParseTests(unittest.TestCase):
def test_the_current_name_and_the_former_names_are_two_different_lists(self):
"""`JULIA` 那页的旧名里有 `京香じゅりあ`。混成一串就分不出这是不是她的中文名。"""
html = page("JULIA", "京香じゅりあ")
self.assertEqual(javdb.current_names(html), ["JULIA"])
self.assertEqual(javdb.former_names(html), ["京香じゅりあ"])
def test_the_film_count_is_not_a_name(self):
self.assertNotIn("324 部影片", javdb.former_names(page("深田詠美, 深田えいみ")))
def test_both_writings_of_the_current_name_come_back_in_order(self):
self.assertEqual(javdb.current_names(page("深田詠美, 深田えいみ")),
["深田詠美", "深田えいみ"])
def test_the_record_kind_suffix_is_not_part_of_the_name(self):
"""无码那条记录的现名挂着 `(無碼)`。留着会让同一个人变成两个人。"""
self.assertEqual(javdb.current_names(page("木下ひまり(無碼)")), ["木下ひまり"])
def test_the_actor_id_is_the_page_identity(self):
self.assertEqual(javdb.actor_id(page("深田詠美, 深田えいみ", actor="Ng03")), "Ng03")
def test_a_search_result_that_names_nobody_we_want_is_not_a_hit(self):
html = ('<div class="box actor-box"><a href="/actors/AAA" title="別人, べつじん">'
'</a></div><div class="box actor-box">'
'<a href="/actors/BBB" title="深田詠美, 深田えいみ"></a></div>')
self.assertEqual(javdb.search_hits(html, {"深田えいみ"}), ["/actors/BBB"])
def test_a_wider_key_can_be_supplied_for_matching(self):
"""账本写 `宫`、站上写 `宮`,字形差别不该让人搜不到自己。"""
html = ('<div class="box actor-box"><a href="/actors/AAA" title="宮本櫻, 宮本さくら">'
'</a></div>')
self.assertEqual(javdb.search_hits(html, {"宫本さくら"}), [])
self.assertEqual(
javdb.search_hits(html, {"宫本さくら"}, lambda name: name.replace("宮", "宫")),
["/actors/AAA"])
def test_two_pages_for_one_name_both_come_back(self):
"""同一个名字在站上真有两位时,取第一个是默默替用户挑了一位。"""
html = ('<div class="box actor-box"><a href="/actors/AAA" title="あおい"></a></div>'
'<div class="box actor-box"><a href="/actors/BBB" title="あおい"></a></div>')
self.assertEqual(javdb.search_hits(html, {"あおい"}), ["/actors/AAA", "/actors/BBB"])
class JudgeTests(unittest.TestCase):
def setUp(self):
self.module = load_module()
self.record = {"entity_id": 1, "name": "深田えいみ", "assets": 6,
"chain": ["深田えいみ", "天海こころ"]}
def judge(self, current, former=""):
return self.module.judge(self.record, page(current, former), "https://javdb.com/x")
def test_a_name_written_both_ways_on_one_page_is_the_evidence(self):
row = self.judge("深田詠美, 深田えいみ")
self.assertEqual(row["verdict"], "ok")
self.assertEqual(row["zh_tw"], "深田詠美")
self.assertEqual(row["jp"], "深田えいみ")
def test_a_ledger_name_found_only_among_former_names_is_left_to_a_human(self):
"""站上已经改了名。用哪个当规范名是人要决定的事,不是这一步该定的。"""
row = self.judge("JULIA", "深田えいみ")
self.assertEqual(row["verdict"], "旧名")
self.assertIn("JULIA", row["evidence"])
def test_a_page_with_only_a_japanese_name_yields_nothing(self):
"""`長谷川るい` 那页只有日文写法。没有就是没有,不转写。"""
self.record.update(name="長谷川るい", chain=["長谷川るい"])
row = self.judge("長谷川るい")
self.assertEqual(row["verdict"], "同形(站上只有日文名)")
self.assertEqual(row["zh_tw"], "")
def test_two_chinese_writings_in_one_field_are_ambiguous(self):
self.record.update(name="田中レモン", chain=["田中レモン", "楓カレン"])
row = self.judge("田中檸檬, 田中レモン, 楓花戀")
self.assertEqual(row["verdict"], "多义")
def test_two_different_stage_names_in_the_current_field_are_not_a_pair(self):
"""`一之瀨亞美莉, 美空あやか` 是两个艺名。姓对不上,中文那个不是这位的名字。"""
self.record.update(name="美空あやか", chain=["美空あやか"])
row = self.judge("一之瀨亞美莉, 美空あやか", "一ノ瀬アメリ")
self.assertEqual(row["verdict"], "不同名(两栏姓氏对不上)")
self.assertEqual(row["zh_cn" if "zh_cn" in row else "zh_tw"], "")
def test_a_shared_single_character_is_not_a_shared_surname(self):
"""`美空` 与 `亞美莉` 都有 `美`。按字取交集会把配错的那一对放过去。"""
self.assertFalse(self.module.same_person("一之瀨亞美莉", "美空あやか"))
self.assertTrue(self.module.same_person("深田詠美", "深田えいみ"))
def test_a_kana_only_japanese_name_has_no_surname_to_compare(self):
"""`あべみかこ` 没有汉字可比。无从判不等于判否。"""
self.assertTrue(self.module.same_person("安部未華子", "あべみかこ"))
def test_a_glyph_difference_is_not_a_name_difference(self):
"""`宫本さくら` 与 `宮本さくら` 是一个人,`宮` 与 `宫` 只是字形。"""
self.record.update(name="宫本さくら", chain=["宫本さくら"])
self.assertEqual(self.judge("宮本櫻, 宮本さくら")["verdict"], "ok")
def test_a_page_matched_only_through_an_alias_is_left_to_a_human(self):
"""账本规范名不在现名栏:站上写的是另一个艺名,换不换规范名是人的决定。"""
self.record.update(name="おっぱい隊長", chain=["おっぱい隊長", "夢見るぅ"])
row = self.judge("夢見露, 夢見るぅ")
self.assertEqual(row["verdict"], "改艺名(现名栏是另一个艺名)")
self.assertIn("おっぱい隊長", row["evidence"])
def test_a_latin_stage_name_is_not_a_chinese_name(self):
self.record["name"] = "京香じゅりあ"
self.record["chain"] = ["京香じゅりあ", "JULIA"]
row = self.judge("JULIA, 京香じゅりあ")
self.assertEqual(row["verdict"], "同形(站上只有日文名)")
class SimplifyTests(unittest.TestCase):
def setUp(self):
self.module = load_module()
self.record = {"entity_id": 1, "name": "深田えいみ", "assets": 6,
"chain": ["深田えいみ"]}
def test_the_traditional_writing_is_kept_next_to_the_simplified_one(self):
"""复核件要看得出转了什么:只留简体的话,转错了没人能发现。"""
rows = self.module.localize(
[{"verdict": "ok", "zh_tw": "永瀨未萌", "adopt": "永瀨未萌"},
{"verdict": "旧名", "zh_tw": "", "adopt": "JULIA"}])
self.assertEqual(rows[0]["zh_cn"], "永濑未萌")
self.assertEqual(rows[0]["zh_tw"], "永瀨未萌")
self.assertEqual(rows[1]["zh_cn"], "")
def test_a_japanese_current_name_is_not_half_converted(self):
"""`きみかわ結衣` 转成 `きみかわ结衣` 是半假名半简体,比原样留着更糟。"""
rows = self.module.localize([{"verdict": "旧名", "zh_tw": "", "adopt": "きみかわ結衣"},
{"verdict": "旧名", "zh_tw": "", "adopt": "美月麗花"}])
self.assertEqual(rows[0]["adopt"], "きみかわ結衣")
self.assertEqual(rows[1]["adopt"], "美月丽花")
def test_a_site_renamed_row_carries_the_new_name_as_the_candidate(self):
"""改了名的那些也要有候选,不然复核件里只写着「站上不是这个名字了」。"""
self.record.update(name="京香じゅりあ", chain=["京香じゅりあ", "JULIA"])
row = self.module.judge(self.record, page("JULIA", "京香じゅりあ"), "https://javdb.com/x")
self.assertEqual(row["verdict"], "改艺名(现名栏是另一个艺名)")
self.assertEqual(row["adopt"], "JULIA")
self.assertEqual(row["jp"], "京香じゅりあ")
class TargetTests(unittest.TestCase):
def setUp(self):
self.module = load_module()
self.tmp = tempfile.TemporaryDirectory()
self.con = sqlite3.connect(Path(self.tmp.name).resolve() / "ledger.db")
self.con.executescript(
"CREATE TABLE entity(id INTEGER PRIMARY KEY, kind TEXT, canonical_name TEXT);"
"CREATE TABLE entity_alias(entity_id INTEGER, alias TEXT);"
"CREATE TABLE asset_entity(asset_id INTEGER, entity_id INTEGER, source TEXT);")
self.con.executemany(
"INSERT INTO entity(id,kind,canonical_name) VALUES(?,'performer',?)",
[(1, "深田えいみ"), (2, "三上悠亚"), (3, "MattieDoll"), (4, "河合あすな")])
self.con.executemany(
"INSERT INTO asset_entity(asset_id,entity_id,source) VALUES(?,?,?)",
[(10, 1, "r18:performer"), (11, 2, "r18:performer"),
(12, 3, "legacy:asset"), (13, 4, "javbus:performer")])
self.con.execute("INSERT INTO entity_alias VALUES(1,'天海こころ')")
self.con.commit()
def tearDown(self):
self.con.close()
self.tmp.cleanup()
def test_only_kana_names_backed_by_release_metadata_are_targets(self):
"""已经是中文的不用查;账号型 performer 不是女优,不翻译。"""
found = {row["name"] for row in self.module.targets(self.con)}
self.assertEqual(found, {"深田えいみ", "河合あすな"})
def test_the_aliases_ride_along_as_search_terms(self):
"""按规范名搜不到时,旧名往往搜得到——名字链是入口,不是装饰。"""
chain = next(row["chain"] for row in self.module.targets(self.con)
if row["name"] == "深田えいみ")
self.assertEqual(chain, ["深田えいみ", "天海こころ"])
def test_a_chinese_name_is_a_target_when_the_scope_is_all(self):
"""补别名要找的恰恰是已经有中文规范名的人:她们的第二个写法一个都没进过账本。"""
found = {row["name"] for row in self.module.targets(self.con, "all")}
self.assertEqual(found, {"深田えいみ", "三上悠亚", "河合あすな"})
def test_a_named_performer_is_looked_up_whatever_the_scope_says(self):
"""起点常常是「我看着这一位,觉得少个名字」。被范围挡下的正是最该查的一位。"""
for named in ("三上悠亚", "2", "天海こころ"):
found = [row["name"] for row in self.module.targets(self.con, "kana", [named])]
self.assertEqual(len(found), 1, named)
def test_naming_nobody_leaves_the_scope_in_charge(self):
self.assertEqual({row["name"] for row in self.module.targets(self.con, "kana", [])},
{"深田えいみ", "河合あすな"})
def test_a_performer_without_release_metadata_is_out_of_scope_either_way(self):
"""账号型 performer 不是女优,放开范围也不查。"""
for scope in ("kana", "all"):
names = {row["name"] for row in self.module.targets(self.con, scope)}
self.assertNotIn("MattieDoll", names)
class AliasCandidateTests(unittest.TestCase):
"""页面上账本还没有的中文写法。
`桥本有菜` 这种名字在账本里一个字都没有:刮削源给日文与罗马字,译名那条路每人只
给一个中文名,改统称时降为别名的又是日文原规范名。旧艺名的中译正是从这一栏来的。
"""
def setUp(self):
self.module = load_module()
self.record = {"entity_id": 1, "name": "新有菜", "assets": 15,
"chain": ["新有菜", "新ありな"]}
def candidates(self, current, former="", owners=None):
return self.module.alias_candidates(
self.record, page(current, former), "https://javdb.com/actors/RJM8",
owners or {})
def test_a_chinese_writing_in_the_former_name_field_is_a_candidate(self):
rows = self.candidates("新有菜, 新ありな", "橋本有菜, 橋本ありな")
self.assertEqual([(row["alias"], row["origin"], row["verdict"]) for row in rows],
[("桥本有菜", "别名栏", "ok")])
def test_a_nickname_in_that_field_is_offered_like_any_other_writing(self):
"""站上不标这一栏放的是什么,`傻梦` 是爱称。认不认得归复核的人,解析层不猜。"""
self.record.update(entity_id=7746, name="凉森玲梦", chain=["凉森玲梦", "涼森れむ"])
rows = self.candidates("涼森玲夢, 涼森れむ", "傻梦")
self.assertEqual([(row["alias"], row["origin"]) for row in rows], [("傻梦", "别名栏")])
def test_the_writing_on_the_page_is_kept_next_to_the_converted_one(self):
"""复核件要看得出转了什么,也要指得回是哪一页。"""
row = self.candidates("新有菜, 新ありな", "橋本有菜")[0]
self.assertEqual(row["alias_zh_tw"], "橋本有菜")
self.assertEqual(row["actor_id"], "RJM8")
def test_a_writing_the_ledger_already_has_is_not_a_candidate(self):
"""查过、没查出东西,和还没轮到她是两件事。"""
self.assertEqual([row["verdict"] for row in self.candidates("新有菜, 新ありな")],
["无新写法"])
def test_the_same_name_in_traditional_characters_is_not_a_new_writing(self):
self.record["chain"] = ["新有菜", "桥本有菜"]
self.assertEqual(
[row["verdict"] for row in self.candidates("新有菜, 新ありな", "橋本有菜")],
["无新写法"])
def test_a_japanese_or_latin_writing_is_not_a_chinese_alias(self):
"""`JULIA` 是艺名本体,`きみかわ結衣` 掺着假名。两个都不是中文写法。"""
self.assertEqual(
[row["verdict"] for row in self.candidates("JULIA, 京香じゅりあ", "きみかわ結衣")],
["无新写法"])
def test_one_writing_listed_in_both_fields_is_offered_once(self):
rows = self.candidates("新有菜, 橋本有菜", "橋本有菜")
self.assertEqual([(row["alias"], row["origin"]) for row in rows],
[("桥本有菜", "现名栏")])
def test_a_writing_that_belongs_to_someone_else_is_flagged_not_offered(self):
"""要么两条该合并、要么真有两位重名,都得人判。"""
owners = {self.module.key("橋本有菜"): (99, "桥本有菜")}
row = self.candidates("新有菜, 新ありな", "橋本有菜", owners)[0]
self.assertEqual(row["verdict"], "占用(另一条实体已有这个名字)")
self.assertIn("99", row["evidence"])
def box(path: str, title: str) -> str:
return f'<div class="box actor-box"><a href="{path}" title="{title}"></a></div>'
class FakeSite:
"""按地址发页的假站。没登记的地址抛错:取页失败要落进 CSV,不是静默的空页。"""
def __init__(self, pages: dict):
self.pages = dict(pages)
self.asked: list[str] = []
def get(self, url: str) -> str:
self.asked.append(url)
if url not in self.pages:
raise LookupError(f"没有这一页 {url}")
return self.pages[url]
class HarvestTests(unittest.TestCase):
"""一趟走完两份产物。分两个脚本就要按 5 秒一页把同一批资料页再走一遍。"""
def setUp(self):
self.module = load_module()
self.tmp = tempfile.TemporaryDirectory()
self.con = sqlite3.connect(Path(self.tmp.name).resolve() / "ledger.db")
self.con.executescript(
"CREATE TABLE entity(id INTEGER PRIMARY KEY, kind TEXT, canonical_name TEXT);"
"CREATE TABLE entity_alias(entity_id INTEGER, alias TEXT);"
"CREATE TABLE asset_entity(asset_id INTEGER, entity_id INTEGER, source TEXT);")
self.con.executemany(
"INSERT INTO entity(id,kind,canonical_name) VALUES(?,'performer',?)",
[(1, "深田えいみ"), (2, "三上悠亚")])
self.con.executemany(
"INSERT INTO asset_entity(asset_id,entity_id,source) VALUES(?,?,'r18:performer')",
[(10, 1), (11, 2)])
self.con.execute("INSERT INTO entity_alias VALUES(1,'天海こころ')")
self.con.commit()
def tearDown(self):
self.con.close()
self.tmp.cleanup()
@staticmethod
def search(name: str) -> str:
return javdb.SEARCH.format(quote(name))
def test_one_page_feeds_both_sheets(self):
site = FakeSite({
self.search("深田えいみ"): box("/actors/BBB", "深田詠美, 深田えいみ"),
"https://javdb.com/actors/BBB": page("深田詠美, 深田えいみ", "天海こころ")})
names, aliases = self.module.harvest(self.con, site, 0)
self.assertEqual([row["verdict"] for row in names], ["ok"])
self.assertEqual([(row["alias"], row["origin"]) for row in aliases],
[("深田咏美", "现名栏")])
def test_two_pages_for_one_name_yield_no_alias_candidate(self):
"""别名进的是身份。配错了人,比缺一个写法更难查回来。"""
site = FakeSite({
self.search("深田えいみ"): box("/actors/BBB", "深田詠美, 深田えいみ"),
"https://javdb.com/actors/BBB": page("深田詠美, 深田えいみ"),
self.search("三上悠亚"): (box("/actors/AAA", "三上悠亞, 三上悠亜")
+ box("/actors/CCC", "三上悠亞, 三上悠亜"))})
names, aliases = self.module.harvest(self.con, site, 0, "all")
crowded = [row for row in aliases if row["current_name"] == "三上悠亚"]
self.assertEqual([row["verdict"] for row in crowded],
["多页(同名两位,分不清是哪一页)"])
self.assertNotIn("https://javdb.com/actors/AAA", site.asked)
def test_the_name_sheet_keeps_to_the_performers_still_named_in_kana(self):
"""`--scope all` 放开的只是别名那一份。中文规范名不需要再定一次规范名。"""
site = FakeSite({
self.search("深田えいみ"): box("/actors/BBB", "深田詠美, 深田えいみ"),
"https://javdb.com/actors/BBB": page("深田詠美, 深田えいみ"),
self.search("三上悠亚"): box("/actors/AAA", "三上悠亞, 三上悠亜"),
"https://javdb.com/actors/AAA": page("三上悠亞, 三上悠亜")})
names, aliases = self.module.harvest(self.con, site, 0, "all")
self.assertEqual({row["current_name"] for row in names}, {"深田えいみ"})
self.assertEqual({row["current_name"] for row in aliases},
{"深田えいみ", "三上悠亚"})
def test_a_performer_the_site_does_not_have_is_recorded_in_both_sheets(self):
site = FakeSite({self.search("深田えいみ"): "", self.search("天海こころ"): ""})
names, aliases = self.module.harvest(self.con, site, 1)
self.assertEqual([row["verdict"] for row in names], ["未取得"])
self.assertEqual([row["verdict"] for row in aliases], ["未取得"])
def test_a_failed_fetch_lands_in_the_sheets_instead_of_stopping_the_batch(self):
site = FakeSite({})
names, aliases = self.module.harvest(self.con, site, 1)
self.assertEqual([row["verdict"] for row in names], ["取页失败"])
self.assertIn("LookupError", str(aliases[0]["evidence"]))
class ApplyAliasTests(unittest.TestCase):
"""候选写进 `entity_alias` 的那一步。四种不写,每种都要留下为什么。"""
def setUp(self):
self.module = load_script("apply_alias_candidates")
self.tmp = tempfile.TemporaryDirectory()
self.con = sqlite3.connect(Path(self.tmp.name).resolve() / "ledger.db")
self.con.executescript(
"CREATE TABLE entity(id INTEGER PRIMARY KEY, kind TEXT,"
" canonical_name TEXT, normalized_name TEXT);"
"CREATE TABLE entity_alias(entity_id INTEGER, alias TEXT,"
" normalized_alias TEXT, source TEXT, confidence REAL);")
for entity_id, name in ((1, "桥本有菜"), (2, "三上悠亚")):
self.con.execute(
"INSERT INTO entity VALUES(?,'performer',?,?)",
(entity_id, name, normalize_entity_name(name)))
self.con.commit()
def tearDown(self):
self.con.close()
self.tmp.cleanup()
def candidate(self, **over):
row = {"entity_id": 1, "current_name": "桥本有菜", "alias": "新有菜",
"origin": "别名栏", "verdict": "ok", "url": "https://javdb.com/actors/RJM8"}
row.update(over)
return row
def plan(self, **over):
return self.module.plan(self.con, [self.candidate(**over)])[0]
def aliases_of(self, entity_id: int):
return [(row[0], row[1]) for row in self.con.execute(
"SELECT alias,source FROM entity_alias WHERE entity_id=?", (entity_id,))]
def test_a_new_writing_is_written_with_the_page_as_its_source(self):
rows = self.module.plan(self.con, [self.candidate()])
self.assertEqual(rows[0]["action"], "写入")
self.assertEqual(self.module.apply_rows(self.con, rows, "javdb-20260912"), 1)
self.assertEqual(self.aliases_of(1),
[("新有菜", f"{javdb.ALIAS_SOURCE}@javdb-20260912")])
def test_the_batch_is_named_in_the_source_so_it_can_be_taken_back_whole(self):
"""解析判错时,认得出批次才能按 `source` 把那一趟整批撤回。"""
self.module.apply_rows(self.con, self.module.plan(self.con, [self.candidate()]),
"javdb-20260912")
self.module.apply_rows(self.con, self.module.plan(
self.con, [self.candidate(alias="岩谷志季")]), "javdb-20261001")
gone = self.con.execute("DELETE FROM entity_alias WHERE source=?",
(self.module.source_for("javdb-20261001"),)).rowcount
self.assertEqual(gone, 1)
self.assertEqual([alias for alias, _ in self.aliases_of(1)], ["新有菜"])
def test_a_writing_the_entity_already_has_is_left_alone(self):
self.con.execute("INSERT INTO entity_alias VALUES(1,'新有菜',?,'javdb-actor-page',1.0)",
(normalize_entity_name("新有菜"),))
self.assertEqual(self.plan()["action"], "已有")
def test_the_canonical_name_is_not_added_to_its_own_alias_list(self):
self.assertEqual(self.plan(alias="桥本有菜")["action"], "已有")
def test_a_writing_that_belongs_to_another_performer_is_refused(self):
"""静默写下去只会给后面的合并判定多送一个假信号。"""
self.con.execute("INSERT INTO entity_alias VALUES(2,'新有菜',?,'javdb-actor-page',1.0)",
(normalize_entity_name("新有菜"),))
row = self.plan()
self.assertEqual(row["action"], "占用")
self.assertIn("三上悠亚", row["detail"])
def test_a_row_whose_entity_was_renamed_after_the_harvest_is_not_written(self):
"""CSV 是某一刻的快照。统称变过说明这个人后来被动过,该重抓一遍再看。"""
row = self.plan(current_name="新ありな")
self.assertEqual(row["action"], "已变")
self.assertIn("桥本有菜", row["detail"])
def test_a_row_pointing_at_nobody_is_not_written(self):
self.assertEqual(self.plan(entity_id=404)["action"], "查无此人")
def test_a_writing_from_another_site_records_that_site_as_its_source(self):
rows = self.module.plan(self.con, [self.candidate()])
self.module.apply_rows(self.con, rows, "namewikis-20260924", base="av_name")
self.assertEqual(self.aliases_of(1), [("新有菜", "av_name@namewikis-20260924")])
def test_a_writing_judged_wrong_is_taken_off_whatever_wrote_it(self):
"""javdb 与 Wiki 的別名栏都混有共演男优名,人看过判「删除」就摘掉,来源留在复核 CSV。"""
self.con.execute("INSERT INTO entity_alias VALUES(1,'島修也',?,'javdb-actor-page@x',1.0)",
(normalize_entity_name("島修也"),))
rows = self.module.plan(self.con, [self.candidate(alias="島修也", verdict="删除")])
self.assertEqual(rows[0]["action"], "删除")
self.assertIn("javdb-actor-page@x", rows[0]["detail"])
self.assertEqual(self.module.apply_rows(self.con, rows, "r"), 0)
self.assertEqual(self.module.remove_rows(self.con, rows), 1)
self.assertEqual(self.aliases_of(1), [])
def test_removing_a_writing_the_entity_does_not_have_changes_nothing(self):
row = self.plan(alias="島修也", verdict="删除")
self.assertEqual(row["action"], "名下没有")
self.assertEqual(self.module.remove_rows(self.con, [row]), 0)
def test_only_the_verdicts_named_on_the_command_line_are_taken(self):
rows = [self.candidate(), self.candidate(verdict="占用(另一条实体已有这个名字)"),
self.candidate(alias="")]
self.assertEqual(len(self.module.accepted(rows, ["ok"])), 1)
self.assertEqual(len(self.module.accepted(rows, ["ok", "占用(另一条实体已有这个名字)"])), 2)
class MappingCsvTests(unittest.TestCase):
def setUp(self):
self.module = load_script("localize_performer_names")
self.tmp = tempfile.TemporaryDirectory()
self.path = Path(self.tmp.name).resolve() / "mapping.csv"
def tearDown(self):
self.tmp.cleanup()
def write(self, *rows):
from peach.review_csv import write_rows
write_rows(self.path, load_module().FIELDS, list(rows), fill_missing=True)
def test_only_the_rows_judged_ok_become_mappings(self):
self.write({"current_name": "深田えいみ", "adopt": "深田咏美", "verdict": "ok",
"actor_id": "A1"},
{"current_name": "長谷川るい", "adopt": "", "verdict": "同形(站上只有日文名)"})
mappings = self.module.read_mapping_csv(self.path)
self.assertEqual([m.jp for m in mappings], ["深田えいみ"])
def test_a_renamed_row_is_only_taken_when_it_is_named_on_the_command_line(self):
"""站上改了名要不要跟着改是人的决定,不能因为查到了就默认采用。"""
self.write({"current_name": "京香じゅりあ", "adopt": "JULIA", "verdict": "改艺名"})
self.assertEqual(self.module.read_mapping_csv(self.path), [])
taken = self.module.read_mapping_csv(self.path, ["ok", "改艺名"])
self.assertEqual([(m.jp, m.zh_cn) for m in taken], [("京香じゅりあ", "JULIA")])
def test_one_performer_matched_on_several_pages_yields_one_mapping(self):
"""`アンナ` 命中了三页。三条映射进去,落库时只会变成一次「没有唯一映射」。"""
self.write({"current_name": "アンナ", "adopt": "Anna", "verdict": "改艺名"},
{"current_name": "アンナ", "adopt": "安娜", "verdict": "ok"},
{"current_name": "アンナ", "adopt": "ANNA", "verdict": "改艺名"})
taken = self.module.read_mapping_csv(self.path, ["ok", "改艺名"])
self.assertEqual([(m.jp, m.zh_cn) for m in taken], [("アンナ", "安娜")])
def test_a_latin_current_name_is_the_stage_name_not_a_romanization(self):
"""XML 那份偶尔把罗马字填进 `zh_cn`;javdb 的现名栏写 `JULIA` 时那就是艺名。"""
self.write({"current_name": "京香じゅりあ", "adopt": "JULIA", "verdict": "改艺名"})
taken = self.module.read_mapping_csv(self.path, ["改艺名"])
self.assertTrue(taken[0].latin_is_a_stage_name)
def test_a_row_with_nothing_to_adopt_is_not_a_mapping(self):
"""判定收了但这一行没给出可用的名字,落库时就是把规范名改成空。"""
self.write({"current_name": "長谷川るい", "adopt": "", "verdict": "ok"})
self.assertEqual(self.module.read_mapping_csv(self.path), [])
def test_the_page_id_becomes_the_provenance_key(self):
"""回溯「这个中文名是谁写的」时,要指得回具体那一页。"""
self.write({"current_name": "深田えいみ", "adopt": "深田咏美", "verdict": "ok",
"actor_id": "A1"})
self.assertEqual(self.module.read_mapping_csv(self.path)[0].key, "javdb:A1")
def test_the_index_points_back_into_the_returned_list(self):
"""`collect` 拿 `index` 当下标回查。跳过的行也算数就会指到别人身上。"""
self.write({"current_name": "跳过", "adopt": "", "verdict": "同形(站上只有日文名)"},
{"current_name": "深田えいみ", "adopt": "深田咏美", "verdict": "ok",
"actor_id": "A1"})
mappings = self.module.read_mapping_csv(self.path)
self.assertEqual(mappings[mappings[0].index].jp, "深田えいみ")
def test_a_japanese_glyph_left_by_the_traditional_conversion_is_normalized(self):
"""opencc 转的是繁简,不管日本字形:`滝` 中文写 `泷`。字形不需要外部证据。"""
self.write({"current_name": "滝田あゆ", "adopt": "滝田亚由", "verdict": "ok",
"actor_id": "A2"})
self.assertEqual(self.module.read_mapping_csv(self.path)[0].zh_cn, "泷田亚由")
if __name__ == "__main__":
unittest.main()