Python爬虫采集商品数据基于DrissionPage库(第36节)


目前,Python作为最为流行和实用的编程语言之一,在爬虫领域得到广泛应用,通过Python的爬虫技术采集商品数据具有显著的优点。Python爬虫几乎可以访问任何网站,包括那些没有提供API接口的网站,这意味着我们可以获取到网络上几乎所有可访问的商品信息。并且Python爬虫可以根据需要定制数据采集的规则和深度,并根据具体需求调整爬虫的代码,以获取特定的商品信息,如商品ID、标题、价格等信息。

在前面第34节教程《Python爬虫基于Tkinte+requests采集商品数据》中已经详细介绍了如何通过Python的requests库发送HTTP请求后爬取goofish.com网页上的商品数据。

在Python中爬取商品数据的方法有很多,主要包括使用requests库、DrissionPage库、BeautifulSoup库、Selenium库、Scrapy框架等。 其中,requests库用于发送HTTP请求获取网页内容,BeautifulSoup库用于解析和提取数据,Selenium库用于处理动态加载的网页,Scrapy框架则是一个强大的爬虫框架,可以高效地抓取和处理大量数据,DrissionPage库作为Python的网页自动化操作工具库,提供了简单易用的API,能够帮助我们快速实现网页自动化操作。

一、DrissionPage库的优点

DrissionPage库作为Python的第三方库,将Selenium库的浏览器操控能力与Requests库的高效数据抓取功能深度整合,提供统一的API接口,开发者可根据需求在两种模式间动态切换,其中浏览器驱动模式可以处理动态渲染内容(如模拟鼠标点击、表单提交)等高级功能,而无头数据模式则提供高效爬取静态数据功能,能让资源消耗降低70%。DrissionPage库的环境配置在上一节教程《Python爬虫使用DrissionPage库爬取网页信息》中已经信息介绍。

DrissionPage库的优点主要包括:

  • DrissionPage库既能控制浏览器,也能收发数据包,还能把两者合二为一。

  • 结合了Selenium和requests的轻量级爬虫框架,可兼顾浏览器自动化的便利性和requests库的高效率。

  • 功能强大,内置无数人性化设计和便捷功能,并且它的操作语法简洁而优雅,代码量少,对新手友好。

  • 自动化控制浏览器,支持驱动Chromium内核的浏览器包括Chrome和Edge浏览器。

二、Python爬虫基于DrissionPage库采集商品数据介绍

为了获取goofish.com的商品信息,我们使用了DrissionPage库,DrissionPage作为流行的爬虫库提供了便捷的浏览器控制功能。该程序的图形界面使用了Tkinter库创建简单的操作窗口,其中包含了以下组件:

搜索框:允许用户输入搜索关键词来查找商品。

按钮:包括“打开原网站”、“开始爬取”、“停止”、“清除日志和结果预览”和“打开爬取结果文件”等按钮,用户可以通过点击这些按钮进行操作。

记录日志(ScrolledText):通过Tkinter库的ScrolledText组件记录日志信息,ScrolledText是一个非常实用的组件,它继承自Text组件,并提供了滚动条功能,使得处理长文本变得更为方便。

表格(Treeview):显示爬取到的商品数据,包括商品ID和标题、价格、标签、卖家ID等信息。

threading:使用多线程库,确保爬取过程不阻塞GUI控制界面。

三、使用教程

安装依赖:首先,确保你已经安装了DrissionPage、openpyxl、和Pillow库。如果还没有安装,可以通过pip安装:

pip install DrissionPage
pip install openpyxl
pip install Pillow

登录网站

点击“打开原网站”按钮后会弹出浏览器并自动打开goofish.com网站,记得登录一下网站,否则对爬取的页面有限制。

设置搜索参数

  • 在关键词输入框中输入想要搜索的商品关键词。

  • 选择要爬取的页数(默认 1 页)。

  • 选择线程(默认 5 个线程)。

开始爬取

点击 "开始爬取" 按钮,程序会自动打开指定的浏览器并开始爬取数据,请耐心等待一小会儿。

查看结果

  • 在“日志信息”区域显示爬取过程记录。

  • 爬取过程中可以在结果预览区看到已爬取的实时数据。

  • 爬取完成后,在当前脚本工作目录下,会自动将商品数据保存为Excel文件。

  • 点击 "打开爬取结果文件" 可以直接查看Excel文件。

  • 爬取成功后会在当前脚本工作目录下创建一个my_images文件夹,并保存所有爬取的商品图片。

四、界面演示

Python爬虫采集商品数据基于DrissionPage库

Python爬虫采集商品数据基于DrissionPage库

Python爬虫采集商品数据基于DrissionPage库

五、免责声明

本项目仅用于学习研究目的,严禁用于任何商业用途或非法数据采集。使用本程序即表示您同意:

遵守相关法律法规及平台用户协议。

控制采集频率,避免对目标服务器造成负担。

若因使用本程序而导致任何法律问题或其他不良后果,使用者需自行承担全部责任,包括但不限于可能面临的民事赔偿、行政处罚甚至刑事处罚。发布者不承担任何因使用者不当使用而引发的连带责任。

六、Python爬虫基于DrissionPage库采集商品数据,完整代码如下所示:

动手练一练:

import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
from DrissionPage import ChromiumPage
import os, queue, threading, time, requests
from openpyxl import Workbook
from openpyxl.drawing.image import Image
from datetime import datetime

# 获取当前脚本文件所在的目录
directory_set = os.path.dirname(os.path.abspath(__file__))
# 将当前脚本文件所在的目录设置为工作目录
os.chdir(directory_set)

# 定义User-Agent字符串,包含了客户端软件的名称、版本号、操作系统信息等内容
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_8; rv:1.9.2.20) Gecko/7342-10-21 10:07:07 Firefox/3.8"
# 最大工作线程数设置为5个线程
MAX_THREADS = 5
# 定义保存图片的文件夹名称
SAVE_IMAGES = "my_images"
# 图片格式定义
IMAGE_SUPPORT = ['jpg', 'jpeg', 'png', 'gif', 'bmp']

# 鼠标非悬停在按钮上
def button_not_hover(button):
    color = '#94D8F6'
    button.configure(bg=color)

# 鼠标悬停在按钮上
def button_hover(button):
    color = '#3aa2fb'
    button.configure(bg=color)

# 定义DataCollection类,用于封装爬虫功能
class DataCollection:
    def __init__(self, root):
        self.root = root
        self.root.title("Python爬虫采集商品数据")

        # 程序窗口设置居中
        window_width = 860
        window_height = 650
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()
        x = (screen_width - window_width) / 2
        y = (screen_height - window_height) / 2
        self.root.geometry('%dx%d+%d+%d' % (window_width, window_height, x, y))
        self.root.resizable(True, True)

        # 程序菜单栏设计
        self.spider_menu = tk.Menu(self.root)
        file_menu = tk.Menu(self.spider_menu, tearoff=0)
        self.spider_menu.add_cascade(label='文件', menu=file_menu)
        file_menu.add_command(label='打开原网站', command=self.open_website)
        file_menu.add_command(label='打开爬取结果文件', command=self.get_result)
        file_menu.add_command(label='退出', command=self.GUI_window_close)

        # 初始化浏览器实例
        # 设置本地启动端口“9999”,如果不指定端口,则默认使用“9222”端口
        self.page = ChromiumPage(9999)

        # 放置菜单栏
        self.root.config(menu=self.spider_menu)

        # 创建日志记录队列
        self.record_queue = queue.Queue()

        # 创建状态变量
        self.is_active = False

        # 判断保存图片的文件夹是否存在,如果不存在则在当前工作目录下创建该文件夹
        if not os.path.exists(SAVE_IMAGES):
            os.makedirs(SAVE_IMAGES)

        # GUI界面创建
        self.set_widgets()

        # 启动日志更新线程
        threading.Thread(target=self.update_info, daemon=True).start()

    # 创建主界面控件
    def set_widgets(self):
        # 创建主框架
        show_frame = ttk.Frame(self.root, padding=10)
        show_frame.pack(fill=tk.BOTH, expand=True)

        # 定义标题标签
        define_title = ttk.Label(show_frame, text="Python爬虫采集商品数据", font=('宋体', 20, 'bold'))
        define_title.pack(pady=(0, 20))

        # 配置设置区域
        config_frame = ttk.LabelFrame(show_frame, text="爬取设置", padding=10)
        config_frame.pack(fill=tk.X, pady=(0, 10))

        # 输入区域
        input_frame = tk.Frame(config_frame)
        input_frame.pack(fill=tk.X, pady=(0, 10))

        self.open_button = tk.Button(input_frame, text="打开原网站", font=('黑体', 13), bg="#94D8F6", command=self.open_website)
        self.open_button.pack(side=tk.LEFT, padx=(0, 30))
        self.open_button.bind("<Enter>", lambda e: button_hover(self.open_button))
        self.open_button.bind("<Leave>", lambda e: button_not_hover(self.open_button))

        # 输入要搜索的关键词
        ttk.Label(input_frame, text="关键词:").pack(side=tk.LEFT, padx=(0, 5))
        self.keyword_var = tk.StringVar()
        self.keyword_entry = ttk.Entry(input_frame, textvariable=self.keyword_var, width=30)
        self.keyword_entry.pack(side=tk.LEFT, padx=(0, 30))

        # 爬取页数设置
        ttk.Label(input_frame, text="爬取页数:").pack(side=tk.LEFT, padx=(0, 5))
        self.page_var = tk.StringVar(value="1")
        self.page_entry = ttk.Entry(input_frame, textvariable=self.page_var, width=10)
        self.page_entry.pack(side=tk.LEFT, padx=(0, 30))

        # 设置线程数
        ttk.Label(input_frame, text="线程数:").pack(side=tk.LEFT, padx=(0, 5))
        self.thread_var = tk.StringVar(value=str(MAX_THREADS))
        self.thread_entry = ttk.Combobox(input_frame, textvariable=self.thread_var, width=5, state="readonly")
        self.thread_entry['values'] = tuple(str(i) for i in range(1, MAX_THREADS + 1))
        self.thread_entry.pack(side=tk.LEFT, padx=(0, 10))

        # 按钮区域
        button_frame = ttk.Frame(show_frame)
        button_frame.pack(fill=tk.X, pady=(0, 10))

        self.start_button = tk.Button(button_frame, text="开始爬取", font=('黑体', 13), bg="#94D8F6", command=self.start_spider)
        self.start_button.pack(side=tk.LEFT, padx=(0, 10))
        self.start_button.bind("<Enter>", lambda e: button_hover(self.start_button))
        self.start_button.bind("<Leave>", lambda e: button_not_hover(self.start_button))

        self.stop_button = tk.Button(button_frame, text="停止",
                                     font=('黑体', 13),
                                     bg="#94D8F6",
                                     command=self.stop_spider,
                                     state=tk.DISABLED)
        self.stop_button.pack(side=tk.LEFT)
        self.stop_button.bind("<Enter>", lambda e: button_hover(self.stop_button))
        self.stop_button.bind("<Leave>", lambda e: button_not_hover(self.stop_button))

        open_result_button = tk.Button(button_frame,
                                       text="打开爬取结果文件",
                                       font=('黑体', 13),
                                       bg="#94D8F6",
                                       command=self.get_result)
        open_result_button.pack(side=tk.RIGHT, padx=(0, 15))
        open_result_button.bind("<Enter>", lambda e: button_hover(open_result_button))
        open_result_button.bind("<Leave>", lambda e: button_not_hover(open_result_button))

        clear_result = tk.Button(button_frame,
                  text="清除日志和结果预览",
                  font=('黑体', 13),
                  bg="#94D8F6",
                  command=self.clear_info)
        clear_result.pack(side=tk.RIGHT, padx=(0, 15))
        clear_result.bind("<Enter>", lambda e: button_hover(clear_result))
        clear_result.bind("<Leave>", lambda e: button_not_hover(clear_result))

        # 日志记录区域
        info_frame = ttk.LabelFrame(show_frame, text="日志信息", padding="10")
        info_frame.pack(fill=tk.BOTH)

        self.info_text = scrolledtext.ScrolledText(info_frame, wrap=tk.WORD, state=tk.DISABLED, height=10)
        self.info_text.pack(fill=tk.BOTH)

        # 结果预览区
        result_frame = ttk.LabelFrame(show_frame, text="结果预览", padding="10")
        result_frame.pack(fill=tk.BOTH)

        # 创建Treeview
        tree_columns = ('商品ID', '标题', '价格', '卖家', '标签')
        self.result_tree = ttk.Treeview(result_frame, columns=tree_columns, show='headings', height=6)

        # 为Treeview定义列
        self.result_tree.heading('商品ID', text='商品ID')
        self.result_tree.heading('标题', text='标题')
        self.result_tree.heading('价格', text='价格')
        self.result_tree.heading('卖家', text='卖家')
        self.result_tree.heading('标签', text='标签')

        # 为Treeview设置列宽
        self.result_tree.column('商品ID', width=50)
        self.result_tree.column('标题', width=250)
        self.result_tree.column('价格', width=30)
        self.result_tree.column('卖家', width=140)
        self.result_tree.column('标签', width=60)

        # 为Treeview创建滚动条
        scrollbar = ttk.Scrollbar(result_frame, orient="vertical", command=self.result_tree.yview)
        self.result_tree.configure(yscrollcommand=scrollbar.set)
        self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # 爬取进度显示区
        progress_frame = ttk.LabelFrame(show_frame, text="爬取进度", padding=5)
        progress_frame.pack(side=tk.BOTTOM, fill=tk.X)

        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X)
        # 初始化进度条显示为0
        self.progress_var.set(0)
        self.progress_bar.update()

        # 爬取状态栏
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(progress_frame, textvariable=self.status_var)
        status_bar.pack(fill=tk.X, pady=5)

    # 将消息添加到日志队列
    def info(self, message):
        # 以"年-月-日 小时-分钟-秒"的格式获取当前日期
        set_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.record_queue.put(f"[{set_time}] {message}")

    # 更新日志记录
    def update_info(self):
        try:
            while True:
                # 从队列获取所有可用消息
                messages = []
                while not self.record_queue.empty():
                    messages.append(self.record_queue.get_nowait())

                if messages:
                    self.info_text.config(state=tk.NORMAL)
                    for msg in messages:
                        self.info_text.insert(tk.END, msg + "\n")
                    self.info_text.config(state=tk.DISABLED)
                    self.info_text.yview(tk.END)

                time.sleep(0.1)
        except Exception as e:
            pass

    # 清空日志内容和结果预览内容
    def clear_info(self):
        self.info_text.config(state=tk.NORMAL)
        self.info_text.delete(1.0, tk.END)
        self.info_text.config(state=tk.DISABLED)
        self.progress_var.set(0)
        # 清空Treeview
        for item in self.result_tree.get_children():
            self.result_tree.delete(item)

    # 打开爬取结果文件,注意是Excel文件
    def get_result(self):
        keyword = self.keyword_entry.get().strip()
        if not keyword:
            messagebox.showinfo("提示", "未找到结果文件")
            return

        # 查找最新的结果文件
        files = [f for f in os.listdir('.') if f.startswith(keyword) and f.endswith('.xlsx')]
        if files:
            latest_file = max(files, key=os.path.getctime)
            try:
                os.startfile(latest_file)
            except:
                # Linux系统使用xdg-open
                os.system(f'xdg-open "{latest_file}"')
        else:
            messagebox.showinfo("提示", "未找到结果文件")

    # 验证用户输入的内容
    def validate_inputs(self):
        # 验证关键词
        keyword = self.keyword_var.get().strip()
        if not keyword:
            messagebox.showwarning("警告", "关键词不能为空")
            return False

        # 验证页数
        try:
            pages = int(self.page_var.get())
            if pages <= 0:
                messagebox.showwarning("警告", "页数必须是正整数")
                return False
        except ValueError:
            messagebox.showwarning("警告", "页数必须是数字")
            return False

        # 验证线程数
        try:
            threads = int(self.thread_var.get())
            if threads <= 0 or threads > MAX_THREADS:
                messagebox.showwarning("警告", f"线程数必须在1-{MAX_THREADS}之间")
                return False
        except ValueError:
            messagebox.showwarning("警告", "线程数必须是数字")
            return False

        return True

    # 开始爬取
    def start_spider(self):
        if self.is_active:
            return

        if not self.validate_inputs():
            return

        # 更新界面状态
        self.progress_var.set(0)
        self.is_active = True
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        self.status_var.set("数据爬取中...")

        # 清空Treeview
        for item in self.result_tree.get_children():
            self.result_tree.delete(item)

        # 获取参数
        keyword = self.keyword_var.get().strip()
        pages = int(self.page_var.get())
        threads = int(self.thread_var.get())

        # 创建任务队列
        self.work_queue = queue.Queue()
        for page in range(1, pages + 1):
            self.work_queue.put(page)

        # 创建结果列表
        self.results = []
        self.failed_pages = []

        # 打开网页
        self.page.get("https://www.goofish.com/")
        time.sleep(1)

        # 输入搜索关键词
        self.page.ele("css:.search-input--WY2l9QD3").input(keyword)

        # 点击搜索按钮
        self.page.ele("css:.search-icon--bewLHteU").check()

        # 开始监听网络请求
        self.page.listen.start("h5/mtop.taobao.idlemtopsearch.pc.search/1.0")

        # 创建并启动工作线程
        self.workers = []
        for i in range(threads):
            worker = threading.Thread(target=self.thread_task)
            worker.daemon = True
            worker.start()
            self.workers.append(worker)
            self.info(f"启动工作线程 #{i + 1}")

        # 启动监视线程
        threading.Thread(target=self.monitor_workers).start()

    # 工作线程任务
    def thread_task(self):
        # 获取当前处理进度,初始化为0
        present = 0
        # 设置进度条最大进度为100%
        set_max_progress = 100.0
        while not self.work_queue.empty() and self.is_active:
            try:
                page = self.work_queue.get_nowait()
                self.info(f"线程 {threading.current_thread().name} 开始爬取第 {page} 页")

                # 等待网络请求响应
                response = self.page.listen.wait()

                try:
                    # 获取响应体
                    response_body = response.response.body
                    result_list = response_body["data"]["resultList"]
                    for item in result_list:
                        item_data = item["data"]["item"]["main"]
                        ex_content = item_data["exContent"]
                        click_params = item_data["clickParam"]["args"]

                        product_id = click_params["id"]
                        category_id = click_params["cCatId"]
                        product_url = f"https://www.goofish.com/item?spm=a21ybx.search.searchFeedList&id={product_id}&categoryId={category_id}"

                        try:
                            detail_params = ex_content["detailParams"]
                            shop_name = detail_params.get("userNick", "未知店铺")
                        except KeyError:
                            shop_name = "未知店铺"

                        # 提取图片URL
                        picture_url = ex_content.get("picUrl", "")
                        if not picture_url:
                            picture_url = click_params.get("picUrl", "无图片链接")

                        # 提取标题和包邮信息
                        title = ex_content.get("title", "").strip()
                        tag_text = click_params.get("tagname", "不包邮")

                        # 商品类型
                        item_type = click_params.get("item_type", "未知类型")

                        # 提取商品ID和链接
                        item_id = ex_content.get("itemId", "")
                        product_url = f"https://www.goofish.com/item?id={item_id}"

                        # 提取价格
                        price = click_params.get("price", "未知价格")

                        # 提取地区
                        area = ex_content.get("area", "未知地区").strip()

                        if item is None:
                            self.failed_pages.append(page)
                            self.info(f"第 {page} 页爬取失败")

                        try:
                            item_data = {
                                "seller": shop_name,
                                "title": title,
                                "url": product_url,
                                "price": price,
                                "area": area,
                                "picture_url": picture_url,
                                "item_id": item_id,
                                "tag_text": tag_text, 
                                "item_type": item_type, 
                            }        

                            self.results.append(item_data)
                            present += 1
                            # 计算当前进度
                            progress = (present / len(result_list)) * set_max_progress
                            # 更新进度条变量
                            self.progress_var.set(progress)
                            # 更新进度条显示
                            self.progress_bar.update()


                            # 插入带空图片的行
                            # 更新界面
                            self.treeview_add(item_data["item_id"], item_data["title"], item_data["price"], item_data["seller"], item_data["tag_text"])
                        except Exception as e:
                            self.info(f"解析商品数据失败: {e}")
                            continue
                except (KeyError, ValueError) as e:
                    self.info(f"处理数据时出错: {e}")

                # 点击下一页按钮
                try:
                    # 请求间隔1秒
                    time.sleep(1)
                    next_page = page + 1
                    num = str(next_page)
                    # 输入下一页页数
                    self.page.ele("css:.search-pagination-to-page-input--NDqqDgSl").input(num)
                    # 点击确认按钮
                    self.page.ele("css:.search-pagination-to-page-confirm-button--b51GmTKS").check()
                except Exception as e:
                    self.info(f"点击下一页按钮时出错: {e}")

                self.info(f"第 {page} 页完成, 获取 {len(result_list)} 条商品")

                # 任务完成
                self.work_queue.task_done()

                # 请求间隔1.5秒
                time.sleep(1.5)

            except queue.Empty:
                break
            except Exception as e:
                self.info(f"线程错误: {str(e)}")

    # 对工作线程状态进行监视
    def monitor_workers(self):
        while any(worker.is_alive() for worker in self.workers):
            time.sleep(0.5)

        # 所有线程完成后
        self.root.after(0, self.finish_spider)

    # 处理爬取完成后的数据
    def finish_spider(self):
        self.is_active = False

        # 保存结果
        if self.results:
            keyword = self.keyword_var.get().strip()
            self.results_save(keyword)
            self.info(f"爬取完成! 共获取 {len(self.results)} 条商品数据")
        else:
            self.info("未获取到任何商品数据")

        # 爬取失败页提示
        if self.failed_pages:
            self.info(f"以下页爬取失败: {', '.join(map(str, self.failed_pages))}")

        # 更新界面状态
        self.start_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.status_var.set("就绪")

    # 停止爬取
    def stop_spider(self):
        self.is_active = False
        self.info("正在停止爬取...")
        self.status_var.set("正在停止...")

    # 下载已爬取的图片到本地,强制使用jpg格式
    def image_download(self, picture_url, item_id):
        try:
            # 跳过无图片链接的情况
            if picture_url == "无图片链接":
                return None

            # 处理URL中的特殊字符,补全协议头
            if not picture_url.startswith(('http://', 'https://')):
                picture_url = f"http:{picture_url}" if picture_url.startswith('//') else f"https://{picture_url}"

            # 提取并验证文件后缀
            extension = picture_url.split(".")[-1].split("?")[0].lower()

            # 处理不支持的格式(如.mpo)
            if extension not in IMAGE_SUPPORT:
                self.info(f"检测到不支持的图片格式: {extension},将自动转换为jpg")
                extension = "jpg"  # 强制使用支持的格式

            # 图片文件名,用item_id避免重复
            file_name = f"{SAVE_IMAGES}/{item_id}.{extension}"

            # 已下载则直接返回路径
            if os.path.exists(file_name):
                return file_name

            # 发送请求下载图片
            headers = {"User-Agent": USER_AGENT}
            response = requests.get(picture_url, headers=headers, timeout=10)
            response.raise_for_status()

            # 保存图片到本地
            with open(file_name, "wb") as f:
                f.write(response.content)

            # 强制转换图片格式为jpg
            if extension == "jpg" and picture_url.lower().endswith(('mpo', 'mpo?')):
                try:
                    from PIL import Image as PILImage
                    # 为了兼容jpg格式,打开图片后转换为RGB模式
                    load_image = PILImage.open(file_name)
                    rgb_load_image = load_image.convert('RGB')
                    # 覆盖保存为jpg
                    rgb_load_image.save(file_name, quality=95)
                    self.info(f"特殊图片格式已成功转换为jpg: {item_id}.jpg")
                except Exception as e:
                    self.info(f"图片格式转换失败: {str(e)},使用原始文件")

            return file_name

        except Exception as e:
            self.info(f"图片下载失败({picture_url}): {str(e)}")
            return None

    # 在Treeview中添加一条记录
    def treeview_add(self, item_id, title, price, seller, tagname):
        self.result_tree.insert("", tk.END, values=(item_id, title, price, seller, tagname))

    # 保存结果到Excel,同时向Excel中插入图片数据
    def results_save(self, keyword):
        try:
            # 创建Excel工作簿和工作表
            workbook = Workbook()
            workbook_list = workbook.active
            # 添加表头(包含图片列)
            workbook_list.append(["商品ID", "标题", "价格", "商品链接", "地区", "标签名", "卖家", "商品类型", "图片"])

            # 调整列宽
            workbook_list.column_dimensions["A"].width = 13  # 商品ID
            workbook_list.column_dimensions["B"].width = 30  # 标题
            workbook_list.column_dimensions["C"].width = 10  # 价格
            workbook_list.column_dimensions["D"].width = 15  # 商品链接
            workbook_list.column_dimensions["E"].width = 10  # 地区
            workbook_list.column_dimensions["F"].width = 10  # 标签名
            workbook_list.column_dimensions["G"].width = 10  # 卖家
            workbook_list.column_dimensions["H"].width = 10  # 商品类型
            workbook_list.column_dimensions["I"].width = 20  # 图片

            # 向Excel中写入数据,从第2行开始写入,跳过第1行表头
            for row_idx, data in enumerate(self.results, start=2):  # 
                # 写入文字信息
                workbook_list.cell(row=row_idx, column=1, value=data["item_id"])  # 商品ID
                workbook_list.cell(row=row_idx, column=2, value=data["title"])  # 标题
                workbook_list.cell(row=row_idx, column=3, value=data["price"]) # 价格
                workbook_list.cell(row=row_idx, column=4, value=data["url"])  # 商品链接
                workbook_list.cell(row=row_idx, column=5, value=data["area"])  # 地区
                workbook_list.cell(row=row_idx, column=6, value=data["tag_text"])  # 标签名
                workbook_list.cell(row=row_idx, column=7, value=data["seller"])  # 卖家
                workbook_list.cell(row=row_idx, column=8, value=data["item_type"])  # 商品类型

                # 下载并插入图片
                picture_path = self.image_download(data["picture_url"], data["item_id"])
                if picture_path and os.path.exists(picture_path):
                    try:
                        # 插入图片
                        load_image = Image(picture_path)
                        # 调整图片大小
                        load_image.width = 100
                        load_image.height = 100
                        # 插入到I列当前行
                        workbook_list.add_image(load_image, anchor=f"I{row_idx}")
                        # 调整行高以适应图片
                        workbook_list.row_dimensions[row_idx].height = 80
                    except Exception as e:
                        self.info(f"图片插入失败({picture_path}): {str(e)}")

            # 生成文件名
            set_time = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"{keyword}_{set_time}.xlsx"
            workbook.save(filename)
            self.info(f"数据已保存到 {filename}(含图片)")

            # 提示成功
            messagebox.showinfo("爬取成功", f"共获取 {len(self.results)} 条商品数据")

        except Exception as e:
            self.info(f"保存Excel文件失败: {str(e)}")

    # 点击关闭窗口触发函数
    def GUI_window_close(self):
        # 弹出确认对话框
        if messagebox.askokcancel("退出", "你确定要退出吗?"):
            # 点击确定后关闭窗口
            self.root.destroy()

    # 打开原网站
    def open_website(self):
        # 打开网页
        self.page.get("https://www.goofish.com/")

# 当前模块直接被执行
if __name__ == "__main__":
    # 创建GUI窗口
    root = tk.Tk()
    app = DataCollection(root)
    # 将窗口关闭事件与GUI_window_close函数关联
    root.protocol("WM_DELETE_WINDOW", app.GUI_window_close)
    # 开启主循环,让窗口处于显示状态
    root.mainloop()