Python爬虫实战:京东拍拍验机(帮你买到成色完美的二手iPhone)

网站介绍&使用痛点

  如图1所示,京东拍拍二手于近期上线了一个新平台:拍拍验机,顾客可以通过手机京东客户端,在拍拍验机上挑选购买心仪的二手电子产品(目前均为固定品牌的手机,如二手小米8、二手iPhone 7 Plus)。
图1 拍拍验机主页示意图

  在首页,我们可以对手机进行筛选,比如笔者目前倾向于购买二手iPhone 7 Plus 128G 国行,那么就选择“苹果”->“苹果7P”->筛选“国行”和“128G”,如图2所示。确定后下拉页面,就都是符合筛选条件的手机了。
图2 拍拍验机筛选示意图

  但是,在下拉查看比较手机的过程当中,笔者发现符合筛选条件的手机太多太多了(实测大概400部左右),如果每一个手机都点进去查看对比的话,要把所有手机看一遍,将会消耗大量的时间,而且往往之前看中的手机再回过头来看,已经显示卖出,从而错失良“机”。此外由于手机app端服务器不稳定的原因,在下拉页面浏览的过程中,往往返回上一页面会显示“加载失败”,从而要从头开始重新筛选一遍,再下拉到刚刚浏览的地方,效率十分低下。

  当在浏览过程中,看到心仪的手机,点击进去查看手机的详细介绍,会发现有很多的关键参数,比如电池电量、是否过保以及手机各项详细检测情况,如图3所示。如果我们能针对这些参数进行自主筛选,那么就可以迅速排除掉400多部手机中的绝大多数,“取其精华,去其糟粕”,只留下少量的“精品”进行挑选,这样效率将大大提高。
图3 手机详细介绍示意图

  但是网站提供的筛选条件少得可怜,只有“价格”、“渠道”、“内存”三个筛选条件,既然如此,我们就要自己动手丰衣足食了。

网站分析

  在浏览拍拍验机的主页后,我们发现它一次只会加载20部手机信息,并且加载的方式不是翻页,而是将页面滑动到底部,那么爬取静态网页数据的那一套方法就不能用在现在这种动态网页上(静态网页用Chrome浏览器右键“查看网页源代码”就能看到所有的网页数据,但是动态网页用这种办法是看不到的),这对获取新增的手机数据带来了困难。这里使用Chrome浏览器的“检查”功能解析AJAX动态加载地址,进而找到加载的数据。

  使用Chrome浏览器打开拍拍验机的主页,页面任意处右键,单机“检查”-> Network-> Ctrl + R刷新,每当滑动网页页面到底部时,Network下方会出现新加载的内容,单击 JS,单击左侧的一个_JPx(最好是选最底部的),单击 Headers,可以看到Request URL,如图4所示,后面要用到。注意链接里必须包含order=2_1,因为在实践中发现有些链接包含的是order=4_2,包含order=4_2的链接不是我们所需要的。
图4 寻找Request URL示意图

  可以发现,每当网页下拉加载新的手机数据的时候,JS里的_JPx就会增加一项,每次下拉刷新得到的新数据就是请求了

1
https://bizgw.jd.com/app/inspected/v1/list?pageSize=20&pageNo=1&brand=Apple&model=iphone7%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=128g&callback=__jp7

这个网页的json数据。单击Preview,可以看到json数据的结构,如图5所示。
图5 json数据的结构

  其中,我们可以看到新加载的每一个手机的唯一的”commodityId”,后面要用到。将网页一直向下拉,JS中的_JPx就会一直增加,直到网页拉到底。如图6所示,可以看到_JPx增加到_JP22,_JP7到_JP22中就包含了所有的手机的”commodityId”。之后就需要通过Python爬虫将所有的”commodityId”收集起来。
图6 所有JS示意图

  接着点击进入任意一个手机的详细介绍页面,同样的方法,我们找到商品详情介绍页面的Request URL,详情介绍页面的数据就是请求了

1
https://bizgw.jd.com/commodityInspected/view?callback=jQuery34005582774663554724_1558160057557&commodityId=5583330&optSource=3&_=1558160057558

这个网页的json数据,如图7所示。
图7 寻找Request URL示意图

  在Preview中可以看到json数据的结构,包括保修期、电池电量、验机报告等详细检测数据,甚至能看到网页中看不到的信息,比如“验机中心”,如图8所示。
图8 单个商品json数据的结构

  通过Python爬虫,根据单个商品json数据的结构中的各种详细参数进行筛选,就能迅速排除掉大量干扰项,达到“取其精华,去其糟粕”的目的了。

项目实施

1. 爬取commodityId

1.1 失败的爬取思路

  通过分析,我们得到_JP7到_JP22包含了所有的手机的”commodityId”。那么就采用常用的requests.get()的方法,通过for循环,将每一个_JPx里的所有”commodityId”爬取出来保存到一个列表中,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#-*- coding: utf-8 -*-
import requests
import re

#定义函数scrapy
def scrapy(link):
headers = {
#User-Agent和Referer的信息在Network->Headers->Request Headers
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
'Referer':'https://paipai.m.jd.com/direct/?lng=119.954575&lat=31.686691&un_area=12_978_4459_53900&sid=b4d89db2c431e6c9631e9859ecdc099w&_ts=1557926133167&utm_source=iosapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL&ad_od=share&ShareTm=kL%2BfusKf1qywVd%2BwTkAVtGYRXA2gzBQqFj4A0YuGC1teMZhlGn5CDqBdFZvEgw8/2zhgK18/FX3A93HO04B6bPi7qv8sqdX5i/bUuilkbnOp3GDR%2BoHeKPmG1hagOq5B0WaZTCW%2B7KvJdTxfEYpNNpGoA%2BsAMlUKlUnkVPBzD2g='
}
r = requests.get(link, headers=headers)
return(r.text)

#将_jp7到_JP22的所有Request URL保存到一个列表中
Req_URL = []
for i in range(7, 23):
Req_URL.append('https://bizgw.jd.com/app/inspected/v1/list?pageSize=20&pageNo=1&brand=Apple&model=iphone7%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=128g&callback=__jp'+str(i))

#将_jp7到_JP22中的的所有commodityId保存到一个列表中
all_Id = []
for i in Req_URL:
m_findall = []
html = ""
html = scrapy(i)
m_findall = re.findall(r'"commodityId":.{7}', html)#目前网站上机器码暂为7位数
for id in m_findall:
all_Id.append(id[-7:])

通过上述代码,就可以将所有的商品ID储存到列表all_Id当中,但是上述代码有两个问题:
  1. 运行代码后,发现每一个_JPx中的商品Id都是相同的,原因尚不清楚;
  2. 拍拍验机平台的手机数量是实时刷新的,现在我们看到的是所有商品Id保存在_JP7到_JP22,也许一小时后就变成_JP8到_JP30。

1.2 改进后的巧妙的爬取思路

  上述两个问题,第二个问题还好解决,但是第一个问题百思不得其解,涉及比较深入,对于笔者这种学习Python爬虫不足一月的人来说很难解,于是乎转而在Request URL上动心思。原版的Request URL为:

1
https://bizgw.jd.com/app/inspected/v1/list?pageSize=20&pageNo=1&brand=Apple&model=iphone7%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=128g&callback=__jp7

笔者将此链接进行一定的删改,发现并不影响request.get()的结果,于是乎将多余的删掉,得到:

1
https://bizgw.jd.com/app/inspected/v1/list?pageSize=20&brand=Apple&model=iphone7%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=128g

此时能爬取出20个商品ID。这时发现是不是pageSize=20限制了爬出的ID数量?遂改成pageSize=10000,神奇的事情发生了,400多个商品ID被一次全部爬出!实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#-*- coding: utf-8 -*-
import re
import requests

#定义函数scrapy
def scrapy(link):
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
'Referer':'https://paipai.m.jd.com/direct/?lng=119.954575&lat=31.686691&un_area=12_978_4459_53900&sid=b4d89db2c431e6c9631e9859ecdc099w&_ts=1557926133167&utm_source=iosapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL&ad_od=share&ShareTm=kL%2BfusKf1qywVd%2BwTkAVtGYRXA2gzBQqFj4A0YuGC1teMZhlGn5CDqBdFZvEgw8/2zhgK18/FX3A93HO04B6bPi7qv8sqdX5i/bUuilkbnOp3GDR%2BoHeKPmG1hagOq5B0WaZTCW%2B7KvJdTxfEYpNNpGoA%2BsAMlUKlUnkVPBzD2g='
}
r = requests.get(link, headers=headers)
return(r.text)

url = 'https://bizgw.jd.com/app/inspected/v1/list?pageSize=10000&brand=Apple&model=iphone7%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=128g'
html = scrapy(url)
m_findall = re.findall(r'"commodityId":.{7}', html)#目前网站上机器码暂为7位数

#将所有commodityId保存到一个列表中
all_Id = []
for i in m_findall:
all_Id.append(i[-7:])

  此方法巧妙地跳过了翻页的限制,只需一个修改后的Request URL就可爬出所有商品ID。

2. 增加筛选项

  拍拍验机平台的筛选项只有“价格”、“渠道”、“内存”三个筛选条件,但我们可以根据手机的详细介绍页面里的各种参数,利用爬虫任意增加筛选条件,这里我想增加的筛选条件为:电量>=80;全网通;背光异常:无;划痕:无;维修痕迹:无;屏幕碎裂:无;黑点:无;红屏:无;老化发黄:无;亮斑:无;色斑:无;背光异常:无;验机中心非:西安、郑州、河北。这里以一个商品ID为5583330的手机举例,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#-*- coding: utf-8 -*-
import re
import requests

#定义函数scrapy
def scrapy(link):
headers = {
#User-Agent和Referer的信息在Network->Headers->Request Headers
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
'Referer':'https://paipai.m.jd.com/c2c/m/popGoodsDetail?commodityId=5583330&refresh=no&scrollY=0'
}
r = requests.get(link, headers=headers)
return(r.text)

link = 'https://paipai.m.jd.com/c2c/m/popGoodsDetail?commodityId=5583330&refresh=no&scrollY=0'
html = scrapy(link)
m01_findall = re.findall(r'[\u4e00-\u9fa5]+', html)#筛出所有汉字
m02_findall = re.findall(r'电池效率","value":"\d{2}', html)#筛出电池电量

try:
if '立即购买' in m01_findall:
'''
筛选条件:
电量>=80;全网通;背光异常:无;划痕:无;维修痕迹:无;屏幕碎裂:无;黑点:无;红屏:无;老化发黄:无;亮斑:无;色斑:无;
背光异常:无;验机中心不是:西安、郑州、河北
'''
check_bat = (int(m02_findall[0][-2:]) >= 80)
check01 = ('全网通' in m01_findall) & (m01_findall[m01_findall.index('背光异常')+1] == '无')
check02 = (m01_findall[m01_findall.index('外壳')+2] == '无') & (m01_findall[m01_findall.index('维修痕迹')+1] == '无')
check03 = (m01_findall[m01_findall.index('屏幕碎裂')+1] == '无') & (m01_findall[m01_findall.index('黑点')+1] == '无')
check04 = (m01_findall[m01_findall.index('红屏')+1] == '无') & (m01_findall[m01_findall.index('老化发黄')+1] == '无')
check05 = (m01_findall[m01_findall.index('亮斑')+1] == '无') & (m01_findall[m01_findall.index('色斑')+1] == '无')
check06 = (m01_findall[-2][:2] != '西安') & (m01_findall[-2][:2] != '河北') & (m01_findall[-2][:2] != '郑州')
if check_bat & check01 & check02 & check03 & check04 & check05 & check06:
print("完美")
else:
print("不完美")
else:
print("页面异常or已卖出")
except:
print("不可预知的异常")

3. 筛选所有商品

  将“爬取commodityId”的代码和“增加筛选项”的代码结合,通过循环依次筛选每一个商品,将符合筛选条件的商品的链接放到一个列表中,最后依次打印输出,并且自动调用默认浏览器打开通过筛选的商品介绍页面。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#-*- coding: utf-8 -*-
import re
import requests
import webbrowser

#定义函数scrapy01
def scrapy01(link):
headers = {
#User-Agent和Referer的信息在Network->Headers->Request Headers
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
'Referer':'https://paipai.m.jd.com/direct/?lng=119.954575&lat=31.686691&un_area=12_978_4459_53900&sid=b4d89db2c431e6c9631e9859ecdc099w&_ts=1557926133167&utm_source=iosapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL&ad_od=share&ShareTm=kL%2BfusKf1qywVd%2BwTkAVtGYRXA2gzBQqFj4A0YuGC1teMZhlGn5CDqBdFZvEgw8/2zhgK18/FX3A93HO04B6bPi7qv8sqdX5i/bUuilkbnOp3GDR%2BoHeKPmG1hagOq5B0WaZTCW%2B7KvJdTxfEYpNNpGoA%2BsAMlUKlUnkVPBzD2g='
}
r = requests.get(link, headers=headers)
return(r.text)

#定义函数scrapy02
def scrapy02(link):
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
'Referer':'https://paipai.m.jd.com/c2c/m/popGoodsDetail?commodityId='+link[-7:]+'&refresh=no&scrollY=0'
}
r = requests.get(link, headers=headers)
return(r.text)

phone_urls = []#保存每个手机相关的Request URL
'''
在拍拍验机主页筛选好机型后,右键检查->Network->JS->Ctrl+R刷新->网页下拉->选择最靠下的_JPxx->Headers->General->Request URL,
将获得的Request URL进行删减和修改pageSize=10000,得到urlNo1
'''
#iPhone7 Plus 128G 国行
urlNo1 = 'https://bizgw.jd.com/app/inspected/v1/list?pageSize=10000&brand=Apple&model=iphone7%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=128g'
#iPhone8 Plus 64G 国行
#urlNo1 = 'https://bizgw.jd.com/app/inspected/v1/list?pageSize=10000&brand=Apple&model=iphone%208%20plus&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=64g'
#iPhone8 64G 国行
#urlNo1 = 'https://bizgw.jd.com/app/inspected/v1/list?pageSize=10000&brand=Apple&model=iphone%208&order=2_1&buychannel=%E5%9B%BD%E8%A1%8C&capacity=64g'

htmlNo1 = scrapy01(urlNo1)
'''
在代码a中存在多处“data-commodityid="5614566"”,要筛选出其中的数字,使用正则表达式r'"commodityId":.{7}'。
其中,r''的形式是防止字符串内的\转义;"commodityId":表示找到字符串中每一处commodityid;
.{7}表示匹配"commodityId":后的7位任意字符(举例:{7}表示只匹配7个字符,{4,7}表示匹配第2位到第4位字符)
'''
m_findall = re.findall(r'"commodityId":.{7}', htmlNo1)#目前网站上机器码暂为7位数,有待以后更新
for i in m_findall:
phone_urls.append('https://bizgw.jd.com/commodityInspected/view?commodityId='+i[-7:])#这里没有加“&refresh=no&scrollY=0”,不影响结果

fir_urls = []#存放验机中心
end_urls = []#存放通过筛选的URL
a_ = 1
for i in phone_urls:
html_01 = ""
m01_findall = []
m02_findall = []
html_01 = scrapy02(i)
m01_findall = re.findall(r'[\u4e00-\u9fa5]+', html_01)#筛出所有汉字
m02_findall = re.findall(r'电池效率","value":"\d{2}', html_01)#筛出电池电量

#输出通过筛选的url
try:
if '立即购买' in m01_findall:
'''
筛选条件:
电量>=80;全网通;背光异常:无;划痕:无;维修痕迹:无;屏幕碎裂:无;黑点:无;红屏:无;老化发黄:无;亮斑:无;色斑:无;
背光异常:无;验机中心不是:西安、郑州、河北
'''
check_bat = (int(m02_findall[0][-2:]) >= 80)
check01 = ('全网通' in m01_findall) & (m01_findall[m01_findall.index('背光异常')+1] == '无')
check02 = (m01_findall[m01_findall.index('外壳')+2] == '无') & (m01_findall[m01_findall.index('维修痕迹')+1] == '无')
check03 = (m01_findall[m01_findall.index('屏幕碎裂')+1] == '无') & (m01_findall[m01_findall.index('黑点')+1] == '无')
check04 = (m01_findall[m01_findall.index('红屏')+1] == '无') & (m01_findall[m01_findall.index('老化发黄')+1] == '无')
check05 = (m01_findall[m01_findall.index('亮斑')+1] == '无') & (m01_findall[m01_findall.index('色斑')+1] == '无')
check06 = (m01_findall[-2][:2] != '西安') & (m01_findall[-2][:2] != '河北') & (m01_findall[-2][:2] != '郑州')
if check_bat & check01 & check02 & check03 & check04 & check05 & check06:
print('No.'+str(a_)+' '+i[-7:]+' '+"完美")
fir_urls.append('https://paipai.m.jd.com/c2c/m/popGoodsDetail?commodityId='+i[-7:]+'&refresh=no&scrollY=0'+' '+m01_findall[-2])
end_urls.append('https://paipai.m.jd.com/c2c/m/popGoodsDetail?commodityId='+i[-7:]+'&refresh=no&scrollY=0')
else:
print('No.'+str(a_)+' '+i[-7:]+' '+"不完美")
else:
print('No.'+str(a_)+' '+i[-7:]+' '+"页面异常or已卖出")
except:
print('No.'+str(a_)+' '+i[-7:]+' '+"不可预知的异常")
a_ += 1

a_02 = 1
for i in fir_urls:
print('No.'+str(a_02)+' '+i)
a_02 += 1
#调用默认浏览器自动打开网址
for i in end_urls:
webbrowser.open(i)

  拍拍验机平台上符合“iPhone 7 Plus”、“国行”、“128G”筛选条件的手机有近400部,通过Python爬虫定向筛选,最后输出十多部,耗时1分钟左右,效率提升显著。并且今后可以根据个人喜好不同,自由搭配、增删筛选条件,提升购机效率和精准度。

相关文章推荐

  Python爬虫实战-帮你选好车

————————— 本文结束 感谢您的阅读 —————————
谢谢你请我喝咖啡ლↂ‿‿ↂლ(支付宝扫一扫即可领红包, 消费时可抵现! 你省钱, 我赚钱, 多谢支持~)