feat(env): 更新目标库配置及添加 PostgreSQL 环境变量
- 目标库名称由 hk_songs 修改为 hk_songs_test,新增中间表 hk_songs_import_staging - 添加测试环境 crawler_dev PostgreSQL 连接配置变量 - 添加正式环境 archive_crawler PostgreSQL 连接配置变量 refactor(check_record_titles): 调整平台原始标题核对逻辑 - 变更核对数据库源为 archive_crawler 与 hikoon-data-spider - 按平台分表批量查询 spider 原始标题替代原录音名查询 - 合并结果中使用 spider_title 与 crawler_title 精确比对 - 输出 Excel 标题及字段相应更新,匹配时整行高亮 - 修改脚本参数说明和默认批处理数量描述 - 日志及信息输出文本调整以反映新数据源 feat(fix_record_titles): 新增基于 spider 标题的归一化批量修复工具 - 支持从 archive_crawler 读取待修复记录的推送关系 - 结合 spider 标题,计算归一化标题和版本号 - 支持对不同平台数据批量生成更新 SQL 预览或应用 - 支持备份更新前数据,支持安全回退操作 - 导出更新报告 Excel,突出显示变化字段 - 支持长文本分批过滤,避免数据库字段长度异常 - 增加命令行参数控制应用范围、批量大小及排除标题列表 - 使用严格的 SQL 语句构造与变更检测优化更新效率 - 实现基于 run_id 的多表回滚机制,保证数据一致性
Showing
14 changed files
with
579 additions
and
118 deletions
| ... | @@ -5,13 +5,14 @@ SOURCE_DB_USER= | ... | @@ -5,13 +5,14 @@ SOURCE_DB_USER= |
| 5 | SOURCE_DB_PASSWORD= | 5 | SOURCE_DB_PASSWORD= |
| 6 | SOURCE_DB_NAME= | 6 | SOURCE_DB_NAME= |
| 7 | 7 | ||
| 8 | # 目标库 - 词曲库 | 8 | # 目标库 - 词曲测试 |
| 9 | TARGET_DB_HOST= | 9 | TARGET_DB_HOST= |
| 10 | TARGET_DB_PORT=3306 | 10 | TARGET_DB_PORT=3306 |
| 11 | TARGET_DB_USER= | 11 | TARGET_DB_USER= |
| 12 | TARGET_DB_PASSWORD= | 12 | TARGET_DB_PASSWORD= |
| 13 | TARGET_DB_NAME= | 13 | TARGET_DB_NAME= |
| 14 | TARGET_TABLE_NAME=hk_songs | 14 | TARGET_TABLE_NAME=hk_songs_test |
| 15 | TARGET_TABLE_NAME_TMP=hk_songs_import_staging | ||
| 15 | 16 | ||
| 16 | # run_etl.py 默认读取并回写的导入状态表:yinyan_song_records 或 yinyan_song_records2 | 17 | # run_etl.py 默认读取并回写的导入状态表:yinyan_song_records 或 yinyan_song_records2 |
| 17 | YINYAN_IMPORT_TABLE=yinyan_song_records | 18 | YINYAN_IMPORT_TABLE=yinyan_song_records |
| ... | @@ -22,3 +23,21 @@ OSS_ACCESS_KEY_SECRET= | ... | @@ -22,3 +23,21 @@ OSS_ACCESS_KEY_SECRET= |
| 22 | OSS_ENDPOINT= | 23 | OSS_ENDPOINT= |
| 23 | OSS_BUCKET_NAME= | 24 | OSS_BUCKET_NAME= |
| 24 | OSS_FILE_BASE_NAME= | 25 | OSS_FILE_BASE_NAME= |
| 26 | |||
| 27 | # 测试环境 - crawler_dev PostgreSQL | ||
| 28 | TEST_CRAWLER_DB_HOST= | ||
| 29 | TEST_CRAWLER_DB_PORT=5432 | ||
| 30 | TEST_CRAWLER_DB_USER= | ||
| 31 | TEST_CRAWLER_DB_PASSWORD= | ||
| 32 | TEST_CRAWLER_DB_NAME=crawler_dev | ||
| 33 | TEST_CRAWLER_DB_SSL=false | ||
| 34 | TEST_ARCHIVE_DATA_DB_NAME=data_dev | ||
| 35 | |||
| 36 | # 正式环境 - archive_crawler PostgreSQL | ||
| 37 | ARCHIVE_CRAWLER_DB_HOST= | ||
| 38 | ARCHIVE_CRAWLER_DB_PORT=5432 | ||
| 39 | ARCHIVE_CRAWLER_DB_USER= | ||
| 40 | ARCHIVE_CRAWLER_DB_PASSWORD= | ||
| 41 | ARCHIVE_CRAWLER_DB_NAME=archive_crawler | ||
| 42 | ARCHIVE_CRAWLER_DB_SSL=false | ||
| 43 | ARCHIVE_DATA_DB_NAME=archive_data | ... | ... |
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | """核对音眼录音名与 crawler 平台歌曲标题,并输出 Excel。 | 2 | """核对 spider 平台原始标题与正式 crawler 歌曲标题,并输出 Excel。 |
| 3 | 3 | ||
| 4 | 数据链路: | 4 | 数据链路: |
| 5 | CRAWLER_DB.yinyan_song_records.record_id | 5 | archive_crawler.yinyan_song_records.(platform, platform_song_id) |
| 6 | -> SOURCE_DB.hk_music_record.id -> record_name | 6 | -> hikoon-data-spider.media_*_songs.id -> spider_title |
| 7 | CRAWLER_DB.yinyan_song_records.(platform, platform_song_id) | 7 | -> archive_crawler.crawler_*_songs.platform_song_id -> crawler_title |
| 8 | -> 对应 crawler_*_songs.platform_song_id -> title | ||
| 9 | 8 | ||
| 10 | 用法: | 9 | 用法: |
| 11 | .venv/bin/python check_record_titles.py | 10 | .venv/bin/python check_record_titles.py |
| ... | @@ -21,7 +20,7 @@ from openpyxl import Workbook | ... | @@ -21,7 +20,7 @@ from openpyxl import Workbook |
| 21 | from openpyxl.styles import Alignment, Font, PatternFill | 20 | from openpyxl.styles import Alignment, Font, PatternFill |
| 22 | from openpyxl.utils import get_column_letter | 21 | from openpyxl.utils import get_column_letter |
| 23 | 22 | ||
| 24 | from etl_to_crawler.connections import close_all_pools, get_pg_conn, get_source_conn | 23 | from etl_to_crawler.connections import close_all_pools, get_archive_crawler_conn, get_spider_conn |
| 25 | 24 | ||
| 26 | 25 | ||
| 27 | log = logging.getLogger(__name__) | 26 | log = logging.getLogger(__name__) |
| ... | @@ -29,6 +28,11 @@ log = logging.getLogger(__name__) | ... | @@ -29,6 +28,11 @@ log = logging.getLogger(__name__) |
| 29 | DEFAULT_OUTPUT = "record_title_check.xlsx" | 28 | DEFAULT_OUTPUT = "record_title_check.xlsx" |
| 30 | DEFAULT_BATCH_SIZE = 2000 | 29 | DEFAULT_BATCH_SIZE = 2000 |
| 31 | ALLOWED_IMPORT_TABLES = {"yinyan_song_records", "yinyan_song_records2"} | 30 | ALLOWED_IMPORT_TABLES = {"yinyan_song_records", "yinyan_song_records2"} |
| 31 | SPIDER_TABLES = { | ||
| 32 | "1": "media_tencent_songs", | ||
| 33 | "2": "media_ku_gou_songs", | ||
| 34 | "4": "media_netease_songs", | ||
| 35 | } | ||
| 32 | 36 | ||
| 33 | HEADER_FILL = PatternFill(fill_type="solid", fgColor="4472C4") | 37 | HEADER_FILL = PatternFill(fill_type="solid", fgColor="4472C4") |
| 34 | MATCH_FILL = PatternFill(fill_type="solid", fgColor="C6EFCE") | 38 | MATCH_FILL = PatternFill(fill_type="solid", fgColor="C6EFCE") |
| ... | @@ -81,67 +85,77 @@ def fetch_crawler_rows(pg_conn, import_table: str) -> list[dict]: | ... | @@ -81,67 +85,77 @@ def fetch_crawler_rows(pg_conn, import_table: str) -> list[dict]: |
| 81 | "platform": row[1], | 85 | "platform": row[1], |
| 82 | "platform_song_id": row[2], | 86 | "platform_song_id": row[2], |
| 83 | "recording_id": row[3], | 87 | "recording_id": row[3], |
| 84 | "title": row[4], | 88 | "crawler_title": row[4], |
| 85 | } | 89 | } |
| 86 | for row in rows | 90 | for row in rows |
| 87 | ] | 91 | ] |
| 88 | 92 | ||
| 89 | 93 | ||
| 90 | def fetch_record_names(mysql_conn, record_ids: Iterable[int], batch_size: int) -> dict[int, str | None]: | 94 | def fetch_spider_titles(mysql_conn, crawler_rows: list[dict], batch_size: int) -> dict[tuple[str, int], str]: |
| 91 | """按 record_id 分批读取 hk_music_record.record_name。""" | 95 | """按 platform_song_id 分平台批量读取 spider 原始 title。""" |
| 92 | unique_ids = sorted({int(record_id) for record_id in record_ids if record_id is not None}) | 96 | titles: dict[tuple[str, int], str] = {} |
| 93 | names: dict[int, str | None] = {} | 97 | for platform, table in SPIDER_TABLES.items(): |
| 94 | 98 | ids = sorted({ | |
| 95 | for batch in chunked(unique_ids, batch_size): | 99 | int(row["platform_song_id"]) |
| 96 | placeholders = ", ".join(["%s"] * len(batch)) | 100 | for row in crawler_rows |
| 97 | sql = f""" | 101 | if str(row["platform"]) == platform and row["platform_song_id"] is not None |
| 98 | SELECT id, record_name | 102 | }) |
| 99 | FROM hk_music_record | 103 | for batch in chunked(ids, batch_size): |
| 100 | WHERE id IN ({placeholders}) | 104 | placeholders = ", ".join(["%s"] * len(batch)) |
| 101 | """ | 105 | with mysql_conn.cursor() as cursor: |
| 102 | with mysql_conn.cursor() as cursor: | 106 | cursor.execute( |
| 103 | cursor.execute(sql, tuple(batch)) | 107 | f"SELECT id, title FROM {table} WHERE id IN ({placeholders})", |
| 104 | for row in cursor.fetchall(): | 108 | tuple(batch), |
| 105 | names[int(row["id"])] = row["record_name"] | 109 | ) |
| 106 | 110 | for row in cursor.fetchall(): | |
| 107 | return names | 111 | if row["title"] is not None: |
| 108 | 112 | titles[(platform, int(row["id"]))] = str(row["title"]) | |
| 109 | 113 | return titles | |
| 110 | def combine_rows(crawler_rows: list[dict], record_names: dict[int, str | None]) -> list[dict]: | 114 | |
| 115 | |||
| 116 | def combine_rows(crawler_rows: list[dict], spider_titles: dict[tuple[str, int], str]) -> list[dict]: | ||
| 111 | """合并两个数据库的查询结果,并计算是否完全相同。""" | 117 | """合并两个数据库的查询结果,并计算是否完全相同。""" |
| 112 | result = [] | 118 | result = [] |
| 113 | for row in crawler_rows: | 119 | for row in crawler_rows: |
| 114 | record_id = row["record_id"] | 120 | platform = str(row["platform"]) |
| 115 | record_name = record_names.get(int(record_id)) if record_id is not None else None | 121 | platform_song_id = row["platform_song_id"] |
| 116 | title = row["title"] | 122 | spider_title = ( |
| 123 | spider_titles.get((platform, int(platform_song_id))) | ||
| 124 | if platform_song_id is not None else None | ||
| 125 | ) | ||
| 126 | crawler_title = row["crawler_title"] | ||
| 117 | result.append( | 127 | result.append( |
| 118 | { | 128 | { |
| 119 | "record_id": record_id, | 129 | "record_id": row["record_id"], |
| 120 | "record_name": record_name, | 130 | "spider_title": spider_title, |
| 121 | "platform": row["platform"], | 131 | "crawler_title": crawler_title, |
| 122 | "platform_song_id": row["platform_song_id"], | 132 | "platform": platform, |
| 133 | "platform_song_id": platform_song_id, | ||
| 123 | "recording_id": row["recording_id"], | 134 | "recording_id": row["recording_id"], |
| 124 | "title": title, | ||
| 125 | # 两边都必须有值;不做 trim、大小写或繁简转换,按库中原值精确比较。 | 135 | # 两边都必须有值;不做 trim、大小写或繁简转换,按库中原值精确比较。 |
| 126 | "matched": record_name is not None and title is not None and record_name == title, | 136 | "matched": ( |
| 137 | spider_title is not None | ||
| 138 | and crawler_title is not None | ||
| 139 | and spider_title == crawler_title | ||
| 140 | ), | ||
| 127 | } | 141 | } |
| 128 | ) | 142 | ) |
| 129 | return result | 143 | return result |
| 130 | 144 | ||
| 131 | 145 | ||
| 132 | def write_excel(rows: list[dict], output_path: Path) -> None: | 146 | def write_excel(rows: list[dict], output_path: Path) -> None: |
| 133 | """写出三列表格;record_name 与 title 相同时整行标绿。""" | 147 | """写出核对表;spider_title 与 crawler_title 相同时整行标绿。""" |
| 134 | output_path.parent.mkdir(parents=True, exist_ok=True) | 148 | output_path.parent.mkdir(parents=True, exist_ok=True) |
| 135 | 149 | ||
| 136 | workbook = Workbook() | 150 | workbook = Workbook() |
| 137 | worksheet = workbook.active | 151 | worksheet = workbook.active |
| 138 | worksheet.title = "录音名与标题核对" | 152 | worksheet.title = "Spider与Crawler标题核对" |
| 139 | worksheet.freeze_panes = "A2" | 153 | worksheet.freeze_panes = "A2" |
| 140 | 154 | ||
| 141 | headers = ( | 155 | headers = ( |
| 142 | "record_id", | 156 | "record_id", |
| 143 | "record_name", | 157 | "spider_title", |
| 144 | "title", | 158 | "crawler_title", |
| 145 | "platform", | 159 | "platform", |
| 146 | "platform_song_id", | 160 | "platform_song_id", |
| 147 | "recording_id", | 161 | "recording_id", |
| ... | @@ -155,8 +169,8 @@ def write_excel(rows: list[dict], output_path: Path) -> None: | ... | @@ -155,8 +169,8 @@ def write_excel(rows: list[dict], output_path: Path) -> None: |
| 155 | for row_number, row in enumerate(rows, start=2): | 169 | for row_number, row in enumerate(rows, start=2): |
| 156 | values = ( | 170 | values = ( |
| 157 | row["record_id"], | 171 | row["record_id"], |
| 158 | row["record_name"], | 172 | row["spider_title"], |
| 159 | row["title"], | 173 | row["crawler_title"], |
| 160 | row["platform"], | 174 | row["platform"], |
| 161 | row["platform_song_id"], | 175 | row["platform_song_id"], |
| 162 | row["recording_id"], | 176 | row["recording_id"], |
| ... | @@ -175,15 +189,15 @@ def write_excel(rows: list[dict], output_path: Path) -> None: | ... | @@ -175,15 +189,15 @@ def write_excel(rows: list[dict], output_path: Path) -> None: |
| 175 | 189 | ||
| 176 | 190 | ||
| 177 | def parse_args() -> argparse.Namespace: | 191 | def parse_args() -> argparse.Namespace: |
| 178 | parser = argparse.ArgumentParser(description="核对 hk_music_record.record_name 与 crawler title") | 192 | parser = argparse.ArgumentParser(description="核对 spider title 与正式 crawler title") |
| 179 | parser.add_argument("--output", default=DEFAULT_OUTPUT, help=f"Excel 输出路径(默认: {DEFAULT_OUTPUT})") | 193 | parser.add_argument("--output", default=DEFAULT_OUTPUT, help=f"Excel 输出路径(默认: {DEFAULT_OUTPUT})") |
| 180 | parser.add_argument( | 194 | parser.add_argument( |
| 181 | "--table", | 195 | "--table", |
| 182 | choices=sorted(ALLOWED_IMPORT_TABLES), | 196 | choices=sorted(ALLOWED_IMPORT_TABLES), |
| 183 | default="yinyan_song_records", | 197 | default="yinyan_song_records", |
| 184 | help="待核对的 CRAWLER_DB 关联表", | 198 | help="待核对的 ARCHIVE_CRAWLER_DB 关联表", |
| 185 | ) | 199 | ) |
| 186 | parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE, help="MySQL 单批查询数量") | 200 | parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE, help="spider MySQL 单批查询数量") |
| 187 | args = parser.parse_args() | 201 | args = parser.parse_args() |
| 188 | if args.batch_size <= 0: | 202 | if args.batch_size <= 0: |
| 189 | parser.error("--batch-size 必须大于 0") | 203 | parser.error("--batch-size 必须大于 0") |
| ... | @@ -195,28 +209,24 @@ def main() -> None: | ... | @@ -195,28 +209,24 @@ def main() -> None: |
| 195 | output_path = Path(args.output).expanduser().resolve() | 209 | output_path = Path(args.output).expanduser().resolve() |
| 196 | 210 | ||
| 197 | try: | 211 | try: |
| 198 | pg_conn = get_pg_conn() | 212 | pg_conn = get_archive_crawler_conn() |
| 199 | mysql_conn = get_source_conn() | 213 | mysql_conn = get_spider_conn() |
| 200 | 214 | ||
| 201 | crawler_rows = fetch_crawler_rows(pg_conn, args.table) | 215 | crawler_rows = fetch_crawler_rows(pg_conn, args.table) |
| 202 | log.info("从 %s 读取 %d 条关联记录", args.table, len(crawler_rows)) | 216 | log.info("从 %s 读取 %d 条关联记录", args.table, len(crawler_rows)) |
| 203 | 217 | ||
| 204 | record_names = fetch_record_names( | 218 | spider_titles = fetch_spider_titles(mysql_conn, crawler_rows, args.batch_size) |
| 205 | mysql_conn, | 219 | log.info("从 hikoon-data-spider 读取 %d 个平台标题", len(spider_titles)) |
| 206 | (row["record_id"] for row in crawler_rows), | ||
| 207 | args.batch_size, | ||
| 208 | ) | ||
| 209 | log.info("从 hk_music_record 读取 %d 个录音名", len(record_names)) | ||
| 210 | 220 | ||
| 211 | rows = combine_rows(crawler_rows, record_names) | 221 | rows = combine_rows(crawler_rows, spider_titles) |
| 212 | write_excel(rows, output_path) | 222 | write_excel(rows, output_path) |
| 213 | 223 | ||
| 214 | matched = sum(row["matched"] for row in rows) | 224 | matched = sum(row["matched"] for row in rows) |
| 215 | missing_name = sum(row["record_name"] is None for row in rows) | 225 | missing_spider = sum(row["spider_title"] is None for row in rows) |
| 216 | missing_title = sum(row["title"] is None for row in rows) | 226 | missing_crawler = sum(row["crawler_title"] is None for row in rows) |
| 217 | log.info( | 227 | log.info( |
| 218 | "核对完成:总数=%d,相同=%d,不同=%d,缺少 record_name=%d,缺少 title=%d", | 228 | "核对完成:总数=%d,相同=%d,不同=%d,缺少 spider_title=%d,缺少 crawler_title=%d", |
| 219 | len(rows), matched, len(rows) - matched, missing_name, missing_title, | 229 | len(rows), matched, len(rows) - matched, missing_spider, missing_crawler, |
| 220 | ) | 230 | ) |
| 221 | log.info("Excel 已输出至 %s", output_path) | 231 | log.info("Excel 已输出至 %s", output_path) |
| 222 | finally: | 232 | finally: | ... | ... |
| ... | @@ -21,13 +21,32 @@ HK_SONGS_DB = { | ... | @@ -21,13 +21,32 @@ HK_SONGS_DB = { |
| 21 | 'charset': 'utf8mb4', | 21 | 'charset': 'utf8mb4', |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | CRAWLER_DB = { | 24 | TEST_CRAWLER_DB = { |
| 25 | 'host': os.environ['CRAWLER_DB_HOST'], | 25 | 'host': os.environ['TEST_CRAWLER_DB_HOST'], |
| 26 | 'port': int(os.environ.get('CRAWLER_DB_PORT', '5432')), | 26 | 'port': int(os.environ.get('TEST_CRAWLER_DB_PORT', '5432')), |
| 27 | 'user': os.environ['CRAWLER_DB_USER'], | 27 | 'user': os.environ['TEST_CRAWLER_DB_USER'], |
| 28 | 'password': os.environ['CRAWLER_DB_PASSWORD'], | 28 | 'password': os.environ['TEST_CRAWLER_DB_PASSWORD'], |
| 29 | 'database': os.environ['CRAWLER_DB_NAME'], | 29 | 'database': os.environ['TEST_CRAWLER_DB_NAME'], |
| 30 | 'ssl_context': None if os.environ.get('CRAWLER_DB_SSL', 'false').lower() != 'true' else True, | 30 | 'ssl_context': None if os.environ.get('TEST_CRAWLER_DB_SSL', 'false').lower() != 'true' else True, |
| 31 | } | ||
| 32 | |||
| 33 | TEST_ARCHIVE_DATA_DB = { | ||
| 34 | **TEST_CRAWLER_DB, | ||
| 35 | 'database': os.environ.get('TEST_ARCHIVE_DATA_DB_NAME', 'data_dev'), | ||
| 36 | } | ||
| 37 | |||
| 38 | ARCHIVE_CRAWLER_DB = { | ||
| 39 | 'host': os.environ['ARCHIVE_CRAWLER_DB_HOST'], | ||
| 40 | 'port': int(os.environ.get('ARCHIVE_CRAWLER_DB_PORT', '5432')), | ||
| 41 | 'user': os.environ['ARCHIVE_CRAWLER_DB_USER'], | ||
| 42 | 'password': os.environ['ARCHIVE_CRAWLER_DB_PASSWORD'], | ||
| 43 | 'database': os.environ.get('ARCHIVE_CRAWLER_DB_NAME', 'archive_crawler'), | ||
| 44 | 'ssl_context': None if os.environ.get('ARCHIVE_CRAWLER_DB_SSL', 'false').lower() != 'true' else True, | ||
| 45 | } | ||
| 46 | |||
| 47 | ARCHIVE_DATA_DB = { | ||
| 48 | **ARCHIVE_CRAWLER_DB, | ||
| 49 | 'database': os.environ.get('ARCHIVE_DATA_DB_NAME', 'archive_data'), | ||
| 31 | } | 50 | } |
| 32 | 51 | ||
| 33 | OSS_CONFIG = { | 52 | OSS_CONFIG = { | ... | ... |
| ... | @@ -3,7 +3,16 @@ import pymysql | ... | @@ -3,7 +3,16 @@ import pymysql |
| 3 | import pymysql.cursors | 3 | import pymysql.cursors |
| 4 | import pg8000 | 4 | import pg8000 |
| 5 | import oss2 | 5 | import oss2 |
| 6 | from .config import SOURCE_DB, HK_SONGS_DB, CRAWLER_DB, OSS_CONFIG, OSS_CONNECTION_POOL_SIZE | 6 | from .config import ( |
| 7 | ARCHIVE_CRAWLER_DB, | ||
| 8 | ARCHIVE_DATA_DB, | ||
| 9 | SOURCE_DB, | ||
| 10 | HK_SONGS_DB, | ||
| 11 | TEST_CRAWLER_DB, | ||
| 12 | TEST_ARCHIVE_DATA_DB, | ||
| 13 | OSS_CONFIG, | ||
| 14 | OSS_CONNECTION_POOL_SIZE, | ||
| 15 | ) | ||
| 7 | 16 | ||
| 8 | 17 | ||
| 9 | CONN_POOL_SIZE = 4 | 18 | CONN_POOL_SIZE = 4 |
| ... | @@ -238,7 +247,10 @@ class _PgConnPool: | ... | @@ -238,7 +247,10 @@ class _PgConnPool: |
| 238 | _source_pool = None | 247 | _source_pool = None |
| 239 | _hk_songs_pool = None | 248 | _hk_songs_pool = None |
| 240 | _spider_pool = None | 249 | _spider_pool = None |
| 241 | _pg_pool = None | 250 | _test_crawler_pool = None |
| 251 | _test_archive_pool = None | ||
| 252 | _archive_pool = None | ||
| 253 | _archive_crawler_pool = None | ||
| 242 | 254 | ||
| 243 | 255 | ||
| 244 | def _get_source_pool() -> _MySqlConnPool: | 256 | def _get_source_pool() -> _MySqlConnPool: |
| ... | @@ -264,11 +276,32 @@ def _get_spider_pool() -> _MySqlConnPool: | ... | @@ -264,11 +276,32 @@ def _get_spider_pool() -> _MySqlConnPool: |
| 264 | return _spider_pool | 276 | return _spider_pool |
| 265 | 277 | ||
| 266 | 278 | ||
| 267 | def _get_pg_pool() -> _PgConnPool: | 279 | def _get_test_crawler_pool() -> _PgConnPool: |
| 268 | global _pg_pool | 280 | global _test_crawler_pool |
| 269 | if _pg_pool is None: | 281 | if _test_crawler_pool is None: |
| 270 | _pg_pool = _PgConnPool(CRAWLER_DB) | 282 | _test_crawler_pool = _PgConnPool(TEST_CRAWLER_DB) |
| 271 | return _pg_pool | 283 | return _test_crawler_pool |
| 284 | |||
| 285 | |||
| 286 | def _get_test_archive_pool() -> _PgConnPool: | ||
| 287 | global _test_archive_pool | ||
| 288 | if _test_archive_pool is None: | ||
| 289 | _test_archive_pool = _PgConnPool(TEST_ARCHIVE_DATA_DB) | ||
| 290 | return _test_archive_pool | ||
| 291 | |||
| 292 | |||
| 293 | def _get_archive_pool() -> _PgConnPool: | ||
| 294 | global _archive_pool | ||
| 295 | if _archive_pool is None: | ||
| 296 | _archive_pool = _PgConnPool(ARCHIVE_DATA_DB) | ||
| 297 | return _archive_pool | ||
| 298 | |||
| 299 | |||
| 300 | def _get_archive_crawler_pool() -> _PgConnPool: | ||
| 301 | global _archive_crawler_pool | ||
| 302 | if _archive_crawler_pool is None: | ||
| 303 | _archive_crawler_pool = _PgConnPool(ARCHIVE_CRAWLER_DB) | ||
| 304 | return _archive_crawler_pool | ||
| 272 | 305 | ||
| 273 | 306 | ||
| 274 | def get_source_conn() -> pymysql.Connection: | 307 | def get_source_conn() -> pymysql.Connection: |
| ... | @@ -283,8 +316,20 @@ def get_hk_songs_conn() -> pymysql.Connection: | ... | @@ -283,8 +316,20 @@ def get_hk_songs_conn() -> pymysql.Connection: |
| 283 | return _get_hk_songs_pool().get() | 316 | return _get_hk_songs_pool().get() |
| 284 | 317 | ||
| 285 | 318 | ||
| 286 | def get_pg_conn() -> _NulSafePgConnection: | 319 | def get_test_crawler_conn() -> _NulSafePgConnection: |
| 287 | return _get_pg_pool().get() | 320 | return _get_test_crawler_pool().get() |
| 321 | |||
| 322 | |||
| 323 | def get_test_archive_conn() -> _NulSafePgConnection: | ||
| 324 | return _get_test_archive_pool().get() | ||
| 325 | |||
| 326 | |||
| 327 | def get_archive_conn() -> _NulSafePgConnection: | ||
| 328 | return _get_archive_pool().get() | ||
| 329 | |||
| 330 | |||
| 331 | def get_archive_crawler_conn() -> _NulSafePgConnection: | ||
| 332 | return _get_archive_crawler_pool().get() | ||
| 288 | 333 | ||
| 289 | 334 | ||
| 290 | def get_oss_bucket() -> oss2.Bucket: | 335 | def get_oss_bucket() -> oss2.Bucket: |
| ... | @@ -298,18 +343,24 @@ def close_all_pools(): | ... | @@ -298,18 +343,24 @@ def close_all_pools(): |
| 298 | for pool in (_source_pool, _hk_songs_pool, _spider_pool): | 343 | for pool in (_source_pool, _hk_songs_pool, _spider_pool): |
| 299 | if pool is not None: | 344 | if pool is not None: |
| 300 | pool.close_all() | 345 | pool.close_all() |
| 301 | if _pg_pool is not None: | 346 | if _test_crawler_pool is not None: |
| 302 | _pg_pool.close_all() | 347 | _test_crawler_pool.close_all() |
| 348 | if _test_archive_pool is not None: | ||
| 349 | _test_archive_pool.close_all() | ||
| 350 | if _archive_pool is not None: | ||
| 351 | _archive_pool.close_all() | ||
| 352 | if _archive_crawler_pool is not None: | ||
| 353 | _archive_crawler_pool.close_all() | ||
| 303 | 354 | ||
| 304 | 355 | ||
| 305 | def refresh_conn(conn, pool_type: str): | 356 | def refresh_conn(conn, pool_type: str): |
| 306 | """验证连接健康度,断开时返回新连接,正常时原样返回。 | 357 | """验证连接健康度,断开时返回新连接,正常时原样返回。 |
| 307 | 358 | ||
| 308 | 用法:在批次循环开头调用,如 ``conn = refresh_conn(conn, 'pg')``。 | 359 | 用法:在批次循环开头调用,如 ``conn = refresh_conn(conn, 'test_crawler')``。 |
| 309 | pool_type 取 'source' | 'hk_songs' | 'spider' | 'pg'。 | 360 | pool_type 取 'source' | 'hk_songs' | 'spider' | 'test_crawler'。 |
| 310 | """ | 361 | """ |
| 311 | try: | 362 | try: |
| 312 | if pool_type == 'pg': | 363 | if pool_type == 'test_crawler': |
| 313 | conn.run("SELECT 1") | 364 | conn.run("SELECT 1") |
| 314 | return conn | 365 | return conn |
| 315 | # MySQL | 366 | # MySQL |
| ... | @@ -322,7 +373,7 @@ def refresh_conn(conn, pool_type: str): | ... | @@ -322,7 +373,7 @@ def refresh_conn(conn, pool_type: str): |
| 322 | 'source': _get_source_pool, | 373 | 'source': _get_source_pool, |
| 323 | 'hk_songs': _get_hk_songs_pool, | 374 | 'hk_songs': _get_hk_songs_pool, |
| 324 | 'spider': _get_spider_pool, | 375 | 'spider': _get_spider_pool, |
| 325 | 'pg': _get_pg_pool, | 376 | 'test_crawler': _get_test_crawler_pool, |
| 326 | } | 377 | } |
| 327 | pool = pools.get(pool_type) | 378 | pool = pools.get(pool_type) |
| 328 | if pool is None: | 379 | if pool is None: | ... | ... |
| ... | @@ -8,7 +8,7 @@ from .config import ( | ... | @@ -8,7 +8,7 @@ from .config import ( |
| 8 | PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE, BATCH_SIZE, BACKFILL_BATCH_SIZE, | 8 | PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE, BATCH_SIZE, BACKFILL_BATCH_SIZE, |
| 9 | OSS_CONFIG, YINYAN_IMPORT_TABLE, | 9 | OSS_CONFIG, YINYAN_IMPORT_TABLE, |
| 10 | ) | 10 | ) |
| 11 | from .connections import get_hk_songs_conn, get_source_conn, get_spider_conn, get_pg_conn, get_oss_bucket, refresh_conn, close_all_pools | 11 | from .connections import get_hk_songs_conn, get_source_conn, get_spider_conn, get_test_crawler_conn, get_oss_bucket, refresh_conn, close_all_pools |
| 12 | from .reader import ( | 12 | from .reader import ( |
| 13 | iter_hk_songs_batches, | 13 | iter_hk_songs_batches, |
| 14 | fetch_hk_songs_by_source_ids, | 14 | fetch_hk_songs_by_source_ids, |
| ... | @@ -1337,7 +1337,7 @@ def _pick_record_with_singer(records: list[dict], spider_conn) -> dict | None: | ... | @@ -1337,7 +1337,7 @@ def _pick_record_with_singer(records: list[dict], spider_conn) -> dict | None: |
| 1337 | def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None = None) -> None: | 1337 | def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None = None) -> None: |
| 1338 | hk_conn = get_hk_songs_conn() | 1338 | hk_conn = get_hk_songs_conn() |
| 1339 | src_conn = get_source_conn() | 1339 | src_conn = get_source_conn() |
| 1340 | pg_conn = get_pg_conn() | 1340 | pg_conn = get_test_crawler_conn() |
| 1341 | 1341 | ||
| 1342 | # 查询已存在的 song_id,用于跳过已处理的记录 | 1342 | # 查询已存在的 song_id,用于跳过已处理的记录 |
| 1343 | with pg_conn.cursor() as pg_cur: | 1343 | with pg_conn.cursor() as pg_cur: |
| ... | @@ -1351,7 +1351,7 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None | ... | @@ -1351,7 +1351,7 @@ def initialize_yinyan_song_records(platforms: list[str], max_batches: int | None |
| 1351 | # 长任务连接可能断开,每批次开始前刷新 | 1351 | # 长任务连接可能断开,每批次开始前刷新 |
| 1352 | hk_conn = refresh_conn(hk_conn, 'hk_songs') | 1352 | hk_conn = refresh_conn(hk_conn, 'hk_songs') |
| 1353 | src_conn = refresh_conn(src_conn, 'source') | 1353 | src_conn = refresh_conn(src_conn, 'source') |
| 1354 | pg_conn = refresh_conn(pg_conn, 'pg') | 1354 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1355 | 1355 | ||
| 1356 | if max_batches is not None and i >= max_batches: | 1356 | if max_batches is not None and i >= max_batches: |
| 1357 | break | 1357 | break |
| ... | @@ -1416,7 +1416,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1416,7 +1416,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1416 | 后续导入允许歌曲的 singers 为空。 | 1416 | 后续导入允许歌曲的 singers 为空。 |
| 1417 | """ | 1417 | """ |
| 1418 | src_conn = get_source_conn() | 1418 | src_conn = get_source_conn() |
| 1419 | pg_conn = get_pg_conn() | 1419 | pg_conn = get_test_crawler_conn() |
| 1420 | total_inserted = 0 | 1420 | total_inserted = 0 |
| 1421 | 1421 | ||
| 1422 | try: | 1422 | try: |
| ... | @@ -1427,7 +1427,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1427,7 +1427,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1427 | for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')): | 1427 | for index, start in enumerate(tqdm(range(0, len(song_ids), BATCH_SIZE), desc='init-yinyan-records2')): |
| 1428 | # 长任务连接可能断开,每批次开始前刷新 | 1428 | # 长任务连接可能断开,每批次开始前刷新 |
| 1429 | src_conn = refresh_conn(src_conn, 'source') | 1429 | src_conn = refresh_conn(src_conn, 'source') |
| 1430 | pg_conn = refresh_conn(pg_conn, 'pg') | 1430 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1431 | 1431 | ||
| 1432 | if max_batches is not None and index >= max_batches: | 1432 | if max_batches is not None and index >= max_batches: |
| 1433 | break | 1433 | break |
| ... | @@ -1467,7 +1467,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non | ... | @@ -1467,7 +1467,7 @@ def initialize_yinyan_song_records2(platforms: list[str], max_batches: int | Non |
| 1467 | 1467 | ||
| 1468 | def backfill_yinyan_record_platforms(max_batches: int | None = None) -> None: | 1468 | def backfill_yinyan_record_platforms(max_batches: int | None = None) -> None: |
| 1469 | src_conn = get_source_conn() | 1469 | src_conn = get_source_conn() |
| 1470 | pg_conn = get_pg_conn() | 1470 | pg_conn = get_test_crawler_conn() |
| 1471 | total = 0 | 1471 | total = 0 |
| 1472 | skipped = 0 | 1472 | skipped = 0 |
| 1473 | 1473 | ||
| ... | @@ -1477,7 +1477,7 @@ def backfill_yinyan_record_platforms(max_batches: int | None = None) -> None: | ... | @@ -1477,7 +1477,7 @@ def backfill_yinyan_record_platforms(max_batches: int | None = None) -> None: |
| 1477 | while max_batches is None or batch_index < max_batches: | 1477 | while max_batches is None or batch_index < max_batches: |
| 1478 | # 长任务连接可能断开,每批次开始前刷新 | 1478 | # 长任务连接可能断开,每批次开始前刷新 |
| 1479 | src_conn = refresh_conn(src_conn, 'source') | 1479 | src_conn = refresh_conn(src_conn, 'source') |
| 1480 | pg_conn = refresh_conn(pg_conn, 'pg') | 1480 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1481 | 1481 | ||
| 1482 | with pg_conn.cursor() as pg_cur: | 1482 | with pg_conn.cursor() as pg_cur: |
| 1483 | rows = fetch_yinyan_records_missing_platform(pg_cur, BACKFILL_BATCH_SIZE) | 1483 | rows = fetch_yinyan_records_missing_platform(pg_cur, BACKFILL_BATCH_SIZE) |
| ... | @@ -1523,7 +1523,7 @@ def _backfill_qq_singers(spider_conn, pg_conn, bucket, base_url, max_batches): | ... | @@ -1523,7 +1523,7 @@ def _backfill_qq_singers(spider_conn, pg_conn, bucket, base_url, max_batches): |
| 1523 | while max_batches is None or batch_index < max_batches: | 1523 | while max_batches is None or batch_index < max_batches: |
| 1524 | # 长任务连接可能断开,每批次开始前刷新 | 1524 | # 长任务连接可能断开,每批次开始前刷新 |
| 1525 | spider_conn = refresh_conn(spider_conn, 'spider') | 1525 | spider_conn = refresh_conn(spider_conn, 'spider') |
| 1526 | pg_conn = refresh_conn(pg_conn, 'pg') | 1526 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1527 | 1527 | ||
| 1528 | with pg_conn.cursor() as pg_cur: | 1528 | with pg_conn.cursor() as pg_cur: |
| 1529 | rows = fetch_qq_songs_missing_singers(pg_cur, BATCH_SIZE) | 1529 | rows = fetch_qq_songs_missing_singers(pg_cur, BATCH_SIZE) |
| ... | @@ -1603,7 +1603,7 @@ def _backfill_kugou_singers(spider_conn, pg_conn, bucket, base_url, max_batches) | ... | @@ -1603,7 +1603,7 @@ def _backfill_kugou_singers(spider_conn, pg_conn, bucket, base_url, max_batches) |
| 1603 | while max_batches is None or batch_index < max_batches: | 1603 | while max_batches is None or batch_index < max_batches: |
| 1604 | # 长任务连接可能断开,每批次开始前刷新 | 1604 | # 长任务连接可能断开,每批次开始前刷新 |
| 1605 | spider_conn = refresh_conn(spider_conn, 'spider') | 1605 | spider_conn = refresh_conn(spider_conn, 'spider') |
| 1606 | pg_conn = refresh_conn(pg_conn, 'pg') | 1606 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1607 | 1607 | ||
| 1608 | with pg_conn.cursor() as pg_cur: | 1608 | with pg_conn.cursor() as pg_cur: |
| 1609 | rows = fetch_kugou_songs_missing_singers(pg_cur, BATCH_SIZE) | 1609 | rows = fetch_kugou_songs_missing_singers(pg_cur, BATCH_SIZE) |
| ... | @@ -1677,7 +1677,7 @@ def _backfill_netease_singers(spider_conn, pg_conn, bucket, base_url, max_batche | ... | @@ -1677,7 +1677,7 @@ def _backfill_netease_singers(spider_conn, pg_conn, bucket, base_url, max_batche |
| 1677 | while max_batches is None or batch_index < max_batches: | 1677 | while max_batches is None or batch_index < max_batches: |
| 1678 | # 长任务连接可能断开,每批次开始前刷新 | 1678 | # 长任务连接可能断开,每批次开始前刷新 |
| 1679 | spider_conn = refresh_conn(spider_conn, 'spider') | 1679 | spider_conn = refresh_conn(spider_conn, 'spider') |
| 1680 | pg_conn = refresh_conn(pg_conn, 'pg') | 1680 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1681 | 1681 | ||
| 1682 | with pg_conn.cursor() as pg_cur: | 1682 | with pg_conn.cursor() as pg_cur: |
| 1683 | rows = fetch_netease_songs_missing_singers(pg_cur, BATCH_SIZE) | 1683 | rows = fetch_netease_songs_missing_singers(pg_cur, BATCH_SIZE) |
| ... | @@ -1746,7 +1746,7 @@ def _backfill_netease_singers(spider_conn, pg_conn, bucket, base_url, max_batche | ... | @@ -1746,7 +1746,7 @@ def _backfill_netease_singers(spider_conn, pg_conn, bucket, base_url, max_batche |
| 1746 | 1746 | ||
| 1747 | def backfill_empty_singers(platforms: list[str], max_batches: int | None = None) -> None: | 1747 | def backfill_empty_singers(platforms: list[str], max_batches: int | None = None) -> None: |
| 1748 | spider_conn = get_spider_conn() | 1748 | spider_conn = get_spider_conn() |
| 1749 | pg_conn = get_pg_conn() | 1749 | pg_conn = get_test_crawler_conn() |
| 1750 | bucket = get_oss_bucket() | 1750 | bucket = get_oss_bucket() |
| 1751 | base_url = OSS_CONFIG['base_url'] | 1751 | base_url = OSS_CONFIG['base_url'] |
| 1752 | try: | 1752 | try: |
| ... | @@ -1769,7 +1769,7 @@ def backfill_qq_invalid_covers(max_batches: int | None = None) -> None: | ... | @@ -1769,7 +1769,7 @@ def backfill_qq_invalid_covers(max_batches: int | None = None) -> None: |
| 1769 | hk_conn = get_hk_songs_conn() | 1769 | hk_conn = get_hk_songs_conn() |
| 1770 | src_conn = get_source_conn() | 1770 | src_conn = get_source_conn() |
| 1771 | spider_conn = get_spider_conn() | 1771 | spider_conn = get_spider_conn() |
| 1772 | pg_conn = get_pg_conn() | 1772 | pg_conn = get_test_crawler_conn() |
| 1773 | bucket = get_oss_bucket() | 1773 | bucket = get_oss_bucket() |
| 1774 | base_url = OSS_CONFIG['base_url'] | 1774 | base_url = OSS_CONFIG['base_url'] |
| 1775 | batch_index = replaced = filtered = retry_later = 0 | 1775 | batch_index = replaced = filtered = retry_later = 0 |
| ... | @@ -1780,7 +1780,7 @@ def backfill_qq_invalid_covers(max_batches: int | None = None) -> None: | ... | @@ -1780,7 +1780,7 @@ def backfill_qq_invalid_covers(max_batches: int | None = None) -> None: |
| 1780 | hk_conn = refresh_conn(hk_conn, 'hk_songs') | 1780 | hk_conn = refresh_conn(hk_conn, 'hk_songs') |
| 1781 | src_conn = refresh_conn(src_conn, 'source') | 1781 | src_conn = refresh_conn(src_conn, 'source') |
| 1782 | spider_conn = refresh_conn(spider_conn, 'spider') | 1782 | spider_conn = refresh_conn(spider_conn, 'spider') |
| 1783 | pg_conn = refresh_conn(pg_conn, 'pg') | 1783 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1784 | 1784 | ||
| 1785 | with pg_conn.cursor() as pg_cur: | 1785 | with pg_conn.cursor() as pg_cur: |
| 1786 | invalid_rows = fetch_qq_songs_with_invalid_covers( | 1786 | invalid_rows = fetch_qq_songs_with_invalid_covers( |
| ... | @@ -1879,7 +1879,7 @@ def run( | ... | @@ -1879,7 +1879,7 @@ def run( |
| 1879 | hk_conn = get_hk_songs_conn() | 1879 | hk_conn = get_hk_songs_conn() |
| 1880 | src_conn = get_source_conn() | 1880 | src_conn = get_source_conn() |
| 1881 | spider_conn = get_spider_conn() | 1881 | spider_conn = get_spider_conn() |
| 1882 | pg_conn = get_pg_conn() | 1882 | pg_conn = get_test_crawler_conn() |
| 1883 | bucket = get_oss_bucket() | 1883 | bucket = get_oss_bucket() |
| 1884 | base_url = OSS_CONFIG['base_url'] | 1884 | base_url = OSS_CONFIG['base_url'] |
| 1885 | total_ok = total_err = 0 | 1885 | total_ok = total_err = 0 |
| ... | @@ -1905,7 +1905,7 @@ def run( | ... | @@ -1905,7 +1905,7 @@ def run( |
| 1905 | hk_conn = refresh_conn(hk_conn, 'hk_songs') | 1905 | hk_conn = refresh_conn(hk_conn, 'hk_songs') |
| 1906 | src_conn = refresh_conn(src_conn, 'source') | 1906 | src_conn = refresh_conn(src_conn, 'source') |
| 1907 | spider_conn = refresh_conn(spider_conn, 'spider') | 1907 | spider_conn = refresh_conn(spider_conn, 'spider') |
| 1908 | pg_conn = refresh_conn(pg_conn, 'pg') | 1908 | pg_conn = refresh_conn(pg_conn, 'test_crawler') |
| 1909 | 1909 | ||
| 1910 | with pg_conn.cursor() as pg_cur: | 1910 | with pg_conn.cursor() as pg_cur: |
| 1911 | pending_records = fetch_pending_yinyan_song_records(pg_cur, BATCH_SIZE, platforms) | 1911 | pending_records = fetch_pending_yinyan_song_records(pg_cur, BATCH_SIZE, platforms) | ... | ... |
fix_record_titles.py
0 → 100644
This diff is collapsed.
Click to expand it.
| ... | @@ -15,7 +15,7 @@ import argparse | ... | @@ -15,7 +15,7 @@ import argparse |
| 15 | import logging | 15 | import logging |
| 16 | 16 | ||
| 17 | from etl_to_crawler.config import PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE | 17 | from etl_to_crawler.config import PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE |
| 18 | from etl_to_crawler.connections import get_source_conn, get_pg_conn, close_all_pools | 18 | from etl_to_crawler.connections import get_source_conn, get_test_crawler_conn, close_all_pools |
| 19 | from etl_to_crawler.utils import split_title_version | 19 | from etl_to_crawler.utils import split_title_version |
| 20 | 20 | ||
| 21 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') | 21 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') |
| ... | @@ -134,7 +134,7 @@ def main(): | ... | @@ -134,7 +134,7 @@ def main(): |
| 134 | args = parser.parse_args() | 134 | args = parser.parse_args() |
| 135 | 135 | ||
| 136 | mysql_conn = get_source_conn() | 136 | mysql_conn = get_source_conn() |
| 137 | pg_conn = get_pg_conn() | 137 | pg_conn = get_test_crawler_conn() |
| 138 | 138 | ||
| 139 | total_updated = 0 | 139 | total_updated = 0 |
| 140 | total_diff = 0 | 140 | total_diff = 0 | ... | ... |
gen_gap_xlsx.py
0 → 100644
This diff is collapsed.
Click to expand it.
python/title-norm/tests/test_title_norm.py
0 → 100644
| 1 | import title_norm as tn | ||
| 2 | |||
| 3 | |||
| 4 | def test_normalize_name(): | ||
| 5 | cases = [ | ||
| 6 | ("周杰伦", "周杰伦"), | ||
| 7 | (" 周杰伦 ", "周杰伦"), | ||
| 8 | ("Glass Animals", "glass animals"), | ||
| 9 | ("Jay Chou", "jay chou"), | ||
| 10 | ("", ""), | ||
| 11 | ] | ||
| 12 | for raw, want in cases: | ||
| 13 | assert tn.normalize_name(raw) == want, raw | ||
| 14 | |||
| 15 | |||
| 16 | def test_singer_identity_key(): | ||
| 17 | assert tn.singer_identity_key("crawler_qqmusic_singers", 162918) == "qqmusic:162918" | ||
| 18 | |||
| 19 | |||
| 20 | def test_author_identity_key(): | ||
| 21 | assert tn.author_identity_key(" 周杰伦 ") == "name:周杰伦" | ||
| 22 | |||
| 23 | |||
| 24 | def test_platform_from_singer_table(): | ||
| 25 | cases = [ | ||
| 26 | ("crawler_qqmusic_singers", "qqmusic"), | ||
| 27 | ("crawler_netease_singers", "netease"), | ||
| 28 | ("crawler_kugou_songs", "kugou"), | ||
| 29 | ] | ||
| 30 | for raw, want in cases: | ||
| 31 | assert tn.platform_from_singer_table(raw) == want | ||
| 32 | |||
| 33 | |||
| 34 | def test_split_names(): | ||
| 35 | got = tn.split_names("周杰伦/方文山") | ||
| 36 | assert got == ["周杰伦", "方文山"] | ||
| 37 | assert tn.split_names("") is None | ||
| 38 | |||
| 39 | |||
| 40 | def test_normalize_title(): | ||
| 41 | cases = [ | ||
| 42 | ("七里香", "七里香", ""), | ||
| 43 | (" 七里香 ", "七里香", ""), | ||
| 44 | ("七里香(Live)", "七里香", "live"), | ||
| 45 | ("七里香(Live版)", "七里香", "live"), | ||
| 46 | ("七里香 [Remix]", "七里香", "remix"), | ||
| 47 | ("七里香(混音版)", "七里香", "remix"), | ||
| 48 | ("七里香(现场版)", "七里香", "live"), | ||
| 49 | ("七里香(DJ版)", "七里香", "dj版"), | ||
| 50 | ("七里香(ass version)", "七里香", "ass version"), | ||
| 51 | ("ANGEL (天使)", "angel (天使)", ""), | ||
| 52 | ("Radio(Dum-Dum)", "radio(dum-dum)", ""), | ||
| 53 | ("七里香(完整版)", "七里香", ""), | ||
| 54 | ("七里香(原版)", "七里香", ""), | ||
| 55 | ("《七里香》", "七里香", ""), | ||
| 56 | ("《七裡香》(Live)", "七里香", "live"), | ||
| 57 | ("七裡香", "七里香", ""), | ||
| 58 | ("七里香 - DJ版", "七里香", "dj版"), | ||
| 59 | ("七里香 - 周杰伦", "七里香 - 周杰伦", ""), | ||
| 60 | ("七里香—Live", "七里香", "live"), | ||
| 61 | ("七里香(天使)(Live)", "七里香(天使)", "live"), | ||
| 62 | ("七里香 现场版", "七里香", "live"), | ||
| 63 | ] | ||
| 64 | for raw, want_title, want_ver in cases: | ||
| 65 | got_title, got_ver = tn.normalize_title(raw) | ||
| 66 | assert (got_title, got_ver) == (want_title, want_ver), raw | ||
| 67 | |||
| 68 | |||
| 69 | def test_normalize_name_set(): | ||
| 70 | a = tn.normalize_name_set("周杰伦,方文山") | ||
| 71 | b = tn.normalize_name_set("方文山、周杰伦") | ||
| 72 | assert a == b | ||
| 73 | assert tn.normalize_name_set("") is None | ||
| 74 | |||
| 75 | |||
| 76 | def test_strip_clip_markers(): | ||
| 77 | cases = [ | ||
| 78 | ("父亲写的散文诗-剪辑版", "父亲写的散文诗"), | ||
| 79 | ("人生路漫漫主歌", "人生路漫漫"), | ||
| 80 | ("Stay with me(氛围beat)-全曲", "Stay with me(氛围beat)"), | ||
| 81 | ("咏春(副歌)", "咏春(副歌)"), | ||
| 82 | ("小气鬼(加速版)", "小气鬼"), | ||
| 83 | ("枪火(剪辑一)", "枪火"), | ||
| 84 | ("恋人", "恋人"), | ||
| 85 | ] | ||
| 86 | for raw, want in cases: | ||
| 87 | assert tn.strip_clip_markers(raw) == want, raw | ||
| 88 | |||
| 89 | |||
| 90 | def test_is_version_marker(): | ||
| 91 | cases = [ | ||
| 92 | ("DJ版", True), | ||
| 93 | ("钢琴版", True), | ||
| 94 | ("女生版", True), | ||
| 95 | ("完整版", False), | ||
| 96 | ("原版", False), | ||
| 97 | ("ass version", True), | ||
| 98 | ("TV Version", True), | ||
| 99 | ("Extended Version", True), | ||
| 100 | ("live", True), | ||
| 101 | ("Live", True), | ||
| 102 | ("伴奏", True), | ||
| 103 | ("对唱", True), | ||
| 104 | ("dj", True), | ||
| 105 | ("remix", True), | ||
| 106 | ("0.8x", True), | ||
| 107 | ("1.1X", True), | ||
| 108 | ("×0.93", True), | ||
| 109 | ("0.8倍", True), | ||
| 110 | ("1.2倍速", True), | ||
| 111 | ("天使", False), | ||
| 112 | ("Dum-Dum", False), | ||
| 113 | ("周杰伦", False), | ||
| 114 | ("", False), | ||
| 115 | ] | ||
| 116 | for raw, want in cases: | ||
| 117 | assert tn.is_version_marker(raw) == want, raw | ||
| 118 | |||
| 119 | |||
| 120 | def test_split_names_clip_authors(): | ||
| 121 | cases = [ | ||
| 122 | ("唐伯虎Annie、伯爵Johnny", 2), | ||
| 123 | ("梨香JZH & 口古口古", 2), | ||
| 124 | ("派偉俊/mac ova sea", 2), | ||
| 125 | ("李荣浩", 1), | ||
| 126 | ] | ||
| 127 | for raw, want in cases: | ||
| 128 | got = tn.split_names(raw) | ||
| 129 | assert len(got or []) == want, raw | ||
| 130 | |||
| 131 | |||
| 132 | def test_normalize_set(): | ||
| 133 | a = tn.normalize_set(["方文山", "周杰伦"]) | ||
| 134 | b = tn.normalize_set(["周杰伦", "方文山"]) | ||
| 135 | assert a == b | ||
| 136 | assert tn.normalize_set(None) is None |
python/title-norm/title_norm/__init__.py
0 → 100644
This diff is collapsed.
Click to expand it.
sync_singer_hot_songs.py
0 → 100644
This diff is collapsed.
Click to expand it.
| ... | @@ -2,7 +2,7 @@ from pathlib import Path | ... | @@ -2,7 +2,7 @@ from pathlib import Path |
| 2 | 2 | ||
| 3 | from openpyxl import load_workbook | 3 | from openpyxl import load_workbook |
| 4 | 4 | ||
| 5 | from check_record_titles import combine_rows, fetch_crawler_rows, fetch_record_names, write_excel | 5 | from check_record_titles import combine_rows, fetch_crawler_rows, fetch_spider_titles, write_excel |
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | class Cursor: | 8 | class Cursor: |
| ... | @@ -42,7 +42,7 @@ def test_fetch_crawler_rows_joins_all_platform_tables(): | ... | @@ -42,7 +42,7 @@ def test_fetch_crawler_rows_joins_all_platform_tables(): |
| 42 | "platform": "1", | 42 | "platform": "1", |
| 43 | "platform_song_id": 101, | 43 | "platform_song_id": 101, |
| 44 | "recording_id": "recording-1", | 44 | "recording_id": "recording-1", |
| 45 | "title": "同名", | 45 | "crawler_title": "同名", |
| 46 | }] | 46 | }] |
| 47 | assert "WHERE ysr.is_archive_push = TRUE" in cursor.calls[0][0] | 47 | assert "WHERE ysr.is_archive_push = TRUE" in cursor.calls[0][0] |
| 48 | 48 | ||
| ... | @@ -58,31 +58,35 @@ def test_fetch_crawler_rows_rejects_unknown_table(): | ... | @@ -58,31 +58,35 @@ def test_fetch_crawler_rows_rejects_unknown_table(): |
| 58 | raise AssertionError("应拒绝非白名单表") | 58 | raise AssertionError("应拒绝非白名单表") |
| 59 | 59 | ||
| 60 | 60 | ||
| 61 | def test_fetch_record_names_batches_queries(): | 61 | def test_fetch_spider_titles_batches_queries_by_platform(): |
| 62 | connection = Connection([ | 62 | connection = Connection([ |
| 63 | [{"id": 1, "record_name": "甲"}, {"id": 2, "record_name": "乙"}], | 63 | [{"id": 101, "title": "甲"}], |
| 64 | [{"id": 3, "record_name": "丙"}], | 64 | [{"id": 102, "title": "乙"}], |
| 65 | ]) | 65 | ]) |
| 66 | crawler_rows = [ | ||
| 67 | {"platform": "1", "platform_song_id": 101}, | ||
| 68 | {"platform": "2", "platform_song_id": 102}, | ||
| 69 | ] | ||
| 66 | 70 | ||
| 67 | names = fetch_record_names(connection, [3, 1, 2, 2], batch_size=2) | 71 | titles = fetch_spider_titles(connection, crawler_rows, batch_size=2) |
| 68 | 72 | ||
| 69 | assert names == {1: "甲", 2: "乙", 3: "丙"} | 73 | assert titles == {("1", 101): "甲", ("2", 102): "乙"} |
| 70 | 74 | ||
| 71 | 75 | ||
| 72 | def test_excel_marks_only_exact_non_null_matches_green(tmp_path: Path): | 76 | def test_excel_marks_only_exact_non_null_matches_green(tmp_path: Path): |
| 73 | crawler_rows = [ | 77 | crawler_rows = [ |
| 74 | {"record_id": 1, "platform": "1", "platform_song_id": 101, "recording_id": "r1", "title": "相同"}, | 78 | {"record_id": 1, "platform": "1", "platform_song_id": 101, "recording_id": "r1", "crawler_title": "相同"}, |
| 75 | {"record_id": 2, "platform": "2", "platform_song_id": 102, "recording_id": "r2", "title": " 名称 "}, | 79 | {"record_id": 2, "platform": "2", "platform_song_id": 102, "recording_id": "r2", "crawler_title": " 名称 "}, |
| 76 | {"record_id": 3, "platform": "4", "platform_song_id": 103, "recording_id": None, "title": None}, | 80 | {"record_id": 3, "platform": "4", "platform_song_id": 103, "recording_id": None, "crawler_title": None}, |
| 77 | ] | 81 | ] |
| 78 | rows = combine_rows(crawler_rows, {1: "相同", 2: "名称", 3: None}) | 82 | rows = combine_rows(crawler_rows, {("1", 101): "相同", ("2", 102): "名称"}) |
| 79 | output = tmp_path / "check.xlsx" | 83 | output = tmp_path / "check.xlsx" |
| 80 | 84 | ||
| 81 | write_excel(rows, output) | 85 | write_excel(rows, output) |
| 82 | 86 | ||
| 83 | worksheet = load_workbook(output).active | 87 | worksheet = load_workbook(output).active |
| 84 | assert [worksheet.cell(1, column).value for column in range(1, 7)] == [ | 88 | assert [worksheet.cell(1, column).value for column in range(1, 7)] == [ |
| 85 | "record_id", "record_name", "title", "platform", "platform_song_id", "recording_id", | 89 | "record_id", "spider_title", "crawler_title", "platform", "platform_song_id", "recording_id", |
| 86 | ] | 90 | ] |
| 87 | assert worksheet["A2"].fill.fgColor.rgb == "00C6EFCE" | 91 | assert worksheet["A2"].fill.fgColor.rgb == "00C6EFCE" |
| 88 | assert worksheet["A3"].fill.fill_type is None | 92 | assert worksheet["A3"].fill.fill_type is None | ... | ... |
test_fix_record_titles.py
0 → 100644
| 1 | from openpyxl import load_workbook | ||
| 2 | from datetime import datetime, timezone | ||
| 3 | |||
| 4 | from fix_record_titles import ( | ||
| 5 | InputRow, | ||
| 6 | _archive_recording_sql, | ||
| 7 | _archive_source_sql, | ||
| 8 | _crawler_sql, | ||
| 9 | _rollback_sql, | ||
| 10 | _row_value, | ||
| 11 | build_backup_records, | ||
| 12 | build_report_rows, | ||
| 13 | build_fix_rows, | ||
| 14 | fetch_input_rows, | ||
| 15 | load_backup, | ||
| 16 | write_backup_atomic, | ||
| 17 | write_report, | ||
| 18 | ) | ||
| 19 | |||
| 20 | |||
| 21 | class Cursor: | ||
| 22 | def __init__(self, rows): | ||
| 23 | self.rows = rows | ||
| 24 | self.calls = [] | ||
| 25 | |||
| 26 | def __enter__(self): | ||
| 27 | return self | ||
| 28 | |||
| 29 | def __exit__(self, exc_type, exc_value, traceback): | ||
| 30 | return False | ||
| 31 | |||
| 32 | def execute(self, sql, params=None): | ||
| 33 | self.calls.append((sql, params)) | ||
| 34 | |||
| 35 | def fetchall(self): | ||
| 36 | return self.rows | ||
| 37 | |||
| 38 | |||
| 39 | class Connection: | ||
| 40 | def __init__(self, rows): | ||
| 41 | self.raw_cursor = Cursor(rows) | ||
| 42 | |||
| 43 | def cursor(self): | ||
| 44 | return self.raw_cursor | ||
| 45 | |||
| 46 | |||
| 47 | def test_fetch_input_rows_reads_archive_push_relations_directly(): | ||
| 48 | connection = Connection([ | ||
| 49 | (1, "1", 101, "REC-1"), | ||
| 50 | (2, "2", 102, "REC-2"), | ||
| 51 | ]) | ||
| 52 | |||
| 53 | rows, stats = fetch_input_rows(connection) | ||
| 54 | |||
| 55 | assert rows == [ | ||
| 56 | InputRow(1, "1", 101, "REC-1"), | ||
| 57 | InputRow(2, "2", 102, "REC-2"), | ||
| 58 | ] | ||
| 59 | assert stats == {"total": 2, "missing_key": 0, "unsupported_platform": 0} | ||
| 60 | sql = connection.raw_cursor.calls[0][0] | ||
| 61 | assert "FROM yinyan_song_records" in sql | ||
| 62 | assert "WHERE is_archive_push = TRUE" in sql | ||
| 63 | |||
| 64 | |||
| 65 | def test_fetch_input_rows_applies_limit(): | ||
| 66 | connection = Connection([(1, "1", 101, "REC-1")]) | ||
| 67 | |||
| 68 | fetch_input_rows(connection, limit=10) | ||
| 69 | |||
| 70 | sql, params = connection.raw_cursor.calls[0] | ||
| 71 | assert "LIMIT %s" in sql | ||
| 72 | assert params == (10,) | ||
| 73 | |||
| 74 | |||
| 75 | def test_build_fix_rows_uses_spider_title_for_normalization(): | ||
| 76 | input_row = InputRow(1, "1", 101, "REC-1") | ||
| 77 | |||
| 78 | rows, fix_stats = build_fix_rows( | ||
| 79 | [input_row], | ||
| 80 | {("1", 101): "七里香(Live版)"}, | ||
| 81 | ) | ||
| 82 | |||
| 83 | assert fix_stats["missing_spider"] == 0 | ||
| 84 | assert rows[0].spider_title == "七里香(Live版)" | ||
| 85 | assert rows[0].title_norm == "七里香" | ||
| 86 | assert rows[0].version == "live" | ||
| 87 | assert _row_value(rows[0], "archive_platform") == "qqmusic" | ||
| 88 | assert _row_value(rows[0], "archive_version") == "live" | ||
| 89 | |||
| 90 | |||
| 91 | def test_build_fix_rows_skips_missing_spider_title(): | ||
| 92 | input_row = InputRow(1, "4", 101, "REC-1") | ||
| 93 | |||
| 94 | rows, fix_stats = build_fix_rows([input_row], {}) | ||
| 95 | |||
| 96 | assert rows == [] | ||
| 97 | assert fix_stats["missing_spider"] == 1 | ||
| 98 | |||
| 99 | |||
| 100 | def test_build_fix_rows_temporarily_excludes_exact_spider_title(): | ||
| 101 | excluded = 'Waiting On A Wish (From "Disney\'s Snow White"/Soundtrack Version|Reprise)' | ||
| 102 | input_rows = [ | ||
| 103 | InputRow(1, "1", 101, "REC-1"), | ||
| 104 | InputRow(2, "1", 102, "REC-2"), | ||
| 105 | ] | ||
| 106 | spider_titles = { | ||
| 107 | ("1", 101): excluded, | ||
| 108 | ("1", 102): "Waiting On A Wish", | ||
| 109 | } | ||
| 110 | |||
| 111 | rows, fix_stats = build_fix_rows( | ||
| 112 | input_rows, | ||
| 113 | spider_titles, | ||
| 114 | exclude_titles={excluded}, | ||
| 115 | ) | ||
| 116 | |||
| 117 | assert [row.platform_song_id for row in rows] == [102] | ||
| 118 | assert fix_stats["excluded_title"] == 1 | ||
| 119 | |||
| 120 | |||
| 121 | def test_update_sql_uses_spider_values_and_only_updates_differences(): | ||
| 122 | recording_sql, recording_params = _archive_recording_sql(1, True) | ||
| 123 | source_sql, source_params = _archive_source_sql(1, True) | ||
| 124 | crawler_sql, crawler_params = _crawler_sql("crawler_qqmusic_songs", 1, True) | ||
| 125 | |||
| 126 | assert recording_params == ["recording_id", "spider_title", "title_norm", "archive_version"] | ||
| 127 | assert "title_norm = source.new_title_norm" in recording_sql | ||
| 128 | assert "IS DISTINCT FROM source.new_title" in recording_sql | ||
| 129 | assert "target.platform_song_id = source.platform_song_id" in source_sql | ||
| 130 | assert "spider_title" in source_params | ||
| 131 | assert crawler_params == ["platform_song_id", "spider_title", "version"] | ||
| 132 | assert "source.platform_song_id::bigint" in crawler_sql | ||
| 133 | assert "version = source.new_version" in crawler_sql | ||
| 134 | assert "RETURNING target.recording_id" in recording_sql | ||
| 135 | assert "RETURNING target.platform_song_id" in crawler_sql | ||
| 136 | locked_sql, _ = _archive_recording_sql(1, False, lock=True) | ||
| 137 | assert "FOR UPDATE OF target" in locked_sql | ||
| 138 | |||
| 139 | |||
| 140 | def test_report_lists_only_returned_target_keys(tmp_path): | ||
| 141 | fix = build_fix_rows( | ||
| 142 | [InputRow(1, "1", 101, "REC-1")], | ||
| 143 | {("1", 101): "七里香(Live版)"}, | ||
| 144 | )[0][0] | ||
| 145 | matched = { | ||
| 146 | "archive_recording": [ | ||
| 147 | ("REC-1", "旧标题", "七里香(Live版)", "旧标题", "七里香", None, "live"), | ||
| 148 | ], | ||
| 149 | "archive_recording_platform_source": [ | ||
| 150 | ("REC-1", "qqmusic", "101", "旧标题", "七里香(Live版)", None, "七里香", None, "live"), | ||
| 151 | ], | ||
| 152 | "crawler_qqmusic_songs": [ | ||
| 153 | (101, "旧标题", "七里香(Live版)", "", "live"), | ||
| 154 | ], | ||
| 155 | "crawler_kugou_songs": [], | ||
| 156 | "crawler_netease_songs": [], | ||
| 157 | } | ||
| 158 | |||
| 159 | rows = build_report_rows([fix], matched, apply=False) | ||
| 160 | output = tmp_path / "report.xlsx" | ||
| 161 | write_report(output, rows) | ||
| 162 | |||
| 163 | worksheet = load_workbook(output).active | ||
| 164 | assert len(rows) == 3 | ||
| 165 | assert worksheet.max_row == 4 | ||
| 166 | assert worksheet["A1"].value == "database" | ||
| 167 | assert worksheet["H1"].value == "old_title" | ||
| 168 | assert worksheet["I1"].value == "new_title" | ||
| 169 | assert worksheet["H2"].value == "旧标题" | ||
| 170 | assert worksheet["I2"].value == "七里香(Live版)" | ||
| 171 | assert worksheet["J3"].value == "<NULL>" | ||
| 172 | assert worksheet["J3"].fill.fgColor.rgb == "00FFC7CE" | ||
| 173 | assert worksheet["K3"].fill.fgColor.rgb == "00C6EFCE" | ||
| 174 | assert worksheet["N2"].value == "would_update" | ||
| 175 | |||
| 176 | |||
| 177 | def test_backup_round_trip_preserves_null_empty_and_updated_at(tmp_path): | ||
| 178 | fix = build_fix_rows( | ||
| 179 | [InputRow(1, "1", 101, "REC-1")], | ||
| 180 | {("1", 101): "七里香(Live版)"}, | ||
| 181 | )[0][0] | ||
| 182 | updated_at = datetime(2026, 7, 15, 10, 0, tzinfo=timezone.utc) | ||
| 183 | preview = { | ||
| 184 | "archive_recording": [ | ||
| 185 | ("REC-1", "旧标题", "七里香(Live版)", None, "七里香", None, "live", updated_at), | ||
| 186 | ], | ||
| 187 | "archive_recording_platform_source": [], | ||
| 188 | "crawler_qqmusic_songs": [ | ||
| 189 | (101, "旧标题", "七里香(Live版)", "", "live", updated_at.replace(tzinfo=None)), | ||
| 190 | ], | ||
| 191 | "crawler_kugou_songs": [], | ||
| 192 | "crawler_netease_songs": [], | ||
| 193 | } | ||
| 194 | records = build_backup_records([fix], preview, "run-1") | ||
| 195 | path = tmp_path / "backup.jsonl" | ||
| 196 | |||
| 197 | write_backup_atomic(path, records, "run-1") | ||
| 198 | metadata, loaded = load_backup(path) | ||
| 199 | |||
| 200 | assert metadata["change_count"] == 2 | ||
| 201 | assert loaded[0]["old"]["title_norm"] is None | ||
| 202 | assert loaded[1]["old"]["version"] == "" | ||
| 203 | assert loaded[0]["old"]["updated_at"] == updated_at.isoformat() | ||
| 204 | |||
| 205 | try: | ||
| 206 | write_backup_atomic(path, records, "run-2") | ||
| 207 | except FileExistsError: | ||
| 208 | pass | ||
| 209 | else: | ||
| 210 | raise AssertionError("已有备份不应被覆盖") | ||
| 211 | |||
| 212 | |||
| 213 | def test_rollback_sql_restores_old_values_with_new_value_guard(): | ||
| 214 | archive_sql, _ = _rollback_sql("archive_recording", 1) | ||
| 215 | crawler_sql, _ = _rollback_sql("crawler_qqmusic_songs", 1) | ||
| 216 | |||
| 217 | assert "SET title = source.old_title" in archive_sql | ||
| 218 | assert "updated_at = source.old_updated_at::timestamptz" in archive_sql | ||
| 219 | assert "target.title IS NOT DISTINCT FROM source.new_title" in archive_sql | ||
| 220 | assert "target.title_norm IS NOT DISTINCT FROM source.new_title_norm" in archive_sql | ||
| 221 | assert "updated_at = source.old_updated_at::timestamp" in crawler_sql | ||
| 222 | assert "target.version IS NOT DISTINCT FROM source.new_version" in crawler_sql |
| ... | @@ -375,7 +375,7 @@ def test_run_imports_only_pending_yinyan_platform_record(monkeypatch): | ... | @@ -375,7 +375,7 @@ def test_run_imports_only_pending_yinyan_platform_record(monkeypatch): |
| 375 | monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: _Connection()) | 375 | monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: _Connection()) |
| 376 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 376 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 377 | monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection()) | 377 | monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection()) |
| 378 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 378 | monkeypatch.setattr(runner, 'get_test_crawler_conn', lambda: pg_conn) |
| 379 | monkeypatch.setattr(runner, 'get_oss_bucket', lambda: object()) | 379 | monkeypatch.setattr(runner, 'get_oss_bucket', lambda: object()) |
| 380 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 380 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 381 | monkeypatch.setattr(runner, 'fetch_pending_yinyan_song_records', lambda cur, batch_size, platforms: [ | 381 | monkeypatch.setattr(runner, 'fetch_pending_yinyan_song_records', lambda cur, batch_size, platforms: [ |
| ... | @@ -436,7 +436,7 @@ def test_run_retry_failed_resets_failures_and_restores_hk_sources(monkeypatch): | ... | @@ -436,7 +436,7 @@ def test_run_retry_failed_resets_failures_and_restores_hk_sources(monkeypatch): |
| 436 | monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: hk_conn) | 436 | monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: hk_conn) |
| 437 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 437 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 438 | monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection()) | 438 | monkeypatch.setattr(runner, 'get_spider_conn', lambda: _Connection()) |
| 439 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 439 | monkeypatch.setattr(runner, 'get_test_crawler_conn', lambda: pg_conn) |
| 440 | monkeypatch.setattr(runner, 'get_oss_bucket', lambda: object()) | 440 | monkeypatch.setattr(runner, 'get_oss_bucket', lambda: object()) |
| 441 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 441 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 442 | monkeypatch.setattr(runner, 'reset_failed_yinyan_song_records', reset) | 442 | monkeypatch.setattr(runner, 'reset_failed_yinyan_song_records', reset) |
| ... | @@ -456,7 +456,7 @@ def test_initialize_yinyan_song_records_inserts_primary_records(monkeypatch): | ... | @@ -456,7 +456,7 @@ def test_initialize_yinyan_song_records_inserts_primary_records(monkeypatch): |
| 456 | 456 | ||
| 457 | monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: _Connection()) | 457 | monkeypatch.setattr(runner, 'get_hk_songs_conn', lambda: _Connection()) |
| 458 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 458 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 459 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 459 | monkeypatch.setattr(runner, 'get_test_crawler_conn', lambda: pg_conn) |
| 460 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 460 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 461 | monkeypatch.setattr(runner, 'fetch_existing_yinyan_song_ids', lambda cur: set()) | 461 | monkeypatch.setattr(runner, 'fetch_existing_yinyan_song_ids', lambda cur: set()) |
| 462 | 462 | ||
| ... | @@ -501,7 +501,7 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( | ... | @@ -501,7 +501,7 @@ def test_initialize_yinyan_song_records2_writes_all_relations_then_deduplicates( |
| 501 | dedupe_calls = [] | 501 | dedupe_calls = [] |
| 502 | 502 | ||
| 503 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 503 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 504 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 504 | monkeypatch.setattr(runner, 'get_test_crawler_conn', lambda: pg_conn) |
| 505 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 505 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 506 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) | 506 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) |
| 507 | monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [ | 507 | monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [ |
| ... | @@ -532,7 +532,7 @@ def test_initialize_yinyan_song_records2_keeps_rows_without_valid_singers(monkey | ... | @@ -532,7 +532,7 @@ def test_initialize_yinyan_song_records2_keeps_rows_without_valid_singers(monkey |
| 532 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 532 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 533 | get_spider_conn = MagicMock(side_effect=AssertionError('records2 init must not query spider')) | 533 | get_spider_conn = MagicMock(side_effect=AssertionError('records2 init must not query spider')) |
| 534 | monkeypatch.setattr(runner, 'get_spider_conn', get_spider_conn) | 534 | monkeypatch.setattr(runner, 'get_spider_conn', get_spider_conn) |
| 535 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 535 | monkeypatch.setattr(runner, 'get_test_crawler_conn', lambda: pg_conn) |
| 536 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 536 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 537 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) | 537 | monkeypatch.setattr(runner, 'fetch_yinyan_song_ids', lambda cur: [10]) |
| 538 | monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [ | 538 | monkeypatch.setattr(runner, 'fetch_all_song_record_relations', lambda conn, song_ids: [ |
| ... | @@ -561,7 +561,7 @@ def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypa | ... | @@ -561,7 +561,7 @@ def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypa |
| 561 | updated = [] | 561 | updated = [] |
| 562 | 562 | ||
| 563 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) | 563 | monkeypatch.setattr(runner, 'get_source_conn', lambda: _Connection()) |
| 564 | monkeypatch.setattr(runner, 'get_pg_conn', lambda: pg_conn) | 564 | monkeypatch.setattr(runner, 'get_test_crawler_conn', lambda: pg_conn) |
| 565 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) | 565 | monkeypatch.setattr(runner, 'refresh_conn', lambda conn, name: conn) |
| 566 | monkeypatch.setattr(runner, 'fetch_yinyan_records_missing_platform', lambda cur, batch_size: [ | 566 | monkeypatch.setattr(runner, 'fetch_yinyan_records_missing_platform', lambda cur, batch_size: [ |
| 567 | {'song_id': 10, 'record_id': 100}, | 567 | {'song_id': 10, 'record_id': 100}, | ... | ... |
-
Please register or sign in to post a comment