Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
沈秋雨
/
import_hk_songs
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit
62fc7695
...
62fc7695c58a9ab67ba41c37e2e9b01617e853bd
authored
2026-07-08 17:04:26 +0800
by
沈秋雨
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
feat(etl): 实现批次编排与 CLI 入口,支持 --max-batches 冒烟测试参数
1 parent
0a14389c
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
385 additions
and
0 deletions
etl_to_crawler/runner.py
run_etl.py
etl_to_crawler/runner.py
0 → 100644
View file @
62fc769
import
uuid
import
json
import
logging
from
tqdm
import
tqdm
from
.config
import
PLATFORM_QQ
,
PLATFORM_KUGOU
,
PLATFORM_NETEASE
,
BATCH_SIZE
,
OSS_CONFIG
from
.connections
import
get_hk_songs_conn
,
get_source_conn
,
get_spider_conn
,
get_pg_conn
,
get_oss_bucket
from
.reader
import
iter_hk_songs_batches
,
fetch_platform_records
from
.spider
import
(
fetch_qq_songs
,
fetch_qq_singers
,
fetch_kugou_songs
,
fetch_kugou_singers
,
fetch_netease_songs
,
fetch_netease_singers
,
)
from
.writer
import
(
upsert_qq_singers
,
upsert_qq_albums
,
upsert_qq_songs
,
upsert_qq_singer_songs
,
upsert_qq_singer_albums
,
upsert_kugou_singers
,
upsert_kugou_albums
,
upsert_kugou_songs
,
upsert_kugou_singer_songs
,
upsert_kugou_singer_albums
,
upsert_netease_singers
,
upsert_netease_albums
,
upsert_netease_songs
,
upsert_netease_singer_songs
,
upsert_netease_singer_albums
,
)
from
.oss
import
transfer_url
,
build_oss_key
from
.lyric
import
strip_timestamps
logging
.
basicConfig
(
level
=
logging
.
INFO
,
format
=
'
%(asctime)
s
%(levelname)
s
%(message)
s'
)
log
=
logging
.
getLogger
(
__name__
)
def
_safe_transfer
(
url
,
oss_key
,
bucket
,
base_url
):
try
:
return
transfer_url
(
url
,
oss_key
,
bucket
,
base_url
)
except
Exception
as
e
:
log
.
warning
(
"OSS transfer failed for
%
s:
%
s"
,
url
,
e
)
return
url
# 失败时保留原 URL,不阻断流程
def
_process_qq
(
hk_row
:
dict
,
pr
:
dict
,
spider_conn
,
pg_cur
,
bucket
,
base_url
):
mid
=
pr
[
'platform_unique_key'
]
songs_map
=
fetch_qq_songs
(
spider_conn
,
[
mid
])
if
mid
not
in
songs_map
:
return
sp
=
songs_map
[
mid
]
song_id_int
=
sp
[
'id'
]
singers_map
=
fetch_qq_singers
(
spider_conn
,
[
song_id_int
])
singer_list
=
singers_map
.
get
(
song_id_int
,
[])
# OSS 转移
audio_url
=
_safe_transfer
(
hk_row
[
'audio_url'
],
build_oss_key
(
'qq'
,
'audio'
,
mid
+
'.mp3'
),
bucket
,
base_url
)
cover_url
=
_safe_transfer
(
hk_row
.
get
(
'cover_url'
)
or
sp
.
get
(
'cover'
,
''
),
build_oss_key
(
'qq'
,
'cover'
,
str
(
song_id_int
)
+
'.jpg'
),
bucket
,
base_url
)
album_cover
=
''
if
sp
.
get
(
'album_id'
)
and
sp
.
get
(
'album_cover'
):
album_cover
=
_safe_transfer
(
sp
[
'album_cover'
],
build_oss_key
(
'qq'
,
'album'
,
str
(
sp
[
'album_id'
])
+
'.jpg'
),
bucket
,
base_url
)
# 歌手头像转移 + 写入 singers
singer_rows
=
[]
for
sg
in
singer_list
:
avatar
=
_safe_transfer
(
sg
.
get
(
'avatar'
,
''
),
build_oss_key
(
'qq'
,
'singer'
,
sg
[
'mid'
]
+
'.jpg'
),
bucket
,
base_url
)
singer_rows
.
append
({
**
sg
,
'id'
:
sg
[
'singer_id'
],
'avatar'
:
avatar
})
upsert_qq_singers
(
pg_cur
,
singer_rows
)
# singers JSONB
singers_json
=
json
.
dumps
([{
'name'
:
sg
[
'name'
],
'singer_id'
:
sg
[
'singer_id'
],
'platform_singer_id'
:
sg
[
'mid'
],
}
for
sg
in
singer_list
],
ensure_ascii
=
False
)
# 写入 album
album_id
=
None
if
sp
.
get
(
'album_id'
):
album_id
=
sp
[
'album_id'
]
upsert_qq_albums
(
pg_cur
,
[{
'id'
:
sp
[
'album_id'
],
'mid'
:
sp
.
get
(
'album_mid'
)
or
''
,
'cover'
:
album_cover
,
'title'
:
sp
.
get
(
'album_title'
)
or
''
,
'intro'
:
sp
.
get
(
'album_intro'
),
'type'
:
sp
.
get
(
'album_type'
)
or
''
,
'company_id'
:
sp
.
get
(
'company_id'
)
or
0
,
'company'
:
sp
.
get
(
'company'
)
or
''
,
'is_owner'
:
sp
.
get
(
'is_owner'
)
or
0
,
'published_at'
:
sp
.
get
(
'album_published_at'
),
}])
# 写入 song
platform_song_id
=
int
(
pr
[
'platform_mid'
])
if
pr
.
get
(
'platform_mid'
)
else
song_id_int
song_uuid
=
str
(
uuid
.
uuid4
())
upsert_qq_songs
(
pg_cur
,
[{
'song_uuid'
:
song_uuid
,
'platform_song_id'
:
platform_song_id
,
'mid'
:
mid
,
'album_id'
:
album_id
,
'cover'
:
cover_url
,
'title'
:
sp
.
get
(
'title'
,
hk_row
[
'name'
]),
'name'
:
hk_row
[
'name'
],
'duration'
:
sp
.
get
(
'duration'
,
0
)
or
0
,
'lyric'
:
strip_timestamps
(
sp
.
get
(
'lyric'
)),
'composer_name'
:
sp
.
get
(
'composer_name'
)
or
hk_row
.
get
(
'composer'
),
'lyricist_name'
:
sp
.
get
(
'lyricist_name'
)
or
hk_row
.
get
(
'lyricist'
),
'url'
:
audio_url
,
'lyric_url'
:
hk_row
.
get
(
'lyrics_url'
),
'platform_index_url'
:
sp
.
get
(
'platform_index_url'
),
'published_at'
:
sp
.
get
(
'published_at'
)
or
hk_row
.
get
(
'issue_time'
),
'singers_json'
:
singers_json
,
}])
# 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID)
pg_cur
.
execute
(
'SELECT id FROM crawler_qqmusic_songs WHERE platform_song_id =
%
s'
,
(
platform_song_id
,))
row
=
pg_cur
.
fetchone
()
if
row
:
song_uuid
=
str
(
row
[
0
])
# singer_songs / singer_albums
upsert_qq_singer_songs
(
pg_cur
,
[(
sg
[
'singer_id'
],
song_uuid
)
for
sg
in
singer_list
])
if
album_id
:
upsert_qq_singer_albums
(
pg_cur
,
[(
sg
[
'singer_id'
],
album_id
)
for
sg
in
singer_list
])
def
_process_kugou
(
hk_row
:
dict
,
pr
:
dict
,
spider_conn
,
pg_cur
,
bucket
,
base_url
):
song_id
=
int
(
pr
[
'platform_unique_key'
])
songs_map
=
fetch_kugou_songs
(
spider_conn
,
[
song_id
])
if
song_id
not
in
songs_map
:
return
sp
=
songs_map
[
song_id
]
singers_map
=
fetch_kugou_singers
(
spider_conn
,
[
song_id
])
singer_list
=
singers_map
.
get
(
song_id
,
[])
audio_url
=
_safe_transfer
(
hk_row
[
'audio_url'
],
build_oss_key
(
'kugou'
,
'audio'
,
str
(
song_id
)
+
'.mp3'
),
bucket
,
base_url
)
cover_url
=
_safe_transfer
(
hk_row
.
get
(
'cover_url'
)
or
sp
.
get
(
'cover'
,
''
),
build_oss_key
(
'kugou'
,
'cover'
,
str
(
song_id
)
+
'.jpg'
),
bucket
,
base_url
)
album_cover
=
''
if
sp
.
get
(
'album_id'
)
and
sp
.
get
(
'album_cover'
):
album_cover
=
_safe_transfer
(
sp
[
'album_cover'
],
build_oss_key
(
'kugou'
,
'album'
,
str
(
sp
[
'album_id'
])
+
'.jpg'
),
bucket
,
base_url
)
singer_rows
=
[]
for
sg
in
singer_list
:
avatar
=
_safe_transfer
(
sg
.
get
(
'avatar'
,
''
),
build_oss_key
(
'kugou'
,
'singer'
,
str
(
sg
[
'singer_id'
])
+
'.jpg'
),
bucket
,
base_url
)
singer_rows
.
append
({
**
sg
,
'id'
:
sg
[
'singer_id'
],
'avatar'
:
avatar
})
upsert_kugou_singers
(
pg_cur
,
singer_rows
)
singers_json
=
json
.
dumps
([{
'name'
:
sg
[
'name'
],
'singer_id'
:
sg
[
'singer_id'
],
'platform_singer_id'
:
str
(
sg
[
'singer_id'
]),
}
for
sg
in
singer_list
],
ensure_ascii
=
False
)
album_id
=
None
if
sp
.
get
(
'album_id'
):
album_id
=
sp
[
'album_id'
]
upsert_kugou_albums
(
pg_cur
,
[{
'id'
:
sp
[
'album_id'
],
'cover'
:
album_cover
,
'title'
:
sp
.
get
(
'album_title'
)
or
''
,
'intro'
:
sp
.
get
(
'album_intro'
),
'type'
:
sp
.
get
(
'album_type'
)
or
''
,
'company_id'
:
sp
.
get
(
'company_id'
)
or
0
,
'company'
:
sp
.
get
(
'company'
)
or
''
,
'is_owner'
:
sp
.
get
(
'is_owner'
)
or
0
,
'published_at'
:
sp
.
get
(
'album_published_at'
),
}])
song_uuid
=
str
(
uuid
.
uuid4
())
upsert_kugou_songs
(
pg_cur
,
[{
'song_uuid'
:
song_uuid
,
'platform_song_id'
:
song_id
,
'hash'
:
sp
.
get
(
'hid'
,
pr
.
get
(
'platform_mid'
,
''
)),
'album_audio_id'
:
sp
.
get
(
'album_audio_id'
)
or
pr
.
get
(
'album_audio_id'
)
or
0
,
'album_id'
:
album_id
,
'cover'
:
cover_url
,
'title'
:
sp
.
get
(
'title'
,
hk_row
[
'name'
]),
'name'
:
hk_row
[
'name'
],
'duration'
:
sp
.
get
(
'duration'
,
0
)
or
0
,
'lyric'
:
strip_timestamps
(
sp
.
get
(
'lyric'
)),
'composer_name'
:
sp
.
get
(
'composer_name'
)
or
hk_row
.
get
(
'composer'
),
'lyricist_name'
:
sp
.
get
(
'lyricist_name'
)
or
hk_row
.
get
(
'lyricist'
),
'url'
:
audio_url
,
'lyric_url'
:
hk_row
.
get
(
'lyrics_url'
),
'platform_index_url'
:
sp
.
get
(
'platform_index_url'
),
'published_at'
:
sp
.
get
(
'published_at'
)
or
hk_row
.
get
(
'issue_time'
),
'singers_json'
:
singers_json
,
}])
# 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID)
pg_cur
.
execute
(
'SELECT id FROM crawler_kugou_songs WHERE platform_song_id =
%
s'
,
(
song_id
,))
row
=
pg_cur
.
fetchone
()
if
row
:
song_uuid
=
str
(
row
[
0
])
upsert_kugou_singer_songs
(
pg_cur
,
[(
sg
[
'singer_id'
],
song_uuid
)
for
sg
in
singer_list
])
if
album_id
:
upsert_kugou_singer_albums
(
pg_cur
,
[(
sg
[
'singer_id'
],
album_id
)
for
sg
in
singer_list
])
def
_process_netease
(
hk_row
:
dict
,
pr
:
dict
,
spider_conn
,
pg_cur
,
bucket
,
base_url
):
song_id
=
int
(
pr
[
'platform_unique_key'
])
songs_map
=
fetch_netease_songs
(
spider_conn
,
[
song_id
])
if
song_id
not
in
songs_map
:
return
sp
=
songs_map
[
song_id
]
singers_map
=
fetch_netease_singers
(
spider_conn
,
[
song_id
])
singer_list
=
singers_map
.
get
(
song_id
,
[])
audio_url
=
_safe_transfer
(
hk_row
[
'audio_url'
],
build_oss_key
(
'netease'
,
'audio'
,
str
(
song_id
)
+
'.mp3'
),
bucket
,
base_url
)
cover_url
=
_safe_transfer
(
hk_row
.
get
(
'cover_url'
)
or
sp
.
get
(
'cover'
,
''
),
build_oss_key
(
'netease'
,
'cover'
,
str
(
song_id
)
+
'.jpg'
),
bucket
,
base_url
)
album_cover
=
''
if
sp
.
get
(
'album_id'
)
and
sp
.
get
(
'album_cover'
):
album_cover
=
_safe_transfer
(
sp
[
'album_cover'
],
build_oss_key
(
'netease'
,
'album'
,
str
(
sp
[
'album_id'
])
+
'.jpg'
),
bucket
,
base_url
)
singer_rows
=
[]
for
sg
in
singer_list
:
avatar
=
_safe_transfer
(
sg
.
get
(
'avatar'
,
''
),
build_oss_key
(
'netease'
,
'singer'
,
str
(
sg
[
'singer_id'
])
+
'.jpg'
),
bucket
,
base_url
)
singer_rows
.
append
({
**
sg
,
'id'
:
sg
[
'singer_id'
],
'avatar'
:
avatar
})
upsert_netease_singers
(
pg_cur
,
singer_rows
)
singers_json
=
json
.
dumps
([{
'name'
:
sg
[
'name'
],
'singer_id'
:
sg
[
'singer_id'
],
'platform_singer_id'
:
str
(
sg
[
'singer_id'
]),
}
for
sg
in
singer_list
],
ensure_ascii
=
False
)
album_id
=
None
if
sp
.
get
(
'album_id'
):
album_id
=
sp
[
'album_id'
]
upsert_netease_albums
(
pg_cur
,
[{
'id'
:
sp
[
'album_id'
],
'cover'
:
album_cover
,
'title'
:
sp
.
get
(
'album_title'
)
or
''
,
'intro'
:
sp
.
get
(
'album_intro'
),
'type'
:
sp
.
get
(
'album_type'
)
or
''
,
'company_id'
:
sp
.
get
(
'company_id'
)
or
0
,
'company'
:
sp
.
get
(
'company'
)
or
''
,
'is_owner'
:
sp
.
get
(
'is_owner'
)
or
0
,
'published_at'
:
sp
.
get
(
'album_published_at'
),
}])
song_uuid
=
str
(
uuid
.
uuid4
())
upsert_netease_songs
(
pg_cur
,
[{
'song_uuid'
:
song_uuid
,
'platform_song_id'
:
song_id
,
'album_id'
:
album_id
,
'cover'
:
cover_url
,
'title'
:
sp
.
get
(
'title'
,
hk_row
[
'name'
]),
'name'
:
hk_row
[
'name'
],
'duration'
:
sp
.
get
(
'duration'
,
0
)
or
0
,
'lyric'
:
strip_timestamps
(
sp
.
get
(
'lyric'
)),
'composer_name'
:
sp
.
get
(
'composer_name'
)
or
hk_row
.
get
(
'composer'
),
'lyricist_name'
:
sp
.
get
(
'lyricist_name'
)
or
hk_row
.
get
(
'lyricist'
),
'url'
:
audio_url
,
'lyric_url'
:
hk_row
.
get
(
'lyrics_url'
),
'platform_index_url'
:
sp
.
get
(
'platform_index_url'
),
'published_at'
:
sp
.
get
(
'published_at'
)
or
hk_row
.
get
(
'issue_time'
),
'singers_json'
:
singers_json
,
}])
# 查询实际 UUID(ON CONFLICT DO NOTHING 时使用已有 UUID)
pg_cur
.
execute
(
'SELECT id FROM crawler_netease_songs WHERE platform_song_id =
%
s'
,
(
song_id
,))
row
=
pg_cur
.
fetchone
()
if
row
:
song_uuid
=
str
(
row
[
0
])
upsert_netease_singer_songs
(
pg_cur
,
[(
sg
[
'singer_id'
],
song_uuid
)
for
sg
in
singer_list
])
if
album_id
:
upsert_netease_singer_albums
(
pg_cur
,
[(
sg
[
'singer_id'
],
album_id
)
for
sg
in
singer_list
])
_PROCESSORS
=
{
PLATFORM_QQ
:
_process_qq
,
PLATFORM_KUGOU
:
_process_kugou
,
PLATFORM_NETEASE
:
_process_netease
,
}
def
run
(
platforms
:
list
[
str
],
max_batches
:
int
|
None
=
None
)
->
None
:
hk_conn
=
get_hk_songs_conn
()
src_conn
=
get_source_conn
()
spider_conn
=
get_spider_conn
()
pg_conn
=
get_pg_conn
()
bucket
=
get_oss_bucket
()
base_url
=
OSS_CONFIG
[
'base_url'
]
total_ok
=
total_err
=
0
try
:
for
i
,
batch
in
enumerate
(
tqdm
(
iter_hk_songs_batches
(
hk_conn
,
BATCH_SIZE
),
desc
=
'batches'
)):
if
max_batches
is
not
None
and
i
>=
max_batches
:
break
song_ids
=
[
int
(
r
[
'source_song_id'
])
for
r
in
batch
if
r
.
get
(
'source_song_id'
)]
platform_records
=
fetch_platform_records
(
src_conn
,
song_ids
)
# index platform records by source_song_id
pr_by_song
:
dict
[
int
,
list
]
=
{}
for
pr
in
platform_records
:
if
pr
[
'platform'
]
in
platforms
:
pr_by_song
.
setdefault
(
int
(
pr
[
'source_song_id'
]),
[])
.
append
(
pr
)
with
pg_conn
.
cursor
()
as
pg_cur
:
for
hk_row
in
batch
:
src_id
=
int
(
hk_row
[
'source_song_id'
])
if
hk_row
.
get
(
'source_song_id'
)
else
None
if
not
src_id
or
src_id
not
in
pr_by_song
:
continue
for
pr
in
pr_by_song
[
src_id
]:
processor
=
_PROCESSORS
.
get
(
pr
[
'platform'
])
if
not
processor
:
continue
pg_cur
.
execute
(
'SAVEPOINT sp_song'
)
try
:
processor
(
hk_row
,
pr
,
spider_conn
,
pg_cur
,
bucket
,
base_url
)
pg_cur
.
execute
(
'RELEASE SAVEPOINT sp_song'
)
total_ok
+=
1
except
Exception
as
e
:
pg_cur
.
execute
(
'ROLLBACK TO SAVEPOINT sp_song'
)
pg_cur
.
execute
(
'RELEASE SAVEPOINT sp_song'
)
log
.
error
(
"Error processing song
%
s platform
%
s:
%
s"
,
hk_row
.
get
(
'name'
),
pr
[
'platform'
],
e
)
total_err
+=
1
pg_conn
.
commit
()
finally
:
hk_conn
.
close
()
src_conn
.
close
()
spider_conn
.
close
()
pg_conn
.
close
()
log
.
info
(
"Done. OK=
%
d ERR=
%
d"
,
total_ok
,
total_err
)
run_etl.py
0 → 100644
View file @
62fc769
#!/usr/bin/env python3
import
argparse
from
etl_to_crawler.config
import
PLATFORM_QQ
,
PLATFORM_KUGOU
,
PLATFORM_NETEASE
,
PLATFORMS
from
etl_to_crawler.runner
import
run
PLATFORM_MAP
=
{
'qq'
:
PLATFORM_QQ
,
'kugou'
:
PLATFORM_KUGOU
,
'netease'
:
PLATFORM_NETEASE
,
}
if
__name__
==
'__main__'
:
parser
=
argparse
.
ArgumentParser
(
description
=
'ETL: hk_songs_test → crawler_dev'
)
parser
.
add_argument
(
'--platform'
,
default
=
'all'
,
choices
=
[
'qq'
,
'kugou'
,
'netease'
,
'all'
],
help
=
'要导入的平台(默认 all)'
)
parser
.
add_argument
(
'--max-batches'
,
type
=
int
,
default
=
None
,
help
=
'最多处理多少批次(冒烟测试用)'
)
args
=
parser
.
parse_args
()
if
args
.
platform
==
'all'
:
platforms
=
PLATFORMS
else
:
platforms
=
[
PLATFORM_MAP
[
args
.
platform
]]
print
(
f
"Starting ETL for platforms: {platforms}"
)
run
(
platforms
,
max_batches
=
args
.
max_batches
)
Please
register
or
sign in
to post a comment