# pip install matplotlib pandas numpy requests
#URL和Cookie记得更新



import requests
import json
import re
import time
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import csv
import os
from datetime import datetime
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import urllib.parse

# 确保中文显示正常
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False


class TaobaoLaptopSystem:
    def __init__(self, root):
        self.root = root
        self.root.title("淘宝笔记本电脑数据爬取与分析系统 (CSV版)")
        self.root.geometry("1200x800")
        self.root.minsize(1000, 700)

        # 爬取控制变量
        self.crawling = False
        self.total_pages = 0  # 总爬取页数
        self.current_crawl_page = 0  # 当前正在爬取的页码
        self.total_items_estimate = 0  # 预估总商品数(每页48个)
        self.current_crawl_items = 0  # 当前已爬取商品数

        # CSV文件路径
        self.csv_file = "taobao_laptops.csv"

        # 数据缓存
        self.data = []

        # 创建主框架
        self.main_frame = ttk.Frame(root)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 创建左侧导航栏
        self.nav_frame = ttk.Frame(self.main_frame, width=200)
        self.nav_frame.pack(side=tk.LEFT, fill=tk.Y)

        self.nav_buttons = [
            ("数据爬取", self.show_crawl_frame),
            ("数据展示", self.show_display_frame),
            ("价格分布", self.show_price_distribution_frame),
            ("销量排行", self.show_sales_rank_frame),
            ("店铺地区分布", self.show_region_distribution_frame),
            ("价格区间分析", self.show_price_range_frame),
            ("导入/导出", self.show_import_export_frame)
        ]

        for text, command in self.nav_buttons:
            btn = ttk.Button(self.nav_frame, text=text, command=command)
            btn.pack(fill=tk.X, padx=5, pady=5)

        # 创建右侧内容框架
        self.content_frame = ttk.Frame(self.main_frame)
        self.content_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # 加载现有数据
        self.load_data()

        # 初始显示数据爬取页面
        self.show_crawl_frame()

    def load_data(self):
        """从CSV文件加载数据"""
        self.data = []
        if os.path.exists(self.csv_file):
            try:
                with open(self.csv_file, 'r', encoding='utf-8') as f:
                    reader = csv.DictReader(f)
                    for row in reader:
                        # 转换数据类型
                        row['price'] = float(row['price'])
                        row['sales'] = int(row['sales'])
                        self.data.append(row)
                print(f"已从 {self.csv_file} 加载 {len(self.data)} 条数据")
            except Exception as e:
                print(f"加载CSV文件出错: {e}")
                self.data = []
        else:
            # 如果文件不存在,创建空的CSV文件
            self.save_data()

    def save_data(self):
        """保存数据到CSV文件"""
        try:
            with open(self.csv_file, 'w', encoding='utf-8', newline='') as f:
                if self.data:
                    fieldnames = self.data[0].keys()
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows(self.data)
                else:
                    # 创建空文件时写入表头
                    fieldnames = ['id', 'title', 'price', 'price_str', 'sales', 'sales_str',
                                  'shop_name', 'address', 'crawl_time']
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
            print(f"数据已保存到 {self.csv_file}")
        except Exception as e:
            print(f"保存CSV文件出错: {e}")

    def clear_content_frame(self):
        """清空内容框架"""
        for widget in self.content_frame.winfo_children():
            widget.destroy()

    def show_crawl_frame(self):
        """显示数据爬取页面"""
        self.clear_content_frame()

        crawl_frame = ttk.Frame(self.content_frame)
        crawl_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 配置网格权重,使控件能够随窗口大小调整
        crawl_frame.columnconfigure(0, weight=1)
        crawl_frame.columnconfigure(1, weight=1)
        crawl_frame.rowconfigure(12, weight=1)
        crawl_frame.rowconfigure(14, weight=1)

        # Cookie配置
        ttk.Label(crawl_frame, text="请输入淘宝Cookie字符串:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.cookie_entry = scrolledtext.ScrolledText(crawl_frame, height=5)
        self.cookie_entry.grid(row=1, column=0, columnspan=2, sticky=tk.W + tk.E + tk.N + tk.S, pady=5)

        # 尝试加载已保存的cookie
        try:
            with open("cookie.txt", "r", encoding="utf-8") as f:
                self.cookie_entry.insert(tk.END, f.read())
        except:
            pass

        ttk.Button(crawl_frame, text="测试Cookie", command=self.test_cookie).grid(row=2, column=0, pady=5)
        ttk.Button(crawl_frame, text="保存Cookie", command=self.save_cookie).grid(row=2, column=1, pady=5)

        # 搜索配置
        ttk.Label(crawl_frame, text="搜索关键词:").grid(row=3, column=0, sticky=tk.W, pady=5)
        self.keyword_entry = ttk.Entry(crawl_frame)
        self.keyword_entry.insert(0, "笔记本电脑")
        self.keyword_entry.grid(row=4, column=0, columnspan=2, sticky=tk.W + tk.E, pady=5)

        ttk.Label(crawl_frame, text="爬取页数:").grid(row=5, column=0, sticky=tk.W, pady=5)
        self.page_entry = ttk.Entry(crawl_frame)
        self.page_entry.insert(0, "3")
        self.page_entry.grid(row=6, column=0, sticky=tk.W, pady=5)

        # 爬取控制按钮
        control_frame = ttk.Frame(crawl_frame)
        control_frame.grid(row=7, column=0, columnspan=2, pady=10)

        ttk.Button(control_frame, text="开始爬取", command=self.start_crawl).pack(side=tk.LEFT, padx=10)
        ttk.Button(control_frame, text="停止爬取", command=self.stop_crawl).pack(side=tk.LEFT, padx=10)
        ttk.Button(control_frame, text="清空数据", command=self.clear_data).pack(side=tk.LEFT, padx=10)

        # 实时进度
        ttk.Label(crawl_frame, text="实时进度:").grid(row=8, column=0, sticky=tk.W, pady=5)
        self.progress_var = tk.StringVar()
        self.progress_var.set("准备就绪")
        ttk.Label(crawl_frame, textvariable=self.progress_var).grid(row=9, column=0, sticky=tk.W, pady=5)

        # 总进度条
        ttk.Label(crawl_frame, text="总爬取进度:").grid(row=10, column=0, sticky=tk.W, pady=5)
        self.total_progress_bar = ttk.Progressbar(
            crawl_frame,
            orient=tk.HORIZONTAL,
            length=100,
            mode='determinate',
            maximum=100
        )
        self.total_progress_bar.grid(row=11, column=0, columnspan=2, sticky=tk.W + tk.E, pady=5)
        self.total_progress_label = ttk.Label(crawl_frame, text="0% (0/0 商品)")
        self.total_progress_label.grid(row=12, column=0, sticky=tk.W, pady=2)

        # 当前页进度条
        ttk.Label(crawl_frame, text="当前页进度:").grid(row=13, column=0, sticky=tk.W, pady=5)
        self.page_progress_bar = ttk.Progressbar(
            crawl_frame,
            orient=tk.HORIZONTAL,
            length=100,
            mode='determinate',
            maximum=48
        )
        self.page_progress_bar.grid(row=14, column=0, columnspan=2, sticky=tk.W + tk.E, pady=5)
        self.page_progress_label = ttk.Label(crawl_frame, text="0/48 商品")
        self.page_progress_label.grid(row=15, column=0, sticky=tk.W, pady=2)

        # 操作日志
        ttk.Label(crawl_frame, text="操作日志:").grid(row=16, column=0, sticky=tk.W, pady=5)
        self.log_text = scrolledtext.ScrolledText(crawl_frame, height=10)
        self.log_text.grid(row=17, column=0, columnspan=2, sticky=tk.W + tk.E + tk.N + tk.S, pady=5)

        # 设置行权重
        for i in [1, 12, 14, 17]:
            crawl_frame.rowconfigure(i, weight=1)

    def test_cookie(self):
        """测试Cookie有效性"""
        try:
            cookie = self.cookie_entry.get("1.0", tk.END).strip()
            if not cookie:
                messagebox.showwarning("警告", "请输入Cookie")
                return

            test_url = "https://h5api.m.taobao.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/"
            headers = {
                'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.68',
                'cookie': cookie,
                'referer': 'https://s.taobao.com/'
            }
            res = requests.get(test_url, headers=headers, timeout=10)

            if res.status_code == 200:
                self.log_text.insert(tk.END, "Cookie测试成功\n")
                messagebox.showinfo("测试结果", "Cookie测试成功")
            else:
                self.log_text.insert(tk.END, f"Cookie测试失败,状态码: {res.status_code}\n")
                messagebox.showerror("测试结果", f"Cookie测试失败,状态码: {res.status_code}")

            self.log_text.see(tk.END)

        except Exception as e:
            self.log_text.insert(tk.END, f"Cookie测试出错: {str(e)}\n")
            messagebox.showerror("测试错误", f"Cookie测试出错: {str(e)}")
            self.log_text.see(tk.END)

    def save_cookie(self):
        """保存Cookie到文件"""
        cookie = self.cookie_entry.get("1.0", tk.END).strip()
        if not cookie:
            messagebox.showwarning("警告", "请输入Cookie")
            return

        with open("cookie.txt", "w", encoding="utf-8") as f:
            f.write(cookie)
        self.log_text.insert(tk.END, "Cookie已保存到cookie.txt\n")
        messagebox.showinfo("保存成功", "Cookie已保存到cookie.txt")
        self.log_text.see(tk.END)

    def start_crawl(self):
        """开始爬取数据"""
        if self.crawling:
            messagebox.showinfo("提示", "正在爬取中,请不要重复点击")
            return

        self.crawling = True
        self.progress_var.set("爬取中...")
        self.log_text.insert(tk.END, "开始爬取数据...\n")
        self.log_text.see(tk.END)

        try:
            keyword = self.keyword_entry.get().strip()
            if not keyword:
                keyword = "笔记本电脑"

            # 编码关键词用于URL
            encoded_keyword = urllib.parse.quote(keyword)

            # 获取爬取页数
            try:
                self.total_pages = int(self.page_entry.get().strip())
                if self.total_pages < 1 or self.total_pages > 20:
                    raise ValueError
            except:
                messagebox.showwarning("警告", "请输入1-20之间的数字作为爬取页数")
                self.crawling = False
                self.progress_var.set("准备就绪")
                return

            # 初始化进度条参数
            self.current_crawl_page = 0
            self.current_crawl_items = 0
            self.total_items_estimate = self.total_pages * 48
            self.total_progress_bar["value"] = 0
            self.total_progress_label["text"] = f"0% (0/{self.total_items_estimate} 商品)"
            self.page_progress_bar["value"] = 0
            self.page_progress_label["text"] = "0/48 商品"

            # 获取Cookie
            try:
                with open("cookie.txt", "r", encoding="utf-8") as f:
                    cookie = f.read().strip()
            except:
                cookie = self.cookie_entry.get("1.0", tk.END).strip()

            if not cookie:
                messagebox.showwarning("警告", "请输入或保存Cookie")
                self.crawling = False
                self.progress_var.set("准备就绪")
                return

            # 设置请求头
            headers = {
                'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.68',
                'cookie': cookie,
                'referer': 'https://s.taobao.com/',
                'accept': 'application/json, text/javascript, */*; q=0.01',
                'x-requested-with': 'XMLHttpRequest'
            }

            item_count = 0
            # 循环爬取每一页
            for page in range(1, self.total_pages + 1):
                if not self.crawling:
                    break

                self.current_crawl_page = page
                page_item_count = 0
                self.log_text.insert(tk.END, f"开始爬取第{page}页...\n")
                self.log_text.see(tk.END)
                self.progress_var.set(f"正在爬取第{page}/{self.total_pages}页")
                self.page_progress_bar["value"] = 0
                self.page_progress_label["text"] = "0/48 商品"
                self.root.update_idletasks()

                try:
                    # 构造请求URL
                    timestamp = int(time.time() * 1000)
                    url = f"https://h5api.m.taobao.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/?jsv=2.7.4&appKey=12574478&t=1758161006706&sign=ed76504f47eb10ccb4e8fe80d3524b53&api=mtop.relationrecommend.wirelessrecommend.recommend&v=2.0&timeout=10000&type=jsonp&dataType=jsonp&callback=mtopjsonp16&data=%7B%22appId%22%3A%2234385%22%2C%22params%22%3A%22%7B%5C%22device%5C%22%3A%5C%22HMA-AL00%5C%22%2C%5C%22isBeta%5C%22%3A%5C%22false%5C%22%2C%5C%22grayHair%5C%22%3A%5C%22false%5C%22%2C%5C%22from%5C%22%3A%5C%22nt_history%5C%22%2C%5C%22brand%5C%22%3A%5C%22HUAWEI%5C%22%2C%5C%22info%5C%22%3A%5C%22wifi%5C%22%2C%5C%22index%5C%22%3A%5C%224%5C%22%2C%5C%22rainbow%5C%22%3A%5C%22%5C%22%2C%5C%22schemaType%5C%22%3A%5C%22auction%5C%22%2C%5C%22elderHome%5C%22%3A%5C%22false%5C%22%2C%5C%22isEnterSrpSearch%5C%22%3A%5C%22true%5C%22%2C%5C%22newSearch%5C%22%3A%5C%22false%5C%22%2C%5C%22network%5C%22%3A%5C%22wifi%5C%22%2C%5C%22subtype%5C%22%3A%5C%22%5C%22%2C%5C%22hasPreposeFilter%5C%22%3A%5C%22false%5C%22%2C%5C%22prepositionVersion%5C%22%3A%5C%22v2%5C%22%2C%5C%22client_os%5C%22%3A%5C%22Android%5C%22%2C%5C%22gpsEnabled%5C%22%3A%5C%22false%5C%22%2C%5C%22searchDoorFrom%5C%22%3A%5C%22srp%5C%22%2C%5C%22debug_rerankNewOpenCard%5C%22%3A%5C%22false%5C%22%2C%5C%22homePageVersion%5C%22%3A%5C%22v7%5C%22%2C%5C%22searchElderHomeOpen%5C%22%3A%5C%22false%5C%22%2C%5C%22search_action%5C%22%3A%5C%22initiative%5C%22%2C%5C%22sugg%5C%22%3A%5C%22_4_1%5C%22%2C%5C%22sversion%5C%22%3A%5C%2213.6%5C%22%2C%5C%22style%5C%22%3A%5C%22list%5C%22%2C%5C%22ttid%5C%22%3A%5C%22600000%40taobao_pc_10.7.0%5C%22%2C%5C%22needTabs%5C%22%3A%5C%22true%5C%22%2C%5C%22areaCode%5C%22%3A%5C%22CN%5C%22%2C%5C%22vm%5C%22%3A%5C%22nw%5C%22%2C%5C%22countryNum%5C%22%3A%5C%22156%5C%22%2C%5C%22m%5C%22%3A%5C%22pc%5C%22%2C%5C%22page%5C%22%3A1%2C%5C%22n%5C%22%3A48%2C%5C%22q%5C%22%3A%5C%22%25E7%25AC%2594%25E8%25AE%25B0%25E6%259C%25AC%25E7%2594%25B5%25E8%2584%2591%5C%22%2C%5C%22qSource%5C%22%3A%5C%22url%5C%22%2C%5C%22pageSource%5C%22%3A%5C%22%5C%22%2C%5C%22channelSrp%5C%22%3A%5C%22%5C%22%2C%5C%22tab%5C%22%3A%5C%22all%5C%22%2C%5C%22pageSize%5C%22%3A48%2C%5C%22totalPage%5C%22%3A100%2C%5C%22totalResults%5C%22%3A4800%2C%5C%22sourceS%5C%22%3A%5C%220%5C%22%2C%5C%22sort%5C%22%3A%5C%22_coefp%5C%22%2C%5C%22bcoffset%5C%22%3A%5C%22%5C%22%2C%5C%22ntoffset%5C%22%3A%5C%22%5C%22%2C%5C%22filterTag%5C%22%3A%5C%22%5C%22%2C%5C%22service%5C%22%3A%5C%22%5C%22%2C%5C%22prop%5C%22%3A%5C%22%5C%22%2C%5C%22loc%5C%22%3A%5C%22%5C%22%2C%5C%22start_price%5C%22%3Anull%2C%5C%22end_price%5C%22%3Anull%2C%5C%22startPrice%5C%22%3Anull%2C%5C%22endPrice%5C%22%3Anull%2C%5C%22itemIds%5C%22%3Anull%2C%5C%22p4pIds%5C%22%3Anull%2C%5C%22p4pS%5C%22%3Anull%2C%5C%22categoryp%5C%22%3A%5C%22%5C%22%2C%5C%22ha3Kvpairs%5C%22%3Anull%2C%5C%22myCNA%5C%22%3A%5C%22gJ5KIcEknxUCAd9KApH0VX4v%5C%22%2C%5C%22screenResolution%5C%22%3A%5C%221536x864%5C%22%2C%5C%22userAgent%5C%22%3A%5C%22Mozilla%2F5.0%20(Windows%20NT%2010.0%3B%20Win64%3B%20x64)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F140.0.0.0%20Safari%2F537.36%20Edg%2F140.0.0.0%5C%22%2C%5C%22couponUnikey%5C%22%3A%5C%22%5C%22%2C%5C%22subTabId%5C%22%3A%5C%22%5C%22%2C%5C%22np%5C%22%3A%5C%22%5C%22%2C%5C%22clientType%5C%22%3A%5C%22h5%5C%22%2C%5C%22isNewDomainAb%5C%22%3A%5C%22false%5C%22%2C%5C%22forceOldDomain%5C%22%3A%5C%22false%5C%22%7D%22%7D&bx-ua=231!Kj43iAmUGDA%2Bj3IAW%2B3Hts%2BjUCg32xTcI9QnxMDGrTnM2MlrTwPsP24S7y72KKXu%2F5b032ZBHPI5DHa9KWgNvWitv2j78RpsRrCyxl9RdrXpog3HVnmUM0cjoJwoJWTasGHGviHQyVsN54uGUCKszzEQ84O9rcuIMHL%2BjeVCBmFeuEvi%2FsHl09AfbQ9bha8tnDWxYxFtexd3sV9DfPTo7sbzi0fkaJcQyhQgfyEVaU7UfetavmVk%2BzC%2B3ko10Q3oyE%2BjauHhUYZJYVIGHkE%2B%2B%2B3%2BqCS4%2BItgC46bg8VCo%2Br%2BJbkdTY3U%2F%2F2KdFHC4tnMsSFUADINl5HMDK3%2FSVt5jzsA9%2BXLYAhzBDVj7b%2Bcd9VG2lJ1klSYLnH9sv0tX%2BtvC8wI5f%2BOEEMFkwPeZVuCKH70%2F5gjx1nPfdWzOTB4vB0c%2BlwENSqy0V%2BL4y4lApdZXkbIlJOIyAx92t6hAaWdU756QzdJBRw2tqsUE952MbEeD8V%2BWka8yyH5ZVlI3Lb3wyp0LjqZoUS3lJgxmX3vb7v8KYZ7ctnEYJ21tyoX9fplSHNs4Wu7MZIeNH0U6E4DyT%2BcDkal1ZIW4ZesE%2F%2Bsox%2FJvwo77F0ws7HriyLN6kjUO7QuVZbA6P7M5VZuo83SZpiLnMc%2FV2fT1yx0qs9X0%2BmBiswlBD8sk%2FJFStIHoQQSbB0M9l8ZkhEjjXxNx57DnAT%2FkcMdCF01J%2Fi2VBfMwGxzYggZdIfmJ%2FDy0SFcP19pGl0bTwTDJ6KRLEwiwuC7TK4gkzoOERXmtYZcd4D7sMaUmeP8%2FxWgjG1LboQe0bRqXc9mB1lpTja%2B6%2BOtv9JEXxKMLHkq7%2FjDPyvt3WVyNauZbxc9LVZPAGtSrtCgoL0UBX78Fj9I1ezt2wbNlLtp77i3PTE8LOIeR9bZNesS6AgNUCUHuLUqRYBcS8JJQOdWG6%2F2W9jCzzhmfTkSsA8OtNNdBbHuKLLQVPwmrRUyUNqSOXKpGnaqYHjdl16mJNy5aVoTOXeu6VwCL%2FdcTMDOkhq9eai8WMCv3bboynelcFJVEnFJTlrpwxHz%2F3y0H8Hw3%2B2q0iCo%2F04Lvks4qZGYPlNUgAWa1eKzyIfdoLi5%2BTZ4Q%2FaMSM6vw4gGOyCmZPav4Vz1pgf3O2xP%2FsyeVU5Us9BDRIh1Abqe%2FBwwfxwKmuQ2K53ZD3NOA2QlgT9fjJsR8SRpQgYE46EOobceONccr2vZ8wR4rxZty5oxf7rcDGeFU2SYuWQng0uKQqw3pY8XCcypLWO4bwy2sDzTihNVxBa77o67MOVTRIel%2FvaSLP1aNlzXa4W9dWk2R6tFzFeqLMgTlO%2FznzzajpFgMmbcNMFgnvHRFTB2dLoVpLXD%2FFudbFJ8xYPhJbk5x35Ohie9eqN4xzvX%2FClm%2FgDv5D4sgZESFSjPI88iF7jbJkIcize6kcv0macCE4ajgp%2BrxtjmVWebYjUVCSIXQolQNLkpbp5AKox12e59okJWhF%2FgBpuc2hxx0JtcJowvKe7DU1ubemR3Pzdh8zZACNyLIsHbkil6ARPIgfD4bgpt0cswBUbvBZE4kuGd%2FZieqJ93vi3FYLDpm02KQsRg81J%2F3NdM0WT9JuxkDu2ppK7ZLtokRLlil53qpJoDkWytKIKniUmKiES%3D&bx-umidtoken=T2gAvVxPB_96oEqI-ZUsyHoyWnkiQ3O2vsjfqlS0Yt6gYvVyoxW0azqDBjKpUFiiXLI%3D&bx_et=gOZEKojpRMIejuO1A5nyQLnwW7iKO05fYuGSE82odXcnO8Do45V5pXwl97rrs8UCp41pUpEag3tC9wek40ilGssfcJHK20fjbPXyzeHxF3jWx3mKj2vOCGsfcJepdvXbIiZ5MLPZeUxuZ20MIxGiKBY3Zc0iefxnEHDksOk-s0DHx2cgIxMexBxuZRXZefcoq2VoSOk-sbmoqjmjZjtZ32XZN_gFc6ux8j2n_nyTbv09JJlwq3qa72lccf-kqlk3UIKh_n7SirhIlbNGfHozIYPI1SjDbc04k8MzTM-nAP4TvDZ1gQgEXX00rVWhxRoETqrn8g5nYzVL42Ueeteix5UxM2bO6ArQc4lx7Cf3CRlnuzPC63cQuRVZllO6c0FTr7DzsBSPPQHMEmq88QYr-AHZGOWNE42hUbHtIp8Jy2UtQj6dpUL--AHZGOWwyU3KDAlfp9C.."

                    # 发送请求
                    res = requests.get(url=url, headers=headers, timeout=15)
                    self.log_text.insert(tk.END, f"第{page}页请求状态码: {res.status_code}\n")

                    if res.status_code == 200:
                        # 处理响应
                        response_text = res.text.strip()

                        # 提取JSON部分
                        try:
                            start_idx = response_text.find('(') + 1
                            end_idx = response_text.rfind(')')
                            json_str = response_text[start_idx:end_idx].strip()
                            json_str = re.sub(r'[\x00-\x1F\x7F]', '', json_str)

                            # 解析JSON
                            da = json.loads(json_str)

                            # 提取商品数据
                            if "data" in da and "itemsArray" in da["data"]:
                                page_items = da["data"]["itemsArray"]
                                total_page_items = len(page_items)
                                self.page_progress_bar["maximum"] = total_page_items

                                for idx, item in enumerate(page_items):
                                    if not self.crawling:
                                        break

                                    try:
                                        # 提取商品信息
                                        title = re.sub(r'<.*?>', '', item.get('title', '未知标题'))

                                        # 处理价格
                                        price_show = item.get('priceShow', {})
                                        price_str = f"{price_show.get('price', '0')}元"
                                        try:
                                            price = float(re.sub(r'[^\d.]', '', price_str))
                                        except:
                                            price = 0

                                        # 处理销量
                                        sales_str = item.get('realSales', '0')
                                        try:
                                            sales_num = float(re.sub(r'[^\d.]', '', sales_str))
                                            if '万' in sales_str:
                                                sales_num *= 10000
                                            sales = int(sales_num)
                                        except:
                                            sales = 0

                                        # 店铺信息
                                        shop_info = item.get('shopInfo', {})
                                        shop_name = shop_info.get('title', '未知店铺')
                                        address = item.get('procity', '未知地区')

                                        # 添加到数据列表
                                        new_item = {
                                            'id': len(self.data) + 1,
                                            'title': title,
                                            'price': price,
                                            'price_str': price_str,
                                            'sales': sales,
                                            'sales_str': sales_str,
                                            'shop_name': shop_name,
                                            'address': address,
                                            'crawl_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                                        }
                                        self.data.append(new_item)

                                        item_count += 1
                                        page_item_count += 1
                                        self.current_crawl_items += 1
                                        self.log_text.insert(tk.END, f"已爬取: {title[:30]}...\n")
                                        self.log_text.see(tk.END)

                                        # 更新进度条
                                        self.page_progress_bar["value"] = page_item_count
                                        self.page_progress_label["text"] = f"{page_item_count}/{total_page_items} 商品"
                                        total_progress_percent = (
                                                                             self.current_crawl_items / self.total_items_estimate) * 100
                                        self.total_progress_bar["value"] = total_progress_percent
                                        self.total_progress_label[
                                            "text"] = f"{total_progress_percent:.1f}% ({self.current_crawl_items}/{self.total_items_estimate} 商品)"
                                        self.root.update_idletasks()

                                        time.sleep(0.5)

                                    except Exception as e:
                                        self.log_text.insert(tk.END, f"处理商品出错: {str(e)}\n")
                                        self.log_text.see(tk.END)
                                        continue

                                self.log_text.insert(tk.END, f"第{page}页爬取完成,共{page_item_count}个商品\n")
                            else:
                                self.log_text.insert(tk.END, f"第{page}页未找到商品数据\n")

                        except json.JSONDecodeError as e:
                            self.log_text.insert(tk.END, f"JSON解析失败: {str(e)}\n")
                        except Exception as e:
                            self.log_text.insert(tk.END, f"处理响应时出错: {str(e)}\n")
                    else:
                        self.log_text.insert(tk.END, f"第{page}页请求失败,状态码: {res.status_code}\n")

                    time.sleep(2)

                except Exception as e:
                    self.log_text.insert(tk.END, f"第{page}页爬取出错: {str(e)}\n")
                    self.log_text.see(tk.END)
                    continue

            # 爬取结束处理
            if self.crawling:
                # 保存数据到CSV
                self.save_data()

                self.total_progress_bar["value"] = 100
                self.total_progress_label["text"] = f"100% ({item_count}/{item_count} 商品)"
                self.page_progress_bar["value"] = self.page_progress_bar["maximum"] if self.page_progress_bar[
                                                                                           "maximum"] > 0 else 0
                self.page_progress_label[
                    "text"] = f"{self.page_progress_bar['value']}/{self.page_progress_bar['value']} 商品"

                self.log_text.insert(tk.END, f"爬取完成,共获取{item_count}个商品数据\n")
                self.progress_var.set(f"爬取完成,共{item_count}个商品")
                messagebox.showinfo("爬取完成", f"成功爬取{item_count}个商品数据")
            else:
                # 保存已爬取的数据
                self.save_data()

                total_progress_percent = (
                                                     self.current_crawl_items / self.total_items_estimate) * 100 if self.total_items_estimate > 0 else 0
                self.total_progress_bar["value"] = total_progress_percent
                self.total_progress_label[
                    "text"] = f"{total_progress_percent:.1f}% ({self.current_crawl_items}/{self.total_items_estimate} 商品)"

                self.log_text.insert(tk.END, f"爬取已停止,已获取{item_count}个商品数据\n")
                self.progress_var.set(f"已停止,共{item_count}个商品")

        except Exception as e:
            self.log_text.insert(tk.END, f"爬取过程出错: {str(e)}\n")
            messagebox.showerror("爬取错误", f"爬取过程出错: {str(e)}")
        finally:
            self.crawling = False
            self.log_text.see(tk.END)
            self.root.update_idletasks()

    def stop_crawl(self):
        """停止爬取数据"""
        if self.crawling:
            self.crawling = False
            self.progress_var.set("正在停止...")
            self.log_text.insert(tk.END, "正在停止爬取...\n")
            self.log_text.see(tk.END)
        else:
            messagebox.showinfo("提示", "当前没有正在进行的爬取任务")

    def clear_data(self):
        """清空数据"""
        if messagebox.askyesno("确认", "确定要清空所有爬取的数据吗?此操作不可恢复!"):
            self.data = []
            self.save_data()
            self.log_text.insert(tk.END, "数据已清空\n")
            messagebox.showinfo("成功", "数据已清空")
            self.log_text.see(tk.END)

    def show_display_frame(self):
        """显示数据展示页面"""
        self.clear_content_frame()

        display_frame = ttk.Frame(self.content_frame)
        display_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 显示数据总量
        ttk.Label(display_frame, text=f"共收录 {len(self.data)} 条笔记本电脑数据", font=("SimHei", 12)).pack(pady=10)

        # 创建表格
        columns = ("id", "标题", "价格", "销量", "店铺名称", "地区")
        tree = ttk.Treeview(display_frame, columns=columns, show="headings")

        # 设置列宽和标题
        tree.column("id", width=50, anchor="center")
        tree.column("标题", width=300, anchor="w")
        tree.column("价格", width=80, anchor="center")
        tree.column("销量", width=80, anchor="center")
        tree.column("店铺名称", width=150, anchor="w")
        tree.column("地区", width=100, anchor="center")

        for col in columns:
            tree.heading(col, text=col)

        # 添加滚动条
        scrollbar_y = ttk.Scrollbar(display_frame, orient=tk.VERTICAL, command=tree.yview)
        scrollbar_x = ttk.Scrollbar(display_frame, orient=tk.HORIZONTAL, command=tree.xview)
        tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)

        # 布局
        scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
        scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
        tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # 分页控制
        pagination_frame = ttk.Frame(display_frame)
        pagination_frame.pack(side=tk.BOTTOM, pady=10)

        self.current_page = 1
        self.page_size = 20

        def load_page(page_num):
            # 清空现有数据
            for item in tree.get_children():
                tree.delete(item)

            # 计算偏移量
            offset = (page_num - 1) * self.page_size
            end_idx = min(offset + self.page_size, len(self.data))

            # 插入数据
            for i in range(offset, end_idx):
                item = self.data[i]
                tree.insert("", tk.END, values=(
                    item['id'],
                    item['title'],
                    f"¥{item['price']}",
                    item['sales'],
                    item['shop_name'],
                    item['address']
                ))

            # 更新页码显示
            total_pages = (len(self.data) + self.page_size - 1) // self.page_size
            page_info_var.set(f"第 {page_num} 页,共 {total_pages} 页")
            self.current_page = page_num

            # 控制按钮状态
            prev_btn.config(state=tk.NORMAL if page_num > 1 else tk.DISABLED)
            next_btn.config(state=tk.NORMAL if page_num < total_pages else tk.DISABLED)

        def prev_page():
            if self.current_page > 1:
                load_page(self.current_page - 1)

        def next_page():
            total_pages = (len(self.data) + self.page_size - 1) // self.page_size
            if self.current_page < total_pages:
                load_page(self.current_page + 1)

        # 分页按钮
        prev_btn = ttk.Button(pagination_frame, text="上一页", command=prev_page)
        prev_btn.pack(side=tk.LEFT, padx=10)

        page_info_var = tk.StringVar()
        ttk.Label(pagination_frame, textvariable=page_info_var).pack(side=tk.LEFT, padx=10)

        next_btn = ttk.Button(pagination_frame, text="下一页", command=next_page)
        next_btn.pack(side=tk.LEFT, padx=10)

        # 初始加载第一页
        load_page(1)

    def show_price_distribution_frame(self):
        """显示价格分布页面"""
        self.clear_content_frame()

        distribution_frame = ttk.Frame(self.content_frame)
        distribution_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        ttk.Label(distribution_frame, text="笔记本电脑价格分布", font=("SimHei", 14)).pack(pady=10)

        try:
            # 获取价格数据
            prices = [item['price'] for item in self.data if item['price'] > 0]

            if not prices:
                ttk.Label(distribution_frame, text="没有找到价格数据,请先爬取数据").pack(pady=20)
                return

            # 创建图表
            fig, ax = plt.subplots(figsize=(8, 5))

            # 设置 bins
            max_price = max(prices)
            if max_price > 20000:
                bins = range(0, 25001, 2500)
            elif max_price > 10000:
                bins = range(0, 15001, 1500)
            else:
                bins = range(0, 10001, 1000)

            # 绘制直方图
            n, bins, patches = ax.hist(prices, bins=bins, alpha=0.7, color='#4CAF50')

            # 设置标题和标签
            ax.set_title('笔记本电脑价格分布直方图', fontsize=12)
            ax.set_xlabel('价格 (元)', fontsize=10)
            ax.set_ylabel('商品数量', fontsize=10)

            # 添加网格线
            ax.grid(axis='y', linestyle='--', alpha=0.7)

            # 调整布局
            plt.tight_layout()

            # 将图表嵌入Tkinter
            canvas = FigureCanvasTkAgg(fig, master=distribution_frame)
            canvas.draw()
            canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

            # 统计信息
            stats_frame = ttk.Frame(distribution_frame)
            stats_frame.pack(pady=20)

            avg_price = sum(prices) / len(prices)
            median_price = np.median(prices)
            min_price = min(prices)
            max_price = max(prices)

            ttk.Label(stats_frame, text=f"平均价格: ¥{avg_price:.2f}", font=("SimHei", 10)).grid(row=0, column=0, padx=20)
            ttk.Label(stats_frame, text=f"中位数价格: ¥{median_price:.2f}", font=("SimHei", 10)).grid(row=0, column=1,
                                                                                                 padx=20)
            ttk.Label(stats_frame, text=f"最低价格: ¥{min_price:.2f}", font=("SimHei", 10)).grid(row=0, column=2, padx=20)
            ttk.Label(stats_frame, text=f"最高价格: ¥{max_price:.2f}", font=("SimHei", 10)).grid(row=0, column=3, padx=20)

        except Exception as e:
            ttk.Label(distribution_frame, text=f"生成价格分布图表出错: {str(e)}").pack(pady=20)

    def show_sales_rank_frame(self):
        """显示销量排行页面"""
        self.clear_content_frame()

        sales_frame = ttk.Frame(self.content_frame)
        sales_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        ttk.Label(sales_frame, text="笔记本电脑销量排行榜", font=("SimHei", 14)).pack(pady=10)

        # 排行数量选择
        rank_frame = ttk.Frame(sales_frame)
        rank_frame.pack(pady=10)

        ttk.Label(rank_frame, text="显示前:").pack(side=tk.LEFT, padx=5)

        self.rank_count = tk.StringVar(value="10")
        ttk.Combobox(rank_frame, textvariable=self.rank_count, values=["5", "10", "20", "30"], width=5).pack(
            side=tk.LEFT, padx=5)

        def update_rank():
            # 清除现有图表
            for widget in chart_frame.winfo_children():
                widget.destroy()

            try:
                count = int(self.rank_count.get())

                # 获取销量排行数据
                sorted_data = sorted(self.data, key=lambda x: x['sales'], reverse=True)[:count]

                if not sorted_data:
                    ttk.Label(chart_frame, text="没有找到销量数据,请先爬取数据").pack(pady=20)
                    return

                # 处理数据
                titles = [f"{item['title'][:10]}..." for item in sorted_data]
                sales = [item['sales'] for item in sorted_data]
                prices = [item['price'] for item in sorted_data]

                # 创建图表
                fig, ax1 = plt.subplots(figsize=(10, 6))

                # 销量柱状图
                color = 'tab:blue'
                ax1.set_xlabel('商品', fontsize=10)
                ax1.set_ylabel('销量', color=color, fontsize=10)
                bars = ax1.bar(titles, sales, color=color, alpha=0.7, label='销量')
                ax1.tick_params(axis='y', labelcolor=color)
                ax1.tick_params(axis='x', rotation=45)

                # 价格折线图(双Y轴)
                ax2 = ax1.twinx()
                color = 'tab:red'
                ax2.set_ylabel('价格 (元)', color=color, fontsize=10)
                ax2.plot(titles, prices, color=color, marker='o', label='价格')
                ax2.tick_params(axis='y', labelcolor=color)

                # 添加标题和图例
                fig.suptitle(f'销量前{count}的笔记本电脑', fontsize=12)
                lines1, labels1 = ax1.get_legend_handles_labels()
                lines2, labels2 = ax2.get_legend_handles_labels()
                ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper right')

                # 调整布局
                plt.tight_layout()

                # 将图表嵌入Tkinter
                canvas = FigureCanvasTkAgg(fig, master=chart_frame)
                canvas.draw()
                canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

            except Exception as e:
                ttk.Label(chart_frame, text=f"生成销量排行榜出错: {str(e)}").pack(pady=20)

        ttk.Button(rank_frame, text="更新排行", command=update_rank).pack(side=tk.LEFT, padx=5)

        # 图表区域
        chart_frame = ttk.Frame(sales_frame)
        chart_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        # 初始显示
        update_rank()

    def show_region_distribution_frame(self):
        """显示店铺地区分布页面"""
        self.clear_content_frame()

        region_frame = ttk.Frame(self.content_frame)
        region_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        ttk.Label(region_frame, text="笔记本电脑销售店铺地区分布", font=("SimHei", 14)).pack(pady=10)

        try:
            # 获取地区数据
            addresses = [item['address'] for item in self.data if item['address'] != '未知地区']

            if not addresses:
                ttk.Label(region_frame, text="没有找到地区数据,请先爬取数据").pack(pady=20)
                return

            # 处理地区数据,提取省份
            province_data = {}
            for addr in addresses:
                if addr.startswith(('北京', '上海', '天津', '重庆')):
                    province = addr[:2]
                elif addr.startswith('内蒙古'):
                    province = '内蒙古'
                elif addr.startswith('新疆'):
                    province = '新疆'
                elif addr.startswith('宁夏'):
                    province = '宁夏'
                elif addr.startswith('广西'):
                    province = '广西'
                elif addr.startswith('西藏'):
                    province = '西藏'
                else:
                    province = addr[:1] + '省'

                if province in province_data:
                    province_data[province] += 1
                else:
                    province_data[province] = 1

            # 按数量排序,取前10
            sorted_provinces = sorted(province_data.items(), key=lambda x: x[1], reverse=True)[:10]
            provinces = [item[0] for item in sorted_provinces]
            counts = [item[1] for item in sorted_provinces]

            # 创建图表
            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))

            # 饼图
            ax1.pie(counts, labels=provinces, autopct='%1.1f%%', startangle=90)
            ax1.axis('equal')
            ax1.set_title('地区分布占比', fontsize=12)

            # 柱状图
            ax2.bar(provinces, counts, color='skyblue')
            ax2.set_title('地区店铺数量', fontsize=12)
            ax2.set_xlabel('地区', fontsize=10)
            ax2.set_ylabel('店铺数量', fontsize=10)
            ax2.tick_params(axis='x', rotation=45)

            # 调整布局
            plt.tight_layout()

            # 将图表嵌入Tkinter
            canvas = FigureCanvasTkAgg(fig, master=region_frame)
            canvas.draw()
            canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        except Exception as e:
            ttk.Label(region_frame, text=f"生成地区分布图表出错: {str(e)}").pack(pady=20)

    def show_price_range_frame(self):
        """显示价格区间分析页面"""
        self.clear_content_frame()

        price_range_frame = ttk.Frame(self.content_frame)
        price_range_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        ttk.Label(price_range_frame, text="笔记本电脑价格区间分析", font=("SimHei", 14)).pack(pady=10)

        try:
            # 获取价格和销量数据
            prices = [item['price'] for item in self.data if item['price'] > 0]
            sales = [item['sales'] for item in self.data if item['sales'] > 0]

            if not prices or not sales:
                ttk.Label(price_range_frame, text="没有找到足够的数据,请先爬取数据").pack(pady=20)
                return

            # 定义价格区间
            ranges = [
                (0, 3000),
                (3000, 5000),
                (5000, 7000),
                (7000, 10000),
                (10000, float('inf'))
            ]
            range_labels = [
                "3000元以下",
                "3000-5000元",
                "5000-7000元",
                "7000-10000元",
                "10000元以上"
            ]

            # 统计每个区间的商品数量和总销量
            range_counts = [0] * len(ranges)
            range_sales = [0] * len(ranges)
            range_avg_price = [0.0] * len(ranges)

            for i, (min_p, max_p) in enumerate(ranges):
                items_in_range = [(p, s) for p, s in zip(prices, sales) if min_p <= p < max_p]
                range_counts[i] = len(items_in_range)
                range_sales[i] = sum(s for _, s in items_in_range)
                if range_counts[i] > 0:
                    range_avg_price[i] = sum(p for p, _ in items_in_range) / range_counts[i]

            # 创建图表
            fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))

            # 各区间商品数量
            ax1.bar(range_labels, range_counts, color='lightgreen')
            ax1.set_title('各价格区间商品数量', fontsize=12)
            ax1.set_xlabel('价格区间', fontsize=10)
            ax1.set_ylabel('商品数量', fontsize=10)
            ax1.tick_params(axis='x', rotation=45)

            # 各区间总销量
            ax2.bar(range_labels, range_sales, color='orange')
            ax2.set_title('各价格区间总销量', fontsize=12)
            ax2.set_xlabel('价格区间', fontsize=10)
            ax2.set_ylabel('总销量', fontsize=10)
            ax2.tick_params(axis='x', rotation=45)

            # 各区间平均价格
            ax3.bar(range_labels, range_avg_price, color='purple')
            ax3.set_title('各价格区间平均价格', fontsize=12)
            ax3.set_xlabel('价格区间', fontsize=10)
            ax3.set_ylabel('平均价格 (元)', fontsize=10)
            ax3.tick_params(axis='x', rotation=45)

            # 调整布局
            plt.tight_layout()

            # 将图表嵌入Tkinter
            canvas = FigureCanvasTkAgg(fig, master=price_range_frame)
            canvas.draw()
            canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        except Exception as e:
            ttk.Label(price_range_frame, text=f"生成价格区间分析图表出错: {str(e)}").pack(pady=20)

    def show_import_export_frame(self):
        """显示导入/导出页面"""
        self.clear_content_frame()

        import_export_frame = ttk.Frame(self.content_frame)
        import_export_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        ttk.Label(import_export_frame, text="数据导入/导出", font=("SimHei", 14)).pack(pady=10)

        # 导出功能
        export_frame = ttk.LabelFrame(import_export_frame, text="导出数据")
        export_frame.pack(fill=tk.X, padx=10, pady=10)

        ttk.Button(export_frame, text="导出为CSV文件", command=self.export_csv).pack(pady=10)

        # 导入功能
        import_frame = ttk.LabelFrame(import_export_frame, text="导入数据")
        import_frame.pack(fill=tk.X, padx=10, pady=10)

        ttk.Button(import_frame, text="从CSV文件导入", command=self.import_csv).pack(pady=10)

        # 状态信息
        self.import_export_status = tk.StringVar()
        self.import_export_status.set("准备就绪")
        ttk.Label(import_export_frame, textvariable=self.import_export_status).pack(pady=10)

    def export_csv(self):
        """导出数据为CSV文件"""
        if not self.data:
            messagebox.showwarning("警告", "没有数据可导出")
            return

        file_path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
            title="保存CSV文件"
        )

        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8', newline='') as f:
                    fieldnames = self.data[0].keys()
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows(self.data)

                self.import_export_status.set(f"数据已导出到: {file_path}")
                messagebox.showinfo("成功", f"数据已成功导出到 {file_path}")
            except Exception as e:
                self.import_export_status.set(f"导出失败: {str(e)}")
                messagebox.showerror("错误", f"导出失败: {str(e)}")

    def import_csv(self):
        """从CSV文件导入数据"""
        file_path = filedialog.askopenfilename(
            filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
            title="选择CSV文件"
        )

        if file_path:
            try:
                with open(file_path, 'r', encoding='utf-8') as f:
                    reader = csv.DictReader(f)
                    imported_data = []
                    for row in reader:
                        # 转换数据类型
                        row['price'] = float(row['price'])
                        row['sales'] = int(row['sales'])
                        imported_data.append(row)

                self.data = imported_data
                self.save_data()
                self.import_export_status.set(f"数据已从 {file_path} 导入")
                messagebox.showinfo("成功", f"成功导入 {len(self.data)} 条数据")
            except Exception as e:
                self.import_export_status.set(f"导入失败: {str(e)}")
                messagebox.showerror("错误", f"导入失败: {str(e)}")


if __name__ == "__main__":
    root = tk.Tk()
    app = TaobaoLaptopSystem(root)
    root.mainloop()

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐