modified: blueprints/__pycache__/__init__.cpython-39.pyc
modified: blueprints/__pycache__/admin.cpython-39.pyc modified: blueprints/__pycache__/auth.cpython-39.pyc modified: blueprints/__pycache__/brand.cpython-311.pyc modified: blueprints/__pycache__/brand.cpython-39.pyc modified: blueprints/brand.py new file: brand_spider/__pycache__/main.cpython-311.pyc modified: brand_spider/__pycache__/main.cpython-39.pyc new file: brand_spider/__pycache__/web_dec.cpython-311.pyc modified: brand_spider/main.py modified: config.py modified: web_source/admin.html modified: web_source/brand.html modified: web_source/home.html modified: web_source/index.html modified: web_source/login.html new file: web_source/templates_backup/admin.html new file: web_source/templates_backup/brand.html new file: web_source/templates_backup/home.html new file: web_source/templates_backup/index.html new file: web_source/templates_backup/login.html
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,12 +4,10 @@
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
import datetime
|
||||
import traceback
|
||||
import zipfile
|
||||
import io
|
||||
import time
|
||||
import tempfile
|
||||
import requests
|
||||
from queue import Queue, Empty
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
@@ -18,7 +16,7 @@ from urllib.parse import urlparse
|
||||
from flask import Blueprint, request, jsonify, session, send_file, redirect, Response
|
||||
|
||||
from app_common import get_db, login_required, BASE_DIR
|
||||
from config import bucket_path
|
||||
from config import bucket_path,JAVA_API_BASE
|
||||
from brand_spider.main import single_file_handle, TaskCancelledError
|
||||
|
||||
brand_bp = Blueprint('brand', __name__)
|
||||
@@ -39,6 +37,64 @@ _task_event_lock = threading.Lock()
|
||||
# }
|
||||
_task_line_progress = {}
|
||||
_task_line_progress_lock = threading.Lock()
|
||||
_cancelled_brand_tasks = set()
|
||||
_cancelled_brand_tasks_lock = threading.Lock()
|
||||
|
||||
|
||||
def _mark_task_cancelled(task_id):
|
||||
with _cancelled_brand_tasks_lock:
|
||||
_cancelled_brand_tasks.add(task_id)
|
||||
|
||||
|
||||
def _is_task_cancelled(task_id):
|
||||
with _cancelled_brand_tasks_lock:
|
||||
return task_id in _cancelled_brand_tasks
|
||||
|
||||
|
||||
def _get_uid_from_request_headers():
|
||||
"""
|
||||
从请求头读取 uid。
|
||||
前端会在 headers 里携带 uid,用于确定本次请求的归属用户。
|
||||
"""
|
||||
uid = request.headers.get('uid')
|
||||
if uid is None:
|
||||
return None
|
||||
uid = str(uid).strip()
|
||||
if not uid:
|
||||
return None
|
||||
try:
|
||||
return int(uid)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_user_id():
|
||||
"""
|
||||
优先使用请求头 uid;没有请求头 uid 时回退到 session['user_id']。
|
||||
若二者同时存在但不一致,则视为无效请求并返回 None。
|
||||
"""
|
||||
req_uid = _get_uid_from_request_headers()
|
||||
if req_uid is not None:
|
||||
sess_uid = session.get('user_id')
|
||||
if sess_uid is not None:
|
||||
try:
|
||||
if int(sess_uid) != int(req_uid):
|
||||
return None
|
||||
except Exception:
|
||||
if str(sess_uid) != str(req_uid):
|
||||
return None
|
||||
return req_uid
|
||||
return session.get('user_id')
|
||||
|
||||
|
||||
def _get_user_id_or_error():
|
||||
user_id = _resolve_user_id()
|
||||
if not user_id:
|
||||
return None, (jsonify({'success': False, 'error': '未登录或缺少uid'}), 401)
|
||||
try:
|
||||
return int(user_id), None
|
||||
except Exception:
|
||||
return None, (jsonify({'success': False, 'error': 'uid无效'}), 400)
|
||||
|
||||
|
||||
def _push_task_event(task_id, event):
|
||||
@@ -54,10 +110,14 @@ def _push_task_event(task_id, event):
|
||||
# 任务进入终态时,顺便清理行级进度缓存
|
||||
with _task_line_progress_lock:
|
||||
_task_line_progress.pop(task_id, None)
|
||||
status = (event or {}).get('status') if isinstance(event, dict) else None
|
||||
if status in ('success', 'failed', 'cancelled'):
|
||||
with _cancelled_brand_tasks_lock:
|
||||
_cancelled_brand_tasks.discard(task_id)
|
||||
|
||||
|
||||
def _expand_folder_xlsx(folder_path):
|
||||
"""返回文件夹下第一层 .xlsx 文件的绝对路径列表(保留旧逻辑,不再用于品牌工具文件夹上传)"""
|
||||
"""返回文件夹下所有 .xlsx 文件的绝对路径列表"""
|
||||
if not folder_path or not os.path.isdir(folder_path):
|
||||
return []
|
||||
paths = []
|
||||
@@ -67,26 +127,6 @@ def _expand_folder_xlsx(folder_path):
|
||||
return paths
|
||||
|
||||
|
||||
def _expand_folder_xlsx_recursive_items(folder_path):
|
||||
"""返回文件夹下所有 .xlsx 文件的绝对路径和相对路径列表(递归)"""
|
||||
if not folder_path or not os.path.isdir(folder_path):
|
||||
return []
|
||||
items = []
|
||||
root = os.path.normpath(folder_path)
|
||||
for current_root, _, filenames in os.walk(root):
|
||||
for name in filenames:
|
||||
if not (name.endswith('.xlsx') or name.endswith('.XLSX')):
|
||||
continue
|
||||
absolute_path = os.path.normpath(os.path.join(current_root, name))
|
||||
relative_path = os.path.relpath(absolute_path, root).replace('\\', '/')
|
||||
items.append({
|
||||
'absolutePath': absolute_path,
|
||||
'relativePath': relative_path,
|
||||
})
|
||||
items.sort(key=lambda item: item['relativePath'])
|
||||
return items
|
||||
|
||||
|
||||
def _upload_local_xlsx_to_oss(local_path, user_id):
|
||||
"""将本地 xlsx 文件上传到 OSS,返回 (url, None) 或 (None, error_message)。"""
|
||||
if not local_path or not os.path.isfile(local_path):
|
||||
@@ -99,7 +139,7 @@ def _upload_local_xlsx_to_oss(local_path, user_id):
|
||||
safe_base = "".join(c if c.isalnum() or c in '-_.' else '_' for c in base)
|
||||
if not safe_base.endswith('.xlsx'):
|
||||
safe_base = safe_base + '.xlsx'
|
||||
key = f"{bucket_path}brand_input/{int(time.time() * 1000)}/{user_id}_{safe_base}"
|
||||
key = f"{bucket_path}brand_input/{user_id}/{int(time.time() * 1000)}/{safe_base}"
|
||||
try:
|
||||
with open(local_path, "rb") as f:
|
||||
content = f.read()
|
||||
@@ -109,347 +149,125 @@ def _upload_local_xlsx_to_oss(local_path, user_id):
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def _run_brand_files(file_paths, task_id=None, check_cancelled=None, strategy="Terms"):
|
||||
"""对多个 xlsx 执行 single_file_handle(线程池多线程),返回 (result_paths, error_message)。
|
||||
check_cancelled: 可调用对象,返回 True 时中止并返回 (result_paths, 'cancelled')。
|
||||
task_id 存在时会在每处理完一个文件后更新 progress_current。
|
||||
strategy: 品牌匹配方式,默认 Terms,可选 Simple。"""
|
||||
os.makedirs(BRAND_OUTPUT_DIR, exist_ok=True)
|
||||
subdir = str(task_id) if task_id else ('immediate_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S'))
|
||||
out_dir = os.path.join(BRAND_OUTPUT_DIR, subdir)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
def _run_brand_single(taskid,brand_ls, fileUrl,strategy,totalLines,chunkTotal,chunkIndex):
|
||||
""""""
|
||||
invalidBrands = [] # 不符合品牌的数据
|
||||
queryFailedBrands = [] # 查询失败的数据
|
||||
keptRows = []
|
||||
for brand in brand_ls:
|
||||
if _is_task_cancelled(taskid):
|
||||
raise TaskCancelledError("任务已取消")
|
||||
faild_data,query_faild_data = single_file_handle(brand,strategy)
|
||||
invalidBrands.extend(faild_data)
|
||||
queryFailedBrands.extend(query_faild_data)
|
||||
if len(faild_data) == 0 and len(query_faild_data) == 0:
|
||||
keptRows.append(brand)
|
||||
|
||||
# 先筛出有效路径并生成 (fp, out_path) 列表
|
||||
tasks = []
|
||||
for fp in file_paths:
|
||||
if not fp or not isinstance(fp, str) or not os.path.isfile(fp):
|
||||
continue
|
||||
base = os.path.splitext(os.path.basename(fp))[0]
|
||||
safe_base = "".join(c if c.isalnum() or c in '-_' else '_' for c in base)
|
||||
out_path = os.path.join(out_dir, safe_base + '_result.xlsx')
|
||||
tasks.append((fp, out_path))
|
||||
|
||||
if not tasks:
|
||||
return [], None
|
||||
|
||||
result_paths = []
|
||||
result_lock = threading.Lock()
|
||||
current = [0] # 用列表以便在闭包中修改
|
||||
|
||||
def process_one(fp, out_path, file_index, files_total):
|
||||
# 行级进度回调:由 single_file_handle 在循环中调用,直接写入内存字典,不落库
|
||||
def progress_callback(current_line, total_lines, extra=None):
|
||||
if not task_id:
|
||||
return
|
||||
info = {
|
||||
'file_index': file_index,
|
||||
'file_total': files_total,
|
||||
'file_path': fp,
|
||||
'current_line': int(current_line or 0),
|
||||
'total_lines': int(total_lines or 0),
|
||||
}
|
||||
if isinstance(extra, dict):
|
||||
info.update(extra)
|
||||
with _task_line_progress_lock:
|
||||
_task_line_progress[task_id] = info
|
||||
|
||||
single_file_handle(fp, out_path, strategy, check_cancelled=check_cancelled, progress_callback=progress_callback)
|
||||
return out_path
|
||||
|
||||
max_workers = min(8, len(tasks))
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
future_to_out = {
|
||||
executor.submit(process_one, fp, out_path, idx + 1, len(tasks)): out_path
|
||||
for idx, (fp, out_path) in enumerate(tasks)
|
||||
}
|
||||
try:
|
||||
for future in as_completed(future_to_out):
|
||||
if check_cancelled and check_cancelled():
|
||||
for f in future_to_out:
|
||||
f.cancel()
|
||||
return result_paths, 'cancelled'
|
||||
try:
|
||||
out_path = future.result()
|
||||
with result_lock:
|
||||
result_paths.append(out_path)
|
||||
current[0] += 1
|
||||
if task_id:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET progress_current = %s WHERE id = %s",
|
||||
(current[0], task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
print(traceback.format_exc())
|
||||
except TaskCancelledError:
|
||||
for f in future_to_out:
|
||||
f.cancel()
|
||||
return result_paths, 'cancelled'
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
print(e)
|
||||
for f in future_to_out:
|
||||
f.cancel()
|
||||
return result_paths, str(e)
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
return result_paths, str(e)
|
||||
return result_paths, None
|
||||
data = { "strategy": strategy ,
|
||||
"files": [
|
||||
{
|
||||
"fileUrl": fileUrl,
|
||||
"originalFilename": "",
|
||||
"relativePath": "",
|
||||
"mainSheetName": "",
|
||||
"chunkIndex": chunkIndex,
|
||||
"chunkTotal": chunkTotal,
|
||||
"totalLines": totalLines,
|
||||
"keptRows": keptRows,
|
||||
"invalidBrands": invalidBrands,
|
||||
"queryFailedBrands": queryFailedBrands
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks/{taskid}/result",headers={"accept":"application/json"},
|
||||
# json={ "strategy": strategy ,
|
||||
# "files": [
|
||||
# { "fileUrl": fileUrl,"originalFilename": "","relativePath": "",
|
||||
# "mainSheetName": "","columns": [],"keptRows": [],
|
||||
# "invalidBrands": invalidBrands,"queryFailedBrands": queryFailedBrands
|
||||
# }]})
|
||||
json=data)
|
||||
print(data)
|
||||
print(taskid,brand_ls, fileUrl)
|
||||
print("提交结果",data,"\n-->",resp.text)
|
||||
return True
|
||||
|
||||
|
||||
def _upload_results_to_oss(source_paths, local_result_paths, task_id):
|
||||
|
||||
def _background_brand_task(task_id,data):
|
||||
"""
|
||||
将源文件和结果文件上传到 OSS,并打包成 zip(分两个文件夹:源文件、结果文件)上传。
|
||||
返回 (result_data, error_message),result_data 为 {"urls": [url1, ...], "zip_url": "..."},出错时为 (None, err)。
|
||||
后台执行爬虫任务
|
||||
参数示例:
|
||||
data : {
|
||||
"taskId": 348,
|
||||
"strategy": "Terms",
|
||||
"files": [
|
||||
{
|
||||
"fileIndex": 1,
|
||||
"fileUrl": "ab8cce4878754bfd89b4cd3ed20e1395",
|
||||
"originalFilename": "品牌样例.xlsx",
|
||||
"relativePath": "店铺A/品牌样例.xlsx",
|
||||
"sheetName": "Sheet1",
|
||||
"columns": [
|
||||
"品牌",
|
||||
"ASIH",
|
||||
"状态",
|
||||
"时间"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"品牌": "YQAUTEC",
|
||||
"ASIH": "B0F28NZ752",
|
||||
"状态": "",
|
||||
"时间": "",
|
||||
"__rowIndex": 2
|
||||
}
|
||||
],
|
||||
"uniqueBrands": [
|
||||
"YQAUTEC"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
if not local_result_paths or not task_id:
|
||||
return None, "无结果文件"
|
||||
try:
|
||||
from ali_oss import upload_file as oss_upload_file
|
||||
except ImportError:
|
||||
return None, "OSS 模块未配置"
|
||||
urls = []
|
||||
subdir = f"{OSS_PREFIX}/{task_id}"
|
||||
for fp in local_result_paths:
|
||||
if not fp or not os.path.isfile(fp):
|
||||
continue
|
||||
name = os.path.basename(fp)
|
||||
key = f"{bucket_path}{subdir}/{name}"
|
||||
try:
|
||||
with open(fp, "rb") as f:
|
||||
content = f.read()
|
||||
url = oss_upload_file(content, key)
|
||||
urls.append(url)
|
||||
except Exception as e:
|
||||
return None, f"上传结果文件失败: {e}"
|
||||
if not urls:
|
||||
return None, "没有可上传的结果文件"
|
||||
# 打包成 zip:源文件 -> 源文件/,结果文件 -> 结果文件/
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for fp in (source_paths or []):
|
||||
if fp and os.path.isfile(fp):
|
||||
zf.write(fp, os.path.join("源文件", os.path.basename(fp)))
|
||||
for fp in local_result_paths:
|
||||
if fp and os.path.isfile(fp):
|
||||
zf.write(fp, os.path.join("结果文件", os.path.basename(fp)))
|
||||
buf.seek(0)
|
||||
zip_key = f"{bucket_path}{subdir}/result.zip"
|
||||
try:
|
||||
zip_url = oss_upload_file(buf.getvalue(), zip_key)
|
||||
except Exception as e:
|
||||
return {"urls": urls, "zip_url": None}, None # 单文件链接已保存,zip 失败不报错
|
||||
return {"urls": urls, "zip_url": zip_url}, None
|
||||
|
||||
|
||||
def _background_brand_task(task_id):
|
||||
"""后台执行单个品牌爬虫任务并更新数据库"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, user_id, file_paths, strategy, `desc` FROM brand_crawl_tasks WHERE id = %s AND status = 'pending'",
|
||||
(task_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return
|
||||
file_paths = row.get('file_paths')
|
||||
if isinstance(file_paths, str):
|
||||
file_paths = json.loads(file_paths) if file_paths else []
|
||||
# 优先使用表里存的 strategy 字段,兼容旧数据则从 desc 解析
|
||||
strategy = (row.get('strategy') or '').strip() or None
|
||||
if not strategy:
|
||||
desc_val = (row.get('desc') or '').strip()
|
||||
strategy = "Simple" if desc_val.startswith('[Simple]') else "Terms"
|
||||
if strategy not in ('Terms', 'Simple'):
|
||||
strategy = 'Terms'
|
||||
if not file_paths:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s",
|
||||
('没有有效文件路径', task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_push_task_event(task_id, {'status': 'failed', 'error_message': '没有有效文件路径'})
|
||||
return
|
||||
# 将 file_paths 解析为本地路径:URL 下载到 BRAND_OUTPUT_DIR 对应任务文件夹下(兼容旧数据中的本地路径)
|
||||
os.makedirs(BRAND_OUTPUT_DIR, exist_ok=True)
|
||||
task_subdir = str(task_id) if task_id else ('immediate_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S'))
|
||||
task_dir = os.path.join(BRAND_OUTPUT_DIR, task_subdir)
|
||||
os.makedirs(task_dir, exist_ok=True)
|
||||
|
||||
local_paths = []
|
||||
downloaded_files_to_cleanup_on_fail = []
|
||||
for idx, p in enumerate(file_paths):
|
||||
if not p or not isinstance(p, str):
|
||||
file_ls = data.get("files")
|
||||
strategy = data.get("strategy")
|
||||
if not isinstance(file_ls, list):
|
||||
file_ls = []
|
||||
tasks = []
|
||||
for file_data in file_ls:
|
||||
rows = file_data.get("rows") or []
|
||||
brand_ls = [i.get("品牌") for i in rows if i.get("品牌")]
|
||||
fileUrl = file_data.get("fileUrl")
|
||||
if not brand_ls:
|
||||
continue
|
||||
p = p.strip()
|
||||
if _is_url(p):
|
||||
try:
|
||||
parsed = urlparse(p)
|
||||
url_name = os.path.basename(parsed.path or '')
|
||||
if not url_name:
|
||||
url_name = f"input_{idx + 1}.xlsx"
|
||||
safe_name = "".join(c if c.isalnum() or c in '-_.' else '_' for c in url_name)
|
||||
if not safe_name.lower().endswith('.xlsx'):
|
||||
safe_name = safe_name + '.xlsx'
|
||||
for i in range(0, len(brand_ls), 5):
|
||||
chunk = brand_ls[i:i + 5]
|
||||
tasks.append((chunk, fileUrl))
|
||||
|
||||
base_no_ext, ext = os.path.splitext(safe_name)
|
||||
dest = os.path.join(task_dir, safe_name)
|
||||
n = 1
|
||||
while os.path.exists(dest):
|
||||
dest = os.path.join(task_dir, f"{base_no_ext}_{n}{ext}")
|
||||
n += 1
|
||||
|
||||
r = requests.get(p, timeout=120, stream=True)
|
||||
r.raise_for_status()
|
||||
with open(dest, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024 * 1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
local_paths.append(dest)
|
||||
downloaded_files_to_cleanup_on_fail.append(dest)
|
||||
except Exception as e:
|
||||
for tf in downloaded_files_to_cleanup_on_fail:
|
||||
try:
|
||||
os.unlink(tf)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s",
|
||||
(f'下载文件失败: {str(e)}', task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_push_task_event(task_id, {'status': 'failed', 'error_message': f'下载文件失败: {str(e)}'})
|
||||
if tasks:
|
||||
max_workers = min(8, len(tasks))
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
if _is_task_cancelled(task_id):
|
||||
_push_task_event(task_id, {'status': 'cancelled'})
|
||||
return
|
||||
elif os.path.isfile(p):
|
||||
local_paths.append(p)
|
||||
if not local_paths:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s",
|
||||
('没有有效的本地或可下载文件', task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_push_task_event(task_id, {'status': 'failed', 'error_message': '没有有效的本地或可下载文件'})
|
||||
return
|
||||
try:
|
||||
# 原子地将 pending 改为 running,避免用户先点取消时仍被覆盖为 running
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'running', progress_total = %s, progress_current = 0 WHERE id = %s AND status = 'pending'",
|
||||
(len(local_paths), task_id)
|
||||
)
|
||||
n = cur.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if n == 0:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT status FROM brand_crawl_tasks WHERE id = %s", (task_id,))
|
||||
r = cur.fetchone()
|
||||
conn.close()
|
||||
if r and (r.get('status') or '') == 'cancelled':
|
||||
futures = [
|
||||
executor.submit(_run_brand_single, task_id, chunk, file_url, strategy,len(brand_ls),len(tasks),chunkIndex+1)
|
||||
for chunkIndex,(chunk, file_url) in enumerate(tasks)
|
||||
]
|
||||
for future in as_completed(futures):
|
||||
if _is_task_cancelled(task_id):
|
||||
for f in futures:
|
||||
f.cancel()
|
||||
_push_task_event(task_id, {'status': 'cancelled'})
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def check_cancelled():
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT status FROM brand_crawl_tasks WHERE id = %s", (task_id,))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
return row and (row.get('status') or '') == 'cancelled'
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
result_paths, err = _run_brand_files(local_paths, task_id=task_id, check_cancelled=check_cancelled, strategy=strategy)
|
||||
if err:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
st = 'cancelled' if err == 'cancelled' else 'failed'
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = %s, error_message = %s WHERE id = %s",
|
||||
(st, err if st == 'failed' else None, task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print(e)
|
||||
pass
|
||||
_push_task_event(task_id, {'status': st, 'error_message': err if st == 'failed' else None})
|
||||
return
|
||||
result_data, upload_err = _upload_results_to_oss(local_paths, result_paths, task_id)
|
||||
if upload_err:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s",
|
||||
(upload_err, task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_push_task_event(task_id, {'status': 'failed', 'error_message': upload_err})
|
||||
return
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'success', result_paths = %s WHERE id = %s",
|
||||
(json.dumps(result_data), task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_push_task_event(task_id, {'status': 'success'})
|
||||
finally:
|
||||
pass
|
||||
return
|
||||
future.result()
|
||||
_push_task_event(task_id, {'status': 'success'})
|
||||
except Exception as e:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s",
|
||||
(str(e), task_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
print("执行出错",traceback.format_exc())
|
||||
_push_task_event(task_id, {'status': 'failed', 'error_message': str(e)})
|
||||
print("执行完成")
|
||||
|
||||
|
||||
def _parse_json(val, default=None):
|
||||
@@ -494,6 +312,9 @@ def _normalize_result_paths(raw):
|
||||
def api_brand_expand_folder():
|
||||
"""展开文件夹,返回其下所有 .xlsx 文件路径列表"""
|
||||
try:
|
||||
_, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
data = request.get_json() or {}
|
||||
folder = (data.get('folder') or '').strip()
|
||||
if not folder:
|
||||
@@ -503,12 +324,33 @@ def api_brand_expand_folder():
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
def _expand_folder_xlsx_recursive_items(folder_path):
|
||||
"""返回文件夹下所有 .xlsx 文件的绝对路径和相对路径列表(递归)"""
|
||||
if not folder_path or not os.path.isdir(folder_path):
|
||||
return []
|
||||
items = []
|
||||
root = os.path.normpath(folder_path)
|
||||
for current_root, _, filenames in os.walk(root):
|
||||
for name in filenames:
|
||||
if not (name.endswith('.xlsx') or name.endswith('.XLSX')):
|
||||
continue
|
||||
absolute_path = os.path.normpath(os.path.join(current_root, name))
|
||||
relative_path = os.path.relpath(absolute_path, root).replace('\\', '/')
|
||||
items.append({
|
||||
'absolutePath': absolute_path,
|
||||
'relativePath': relative_path,
|
||||
})
|
||||
items.sort(key=lambda item: item['relativePath'])
|
||||
return items
|
||||
|
||||
@brand_bp.route('/api/brand/expand-folder-recursive', methods=['POST'])
|
||||
@login_required
|
||||
def api_brand_expand_folder_recursive():
|
||||
"""递归展开文件夹,返回 .xlsx 文件绝对路径及相对路径列表"""
|
||||
try:
|
||||
_, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
data = request.get_json() or {}
|
||||
folder = (data.get('folder') or '').strip()
|
||||
if not folder:
|
||||
@@ -542,65 +384,58 @@ def api_brand_run():
|
||||
if not paths:
|
||||
return jsonify({'success': False, 'error': '没有有效的 xlsx 文件路径'}), 400
|
||||
# 先上传到 OSS,获取链接再入库
|
||||
user_id = session['user_id']
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
urls = []
|
||||
for p in paths:
|
||||
url, err = _upload_local_xlsx_to_oss(p, user_id)
|
||||
if err:
|
||||
return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500
|
||||
urls.append(url)
|
||||
desc_core = ', '.join(os.path.basename(p) for p in paths)[:500] if paths else ''
|
||||
desc = f'[{strategy}] ' + (desc_core or '')
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO brand_crawl_tasks (user_id, file_paths, `desc`, strategy, status, task_type) VALUES (%s, %s, %s, %s, 'pending', %s)",
|
||||
(user_id, json.dumps(urls), desc or None, strategy, task_type)
|
||||
)
|
||||
task_id = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
threading.Thread(target=_background_brand_task, args=(task_id,), daemon=True).start()
|
||||
return jsonify({'success': True, 'task_id': task_id})
|
||||
base_name = os.path.basename(p)
|
||||
urls.append({"fileUrl": url,"originalFilename": base_name,"relativePath": p })
|
||||
|
||||
# 请求提交
|
||||
resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id}",headers={
|
||||
"content-type":"application/json",
|
||||
},data=json.dumps({ "files": urls, "strategy": strategy,"taskType": 1,"archiveName": ""}))
|
||||
print({ "files": urls, "strategy": strategy,"taskType": 1,"archiveName": ""})
|
||||
resp_data = resp.json()
|
||||
# print(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id} 返回:",resp_data,"状态:",resp.status_code)
|
||||
if resp_data.get("success"):
|
||||
task_id = resp_data.get("data").get("taskId")
|
||||
data = resp_data.get("data")
|
||||
threading.Thread(target=_background_brand_task, args=(task_id,data), daemon=True).start()
|
||||
return jsonify({'success': True, 'task_id': task_id})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': "请求接口失败"}), 500
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@brand_bp.route('/api/brand/tasks', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
# @login_required
|
||||
def api_brand_tasks():
|
||||
"""GET: 获取当前用户的任务列表;POST: 添加任务(后台执行)"""
|
||||
if request.method == 'GET':
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT id, file_paths, `desc`, strategy, status, result_paths, error_message,
|
||||
COALESCE(progress_current, 0) AS progress_current,
|
||||
COALESCE(progress_total, 0) AS progress_total,
|
||||
created_at, updated_at
|
||||
FROM brand_crawl_tasks WHERE user_id = %s ORDER BY id DESC LIMIT 100""",
|
||||
(session['user_id'],)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
items = []
|
||||
for r in rows:
|
||||
items.append({
|
||||
'id': r['id'],
|
||||
'file_paths': _parse_json(r['file_paths'], []),
|
||||
'desc': (r.get('desc') or '').strip() or None,
|
||||
'strategy': (r.get('strategy') or 'Terms').strip() or 'Terms',
|
||||
'status': r.get('status') or 'pending',
|
||||
'result_paths': _parse_json(r['result_paths']),
|
||||
'error_message': (r.get('error_message') or '').strip() or None,
|
||||
'progress_current': int(r.get('progress_current') or 0),
|
||||
'progress_total': int(r.get('progress_total') or 0),
|
||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||
'updated_at': r['updated_at'].strftime('%Y-%m-%d %H:%M') if r.get('updated_at') else '',
|
||||
})
|
||||
return jsonify({'success': True, 'items': items})
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
resp = requests.get(f"{JAVA_API_BASE}/api/brand/tasks", params={
|
||||
"userId": user_id
|
||||
})
|
||||
resp_data = resp.json()
|
||||
print("获取列表",resp.text)
|
||||
if resp_data.get("success"):
|
||||
items = resp_data.get("data").get("items")
|
||||
return jsonify({'success': True, 'items': items})
|
||||
return jsonify({'success': True, 'items': []})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print("获取列表失败",e)
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
# POST
|
||||
try:
|
||||
@@ -621,111 +456,95 @@ def api_brand_tasks():
|
||||
paths = [p.strip() for p in paths if p and isinstance(p, str)]
|
||||
if not paths:
|
||||
return jsonify({'success': False, 'error': '请提供至少一个文件路径或链接'}), 400
|
||||
user_id = session['user_id']
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
urls = []
|
||||
for p in paths:
|
||||
if _is_url(p):
|
||||
urls.append(p)
|
||||
elif os.path.isfile(p):
|
||||
url, err = _upload_local_xlsx_to_oss(p, user_id)
|
||||
if err:
|
||||
return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500
|
||||
urls.append(url)
|
||||
else:
|
||||
return jsonify({'success': False, 'error': f'无效路径或链接: {p[:80]}'}), 400
|
||||
desc_core = ', '.join(os.path.basename(urlparse(u).path or u) for u in urls)[:500] if urls else ''
|
||||
desc = f'[{strategy}] ' + (desc_core or '')
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO brand_crawl_tasks (user_id, file_paths, `desc`, strategy, status, task_type) VALUES (%s, %s, %s, %s, 'pending', %s)",
|
||||
(user_id, json.dumps(urls), desc or None, strategy, task_type)
|
||||
)
|
||||
task_id = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# threading.Thread(target=_background_brand_task, args=(task_id,), daemon=True).start()
|
||||
return jsonify({'success': True, 'task_id': task_id})
|
||||
url, err = _upload_local_xlsx_to_oss(p, user_id)
|
||||
if err:
|
||||
return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500
|
||||
base_name = os.path.basename(p)
|
||||
urls.append({"fileUrl": url, "originalFilename": base_name, "relativePath": p})
|
||||
params = {
|
||||
"userId": user_id
|
||||
}
|
||||
# resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id}",headers={
|
||||
# "content-type":"application/json",
|
||||
# },data=json.dumps({ "files": urls, "strategy": strategy,"taskType": 1,"archiveName": ""}))
|
||||
req_data = {"files": urls, "strategy": strategy, "taskType": 2, "archiveName": ""}
|
||||
resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id}", headers={"content-type": "application/json"},
|
||||
data=json.dumps(req_data))
|
||||
print(req_data)
|
||||
# print(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id},返回", resp.text)
|
||||
resp_data = resp.json()
|
||||
if resp_data.get("success"):
|
||||
task_id = resp_data.get("data").get("taskId")
|
||||
data = resp_data.get("data")
|
||||
return jsonify({'success': True, 'task_id': task_id})
|
||||
return jsonify({'success': False, 'task_id': "请求异常"})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@brand_bp.route('/api/brand/tasks/<int:task_id>')
|
||||
@login_required
|
||||
# @login_required
|
||||
def api_brand_task_detail(task_id):
|
||||
"""获取单个任务详情(用于轮询状态)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT id, file_paths, `desc`, strategy, status, result_paths, error_message,
|
||||
COALESCE(progress_current, 0) AS progress_current,
|
||||
COALESCE(progress_total, 0) AS progress_total,
|
||||
created_at, updated_at
|
||||
FROM brand_crawl_tasks WHERE id = %s AND user_id = %s""",
|
||||
(task_id, session['user_id'])
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'task': {
|
||||
'id': row['id'],
|
||||
'file_paths': _parse_json(row['file_paths'], []),
|
||||
'desc': (row.get('desc') or '').strip() or None,
|
||||
'strategy': (row.get('strategy') or 'Terms').strip() or 'Terms',
|
||||
'status': row.get('status') or 'pending',
|
||||
'result_paths': _parse_json(row['result_paths']),
|
||||
'error_message': (row.get('error_message') or '').strip() or None,
|
||||
'progress_current': int(row.get('progress_current') or 0),
|
||||
'progress_total': int(row.get('progress_total') or 0),
|
||||
'created_at': row['created_at'].strftime('%Y-%m-%d %H:%M') if row.get('created_at') else '',
|
||||
'updated_at': row['updated_at'].strftime('%Y-%m-%d %H:%M') if row.get('updated_at') else '',
|
||||
}
|
||||
})
|
||||
resp = requests.get(f"{JAVA_API_BASE}/api/brand/tasks/{task_id}")
|
||||
resp_data = resp.json()
|
||||
if resp_data.get("success"):
|
||||
task = resp_data["data"]["task"]
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'task': task
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@brand_bp.route('/api/brand/tasks/<int:task_id>/line-progress')
|
||||
@login_required
|
||||
# @login_required
|
||||
def api_brand_task_line_progress(task_id):
|
||||
"""
|
||||
获取任务的“当前文件行级进度”(当前行数/总行数)。
|
||||
该信息仅存放在内存字典 _task_line_progress 中,不写入数据库。
|
||||
"""
|
||||
try:
|
||||
with _task_line_progress_lock:
|
||||
info = _task_line_progress.get(task_id)
|
||||
if not info:
|
||||
return jsonify({'success': True, 'has_progress': False})
|
||||
# 为了避免暴露完整路径,只返回文件名
|
||||
file_name = os.path.basename(info.get('file_path') or '') if info.get('file_path') else ''
|
||||
resp = {
|
||||
'file_index': int(info.get('file_index') or 0),
|
||||
'file_total': int(info.get('file_total') or 0),
|
||||
'file_name': file_name,
|
||||
'current_line': int(info.get('current_line') or 0),
|
||||
'total_lines': int(info.get('total_lines') or 0),
|
||||
}
|
||||
return jsonify({'success': True, 'has_progress': True, 'info': resp})
|
||||
resp = requests.get(f"{JAVA_API_BASE}/api/brand/tasks/{task_id}")
|
||||
resp_data = resp.json()
|
||||
if resp_data.get("success"):
|
||||
if resp_data["data"]["line_progress"]["has_progress"]:
|
||||
info = resp_data["data"]["line_progress"]["info"]
|
||||
resp = {
|
||||
'file_index': int(info.get('file_index') or 0),
|
||||
'file_total': int(info.get('file_total') or 0),
|
||||
'file_name': info.get("file_name",""),
|
||||
'current_line': int(info.get('current_line') or 0),
|
||||
'total_lines': int(info.get('total_lines') or 0),
|
||||
}
|
||||
return jsonify({'success': True, 'has_progress': True, 'info': resp})
|
||||
raise RuntimeError("获取进度失败")
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@brand_bp.route('/api/brand/tasks/<int:task_id>/events')
|
||||
@login_required
|
||||
# @login_required
|
||||
def api_brand_task_events(task_id):
|
||||
"""SSE:任务完成时后端主动推送事件,前端监听后隐藏进度条与取消按钮"""
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
|
||||
def _task_status_event():
|
||||
conn = get_db()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT status, error_message FROM brand_crawl_tasks WHERE id = %s AND user_id = %s",
|
||||
(task_id, session['user_id'])
|
||||
(task_id, user_id)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
finally:
|
||||
@@ -778,21 +597,28 @@ def api_brand_task_events(task_id):
|
||||
|
||||
|
||||
@brand_bp.route('/api/brand/tasks/<int:task_id>/cancel', methods=['POST'])
|
||||
@login_required
|
||||
# @login_required
|
||||
def api_brand_task_cancel(task_id):
|
||||
"""取消正在执行或等待中的任务(pending/running 均可取消)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE brand_crawl_tasks SET status = 'cancelled' WHERE id = %s AND user_id = %s AND status IN ('pending', 'running')",
|
||||
(task_id, session['user_id'])
|
||||
)
|
||||
n = cur.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if n == 0:
|
||||
return jsonify({'success': False, 'error': '任务不存在或无法取消'}), 400
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
|
||||
|
||||
resp = requests.post(
|
||||
f"{JAVA_API_BASE}/api/brand/tasks/{task_id}/cancel",
|
||||
headers={"accept": "application/json"},
|
||||
timeout=20
|
||||
)
|
||||
try:
|
||||
resp_data = resp.json()
|
||||
except Exception:
|
||||
return jsonify({'success': False, 'error': '取消接口返回非 JSON'}), 500
|
||||
if not resp_data.get('success'):
|
||||
return jsonify({'success': False, 'error': '第三方取消任务失败'}), 400
|
||||
|
||||
_mark_task_cancelled(task_id)
|
||||
_push_task_event(task_id, {'status': 'cancelled'})
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
@@ -804,17 +630,21 @@ def api_brand_task_cancel(task_id):
|
||||
def api_brand_task_delete(task_id):
|
||||
"""删除任务(仅限非 running 状态)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM brand_crawl_tasks WHERE id = %s AND user_id = %s AND status != 'running'",
|
||||
(task_id, session['user_id'])
|
||||
)
|
||||
n = cur.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if n == 0:
|
||||
return jsonify({'success': False, 'error': '任务不存在或正在执行中无法删除'}), 400
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
|
||||
resp = requests.delete(
|
||||
f"{JAVA_API_BASE}/api/brand/tasks/{task_id}",
|
||||
headers={"accept": "application/json"},
|
||||
timeout=20
|
||||
)
|
||||
try:
|
||||
resp_data = resp.json()
|
||||
except Exception:
|
||||
return jsonify({'success': False, 'error': '取消接口返回非 JSON'}), 500
|
||||
if not resp_data.get('success'):
|
||||
return jsonify({'success': False, 'error': '第三方取消任务失败'}), 400
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
@@ -825,11 +655,15 @@ def api_brand_task_delete(task_id):
|
||||
def api_brand_download(task_id):
|
||||
"""下载任务结果:优先使用 OSS zip_url 重定向;否则本地/单链接/多链接按原逻辑处理"""
|
||||
try:
|
||||
user_id, err = _get_user_id_or_error()
|
||||
if err:
|
||||
return err
|
||||
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT result_paths FROM brand_crawl_tasks WHERE id = %s AND user_id = %s",
|
||||
(task_id, session['user_id'])
|
||||
(task_id, user_id)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
BIN
app/brand_spider/__pycache__/main.cpython-311.pyc
Normal file
BIN
app/brand_spider/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/brand_spider/__pycache__/web_dec.cpython-311.pyc
Normal file
BIN
app/brand_spider/__pycache__/web_dec.cpython-311.pyc
Normal file
Binary file not shown.
@@ -3,10 +3,7 @@ import re
|
||||
import random
|
||||
import requests
|
||||
import time
|
||||
import datetime
|
||||
import openpyxl
|
||||
import unicodedata
|
||||
from openpyxl import Workbook
|
||||
|
||||
from brand_spider.web_dec import decrypt_via_service, get_guid
|
||||
|
||||
@@ -77,7 +74,7 @@ def search(hashsearch, data, proxies=None):
|
||||
# print("原始结果", enc_text)
|
||||
|
||||
# 若返回 Forbidden,则从 config 的 proxy_url 获取代理 IP 后重试,并开启 3 分钟代理窗口
|
||||
if enc_text.strip() == '{"message":"Forbidden"}' and CONFIG_PROXY_URL:
|
||||
if (enc_text.strip() == '{"message":"Forbidden"}' or "Too Many Requests" in enc_text) and CONFIG_PROXY_URL:
|
||||
try:
|
||||
proxy_resp = requests.get(CONFIG_PROXY_URL, timeout=10)
|
||||
print("代理请求结果->:", proxy_resp.text)
|
||||
@@ -125,7 +122,7 @@ class TaskCancelledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def single_file_handle(file_path, output_path, strategy="Terms", check_cancelled=None, progress_callback=None):
|
||||
def single_file_handle(brand, strategy="Terms"):
|
||||
status_info = {
|
||||
"Ended": "已结束",
|
||||
"Expired": "已过期的",
|
||||
@@ -471,208 +468,123 @@ def single_file_handle(file_path, output_path, strategy="Terms", check_cancelled
|
||||
"欧洲联盟"
|
||||
]
|
||||
check_staus = ["已过期的","已结束"]
|
||||
# 1. 读取 Excel 活动工作表
|
||||
wb = openpyxl.load_workbook(file_path, data_only=True)
|
||||
active_sheet = wb.active
|
||||
active_sheet_name = active_sheet.title
|
||||
print(active_sheet_name)
|
||||
|
||||
# 2. 将数据转换为列表嵌套字典
|
||||
rows = list(active_sheet.iter_rows(values_only=True))
|
||||
if rows:
|
||||
columns = list(rows[0])
|
||||
data_rows = []
|
||||
for row in rows[1:]:
|
||||
row_dict = {}
|
||||
for idx, col in enumerate(columns):
|
||||
row_dict[col] = row[idx] if idx < len(row) else None
|
||||
data_rows.append(row_dict)
|
||||
else:
|
||||
columns = []
|
||||
data_rows = []
|
||||
|
||||
# 3. 确保必要列存在
|
||||
# required_cols = ["状态", "国家", "时间"]
|
||||
# for col in required_cols:
|
||||
# if col not in columns:
|
||||
# columns.append(col)
|
||||
# for row_dict in data_rows:
|
||||
# row_dict[col] = ""
|
||||
|
||||
# 4. 提取唯一品牌列表
|
||||
brand_ls = set()
|
||||
for row in data_rows:
|
||||
brand = row.get("品牌")
|
||||
if brand:
|
||||
brand_ls.add(brand)
|
||||
|
||||
# 5. 初始化不符合品牌的数据列表
|
||||
faild_data = []
|
||||
# 初始化查询失败的品牌的数据列表
|
||||
query_faild_data = []
|
||||
|
||||
# 6. 对每个品牌进行处理
|
||||
brands = list(brand_ls)
|
||||
total_brands = len(brands)
|
||||
for idx, brand in enumerate(brands, start=1):
|
||||
if check_cancelled and check_cancelled():
|
||||
raise TaskCancelledError()
|
||||
|
||||
# 行级进度回调:当前第 idx 条 / total_brands
|
||||
if progress_callback and total_brands > 0:
|
||||
try:
|
||||
progress_callback(idx, total_brands, extra={'current_brand': str(brand)})
|
||||
except Exception:
|
||||
# 回调失败不影响主流程
|
||||
pass
|
||||
# Simple 策略下最多翻 3 页(0,30,60),直到命中 554 行判断
|
||||
max_pages = 3 if strategy == "Simple" else 1
|
||||
page = 0
|
||||
matched = False
|
||||
|
||||
# Simple 策略下最多翻 3 页(0,30,60),直到命中 554 行判断
|
||||
max_pages = 3 if strategy == "Simple" else 1
|
||||
page = 0
|
||||
matched = False
|
||||
while page < max_pages and not matched:
|
||||
try:
|
||||
as_structure_dict = {
|
||||
"_id": create_code_generator(),
|
||||
"boolean": "AND",
|
||||
"bricks": [
|
||||
{
|
||||
"_id": create_code_generator(),
|
||||
"key": "brandName",
|
||||
"value": brand,
|
||||
"strategy": strategy
|
||||
}
|
||||
]
|
||||
}
|
||||
# 构造 asStructure 内部 JSON 对象
|
||||
as_structure_dict = as_structure_dict
|
||||
# print(as_structure_dict)
|
||||
|
||||
while page < max_pages and not matched:
|
||||
try:
|
||||
as_structure_dict = {
|
||||
"_id": create_code_generator(),
|
||||
"boolean": "AND",
|
||||
"bricks": [
|
||||
{
|
||||
"_id": create_code_generator(),
|
||||
"key": "brandName",
|
||||
"value": brand,
|
||||
"strategy": strategy
|
||||
}
|
||||
]
|
||||
}
|
||||
# 构造 asStructure 内部 JSON 对象
|
||||
as_structure_dict = as_structure_dict
|
||||
# print(as_structure_dict)
|
||||
# 将内部对象转为 JSON 字符串(作为 asStructure 字段的值)
|
||||
as_structure_str = json.dumps(as_structure_dict, ensure_ascii=False)
|
||||
data = {
|
||||
"sort": "score desc",
|
||||
"rows": "30",
|
||||
"asStructure": as_structure_str,
|
||||
"fg": "_void_"
|
||||
}
|
||||
|
||||
# 将内部对象转为 JSON 字符串(作为 asStructure 字段的值)
|
||||
as_structure_str = json.dumps(as_structure_dict, ensure_ascii=False)
|
||||
data = {
|
||||
"sort": "score desc",
|
||||
"rows": "30",
|
||||
"asStructure": as_structure_str,
|
||||
"fg": "_void_"
|
||||
}
|
||||
# 翻页:第二页 start=30,第三页 start=60(仅 Simple 策略)
|
||||
if strategy == "Simple" and page > 0:
|
||||
data["start"] = str(30 * page)
|
||||
# 翻页:第二页 start=30,第三页 start=60(仅 Simple 策略)
|
||||
if strategy == "Simple" and page > 0:
|
||||
data["start"] = str(30 * page)
|
||||
|
||||
# print("请求参数", as_structure_dict, "页码:", page)
|
||||
hashsearch = get_guid()
|
||||
enc_text = search(hashsearch, data)
|
||||
print(f"【{hashsearch}】待解密-->", enc_text)
|
||||
dec_text = decrypt_via_service(hashsearch, enc_text)
|
||||
# print(type(dec_text))
|
||||
dec_text = str(dec_text)
|
||||
# print(f"解密之后的结果-->", dec_text)
|
||||
data = json.loads(dec_text)
|
||||
except Exception as e:
|
||||
# 仅第一页请求失败时记录为查询失败品牌
|
||||
if page == 0:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
safe_brand = brand.encode('gbk', errors='replace').decode('gbk')
|
||||
print(f"品牌:{safe_brand},处理失败:{e}")
|
||||
query_faild_data.append({"品牌": brand, "时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")})
|
||||
break
|
||||
# print("请求参数", as_structure_dict, "页码:", page)
|
||||
hashsearch = get_guid()
|
||||
enc_text = search(hashsearch, data)
|
||||
print(f"【{hashsearch}】待解密-->", enc_text)
|
||||
dec_text = decrypt_via_service(hashsearch, enc_text)
|
||||
# print(type(dec_text))
|
||||
dec_text = str(dec_text)
|
||||
# print(f"解密之后的结果-->", dec_text)
|
||||
data = json.loads(dec_text)
|
||||
except Exception as e:
|
||||
# 仅第一页请求失败时记录为查询失败品牌
|
||||
if page == 0:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
safe_brand = brand.encode('gbk', errors='replace').decode('gbk')
|
||||
print(f"品牌:{safe_brand},处理失败:{e}")
|
||||
# query_faild_data.append({"brand": brand, "time": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")})
|
||||
query_faild_data.append(brand)
|
||||
break
|
||||
|
||||
# print(data)
|
||||
# print(data)
|
||||
|
||||
if data.get("response", {}).get("numFound", 0) > 0:
|
||||
if strategy == "Simple":
|
||||
new_brand_name = clean_text(brand)
|
||||
_brand = new_brand_name.lower()
|
||||
else:
|
||||
_brand = brand
|
||||
|
||||
docs = data["response"]["docs"]
|
||||
for d in docs:
|
||||
office = d.get("office")
|
||||
status = d.get("status")
|
||||
registrationDate = d.get("registrationDate")
|
||||
status_name = status_info.get(status, status)
|
||||
brand_name = d.get("brandName")
|
||||
if isinstance(brand_name, list):
|
||||
brand_name = "|".join(brand_name)
|
||||
|
||||
if data.get("response", {}).get("numFound", 0) > 0:
|
||||
if strategy == "Simple":
|
||||
new_brand_name = clean_text(brand)
|
||||
_brand = new_brand_name.lower()
|
||||
else:
|
||||
_brand = brand
|
||||
new_brand_name = clean_text(brand_name)
|
||||
new_brand_name = new_brand_name.lower()
|
||||
if _brand != new_brand_name:
|
||||
continue
|
||||
|
||||
docs = data["response"]["docs"]
|
||||
for d in docs:
|
||||
office = d.get("office")
|
||||
status = d.get("status")
|
||||
registrationDate = d.get("registrationDate")
|
||||
status_name = status_info.get(status, status)
|
||||
brand_name = d.get("brandName")
|
||||
if isinstance(brand_name, list):
|
||||
brand_name = "|".join(brand_name)
|
||||
check_office = d.get("designation")
|
||||
|
||||
if strategy == "Simple":
|
||||
new_brand_name = clean_text(brand_name)
|
||||
new_brand_name = new_brand_name.lower()
|
||||
if _brand != new_brand_name:
|
||||
check_office.append(office)
|
||||
for i in set(check_office):
|
||||
office_name = country_info.get(i, i)
|
||||
# print(office_name,office_name in check_value and status_name not in check_staus)
|
||||
if office_name in check_value and status_name not in check_staus:
|
||||
# 命中 554 行判断,记录并结束当前品牌后续翻页
|
||||
# data_rows = [row for row in data_rows if row.get("品牌") != brand]
|
||||
# print(office_name, office in ["EM","QZ"] and i not in ["EM","QZ"])
|
||||
if office in ["EM","QZ"] and i not in ["EM","QZ"]:
|
||||
continue
|
||||
|
||||
check_office = d.get("designation")
|
||||
if strategy == "Simple":
|
||||
faild_data.append({"brand": brand, "country": office_name, "status": status_name}) # "reason":"|".join(d.get("brandName","")
|
||||
matched = True
|
||||
else:
|
||||
# print("添加",brand,office_name)
|
||||
faild_data.append({"brand": brand, "country": office_name, "status": status_name})
|
||||
# break
|
||||
if matched:
|
||||
break
|
||||
|
||||
check_office.append(office)
|
||||
for i in set(check_office):
|
||||
office_name = country_info.get(i, i)
|
||||
# print(office_name,office_name in check_value and status_name not in check_staus)
|
||||
if office_name in check_value and status_name not in check_staus:
|
||||
# 命中 554 行判断,记录并结束当前品牌后续翻页
|
||||
data_rows = [row for row in data_rows if row.get("品牌") != brand]
|
||||
# print(office_name, office in ["EM","QZ"] and i not in ["EM","QZ"])
|
||||
if office in ["EM","QZ"] and i not in ["EM","QZ"]:
|
||||
continue
|
||||
# 如果 Simple 策略下本页未命中,则翻下一页;其他策略不翻页
|
||||
if not matched:
|
||||
page += 1
|
||||
|
||||
if strategy == "Simple":
|
||||
faild_data.append({"品牌": brand, "国家": office_name, "状态": status_name, "原因":"|".join(d.get("brandName","")),"时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")})
|
||||
matched = True
|
||||
else:
|
||||
# print("添加",brand,office_name)
|
||||
faild_data.append({"品牌": brand, "国家": office_name, "状态": status_name, "时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")})
|
||||
# break
|
||||
if matched:
|
||||
break
|
||||
|
||||
# 如果 Simple 策略下本页未命中,则翻下一页;其他策略不翻页
|
||||
if not matched:
|
||||
page += 1
|
||||
# else:
|
||||
# # 更新主表数据
|
||||
# for row in data_rows:
|
||||
# if row.get("品牌") == brand:
|
||||
# row["状态"] = status_name
|
||||
# row["国家"] = office_name
|
||||
# row["时间"] = datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S") # 品牌匹配行
|
||||
# else:
|
||||
# # 非匹配行:时间 = 当前国家值(模拟原 mask 逻辑)
|
||||
# row["时间"] = ""
|
||||
|
||||
# 7. 创建输出工作簿
|
||||
out_wb = Workbook()
|
||||
out_wb.remove(out_wb.active) # 删除默认 sheet
|
||||
|
||||
# 主工作表
|
||||
main_ws = out_wb.create_sheet(title=active_sheet_name)
|
||||
main_ws.append(columns)
|
||||
for row_dict in data_rows:
|
||||
main_ws.append([row_dict.get(col, "") for col in columns])
|
||||
|
||||
# 不符合品牌工作表
|
||||
faild_ws = out_wb.create_sheet(title="不符合品牌")
|
||||
if strategy == "Simple":
|
||||
faild_columns = ["品牌", "国家", "状态","原因"]
|
||||
else:
|
||||
faild_columns = ["品牌", "国家", "状态"]
|
||||
faild_ws.append(faild_columns)
|
||||
for row_dict in faild_data:
|
||||
# print("写入",row_dict)
|
||||
faild_ws.append([row_dict.get(col, "") for col in faild_columns])
|
||||
|
||||
# 查询失败品牌工作表
|
||||
query_faild_ws = out_wb.create_sheet(title="查询失败品牌")
|
||||
query_faild_columns = ["品牌", "时间"]
|
||||
query_faild_ws.append(query_faild_columns)
|
||||
for row_dict in query_faild_data:
|
||||
query_faild_ws.append([row_dict.get(col, "") for col in query_faild_columns])
|
||||
|
||||
out_wb.save(output_path)
|
||||
print("生成完成--->", output_path)
|
||||
return output_path
|
||||
return faild_data,query_faild_data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -6,9 +6,7 @@ STITCH_WORKFLOW_ID = "7608813873483300907"
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
||||
java_api_base = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
||||
JAVA_API_BASE = java_api_base
|
||||
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
||||
|
||||
# MySQL 配置
|
||||
mysql_host = os.getenv("mysql_host")
|
||||
@@ -18,8 +16,9 @@ mysql_database = os.getenv("mysql_database","aiimage")
|
||||
proxy_url = os.getenv("proxy_url")
|
||||
proxy_mode = int(os.getenv("proxy_mode",1))
|
||||
|
||||
|
||||
client_name=os.getenv("client_name") + ".exe"
|
||||
JAVA_API_BASE = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
||||
|
||||
|
||||
cache_path = "./user_data"
|
||||
|
||||
@@ -39,8 +38,8 @@ os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||
|
||||
|
||||
debug = True
|
||||
version = "1.0.3"
|
||||
debug = False
|
||||
version = "1.0.13"
|
||||
APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" }
|
||||
os.environ['APP_VERSION'] = version
|
||||
os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL
|
||||
|
||||
Reference in New Issue
Block a user