feat(etl): 实现三平台 PostgreSQL 写入函数
Showing
2 changed files
with
272 additions
and
0 deletions
etl_to_crawler/writer.py
0 → 100644
| 1 | import uuid | ||
| 2 | |||
| 3 | |||
| 4 | # ─── QQ Music ──────────────────────────────────────────────────────────────── | ||
| 5 | |||
| 6 | def upsert_qq_singers(cur, singers: list[dict]) -> None: | ||
| 7 | if not singers: | ||
| 8 | return | ||
| 9 | sql = """ | ||
| 10 | INSERT INTO crawler_qqmusic_singers | ||
| 11 | (id, mid, name, avatar, sex, area, "index", intro, home_url, created_at, updated_at) | ||
| 12 | VALUES (%s, %s, %s, %s, %s::singer_sex, %s::singer_area, %s::singer_index, %s, %s, NOW(), NOW()) | ||
| 13 | ON CONFLICT (id) DO NOTHING | ||
| 14 | """ | ||
| 15 | rows = [( | ||
| 16 | s['id'], s['mid'], s['name'], s.get('avatar', ''), | ||
| 17 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', | ||
| 18 | s.get('intro'), s.get('home_url'), | ||
| 19 | ) for s in singers] | ||
| 20 | cur.executemany(sql, rows) | ||
| 21 | |||
| 22 | |||
| 23 | def upsert_qq_albums(cur, albums: list[dict]) -> None: | ||
| 24 | if not albums: | ||
| 25 | return | ||
| 26 | sql = """ | ||
| 27 | INSERT INTO crawler_qqmusic_albums | ||
| 28 | (id, mid, cover, title, intro, type, company_id, company, is_owner, published_at, created_at, updated_at) | ||
| 29 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) | ||
| 30 | ON CONFLICT (id) DO NOTHING | ||
| 31 | """ | ||
| 32 | rows = [( | ||
| 33 | a['id'], a.get('mid', ''), a.get('cover', ''), a.get('title', ''), | ||
| 34 | a.get('intro'), a.get('type', ''), a.get('company_id', 0) or 0, | ||
| 35 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), | ||
| 36 | ) for a in albums] | ||
| 37 | cur.executemany(sql, rows) | ||
| 38 | |||
| 39 | |||
| 40 | def upsert_qq_songs(cur, songs: list[dict]) -> None: | ||
| 41 | if not songs: | ||
| 42 | return | ||
| 43 | sql = """ | ||
| 44 | INSERT INTO crawler_qqmusic_songs | ||
| 45 | (id, platform_song_id, mid, album_id, cover, title, name, duration, | ||
| 46 | lyric, composer_name, lyricist_name, url, lyric_url, | ||
| 47 | platform_index_url, published_at, singers, status, created_at, updated_at) | ||
| 48 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW()) | ||
| 49 | ON CONFLICT (platform_song_id) DO NOTHING | ||
| 50 | """ | ||
| 51 | rows = [( | ||
| 52 | s['song_uuid'], s['platform_song_id'], s['mid'], s.get('album_id'), | ||
| 53 | s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0, | ||
| 54 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), | ||
| 55 | s.get('url', ''), s.get('lyric_url'), | ||
| 56 | s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), | ||
| 57 | ) for s in songs] | ||
| 58 | cur.executemany(sql, rows) | ||
| 59 | |||
| 60 | |||
| 61 | def upsert_qq_singer_songs(cur, pairs: list[tuple]) -> None: | ||
| 62 | """pairs: [(singer_id, song_uuid), ...]""" | ||
| 63 | if not pairs: | ||
| 64 | return | ||
| 65 | sql = """ | ||
| 66 | INSERT INTO crawler_qqmusic_singer_songs (id, singer_id, song_id) | ||
| 67 | VALUES (%s, %s, %s) | ||
| 68 | ON CONFLICT (singer_id, song_id) DO NOTHING | ||
| 69 | """ | ||
| 70 | rows = [(str(uuid.uuid4()), singer_id, song_uuid) for singer_id, song_uuid in pairs] | ||
| 71 | cur.executemany(sql, rows) | ||
| 72 | |||
| 73 | |||
| 74 | def upsert_qq_singer_albums(cur, pairs: list[tuple]) -> None: | ||
| 75 | """pairs: [(singer_id, album_id), ...]""" | ||
| 76 | if not pairs: | ||
| 77 | return | ||
| 78 | sql = """ | ||
| 79 | INSERT INTO crawler_qqmusic_singer_albums (id, singer_id, album_id) | ||
| 80 | VALUES (%s, %s, %s) | ||
| 81 | ON CONFLICT (singer_id, album_id) DO NOTHING | ||
| 82 | """ | ||
| 83 | rows = [(str(uuid.uuid4()), singer_id, album_id) for singer_id, album_id in pairs] | ||
| 84 | cur.executemany(sql, rows) | ||
| 85 | |||
| 86 | |||
| 87 | # ─── Kugou ─────────────────────────────────────────────────────────────────── | ||
| 88 | |||
| 89 | def upsert_kugou_singers(cur, singers: list[dict]) -> None: | ||
| 90 | if not singers: | ||
| 91 | return | ||
| 92 | sql = """ | ||
| 93 | INSERT INTO crawler_kugou_singers | ||
| 94 | (id, name, avatar, sex, area, "index", intro, home_url, created_at, updated_at) | ||
| 95 | VALUES (%s, %s, %s, %s::kugou_singer_sex, %s::kugou_singer_area, %s::kugou_singer_index, %s, %s, NOW(), NOW()) | ||
| 96 | ON CONFLICT (id) DO NOTHING | ||
| 97 | """ | ||
| 98 | rows = [( | ||
| 99 | s['id'], s['name'], s.get('avatar', ''), | ||
| 100 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', | ||
| 101 | s.get('intro'), s.get('home_url', ''), | ||
| 102 | ) for s in singers] | ||
| 103 | cur.executemany(sql, rows) | ||
| 104 | |||
| 105 | |||
| 106 | def upsert_kugou_albums(cur, albums: list[dict]) -> None: | ||
| 107 | if not albums: | ||
| 108 | return | ||
| 109 | sql = """ | ||
| 110 | INSERT INTO crawler_kugou_albums | ||
| 111 | (id, cover, title, intro, type, company_id, company, is_owner, published_at, created_at, updated_at) | ||
| 112 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) | ||
| 113 | ON CONFLICT (id) DO NOTHING | ||
| 114 | """ | ||
| 115 | rows = [( | ||
| 116 | a['id'], a.get('cover', ''), a.get('title', ''), a.get('intro'), | ||
| 117 | a.get('type', ''), a.get('company_id', 0) or 0, | ||
| 118 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), | ||
| 119 | ) for a in albums] | ||
| 120 | cur.executemany(sql, rows) | ||
| 121 | |||
| 122 | |||
| 123 | def upsert_kugou_songs(cur, songs: list[dict]) -> None: | ||
| 124 | if not songs: | ||
| 125 | return | ||
| 126 | sql = """ | ||
| 127 | INSERT INTO crawler_kugou_songs | ||
| 128 | (id, platform_song_id, hash, album_audio_id, album_id, cover, title, name, duration, | ||
| 129 | lyric, composer_name, lyricist_name, url, lyric_url, | ||
| 130 | platform_index_url, published_at, singers, status, created_at, updated_at) | ||
| 131 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW()) | ||
| 132 | ON CONFLICT (platform_song_id) DO NOTHING | ||
| 133 | """ | ||
| 134 | rows = [( | ||
| 135 | s['song_uuid'], s['platform_song_id'], s.get('hash', ''), | ||
| 136 | s.get('album_audio_id', 0) or 0, s.get('album_id'), | ||
| 137 | s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0, | ||
| 138 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), | ||
| 139 | s.get('url', ''), s.get('lyric_url'), | ||
| 140 | s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), | ||
| 141 | ) for s in songs] | ||
| 142 | cur.executemany(sql, rows) | ||
| 143 | |||
| 144 | |||
| 145 | def upsert_kugou_singer_songs(cur, pairs: list[tuple]) -> None: | ||
| 146 | if not pairs: | ||
| 147 | return | ||
| 148 | sql = """ | ||
| 149 | INSERT INTO crawler_kugou_singer_songs (id, singer_id, song_id) | ||
| 150 | VALUES (%s, %s, %s) | ||
| 151 | ON CONFLICT (singer_id, song_id) DO NOTHING | ||
| 152 | """ | ||
| 153 | cur.executemany(sql, [(str(uuid.uuid4()), s, sg) for s, sg in pairs]) | ||
| 154 | |||
| 155 | |||
| 156 | def upsert_kugou_singer_albums(cur, pairs: list[tuple]) -> None: | ||
| 157 | if not pairs: | ||
| 158 | return | ||
| 159 | sql = """ | ||
| 160 | INSERT INTO crawler_kugou_singer_albums (id, singer_id, album_id) | ||
| 161 | VALUES (%s, %s, %s) | ||
| 162 | ON CONFLICT (singer_id, album_id) DO NOTHING | ||
| 163 | """ | ||
| 164 | cur.executemany(sql, [(str(uuid.uuid4()), s, a) for s, a in pairs]) | ||
| 165 | |||
| 166 | |||
| 167 | # ─── Netease ───────────────────────────────────────────────────────────────── | ||
| 168 | |||
| 169 | def upsert_netease_singers(cur, singers: list[dict]) -> None: | ||
| 170 | if not singers: | ||
| 171 | return | ||
| 172 | sql = """ | ||
| 173 | INSERT INTO crawler_netease_singers | ||
| 174 | (id, name, avatar, sex, area, "index", intro, home_url, created_at, updated_at) | ||
| 175 | VALUES (%s, %s, %s, %s::netease_singer_sex, %s::netease_singer_area, %s::netease_singer_index, %s, %s, NOW(), NOW()) | ||
| 176 | ON CONFLICT (id) DO NOTHING | ||
| 177 | """ | ||
| 178 | rows = [( | ||
| 179 | s['id'], s['name'], s.get('avatar', ''), | ||
| 180 | s.get('sex') or 'U', s.get('area') or '其他', s.get('index') or '#', | ||
| 181 | s.get('intro'), s.get('home_url'), | ||
| 182 | ) for s in singers] | ||
| 183 | cur.executemany(sql, rows) | ||
| 184 | |||
| 185 | |||
| 186 | def upsert_netease_albums(cur, albums: list[dict]) -> None: | ||
| 187 | if not albums: | ||
| 188 | return | ||
| 189 | sql = """ | ||
| 190 | INSERT INTO crawler_netease_albums | ||
| 191 | (id, cover, title, intro, type, company_id, company, is_owner, published_at, created_at, updated_at) | ||
| 192 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) | ||
| 193 | ON CONFLICT (id) DO NOTHING | ||
| 194 | """ | ||
| 195 | rows = [( | ||
| 196 | a['id'], a.get('cover', ''), a.get('title', ''), a.get('intro'), | ||
| 197 | a.get('type', ''), a.get('company_id', 0) or 0, | ||
| 198 | a.get('company', ''), a.get('is_owner', 0) or 0, a.get('published_at'), | ||
| 199 | ) for a in albums] | ||
| 200 | cur.executemany(sql, rows) | ||
| 201 | |||
| 202 | |||
| 203 | def upsert_netease_songs(cur, songs: list[dict]) -> None: | ||
| 204 | if not songs: | ||
| 205 | return | ||
| 206 | sql = """ | ||
| 207 | INSERT INTO crawler_netease_songs | ||
| 208 | (id, platform_song_id, album_id, cover, title, name, duration, | ||
| 209 | lyric, composer_name, lyricist_name, url, lyric_url, | ||
| 210 | platform_index_url, published_at, singers, status, created_at, updated_at) | ||
| 211 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, 0, NOW(), NOW()) | ||
| 212 | ON CONFLICT (platform_song_id) DO NOTHING | ||
| 213 | """ | ||
| 214 | rows = [( | ||
| 215 | s['song_uuid'], s['platform_song_id'], s.get('album_id'), | ||
| 216 | s.get('cover', ''), s.get('title', ''), s.get('name', ''), s.get('duration', 0) or 0, | ||
| 217 | s.get('lyric'), s.get('composer_name'), s.get('lyricist_name'), | ||
| 218 | s.get('url', ''), s.get('lyric_url'), | ||
| 219 | s.get('platform_index_url'), s.get('published_at'), s.get('singers_json', '[]'), | ||
| 220 | ) for s in songs] | ||
| 221 | cur.executemany(sql, rows) | ||
| 222 | |||
| 223 | |||
| 224 | def upsert_netease_singer_songs(cur, pairs: list[tuple]) -> None: | ||
| 225 | if not pairs: | ||
| 226 | return | ||
| 227 | sql = """ | ||
| 228 | INSERT INTO crawler_netease_singer_songs (id, singer_id, song_id) | ||
| 229 | VALUES (%s, %s, %s) | ||
| 230 | ON CONFLICT (singer_id, song_id) DO NOTHING | ||
| 231 | """ | ||
| 232 | cur.executemany(sql, [(str(uuid.uuid4()), s, sg) for s, sg in pairs]) | ||
| 233 | |||
| 234 | |||
| 235 | def upsert_netease_singer_albums(cur, pairs: list[tuple]) -> None: | ||
| 236 | if not pairs: | ||
| 237 | return | ||
| 238 | sql = """ | ||
| 239 | INSERT INTO crawler_netease_singer_albums (id, singer_id, album_id) | ||
| 240 | VALUES (%s, %s, %s) | ||
| 241 | ON CONFLICT (singer_id, album_id) DO NOTHING | ||
| 242 | """ | ||
| 243 | cur.executemany(sql, [(str(uuid.uuid4()), s, a) for s, a in pairs]) |
tests/test_writer.py
0 → 100644
| 1 | from unittest.mock import MagicMock, call | ||
| 2 | from etl_to_crawler.writer import upsert_qq_singers, upsert_qq_singer_songs | ||
| 3 | |||
| 4 | |||
| 5 | def test_upsert_qq_singers_executes_insert(): | ||
| 6 | cur = MagicMock() | ||
| 7 | singers = [{ | ||
| 8 | 'id': 1, 'mid': 'abc', 'name': '歌手', 'avatar': 'https://archive-dev.oss-cn-beijing.aliyuncs.com/a.jpg', | ||
| 9 | 'sex': 'M', 'area': '华语', 'index': 'G', 'intro': None, 'home_url': None | ||
| 10 | }] | ||
| 11 | upsert_qq_singers(cur, singers) | ||
| 12 | assert cur.executemany.called | ||
| 13 | args = cur.executemany.call_args | ||
| 14 | assert 'crawler_qqmusic_singers' in args[0][0] | ||
| 15 | assert 'ON CONFLICT' in args[0][0] | ||
| 16 | |||
| 17 | |||
| 18 | def test_upsert_qq_singers_empty_does_nothing(): | ||
| 19 | cur = MagicMock() | ||
| 20 | upsert_qq_singers(cur, []) | ||
| 21 | cur.executemany.assert_not_called() | ||
| 22 | |||
| 23 | |||
| 24 | def test_upsert_qq_singer_songs(): | ||
| 25 | cur = MagicMock() | ||
| 26 | upsert_qq_singer_songs(cur, [(1, 'uuid-abc'), (2, 'uuid-def')]) | ||
| 27 | assert cur.executemany.called | ||
| 28 | sql = cur.executemany.call_args[0][0] | ||
| 29 | assert 'crawler_qqmusic_singer_songs' in sql |
-
Please register or sign in to post a comment