refactor(etl_to_crawler): 优化QQ音乐入库流程并新增album_json字段
- 将BATCH_SIZE由4调整为1以支持更细粒度的数据处理 - 在runner中为QQ音乐歌曲构建完整album_json对象并序列化 - 修改upsert_qq_songs接口,新增album_json参数的传递和入库支持 - 在writer模块SQL插入语句中添加album字段,支持json类型数据写入 - 编写单元测试验证album_json字段正确生成与写入逻辑 - 兼容旧逻辑,album_json字段为空时不影响其他数据写入
Showing
5 changed files
with
111 additions
and
4 deletions
| ... | @@ -43,4 +43,4 @@ PLATFORM_KUGOU = '2' | ... | @@ -43,4 +43,4 @@ PLATFORM_KUGOU = '2' |
| 43 | PLATFORM_NETEASE = '4' | 43 | PLATFORM_NETEASE = '4' |
| 44 | PLATFORMS = [PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE] | 44 | PLATFORMS = [PLATFORM_QQ, PLATFORM_KUGOU, PLATFORM_NETEASE] |
| 45 | 45 | ||
| 46 | BATCH_SIZE = 4 | 46 | BATCH_SIZE = 1 | ... | ... |
| ... | @@ -133,8 +133,22 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): | ... | @@ -133,8 +133,22 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): |
| 133 | 133 | ||
| 134 | # 写入 album | 134 | # 写入 album |
| 135 | album_id = None | 135 | album_id = None |
| 136 | album_json = None | ||
| 136 | if sp.get('album_id'): | 137 | if sp.get('album_id'): |
| 137 | album_id = sp['album_id'] | 138 | album_id = sp['album_id'] |
| 139 | album_payload = { | ||
| 140 | 'id': sp['album_id'], | ||
| 141 | 'mid': sp.get('album_mid') or '', | ||
| 142 | 'cover': album_cover, | ||
| 143 | 'title': sp.get('album_title') or '', | ||
| 144 | 'intro': sp.get('album_intro'), | ||
| 145 | 'type': sp.get('album_type') or '', | ||
| 146 | 'company_id': sp.get('company_id') or 0, | ||
| 147 | 'company': sp.get('company') or '', | ||
| 148 | 'is_owner': sp.get('is_owner') or 0, | ||
| 149 | 'published_at': str(sp.get('album_published_at')) if sp.get('album_published_at') else None, | ||
| 150 | } | ||
| 151 | album_json = json.dumps(album_payload, ensure_ascii=False) | ||
| 138 | upsert_qq_albums(pg_cur, [{ | 152 | upsert_qq_albums(pg_cur, [{ |
| 139 | 'id': sp['album_id'], 'mid': sp.get('album_mid') or '', | 153 | 'id': sp['album_id'], 'mid': sp.get('album_mid') or '', |
| 140 | 'cover': album_cover, 'title': sp.get('album_title') or '', | 154 | 'cover': album_cover, 'title': sp.get('album_title') or '', |
| ... | @@ -167,6 +181,7 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): | ... | @@ -167,6 +181,7 @@ def _process_qq(hk_row: dict, pr: dict, spider_conn, pg_cur, bucket, base_url): |
| 167 | 'platform_song_id': platform_song_id, | 181 | 'platform_song_id': platform_song_id, |
| 168 | 'mid': mid, | 182 | 'mid': mid, |
| 169 | 'album_id': album_id, | 183 | 'album_id': album_id, |
| 184 | 'album_json': album_json, | ||
| 170 | 'cover': cover_url, | 185 | 'cover': cover_url, |
| 171 | 'title': title, | 186 | 'title': title, |
| 172 | 'version': version, | 187 | 'version': version, | ... | ... |
| ... | @@ -138,9 +138,9 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: | ... | @@ -138,9 +138,9 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: |
| 138 | INSERT INTO crawler_qqmusic_songs | 138 | INSERT INTO crawler_qqmusic_songs |
| 139 | (id, platform_song_id, mid, album_id, cover, title, name, duration, | 139 | (id, platform_song_id, mid, album_id, cover, title, name, duration, |
| 140 | lyric, composer_name, lyricist_name, url, lyric_url, | 140 | lyric, composer_name, lyricist_name, url, lyric_url, |
| 141 | platform_index_url, published_at, singers, status, created_at, updated_at, | 141 | platform_index_url, published_at, album, singers, status, created_at, updated_at, |
| 142 | version, audio_md5, provider_name, crawler_source_data) | 142 | version, audio_md5, provider_name, crawler_source_data) |
| 143 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW(), %s, %s, %s, %s::json) | 143 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::json, %s::jsonb, 0, NOW(), NOW(), %s, %s, %s, %s::json) |
| 144 | ON CONFLICT (platform_song_id) DO NOTHING | 144 | ON CONFLICT (platform_song_id) DO NOTHING |
| 145 | """ | 145 | """ |
| 146 | rows = [( | 146 | rows = [( |
| ... | @@ -148,7 +148,7 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: | ... | @@ -148,7 +148,7 @@ def upsert_qq_songs(cur, songs: list[dict]) -> None: |
| 148 | s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0, | 148 | s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0, |
| 149 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), | 149 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), |
| 150 | s.get('url', ''), s.get('lyric_url'), | 150 | s.get('url', ''), s.get('lyric_url'), |
| 151 | s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), | 151 | s.get('platform_index_url'), s.get('published_at'), s.get('album_json'), s.get('singers_json', '[]'), |
| 152 | s.get('version'), | 152 | s.get('version'), |
| 153 | s.get('audio_md5'), | 153 | s.get('audio_md5'), |
| 154 | s.get('provider_name'), s.get('crawler_source_data'), | 154 | s.get('provider_name'), s.get('crawler_source_data'), | ... | ... |
| ... | @@ -169,6 +169,70 @@ def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypa | ... | @@ -169,6 +169,70 @@ def test_backfill_yinyan_record_platforms_updates_missing_platform_rows(monkeypa |
| 169 | assert pg_conn.commits == 1 | 169 | assert pg_conn.commits == 1 |
| 170 | 170 | ||
| 171 | 171 | ||
| 172 | def test_process_qq_builds_album_json_for_song_insert(monkeypatch): | ||
| 173 | pg_cur = MagicMock() | ||
| 174 | pg_cur.fetchone.return_value = ('song-uuid',) | ||
| 175 | inserted_songs = [] | ||
| 176 | |||
| 177 | monkeypatch.setattr(runner, 'fetch_qq_songs', lambda conn, mids: { | ||
| 178 | 'qq-mid': { | ||
| 179 | 'id': 200, | ||
| 180 | 'mid': 'qq-mid', | ||
| 181 | 'album_id': 20, | ||
| 182 | 'album_mid': 'album-mid', | ||
| 183 | 'album_cover': 'https://example.com/album.jpg', | ||
| 184 | 'album_title': '专辑', | ||
| 185 | 'album_intro': '简介', | ||
| 186 | 'album_type': '专辑类型', | ||
| 187 | 'company_id': 7, | ||
| 188 | 'company': '唱片公司', | ||
| 189 | 'is_owner': 1, | ||
| 190 | 'album_published_at': '2020-01-01', | ||
| 191 | 'cover': 'https://example.com/cover.jpg', | ||
| 192 | 'title': '化风行万里 (DJ默涵版)', | ||
| 193 | 'duration': 180, | ||
| 194 | 'lyric': '[00:01.00]歌词', | ||
| 195 | 'composer_name': '曲作者', | ||
| 196 | 'lyricist_name': '词作者', | ||
| 197 | 'platform_index_url': None, | ||
| 198 | 'published_at': '2020-01-02', | ||
| 199 | }, | ||
| 200 | }) | ||
| 201 | monkeypatch.setattr(runner, 'fetch_qq_singers', lambda conn, song_ids: {}) | ||
| 202 | monkeypatch.setattr(runner, '_safe_transfer', lambda url, oss_key, bucket, base_url: url) | ||
| 203 | monkeypatch.setattr(runner, '_safe_transfer_audio', lambda url, oss_key, bucket, base_url: (url, 'audio-md5')) | ||
| 204 | monkeypatch.setattr(runner, 'upsert_qq_singers', lambda cur, singers: None) | ||
| 205 | monkeypatch.setattr(runner, 'upsert_qq_albums', lambda cur, albums: None) | ||
| 206 | monkeypatch.setattr(runner, 'upsert_qq_songs', lambda cur, songs: inserted_songs.extend(songs)) | ||
| 207 | monkeypatch.setattr(runner, 'upsert_qq_singer_songs', lambda cur, pairs: None) | ||
| 208 | monkeypatch.setattr(runner, 'upsert_qq_singer_albums', lambda cur, pairs: None) | ||
| 209 | |||
| 210 | runner._process_qq( | ||
| 211 | { | ||
| 212 | 'name': '词曲名', | ||
| 213 | 'audio_url': 'https://example.com/audio.mp3', | ||
| 214 | 'lyrics_url': 'https://example.com/lyric.lrc', | ||
| 215 | 'cover_url': '', | ||
| 216 | 'composer': '词曲曲作者', | ||
| 217 | 'lyricist': '词曲词作者', | ||
| 218 | 'issue_time': '2019-01-01', | ||
| 219 | 'song_time': 120, | ||
| 220 | }, | ||
| 221 | {'platform_unique_key': 'qq-mid', 'platform_mid': '200'}, | ||
| 222 | spider_conn=object(), | ||
| 223 | pg_cur=pg_cur, | ||
| 224 | bucket=object(), | ||
| 225 | base_url='https://bucket.example.com', | ||
| 226 | ) | ||
| 227 | |||
| 228 | assert inserted_songs[0]['album_json'] | ||
| 229 | assert '"id": 20' in inserted_songs[0]['album_json'] | ||
| 230 | assert '"mid": "album-mid"' in inserted_songs[0]['album_json'] | ||
| 231 | assert '"title": "专辑"' in inserted_songs[0]['album_json'] | ||
| 232 | assert inserted_songs[0]['title'] == '化风行万里' | ||
| 233 | assert inserted_songs[0]['version'] == 'DJ默涵版' | ||
| 234 | |||
| 235 | |||
| 172 | def test_process_netease_builds_album_json_for_song_insert(monkeypatch): | 236 | def test_process_netease_builds_album_json_for_song_insert(monkeypatch): |
| 173 | pg_cur = MagicMock() | 237 | pg_cur = MagicMock() |
| 174 | pg_cur.fetchone.return_value = ('song-uuid',) | 238 | pg_cur.fetchone.return_value = ('song-uuid',) | ... | ... |
| ... | @@ -130,6 +130,34 @@ def test_upsert_netease_songs_writes_album_json_column(): | ... | @@ -130,6 +130,34 @@ def test_upsert_netease_songs_writes_album_json_column(): |
| 130 | assert rows[0][14] == '{"id": 20, "title": "专辑"}' | 130 | assert rows[0][14] == '{"id": 20, "title": "专辑"}' |
| 131 | 131 | ||
| 132 | 132 | ||
| 133 | def test_upsert_qq_songs_writes_album_json_column(): | ||
| 134 | cur = MagicMock() | ||
| 135 | upsert_qq_songs(cur, [{ | ||
| 136 | 'song_uuid': 'song-uuid', | ||
| 137 | 'platform_song_id': 200, | ||
| 138 | 'mid': 'song-mid', | ||
| 139 | 'album_id': 20, | ||
| 140 | 'album_json': '{"id": 20, "title": "专辑"}', | ||
| 141 | 'cover': 'https://example.com/cover.jpg', | ||
| 142 | 'title': '标题', | ||
| 143 | 'name': '词曲名', | ||
| 144 | 'duration': 180, | ||
| 145 | 'lyric': '歌词', | ||
| 146 | 'composer_name': '曲作者', | ||
| 147 | 'lyricist_name': '词作者', | ||
| 148 | 'url': 'https://example.com/audio.mp3', | ||
| 149 | 'lyric_url': 'https://example.com/lyric.lrc', | ||
| 150 | 'platform_index_url': 'https://y.qq.com/n/ryqq/songDetail/song-mid', | ||
| 151 | 'published_at': '2020-01-01', | ||
| 152 | 'singers_json': '[]', | ||
| 153 | }]) | ||
| 154 | |||
| 155 | sql, rows = cur.executemany.call_args[0] | ||
| 156 | assert 'album, singers' in sql | ||
| 157 | assert '%s::json, %s::jsonb' in sql | ||
| 158 | assert rows[0][15] == '{"id": 20, "title": "专辑"}' | ||
| 159 | |||
| 160 | |||
| 133 | def test_upsert_kugou_songs_writes_provider_and_source_data(): | 161 | def test_upsert_kugou_songs_writes_provider_and_source_data(): |
| 134 | cur = MagicMock() | 162 | cur = MagicMock() |
| 135 | upsert_kugou_songs(cur, [{ | 163 | upsert_kugou_songs(cur, [{ | ... | ... |
-
Please register or sign in to post a comment