-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
246 lines (198 loc) · 7.79 KB
/
cli.py
File metadata and controls
246 lines (198 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
统一 CLI:豆瓣读书/影视迁移工具
用法:
python3 cli.py --book # 爬全部 + 生成 Goodreads CSV
python3 cli.py --movie # 爬全部 + 生成 Letterboxd CSV
python3 cli.py --book --movie # 两者同时执行
python3 cli.py --book --mode test # 测试模式
python3 cli.py --book --mode limited # 有限模式
python3 cli.py --help
"""
import sys
import argparse
import os
import logging
import shared
import books
import movies
logger = logging.getLogger("cli")
# ==================== CLI 参数解析 ====================
def build_parser():
parser = argparse.ArgumentParser(
description="豆瓣读书/影视迁移工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"\n"
"示例:\n"
" python3 cli.py --book # 爬取全部图书\n"
" python3 cli.py --movie # 爬取全部影视\n"
" python3 cli.py --book --movie # 同时爬取两者\n"
" python3 cli.py --book --mode test # 测试模式(极小量)\n"
" python3 cli.py --book --mode limited # 有限模式(2 页)\n"
" python3 cli.py --book --dump-html # 保存 HTML 供调试\n"
" python3 cli.py --book --no-import-review # 不输出评论\n"
),
)
parser.add_argument(
"--book", dest="target", action="append_const", const="book",
help="爬取豆瓣图书 → Goodreads CSV",
)
parser.add_argument(
"--movie", dest="target", action="append_const", const="movie",
help="爬取豆瓣影视 → Letterboxd CSV",
)
parser.add_argument(
"--mode", "-m",
choices=["test", "limited", "full"],
default="full",
help="运行模式:test=极小测试,limited=有限运行,full=完整运行(默认)",
)
parser.add_argument(
"--pages", "-p", type=int, default=None,
help="强制设置最大爬取页数",
)
parser.add_argument(
"--resume", "-r", action="store_true",
help="从上次断点继续",
)
parser.add_argument(
"--dump-html", action="store_true",
help="将所有响应 HTML 保存到 output/.work/html_dump/ 供调试",
)
parser.add_argument(
"--ua", default=None,
help="覆盖默认 User-Agent",
)
parser.add_argument(
"--no-import-review", dest="import_review", action="store_false",
help="不在 CSV 中包含评论(默认输出评论)",
)
parser.set_defaults(import_review=True)
return parser
# ==================== 辅助 ====================
def _resolve_max_pages(mode, explicit_pages):
if explicit_pages is not None:
return explicit_pages
if mode == "test":
return 1
if mode == "limited":
return 2
return None # full
def _apply_ua_override(ua):
if ua:
shared.DEFAULT_UA = ua
logger.info(f"User-Agent 已覆盖:{ua}")
def _test_auth(target):
from shared import request_with_retry, get_default_headers
if target == "book":
from books import BASE_URL as B
from shared import USER_ID as U
url = f"{B}/people/{U}/collect"
else:
from movies import BASE_URL as M
from shared import USER_ID as U
url = f"{M}/people/{U}/collect"
logger.info(f"正在验证 Cookie({target})...")
resp = request_with_retry("GET", url, headers=get_default_headers())
if resp:
logger.info(f"Cookie 验证成功,返回状态码:{resp.status_code}")
# ==================== Book ====================
def run_book(args):
max_pages = _resolve_max_pages(args.mode, args.pages)
import_review = args.import_review
if args.mode == "test":
_test_auth("book")
logger.info("[test] 爬取图书列表第 1 页(无延迟)...")
result = books.scrape(start_page=0, max_pages=1)
if result:
url = result[0]["url"]
title = result[0]["title"]
isbn, detail_title = books._fetch_book_detail(url)
logger.info(f"[test] 《{detail_title or title}》ISBN: {isbn}")
logger.info("[test] 图书测试完成!")
return
if args.resume:
start = books._load_checkpoint()
if start == 0:
logger.info("未发现有效断点,将从头开始")
else:
books._clear_checkpoint()
start = 0
logger.info(f"开始爬取图书列表(最多 {max_pages or '全部'} 页)...")
result = books.scrape(start_page=start, max_pages=max_pages)
if not result:
logger.error("图书列表爬取失败")
sys.exit(1)
if args.resume and os.path.exists(shared.CHECKPOINT_ISBN):
logger.info("从断点继续 Goodreads CSV 生成...")
books.build_goodreads_csv(resume=True, import_review=import_review)
elif args.mode == "limited":
n = (max_pages or 2) * 15
books.build_goodreads_csv(records=result[:n], import_review=import_review)
else:
books.build_goodreads_csv(records=result, import_review=import_review)
logger.info("图书迁移全部完成!")
# ==================== Movie ====================
def run_movie(args):
max_pages = _resolve_max_pages(args.mode, args.pages)
import_review = args.import_review
if args.mode == "test":
_test_auth("movie")
logger.info("[test] 爬取影视列表第 1 页(无延迟)...")
result = movies.scrape(start_page=0, max_pages=1)
if result:
url = result[0]["url"]
title = result[0]["title"]
imdb_id, directors, year, orig_title = movies._fetch_letterboxd_fields(url)
logger.info(f"[test] 《{title}》→ {orig_title} | IMDB: {imdb_id} | 导演: {directors[:40]} | Year: {year}")
logger.info("[test] 影视测试完成!")
return
if args.resume:
start = movies._load_checkpoint()
if start == 0:
logger.info("未发现有效断点,将从头开始")
else:
movies._clear_checkpoint()
start = 0
logger.info(f"开始爬取影视列表(最多 {max_pages or '全部'} 页)...")
result = movies.scrape(start_page=start, max_pages=max_pages)
if not result:
logger.error("影视列表爬取失败")
sys.exit(1)
if args.resume and os.path.exists(shared.CHECKPOINT_LETTERBOXD):
logger.info("从断点继续 Letterboxd CSV 生成...")
movies.build_letterboxd_csv(resume=True, import_review=import_review)
elif args.mode == "limited":
n = (max_pages or 2) * 15
movies.build_letterboxd_csv(records=result[:n], import_review=import_review)
else:
movies.build_letterboxd_csv(records=result, import_review=import_review)
logger.info("影视迁移全部完成!")
# ==================== 主入口 ====================
def main():
parser = build_parser()
args = parser.parse_args()
if not args.target:
parser.print_help()
print()
logger.info("请使用 --book 或 --movie 指定要迁移的数据类型。")
logger.info("例如:python3 cli.py --book")
sys.exit(0)
if args.dump_html:
shared.DUMP_HTML = True
logger.info("HTML 转储已开启,内容将保存至 output/.work/html_dump/")
if args.ua:
_apply_ua_override(args.ua)
for t in args.target:
logger.info("=" * 60)
label = "图书(→ Goodreads)" if t == "book" else "影视(→ Letterboxd)"
logger.info(f"开始执行:{label}")
logger.info("=" * 60)
if t == "book":
run_book(args)
elif t == "movie":
run_movie(args)
if __name__ == "__main__":
main()