异步数据
之前爬取的网站基本上都是同步数据,每一页的内容都不需要再次加载,而是通过翻页获取更多数据;其实有很多网站显示在页面上的的数据并不是一次性从服务端获取的,如图像搜索网站,当滚动下拉条时,会随着滚动条向下移动,有更多的图片显示出来。这些图片就是通过异步的方不断从服务端获取的,这就是异步数据。
一、异步加载与AJAX
传统的网如果要更新动态的内容,必须重新加载整个网页,因为不管是动态内容,还是静态内容,都是通过服务端以同步的方式按顺序发送给客户端的。异步加载:也就是让静态部分先以同步加载的方式显示在页面上,然后动态部分在另外向服务端发送一个或多个异步请求,从服务端接受到数据后,再将数据显示在页面上。这种技术就是常说的AJAX,中文称为“异步JavaScript和XML”
AJAX有两层含义:一层含义是同步;另一层含义是指传输数据格式。
二、基本原理
AJAX实现分为3步:发送请求、解析响应和渲染页面
1、发送请求
在web端页面中实现业务逻辑与页面交互的是JavaScript语言,所以网页是用JavaScript语言发送请求的。
2、解析响应
这里响应的数据主要是JSON格式的数据。可以使用parse函数对JSON格式的数据进行解析
3、渲染页面
渲染页面主要是指将服务端获取的响应数据以某种形式显示在web页面的某些元素上。
三、逆向工程
在之前爬取的一些网站,都是打开开发者环境,然后切换到开发者工具的Network选项卡,然后选取All选项里的第一个数据,查看response就可以看到网页源代码。但是在异步加载中,并看不到所有的网页中显示的内容。
原因是:Response选项卡显示的HTML代码是在JavaScript渲染页面前,而Elements选项卡显示的HTML代码是在JavaScript渲染后的。
所以在这里,大家如果爬取数据时发现这个问题,那么这个网站很可能就是异步加载的方式。
那么现在我们如何获取数据呢?
我们需要打开开发者环境,然后切换到Network页面,然后选取fetch/XHR选项卡,就可以找到JavaScript写出的动态网页。
项目实战:爬取百度图片(https://image.baidu.com/)
#由于本项目是爬取异步数据,不涉及解析数据,只是对json格式的数据进行提取
import requests
#导入解析函数,可以将汉字进行解码,以及编码
from urllib import parse
#导入代理池
from UA_info import ua_list
import random
import os
import json
import time
class PhotoSpider:
os_path = os.getcwd()+'/百度图片/'
if not os.path.exists(os_path):
os.mkdir(os_path)
def __init__(self):
#计数器
self.i=1
self.headers = {
'User-Agent':random.choice(ua_list)
}
#这里只设置了一个值,其实可以设置很多值,如果有人感兴趣,可以在这里直接添加数据。
self.word = [
'原神高清壁纸'
]
def get_json(self):
#根据word的长度进行遍历,所以大家可以直接在列表里添加数据
for k in range(len(self.word)):
self.i = 1
for i in range(30,121,30):
file = self.os_path+f'/{self.word[k]}/'
if not os.path.exists(file):
os.mkdir(file)
#对汉字进行编码
search = parse.quote(self.word[k])
#这里使用了format函数,其实和前面的f差不多
url = 'https://image.baidu.com/search/acjson?tn=resultjson_com&logid=' \
'8364019978235621825&ipn=rj&ct=201326592&is=&fp=result&fr=&word={}' \
'&queryWord={}&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=0' \
'&hd=&latest=©right=&s=&se=&tab=&width=&height=&face=0&istype=2' \
'&qc=&nc=1&expermode=&nojc=&isAsync=&pn={}&rn=30&gsm=1e&1705133969553='.format(search,search,i)
response = requests.get(url,headers=self.headers)
if response.status_code == 200:
json_text = response.text
self.parse_json(json_text,file)
def parse_json(self,json_text,file):
#当我们获取了网页的json数据后,需要对其进行转换,否则不能用字典的方法进行获取值
json_data = json.loads(json_text)
data = json_data["data"]
for img_urls in data:
"""
在这里我遇到了一个问题,就是有的时候这个字典获取不到值,可能会直接报错
导致程序终止,所以在这里使用了try...except方法,如果出错了,程序不会终止
而是将出错的一段程序报错信息改成了except后面的内容。
"""
try:
img_obj = img_urls["replaceUrl"]
img_url = img_obj[0]["ObjURL"]
print(img_url)
img = requests.get(img_url).content
time.sleep(1)
self.save_data(img,file)
time.sleep(1)
print("图片下载成功!!!!!!!!!")
except Exception as massg:
print("抓取错误!!!!!!!!!!!")
def save_data(self,img,file):
with open(file+f'{self.i}'+'.jpg','wb') as f:
f.write(img)
self.i += 1
if __name__ == '__main__':
photo = PhotoSpider()
photo.get_json()
https://img.win3000.com/m00/b1/50/f8746f7aa12187baacdbf878c12d957c.jpg
图片下载成功!!!!!!!!!
https://i0.hdslb.com/bfs/archive/447373a8119d6e4019f904bc0d946953d5020535.jpg
图片下载成功!!!!!!!!!
https://i0.hdslb.com/bfs/article/66115781038f5620020264702b4974a9ea362b00.jpg
图片下载成功!!!!!!!!!
https://www.lk48.com/wp-content/uploads/2021/07/20210702110413-60def2adad160.jpg
抓取错误!!!!!!!!!!!
https://pica.zhimg.com/v2-1a3227100bca1a4d635bb3877914050a_r.jpg?source=1940ef5c
图片下载成功!!!!!!!!!
https://pic.netbian.com/uploads/allimg/220225/224226-16458001461d27.jpg
图片下载成功!!!!!!!!!
https://i1.hdslb.com/bfs/archive/1cfe783b7c066f610018e55c4479057117670669.jpg
图片下载成功!!!!!!!!!
https://file.moyublog.com/d/file/2021-08-05/2kln03evkmp.jpg
图片下载成功!!!!!!!!!
抓取错误!!!!!!!!!!!
https://img.win3000.com/m00/61/8b/d033fa91537c806e9a7b1601386284b6.jpg
图片下载成功!!!!!!!!!
可见即可爬:Selenium
到现在为止,我们已经爬了同步数据和异步数据,我个人认为异步数据抓取更加容易,不需要任何的解析语法。
但是,还有一类页面,并不是采用这两种方式获取的数据,例如,有一些页面的数据是通过JavaScript在前端得到的,要想找到规律,需要分析JavaScript代码。即使数据是通过AJAX方式获取的,但由于很多参数都是加密的,很难找到规律,采用前面的方式很难抓取数据,甚至不可能完成。
有一类IDE称为所见即所得IDE,也就是通过拖放控件的方式设计UI,设计是什么样的,运行就是什么样的。其实爬虫也可以用所见即所得的方式,也就是说,在浏览器中看到的是什么,抓取的就是什么。
首先我们需要安转驱动器WebDriver以及自动化脚本Selenium。
一、基本使用方法
Selenium的主要功能有如下几类:
- 打开浏览器
- 获取浏览器页面的特定内容
- 控制浏览器页面上的控件,如向一个文本框中输入一个字符串
- 关闭浏览器
二、定位节点
1、查找节点
Selenium 提供了 8 种查找单个节点的方法,如下所示:
方法 | 说明 |
---|---|
find_element_by_id() | 通过 id 属性值定位 |
find_element_by_name() | 通过 name 属性值定位 |
find_element_by_class_name() | 通过 class 属性值定位 |
find_element_by_tag_name() | 通过 tag 标签名定位 |
find_element_by_link_text() | 通过标签内文本定位,即精准定位。 |
find_element_by_partial_link_text() | 通过标签内部分文本定位,即模糊定位。 |
find_element_by_xpath() | 通过 xpath 表达式定位 |
find_element_by_css_selector() | 通过 css 选择器定位 |
2、节点交互
selenium优于前面介绍的其他分析框架的重要特性就是可以与节点进行交互,也就是模拟浏览器的动作。例如,单击页面上的某个按钮、在文本输入框中输入某个文本,都属于节点交互。
1) 鼠标事件
Selenium WebDriver 将关于鼠标的操作方法都封装在 ActionChains 类中,使用时需要引入 ActionChains 类,如下所示:
from selenium.webdriver.common.action_chains import ActionChains
该类包含了鼠标操作的常用方法:
方法 | 说明 |
---|---|
ActionChains(driver) | 构造 ActionChains 鼠标对象。 |
click() | 单击 |
click_and_hold(on_element=None) | 单击鼠标左键,不松开 |
context_click() | 右击 |
double_click() | 双击 |
drag_and_drop() | 拖动 |
move_to_element(above) | 执行鼠标悬停操作 |
context_click() | 用于模拟鼠标右键操作, 在调用时需要指定元素定位。 |
perform() | 将所有鼠标操作提交执行。 |
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
import time
browser = webdriver.Chrome()
try:
browser.get('https://www.jd.com')
actions = ActionChains(browser)
li_list = browser.find_elements(By.XPATH,'//li[@class="cate_menu_item"]')
for li in li_list:
actions.move_to_element(li).perform()
time.sleep(1)
except Exception as e:
print(e)
browser.close()
2) 键盘事件
Selenium WebDriver 的 Keys 模块提供了模拟键盘输入的 send_keys() 方法,除此之外,该模块也提供了操作键盘的其他方法,比如复制、粘贴等等。
from selenium.webdriver.common.keys import Keys
在使用之前,首先需要导入 Keys 类,如下所示:下面列举了一些常用方法:
方法 | 说明 |
---|---|
send_keys(Keys.BACK_SPACE) | 删除键(BackSpace) |
send_keys(Keys.SPACE) | 空格键(Space) |
send_keys(Keys.TAB) | 制表键(Tab) |
send_keys(Keys.ESCAPE) | 回退键(Esc) |
send_keys(Keys.ENTER) | 回车键(Enter) |
send_keys(Keys.CONTROL,'a') | 全选(Ctrl+A) |
send_keys(Keys.CONTROL,'c') | 复制(Ctrl+C) |
send_keys(Keys.CONTROL,'x') | 剪切(Ctrl+X) |
send_keys(Keys.CONTROL,'v') | 粘贴(Ctrl+V) |
send_keys(Keys.F1…Fn) | 键盘 F1…Fn |
keys.down(value,element=None) | 按下键盘上的某个键 |
keys.up(value,element=None) | 松开键盘上的某个键 |
3)执行JavaScript代码
对于某些操作,Selenium并没有提供相应的API,例如,下拉页面,不过可以使用Selenium的execute_script方法直接运行JavaScript代码,以便扩展Selenium的功能。
from selenium import webdriver
import time
browser = webdriver.Chrome()
browser.get('https://www.jd.com')
#将京东商城首页滚动到最底端
browser.execute_script('window.scrollTo(0,document.body.scrollHeight)')
#弹出对话框
browser.execute_async_script('alert("已到达最低端!!!!")')
time.sleep(4)
browser.close()
三、改变节点的属性值
本例会通过JavaScript代码改变百度搜索按钮的位置,让这个按钮在多个位置之间移动。
from selenium import webdriver
import time
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.baidu.com')
search_button = driver.find_element(By.ID,'su')
x_position = [10,40,80,100]
y_position = [10,50,160,400]
for i in range(len(x_position)):
js = '''
arguments[0].style.position = 'absolute';
arguments[0].style.left="{}px";
arguments[0].style.top="{}px";
'''.format(x_position[i],y_position[i])
driver.execute_script(js,search_button)
time.sleep(2)
项目实战:爬取京东美食(https://www.jd.com)
from selenium import webdriver # # 驱动浏览器
from selenium.webdriver.common.by import By #选择器
from selenium.webdriver.common.keys import Keys #按键
from selenium.webdriver.support.wait import WebDriverWait #等待页面加载完毕,寻找某些元素
from selenium.webdriver.support import expected_conditions as EC ##等待指定标签加载完毕
from selenium.common.exceptions import TimeoutException,NoSuchElementException
import time
from bs4 import BeautifulSoup
class Spider():
def __init__(self):
self.url = 'https://www.jd.com/'
#下面三行代码,是创建无头模式,可以发现打开的浏览器不会显示收到自动化控制
self.options = webdriver.ChromeOptions()
self.options.add_experimental_option('excludeSwitches',['enable-automation'])
self.browser = webdriver.Chrome(options=self.options)
def get_jd(self):
#获取地址
self.browser.get(self.url)
#等待事件
wait = WebDriverWait(self.browser,300)
#等待所有元素加载出,传入定位元组
wait.until(EC.presence_of_all_elements_located((By.ID,'key')))
text_input = self.browser.find_element(By.ID,'key')#寻找输入框
text_input.send_keys('美食')#输入关键字
text_input.send_keys(Keys.ENTER)#键盘事件,回车键
time.sleep(20)#因为京东需要登录,所以最好在20秒之内登录成功
def get_data(self):
#这里是滚动页面,将所有数据加载出来,因为有些网页是异步加载
self.browser.execute_script('window.scrollTo(0,document.body.scrollHeight)')
#在这里尽量等待一下,让页面加载出来,不然数据可能不好获取
time.sleep(5)
data = self.browser.page_source#查看源代码
self.parse_data(data)#查看源代码
def parse_data(self,data):
soup = BeautifulSoup(data,'lxml')
name = soup.select('.gl-i-wrap a em')
price = soup.select('.p-price strong i')
shop_name = soup.select('.gl-i-wrap .J_im_icon a')
for names,prices,shop_names in zip(name,price,shop_name):
n = names.get_text()
p = prices.get_text()
s = shop_names.get_text()
print(n)
print(p)
print(s)
print('--------------------')
if __name__ == '__main__':
s = Spider()
s.get_jd()
s.get_data()
孙尚香熟食腊味风干鸡肉即食湖北荆门特产美食速食预制菜下酒菜食品428g
49.00
孙尚香京东自营旗舰店
--------------------
植宴 新货开心果原味大颗粒500g罐装年货散装干果类孕妇坚果零食 (新货大颗粒)原色100g/罐
10.26
植宴食品饮料店
--------------------
品馋坊(PINCHANFANG)夏威夷果大颗粒奶香味袋装 干货坚果炒货每日坚果办公室休闲孕妇 大颗粒夏威夷果250g*2
17.80
品馋坊旗舰店
--------------------
海领冠麻辣爆头八爪鱼海鲜罐头即食熟食龙虾尾生蚝鱿鱼章鱼开罐零食小吃 试吃款【八爪鱼1罐】
11.76
鲜现水产旗舰店
--------------------