Commit e2fbce20 e2fbce20b8c8dc21a07a4a10ed8771b06680339c by 沈秋雨

feat(etl): 新增 spider DB 歌手校验过滤无效录音

- 初始化阶段即校验 spider DB 歌手关联,过滤掉歌手数据缺失的录音
- 根据平台分别查询 QQ、酷狗和网易的有效歌手 key,过滤无效录音数据
- 统计并记录过滤掉的无效录音条数,日志输出详细过滤信息
- 文档更新新增 spider DB 歌手校验及相关数据统计说明
- 单元测试新增过滤无效歌手的情况,保证功能正确性
1 parent 5217dcac
1 # 数据统计与流转说明 1 # 数据统计与流转说明
2 2
3 > 统计时间:2026-07-13。records2 已按“不过滤 `pub_time`”的新规则重新初始化 3 > 统计时间:2026-07-14。records2 已按"不过滤 `pub_time`"的新规则重新初始化,并新增 spider DB 歌手校验
4 4
5 ## 1. 主词曲录音关联表当前情况 5 ## 1. 主词曲录音关联表当前情况
6 6
...@@ -26,17 +26,20 @@ records1 当前共 **86,186** 条关联,且 **song_id** 也是 **86,186** 个 ...@@ -26,17 +26,20 @@ records1 当前共 **86,186** 条关联,且 **song_id** 也是 **86,186** 个
26 | 歌手非空后 | 208,368 | `hk_music_record.singer_name` | 26 | 歌手非空后 | 208,368 | `hk_music_record.singer_name` |
27 | 歌曲链接非空后 | 206,201 | `hk_music_record.platform_index_url` | 27 | 歌曲链接非空后 | 206,201 | `hk_music_record.platform_index_url` |
28 | 音频链接非空后 | 206,201 | `storage_url``platform_play_url`,本次未减少 | 28 | 音频链接非空后 | 206,201 | `storage_url``platform_play_url`,本次未减少 |
29 | 删除 records1 已有关联 | 120,949 | 删除 **85,252** 条相同 `(song_id, record_id)` 关联,输出最终 records2 | 29 | spider DB 歌手校验后 | 203,987 | 过滤 **2,214** 条歌手关联缺失的录音(QQ 为主) |
30 | 删除 records1 已有关联 | 118,848 | 删除 **85,139** 条相同 `(song_id, record_id)` 关联,输出最终 records2 |
30 31
31 # 数据规模汇总 32 # 数据规模汇总
32 33
33 | 数据集 | 数量 | 说明 | 34 | 数据集 | 数量 | 说明 |
34 | -------------------- | ----------: | ----------------------------- | 35 | -------------------- | ----------: | ----------------------------- |
35 | records1 全量 | 86,186 | 每首歌一条主录音关联 | 36 | records1 全量 | 86,186 | 每首歌一条主录音关联 |
36 | records2 全量 | 120,949 | 补充录音关联 | 37 | records2 全量(歌手校验后)| 118,848 | 补充录音关联(过滤 spider 歌手缺失)|
37 | 重复关联 | 0 | `(song_id, record_id)` 无重复 | 38 | 重复关联 | 0 | `(song_id, record_id)` 无重复 |
38 | **最终去重关联总数** | **207,135** | records1 + records2 | 39 | **最终去重关联总数** | **205,034** | records1 86,186 + records2 118,848 |
39 40
40 1. 已去除 records1 封面限制,已去除 records2 封面、歌词和 `pub_time` 限制,优先保留音频。records2 后续导入行为与 records1 保持一致,不对空 `pub_time` 做特殊转换。 41 1. 已去除 records1 封面限制,已去除 records2 封面、歌词和 `pub_time` 限制,优先保留音频。records2 后续导入行为与 records1 保持一致,不对空 `pub_time` 做特殊转换。
41 42
42 2. 后续需给出各个环节数据缺失原因,判断是否可以人工补充。 43 2. spider DB 歌手校验:过滤 `media_tencent_singer_has_songs``singer_id = 0` 或无关联记录的录音。QQ 平台缺失最多(2,000 条),主要因为翻唱/二创版本的录音在爬虫库中歌手关联未正确建立。
44
45 3. 后续需给出各个环节数据缺失原因,判断是否可以人工补充。
......
...@@ -1409,11 +1409,54 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None ...@@ -1409,11 +1409,54 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None
1409 log.info("Initialized yinyan_song_records candidates=%d, skipped_batches=%d", total, skipped_batches) 1409 log.info("Initialized yinyan_song_records candidates=%d, skipped_batches=%d", total, skipped_batches)
1410 1410
1411 1411
1412 def _row_singer_key(row: dict) -> str | int:
1413 """返回一条 relation 用于匹配歌手的 key:QQ 用 mid(str),其他平台用 int id。"""
1414 if row['platform'] == PLATFORM_QQ:
1415 return row.get('platform_unique_key', '')
1416 return int(row.get('platform_unique_key', 0))
1417
1418
1419 def _fetch_valid_singer_keys(spider_conn, rows: list[dict]) -> set:
1420 """查询 spider DB,返回有有效歌手关联的 platform_unique_key 集合。"""
1421 qq_mids = [r.get('platform_unique_key') for r in rows if r['platform'] == PLATFORM_QQ and r.get('platform_unique_key')]
1422 kugou_ids = [int(r['platform_unique_key']) for r in rows if r['platform'] == PLATFORM_KUGOU and r.get('platform_unique_key')]
1423 netease_ids = [int(r['platform_unique_key']) for r in rows if r['platform'] == PLATFORM_NETEASE and r.get('platform_unique_key')]
1424
1425 valid: set = set()
1426
1427 if qq_mids:
1428 songs = fetch_qq_songs(spider_conn, qq_mids)
1429 db_ids = [v['id'] for v in songs.values()]
1430 singers = fetch_qq_singers(spider_conn, db_ids) if db_ids else {}
1431 # singers 以 spider DB song_id 为 key,反查回 mid
1432 id_to_mid = {v['id']: mid for mid, v in songs.items()}
1433 for song_id in singers:
1434 mid = id_to_mid.get(song_id)
1435 if mid:
1436 valid.add(mid)
1437
1438 if kugou_ids:
1439 songs = fetch_kugou_songs(spider_conn, kugou_ids)
1440 singers = fetch_kugou_singers(spider_conn, list(songs.keys()))
1441 valid.update(int(k) for k in singers)
1442
1443 if netease_ids:
1444 songs = fetch_netease_songs(spider_conn, netease_ids)
1445 singers = fetch_netease_singers(spider_conn, list(songs.keys()))
1446 valid.update(int(k) for k in singers)
1447
1448 return valid
1449
1450
1412 def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | None = None) -> None: 1451 def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | None = None) -> None:
1413 """以 records1 全量 song_id 为范围,写入补充录音关联并去重。""" 1452 """以 records1 全量 song_id 为范围,写入补充录音关联并去重。
1453 初始化阶段即校验 spider DB 歌手关联,过滤掉歌手数据缺失的录音。
1454 """
1414 src_conn = get_source_conn() 1455 src_conn = get_source_conn()
1456 spider_conn = get_spider_conn()
1415 pg_conn = get_pg_conn() 1457 pg_conn = get_pg_conn()
1416 total_inserted = 0 1458 total_inserted = 0
1459 total_filtered_no_singer = 0
1417 1460
1418 try: 1461 try:
1419 with pg_conn.cursor() as pg_cur: 1462 with pg_conn.cursor() as pg_cur:
...@@ -1423,6 +1466,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non ...@@ -1423,6 +1466,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non
1423 for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')): 1466 for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')):
1424 # 长任务连接可能断开,每批次开始前刷新 1467 # 长任务连接可能断开,每批次开始前刷新
1425 src_conn = refresh_conn(src_conn, 'source') 1468 src_conn = refresh_conn(src_conn, 'source')
1469 spider_conn = refresh_conn(spider_conn, 'spider')
1426 pg_conn = refresh_conn(pg_conn, 'pg') 1470 pg_conn = refresh_conn(pg_conn, 'pg')
1427 1471
1428 if max_batches is not None and index >= max_batches: 1472 if max_batches is not None and index >= max_batches:
...@@ -1435,6 +1479,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non ...@@ -1435,6 +1479,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non
1435 'song_id': int(relation['source_song_id']), 1479 'song_id': int(relation['source_song_id']),
1436 'record_id': int(relation['record_id']), 1480 'record_id': int(relation['record_id']),
1437 'platform': str(relation['platform']), 1481 'platform': str(relation['platform']),
1482 'platform_unique_key': relation.get('platform_unique_key', ''),
1438 } 1483 }
1439 for relation in relations 1484 for relation in relations
1440 if str(relation['platform']) in platforms 1485 if str(relation['platform']) in platforms
...@@ -1442,6 +1487,20 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non ...@@ -1442,6 +1487,20 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non
1442 if not rows: 1487 if not rows:
1443 continue 1488 continue
1444 1489
1490 # ── 校验 spider DB 歌手关联,过滤无效录音 ──
1491 valid_keys = _fetch_valid_singer_keys(spider_conn, rows)
1492 before = len(rows)
1493 rows = [
1494 r for r in rows
1495 if _row_singer_key(r) in valid_keys
1496 ]
1497 filtered = before - len(rows)
1498 total_filtered_no_singer += filtered
1499 if filtered:
1500 log.info("init-records2 batch %d: filtered %d rows without valid singers", index, filtered)
1501 if not rows:
1502 continue
1503
1445 with pg_conn.cursor() as pg_cur: 1504 with pg_conn.cursor() as pg_cur:
1446 insert_yinyan_song_records2(pg_cur, rows) 1505 insert_yinyan_song_records2(pg_cur, rows)
1447 pg_conn.commit() 1506 pg_conn.commit()
...@@ -1455,9 +1514,11 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non ...@@ -1455,9 +1514,11 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non
1455 close_all_pools() 1514 close_all_pools()
1456 1515
1457 log.info( 1516 log.info(
1458 "Initialized yinyan_song_records2 candidates=%d, removed_existing_records1_relations=%d", 1517 "Initialized yinyan_song_records2 candidates=%d, removed_existing_records1_relations=%d, "
1518 "filtered_no_singer=%d",
1459 total_inserted, 1519 total_inserted,
1460 deleted, 1520 deleted,
1521 total_filtered_no_singer,
1461 ) 1522 )
1462 1523
1463 1524
......
...@@ -463,13 +463,15 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( ...@@ -463,13 +463,15 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates(
463 dedupe_calls = [] 463 dedupe_calls = []
464 464
465 monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) 465 monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection())
466 monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection())
466 monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) 467 monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn)
467 monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) 468 monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn)
468 monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) 469 monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10])
469 monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [ 470 monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [
470 {'source_song_id': 10, 'record_id': 100, 'platform': '1'}, 471 {'source_song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid100'},
471 {'source_song_id': 10, 'record_id': 200, 'platform': '2'}, 472 {'source_song_id': 10, 'record_id': 200, 'platform': '2', 'platform_unique_key': '200'},
472 ]) 473 ])
474 monkeypatch.setattr(runner, '_fetch_valid_singer_keys', lambda conn, rows: {'mid100', 200})
473 monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows)) 475 monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows))
474 monkeypatch.setattr( 476 monkeypatch.setattr(
475 runner, 477 runner,
...@@ -480,13 +482,42 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( ...@@ -480,13 +482,42 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates(
480 runner.initialize_yinyan_song_records2(['1', '2']) 482 runner.initialize_yinyan_song_records2(['1', '2'])
481 483
482 assert inserted == [ 484 assert inserted == [
483 {'song_id': 10, 'record_id': 100, 'platform': '1'}, 485 {'song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid100'},
484 {'song_id': 10, 'record_id': 200, 'platform': '2'}, 486 {'song_id': 10, 'record_id': 200, 'platform': '2', 'platform_unique_key': '200'},
485 ] 487 ]
486 assert dedupe_calls == [True] 488 assert dedupe_calls == [True]
487 assert pg_conn.commits == 2 489 assert pg_conn.commits == 2
488 490
489 491
492 def test_initialize_yinyan_song_records2_filters_rows_without_valid_singers(monkeypatch):
493 pg_conn = _PgConnection()
494 inserted = []
495
496 monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection())
497 monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection())
498 monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn)
499 monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn)
500 monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10])
501 monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [
502 {'source_song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid_good'},
503 {'source_song_id': 10, 'record_id': 200, 'platform': '1', 'platform_unique_key': 'mid_bad'},
504 {'source_song_id': 10, 'record_id': 300, 'platform': '2', 'platform_unique_key': '300'},
505 ])
506 # mid_bad 没有歌手关联,300 也没有
507 monkeypatch.setattr(runner, '_fetch_valid_singer_keys', lambda conn, rows: {'mid_good'})
508 monkeypatch.setattr(runner, 'insert_yinyan_song_records2', lambda cur, rows: inserted.extend(rows))
509 monkeypatch.setattr(
510 runner, 'delete_yinyan_song_records2_existing_relations', lambda cur: 0,
511 )
512
513 runner.initialize_yinyan_song_records2(['1', '2'])
514
515 # 只有 mid_good 的录音被保留
516 assert inserted == [
517 {'song_id': 10, 'record_id': 100, 'platform': '1', 'platform_unique_key': 'mid_good'},
518 ]
519
520
490 def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypatch): 521 def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypatch):
491 pg_conn = _PgConnection() 522 pg_conn = _PgConnection()
492 updated = [] 523 updated = []
......