京东商品爬虫工具:从需求到 GUI 实现的完整实践

发布于:2025-08-17 ⋅ 阅读:(22) ⋅ 点赞:(0)

在电商数据分析、竞品调研等场景下,获取京东商品信息是很常见的需求。手动一个个去查看、记录商品数据效率低下,于是我开发了这款带图形界面的京东商品爬虫工具,能便捷地爬取指定商品、指定页数的相关数据并保存。

工具功能与技术栈

功能亮点

  • 图形化界面操作:无需编写代码,通过简单的输入和点击按钮,就能完成商品数据爬取任务。
  • 多参数自定义:可指定要爬取的商品名称,以及爬取的页数(范围 1 - 100 页)。
  • 实时进度反馈:在界面上能实时看到爬取进度、商品提取情况以及错误信息等。
  • 数据持久化:将爬取到的商品数据(包括 SKU、链接、标题、价格等多维度信息)保存到 Excel 文件,方便后续分析。
  • 多线程处理:采用多线程技术,爬取过程不阻塞 GUI 界面,可随时停止爬取任务。

技术栈

  • 界面开发:使用 tkinter 库构建图形化界面,这是 Python 内置的 GUI 库,简单易用。
  • 网页交互与数据提取:借助 DrissionPage 库操作 Chromium 浏览器,实现网页的加载、元素查找与数据提取。
  • 数据处理与存储:利用 pandas 进行数据处理,结合 openpyxl 实现 Excel 文件的读写操作。
  • 多线程:通过 threading 模块实现多线程,避免爬取过程中界面卡顿。

核心代码解析

界面构建

def create_widgets(self):
    # 创建主框架
    main_frame = ttk.Frame(self.root, padding="20")
    main_frame.pack(fill=tk.BOTH, expand=True)

    # 商品名称输入区域
    ttk.Label(main_frame, text="商品名称:").grid(row=0, column=0, sticky=tk.W, pady=5)
    self.product_entry = ttk.Entry(main_frame, width=50)
    self.product_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
    self.product_entry.insert(0, "小米")  # 默认值

    # 爬取页数设置区域
    ttk.Label(main_frame, text="爬取页数:").grid(row=1, column=0, sticky=tk.W, pady=5)
    self.page_frame = ttk.Frame(main_frame)
    self.page_frame.grid(row=1, column=1, sticky=tk.W, pady=5)

    self.page_entry = ttk.Entry(self.page_frame, width=10)
    self.page_entry.pack(side=tk.LEFT)
    self.page_entry.insert(0, "10")  # 默认值

    ttk.Label(self.page_frame, text="页 (最多100页)").pack(side=tk.LEFT, padx=5)

    # 按钮区域
    self.button_frame = ttk.Frame(main_frame)
    self.button_frame.grid(row=2, column=0, columnspan=2, pady=10)

    self.start_btn = ttk.Button(self.button_frame, text="开始爬取", command=self.start_scraping)
    self.start_btn.pack(side=tk.LEFT, padx=5)

    self.stop_btn = ttk.Button(self.button_frame, text="停止爬取", command=self.stop_scraping, state=tk.DISABLED)
    self.stop_btn.pack(side=tk.LEFT, padx=5)

    # 进度显示区域
    ttk.Label(main_frame, text="爬取进度:").grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=5)

    self.progress_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=20)
    self.progress_text.grid(row=4, column=0, columnspan=2, sticky=tk.NSEW, pady=5)
    self.progress_text.config(state=tk.DISABLED)

    # 设置网格权重,使文本区域可伸缩
    main_frame.grid_rowconfigure(4, weight=1)
    main_frame.grid_columnconfigure(1, weight=1)

这段代码通过 tkinter 的布局管理器(grid 和 pack),搭建了包含商品名称输入框、爬取页数输入框、开始 / 停止按钮以及进度显示文本框的界面,并且对界面组件的样式(如字体)进行了设置,确保中文显示正常。

爬取控制与多线程

def start_scraping(self):
    product_name = self.product_entry.get().strip()
    page_str = self.page_entry.get().strip()

    # 验证输入
    if not product_name:
        messagebox.showerror("输入错误", "请输入商品名称")
        return

    try:
        num_pages = int(page_str)
        if num_pages <= 0 or num_pages > 100:
            messagebox.showerror("输入错误", "请输入1-100之间的页数")
            return
    except ValueError:
        messagebox.showerror("输入错误", "请输入有效的页数")
        return

    # 更新按钮状态
    self.start_btn.config(state=tk.DISABLED)
    self.stop_btn.config(state=tk.NORMAL)
    self.scraping = True

    # 清空日志
    self.progress_text.config(state=tk.NORMAL)
    self.progress_text.delete(1.0, tk.END)
    self.progress_text.config(state=tk.DISABLED)

    # 在新线程中开始爬取,避免界面卡顿
    threading.Thread(target=self.scrape_jd, args=(product_name, num_pages), daemon=True).start()

start_scraping 方法先对用户输入的商品名称和爬取页数进行验证,确保输入合法。然后调整按钮状态,最后创建新线程执行核心爬取方法 scrape_jd,这样爬取过程在后台线程进行,不会导致 GUI 界面卡死,提升了用户体验。

商品数据爬取与保存

def scrape_jd(self, shop_name, num_pages=10, batch_size=20):
    # 初始化存储数据的列表
    all_data = []
    # 生成带时间戳的文件名
    timestamp = time.strftime("%Y%m%d_%H%M%S")
    filename = f"京东_{shop_name}_商品数据_{timestamp}.xlsx"
    first_save = True  # 标记是否是第一次保存

    try:
        # 初始化浏览器页面
        dp = ChromiumPage()
        dp.get('https://www.jd.com/')
        self.log("已打开京东首页")

        # 搜索商品(等待搜索框加载完成再输入)
        try:
            search_box = dp.ele('css:.text', timeout=10)
            search_box.input(shop_name)
      
            self.log(f"已搜索: {shop_name}")
            time.sleep(1)  # 等待搜索结果加载
        except Exception as e:
            self.log(f"搜索失败: {str(e)}")
            self.cleanup(dp)
            return

        for page in range(1, num_pages + 1):
            # 检查是否需要停止
            if not self.scraping:
                break

            self.log(f"\n===== 正在爬取第 {page} 页 =====")
            page_data = []  # 存储当前页数据

            # 等待商品列表加载完成(最多等5秒)
            try:
                dp.ele('css:._wrapper_2xp6d_3.plugin_goodsCardWrapper', timeout=5)
            except:
                self.log("当前页商品列表加载失败,跳过该页")
                break

            # 只在页面内容未完全加载时滚动
            if page == 1:
                dp.scroll.to_bottom()
                time.sleep(0.5)

            # 获取当前页所有商品
            lis = dp.eles('css:._wrapper_2xp6d_3.plugin_goodsCardWrapper')
            if not lis:
                self.log("未找到商品,停止爬取")
                break

            for index, li in enumerate(lis, 1):
                # 检查是否需要停止
                if not self.scraping:
                    break

                try:
                    # 1. 商品SKU
                    sku = li.attr('data-sku') or ""
                    # 2. 商品链接
                    product_url = f"https://item.jd.com/{sku}.html" if sku else ""
                    # 3. 商品标题
                    title_elem = li.ele('css:._goods_title_container_1x4i2_1 span', timeout=1)
                    title = title_elem.text.strip() if title_elem else ""
                    # 4. 商品图片链接
                    img_elem = li.ele('css:._img_18s24_1', timeout=1)
                    img_url = img_elem.attr('src') or img_elem.attr('data-src') or ""
                    img_url = f"https:{img_url}" if img_url and not img_url.startswith('http') else img_url
                    # 5. 价格信息
                    price = li.ele('css:._price_1tn4o_13', timeout=1).text.strip() if li.ele('css:._price_1tn4o_13',
                                                                                             timeout=1) else ""
                    original_price = li.ele('css:._gray_1tn4o_48', timeout=1).text.strip() if li.ele(
                        'css:._gray_1tn4o_48',
                        timeout=1) else ""
                    # 6. 销量信息
                    sales = li.ele('css:._goods_volume_1xkku_1 span', timeout=1).text.strip() if li.ele(
                        'css:._goods_volume_1xkku_1 span', timeout=1) else ""
                    # 7. 店铺信息
                    shop_name_elem = li.ele('css:._name_d19t5_35 span', timeout=1)
                    shop_name_text = shop_name_elem.text.strip() if shop_name_elem else ""
                    shop_url_elem = li.ele('css:._name_d19t5_35', timeout=1)
                    shop_url = shop_url_elem.attr('href') if shop_url_elem else ""
                    shop_url = f"https:{shop_url}" if shop_url and not shop_url.startswith('http') else shop_url
                    # 8. 服务标签
                    tag = li.ele('css:._textTag_1qbwk_10 span', timeout=1).text.strip() if li.ele(
                        'css:._textTag_1qbwk_10 span', timeout=1) else ""
                    # 9. 客服链接
                    service_url_elem = li.ele('css:._customer_service_icon_d19t5_14', timeout=1)
                    service_url = service_url_elem.attr('href') if service_url_elem else ""
                    service_url = f"https:{service_url}" if service_url and not service_url.startswith(
                        'http') else service_url

                    # 添加到当前页数据列表
                    page_data.append({
                        'SKU': sku,
                        '商品链接': product_url,
                        '标题': title,
                        '图片链接': img_url,
                        '到手价': price,
                        '原价': original_price,
                        '销量': sales,
                        '店铺名称': shop_name_text,
                        '店铺链接': shop_url,
                        '服务标签': tag,
                        '客服链接': service_url,
                        '爬取页码': page
                    })

                    # 每积累一定数量商品打印一次进度
                    if index % batch_size == 0:
                        self.log(f"第 {page} 页已爬取 {index} 个商品")

                except Exception as e:
                    self.log(f"商品 {index} 提取失败: {str(e)[:50]}")

            # 批量添加当前页数据到总列表
            all_data.extend(page_data)
            self.log(f"第 {page} 页爬取完成,共 {len(page_data)} 个商品")

            # 写入数据到Excel
            try:
                df = pd.DataFrame(page_data)
                if first_save:
                    df.to_excel(filename, index=False, engine='openpyxl')
                    first_save = False
                else:
                    with pd.ExcelWriter(filename, engine='openpyxl', mode='a', if_sheet_exists='overlay') as writer:
                        startrow = writer.sheets['Sheet1'].max_row
                        df.to_excel(writer, index=False, header=False, startrow=startrow)
                self.log(f"第 {page} 页数据已写入文件")
            except Exception as e:
                self.log(f"写入Excel失败: {str(e)}")

            # 翻页
            if page < num_pages and self.scraping:
                try:
                    next_btn = dp.ele('css:._pagination_next_1jczn_8', timeout=3)
                    next_btn.click()
                    time.sleep(1)
                except Exception as e:
                    self.log(f"无法翻到下一页: {e}")
                    break

        # 爬取完成
        self.log(f"\n爬取完成,共获取 {len(all_data)} 条记录")
        self.log(f"数据已保存到: {filename}")


    finally:
        # 清理资源
        try:
            dp.quit()
        except:
            pass
        self.cleanup()

scrape_jd 方法是核心爬取逻辑:

  • 首先初始化浏览器,打开京东首页并搜索指定商品。
  • 然后逐页爬取商品数据,通过 DrissionPage 的元素查找方法,提取商品的 SKU、链接、标题、价格等信息,存储到列表中。
  • 每爬取完一页,就使用 pandas 将当前页数据写入 Excel 文件,支持增量写入,避免数据丢失。
  • 爬取过程中还会处理翻页逻辑,以及异常情况,确保爬取任务尽可能顺利完成。

使用方法

  1. 运行代码后,会弹出图形界面窗口。
  2. 在 “商品名称” 输入框中输入要爬取的商品名称(如 “小米”)。
  3. 在 “爬取页数” 输入框中输入要爬取的页数(1 - 100 之间的整数)。
  4. 点击 “开始爬取” 按钮,开始爬取任务,进度会实时显示在下方文本框中。
  5. 若要中途停止爬取,点击 “停止爬取” 按钮即可。
  6. 爬取完成后,数据会保存为 Excel 文件,文件名包含商品名称和时间戳,方便识别。

资料获取

私信老师

视频讲解链接:自制京东商品爬虫工具,python京东数据采集脚本。_哔哩哔哩_bilibili


网站公告

今日签到

点亮在社区的每一天
去签到