前言

最近比较痴迷于爬虫,于是乎在B站上找了相关视频进行学习,毕竟都是免费的课,有的比较老,在跟着视频实战爬虫的时候和视频中的结果不一样了,我就知道这个网站的反爬又升级了,于是就开始了我的探索之路。

坑点1——分类页面js动态加载数据

京东图书,这是要爬取的分类页面


根据视频,爬取图书分类的界面原本都是静态页面,但是现在已经变成了js动态加载数据,而且还不是ajax请求, 抓包分析是加载的js文件,如下图所示


本来还以为找不到接口地址了,准备使用Splash或者Selenium,在写博客的时候又尝试了以下,可在js文件中找到了。
接口地址:https://pjapi.jd.com/book/sort?source=bookSort,不过还需要在请求头中添加User-AgentReferer,不然进不去,完整测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
from fake_useragent import UserAgent

headers = {
"User-agent": UserAgent().random,
"Referer": "https://book.jd.com/",
}

r = requests.get(
"https://pjapi.jd.com/book/sort?source=bookSort", headers=headers)
for category in r.json()['data']:
for son in category['sonList']:
print(son)
# 打印结果(内容太多只显示部分)
{'categoryId': 3297.0, 'categoryName': '中国当代小说', 'fatherCategoryId': 3258.0}
{'categoryId': 3298.0, 'categoryName': '中国近现代小说', 'fatherCategoryId': 3258.0}
{'categoryId': 3299.0, 'categoryName': '中国古典小说', 'fatherCategoryId': 3258.0}
{'categoryId': 3300.0, 'categoryName': '名著', 'fatherCategoryId': 3258.0}
{'categoryId': 3301.0, 'categoryName': '港澳台小说', 'fatherCategoryId': 3258.0}
{'categoryId': 3302.0, 'categoryName': '穿越/重生/架空', 'fatherCategoryId': 3258.0}
{'categoryId': 3303.0, 'categoryName': '外国小说', 'fatherCategoryId': 3258.0}
{'categoryId': 3304.0, 'categoryName': '侦探/推理', 'fatherCategoryId': 3258.0}
{'categoryId': 3305.0, 'categoryName': '悬疑/惊悚', 'fatherCategoryId': 3258.0}

坑点2——图书页面数据加载

在分类页面搞定之后,就该进入具体的某个分类取爬取图书信息了。

要点1

首先需要注意的是分类页面的链接和最终跳转的并不是同一个url,中间可能经过了重定向,如图。


原来的链接是https://list.jd.com/1713-3258-3298.html


页面跳转后的链接是https://list.jd.com/list.html?cat=1713,3258,3298,这三个数字有不同的涵义:

  • 1713:根据分类接口返回的json数据分析得出是所有大分类的父分类ID,经过观察,在图书分类页面全部都是1713开头
  • 3258:就像小说、文学、青春文学这种大分类ID
  • 3298:小分类ID,每个大分类下均有好多小分类,小说大分类下就包括中国当代小说、中国近代小说等

下图的分类页面返回的json数据

如果要使用splash或者selenium的话,这步分析并没有什么卵用。因为我打算抓包分析接口

要点2

它这个图书列表的数据很阁僚,第一次打开页面会显示30条图书信息,根据网页源代码分析,这些数据是静态加载的。

但是, 要命的是当你往下滚动页面时,它会自动发送一条ajax请求,重新加载30条数据,此时页面一共有60条数据。
每次发送request请求,只能获得30条数据,拿不到完整的60条数据(前30条数据时直接渲染的,后30条数据是动态加载的),就让人跟头疼。

于是我又开始抓包,如图:

接口地址是:https://list.jd.com/listNew.php?cat=1713%2C3258%2C3298&page=8&s=211&scrolling=y&log_id=1658042504010.1290&tpl=2_M&isList=1&show_items=13635778,12855282,13297946,13107548,10040352351581,12547051,11795657,12197693,12376589,10041030594381,10053150510702,13023202,10047004563879,10026590939837,12940777,12551391,13165490,13105314,70720165683,11936435,12926730,12772617,12393668,13167967,12695906,12369479,10041882507727,13412484,13308258,12778511
它返回的直接是html部分页面,而不是json数据。经过尝试,上面的很多参数都没用,最有用的信息是:https://list.jd.com/listNew.php?cat=1713,3258,3298&page=8,参数说明如下:

  • cat:就是刚才分析的大分类ID,小分类ID之类
  • page:没什么好说的就是页数

不过经过我多次测试,这个接口每页返回30条数据,当浏览器第一次跳转到这个页面是page=1,浏览器滚动到底下时page=2,也就是说,咱们再浏览器正常访问一页,实际时访问这个接口两页的数据。
不过编写代码时还是需要添加请求头信息User-agentReferer,不然就被反爬跳转到登录页面了。

代码如下:

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
import requests
from fake_useragent import UserAgent
from lxml import etree
payload = {
'cat': '1713,13634,13635',
'page': 1,
}
# 请求的cat 参数要和请求头中的Referer中cat 参数一致,不然会被反爬
r = requests.get(
"https://list.jd.com/listNew.php",
params=payload,
headers={
"User-agent": UserAgent().random,
"Referer": "https://list.jd.com/list.html?cat=1713,13634,13635",
})
content = r.text
# print(content)


tree = etree.HTML(content)
result = tree.xpath("//div[@id='J_goodsList']/ul[@class='gl-warp clearfix']/li[@class='gl-item']")
# print(len(result))
for book in result:
img = "http:" + book.xpath(".//div[@class='p-img']/a/@href")[0]
price = book.xpath(".//div[@class='p-price']/strong/i/text()")[0]
book_name = book.xpath(".//div[@class='p-name']/a/em/text()")[0]
author = book.xpath(".//div[contains(@class,'bookdetails')]")[0].xpath("string(.)").replace("\t", "").replace("\n", "").strip()
print("{},{},{},{}".format(book_name, author, price, img))

坑点3——图书列表中的蜜罐陷阱

正当我洋洋得意以为搞定的时候,把代码改造成scrapy项目重新运行时,每次都报错,通过xpath语法,某个元素获取不到。是因为如果直接使用lxml中的xpath,获取不到元素会返回空字符串;而在scrapy中,获取不到元素就会报错。

那么为什么会这样呢?经过我在图书列表页面仔细分析,发现源代码中有的图书信息不会被展示出来,而且还缺少某一子元素导致代码报异常。


经过我的仔细分析,发现li标签中的数据如果出现ware-type="0",那么它就是假数据,浏览器并不会显示它,这个假数据和正常数据的子元素是有些不同的,如缺少class="p-bookdetails"div
下图是假数据(不会被浏览器显示):

下图是真数据:

图书列表页代码:

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
import requests
from fake_useragent import UserAgent
from lxml import etree
payload = {
'cat': '1713,13634,13635',
'page': 1,
}
r = requests.get(
"https://list.jd.com/listNew.php",
params=payload,
headers={
"User-agent": UserAgent().random,
"Referer": "https://list.jd.com/list.html?cat=1713,13634,13635",
})
content = r.text
# print(content)

tree = etree.HTML(content)
result = tree.xpath("//div[@id='J_goodsList']/ul[@class='gl-warp clearfix']/li[@class='gl-item']")
# print(len(result))
for book in result:
if book.get_attribute("ware-type") != "0": # 判断是否为假数据
img = "http:" + book.xpath(".//div[@class='p-img']/a/@href")[0]
price = book.xpath(".//div[@class='p-price']/strong/i/text()")[0]
book_name = book.xpath(".//div[@class='p-name']/a/em/text()")[0]
author = book.xpath(".//div[contains(@class,'bookdetails')]")[0].xpath("string(.)").replace("\t", "").replace("\n", "").strip()
print("{},{},{},{}".format(book_name, author, price, img))

至此,终于把所有坑都填了。

使用scrapy实现完整功能

尽情期待。。。

__END__

三国小梦
文章作者:三国小梦
文章出处京东图书反爬与反反爬
作者签名:简单地活着, 肆意又精彩.
关于主题Hexo - Live For Code
版权声明:文章除特别声明外,均采用 BY-NC-SA 许可协议,转载请注明出处