[Python] 纯文本查看 复制代码
import os
import re
import json
import requests
from bs4 import BeautifulSoup
import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
from io import BytesIO
import threading
from urllib.parse import urljoin, urlparse
import webbrowser
import time
import random
class MxdBrowserPro:
def __init__(self, root):
self.root = root
self.root.title("梦想岛高级下载器 v7.8")
self.root.geometry("1200x800")
# 确保窗口关闭时正确退出
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
# 初始化分类
self.categories = {
"首页": "/",
"每日更新": "/gallery/",
"秀人网": "/jigou/1.html",
"异思趣向": "/jigou/7.html",
"ROSI写真": "/jigou/17.html",
"丽柜": "/jigou/19.html",
"语画界": "/jigou/593.html",
"尤蜜荟": "/jigou/98.html",
"克拉女神": "/jigou/1419.html",
"爱蜜社": "/jigou/118.html",
"美媛馆": "/jigou/1934.html",
"花漾show": "/jigou/128.html",
"嗲囡囡": "/jigou/830.html",
"丝袜美腿": "/tags/siwameitui.html",
"黑丝诱惑": "/tags/heisiyouhuo.html",
"性感少女": "/tags/xingganshaonv.html",
"日本少女": "/tags/ribenshaonv.html"
}
# 网站配置
self.base_url = "https://www.mxd009.cc/"
self.login_url = urljoin(self.base_url, "/e/member/doaction.php")
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': self.base_url,
'Origin': self.base_url,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 初始化数据
self.current_page = 1
self.max_page = 1
self.galleries = []
self.current_category = "首页"
self.downloading = False
self.logged_in = False
self.session = requests.Session()
self.session.headers.update(self.headers)
# 加载保存的cookies
self.load_cookies()
# 设置UI
self.setup_ui()
self.load_homepage()
def on_close(self):
"""窗口关闭事件处理"""
if self.downloading:
if not messagebox.askokcancel("退出", "当前有下载任务进行中,确定要退出吗?"):
return
self.root.destroy()
def load_cookies(self):
"""加载保存的cookies"""
if os.path.exists("mxd_cookies.json"):
try:
with open("mxd_cookies.json") as f:
cookies = json.load(f)
self.session.cookies.update(cookies)
# 验证cookies是否有效
test_url = urljoin(self.base_url, "/e/member/cp/")
response = self.session.get(test_url, timeout=10)
if "login" not in response.url.lower():
self.logged_in = True
except Exception as e:
print(f"加载cookies失败: {str(e)}")
def setup_ui(self):
"""设置现代化UI界面"""
# 主框架
main_frame = tk.Frame(self.root, bg="#f5f5f5")
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 顶部控制栏
control_frame = tk.Frame(main_frame, bg="#f5f5f5")
control_frame.pack(fill=tk.X, pady=(0, 10))
# 分类选择
tk.Label(control_frame, text="分类:", bg="#f5f5f5", font=('微软雅黑', 10)).pack(side=tk.LEFT, padx=5)
self.category_var = tk.StringVar()
self.category_combo = ttk.Combobox(
control_frame,
textvariable=self.category_var,
values=list(self.categories.keys()),
state="readonly",
width=18,
font=('微软雅黑', 10)
)
self.category_combo.pack(side=tk.LEFT, padx=5)
self.category_combo.current(0)
self.category_combo.bind("<<ComboboxSelected>>", self.on_category_change)
# 登录状态
login_status_text = "已登录(自动)" if self.logged_in else "未登录"
login_status_color = "green" if self.logged_in else "red"
self.login_status = tk.Label(
control_frame,
text=login_status_text,
bg="#f5f5f5",
fg=login_status_color,
font=('微软雅黑', 10),
padx=10
)
self.login_status.pack(side=tk.LEFT, padx=10)
# 登录按钮
self.login_btn = ttk.Button(
control_frame,
text="退出登录" if self.logged_in else "登录账号",
command=self.show_login_dialog,
width=10
)
self.login_btn.pack(side=tk.LEFT, padx=5)
# 翻页控制
page_frame = tk.Frame(control_frame, bg="#f5f5f5")
page_frame.pack(side=tk.LEFT, padx=20)
style = ttk.Style()
style.configure('TButton', padding=5, font=('微软雅黑', 9))
style.configure('TCombobox', font=('微软雅黑', 10))
self.prev_btn = ttk.Button(
page_frame,
text="◀ 上一页",
command=self.prev_page,
style='TButton'
)
self.prev_btn.pack(side=tk.LEFT, padx=2)
self.page_label = tk.Label(
page_frame,
text="1/1",
bg="#f5f5f5",
width=10,
font=('微软雅黑', 10)
)
self.page_label.pack(side=tk.LEFT)
self.next_btn = ttk.Button(
page_frame,
text="下一页 ▶",
command=self.next_page,
style='TButton'
)
self.next_btn.pack(side=tk.LEFT, padx=2)
# 下载控制
download_frame = tk.Frame(control_frame, bg="#f5f5f5")
download_frame.pack(side=tk.RIGHT)
self.download_btn = ttk.Button(
download_frame,
text="下载选中图集",
command=self.download_selected,
style='TButton'
)
self.download_btn.pack(side=tk.LEFT, padx=5)
self.open_folder_btn = ttk.Button(
download_frame,
text="打开下载目录",
command=self.open_download_folder,
style='TButton'
)
self.open_folder_btn.pack(side=tk.LEFT, padx=5)
# 主内容区
content_frame = tk.Frame(main_frame, bg="#ffffff", bd=1, relief=tk.SOLID)
content_frame.pack(fill=tk.BOTH, expand=True)
# 图集列表
self.tree = ttk.Treeview(
content_frame,
columns=("title", "model", "count", "tags", "source"),
show="headings",
selectmode="extended",
style='Custom.Treeview'
)
style.configure('Custom.Treeview', font=('微软雅黑', 10), rowheight=25)
style.configure('Custom.Treeview.Heading', font=('微软雅黑', 10, 'bold'))
self.tree.heading("title", text="标题")
self.tree.heading("model", text="模特")
self.tree.heading("count", text="图片数")
self.tree.heading("tags", text="标签")
self.tree.heading("source", text="来源")
self.tree.column("title", width=350, anchor='w')
self.tree.column("model", width=120, anchor='center')
self.tree.column("count", width=80, anchor='center')
self.tree.column("tags", width=200, anchor='w')
self.tree.column("source", width=150, anchor='w')
scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.bind("<<TreeviewSelect>>", self.on_item_select)
# 预览区域
preview_frame = tk.Frame(content_frame, bg="#ffffff", width=380, bd=1, relief=tk.SOLID)
preview_frame.pack_propagate(False)
preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH)
self.preview_img = tk.Label(preview_frame, bg="#f9f9f9")
self.preview_img.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.info_label = tk.Label(
preview_frame,
text="请选择图集查看详情",
bg="#ffffff",
fg="#666",
font=('微软雅黑', 10),
wraplength=360,
justify='left'
)
self.info_label.pack(fill=tk.X, padx=10, pady=5)
# 下载进度条
self.progress_frame = tk.Frame(preview_frame, bg="#ffffff")
self.progress_frame.pack(fill=tk.X, padx=10, pady=5)
self.progress_label = tk.Label(
self.progress_frame,
text="准备下载...",
bg="#ffffff",
fg="#333",
font=('微软雅黑', 9)
)
self.progress_label.pack(fill=tk.X)
self.progress_bar = ttk.Progressbar(
self.progress_frame,
orient='horizontal',
mode='determinate',
length=350
)
self.progress_bar.pack(fill=tk.X)
btn_frame = tk.Frame(preview_frame, bg="#ffffff")
btn_frame.pack(fill=tk.X, padx=10, pady=10)
self.detail_btn = ttk.Button(
btn_frame,
text="浏览器查看详情",
command=self.view_details,
style='TButton'
)
self.detail_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
self.download_all_btn = ttk.Button(
btn_frame,
text="下载全部图片",
command=self.download_selected,
style='TButton'
)
self.download_all_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
# 状态栏
self.status_var = tk.StringVar()
self.status_var.set("就绪 | 欢迎使用梦想岛图集下载器")
status_bar = tk.Label(
main_frame,
textvariable=self.status_var,
relief=tk.SUNKEN,
anchor=tk.W,
bg="#e0e0e0",
fg="#333",
font=('微软雅黑', 9)
)
status_bar.pack(fill=tk.X, pady=(10, 0))
def show_login_dialog(self):
"""显示登录对话框"""
if self.logged_in:
self.logout()
return
login_dialog = tk.Toplevel(self.root)
login_dialog.title("登录梦想岛")
login_dialog.geometry("300x200")
login_dialog.resizable(False, False)
login_dialog.grab_set()
login_dialog.transient(self.root)
# 居中对话框
window_width = 300
window_height = 200
screen_width = login_dialog.winfo_screenwidth()
screen_height = login_dialog.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
login_dialog.geometry(f"{window_width}x{window_height}+{x}+{y}")
# 用户名
tk.Label(login_dialog, text="用户名:", font=('微软雅黑', 10)).place(x=30, y=30)
username_var = tk.StringVar()
username_entry = ttk.Entry(login_dialog, textvariable=username_var, width=20)
username_entry.place(x=100, y=30)
username_entry.focus()
# 密码
tk.Label(login_dialog, text="密码:", font=('微软雅黑', 10)).place(x=30, y=70)
password_var = tk.StringVar()
password_entry = ttk.Entry(login_dialog, textvariable=password_var, width=20, show="*")
password_entry.place(x=100, y=70)
# 登录按钮
def attempt_login():
username = username_var.get()
password = password_var.get()
if not username or not password:
messagebox.showerror("错误", "用户名和密码不能为空")
return
login_dialog.destroy()
threading.Thread(target=self.perform_login, args=(username, password), daemon=True).start()
login_btn = ttk.Button(login_dialog, text="登录", command=attempt_login)
login_btn.place(x=120, y=120, width=80)
# 绑定回车键
login_dialog.bind('<Return>', lambda event: attempt_login())
def perform_login(self, username, password):
"""执行登录操作"""
self.status_var.set("正在登录...")
try:
# 1. 获取登录页面以获取必要的cookies
login_page_url = urljoin(self.base_url, "/e/member/login/")
login_page_response = self.session.get(login_page_url, timeout=15)
login_page_response.raise_for_status()
# 2. 检查是否有验证码要求
soup = BeautifulSoup(login_page_response.text, 'html.parser')
if soup.select_one('input[name="ecmscheck"]'):
self.root.after(0, lambda: messagebox.showwarning(
"需要验证码",
"当前登录需要验证码,请先在浏览器中登录后再使用本程序"
))
return False
# 3. 准备登录数据
login_data = {
'enews': 'login',
'username': username,
'password': password,
'ecmsfrom': self.base_url,
'tobind': '0',
'way': 'login',
'type': 'login',
'Submit': '登 录'
}
# 4. 设置正确的请求头
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': login_page_url,
'Origin': self.base_url
}
# 5. 发送登录请求
response = self.session.post(
self.login_url,
data=login_data,
headers=headers,
timeout=15,
allow_redirects=True
)
# 6. 验证登录结果
if response.status_code != 200:
raise Exception(f"HTTP状态码异常: {response.status_code}")
# 检查是否登录成功
if "登录成功" in response.text or "login" not in response.url.lower():
# 7. 验证会员中心访问
profile_url = urljoin(self.base_url, "/e/member/cp/")
profile_response = self.session.get(profile_url, timeout=10)
if "login" in profile_response.url.lower():
raise Exception("登录状态验证失败")
# 8. 登录成功处理
self.logged_in = True
self.root.after(0, lambda: self.login_status.config(text=f"已登录: {username}", fg="green"))
self.root.after(0, lambda: self.login_btn.config(text="退出登录"))
self.status_var.set(f"登录成功: {username}")
# 保存cookies
with open("mxd_cookies.json", "w") as f:
json.dump(self.session.cookies.get_dict(), f)
messagebox.showinfo("登录成功", "您已成功登录梦想岛")
return True
else:
# 提取具体错误信息
error_msg = "登录失败"
soup = BeautifulSoup(response.text, 'html.parser')
# 尝试从layui弹窗中提取错误
script_tags = soup.find_all('script')
for script in script_tags:
if script.string and 'alert' in script.string:
match = re.search(r'alert\(["\'](.*?)["\']\)', script.string)
if match:
error_msg = match.group(1)
break
if not error_msg:
error_div = soup.select_one('.layui-layer-content')
if error_div:
error_msg = error_div.get_text(strip=True)
raise Exception(error_msg or "未知登录错误")
except Exception as e:
error_msg = str(e)
# 保存错误响应供调试
with open("login_error.html", "w", encoding="utf-8") as f:
f.write(response.text if 'response' in locals() else "No response")
self.root.after(0, lambda: messagebox.showerror(
"登录失败",
f"登录失败: {error_msg}\n错误详情已保存到login_error.html"
))
self.status_var.set("登录失败")
return False
def logout(self):
"""退出登录"""
try:
# 发送退出请求
logout_url = urljoin(self.base_url, "/e/member/doaction.php?enews=exit")
self.session.get(logout_url, timeout=10)
# 删除cookies文件
if os.path.exists("mxd_cookies.json"):
os.remove("mxd_cookies.json")
except Exception as e:
print(f"退出登录时出错: {str(e)}")
self.logged_in = False
self.session = requests.Session() # 重置会话
self.session.headers.update(self.headers)
self.login_status.config(text="未登录", fg="red")
self.login_btn.config(text="登录账号")
self.status_var.set("已退出登录")
def load_homepage(self):
"""加载当前分类页面"""
if self.downloading:
return
self.status_var.set(f"正在加载 {self.current_category}...")
url = urljoin(self.base_url, self.categories[self.current_category])
# 处理分页
if self.current_page > 1:
if "?" in url:
url += f"&page={self.current_page}"
else:
url += f"page/{self.current_page}/" if not url.endswith("/") else f"page/{self.current_page}"
threading.Thread(target=self.fetch_page, args=(url,), daemon=True).start()
def fetch_page(self, url):
"""获取页面数据"""
try:
response = self.session.get(url, timeout=15)
soup = BeautifulSoup(response.text, 'html.parser')
# 检查是否跳转到登录页面
if "login" in response.url:
self.root.after(0, lambda: messagebox.showwarning(
"需要登录",
"访问此内容需要登录,请先登录账号"
))
self.status_var.set("需要登录才能访问")
return
# 解析图集数据
galleries = []
gallery_items = soup.select('.databox li') or soup.select('.gallery-list li') or soup.select('.pic-list li')
for item in gallery_items:
try:
title_elem = item.select_one('.ztitle a') or item.select_one('h2 a') or item.select_one('.title a')
title = title_elem.text.strip() if title_elem else "无标题"
link = urljoin(self.base_url, title_elem['href']) if title_elem else "#"
model_elem = item.select_one('.chujing') or item.select_one('.model')
model = model_elem.text.replace('模特:', '').strip() if model_elem else "未知模特"
count_elem = item.select_one('.num') or item.select_one('.pic-num')
count = count_elem.text.strip() if count_elem else "0P"
tags = ' '.join([a.text for a in item.select('.rtitle a')]) or "无标签"
source_elem = item.select_one('.rtitle a') or item.select_one('.source a')
source = source_elem.text.strip() if source_elem else "未知来源"
cover_elem = item.select_one('.img-box img') or item.select_one('img')
cover = cover_elem['src'] if cover_elem and 'src' in cover_elem.attrs else ""
if cover and not cover.startswith(('http://', 'https://')):
cover = urljoin(self.base_url, cover)
galleries.append({
'title': title,
'link': link,
'model': model,
'count': count,
'tags': tags,
'source': source,
'cover': cover
})
except Exception as e:
continue
# 尝试获取最大页数
pagination = soup.select_one('.pagination') or soup.select_one('.page-nav')
if pagination:
page_links = pagination.select('a')
if page_links:
last_page = 1
for link in page_links:
if link.text.isdigit():
last_page = max(last_page, int(link.text))
self.max_page = last_page
# 更新UI
self.root.after(0, self.update_gallery_list, galleries)
except Exception as e:
error_msg = str(e)
self.root.after(0, lambda: self.show_error(f"加载失败: {error_msg}"))
def update_gallery_list(self, galleries):
"""更新图集列表"""
self.tree.delete(*self.tree.get_children())
self.galleries = galleries
for gallery in galleries:
self.tree.insert("", "end", values=(
gallery['title'],
gallery['model'],
gallery['count'],
gallery['tags'],
gallery['source']
))
self.status_var.set(
f"{self.current_category} | 共 {len(galleries)} 个图集 | 第 {self.current_page}/{self.max_page} 页")
self.update_buttons()
def on_item_select(self, event):
"""选中图集事件"""
selection = self.tree.selection()
if not selection:
return
index = self.tree.index(selection[0])
if index >= len(self.galleries):
return
self.current_selection = self.galleries[index]
self.detail_btn.config(state=tk.NORMAL)
self.download_all_btn.config(state=tk.NORMAL)
# 显示信息
info_text = f"标题: {self.current_selection['title']}\n\n"
info_text += f"模特: {self.current_selection['model']}\n\n"
info_text += f"图片数: {self.current_selection['count']}\n\n"
info_text += f"标签: {self.current_selection['tags']}\n\n"
info_text += f"来源: {self.current_selection['source']}"
self.info_label.config(text=info_text)
# 加载缩略图
if self.current_selection['cover']:
threading.Thread(target=self.load_thumbnail, args=(self.current_selection['cover'],), daemon=True).start()
else:
self.preview_img.config(image=None)
self.preview_img.image = None
def load_thumbnail(self, img_url):
"""加载缩略图"""
try:
response = self.session.get(img_url, timeout=10)
img = Image.open(BytesIO(response.content))
# 计算等比例缩放的尺寸
width, height = img.size
max_size = 360
if width > height:
new_width = max_size
new_height = int(height * (max_size / width))
else:
new_height = max_size
new_width = int(width * (max_size / height))
img = img.resize((new_width, new_height), Image.LANCZOS)
tk_img = ImageTk.PhotoImage(img)
self.preview_img.config(image=tk_img)
self.preview_img.image = tk_img
except Exception as e:
print(f"加载缩略图失败: {str(e)}")
self.preview_img.config(image=None)
self.preview_img.image = None
def download_selected(self):
"""下载选中图集"""
if not hasattr(self, 'current_selection'):
messagebox.showwarning("提示", "请先选择要下载的图集")
return
if self.downloading:
messagebox.showwarning("提示", "当前有下载任务正在进行")
return
if not self.logged_in:
messagebox.showwarning("需要登录", "下载功能需要登录后才能使用")
return
gallery = self.current_selection
confirm = messagebox.askyesno(
"确认下载",
f"确定要下载图集:\n{gallery['title']}\n模特: {gallery['model']}\n共 {gallery['count']} 张图片吗?"
)
if confirm:
self.downloading = True
self.update_buttons()
threading.Thread(target=self.download_gallery, args=(gallery,), daemon=True).start()
def download_gallery(self, gallery):
"""下载图集所有图片 - 基于CDN直接构建URL的优化逻辑"""
try:
# 创建保存目录
save_dir = os.path.join("downloads", self.sanitize_filename(f"{gallery['source']}_{gallery['title']}"))
os.makedirs(save_dir, exist_ok=True)
# 获取图集详情页
detail_response = self.session.get(gallery['link'], timeout=20)
# 检查是否登录状态
if "login" in detail_response.url.lower():
self.root.after(0, lambda: messagebox.showwarning(
"需要登录",
"下载此图集需要登录,请先登录账号"
))
self.downloading = False
self.root.after(0, self.update_buttons)
return
detail_soup = BeautifulSoup(detail_response.text, 'html.parser')
# 提取CDN域名列表
cdn_domains = set()
for script in detail_soup.find_all('script'):
if script.string:
# 查找可能的CDN域名
cdn_matches = re.findall(r'https?://(oss-img-mmxxdd\.[^/"\']+)', script.string)
for match in cdn_matches:
cdn_domains.add(match)
# 如果没找到CDN域名,使用默认值
if not cdn_domains:
cdn_domains = {
"oss-img-mmxxdd.ojbkcdn.cc",
"oss-img-mmxxdd.mengguzhiai.com"
}
# 解析总页数
total_pages = 1
pagination = detail_soup.select_one('.pagination') or detail_soup.select_one('.page-nav')
if pagination:
# 尝试从文本中提取总页数(如"共10页")
page_text = pagination.get_text(strip=True)
match = re.search(r'共(\d+)页', page_text)
if match:
total_pages = int(match.group(1))
else:
# 尝试从最后一页按钮提取
last_page_link = pagination.find('a', text=re.compile(r'末页|尾页|最后'))
if last_page_link and 'href' in last_page_link.attrs:
href = last_page_link['href']
match = re.search(r'_(\d+)\.html', href)
if match:
total_pages = int(match.group(1)) + 1 # 因为是从0开始计数
# 尝试从页面提取日期部分
date_part = None
date_match = re.search(r'upload/images/(\d+)/', detail_response.text)
if date_match:
date_part = date_match.group(1)
# 生成所有可能的图片URL
img_urls = []
# 方法1: 尝试直接从CDN构建URL
if date_part:
for cdn_domain in cdn_domains:
for i in range(total_pages):
img_url = f"https://{cdn_domain}/upload/images/{date_part}/{i}.jpg"
img_urls.append(img_url)
# 方法2: 如果方法1失败,尝试从分页提取
if not img_urls:
base_url = gallery['link'].rsplit('_', 1)[0] # 处理带_0.html的URL
page_urls = []
for page in range(0, total_pages):
page_url = f"{base_url}_{page}.html" if '_' in gallery['link'] else f"{base_url}_{page}.html"
page_urls.append(urljoin(self.base_url, page_url))
# 提取所有图片URL
for idx, page_url in enumerate(page_urls, 1):
try:
# 添加随机延迟,避免请求过快
time.sleep(random.uniform(0.5, 1.5))
page_response = self.session.get(page_url, timeout=15)
page_soup = BeautifulSoup(page_response.text, 'html.parser')
# 尝试多种选择器提取图片
img_tag = (
page_soup.select_one('.gallerypic img') or
page_soup.select_one('.img-responsive') or
page_soup.select_one('.main-img img') or
page_soup.select_one('.content img')
)
if img_tag and 'src' in img_tag.attrs:
img_url = img_tag['src']
if not img_url.startswith(('http:', 'https:')):
img_url = urljoin(self.base_url, img_url)
img_urls.append(img_url)
self.root.after(0, lambda: self.status_var.set(f"已提取第{idx}/{total_pages}页图片"))
else:
self.root.after(0, lambda: self.status_var.set(f"第{idx}页未找到图片,尝试从JS提取"))
# 尝试从JavaScript中提取
scripts = page_soup.find_all('script')
for script in scripts:
if script.string:
# 查找img_url变量
match = re.search(r'var\s+img_url\s*=\s*["\']([^"\']+)["\']', script.string)
if match:
img_url = match.group(1)
if not img_url.startswith(('http:', 'https:')):
img_url = urljoin(self.base_url, img_url)
img_urls.append(img_url)
self.root.after(0, lambda: self.status_var.set(
f"从JS提取第{idx}/{total_pages}页图片"))
break
# 尝试其他可能的JS变量名
match = re.search(r'var\s+imgSrc\s*=\s*["\']([^"\']+)["\']', script.string)
if match:
img_url = match.group(1)
if not img_url.startswith(('http:', 'https:')):
img_url = urljoin(self.base_url, img_url)
img_urls.append(img_url)
self.root.after(0, lambda: self.status_var.set(
f"从JS提取第{idx}/{total_pages}页图片"))
break
except Exception as e:
self.root.after(0, lambda: self.status_var.set(f"提取第{idx}页图片失败: {str(e)}"))
continue
# 去重
img_urls = list(set(img_urls))
# 保存下载日志
with open(os.path.join(save_dir, "download_log.txt"), "w", encoding="utf-8") as f:
f.write(f"图集标题: {gallery['title']}\n")
f.write(f"模特: {gallery['model']}\n")
f.write(f"来源: {gallery['source']}\n")
f.write(f"总图片数: {len(img_urls)}\n\n")
f.write("图片URL列表:\n")
for url in img_urls:
f.write(url + "\n")
# 下载图片
total = len(img_urls)
success_count = 0
failed_count = 0
for i, url in enumerate(img_urls, 1):
try:
# 显示进度
progress = int((i / total) * 100)
self.root.after(0, lambda p=progress: self.progress_bar.config(value=p))
self.root.after(0, lambda m=f"下载中: {i}/{total}": self.progress_label.config(text=m))
# 尝试多个CDN域名
base_url = url.split('/upload')[0]
path = url.split('/upload')[1]
cdn_tried = []
for cdn_domain in cdn_domains:
cdn_tried.append(cdn_domain)
try_url = f"{cdn_domain}/upload{path}"
response = self.session.get(try_url, timeout=30)
if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''):
# 保存图片
ext = os.path.splitext(try_url)[1] or '.jpg'
filename = f"{i:03d}{ext}"
with open(os.path.join(save_dir, filename), 'wb') as f:
f.write(response.content)
success_count += 1
self.root.after(0, lambda: self.status_var.set(f"下载成功: {filename} ({i}/{total})"))
break
# 如果所有CDN都失败
if i == len(img_urls) and success_count == 0:
raise Exception(f"所有CDN域名尝试失败: {cdn_tried}")
# 添加随机延迟,避免请求过快
time.sleep(random.uniform(0.5, 1.5))
except Exception as e:
failed_count += 1
self.root.after(0, lambda: self.status_var.set(f"下载失败 {i}/{total}: {str(e)}"))
# 下载完成
self.root.after(0, lambda: self.progress_bar.config(value=100))
self.root.after(0, lambda: self.progress_label.config(
text=f"下载完成: {success_count} 成功, {failed_count} 失败"))
self.root.after(0, lambda: self.status_var.set(f"图集下载完成: {gallery['title']}"))
# 显示结果
result_msg = f"图集下载完成!\n成功: {success_count} 张\n失败: {failed_count} 张\n\n保存路径: {os.path.abspath(save_dir)}"
self.root.after(0, lambda: messagebox.showinfo("下载完成", result_msg))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("下载错误", f"下载失败: {str(e)}"))
self.root.after(0, lambda: self.status_var.set("下载失败"))
finally:
self.downloading = False
self.root.after(0, self.update_buttons)
def sanitize_filename(self, filename):
"""清理文件名,移除不合法字符"""
invalid_chars = r'[\\/:*?"<>|]'
return re.sub(invalid_chars, '_', filename)
def update_buttons(self):
"""更新按钮状态"""
self.prev_btn.config(state=tk.NORMAL if self.current_page > 1 and not self.downloading else tk.DISABLED)
self.next_btn.config(
state=tk.NORMAL if self.current_page < self.max_page and not self.downloading else tk.DISABLED)
self.page_label.config(text=f"{self.current_page}/{self.max_page}")
if hasattr(self, 'detail_btn'):
self.detail_btn.config(
state=tk.NORMAL if hasattr(self, 'current_selection') and not self.downloading else tk.DISABLED)
self.download_all_btn.config(
state=tk.NORMAL if hasattr(self, 'current_selection') and not self.downloading else tk.DISABLED)
self.download_btn.config(
state=tk.NORMAL if hasattr(self, 'current_selection') and not self.downloading else tk.DISABLED)
def prev_page(self):
"""上一页"""
if self.current_page > 1 and not self.downloading:
self.current_page -= 1
self.load_homepage()
def next_page(self):
"""下一页"""
if self.current_page < self.max_page and not self.downloading:
self.current_page += 1
self.load_homepage()
def on_category_change(self, event):
"""分类变更事件"""
self.current_category = self.category_var.get()
self.current_page = 1
self.load_homepage()
def view_details(self):
"""在浏览器中查看详情"""
if hasattr(self, 'current_selection'):
webbrowser.open(self.current_selection['link'])
def open_download_folder(self):
"""打开下载目录"""
if not os.path.exists("downloads"):
os.makedirs("downloads")
# 根据操作系统选择打开方式
if os.name == 'nt': # Windows
os.system('start downloads')
elif os.name == 'posix': # macOS/Linux
os.system('open downloads')
def show_error(self, message):
"""显示错误消息"""
messagebox.showerror("错误", message)
self.status_var.set("就绪")
if __name__ == "__main__":
root = tk.Tk()
app = MxdBrowserPro(root)
root.mainloop()