网上很多版本的爬取京东图书都失效了
现在这个版本是能运行的截至到编辑的日期的前后(往后不敢保证)
gitee仓库网址:https://gitee.com/cc2436686/jd_book_spider (有详细注释和思考过程)
下面就来看看吧
首先看看我们要爬取的页面
https://book.jd.com/booksort.html
然后用request直接请求在对返回结果进行关键字匹配
好了接下来就转向目标去按f12抓包去
可以看出这个接口就包含了我们想要的内容接下来只要伪造请求获取数据就行了
直接请求这个接口
那就研究研究这个接口
https://pjapi.jd.com/book/sort?source=bookSort&callback=jsonp_1606557102964_82922
经过测试可以的出除了数字以外其他都是固定参数
而且中间一大串数字一看就是时间戳
我们可以用python来模拟
import requests
import time
#因为要爬取的项目是从这个https://book.jd.com/booksort.html主页面开始的,
# 因为大分类的小说和小分类的中国当代小说包括表示其种类的id都是通过请求接口来渲染
# 所以直接通过请求页面是获取不到相应的数据的所以只能破解下面这个接口
# https://pjapi.jd.com/book/sort?source=bookSort&callback=jsonp_1606487589792_60024
# 直接通过浏览器控制台中的network就能抓到这个包
# 经过测试source后面的参数是不变的
# callback后面的参数是jsonp_加时间戳_随机的四到五位数字
# 下面就用python自带的time软件生成时间戳来进行请求
# 再加上referer参数https://book.jd.com/ 伪装成官网跳转过来就ok了
t=int(time.time()* 1000)
url = "https://pjapi.jd.com/book/sort?source=bookSort"
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36' ,
'callback':'jsonp_'+str(t)+'_60005',
'referer':'https://book.jd.com/'
}
response2 = requests.get(url, headers=headers)
print(response2.request.headers)
print(response2.content.decode())
ok接口数据获取成功接下来就去提取页面中的url
从上面两张图片可以看出分类链接的地址是由大分类小说的fathercategoryid和大分类小说的categoryid和子节点的categoryid拼接而成的
也就是说我们可以直接通过这个接口来构造所有的请求
class BookSpider (scrapy.Spider):
name = 'book'
# 修改允许的域
allowed_domains = ['jd.com']
# 修改起始的url
start_urls = ['https://pjapi.jd.com/book/sort?source=bookSort']
# 起始的京东所有图书分类是通过ajax动态加载的所以在页面上搜寻不到所以只能通过
# 请求接口来获取所有的图书分类信息和对应的编号用于下面的请求拼接
# 此请求接口直接输入接口地址或者去用页面当时加载的页面接口参数去请求是会被认为
# 是非法请求如果想要知道如何请求接口请看JD下面的text.py去看相应的伪装过程
# 当然也可以去pipelines.py里去看当然没text.py里详细
def parse(self, response):
# print(response.body.decode())#如果请求成功返回就是json格式用json模块解析
big_dict = json.loads (response.body.decode ())
for big_node in big_dict['data']: # 对返回内容中的data进行遍历
for small_node in big_node['sonList']:
item = JdItem ()
item['big_category'] = big_node['categoryName'] # 获取data中大分类的名字赋值给item
# 因为大分类的链接点进去之后可以看出是https://channel.jd.com/1713-3258.html格式的可以看出
# 是由大分类中的fatherCategoryId: 1713和大分类本身的id拼接而成因为我爬的是图书所以大分类中的
# 的fatherCategoryId一直不会变所以图省事直接1713写死了后面再加上大分类本身的id就完成了
item['big_category_link'] = 'https://channel.jd.com/1713-' + str (
int (big_node['categoryId'])) + '.html'
item['small_category'] = small_node['categoryName']
# 再找到大分类中的小分类的名字赋值给small_category
item['small_category_link'] = "https://list.jd.com/list.html?cat=1713," + str (
int (big_node['categoryId'])) + "," + str (int (small_node['categoryId'])) + '&page=1'
# 小分类的链接就是大分类的father的id加上小分类的id再加上图书本身的id就行了
# 如果不加&page=1拼接之后https://list.jd.com/list.html?cat=1713,3258,3297&page=1请求的默认也是第一页
# 因为下面的翻页需要这里还是添加上page=1这个参数
# https://list.jd.com/list.html?cat=1713,3258,3297&page=1 小分类的完整链接(样例)
# 再为所有的小分类链接创建请求并且跳转到parse_detail这个解析函数再用meta将详细item项目传输过去
yield scrapy.Request (
url=item['small_category_link'],
callback=self.parse_detail,
meta={'item': item}
)
构造好所有的小分类的请求url后接下来就是爬取对应的详情页面的图书信息了
我们要爬取的目标
import scrapy
class JdItem(scrapy.Item):
#爬虫要抓取的内容如下
big_category = scrapy.Field()#图书大分类的名字
big_category_link = scrapy.Field()#图书大分类的链接(好像没啥用0.0)
small_category = scrapy.Field()#图书小分类的名字
small_category_link = scrapy.Field()#图书小分类的链接
bookname=scrapy.Field()#图书的名字
author=scrapy.Field()#作者的名字(有些图书可能没有作者默认就是None)
link =scrapy.Field()#图书的详情页的链接
price=scrapy.Field()#图书的价格
pass
这些基本的信息提取就不细讲了后面有代码要解决的就是翻页问题因为你查看翻页的按钮信息会发现
又是通过js动态加载的
本着能用scrapy就不用selenuim的原则
继续分析
可以看到前面onclick时间中的参数却透露出信息了
通过翻页前后
第一页
https://list.jd.com/list.html?cat=1713,3258,3297
或者
https://list.jd.com/list.html?cat=1713,3258,3297&page=1
第二页
https://list.jd.com/list.html?cat=1713%2C3258%2C3297&page=3&s=57&click=0
的对比可以发现
page参数是翻页的关键其他代码可以忽略
也就是说只要修改page的信息就可以完成
page是以+=2递增来翻页的
所以我们可以不去提取翻页url(当然也提取不到)直接构造翻页请求
但是如何判断是否是最后一页呢?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(我没注意到这个页面图书信息的加载是懒加载
又快马加鞭地修改。。。。。。。。。。。。。。。。。。。。。
好了本人学识浅薄只能用selenuim作为中间件完成了图书的懒加载
中途又遇到频繁请求被重定向的问题导致分支被断
我试了试判断是否被重定向再吃重复发起请求也解决了这个问题
我单独放了一个小分类进去成功完成了一个小分支的所有图书的全部抓取)
好了当无事发生继续分析
当我门翻页后有且只有30个标签也就是说只有30个图书信息但是当我们往下拉的时候
触发了懒加载才能继续加载数据
数量刚好也就是60个所以我们可以使用selenuim作为中间件来触发懒加载
再判断li的个数如果为0就是被重定向了,因为最后一页不可能一本书都没
逻辑如下
if len (books_list) == 0:
print ("貌似被重定向")
print("重新发起请求")
yield scrapy.Request (
url=response.url,
callback=self.parse_detail,
dont_filter=True,
meta={'item': response.meta['item'],
'dont_redirect': True})
if len (books_list) == 60:
# https://list.jd.com/list.html?cat=1713,3258,3297&page=1 #例子url就是这样下面就是对url中的page进行累加操作最后再拼接
list = re.split ("&", response.url)
# 通过&分隔url
str1 = str (int (re.findall ('\d+', list[1])[0]) + 2)
# 提取第二个page中的数字再将其加2后转为字符串
next_url = list[0] + '&page=' + str1
# 拼接url
print ("拼接下一页url", next_url)
# 为url创建request请求将meta中的item对象传入
# (如果不传入item对象翻页后会因为response取不到item对象而报错而不会去执行下面的代码)
yield scrapy.Request (
url=next_url,
callback=self.parse_detail,
dont_filter=True,
meta={'item': response.meta['item'],
'dont_redirect': True}
)
因为我是临时加入的selenuim所以只是单纯地作为中间件来使用
大家可能在翻页的时候会遇到
https://list.jd.com/list.html?cat=1713%2C3258%2C3297&page=135&s=4021&click=0
类似的url看到这个cat后面的参数再看看之前cat后面的参数就知道被加密了
百度搜索url解密直接丢进去就能看到
https://list.jd.com/list.html?cat=1713,3258,3297&page=135&s=4021&click=0
跟我们之前id拼接一模一样所以不必担心
直接传我们通过id拼接的url即可
接下来就展示自己的一些关键代码如果想要全部的项目就去gitee仓库那边去拷贝
gitee仓库网址:https://gitee.com/cc2436686/jd_book_spider (有详细注释和思考过程)
爬虫核心代码book
import time
import scrapy
import json
from JD.items import JdItem
import re
class BookSpider (scrapy.Spider):
name = 'book'
# 修改允许的域
allowed_domains = ['jd.com']
# 修改起始的url
start_urls = ['https://pjapi.jd.com/book/sort?source=bookSort']
# 起始的京东所有图书分类是通过ajax动态加载的所以在页面上搜寻不到所以只能通过
# 请求接口来获取所有的图书分类信息和对应的编号用于下面的请求拼接
# 此请求接口直接输入接口地址或者去用页面当时加载的页面接口参数去请求是会被认为
# 是非法请求如果想要知道如何请求接口请看JD下面的text.py去看相应的伪装过程
# 当然也可以去pipelines.py里去看当然没text.py里详细
def parse(self, response):
# print(response.body.decode())#如果请求成功返回就是json格式用json模块解析
big_dict = json.loads (response.body.decode ())
for big_node in big_dict['data']: # 对返回内容中的data进行遍历
for small_node in big_node['sonList']:
item = JdItem ()
item['big_category'] = big_node['categoryName'] # 获取data中大分类的名字赋值给item
# 因为大分类的链接点进去之后可以看出是https://channel.jd.com/1713-3258.html格式的可以看出
# 是由大分类中的fatherCategoryId: 1713和大分类本身的id拼接而成因为我爬的是图书所以大分类中的
# 的fatherCategoryId一直不会变所以图省事直接1713写死了后面再加上大分类本身的id就完成了
item['big_category_link'] = 'https://channel.jd.com/1713-' + str (
int (big_node['categoryId'])) + '.html'
item['small_category'] = small_node['categoryName']
# 再找到大分类中的小分类的名字赋值给small_category
item['small_category_link'] = "https://list.jd.com/list.html?cat=1713," + str (
int (big_node['categoryId'])) + "," + str (int (small_node['categoryId'])) + '&page=1'
# 小分类的链接就是大分类的father的id加上小分类的id再加上图书本身的id就行了
# 如果不加&page=1拼接之后https://list.jd.com/list.html?cat=1713,3258,3297&page=1请求的默认也是第一页
# 因为下面的翻页需要这里还是添加上page=1这个参数
# https://list.jd.com/list.html?cat=1713,3258,3297&page=1 小分类的完整链接(样例)
# 再为所有的小分类链接创建请求并且跳转到parse_detail这个解析函数再用meta将详细item项目传输过去
yield scrapy.Request (
# url=item['small_category_link'],
url=item['small_category_link'],
callback=self.parse_detail,
meta={'item': item,
'dont_redirect': True
}
)
def parse_detail(self, response):
# 接收到parse传过来的item信息
# 里面包含着大小分类的名字和链接
item = response.meta['item']
books_list = response.xpath ("//*[@id='J_goodsList']/ul/li")
# 查看小分类链接里所有的图书信息都是由ul下面的li标签排列而成的所以就先获取所有的li标签
# 通过查看页面后得知翻页是没有对应的url是通过js动态加载的
# 但是通过对于翻页前后网址的对比可以看出从翻页操作是通过page+=2完成的
# 也就是说只要改变page的值就能实现翻页效果不需要提取url(也没有url可以提取)
# 下面就是判断当前页面中的图书也就是目标li的个数是不是60个
# 泪崩。。图书是懒加载的得通过selenuim作为中间件渲染页面
# 如果是尾页的话就不满30个就会跳过这个判断不去创建请求
if len (books_list) == 0:
print ("貌似被重定向")
time.sleep(3)
print("3秒后重新发起请求")
yield scrapy.Request (
url=response.url,
callback=self.parse_detail,
dont_filter=True,
meta={'item': response.meta['item'],
'dont_redirect': True})
if len (books_list) == 60:
# https://list.jd.com/list.html?cat=1713,3258,3297&page=1 #例子url就是这样下面就是对url中的page进行累加操作最后再拼接
list = re.split ("&", response.url)
# 通过&分隔url
str1 = str (int (re.findall ('\d+', list[1])[0]) + 2)
# 提取第二个page中的数字再将其加2后转为字符串
next_url = list[0] + '&page=' + str1
# 拼接url
print ("拼接下一页url", next_url)
# 为url创建request请求将meta中的item对象传入
# (如果不传入item对象翻页后会因为response取不到item对象而报错而不会去执行下面的代码)
yield scrapy.Request (
url=next_url,
callback=self.parse_detail,
dont_filter=True,
meta={'item': response.meta['item'],
'dont_redirect': True}
)
for book in books_list:
# 下面就是开心的获取信息
item['small_category_link'] = response.url
# 这里要重置一下小分类链接因为翻页的缘故所以page的信息有所改变
item['link'] = response.urljoin (book.xpath ("./div/div[@class='p-img']/a/@href").extract_first ())
# 提取到每个图书的详情页链接
item['bookname'] = book.xpath ("./div/div[@class='p-name']/a/em/text()").extract_first ()
# 提取到每个图书的书名
item['author'] = book.xpath (
"./div/div[@class='p-bookdetails']/span[@class='p-bi-name']/a/text()").extract_first ()
# 提取到每个图书的作者
item['price'] = book.xpath ("./div/div[@class='p-price']/strong/i/text()").extract_first ()
# 提取到每个图书的价格(跟之前比起来价格不是通过接口请求的而是直接加载到页面的省了一些功夫)
yield item
# 返回图书的信息
下载中间件
from scrapy.http import HtmlResponse
from selenium import webdriver
import time
class JdSpiderMiddleware:
pass
class JdDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
def __init__(self):
opt = webdriver.ChromeOptions ()
# opt.add_argument ('--headless') # 如果想看到具体的翻页情况(就注释)可以打开浏览器看(蛮有趣的)
# 建议用有界面的比较好因为可以看到运行情况
opt.add_argument ('user-agent="Mozilla/5.0 (iPod; U; CPU iPhone OS 2_1 like Mac OS X; ja-jp) '
'AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5F137 Safari/525.20"')
opt.add_argument ('https://book.jd.com/booksort.html')
self.driver = webdriver.Chrome (options=opt)
# 其他都不重要只用看下面这个方法就够了
def process_request(self, request, spider):
if 'page' in request.url:
# 直接跳转小分类链接是会出错的必须要加上referer模拟从https://book.jd.com/booksort.html页面点击过来
self.driver.get (request.url)
self.driver.execute_script ('window.scrollTo(0,document.body.scrollHeight)')
time.sleep (2)
html = self.driver.page_source
self.preurl = request.url
return HtmlResponse (url=request.url, body=html.encode ())
if "sort?source=bookSort" in request.url:
# 判断请求的是不是请求大分类和小分类图书的详细信息的接口网址
# 如果是的话就模拟时间戳凭借url
print ("请求接口数据")
t = int (time.time () * 1000)
request.headers['callback'] = 'jsonp_{0}_2175'.format (t)
# 这里需要添加从https://book.jd.com/的referer信息
request.headers['referer'] = 'https://book.jd.com/'
return None
def close(self, spider):
self.driver.quit ()
管道(一定要输入自己的mongodb数据库的ip和端口,还有登陆验证才行)
from pymongo import MongoClient
class JdPipeline:
def __init__(self):
self.number=0
# 创建数据库链接对象
#只要创建好mongodb的客户端和登录操作就可以直接插入数据了
client = MongoClient('您的mongodb数据库地址',27017)#输入mongodb的ip地址和端口号
self.db = client['admin']# 选择admin数据库
self.db.authenticate('您的用户名','您的密码')#进行用户验证
def process_item(self, item, spider):
Item=dict(item)
self.db.book.insert(Item)#往book集合里插入数据
self.number+=1
# print(Item)#书的详细信息
print('第',self.number,'本书存入成功')
return item
setting(只是配置了下载中间件和管道)
BOT_NAME = 'JD'
SPIDER_MODULES = ['JD.spiders']
NEWSPIDER_MODULE = 'JD.spiders'
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
LOG_LEVEL = "WARNING"
#如果想看详细信息可以吧LOG_LEVEL改成debug
ROBOTSTXT_OBEY = False
REDIRECT_ENABLED = False
DOWNLOADER_MIDDLEWARES = {
'JD.middlewares.JdDownloaderMiddleware': 543,
}
ITEM_PIPELINES = {
'JD.pipelines.JdPipeline': 300,
}
items文件
import scrapy
class JdItem(scrapy.Item):
#爬虫要抓取的内容如下
big_category = scrapy.Field()#图书大分类的名字
big_category_link = scrapy.Field()#图书大分类的链接(好像没啥用0.0)
small_category = scrapy.Field()#图书小分类的名字
small_category_link = scrapy.Field()#图书小分类的链接
bookname=scrapy.Field()#图书的名字
author=scrapy.Field()#作者的名字(有些图书可能没有作者默认就是None)
link =scrapy.Field()#图书的详情页的链接
price=scrapy.Field()#图书的价格
pass