diff --git a/PODTrendAgent.spec b/PODTrendAgent.spec index f2226c1..cc9b1b5 100644 --- a/PODTrendAgent.spec +++ b/PODTrendAgent.spec @@ -13,9 +13,16 @@ a = Analysis( ], datas=[('config.yaml', '.'), ('configs', 'configs'), ('prompts', 'prompts'), ('templates', 'templates'), ('basemap', 'basemap'), - ('material_library', 'material_library')], + ('material_library', 'material_library'), + # Playwright 驱动(Node.js driver):Pinterest 爬取用 playwright 控制系统 Chrome。 + # 冻结后 _driver.py 按 inspect.getfile(playwright)/driver 定位,必须放 playwright/driver。 + ('C:\\Users\\Admin\\Desktop\\test模版\\design_agent\\pod_trend_agent\\.venv\\Lib\\site-packages\\playwright\\driver', 'playwright/driver')], # 注意:db/ 不打进 exe —— 运行时常读 exe 旁 db/spu_sku.db(用户可随时替换取最新) - hiddenimports=['pytrends', 'pytrends.request', 'PIL', 'openpyxl'], + hiddenimports=['pytrends', 'pytrends.request', 'PIL', 'openpyxl', + # Pinterest 参考模式:scraper 包 + playwright + 异步下载依赖 + 'pinterest_scraper', 'pinterest_scraper.scraper', + 'pinterest_scraper.pinterest_image_capture', + 'playwright', 'playwright.async_api', 'aiohttp', 'aiofiles'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/pinterest_scraper/pinterest_image_capture.py b/pinterest_scraper/pinterest_image_capture.py index 5efe4cb..bc07474 100644 --- a/pinterest_scraper/pinterest_image_capture.py +++ b/pinterest_scraper/pinterest_image_capture.py @@ -4,6 +4,7 @@ import mimetypes import os import random import socket +import sys import urllib.request from urllib.parse import urlparse, quote @@ -144,13 +145,20 @@ def get_system_proxy() -> str | None: def _session_user_data_dir() -> str: - """返回项目内持久化的 Chrome 用户数据目录。 + """返回持久化的 Chrome 用户数据目录。 该目录会一直保留(已加入 .gitignore),首次手动登录 Pinterest 后, 登录态(cookies 等)自动缓存在这里,后续运行直接复用,不再需要登录。 + 源码运行时:pinterest_scraper/.chrome_session(项目内); + 打包 exe 运行时:exe 同目录 pinterest_scraper/.chrome_session(持久, + 打包时把当前登录态复制过去即可复用,不随临时解压目录消失)。 """ - base = os.path.dirname(os.path.abspath(__file__)) - ud = os.path.join(base, ".chrome_session", "User Data") + if getattr(sys, "frozen", False): + base = os.path.dirname(os.path.abspath(sys.executable)) + ud = os.path.join(base, "pinterest_scraper", ".chrome_session", "User Data") + else: + base = os.path.dirname(os.path.abspath(__file__)) + ud = os.path.join(base, ".chrome_session", "User Data") os.makedirs(ud, exist_ok=True) return ud diff --git a/scripts/package.py b/scripts/package.py new file mode 100644 index 0000000..551e742 --- /dev/null +++ b/scripts/package.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +"""一键打包脚本:PyInstaller 构建(不跑自检)→ 同步数据目录 → 复制 Chrome 登录态 → 生成 dist_vXX。 + +用法(在项目根目录执行): + python scripts/package.py # 自动取下一个版本号 + python scripts/package.py 71 # 指定版本号 +""" +import argparse +import os +import re +import shutil +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DIST = os.path.join(ROOT, "dist") + +# 优先使用项目 .venv 的 Python(含 PyInstaller),避免系统 python 无该模块 +VENV_PY = os.path.join(ROOT, ".venv", "Scripts", "python.exe") +PYTHON = VENV_PY if os.path.isfile(VENV_PY) else sys.executable + +# 从源码同步到 dist 的目录/文件 +SYNC_ITEMS = [ + "config.yaml", + "configs", + "prompts", + "templates", + "basemap", + "material_library", + "db", + "output", + "logs", +] + +# 登录态目录:打包时把当前 Chrome 登录态复制到 exe 同目录,供冻结后复用 +CHROME_SESSION_SRC = os.path.join(ROOT, "pinterest_scraper", ".chrome_session") +CHROME_SESSION_DST = os.path.join(DIST, "pinterest_scraper", ".chrome_session") + + +def find_prev_dist() -> str | None: + """找到上一个 dist_vXX(用于同步 .cache,避免新 exe 首次运行重新采集)。""" + versions = [] + for name in os.listdir(ROOT): + m = re.fullmatch(r"dist_v(\d+)", name) + if m and os.path.isdir(os.path.join(ROOT, name)): + versions.append(int(m.group(1))) + if not versions: + return None + return os.path.join(ROOT, f"dist_v{max(versions)}") + + +def next_version() -> int: + versions = [] + for name in os.listdir(ROOT): + m = re.fullmatch(r"dist_v(\d+)", name) + if m and os.path.isdir(os.path.join(ROOT, name)): + versions.append(int(m.group(1))) + return (max(versions) + 1) if versions else 71 + + +def sync_dir(src: str, dst: str) -> None: + if not os.path.isdir(src): + print(f"[sync] 跳过(源不存在): {src}") + return + os.makedirs(dst, exist_ok=True) + for name in os.listdir(src): + s = os.path.join(src, name) + d = os.path.join(dst, name) + if os.path.isdir(s): + if os.path.isdir(d): + shutil.rmtree(d) + shutil.copytree(s, d) + else: + shutil.copy2(s, d) + print(f"[sync] 目录同步完成: {src} -> {dst}") + + +def sync_file(src: str, dst: str) -> None: + if not os.path.isfile(src): + print(f"[sync] 跳过(源不存在): {src}") + return + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy2(src, dst) + print(f"[sync] 文件同步完成: {src} -> {dst}") + + +def build_exe() -> None: + print("===== 1/4 PyInstaller 构建(不跑自检) =====") + if os.path.isdir(DIST): + shutil.rmtree(DIST) + os.makedirs(DIST, exist_ok=True) + subprocess.run( + [PYTHON, "-m", "PyInstaller", "--noconfirm", "--clean", "PODTrendAgent.spec"], + cwd=ROOT, check=True, + ) + exe = os.path.join(DIST, "PODTrendAgent.exe") + if not os.path.isfile(exe): + raise SystemExit(f"[error] 构建失败:未找到 {exe}") + print(f"[build] 构建完成: {exe} ({os.path.getsize(exe) / 1024 / 1024:.1f} MB)") + + +def sync_data(prev_dist: str | None) -> None: + print("===== 2/4 同步数据目录 =====") + for item in SYNC_ITEMS: + sync_file(os.path.join(ROOT, item), os.path.join(DIST, item)) \ + if os.path.isfile(os.path.join(ROOT, item)) \ + else sync_dir(os.path.join(ROOT, item), os.path.join(DIST, item)) + # .cache 从上一个 dist_vXX 同步(避免新 exe 首次运行重新采集) + if prev_dist: + prev_cache = os.path.join(prev_dist, ".cache") + if os.path.isdir(prev_cache): + sync_dir(prev_cache, os.path.join(DIST, ".cache")) + else: + print(f"[sync] 上一个版本无 .cache,跳过: {prev_cache}") + else: + print("[sync] 未找到上一个 dist_vXX,跳过 .cache 同步") + + +def copy_chrome_session() -> None: + print("===== 3/4 复制当前 Chrome 登录态 =====") + if os.path.isdir(CHROME_SESSION_SRC): + sync_dir(CHROME_SESSION_SRC, CHROME_SESSION_DST) + else: + print(f"[warn] 未找到 Chrome 登录态目录,跳过: {CHROME_SESSION_SRC}") + + +def rename_dist(version: int) -> str: + print("===== 4/4 生成 dist_vXX =====") + target = os.path.join(ROOT, f"dist_v{version}") + if os.path.isdir(target): + shutil.rmtree(target) + shutil.move(DIST, target) + print(f"[done] 发布目录: {target}") + return target + + +def main() -> None: + parser = argparse.ArgumentParser(description="PODTrendAgent 一键打包") + parser.add_argument("version", nargs="?", type=int, default=None, help="版本号(默认自动取下一个)") + args = parser.parse_args() + + version = args.version or next_version() + prev_dist = find_prev_dist() + print(f"目标版本: dist_v{version},上一个版本: {os.path.basename(prev_dist) if prev_dist else '无'}") + + build_exe() + sync_data(prev_dist) + copy_chrome_session() + target = rename_dist(version) + print(f"\n打包完成:{target}") + + +if __name__ == "__main__": + main()