使用DrissionPage库爬取京东上的商品信息

0 阅读10分钟

1.配置浏览器内核

1.1代码

from DrissionPage import ChromiumOptions
path = r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'#这里设置为你电脑的浏览器可执行文件路径,注意不要是快捷方式路径,我这里使用的是edge浏览器
ChromiumOptions().set_browser_path(path).save()

1.2使用方法

将上述代码复制并保存到一个python源文件中,使用python运行保存的源文件

1.3结果

如图

image.png 显示上述内容就说明浏览器内核配置完成,之后使用DrissionPage库调用的浏览器就都是这次传入地址的浏览器,除非你更改了浏览器可执行文件地址,所以,运行并正常保存配置后就可以删除该源文件。

2.爬取的原理介绍

2.1核心原理

采取的方式是,使用DrissionPage库进行浏览器自动化加网络监听。 浏览器自动化指它直接操作一个真实的浏览器,模拟人的行为,如打开网页,滚动屏幕,点击下一页等。 网络监听指浏览器在加载网页的时候,会向京东服务器发送异步请求。这些请求返回的是纯净的json数据。DrissionPagelisten功能会拦截这些数据包,从而替代从复杂的html结构中获取数据。

3.确认监听哪个服务器接口

3.1基本操作过程

打开京东网站首页,登陆,在搜索框数据想要爬取的商品,这里以手机为例子。 输入手机后,按f12打开开发者工具,刷新页面,任意选取一个商品的相关信息,比如下图中

image.png

选取"小米 (MI) 小米 17"作为关键词,点击标签栏中的网络选项,使用快捷键ctrl + f调出搜索框,输入"小米 (MI) 小米 17"然后能够出现该信息来源于哪个网站,从而确定其他商品信息也是来源于该网站。 在京东首页搜索栏输入"手机"获取商品信息的来源来自 'api.m.jd.com/api?appid=s…'

4.爬取第一页的所需要的商品信息

4.1提示

一个网页的数据信息可能来源于多个接口,但是同类型的信息应来自于一个接口,与此同时,一个接口可能提供多个不同类型的信息。所以我们的商品信息来自 'api.m.jd.com/api?appid=s…' 不假,但这个接口也可能提供一些无效信息,比如广告信息,推荐信息等。所以我们后续需要处理这个问题

4.2代码

from DrissionPage import ChromiumPage
#创建一个浏览器实例
dp = ChromiumPage()
#浏览器监听这个接口
dp.listen.start('https://api.m.jd.com/api?appid=search-pc-java&t')
#浏览器搜索这个网址
dp.get('https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&enc=utf-8&wq=%E6%89%8B%E6%9C%BA&pvid=12623a1ad5164860ab3fd608da276fc8&spmTag=YTAyMTkuYjAwMjM1Ni5jMDAwMDQ2ODkuc2VhcmNoX2NvbmZpcm0')
#保证爬取的每个字典都是商品字典,能够保证数据结构一致,需要的键都存在
def is_valid_product(item):
    try:
        #只要有一层不存在,或者中间某一层不是字典,就会抛出异常
        _ = item['wareName']
        _ = item['wareBuried']['ori_price']
        _ = item['finalPrice']['estimatedPrice']
        _ = item['totalSales']
        _ = item['shopName']
        _ = item['skuId']
        return True
    except (KeyError, TypeError, IndexError):
        return False
#下滑操作加载更多内容
next_page = dp.ele('text=下一页',timeout=2)
dp.scroll.to_see(next_page)
#监听5个数据包,其中两个数据包是想要的数据信息,将监听到的5个数据包放入字典列表
resp_list = dp.listen.wait(5)
print(f"监听到{len(resp_list)}个响应")
#遍历字典列表里面的每一个字典
for resp in resp_list:
    json_data = resp.response.body
    keys = json_data.keys()
    #做一个条件判断,当字典为abBuriedTagMap这个的时候进行下面的操作(就是将不需要的字典筛选出去)
    if 'abBuriedTagMap' in keys:
        for index in json_data['data']['wareList']:
            if not is_valid_product(index):
                    continue
            title = index['wareName'].replace('\n','')
            dit = {
                '标题': title,
                '原价': index['wareBuried']['ori_price'],
                '售价': index['finalPrice']['estimatedPrice'],
                '销量': index['totalSales'],
                '店铺': index['shopName'],
                '商品ID': index['skuId'],
            }
            print(dit)

这里逐个说明:

#保证爬取的每个字典都是商品字典,能够保证数据结构一致,需要的键都存在
def is_valid_product(item):
    try:
        #只要有一层不存在,或者中间某一层不是字典,就会抛出异常
        _ = item['wareName']
        _ = item['wareBuried']['ori_price']
        _ = item['finalPrice']['estimatedPrice']
        _ = item['totalSales']
        _ = item['shopName']
        _ = item['skuId']
        return True
    except (KeyError, TypeError, IndexError):
        return False

这里的函数是为了保证我们采集到的字典包含需要的子字典的键,防止程序异常中断,因为即使某一个字典和其他商品类型的字典的键是同一个名称,但是还是有可能缺少某些关键的值,或者这个字典只是和商品信息的字典的键名一致而内部完全不是商品信息的相关值。

#下滑操作加载更多内容 
next_page = dp.ele('text=下一页',timeout=2) 
dp.scroll.to_see(next_page) 
#监听5个数据包,其中两个数据包是想要的数据信息,将监听到的5个数据包放入字典列表 resp_list = dp.listen.wait(5) 
print(f"监听到{len(resp_list)}个响应")

这里下滑操作模拟人类的下滑操作,如果不进行下滑,在"手机"这个例子中,服务器的响应只包含了59个商品中的30个商品的数据。

然后是监听5个数据包刚好能够覆盖掉所需要的两个数据包(59个商品信息都包含在这个里面了)

 #做一个条件判断,当字典为abBuriedTagMap这个的时候进行下面的操作(就是将不需要的字典筛选出去)
    if 'abBuriedTagMap' in keys:
        for index in json_data['data']['wareList']:
            if not is_valid_product(index):
                    continue
            title = index['wareName'].replace('\n','')
            dit = {
                '标题': title,
                '原价': index['wareBuried']['ori_price'],
                '售价': index['finalPrice']['estimatedPrice'],
                '销量': index['totalSales'],
                '店铺': index['shopName'],
                '商品ID': index['skuId'],
            }
            print(dit)

这里就是将读取到的字典列表中的5个字典进行筛选,键为"abBuriedTagMap"就是商品,这样就解决了前面提示部分中涉及的问题,"abBuriedTagMap"字典的"['data']['wareList']"子字典难免会缺值,所以调用前面的"is_valid_product()"函数解决这个问题。

4.3结果

如图

image.png 我们观察到标题中有一些html标签,我们可以采用正则匹配的方式过滤。

4.4优化

#增加正则模块
import re
#将"<>"以及里面的内容替换为空
title = index['wareName'].replace('\n','')
new_title = re.sub('<.*?>','',title)
dit = {
    '标题': new_title,

4.5结果

image.png

5.爬取多个页面的商品信息

5.1基本方法

使用while循环,结构如下

while true:
    ......
    if 条件满足:
        break

5.2修改思路

1.添加一个计数器,用来记录当前读取的页数。 2.在while循环中添加“下滑加载页面”“监听响应”“找到下一页按钮并点击”的逻辑

5.3代码

from DrissionPage import ChromiumPage
#导入正则
import re
#创建一个浏览器实例
dp = ChromiumPage()
#浏览器监听这个接口
dp.listen.start('https://api.m.jd.com/api?appid=search-pc-java&t')
#浏览器搜索这个网址
dp.get('https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&enc=utf-8&wq=%E6%89%8B%E6%9C%BA&pvid=12623a1ad5164860ab3fd608da276fc8&spmTag=YTAyMTkuYjAwMjM1Ni5jMDAwMDQ2ODkuc2VhcmNoX2NvbmZpcm0')
#保证爬取的每个字典都是商品字典,能够保证数据结构一致,需要的键都存在
def is_valid_product(item):
    try:
        #只要有一层不存在,或者中间某一层不是字典,就会抛出异常
        _ = item['wareName']
        _ = item['wareBuried']['ori_price']
        _ = item['finalPrice']['estimatedPrice']
        _ = item['totalSales']
        _ = item['shopName']
        _ = item['skuId']
        return True
    except (KeyError, TypeError, IndexError):
        return False
page_count = 1
while True:
    if page_count>=6:
        print("已达到最大读取页数5页,停止读取")
        break
    print(f"正在处理第{page_count}页的商品数据")
    #下滑操作加载更多内容
    next_page = dp.ele('text=下一页',timeout=2)
    dp.scroll.to_see(next_page)
    #监听5个数据包,其中两个数据包是想要的数据信息,将监听到的5个数据包放入字典列表
    resp_list = dp.listen.wait(5)
    print(f"在第{page_count}页监听到{len(resp_list)}个响应")
    #遍历字典列表里面的每一个字典
    for resp in resp_list:
        json_data = resp.response.body
        keys = json_data.keys()
        #做一个条件判断,当字典为abBuriedTagMap这个的时候进行下面的操作(就是将不需要的字典筛选出去)
        if 'abBuriedTagMap' in keys:
            for index in json_data['data']['wareList']:
                if not is_valid_product(index):
                        continue
                title = index['wareName'].replace('\n','')
                new_title = re.sub('<.*?>','',title)
                dit = {
                    '标题': new_title,
                    '原价': index['wareBuried']['ori_price'],
                    '售价': index['finalPrice']['estimatedPrice'],
                    '销量': index['totalSales'],
                    '店铺': index['shopName'],
                    '商品ID': index['skuId'],
                }
                print(dit)
    if next_page:
        print("找到下一页按钮,准备跳转...")
        next_page.click()
        page_count+=1
        # 给页面一点加载时间,或者等待特定的加载圆圈消失
        dp.wait(1)
    else:
        print("找不到下一页按钮,翻页结束。")
        break

逐步解释:

1.page_count是添加的页面计数器

#下滑操作加载更多内容
    next_page = dp.ele('text=下一页',timeout=2)
    dp.scroll.to_see(next_page)
    #监听5个数据包,其中两个数据包是想要的数据信息,将监听到的5个数据包放入字典列表
    resp_list = dp.listen.wait(5)
    print(f"在第{page_count}页监听到{len(resp_list)}个响应")

这里是下滑加载和监听响应的逻辑

if next_page:
        print("找到下一页按钮,准备跳转...")
        next_page.click()
        page_count+=1
        # 给页面一点加载时间,或者等待特定的加载圆圈消失
        dp.wait(1)
    else:
        print("找不到下一页按钮,翻页结束。")
        break

这里是找到下一页并点击,如果没有找到就退出循环。

4.整个过程不断循环,这里我设置的最大读取页数为5页,所以读取完5页就会结束了。

6.保存到文件中

6.1代码

from DrissionPage import ChromiumPage
#导入正则
import re
#引入csv
import csv
#创建文件对象
f = open('output.csv',mode='a',encoding='utf-8-sig',newline='')
#字典写入的方法
csv_writer = csv.DictWriter(f,fieldnames=[
    '标题',
    '原价',
    '售价',
    '销量',
    '店铺',
    '商品ID',
])
#写入表头
if f.tell() == 0:
    csv_writer.writeheader()
#创建一个浏览器实例
dp = ChromiumPage()
#浏览器监听这个接口
dp.listen.start('https://api.m.jd.com/api?appid=search-pc-java&t')
#浏览器搜索这个网址
dp.get('https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&enc=utf-8&wq=%E6%89%8B%E6%9C%BA&pvid=12623a1ad5164860ab3fd608da276fc8&spmTag=YTAyMTkuYjAwMjM1Ni5jMDAwMDQ2ODkuc2VhcmNoX2NvbmZpcm0')
#保证爬取的每个字典都是商品字典,能够保证数据结构一致,需要的键都存在
def is_valid_product(item):
    try:
        #只要有一层不存在,或者中间某一层不是字典,就会抛出异常
        _ = item['wareName']
        _ = item['wareBuried']['ori_price']
        _ = item['finalPrice']['estimatedPrice']
        _ = item['totalSales']
        _ = item['shopName']
        _ = item['skuId']
        return True
    except (KeyError, TypeError, IndexError):
        return False
page_count = 1
while True:
    if page_count>=6:
        print("已达到最大读取页数5页,停止读取")
        break
    print(f"正在处理第{page_count}页的商品数据")
    #下滑操作加载更多内容
    next_page = dp.ele('text=下一页',timeout=2)
    dp.scroll.to_see(next_page)
    #监听5个数据包,其中两个数据包是想要的数据信息,将监听到的5个数据包放入字典列表
    resp_list = dp.listen.wait(5)
    print(f"在第{page_count}页监听到{len(resp_list)}个响应")
    #遍历字典列表里面的每一个字典
    for resp in resp_list:
        json_data = resp.response.body
        keys = json_data.keys()
        #做一个条件判断,当字典为abBuriedTagMap这个的时候进行下面的操作(就是将不需要的字典筛选出去)
        if 'abBuriedTagMap' in keys:
            for index in json_data['data']['wareList']:
                if not is_valid_product(index):
                        continue
                title = index['wareName'].replace('\n','')
                new_title = re.sub('<.*?>','',title)
                dit = {
                    '标题': new_title,
                    '原价': index['wareBuried']['ori_price'],
                    '售价': index['finalPrice']['estimatedPrice'],
                    '销量': index['totalSales'],
                    '店铺': index['shopName'],
                    '商品ID': index['skuId'],
                }
                print(dit)
                csv_writer.writerow(dit)
    if next_page:
        print("找到下一页按钮,准备跳转...")
        next_page.click()
        page_count+=1
        # 给页面一点加载时间,或者等待特定的加载圆圈消失
        dp.wait(1)
    else:
        print("找不到下一页按钮,翻页结束。")
        break

增加的内容是:

#引入csv
import csv
#创建文件对象
f = open('output.csv',mode='a',encoding='utf-8-sig',newline='')
#字典写入的方法
csv_writer = csv.DictWriter(f,fieldnames=[
    '标题',
    '原价',
    '售价',
    '销量',
    '店铺',
    '商品ID',
])
#写入表头
if f.tell() == 0:
    csv_writer.writeheader()

以及在print(dit)下面写 csv_writer.writerow(dit)

需要提醒的是,open函数的参数encoding需要设置为utf-8-sig,否则使用excel打开生成的文件可能会乱码。

6.2结果

image.png 5页有285条数据。