v1.0.5: 网址列表支持上传txt批量导入(UTF-8/GBK自动识别/去重/行内注释清理)

This commit is contained in:
2026-08-11 15:26:17 +08:00
parent e8541a8545
commit 44852f7be7
4 changed files with 68 additions and 4 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
- 运行中的任务参数支持**热更新**(修改后从下一页起生效)
### 2. 批量爬取模式
一次粘贴多个网址(每行一个,`#` 注释),可配置
一次粘贴多个网址(每行一个,`#` 注释),或点击「📂 导入网址文件」上传 .txt 文件批量导入(自动识别 UTF-8/GBK 编码、自动去重、自动清理行内注释)
- 项目名称、输出目录(默认 `out/<任务ID>`,可填绝对路径)
- 爬取间隔(随机秒数区间,防封 IP)、单页超时
- 失败重试次数 / 重试间隔
+15 -2
View File
@@ -154,6 +154,19 @@ def api_tasks():
return jsonify(tasks)
def _clean_urls(lines):
"""清洗网址行: 去空白, 去行内 # 注释 (URL 本身不含空格, 安全), 自动补 https 前缀"""
out = []
for u in lines or []:
u = str(u).split(" #")[0].strip()
if not u:
continue
if not u.startswith("http"):
u = "https://" + u
out.append(u)
return out
@app.route("/api/tasks", methods=["POST"])
def api_create_task():
body = request.get_json(force=True) or {}
@@ -171,7 +184,7 @@ def api_create_task():
"created_at": now_str(),
"updated_at": now_str(),
"config": {**DEFAULT_CONFIG, **(body.get("config") or {})},
"urls": [u.strip() for u in (body.get("urls") or []) if u.strip()],
"urls": _clean_urls(body.get("urls") or []),
}
if mode == "auto":
@@ -230,7 +243,7 @@ def api_update_task(tid):
if "name" in body and str(body["name"]).strip():
task["name"] = str(body["name"]).strip()
if "urls" in body:
task["urls"] = [u.strip() for u in body["urls"] if u.strip()]
task["urls"] = _clean_urls(body["urls"])
if "config" in body:
merged = {**task.get("config", {}), **body["config"]}
task["config"] = merged
+45
View File
@@ -161,6 +161,51 @@ async function delTask(tid) {
} catch (e) { toast(e.message, true); }
}
/* ---------------- 网址文件导入 ---------------- */
const MAX_IMPORT_SIZE = 2 * 1024 * 1024; // 2MB 上限
async function readFileSmart(file) {
/* 自动识别 UTF-8 / GBK 编码 */
const buf = await file.arrayBuffer();
let text = new TextDecoder("utf-8").decode(buf);
if (text.includes("\uFFFD")) {
try { text = new TextDecoder("gbk").decode(buf); } catch (e) { /* 保留 utf-8 结果 */ }
}
return text;
}
function importUrlsText(text) {
const ta = $("taskForm").elements["urls"];
const clean = (s) => s.split(" #")[0].trim(); // 去掉行内注释 (URL 不含空格, 安全)
const existing = new Set(ta.value.split(/\r?\n/).map(clean).filter(Boolean));
const fresh = text.split(/\r?\n/).map(clean).filter(Boolean);
let added = 0;
for (const line of fresh) {
if (!existing.has(line)) { existing.add(line); added++; }
}
ta.value = Array.from(existing).join("\n");
return { total: fresh.length, added };
}
$("btnImportUrls").onclick = () => $("urlFileInput").click();
$("urlFileInput").onchange = async (e) => {
const file = e.target.files && e.target.files[0];
e.target.value = ""; // 允许重复选择同一文件
if (!file) return;
if (file.size > MAX_IMPORT_SIZE) {
toast("文件过大(上限 2MB),请拆分后导入", true);
return;
}
try {
const text = await readFileSmart(file);
const r = importUrlsText(text);
formDirty = true;
toast(`已导入 ${file.name}:共 ${r.total} 行,新增 ${r.added} 条网址`);
} catch (err) {
toast("文件读取失败: " + err.message, true);
}
};
/* ---------------- 搜索 ---------------- */
async function doSearch() {
const q = $("searchInput").value.trim();
+7 -1
View File
@@ -73,7 +73,13 @@
<div class="field"><label>通知邮箱</label><input name="notify_email" value="wlq@tphai.com"></div>
</div>
<div class="field" id="urlsField"><label>网址列表(每行一个,# 开头为注释)</label>
<div class="field" id="urlsField">
<label>网址列表(每行一个,# 开头为注释)</label>
<div class="row2" style="align-items:center">
<button type="button" class="btn sm" id="btnImportUrls">📂 导入网址文件(.txt)</button>
<span class="card-line">支持 UTF-8 / GBK 编码,自动去重追加</span>
<input type="file" id="urlFileInput" accept=".txt,.csv,.urls,text/plain" hidden>
</div>
<textarea name="urls" rows="5" placeholder="https://www.example.com/&#10;https://www.example.com/page2"></textarea>
</div>