某写真网站专辑批量下载,带预览功能

查看 58|回复 11
作者:DrCatcher   
某写真网站专辑批量下载,仅供用于学习参考,请勿用于非法用途
修复了吾友提出的bug问题
偶有闪退,暂未发现原因,重新打开软件重新下载即可,已下载的会自动跳过
代码:
[Python] 纯文本查看 复制代码import sys
import threading
import requests
from bs4 import BeautifulSoup
import re
import os
import html
from collections import deque
import time
import random
from PySide6.QtCore import QThread, Signal
from typing import List, Dict
from urllib.parse import urljoin
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem,
    QMessageBox, QLabel, QSizePolicy, QDialog,
    QScrollArea, QGridLayout
)
from PySide6.QtWidgets import QProgressBar
from PySide6.QtGui import QPixmap
from PySide6.QtCore import Qt, QThread, QUrl, QSize
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PySide6.QtCore import Qt, QByteArray
import logging
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 常量
BASE_URL = "https://www.mxd009.cc"
def submit_search(keywords: str) -> str:
    """
    提交搜索请求,返回重定向后的 URL
    """
    form_data = {
        "keyboard": keywords,
        "show": "title",
        "tempid": "1",
        "tbname": "news"
    }
    SEARCH_URL = f"{BASE_URL}/e/search/index.php"
    session = requests.Session()
    response = session.post(SEARCH_URL, data=form_data, allow_redirects=False)
    if response.status_code == 302:
        new_location = response.headers.get("Location")
        return urljoin(SEARCH_URL, new_location)
    else:
        print("未发生重定向,状态码:", response.status_code)
        return ""
def get_total_count(soup: BeautifulSoup) -> int:
    """
    从页面中提取总图片组数
    """
    biaoqian_div = soup.find("div", class_="biaoqian")
    if biaoqian_div:
        p_text = biaoqian_div.find("p").get_text(strip=True)
        match = re.search(r"(\d+)", p_text)
        if match:
            return int(match.group(1))
    return 0
def parse_gallery_items_from_root(soup: BeautifulSoup) -> List[Dict[str, str]]:
    """
    提取页面中所有图片组信息
    """
    gallery_root = soup.find("div", class_="box galleryList")
    items = []
    if not gallery_root:
        return items
    for li in gallery_root.select("ul.databox > li"):
        img_tag = li.select_one("div.img-box img")
        ztitle_tag = li.select_one("p.ztitle a")
        rtitle_tag = li.select_one("p.rtitle a")
        author_tag = li.select_one("p.ztitle font")
        href = ztitle_tag["href"] if ztitle_tag and ztitle_tag.has_attr("href") else ""
        full_link = urljoin(BASE_URL, href)
        item = {
            "img": img_tag["src"] if img_tag else "",
            "ztitle": ztitle_tag.get_text(strip=True) if ztitle_tag else "",
            "ztitle_href": full_link,
            "author": author_tag.get_text(strip=True) if author_tag else "UK_AUTHOR",
            "rtitle": rtitle_tag.get_text(strip=True) if rtitle_tag else ""
        }
        items.append(item)
    return items
def crawl_all_pages(search_url: str) -> List[Dict[str, str]]:
    """
    分页抓取所有图片组信息
    """
    all_results = []
    page = 0
    searchid_match = re.search(r"searchid=(\d+)", search_url)
    if not searchid_match:
        print("无法提取 searchid")
        return []
    searchid = searchid_match.group(1)
    while True:
        # 构造分页 URL
        if page == 0:
            page_url = search_url
        else:
            page_url = f"{BASE_URL}/e/search/result/index.php?page={page}&searchid={searchid}"
        print(f"\n[抓取第 {page + 1} 页] {page_url}")
        try:
            response = requests.get(page_url, timeout=10)
            soup = BeautifulSoup(response.text, "html.parser")
        except Exception as e:
            print("请求失败:", e)
            break
        if page == 0:
            total = get_total_count(soup)
            print(f"[总计] 页面声明图片组总数: {total}")
        results = parse_gallery_items_from_root(soup)
        if not results:
            print("[结束] 当前页无数据,提前结束。")
            break
        all_results.extend(results)
        # 打印当前页的结果
        #for i, item in enumerate(results, 1):
        #    print(f"  第 {len(all_results) - len(results) + i} 项:")
        #    print(f"    缩略图: {item['img']}")
        #    print(f"    主标题: {item['ztitle']}")
        #    print(f"    分类  : {item['rtitle']}")
        #    print(f"    链接  : {item['ztitle_href']}")
        #
        if len(all_results) >= total:
            print("[完成] 已抓取全部项目。")
            break
        page += 1
    return all_results
# --- PySide6 GUI ---
class GalleryCrawler(QWidget):
    def __init__(self, cookies=None):
        super().__init__()
        self.setWindowTitle("写真专辑下载")
        self.resize(900, 600)
        self.init_ui()
        
        self.download_queue = deque()
        self.current_worker = None
        self.cancel_requested = False
        self.selected_items = None
        self.cookies = cookies
        self.headers = self._default_headers()
        self.session = requests.Session()
        self.session.headers.update(self.headers)
        if cookies:
            self.session.cookies.update(cookies)
    def init_ui(self):
        layout = QVBoxLayout(self)
        main_layout = QHBoxLayout()  # 水平主布局
        # 左侧竖直布局(搜索条 + TreeView + 按钮)
        left_layout = QVBoxLayout()
        search_layout = QHBoxLayout()
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("输入关键词搜索...")
        self.search_input.setText("奈汐酱")
        self.search_btn = QPushButton("搜索")
        self.search_btn.clicked.connect(self.start_search)
        search_layout.addWidget(self.search_input)
        search_layout.addWidget(self.search_btn)
        self.tree = QTreeWidget()
        self.tree.setHeaderLabels(["序号", "主标题", "分类", "链接","作者"])
        self.tree.setColumnWidth(0, 50)
        self.tree.setColumnWidth(1, 350)
        self.tree.setColumnWidth(2, 150)
        self.tree.setColumnWidth(3, 300)
        self.tree.setColumnHidden(4, True)
        self.tree.itemSelectionChanged.connect(self.on_tree_selection_changed)  # 监听选中变化
        btn_layout = QHBoxLayout()
        self.btn_download_selected = QPushButton("下载当前选中")
        self.btn_download_selected.clicked.connect(self.download_selected)
        self.btn_download_all = QPushButton("下载全部")
        self.btn_download_all.clicked.connect(self.download_all)
        
        self.btn_cancel_download = QPushButton("取消下载")
        self.btn_cancel_download.clicked.connect(self.cancel_download)
        self.btn_cancel_download.setEnabled(False)  # 初始禁用
        
        self.btn_show_all_more = QPushButton("显示缩略图")
        self.btn_show_all_more.clicked.connect(self.show_allthumbnails)
        
        btn_layout.addWidget(self.btn_download_selected)
        btn_layout.addWidget(self.btn_download_all)
        btn_layout.addWidget(self.btn_cancel_download)
        btn_layout.addWidget(self.btn_show_all_more)
        left_layout.addLayout(search_layout)
        left_layout.addWidget(self.tree)
        left_layout.addLayout(btn_layout)
        right_layout = QVBoxLayout()
        
        # 右侧显示缩略图
        self.image_label = QLabel("选中项缩略图显示区域")
        self.image_label.setAlignment(Qt.AlignCenter)
        self.image_label.setMinimumWidth(320)
        self.image_label.setStyleSheet("border: 1px solid gray;")
        
        self.link_label = QLabel()
        self.link_label.setText('[url=]在线浏览[/url]')
        self.link_label.setOpenExternalLinks(True)  # ✅ 允许打开外部链接
        self.link_label.setTextInteractionFlags(Qt.TextBrowserInteraction)  # 可点击
        self.link_label.setAlignment(Qt.AlignCenter)
        
        # 显示更多按钮
        self.btn_show_more = QPushButton("显示更多缩略图")
        self.btn_show_more.clicked.connect(self.show_thumbnails)
        self.btn_show_more.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
        
        right_layout.addWidget(self.image_label, stretch=1)
        right_layout.addWidget(self.link_label, stretch=0)
        right_layout.addWidget(self.btn_show_more, stretch=0, alignment=Qt.AlignHCenter)
        # 底部添加进度条
        bottom_layout = QHBoxLayout()
        self.album_label = QLabel()
        self.album_label.setText('')
        
        self.progress_bar = QProgressBar()
        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(100)
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(True)
        self.progress_bar.setFormat("下载进度:%p%")
        
        bottom_layout.addWidget(self.album_label, stretch=0)
        bottom_layout.addWidget(self.progress_bar, stretch=1)
        # 添加左右布局
        main_layout.addLayout(left_layout, 3)  # 左边占3份
        main_layout.addLayout(right_layout, 1)  # 右边占1份
        
        layout.addLayout(main_layout)
        # 此处添加进度条
        layout.addLayout(bottom_layout)
        
    def _default_headers(self):
        """Default headers to mimic browser behavior"""
        return {
            "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
            "accept-encoding": "gzip, deflate, br, zstd",
            "accept-language": "zh-CN,zh;q=0.9",
            "cache-control": "max-age=0",
            "sec-ch-ua": '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": '"Windows"',
            "sec-fetch-dest": "document",
            "sec-fetch-mode": "navigate",
            "sec-fetch-site": "same-origin",
            "upgrade-insecure-requests": "1",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
        }
   
    def show_allthumbnails(self):
        img_list = []
        for i in range(self.tree.topLevelItemCount()):
            item = self.tree.topLevelItem(i)
            img = item.data(0, Qt.ItemDataRole.UserRole + 1)
            if img:
                img_list.append(img)
        # 去重并保留顺序
        img_urls = list(dict.fromkeys(img_list))
        if len(img_urls) == 0:
            QMessageBox.warning(self, "提示", "请先搜索")
            return
        # 创建实例
        worker = DownloadWorker(self.session, "", "", img_urls)
        dialog = ThumbnailViewer(img_urls, self)
        dialog.exec()  
   
    def show_thumbnails(self):
        selected_item = self.tree.currentItem()
        if selected_item is None:
            QMessageBox.warning(self, "提示", "请先选择一项")
            return
        
        img_url = selected_item.data(0, Qt.ItemDataRole.UserRole + 1)
        album_index = selected_item.text(0)
        album_title = selected_item.text(1)
        img_url = selected_item.text(3)
        author = selected_item.text(4)
        # 创建实例
        worker = DownloadWorker(self.session, author, album_title, img_url)
        # 调用方法获取图片链接
        image_urls = worker.get_image_urls()
        dialog = ThumbnailViewer(image_urls, self)
        dialog.exec()  
        
    def cancel_download(self):
        self.cancel_requested = True
        self.download_queue.clear()  # 清除后续任务
        if self.current_worker and self.current_worker.isRunning():
            self.current_worker.terminate()  # 强行终止当前线程(⚠️ 不推荐用于长期任务)
            self.current_worker.wait()
        self.progress_bar.setValue(0)
        self.btn_cancel_download.setEnabled(False)
        self.btn_download_all.setEnabled(True)
        self.search_btn.setEnabled(True)
        
        self.enable_search()
        QMessageBox.information(self, "提示", "下载任务已被取消。")
        self.set_album("")
   
    def set_album(self, value:str):
        def update():
            self.album_label.setText(value)
        self.run_on_main_thread(update)
   
    def set_progress(self, value: int):
        def update():
            self.progress_bar.setValue(value)
        self.run_on_main_thread(update)
   
    def on_tree_selection_changed(self):
        selected_items = self.tree.selectedItems()
        if not selected_items:
            self.image_label.setText("选中项缩略图显示区域")
            self.image_label.setPixmap(QPixmap())  # 清空图片
            self.link_label.setText('[url=]在线浏览[/url]')
            return
        self.set_progress(0)
        
        item = selected_items[0]
        img_url = item.data(0, Qt.ItemDataRole.UserRole + 1)
        album_index = item.text(0)
        album_title = item.text(1)
        self.set_album(album_index + " " + album_title)
        
        if not img_url:
            self.image_label.setText("无缩略图")
            self.image_label.setPixmap(QPixmap())
            self.link_label.setText('[url=]在线浏览[/url]')
            return
        
        self.link_label.setText(f'[url=]在线浏览[/url]')        
        # 异步加载图片避免卡界面
        threading.Thread(target=self.load_image_from_url, args=(img_url,), daemon=True).start()
    def load_image_from_url(self, url):
        try:
            response = requests.get(url, timeout=10)
            response.raise_for_status()
            data = response.content
        except Exception:
            self.run_on_main_thread(lambda: self.image_label.setText("加载图片失败"))
            return
        pixmap = QPixmap()
        pixmap.loadFromData(data)
        scaled = pixmap.scaled(self.image_label.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
        def set_pix():
            self.image_label.setPixmap(scaled)
            self.image_label.setText("")
        self.run_on_main_thread(set_pix)
    def start_search(self):
        keyword = self.search_input.text().strip()
        if not keyword:
            QMessageBox.warning(self, "提示", "请输入关键词!")
            return
        self.search_btn.setEnabled(False)
        self.tree.clear()
        threading.Thread(target=self.search_and_load, args=(keyword,), daemon=True).start()
        self.set_album("")
        self.set_progress(0)
        self.selected_items = None
        self.image_label.setText("选中项缩略图显示区域")
    def search_and_load(self, keyword):
        search_url = submit_search(keyword)
        if not search_url:
            self.show_message("搜索失败,未获取有效跳转链接。")
            self.enable_search()
            return
        results = crawl_all_pages(search_url)
        if not results:
            self.show_message("未找到任何结果。")
            self.enable_search()
            return
        self.load_results(results)
        self.enable_search()
    def load_results(self, results):
        def update_ui():
            self.tree.clear()
            for idx, item in enumerate(results, 1):
                tree_item = QTreeWidgetItem([
                    str(idx),
                    item["ztitle"],
                    item["rtitle"],
                    item["ztitle_href"],
                    item["author"]
                ])
                tree_item.setData(0, Qt.ItemDataRole.UserRole + 1, item["img"])
                self.tree.addTopLevelItem(tree_item)
        self.run_on_main_thread(update_ui)
    def download_selected(self):
        items = self.tree.selectedItems()
        if not items:
            QMessageBox.information(self, "提示", "请先选中要下载的项。")
            return
        album_index = items[0].text(0)
        album_title = items[0].text(1)
        download_url = items[0].text(3)
        author = items[0].text(4)
        self.progress_bar.setValue(0)
        self.set_album(album_index + " " + album_title)
        self.worker = DownloadWorker(self.session, author, album_title, download_url)
        self.worker.progress.connect(self.set_progress)
        self.worker.finished.connect(lambda msg: QMessageBox.information(self, "完成", msg))
        self.worker.message.connect(lambda err: QMessageBox.critical(self, "错误", err))
        self.worker.start()
        self.worker.finished.connect(lambda _: self.enable_search())
        
    def download_all(self):
        count = self.tree.topLevelItemCount()
        if count == 0:
            QMessageBox.information(self, "提示", "无数据可下载。")
            return
            
        self.download_queue.clear()
        self.cancel_requested = False
        self.btn_cancel_download.setEnabled(True)
        self.btn_download_all.setEnabled(False)
        self.search_btn.setEnabled(False)
        for i in range(count):
            item = self.tree.topLevelItem(i)
            album_index = item.text(0)
            album_title = item.text(1)
            download_url = item.text(3)
            author = item.text(4)
            self.download_queue.append((album_index,author, album_title, download_url))
        self.progress_bar.setValue(0)
        self.start_next_download()
    def start_next_download(self):
        if self.cancel_requested:
            QMessageBox.information(self, "取消", "下载已取消。")
            self.btn_cancel_download.setEnabled(False)
            self.btn_download_all.setEnabled(True)
            self.search_btn.setEnabled(True)
            self.enable_search()
            return
        
        if not self.download_queue:
            QMessageBox.information(self, "完成", "全部下载完成!")
            self.btn_cancel_download.setEnabled(False)
            self.btn_download_all.setEnabled(True)
            self.search_btn.setEnabled(True)
            self.enable_search()
            return
        album_index,author, album_title, url = self.download_queue.popleft()
        self.progress_bar.setValue(0)
        self.set_album(album_index + " " + album_title)
        self.current_worker = DownloadWorker(self.session, author, album_title, url)
        self.current_worker.progress.connect(self.set_progress)
        self.current_worker.message.connect(lambda err: QMessageBox.critical(self, "错误", err))
        def on_finished(msg):
            logger.info(msg)
            self.start_next_download()
        self.current_worker.finished.connect(on_finished)
        self.current_worker.start()
    def show_message(self, text):
        def msg():
            QMessageBox.information(self, "提示", text)
        self.run_on_main_thread(msg)
    def enable_search(self):
        def en():
            self.search_btn.setEnabled(True)
        self.run_on_main_thread(en)
    def run_on_main_thread(self, func):
        # PySide6 主线程调用
        QApplication.instance().postEvent(self, _FuncEvent(func))
    def customEvent(self, event):
        event.func()
class DownloadWorker(QThread):
    progress = Signal(int)
    finished = Signal(str)
    message = Signal(str)
    def __init__(self, session, album_title, title, url):
        super().__init__()
        self.session = session
        self.album_title = album_title
        self.title = title
        self.url = url
    def run(self):
        try:
            success = self.process_album()
            if success:
                self.finished.emit(f"{self.title} 下载完成")
            else:
                self.finished.emit(f"{self.title} 下载失败")
        except Exception as e:
            self.message.emit(f"{self.title} 异常: {e}")
    def safe_request(self, url, timeout=10):
        try:
            response = self.session.get(url, timeout=timeout)
            response.raise_for_status()
            response.encoding = response.apparent_encoding
            return response
        except Exception:
            return None
    def download_image(self, url, filepath):
        if os.path.exists(filepath):
            return True
        try:
            response = self.session.get(url, timeout=15, stream=True)
            response.raise_for_status()
            os.makedirs(os.path.dirname(filepath), exist_ok=True)
            with open(filepath, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
            return True
        except:
            return False
            
    def get_image_urls(self):
        response = self.safe_request(self.url)
        if not response:
            return []
        soup = BeautifulSoup(response.text, "html.parser")
        img_tags = soup.select("div.gallerypic img")
        if not img_tags:
            return []
        first_img_url = img_tags[0].get("src", "")
        is_numbered = re.search(r"/(\d{3})\.jpg$", first_img_url)
        image_urls = []
        if is_numbered:
            base_url = re.match(r"(.+/)\d+\.jpg", first_img_url).group(1).rstrip("/")
            count_tag = soup.select_one("#tishi span")
            total_count = int(count_tag.text.strip()) if count_tag else len(img_tags)
            for i in range(1, total_count + 1):
                img_url = f"{base_url}/{i:03d}.jpg"
                image_urls.append(img_url)
        else:
            for img in img_tags:
                img_url = img.get("src", "")
                img_url = urljoin(self.url, img_url)
                image_urls.append(img_url)
        return image_urls
    def process_album(self):
        image_urls = self.get_image_urls()
        if not image_urls:
            return False
        album_dir = os.path.join(self.album_title, self.title) if self.album_title else self.title
        os.makedirs(album_dir, exist_ok=True)
        total_count = len(image_urls)
        success_count = 0
        for index, img_url in enumerate(image_urls):
            # 保留原始扩展名,默认使用 .jpg
            ext = os.path.splitext(img_url)[1] or '.jpg'
            filename = os.path.join(album_dir, f"{index + 1:03d}{ext}")
            if self.download_image(img_url, filename):
                success_count += 1
                self.progress.emit(int(success_count / total_count * 100))
        return success_count > 0
from PySide6.QtCore import QEvent
class _FuncEvent(QEvent):
    def __init__(self, func):
        super().__init__(QEvent.Type.User)
        self.func = func
class ThumbnailViewer(QDialog):
    def __init__(self, image_urls, parent=None):
        super().__init__(parent)
        title = "缩略图预览 共" + str(len(image_urls)) + "张"
        self.setWindowTitle(title)
        self.resize(850, 600)
        scroll = QScrollArea(self)
        scroll.setWidgetResizable(True)
        container = QWidget()
        scroll.setWidget(container)
        layout = QVBoxLayout(self)
        layout.addWidget(scroll)
        grid = QGridLayout(container)
        self.nam = QNetworkAccessManager(self)
        self.thumb_size = QSize(150, 250)  # 缩略图大小
        for i, url in enumerate(image_urls):
            # 创建一个垂直容器,放图片和序号label
            widget = QWidget()
            v_layout = QVBoxLayout(widget)
            v_layout.setContentsMargins(0, 0, 0, 0)
            v_layout.setSpacing(2)
            label = QLabel("加载中…")
            label.setFixedSize(self.thumb_size)
            label.setAlignment(Qt.AlignCenter)
            label.setStyleSheet("border: 1px solid gray;")
            number_label = QLabel(str(i + 1))
            number_label.setAlignment(Qt.AlignCenter)
            v_layout.addWidget(label)
            v_layout.addWidget(number_label)
            grid.addWidget(widget, i // 5, i % 5)  # 每行5个
            self.load_image_async(url, label)
    def load_image_async(self, url, label):
        request = QNetworkRequest(QUrl(url))
        request.setAttribute(QNetworkRequest.Http2AllowedAttribute, False)  # 禁用 HTTP/2
        reply = self.nam.get(request)
        def handle_finished():
            pixmap = QPixmap()
            if pixmap.loadFromData(reply.readAll()):
                label.setPixmap(pixmap.scaled(
                    self.thumb_size, Qt.KeepAspectRatio, Qt.SmoothTransformation))
            else:
                label.setText("加载失败")
            reply.deleteLater()
        reply.finished.connect(handle_finished)
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = GalleryCrawler()
    window.show()
    sys.exit(app.exec())
说明:最好加一个随机延时,免得IP被封
效果图:



@xiaofanvv 写了一个这个,你可以无缝集成到你的程序中。
类似的功能可以看这篇帖子里面的。https://www.52pojie.cn/thread-2042950-1-1.html
使用方法 将代码保存到文本中,文本后缀改为py  直接使用 python 文件名.py 就可以允许使用了

缩略图, 提示

liwenbin0210   

[color=]修改了下载全部完成之后,下载全部按钮无法使用的bug
[color=]import

[color=]sys
[color=]import

[color=]threading
[color=]import

[color=]requests
[color=]from

[color=]bs4

[color=]import

[color=]BeautifulSoup
[color=]import

[color=]re
[color=]import

[color=]os
[color=]import

[color=]html
[color=]from

[color=]collections

[color=]import

[color=]deque
[color=]import

[color=]time
[color=]import

[color=]random
[color=]from
PySide6.QtCore
[color=]import
QThread, Signal
[color=]from

[color=]typing

[color=]import

[color=]List
,
[color=]Dict
[color=]from

[color=]urllib
.
[color=]parse

[color=]import

[color=]urljoin
[color=]from
PySide6.QtWidgets
[color=]import
(
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QMessageBox, QLabel
)
[color=]from
PySide6.QtWidgets
[color=]import
QProgressBar
[color=]from
PySide6.QtCore
[color=]import
Qt
[color=]from
PySide6.QtGui
[color=]import
QPixmap
[color=]from
PySide6.QtCore
[color=]import
Qt, QByteArray
[color=]import

[color=]logging
[color=]# Configure logging
[color=]logging
.
[color=]basicConfig
(
[color=]level
[color=]=
[color=]logging
.
[color=]INFO
,
[color=]format
[color=]=
[color=]'
[color=]%(asctime)s
[color=] -
[color=]%(levelname)s
[color=] -
[color=]%(message)s
[color=]'
)
[color=]logger

[color=]=

[color=]logging
.
[color=]getLogger
(
[color=]__name__
)
[color=]# 常量
[color=]BASE_URL

[color=]=

[color=]"https://www.mxd009.cc"
[color=]def

[color=]submit_search
(
[color=]keywords
:
[color=]str
) ->
[color=]str
:
   
[color=]"""
[color=]    提交搜索请求,返回重定向后的 URL
[color=]    """
   
[color=]form_data

[color=]=
{
        
[color=]"keyboard"
:
[color=]keywords
,
        
[color=]"show"
:
[color=]"title"
,
        
[color=]"tempid"
:
[color=]"1"
,
        
[color=]"tbname"
:
[color=]"news"
    }
   
[color=]SEARCH_URL

[color=]=

[color=]f
[color=]"
[color=]{
[color=]BASE_URL
[color=]}
[color=]/e/search/index.php"
   
[color=]session

[color=]=

[color=]requests
.
[color=]Session
()
   
[color=]response

[color=]=

[color=]session
.
[color=]post
(
[color=]SEARCH_URL
,
[color=]data
[color=]=
[color=]form_data
,
[color=]allow_redirects
[color=]=
[color=]False
)
   
[color=]if

[color=]response
.
[color=]status_code

[color=]==

[color=]302
:
        
[color=]new_location

[color=]=

[color=]response
.
[color=]headers
.
[color=]get
(
[color=]"Location"
)
        
[color=]return

[color=]urljoin
(
[color=]SEARCH_URL
,
[color=]new_location
)
   
[color=]else
:
        
[color=]print
(
[color=]"未发生重定向,状态码:"
,
[color=]response
.
[color=]status_code
)
        
[color=]return

[color=]""
[color=]def

[color=]get_total_count
(
[color=]soup
:
[color=]BeautifulSoup
) ->
[color=]int
:
   
[color=]"""
[color=]    从页面中提取总图片组数
[color=]    """
   
[color=]biaoqian_div

[color=]=

[color=]soup
.
[color=]find
(
[color=]"div"
,
[color=]class_
[color=]=
[color=]"biaoqian"
)
   
[color=]if

[color=]biaoqian_div
:
        
[color=]p_text

[color=]=

[color=]biaoqian_div
.
[color=]find
(
[color=]"p"
).
[color=]get_text
(
[color=]strip
[color=]=
[color=]True
)
        
[color=]match

[color=]=

[color=]re
.
[color=]search
(
[color=]r
[color=]"
[color=](
[color=]\d
[color=]+
[color=])
[color=]"
,
[color=]p_text
)
        
[color=]if

[color=]match
:
            
[color=]return

[color=]int
(
[color=]match
.
[color=]group
(
[color=]1
))
   
[color=]return

[color=]0
[color=]def

[color=]parse_gallery_items_from_root
(
[color=]soup
:
[color=]BeautifulSoup
) ->
[color=]List
[
[color=]Dict
[
[color=]str
,
[color=]str
]]:
   
[color=]"""
[color=]    提取页面中所有图片组信息
[color=]    """
   
[color=]gallery_root

[color=]=

[color=]soup
.
[color=]find
(
[color=]"div"
,
[color=]class_
[color=]=
[color=]"box galleryList"
)
   
[color=]items

[color=]=
[]
   
[color=]if

[color=]not

[color=]gallery_root
:
        
[color=]return

[color=]items
   
[color=]for

[color=]li

[color=]in

[color=]gallery_root
.
[color=]select
(
[color=]"ul.databox > li"
):
        
[color=]img_tag

[color=]=

[color=]li
.
[color=]select_one
(
[color=]"div.img-box img"
)
        
[color=]ztitle_tag

[color=]=

[color=]li
.
[color=]select_one
(
[color=]"p.ztitle a"
)
        
[color=]rtitle_tag

[color=]=

[color=]li
.
[color=]select_one
(
[color=]"p.rtitle a"
)
        
[color=]author_tag

[color=]=

[color=]li
.
[color=]select_one
(
[color=]"p.ztitle font"
)
        
[color=]href

[color=]=

[color=]ztitle_tag
[
[color=]"href"
]
[color=]if

[color=]ztitle_tag

[color=]and

[color=]ztitle_tag
.
[color=]has_attr
(
[color=]"href"
)
[color=]else

[color=]""
        
[color=]full_link

[color=]=

[color=]urljoin
(
[color=]BASE_URL
,
[color=]href
)
        
[color=]item

[color=]=
{
            
[color=]"img"
:
[color=]img_tag
[
[color=]"src"
]
[color=]if

[color=]img_tag

[color=]else

[color=]""
,
            
[color=]"ztitle"
:
[color=]ztitle_tag
.
[color=]get_text
(
[color=]strip
[color=]=
[color=]True
)
[color=]if

[color=]ztitle_tag

[color=]else

[color=]""
,
            
[color=]"ztitle_href"
:
[color=]full_link
,
            
[color=]"author"
:
[color=]author_tag
.
[color=]get_text
(
[color=]strip
[color=]=
[color=]True
)
[color=]if

[color=]author_tag

[color=]else

[color=]"UK_AUTHOR"
,
            
[color=]"rtitle"
:
[color=]rtitle_tag
.
[color=]get_text
(
[color=]strip
[color=]=
[color=]True
)
[color=]if

[color=]rtitle_tag

[color=]else

[color=]""
        }
        
[color=]items
.
[color=]append
(
[color=]item
)
   
[color=]return

[color=]items
[color=]def

[color=]crawl_all_pages
(
[color=]search_url
:
[color=]str
) ->
[color=]List
[
[color=]Dict
[
[color=]str
,
[color=]str
]]:
   
[color=]"""
[color=]    分页抓取所有图片组信息
[color=]    """
   
[color=]all_results

[color=]=
[]
   
[color=]page

[color=]=

[color=]0
   
[color=]searchid_match

[color=]=

[color=]re
.
[color=]search
(
[color=]r
[color=]"searchid=
[color=](
[color=]\d
[color=]+
[color=])
[color=]"
,
[color=]search_url
)
   
[color=]if

[color=]not

[color=]searchid_match
:
        
[color=]print
(
[color=]"无法提取 searchid"
)
        
[color=]return
[]
   
[color=]searchid

[color=]=

[color=]searchid_match
.
[color=]group
(
[color=]1
)
   
[color=]while

[color=]True
:
        
[color=]# 构造分页 URL
        
[color=]if

[color=]page

[color=]==

[color=]0
:
            
[color=]page_url

[color=]=

[color=]search_url
        
[color=]else
:
            
[color=]page_url

[color=]=

[color=]f
[color=]"
[color=]{
[color=]BASE_URL
[color=]}
[color=]/e/search/result/index.php?page=
[color=]{
[color=]page
[color=]}
[color=]&searchid=
[color=]{
[color=]searchid
[color=]}
[color=]"
        
[color=]print
(
[color=]f
[color=]"
[color=]\n
[color=][抓取第
[color=]{
[color=]page

[color=]+

[color=]1
[color=]}
[color=] 页]
[color=]{
[color=]page_url
[color=]}
[color=]"
)
        
[color=]try
:
            
[color=]response

[color=]=

[color=]requests
.
[color=]get
(
[color=]page_url
,
[color=]timeout
[color=]=
[color=]10
)
            
[color=]soup

[color=]=

[color=]BeautifulSoup
(
[color=]response
.
[color=]text
,
[color=]"html.parser"
)
        
[color=]except

[color=]Exception

[color=]as

[color=]e
:
            
[color=]print
(
[color=]"请求失败:"
,
[color=]e
)
            
[color=]break
        
[color=]if

[color=]page

[color=]==

[color=]0
:
            
[color=]total

[color=]=

[color=]get_total_count
(
[color=]soup
)
            
[color=]print
(
[color=]f
[color=]"[总计] 页面声明图片组总数:
[color=]{
[color=]total
[color=]}
[color=]"
)
        
[color=]results

[color=]=

[color=]parse_gallery_items_from_root
(
[color=]soup
)
        
[color=]if

[color=]not

[color=]results
:
            
[color=]print
(
[color=]"[结束] 当前页无数据,提前结束。"
)
            
[color=]break
        
[color=]all_results
.
[color=]extend
(
[color=]results
)
        
[color=]# 打印当前页的结果
        
[color=]#for i, item in enumerate(results, 1):
        
[color=]#    print(f"  第 {len(all_results) - len(results) + i} 项:")
        
[color=]#    print(f"    缩略图: {item['img']}")
        
[color=]#    print(f"    主标题: {item['ztitle']}")
        
[color=]#    print(f"    分类  : {item['rtitle']}")
        
[color=]#    print(f"    链接  : {item['ztitle_href']}")
        
[color=]#
        
[color=]if

[color=]len
(
[color=]all_results
)
[color=]>=

[color=]total
:
            
[color=]print
(
[color=]"[完成] 已抓取全部项目。"
)
            
[color=]break
        
[color=]page

[color=]+=

[color=]1
   
[color=]return

[color=]all_results
[color=]# --- PySide6 GUI ---
[color=]class

[color=]GalleryCrawler
(
[color=]QWidget
):
   
[color=]def

[color=]__init__
(
[color=]self
,
[color=]cookies
[color=]=
[color=]None
):
        
[color=]super
().
[color=]__init__
()
        
[color=]self
.setWindowTitle(
[color=]"写真专辑下载"
)
        
[color=]self
.resize(
[color=]900
,
[color=]600
)
        
[color=]self
.
[color=]init_ui
()
        
        
[color=]self
.
[color=]download_queue

[color=]=

[color=]deque
()
        
[color=]self
.
[color=]current_worker

[color=]=

[color=]None
        
[color=]self
.
[color=]cancel_requested

[color=]=

[color=]False
        
        
[color=]self
.
[color=]cookies

[color=]=

[color=]cookies
        
[color=]self
.
[color=]headers

[color=]=

[color=]self
.
[color=]_default_headers
()
        
[color=]self
.
[color=]session

[color=]=

[color=]requests
.
[color=]Session
()
        
[color=]self
.
[color=]session
.
[color=]headers
.
[color=]update
(
[color=]self
.
[color=]headers
)
        
[color=]if

[color=]cookies
:
            
[color=]self
.
[color=]session
.
[color=]cookies
.
[color=]update
(
[color=]cookies
)
   
[color=]def

[color=]init_ui
(
[color=]self
):
        
[color=]layout

[color=]=
QVBoxLayout(
[color=]self
)
        
[color=]main_layout

[color=]=
QHBoxLayout()  
[color=]# 水平主布局
        
[color=]# 左侧竖直布局(搜索条 + TreeView + 按钮)
        
[color=]left_layout

[color=]=
QVBoxLayout()
        
[color=]search_layout

[color=]=
QHBoxLayout()
        
[color=]self
.
[color=]search_input

[color=]=
QLineEdit()
        
[color=]self
.
[color=]search_input
.setPlaceholderText(
[color=]"输入关键词搜索..."
)
        
[color=]self
.
[color=]search_input
.setText(
[color=]"奈汐酱"
)
        
[color=]self
.
[color=]search_btn

[color=]=
QPushButton(
[color=]"搜索"
)
        
[color=]self
.
[color=]search_btn
.clicked.connect(
[color=]self
.
[color=]start_search
)
        
[color=]search_layout
.addWidget(
[color=]self
.
[color=]search_input
)
        
[color=]search_layout
.addWidget(
[color=]self
.
[color=]search_btn
)
        
[color=]self
.
[color=]tree

[color=]=
QTreeWidget()
        
[color=]self
.
[color=]tree
.setHeaderLabels([
[color=]"序号"
,
[color=]"主标题"
,
[color=]"分类"
,
[color=]"链接"
,
[color=]"作者"
])
        
[color=]self
.
[color=]tree
.setColumnWidth(
[color=]0
,
[color=]50
)
        
[color=]self
.
[color=]tree
.setColumnWidth(
[color=]1
,
[color=]350
)
        
[color=]self
.
[color=]tree
.setColumnWidth(
[color=]2
,
[color=]150
)
        
[color=]self
.
[color=]tree
.setColumnWidth(
[color=]3
,
[color=]300
)
        
[color=]self
.
[color=]tree
.setColumnHidden(
[color=]4
,
[color=]True
)
        
[color=]self
.
[color=]tree
.itemSelectionChanged.connect(
[color=]self
.
[color=]on_tree_selection_changed
)  
[color=]# 监听选中变化
        
[color=]btn_layout

[color=]=
QHBoxLayout()
        
[color=]self
.
[color=]btn_download_selected

[color=]=
QPushButton(
[color=]"下载当前选中"
)
        
[color=]self
.
[color=]btn_download_selected
.clicked.connect(
[color=]self
.
[color=]download_selected
)
        
[color=]self
.
[color=]btn_download_all

[color=]=
QPushButton(
[color=]"下载全部"
)
        
[color=]self
.
[color=]btn_download_all
.clicked.connect(
[color=]self
.
[color=]download_all
)
        
        
[color=]self
.
[color=]btn_cancel_download

[color=]=
QPushButton(
[color=]"取消下载"
)
        
[color=]self
.
[color=]btn_cancel_download
.clicked.connect(
[color=]self
.
[color=]cancel_download
)
        
[color=]self
.
[color=]btn_cancel_download
.setEnabled(
[color=]False
)  
[color=]# 初始禁用
        
        
[color=]btn_layout
.addWidget(
[color=]self
.
[color=]btn_download_selected
)
        
[color=]btn_layout
.addWidget(
[color=]self
.
[color=]btn_download_all
)
        
[color=]btn_layout
.addWidget(
[color=]self
.
[color=]btn_cancel_download
)
        
[color=]left_layout
.addLayout(
[color=]search_layout
)
        
[color=]left_layout
.addWidget(
[color=]self
.
[color=]tree
)
        
[color=]left_layout
.addLayout(
[color=]btn_layout
)
        
[color=]right_layout

[color=]=
QVBoxLayout()
        
        
[color=]# 右侧显示缩略图
        
[color=]self
.
[color=]image_label

[color=]=
QLabel(
[color=]"选中项缩略图显示区域"
)
        
[color=]self
.
[color=]image_label
.setAlignment(Qt.AlignCenter)
        
[color=]self
.
[color=]image_label
.setMinimumWidth(
[color=]320
)
        
[color=]self
.
[color=]image_label
.setStyleSheet(
[color=]"border: 1px solid gray;"
)
        
        
[color=]self
.
[color=]link_label

[color=]=
QLabel()
        
[color=]self
.
[color=]link_label
.setText('[url=]在线浏览[/url]')
        
[color=]self
.
[color=]link_label
.setOpenExternalLinks(
[color=]True
)  
[color=]# ✅ 允许打开外部链接
        
[color=]self
.
[color=]link_label
.setTextInteractionFlags(Qt.TextBrowserInteraction)  
[color=]# 可点击
        
[color=]self
.
[color=]link_label
.setAlignment(Qt.AlignCenter)
        
        
[color=]right_layout
.addWidget(
[color=]self
.
[color=]image_label
,
[color=]stretch
[color=]=
[color=]1
)
        
[color=]right_layout
.addWidget(
[color=]self
.
[color=]link_label
,
[color=]stretch
[color=]=
[color=]0
)
        
[color=]# 底部添加进度条
        
[color=]bottom_layout

[color=]=
QHBoxLayout()
        
[color=]self
.
[color=]album_label

[color=]=
QLabel()
        
[color=]self
.
[color=]album_label
.setText(
[color=]''
)
        
        
[color=]self
.
[color=]progress_bar

[color=]=
QProgressBar()
        
[color=]self
.
[color=]progress_bar
.setMinimum(
[color=]0
)
        
[color=]self
.
[color=]progress_bar
.setMaximum(
[color=]100
)
        
[color=]self
.
[color=]progress_bar
.setValue(
[color=]0
)
        
[color=]self
.
[color=]progress_bar
.setTextVisible(
[color=]True
)
        
[color=]self
.
[color=]progress_bar
.setFormat(
[color=]"下载进度:%p%"
)
        
        
[color=]bottom_layout
.addWidget(
[color=]self
.
[color=]album_label
,
[color=]stretch
[color=]=
[color=]0
)
        
[color=]bottom_layout
.addWidget(
[color=]self
.
[color=]progress_bar
,
[color=]stretch
[color=]=
[color=]1
)
        
[color=]# 添加左右布局
        
[color=]main_layout
.addLayout(
[color=]left_layout
,
[color=]3
)  
[color=]# 左边占3份
        
[color=]main_layout
.addLayout(
[color=]right_layout
,
[color=]1
)  
[color=]# 右边占1份
        
        
[color=]layout
.addLayout(
[color=]main_layout
)
        
[color=]# 此处添加进度条
        
[color=]layout
.addLayout(
[color=]bottom_layout
)
        
   
[color=]def

[color=]_default_headers
(
[color=]self
):
        
[color=]"""Default headers to mimic browser behavior"""
        
[color=]return
{
            
[color=]"accept"
:
[color=]"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
,
            
[color=]"accept-encoding"
:
[color=]"gzip, deflate, br, zstd"
,
            
[color=]"accept-language"
:
[color=]"zh-CN,zh;q=0.9"
,
            
[color=]"cache-control"
:
[color=]"max-age=0"
,
            
[color=]"sec-ch-ua"
:
[color=]'"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"'
,
            
[color=]"sec-ch-ua-mobile"
:
[color=]"?0"
,
            
[color=]"sec-ch-ua-platform"
:
[color=]'"Windows"'
,
            
[color=]"sec-fetch-dest"
:
[color=]"document"
,
            
[color=]"sec-fetch-mode"
:
[color=]"navigate"
,
            
[color=]"sec-fetch-site"
:
[color=]"same-origin"
,
            
[color=]"upgrade-insecure-requests"
:
[color=]"1"
,
            
[color=]"user-agent"
:
[color=]"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
,
        }
   
   
[color=]def

[color=]cancel_download
(
[color=]self
):
        
[color=]self
.
[color=]cancel_requested

[color=]=

[color=]True
        
[color=]self
.
[color=]download_queue
.
[color=]clear
()  
[color=]# 清除后续任务
        
[color=]if

[color=]self
.
[color=]current_worker

[color=]and

[color=]self
.
[color=]current_worker
.isRunning():
            
[color=]self
.
[color=]current_worker
.terminate()  
[color=]# 强行终止当前线程(⚠️ 不推荐用于长期任务)
            
[color=]self
.
[color=]current_worker
.wait()
        
[color=]self
.
[color=]progress_bar
.setValue(
[color=]0
)
        
[color=]self
.
[color=]btn_cancel_download
.setEnabled(
[color=]False
)
        
[color=]self
.
[color=]btn_download_all
.setEnabled(
[color=]True
)
        
        
[color=]self
.
[color=]enable_search
()
        QMessageBox.information(
[color=]self
,
[color=]"提示"
,
[color=]"下载任务已被取消。"
)
        
[color=]self
.
[color=]set_album
(
[color=]""
)
   
   
[color=]def

[color=]set_album
(
[color=]self
,
[color=]value
:
[color=]str
):
        
[color=]def

[color=]update
():
            
[color=]self
.
[color=]album_label
.setText(
[color=]value
)
        
[color=]self
.
[color=]run_on_main_thread
(
[color=]update
)
   
   
[color=]def

[color=]set_progress
(
[color=]self
,
[color=]value
:
[color=]int
):
        
[color=]def

[color=]update
():
            
[color=]self
.
[color=]progress_bar
.setValue(
[color=]value
)
        
[color=]self
.
[color=]run_on_main_thread
(
[color=]update
)
   
   
[color=]def

[color=]on_tree_selection_changed
(
[color=]self
):
        
[color=]selected_items

[color=]=

[color=]self
.
[color=]tree
.selectedItems()
        
[color=]if

[color=]not

[color=]selected_items
:
            
[color=]self
.
[color=]image_label
.setText(
[color=]"选中项缩略图显示区域"
)
            
[color=]self
.
[color=]image_label
.setPixmap(QPixmap())  
[color=]# 清空图片
            
[color=]self
.
[color=]link_label
.setText('[url=]在线浏览[/url]')
            
[color=]return
        
[color=]self
.
[color=]set_progress
(
[color=]0
)
        
[color=]item

[color=]=

[color=]selected_items
[
[color=]0
]
        
[color=]img_url

[color=]=

[color=]item
.data(
[color=]0
, Qt.UserRole
[color=]+

[color=]1
)
        
[color=]if

[color=]not

[color=]img_url
:
            
[color=]self
.
[color=]image_label
.setText(
[color=]"无缩略图"
)
            
[color=]self
.
[color=]image_label
.setPixmap(QPixmap())
            
[color=]self
.
[color=]link_label
.setText('[url=]在线浏览[/url]')
            
[color=]return
        
        
[color=]self
.
[color=]link_label
.setText(
[color=]f
'[url=]
[color=]{
[color=]item
.text(
[color=]3
)
[color=]}
">在线浏览[/url]')        
        
[color=]# 异步加载图片避免卡界面
        
[color=]threading
.
[color=]Thread
(
[color=]target
[color=]=
[color=]self
.
[color=]load_image_from_url
,
[color=]args
[color=]=
(
[color=]img_url
,),
[color=]daemon
[color=]=
[color=]True
).
[color=]start
()
   
[color=]def

[color=]load_image_from_url
(
[color=]self
,
[color=]url
):
        
[color=]try
:
            
[color=]response

[color=]=

[color=]requests
.
[color=]get
(
[color=]url
,
[color=]timeout
[color=]=
[color=]10
)
            
[color=]response
.
[color=]raise_for_status
()
            
[color=]data

[color=]=

[color=]response
.
[color=]content
        
[color=]except

[color=]Exception
:
            
[color=]self
.
[color=]run_on_main_thread
(
[color=]lambda
:
[color=]self
.
[color=]image_label
.setText(
[color=]"加载图片失败"
))
            
[color=]return
        
[color=]pixmap

[color=]=
QPixmap()
        
[color=]pixmap
.loadFromData(
[color=]data
)
        
[color=]scaled

[color=]=

[color=]pixmap
.scaled(
[color=]self
.
[color=]image_label
.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
        
[color=]def

[color=]set_pix
():
            
[color=]self
.
[color=]image_label
.setPixmap(
[color=]scaled
)
            
[color=]self
.
[color=]image_label
.setText(
[color=]""
)
        
[color=]self
.
[color=]run_on_main_thread
(
[color=]set_pix
)
   
[color=]def

[color=]start_search
(
[color=]self
):
        
[color=]keyword

[color=]=

[color=]self
.
[color=]search_input
.text().strip()
        
[color=]if

[color=]not

[color=]keyword
:
            QMessageBox.warning(
[color=]self
,
[color=]"提示"
,
[color=]"请输入关键词!"
)
            
[color=]return
        
[color=]self
.
[color=]search_btn
.setEnabled(
[color=]False
)
        
[color=]self
.
[color=]tree
.clear()
        
[color=]threading
.
[color=]Thread
(
[color=]target
[color=]=
[color=]self
.
[color=]search_and_load
,
[color=]args
[color=]=
(
[color=]keyword
,),
[color=]daemon
[color=]=
[color=]True
).
[color=]start
()
   
[color=]def

[color=]search_and_load
(
[color=]self
,
[color=]keyword
):
        
[color=]search_url

[color=]=

[color=]submit_search
(
[color=]keyword
)
        
[color=]if

[color=]not

[color=]search_url
:
            
[color=]self
.
[color=]show_message
(
[color=]"搜索失败,未获取有效跳转链接。"
)
            
[color=]self
.
[color=]enable_search
()
            
[color=]return
        
[color=]results

[color=]=

[color=]crawl_all_pages
(
[color=]search_url
)
        
[color=]if

[color=]not

[color=]results
:
            
[color=]self
.
[color=]show_message
(
[color=]"未找到任何结果。"
)
            
[color=]self
.
[color=]enable_search
()
            
[color=]return
        
[color=]self
.
[color=]load_results
(
[color=]results
)
        
[color=]self
.
[color=]enable_search
()
   
[color=]def

[color=]load_results
(
[color=]self
,
[color=]results
):
        
[color=]def

[color=]update_ui
():
            
[color=]self
.
[color=]tree
.clear()
            
[color=]for

[color=]idx
,
[color=]item

[color=]in

[color=]enumerate
(
[color=]results
,
[color=]1
):
               
[color=]tree_item

[color=]=
QTreeWidgetItem([
                    
[color=]str
(
[color=]idx
),
                    
[color=]item
[
[color=]"ztitle"
],
                    
[color=]item
[
[color=]"rtitle"
],
                    
[color=]item
[
[color=]"ztitle_href"
],
                    
[color=]item
[
[color=]"author"
]
                ])
               
[color=]tree_item
.setData(
[color=]0
, Qt.UserRole
[color=]+

[color=]1
,
[color=]item
[
[color=]"img"
])
               
[color=]self
.
[color=]tree
.addTopLevelItem(
[color=]tree_item
)
        
[color=]self
.
[color=]run_on_main_thread
(
[color=]update_ui
)
   
[color=]def

[color=]download_selected
(
[color=]self
):
        
[color=]items

[color=]=

[color=]self
.
[color=]tree
.selectedItems()
        
[color=]if

[color=]not

[color=]items
:
            QMessageBox.information(
[color=]self
,
[color=]"提示"
,
[color=]"请先选中要下载的项。"
)
            
[color=]return
        
[color=]album_index

[color=]=

[color=]items
[
[color=]0
].text(
[color=]0
)
        
[color=]album_title

[color=]=

[color=]items
[
[color=]0
].text(
[color=]1
)
        
[color=]download_url

[color=]=

[color=]items
[
[color=]0
].text(
[color=]3
)
        
[color=]author

[color=]=

[color=]items
[
[color=]0
].text(
[color=]4
)
        
[color=]self
.
[color=]progress_bar
.setValue(
[color=]0
)
        
[color=]self
.
[color=]set_album
(
[color=]album_index

[color=]+

[color=]" "

[color=]+

[color=]album_title
)
        
[color=]self
.
[color=]worker

[color=]=

[color=]DownloadWorker
(
[color=]self
.
[color=]session
,
[color=]author
,
[color=]album_title
,
[color=]download_url
)
        
[color=]self
.
[color=]worker
.
[color=]progress
.connect(
[color=]self
.
[color=]set_progress
)
        
[color=]self
.
[color=]worker
.
[color=]finished
.connect(
[color=]lambda

[color=]msg
: QMessageBox.information(
[color=]self
,
[color=]"完成"
,
[color=]msg
))
        
[color=]self
.
[color=]worker
.
[color=]message
.connect(
[color=]lambda

[color=]err
: QMessageBox.critical(
[color=]self
,
[color=]"错误"
,
[color=]err
))
        
[color=]self
.
[color=]worker
.start()
        
[color=]self
.
[color=]worker
.
[color=]finished
.connect(
[color=]lambda

[color=]_
:
[color=]self
.
[color=]enable_search
())
        
   
[color=]def

[color=]download_all
(
[color=]self
):
        
[color=]count

[color=]=

[color=]self
.
[color=]tree
.topLevelItemCount()
        
[color=]if

[color=]count

[color=]==

[color=]0
:
            QMessageBox.information(
[color=]self
,
[color=]"提示"
,
[color=]"无数据可下载。"
)
            
[color=]return
            
        
[color=]self
.
[color=]download_queue
.
[color=]clear
()
        
[color=]self
.
[color=]cancel_requested

[color=]=

[color=]False
        
[color=]self
.
[color=]btn_cancel_download
.setEnabled(
[color=]True
)
        
[color=]self
.
[color=]btn_download_all
.setEnabled(
[color=]False
)  
[color=]# 开始下载时禁用按钮
        
[color=]for

[color=]i

[color=]in

[color=]range
(
[color=]count
):
            
[color=]item

[color=]=

[color=]self
.
[color=]tree
.topLevelItem(
[color=]i
)
            
[color=]album_index

[color=]=

[color=]item
.text(
[color=]0
)
            
[color=]album_title

[color=]=

[color=]item
.text(
[color=]1
)
            
[color=]download_url

[color=]=

[color=]item
.text(
[color=]3
)
            
[color=]author

[color=]=

[color=]item
.text(
[color=]4
)
            
[color=]self
.
[color=]download_queue
.
[color=]append
((
[color=]album_index
,
[color=]author
,
[color=]album_title
,
[color=]download_url
))
        
[color=]self
.
[color=]progress_bar
.setValue(
[color=]0
)
        
[color=]self
.
[color=]start_next_download
()
   
[color=]def

[color=]start_next_download
(
[color=]self
):
        
[color=]if

[color=]self
.
[color=]cancel_requested
:
            
[color=]# 取消下载时恢复按钮状态
            
[color=]self
.
[color=]btn_cancel_download
.setEnabled(
[color=]False
)
            
[color=]self
.
[color=]btn_download_all
.setEnabled(
[color=]True
)
            
[color=]self
.
[color=]enable_search
()
            
[color=]return
        
        
[color=]if

[color=]not

[color=]self
.
[color=]download_queue
:
            
[color=]# 所有任务完成后恢复按钮状态
            
[color=]self
.
[color=]btn_cancel_download
.setEnabled(
[color=]False
)
            
[color=]self
.
[color=]btn_download_all
.setEnabled(
[color=]True
)
            QMessageBox.information(
[color=]self
,
[color=]"完成"
,
[color=]"全部下载完成!"
)
            
[color=]self
.
[color=]enable_search
()
            
[color=]return
        
[color=]album_index
,
[color=]author
,
[color=]album_title
,
[color=]url

[color=]=

[color=]self
.
[color=]download_queue
.
[color=]popleft
()
        
[color=]self
.
[color=]progress_bar
.setValue(
[color=]0
)
        
[color=]self
.
[color=]set_album
(
[color=]album_index

[color=]+

[color=]" "

[color=]+

[color=]album_title
)
        
[color=]self
.
[color=]current_worker

[color=]=

[color=]DownloadWorker
(
[color=]self
.
[color=]session
,
[color=]author
,
[color=]album_title
,
[color=]url
)
        
[color=]self
.
[color=]current_worker
.
[color=]progress
.connect(
[color=]self
.
[color=]set_progress
)
        
[color=]self
.
[color=]current_worker
.
[color=]message
.connect(
[color=]lambda

[color=]err
: QMessageBox.critical(
[color=]self
,
[color=]"错误"
,
[color=]err
))
        
[color=]def

[color=]on_finished
(
[color=]msg
):
            
[color=]logger
.
[color=]info
(
[color=]msg
)
            
[color=]self
.
[color=]start_next_download
()
        
[color=]self
.
[color=]current_worker
.
[color=]finished
.connect(
[color=]on_finished
)
        
[color=]self
.
[color=]current_worker
.start()
   
[color=]def

[color=]show_message
(
[color=]self
,
[color=]text
):
        
[color=]def

[color=]msg
():
            QMessageBox.information(
[color=]self
,
[color=]"提示"
,
[color=]text
)
        
[color=]self
.
[color=]run_on_main_thread
(
[color=]msg
)
   
[color=]def

[color=]enable_search
(
[color=]self
):
        
[color=]def

[color=]en
():
            
[color=]self
.
[color=]search_btn
.setEnabled(
[color=]True
)
        
[color=]self
.
[color=]run_on_main_thread
(
[color=]en
)
   
[color=]def

[color=]run_on_main_thread
(
[color=]self
,
[color=]func
):
        
[color=]# PySide6 主线程调用
        QApplication.instance().postEvent(
[color=]self
,
[color=]_FuncEvent
(
[color=]func
))
   
[color=]def

[color=]customEvent
(
[color=]self
,
[color=]event
):
        
[color=]event
.func()
[color=]class

[color=]DownloadWorker
(
[color=]QThread
):
   
[color=]progress

[color=]=
Signal(
[color=]int
)
   
[color=]finished

[color=]=
Signal(
[color=]str
)
   
[color=]message

[color=]=
Signal(
[color=]str
)
   
[color=]def

[color=]__init__
(
[color=]self
,
[color=]session
,
[color=]album_title
,
[color=]title
,
[color=]url
):
        
[color=]super
().
[color=]__init__
()
        
[color=]self
.
[color=]session

[color=]=

[color=]session
        
[color=]self
.
[color=]album_title

[color=]=

[color=]album_title
        
[color=]self
.
[color=]title

[color=]=

[color=]title
        
[color=]self
.
[color=]url

[color=]=

[color=]url
   
[color=]def

[color=]run
(
[color=]self
):
        
[color=]try
:
            
[color=]success

[color=]=

[color=]self
.
[color=]process_album
()
            
[color=]if

[color=]success
:
               
[color=]self
.
[color=]finished
.emit(
[color=]f
[color=]"
[color=]{
[color=]self
.
[color=]title
[color=]}
[color=] 下载完成"
)
            
[color=]else
:
               
[color=]self
.
[color=]finished
.emit(
[color=]f
[color=]"
[color=]{
[color=]self
.
[color=]title
[color=]}
[color=] 下载失败"
)
        
[color=]except

[color=]Exception

[color=]as

[color=]e
:
            
[color=]self
.
[color=]message
.emit(
[color=]f
[color=]"
[color=]{
[color=]self
.
[color=]title
[color=]}
[color=] 异常:
[color=]{
[color=]e
[color=]}
[color=]"
)
   
[color=]def

[color=]safe_request
(
[color=]self
,
[color=]url
,
[color=]timeout
[color=]=
[color=]10
):
        
[color=]try
:
            
[color=]response

[color=]=

[color=]self
.
[color=]session
.get(
[color=]url
,
[color=]timeout
[color=]=
[color=]timeout
)
            
[color=]response
.raise_for_status()
            
[color=]response
.encoding
[color=]=

[color=]response
.apparent_encoding
            
[color=]return

[color=]response
        
[color=]except

[color=]Exception
:
            
[color=]return

[color=]None
   
[color=]def

[color=]download_image
(
[color=]self
,
[color=]url
,
[color=]filepath
):
        
[color=]if

[color=]os
.
[color=]path
.
[color=]exists
(
[color=]filepath
):
            
[color=]return

[color=]True
        
[color=]try
:
            
[color=]response

[color=]=

[color=]self
.
[color=]session
.get(
[color=]url
,
[color=]timeout
[color=]=
[color=]15
,
[color=]stream
[color=]=
[color=]True
)
            
[color=]response
.raise_for_status()
            
[color=]os
.
[color=]makedirs
(
[color=]os
.
[color=]path
.
[color=]dirname
(
[color=]filepath
),
[color=]exist_ok
[color=]=
[color=]True
)
            
[color=]with

[color=]open
(
[color=]filepath
,
[color=]"wb"
)
[color=]as

[color=]f
:
               
[color=]for

[color=]chunk

[color=]in

[color=]response
.iter_content(
[color=]chunk_size
[color=]=
[color=]8192
):
                    
[color=]f
.
[color=]write
(
[color=]chunk
)
            
[color=]return

[color=]True
        
[color=]except
:
            
[color=]return

[color=]False
   
[color=]def

[color=]process_album
(
[color=]self
):
        
[color=]response

[color=]=

[color=]self
.
[color=]safe_request
(
[color=]self
.
[color=]url
)
        
[color=]if

[color=]not

[color=]response
:
            
[color=]return

[color=]False
        
[color=]soup

[color=]=

[color=]BeautifulSoup
(
[color=]response
.text,
[color=]"html.parser"
)
        
[color=]img_tags

[color=]=

[color=]soup
.
[color=]select
(
[color=]"div.gallerypic img"
)
        
[color=]if

[color=]not

[color=]img_tags
:
            
[color=]return

[color=]False
        
[color=]album_dir

[color=]=

[color=]os
.
[color=]path
.
[color=]join
(
[color=]self
.
[color=]album_title
,
[color=]self
.
[color=]title
)
[color=]if

[color=]self
.
[color=]album_title

[color=]else

[color=]self
.
[color=]title
        
[color=]os
.
[color=]makedirs
(
[color=]album_dir
,
[color=]exist_ok
[color=]=
[color=]True
)
        
[color=]first_img_url

[color=]=

[color=]img_tags
[
[color=]0
].
[color=]get
(
[color=]"src"
,
[color=]""
)
        
[color=]is_numbered

[color=]=

[color=]re
.
[color=]search
(
[color=]r
[color=]"/
[color=](
[color=]\d
[color=]{3}
[color=])
[color=]\.
[color=]jpg$"
,
[color=]first_img_url
)
        
[color=]success_count

[color=]=

[color=]0
        
[color=]if

[color=]is_numbered
:
            
[color=]base_url

[color=]=

[color=]re
.
[color=]match
(
[color=]r
[color=]"
[color=](
[color=].
[color=]+
[color=]/
[color=])
[color=]\d
[color=]+\.
[color=]jpg"
,
[color=]first_img_url
).group(
[color=]1
).rstrip(
[color=]"/"
)
            
[color=]count_tag

[color=]=

[color=]soup
.
[color=]select_one
(
[color=]"#tishi span"
)
            
[color=]total_count

[color=]=

[color=]int
(
[color=]count_tag
.
[color=]text
.
[color=]strip
())
[color=]if

[color=]count_tag

[color=]else

[color=]len
(
[color=]img_tags
)
            
[color=]for

[color=]i

[color=]in

[color=]range
(
[color=]1
,
[color=]total_count

[color=]+

[color=]1
):
               
[color=]img_url

[color=]=

[color=]f
[color=]"
[color=]{
[color=]base_url
[color=]}
[color=]/
[color=]{
[color=]i
[color=]:03d}
[color=].jpg"
               
[color=]filename

[color=]=

[color=]os
.
[color=]path
.
[color=]join
(
[color=]album_dir
,
[color=]f
[color=]"
[color=]{
[color=]i
[color=]:03d}
[color=].jpg"
)
               
[color=]if

[color=]self
.
[color=]download_image
(
[color=]img_url
,
[color=]filename
):
                    
[color=]success_count

[color=]+=

[color=]1
                    
[color=]self
.
[color=]progress
.emit(
[color=]int
(
[color=]success_count

[color=]/

[color=]total_count

[color=]*

[color=]100
))
        
[color=]else
:
            
[color=]total_count

[color=]=

[color=]len
(
[color=]img_tags
)
            
[color=]for

[color=]index
,
[color=]img

[color=]in

[color=]enumerate
(
[color=]img_tags
):
               
[color=]img_url

[color=]=

[color=]img
.
[color=]get
(
[color=]"src"
,
[color=]""
)
               
[color=]img_url

[color=]=

[color=]urljoin
(
[color=]self
.
[color=]url
,
[color=]img_url
)
               
[color=]ext

[color=]=

[color=]os
.
[color=]path
.
[color=]splitext
(
[color=]img_url
)[
[color=]1
]
[color=]or

[color=]'.jpg'
               
[color=]filename

[color=]=

[color=]os
.
[color=]path
.
[color=]join
(
[color=]album_dir
,
[color=]f
[color=]"
[color=]{
[color=]index

[color=]+

[color=]1
[color=]:03d}{
[color=]ext
[color=]}
[color=]"
)
               
[color=]if

[color=]self
.
[color=]download_image
(
[color=]img_url
,
[color=]filename
):
                    
[color=]success_count

[color=]+=

[color=]1
                    
[color=]self
.
[color=]progress
.emit(
[color=]int
(
[color=]success_count

[color=]/

[color=]total_count

[color=]*

[color=]100
))
        
[color=]return

[color=]success_count

[color=]>

[color=]0
[color=]from
PySide6.QtCore
[color=]import
QEvent
[color=]class

[color=]_FuncEvent
(
[color=]QEvent
):
   
[color=]def

[color=]__init__
(
[color=]self
,
[color=]func
):
        
[color=]super
().
[color=]__init__
(QEvent.User)
        
[color=]self
.
[color=]func

[color=]=

[color=]func
[color=]if

[color=]__name__

[color=]==

[color=]"__main__"
:
   
[color=]app

[color=]=
QApplication(
[color=]sys
.
[color=]argv
)
   
[color=]window

[color=]=

[color=]GalleryCrawler
()
   
[color=]window
.show()
   
[color=]sys
.
[color=]exit
(
[color=]app
.exec())   
chydroid   

PySide6在不少方面已经有所改进,所以应该估以下修改:
第231行附近的代码的修改:在PySide6中,交互标志需要完整指定为Qt.TextInteractionFlag.TextBrowserInteraction,而不仅仅是Qt.TextBrowserInteraction。
类似的还有以下一些(不是全部)
第337行:Qt.KeepAspectRatio → Qt.AspectRatioMode.KeepAspectRatio
第337行:Qt.SmoothTransformation → Qt.TransformationMode.SmoothTransformation
第379行:Qt.UserRole → Qt.ItemDataRole.UserRole
第566行:QEvent.User → QEvent.Type.User
happyweeks365   

学习。。。。。。。。。。。。。。。。。。。。
Crakr   

好东西,学习了。
apull   

不错不错,多谢分享。
xiaofanvv   

我试试哈,集成一下
yyd841122   

学习下!好东东!!
jone33   

这是相当好的资源
laolaide   

学习学习
您需要登录后才可以回帖 登录 | 立即注册