feat(etl): 新增 spider DB 歌手校验过滤无效录音
- 初始化阶段即校验 spider DB 歌手关联,过滤掉歌手数据缺失的录音 - 根据平台分别查询 QQ、酷狗和网易的有效歌手 key,过滤无效录音数据 - 统计并记录过滤掉的无效录音条数,日志输出详细过滤信息 - 文档更新新增 spider DB 歌手校验及相关数据统计说明 - 单元测试新增过滤无效歌手的情况,保证功能正确性
Showing
3 changed files
with
106 additions
and
11 deletions
| 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 = [] | ... | ... |
-
Please register or sign in to post a comment