京东商品数据爬取:Python 实战指南

by Mei Lin 21 views

Hey guys! 今天我们要聊聊如何用 Python 爬取京东的商品数据。这个话题是不是听起来就很酷炫?掌握了这项技能,你就可以轻松获取海量的商品信息,为你的研究、分析或者购物决策提供强大的数据支持。Let's dive in!

初步准备:环境配置和库的安装

在开始我们的爬虫之旅之前,我们需要做一些准备工作。首先,确保你的电脑上已经安装了 Python。如果没有安装,赶紧去 Python 官网下载一个吧!接下来,我们需要安装一些必要的 Python 库,这些库将帮助我们更方便地进行网页抓取和数据解析。

安装 DrissionPage

我们今天要使用的主要工具是 DrissionPage,它是一个强大且易于使用的 Python 库,可以帮助我们模拟浏览器行为,从而轻松地抓取动态网页数据。你可以使用 pip 命令来安装它:

pip install DrissionPage

安装完成后,我们就可以开始编写我们的爬虫代码了。记住,好的工具是成功的一半!

其他依赖库

除了 DrissionPage,我们可能还会用到其他一些常用的库,比如 timerandomtime 库可以让我们在爬取过程中控制爬取速度,避免对服务器造成过大的压力;random 库可以生成随机数,用于模拟用户行为,从而更好地避免被网站的反爬机制识别。

import time
import random
from DrissionPage import ChromiumPage,ChromiumOptions

核心代码:JDProductSpider 类的实现

接下来,我们将创建一个名为 JDProductSpider 的类,这个类将负责实现我们的爬虫逻辑。这个类包含了初始化爬虫、搜索商品、提取商品信息以及保存数据等功能。接下来,我会一步步地带你了解这个类的实现细节。

初始化爬虫:__init__ 方法

__init__ 方法是类的构造函数,它在创建类的实例时被调用。在这个方法中,我们会初始化爬虫的一些基本设置,比如配置 Chrome 浏览器、设置用户数据路径、设置端口以及设置一些启动参数。这些参数的设置对于避免被京东的反爬机制识别至关重要。特别是 co.set_argument('--disable-blink-features=AutomationControlled') 这一行代码,它可以有效地隐藏我们的爬虫行为。

class JDProductSpider:
    def __init__(self):
        """
        初始化爬虫
        """
        co = ChromiumOptions()
        co.set_user_data_path('./chrome_data')
        co.set_local_port(9222)
        co.set_argument('--start-maximized')  # 最大化窗口
        co.set_argument('--disable-blink-features=AutomationControlled')  # 避免被检测
        self.page = ChromiumPage(co)
        self.base_url = "https://search.jd.com/Search"

搜索商品:search_products 方法

search_products 方法是爬虫的核心方法之一,它负责根据关键词搜索商品,并获取商品信息。这个方法接收两个参数:keyword(搜索关键词)和 max_pages(要爬取的页数)。在这个方法中,我们首先构造搜索 URL,然后使用 DrissionPage 加载网页。 为了避免被反爬机制识别,我们会模拟用户行为,比如等待页面加载完成、滚动页面以触发懒加载等。同时,我们还会检查页面标题和内容,以确保页面加载正确。

    def search_products(self, keyword, max_pages=1):
        """
        搜索商品并获取信息
        """
        products = []

        for page in range(1, max_pages + 1):
            print(f"正在爬取第 {page} 页...")

            # 构造搜索URL
            search_url = f"{self.base_url}?keyword={keyword}&page={page}"
            print(f"访问URL: {search_url}")
            self.page.get(search_url)

            # 等待页面加载完成
            print("等待页面加载...")
            time.sleep(random.uniform(3, 5))  # 增加等待时间

            # 检查页面标题和内容
            print(f"页面标题: {self.page.title}")
            # 打印部分页面内容用于调试
            print(f"页面部分HTML: {self.page.html[:500]}")

            # 滚动页面以触发懒加载
            print("滚动页面以加载更多内容...")
            for i in range(5):  # 增加滚动次数
                self.page.scroll.down(300)
                time.sleep(1)  # 增加每次滚动后的等待时间

            # 等待元素出现
            print("等待商品元素加载...")
            self.page.wait.eles_loaded('.gl-item', timeout=10)

            # 获取商品列表
            product_items = self.page.eles('.gl-item')
            print(f"找到 {len(product_items)} 个商品元素")

            # 如果没有找到商品,尝试其他选择器
            if len(product_items) == 0:
                print("尝试备用选择器...")
                product_items = self.page.eles('[data-sku]') or self.page.eles('.goods-item')
                print(f"备用选择器找到 {len(product_items)} 个商品元素")

            if len(product_items) == 0:
                print("警告:未找到任何商品元素")
                # 打印更多页面内容用于分析
                print("页面完整HTML结构(前1000字符):")
                print(self.page.html[:1000])

            for i, item in enumerate(product_items):
                print(f"处理第 {i + 1} 个商品...")
                try:
                    product_info = self._extract_product_info(item)
                    if product_info:
                        products.append(product_info)
                        print(f"已添加商品,当前共 {len(products)} 个商品")
                    else:
                        print("未提取到有效商品信息")

                    # 限制处理数量以便调试
                    # if i >= 2:  # 只处理前3个商品进行测试
                    #     print("为调试目的,仅处理前3个商品")
                    #     break
                except Exception as e:
                    print(f"提取第 {i + 1} 个商品信息时出错: {e}")
                    continue

            time.sleep(random.uniform(2, 3))

        return products

提取商品信息:_extract_product_info 方法

_extract_product_info 方法负责从商品列表中提取单个商品的信息。这个方法是整个爬虫中最关键的部分之一,因为它直接决定了我们能够获取哪些数据。在这个方法中,我们会使用 CSS 选择器来定位商品信息的各个元素,比如标题、价格、链接、店铺信息和评论数。为了提高代码的健壮性,我们会对可能出现的异常情况进行处理,并打印详细的调试信息。

    # 在 _extract_product_info 方法中添加更详细的调试信息
    def _extract_product_info(self, item):
        """
        提取单个商品信息
        """
        try:
            print("开始提取商品信息...")

            # 提取标题 - 处理title属性为空的情况
            title_ele = item.ele('.p-name p-name-type-2').ele('tag:a')
            print(f"标题元素: {title_ele}")
            if title_ele:
                print(f"title_ele 的 HTML 内容: {title_ele.html}")
                print(f"title_ele 的所有属性: {title_ele.attrs}")
            else:
                print("title_ele 元素未找到。请检查选择器。")


            title = ""
            if title_ele:
                # 按优先级尝试获取标题
                title = (title_ele.attr('title') or
                         title_ele.text or
                         "")
                # 如果还是空,尝试从em标签获取
                if not title:
                    em_title = title_ele.ele('tag:em')
                    if em_title:
                        title = em_title.text
                print(f"标题内容: '{title}'")


            # 提取价格
            price_ele = item.ele('.p-price i') or item.ele('.price') or item.ele('tag:strong')
            print(f"价格元素: {price_ele}")

            price = ""
            if price_ele:
                price = price_ele.text or price_ele.attr('data-price') or ""
            print(f"价格内容: '{price}'")


            # 提取链接
            link_ele = item.ele('.p-name p-name-type-2').ele('tag:a')
            print(f"链接元素: {link_ele}")

            link = ""
            if link_ele:
                link = link_ele.attr('href') or ""
                if link and not link.startswith('http'):
                    link = 'https:' + link
            print(f"商品链接: '{link}'")

            # 提取店铺信息 - 尝试更多选择器

            shop_ele = item.ele('.curr-shop hd-shopname')
            print(f"店铺元素: {shop_ele}")
            shop = ""
            if shop_ele:
                shop = shop_ele.text or shop_ele.attr('title') or ""
            print(f"店铺名称: '{shop}'")

            # 提取评论数 - 尝试更多选择器
            commit_ele = item.ele('.p-commit').ele('tag:a')
            print(f"评价元素: {commit_ele}")

            commit_count = ""
            if commit_ele:
                commit_count = (commit_ele.text or commit_ele.attr('title') or
                                "")
                print(f"评价数量: '{commit_count}'")


            # 构建结果
            result = {
                'title': title.strip() if title else "标题未知",
                'price': price.strip() if price else "价格未知",
                'link': link.strip() if link else "",
                'shop': shop.strip() if shop else "店铺未知",
                'commit_count': commit_count.strip() if commit_count else "评论未知"
            }

            # 检查是否获取到有效信息
            has_valid_info = any([
                result['title'] != "标题未知",
                result['price'] != "价格未知",
                result['shop'] != "店铺未知"
            ])

            if has_valid_info:
                print(f"成功提取商品: {result['title'][:30]}...")
                return result
            else:
                print("未提取到有效商品信息")
                return None

        except Exception as e:
            print(f"提取商品信息失败: {e}")
            import traceback
            traceback.print_exc()
            return None

保存商品信息:save_to_file 方法

save_to_file 方法负责将爬取到的商品信息保存到文件中。这个方法接收两个参数:products(商品信息列表)和 filename(保存的文件名)。 我们会将商品信息以一定的格式写入文件中,方便后续的分析和使用。保存数据是爬虫的重要环节,因为它可以让我们将爬取到的数据持久化,避免数据丢失。

    def save_to_file(self, products, filename="jd_products.txt"):
        """
        保存商品信息到文件

        Args:
            products (list): 商品信息列表
            filename (str): 保存的文件名
        """
        with open(filename, 'w', encoding='utf-8') as f:
            for product in products:
                f.write(f"标题: {product['title']}\n")
                f.write(f"价格: {product['price']}\n")
                f.write(f"店铺: {product['shop']}\n")
                f.write(f"评论数: {product['commit_count']}\n")
                f.write(f"链接: {product['link']}\n")
                f.write("-" * 50 + "\n")

        print(f"已保存 {len(products)} 条商品信息到 {filename}")

关闭浏览器:close 方法

close 方法负责关闭浏览器。这是一个良好的编程习惯,可以释放资源,避免内存泄漏。在爬虫完成工作后,我们应该调用这个方法来关闭浏览器。

    def close(self):
        """
        关闭浏览器
        """
        self.page.quit()

主函数:main 方法

main 方法是程序的主入口,它负责调用爬虫的各个方法,实现完整的爬取流程。在这个方法中,我们会创建 JDProductSpider 类的实例,然后根据用户输入的关键词和页数,开始爬取商品信息。 爬取完成后,我们会显示爬取到的商品信息,并询问用户是否保存到文件。同时,我们还会对可能出现的异常情况进行处理,并在程序结束时关闭浏览器。

def main():
    """
    主函数
    """
    # 创建爬虫实例
    spider = JDProductSpider()

    try:
        # 搜索关键词
        keyword = input("请输入要搜索的商品关键词: ")
        max_pages = int(input("请输入要爬取的页数(默认1页): ") or "1")

        # 开始爬取
        print(f"开始爬取京东 '{keyword}' 的商品信息...")
        products = spider.search_products(keyword, max_pages)

        # 显示结果
        print(f"\n共爬取到 {len(products)} 件商品:")
        for i, product in enumerate(products[:5], 1):  # 只显示前5个
            print(f"\n{i}. {product['title'][:50]}...")
            print(f"   价格: {product['price']}")
            print(f"   店铺: {product['shop']}")

        # 保存到文件
        if products:
            save_option = input("\n是否保存到文件? (y/n): ")
            if save_option.lower() == 'y':
                spider.save_to_file(products, f"jd_{keyword}_products.txt")

    except Exception as e:
        print(f"爬取过程中出现错误: {e}")
    finally:
        # 关闭浏览器
        spider.close()

if __name__ == "__main__":
    main()

运行爬虫:实战演练

现在,我们已经完成了爬虫代码的编写。接下来,让我们运行一下爬虫,看看效果如何。在运行爬虫之前,请确保你的网络连接正常,并且已经安装了所有必要的库。

  1. 打开终端或命令提示符,进入到你保存爬虫代码的目录。

  2. 运行 Python 脚本,输入以下命令:

    python your_spider_file_name.py
    

    请将 your_spider_file_name.py 替换为你实际保存的文件名。

  3. 按照提示输入要搜索的商品关键词和要爬取的页数。例如,你可以输入 “手机” 作为关键词,输入 “1” 作为页数。

  4. 等待爬虫运行完成。在运行过程中,你会在终端或命令提示符中看到爬虫的运行信息,比如正在爬取的页数、找到的商品数量等。

  5. 查看结果。爬虫运行完成后,如果选择了保存到文件,你会在当前目录下找到一个以 “jd_” 开头的文件,里面包含了爬取到的商品信息。你也可以在终端或命令提示符中看到爬取到的商品信息。

优化建议:让你的爬虫更强大

我们的爬虫已经可以正常工作了,但是还有一些地方可以进行优化,让我们的爬虫更强大、更健壮。以下是一些优化建议:

1. 使用代理 IP

为了避免被京东的反爬机制识别,我们可以使用代理 IP。代理 IP 可以隐藏我们的真实 IP 地址,从而提高爬虫的匿名性。你可以从一些免费的代理 IP 网站或者付费的代理 IP 提供商获取代理 IP。使用代理 IP 的方法有很多,比如可以使用 requests 库的 proxies 参数,或者使用 DrissionPage 提供的代理 IP 设置功能。

2. 设置 User-Agent

User-Agent 是 HTTP 请求头中的一个字段,它包含了客户端的信息,比如浏览器类型、操作系统等。一些网站会根据 User-Agent 来判断请求是否来自爬虫。为了避免被识别为爬虫,我们可以设置 User-Agent 为浏览器的 User-Agent。你可以从网上搜索浏览器的 User-Agent,然后将其设置为爬虫的 User-Agent。

3. 控制爬取速度

过快的爬取速度可能会对服务器造成过大的压力,从而导致 IP 被封禁。为了避免这种情况,我们可以控制爬取速度,比如在每次请求之间添加一定的延迟。你可以使用 time.sleep 函数来实现延迟。

4. 使用多线程或异步 IO

多线程或异步 IO 可以提高爬虫的爬取效率。你可以使用 Python 的 threading 模块来实现多线程,或者使用 asyncio 库来实现异步 IO。但是,使用多线程或异步 IO 也需要注意控制并发数,避免对服务器造成过大的压力。

5. 异常处理和重试机制

在爬取过程中,可能会出现各种各样的异常情况,比如网络连接错误、页面加载失败等。为了提高爬虫的健壮性,我们需要对这些异常情况进行处理。你可以使用 try...except 语句来捕获异常,并进行相应的处理。同时,我们还可以实现重试机制,当爬取失败时,可以尝试重新爬取。

总结:爬虫的乐趣与挑战

通过本文,我们一起学习了如何使用 Python 和 DrissionPage 库来爬取京东的商品数据。爬虫是一项既有趣又有挑战性的技术,它可以帮助我们获取海量的数据,为我们的研究、分析或者购物决策提供强大的数据支持。但是,在编写爬虫的过程中,我们也需要遵守一些规则和道德规范,比如尊重网站的反爬机制、避免对服务器造成过大的压力等。希望本文能够帮助你入门爬虫技术,开启你的数据之旅!

常见问题解答(FAQ)

Q1: 为什么我运行爬虫时总是出现 “403 Forbidden” 错误?

A1: 出现 “403 Forbidden” 错误通常是因为你的 IP 被网站封禁了。你可以尝试使用代理 IP 来解决这个问题。

Q2: 为什么我爬取到的数据不完整?

A2: 爬取到的数据不完整可能是因为网站使用了懒加载技术。你可以尝试滚动页面以触发懒加载,或者使用 DrissionPage 提供的等待元素加载功能。

Q3: 为什么我的爬虫运行速度很慢?

A3: 爬虫运行速度慢可能是因为网络连接不稳定、爬取速度过快或者网站的反爬机制等原因。你可以尝试优化网络连接、控制爬取速度或者使用多线程或异步 IO 来提高爬取效率。

Q4: 如何避免被网站的反爬机制识别?

A4: 避免被网站的反爬机制识别的方法有很多,比如使用代理 IP、设置 User-Agent、控制爬取速度等。同时,我们还需要遵守一些规则和道德规范,尊重网站的反爬机制。

Q5: 如何保存爬取到的数据?

A5: 保存爬取到的数据的方法有很多,比如可以保存到文件中、数据库中等。在本文中,我们使用了 save_to_file 方法将数据保存到文件中。你可以根据自己的需求选择合适的数据保存方式。