By Mejias
背景:
应团队的PMP的要求,为自己的Team开发了一个内部网站信息抓取的工具(整体代码展示见文章末尾,可能稍微有点长)。上上周周写完测试后推给了大家,没有什么问题。今天Team的一个小伙伴突然告诉我报错,显示是Chrome Driver与Chrome版本不对,搜索Chrome://version,才发现是Chrome自动升级了。这样原来版本的Chrome driver就不支持了,导致程序报错。
在Chrome Driver官网here重新搜索了版本匹配的Chrome Driver并下载安装好之后,再次运行程序,发现前面都运行的很好直到后面首尾部分报错如下:
报错显示的是下面这里出了问题:
def control_in_shadow(driver,js):
shadow = driver.execute_script(js)
return shadow #返回的对象在这里
js = 'return document.querySelector("#ra-shadow-root").shadowRoot'
shadow= control_in_shadow(driver,js)
shadow.find_element(By.ID,'ra-asin-list-count-input').clear()
shadow.find_element(By.ID,'ra-asin-list-count-input').send_keys('1000')
shadow.find_element(By.ID,'ra-asin-list-load-btn').click()
上面的代码访问下列Shadow Doml里的元素。
采用的方法就是常说的三步法:
定位到Shadow Dom的Host节点 =》 使用.shadowRoot属性定位到根节点 =》
直接通过页面Element的方法访问Shadow Dom内部的元素。这种方法在未更新chrome driver 的版本之前一直用的很好的。
但是在更新了Chrome版本和chrome driver版本之后就会报错了。原来的方法在新的chrome driver并不适用。于是在收到小伙伴的反馈后,就需要测试代码问题以及修改和Refine了。
测试&发现问题:
首先根据上面的报错可以定位到是下面的代码出了问题。
def control_in_shadow(driver,js):
shadow = driver.execute_script(js)
return shadow #返回的对象在这里
js = 'return document.querySelector("#ra-shadow-root").shadowRoot'
shadow= control_in_shadow(driver,js)
shadow.find_element(By.ID,'ra-asin-list-count-input').clear()
shadow.find_element(By.ID,'ra-asin-list-count-input').send_keys('1000')
shadow.find_element(By.ID,'ra-asin-list-load-btn').click()
因为.execute_script(js)是driver对象自带的方法,这一段执行JS语句也没有报错,所以肯定不是定义的新方法的问题。而且看代码报错是说函数返回的对象是一个dict,而不是原来应该是的remote controlled web element元素,导致web element的查找元素的方法.find_element(By.)使用报错。所以我这里定位到这个返回对象“shadow”是否问题。
为了测试它是否只是一个dict还是说代表了shadowRoot这个节点,我们可以使用shadowRoot的一些属性和方法去测试他是否是一个shadowRoot。
shadowDom的shadowRoot有许多的属性,例如:
shadowRoot.host(返回根节点的宿主节点的引用);
shadowRoot.innerHTML(返回对shadowRoot内的DOM树的引用)
shadowRoot.mode(返回shadowRoot的模式 -open或 -closed)
测试代码如下:
def control_in_shadow(driver,js):
shadow = driver.execute_script(js)
return shadow #返回的对象在这里
js = 'return document.querySelector("#ra-shadow-root").shadowRoot'
shadow= control_in_shadow(driver,js)
shadow.host
shadow.mode
shadow.innerHTML
测试结果如下:
可以看出返回的shadow对象已经不是一个shadowRoot元素,所以也不能使用他的一些属性,包括上述代码的.find_element(By.Id)方法了。
解决bug的尝试:
这里我们可以看到直接用driver.execute_script()返回值不能再进行页面的一些操作了,但是在代码里运行js语言依然没有问题,所以我们想到的解决办法就是直接书写JS语言在Python代码中运行。比如上述的几个属性可以通过下面的代码得到返回值。
def control_in_shadow(driver,js):
shadow = driver.execute_script(js)
return shadow #返回的对象在这里
js = 'return document.querySelector("#ra-shadow-root").shadowRoot'
shadow= control_in_shadow(driver,js)
js1 = 'return document.querySelector("#ra-shadow-root").shadowRoot.host'
js2 = 'return document.querySelector("#ra-shadow-root").shadowRoot.mode'
js3 = 'return document.querySelector("#ra-shadow-root").shadowRoot.innerHTML'
host_res = control_in_shadow(driver,js1)
mode_res = control_in_shadow(driver,js2)
inner_HTML_res = control_in_shadow(driver,js3)
print(host_res)
print(mode_res)
print(inner_HTML_res)
解决测试的运行结果:
可以看出通过直接运行JS可以成功的找到所有的属性返回值,以及访问到宿主节点的IP为一个web element对象。
代码修复:
根据以上的探索,我们可以知道可以通过直接在Python中运行JS语句实现成功的访问shadow Dom里面的元素。基于此我们可以把原始代码修改如下(原始代码不需要的行已经注释起来了):
def control_in_shadow(driver,js):
shadow = driver.execute_script(js)
return shadow #返回的对象在这里
js1 = 'return document.querySelector("#ra-shadow-root").shadowRoot.getElementById("ra-asin-list-count-input")'
#shadow= control_in_shadow(driver,js)
input_id = control_in_shadow(driver,js1)
input_id.clear()
input_id.send_keys('1000')
#shadow.find_element(By.ID,'ra-asin-list-count-input').clear()
#shadow.find_element(By.ID,'ra-asin-list-count-input').send_keys('1000')
#shadow.find_element(By.ID,'ra-asin-list-load-btn').click()
js2 = 'document.querySelector("#ra-shadow-root").shadowRoot.getElementById("ra-asin-list-load-btn").click()'
load_id = control_in_shadow(driver,js2)
load_id
js3 = 'return document.querySelector("#ra-shadow-root").shadowRoot.getElementById("ra-asin-list-csv-btn")'
save_id = control_in_shadow(driver,js3)
上面的代码运行起来就没有问题了。而且这是通过直接操作JS语句的,速度上也是可以的。
初始代码展示:
以下为整段代码(可能稍微有点长)。后续有机会可以和大家分享下面代码的编写的逻辑。
import pandas as pd
import numpy as np
import os
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import selenium.common.exceptions
import requests
import time
import json
import mitmproxy
import pyautogui
def open_chrome_driver(option):
driver = webdriver.Chrome(options = option)
return driver
def open_new_window(driver,url):
new = 'window.open(%s)'%url
driver.execute_script(new)
handles = driver.window_handles
driver.switch_to.window(handles[-1])
return driver
def open_url(driver,url):
driver.get(url)
def wait_by_clickable(driver,wait_time,ID):
wait = WebDriverWait(driver,wait_time)
wait_name = wait.until(EC.element_to_be_clickable(driver.find_element(By.ID,ID)))
return wait_name
def wait_by_presence(driver,wait_time,ID):
wait = WebDriverWait(driver,wait_time)
wait_name = wait.until(EC.presence_of_element_located((By.ID,ID)))
return wait_name
def keyboard_perform(driver,ID):
logo = driver.find_element(By.ID,ID)
rightClick = ActionChains(driver)
rightClick.context_click(logo).perform()
time.sleep(4)
pyautogui.typewrite(['down','down','down','down','down','down','down','down','down'])
time.sleep(4)
pyautogui.typewrite(['enter'])
time.sleep(5)
pyautogui.typewrite(['enter'])
time.sleep(5)
def control_in_shadow(driver,js):
shadow = driver.execute_script(js)
return shadow
def find_key_words(file_name):
pecos = pd.read_excel(file_name)
key_words = list(pecos.loc[:,'title'])
print(key_words)
return key_words
def find_index(list,element):
for i in range (0,len(list)):
if list[i] == element:
return i
else:
pass
def concatenate_file(path):
os.chdir(r'%s./Peco_File_Download'%path)
filelist = []
list_link=[]
filelist2 = []
key_content_list = []
for root, dirs,files in os.walk(".",topdown = False):
for name in files:
str = os.path.join(root,name)
if str.split('.')[-1] == 'csv':
filelist.append(str)
for each_file in filelist:
name = each_file.split('\\')[1]
filelist2.append(name)
key = name.split('-ASIN')[0]
key_content_list.append(key)
for each_range in range(len(filelist2)):
list_count = pd.read_csv(r'%s'%filelist2[each_range])
list_link.append(list_count)
for each_file in filelist:
inx = find_index(filelist,each_file)
current_list = list_link[inx]
current_list['key_words'] = key_content_list[inx]
df1 = pd.concat(list_link,ignore_index = True)
df2 = df1[['asin','key_words']]
df2.to_csv('合并后的表格.csv')
def find_one_B_one(keywords,final_sleep_time):
#open_existed_chrome_option
option = webdriver.ChromeOptions()
option.add_experimental_option("debuggerAddress","127.0.0.1:9999")
driver = open_chrome_driver(option)
for i in keywords:
url = "https://www.amazon.com/"
open_url(driver,url)
#search&submit keywords
search_box = wait_by_clickable(driver,1200,'twotabsearchtextbox')
search_box
search_box.send_keys(i)
submit_button = wait_by_clickable(driver,1200,'nav-search-submit-button')
submit_button
submit_button.click()
time.sleep(0.01)
#retail_assistance
keyboard_perform(driver,'nav-search-submit-button')
shadow_root = wait_by_presence(driver,1200,'ra-shadow-root')
shadow_root
time.sleep(4)
js = 'return document.querySelector("#ra-shadow-root").shadowRoot'
shadow= control_in_shadow(driver,js)
shadow.find_element(By.ID,'ra-asin-list-count-input').clear()
shadow.find_element(By.ID,'ra-asin-list-count-input').send_keys('1000')
shadow.find_element(By.ID,'ra-asin-list-load-btn').click()
time.sleep(final_sleep_time)
wait = WebDriverWait(driver,4000)
tag = shadow.find_element(By.ID,'ra-asin-list-csv-btn')
save_button = wait.until(EC.element_to_be_clickable(tag))
save_button
time.sleep(4)
save_button.click()
time.sleep(4)
if __name__ == "__main__":
path = os.getcwd()
wait_sleep_time = int(input('请输入您需要等待pecos load的时长/second:'))
keywords = find_key_words('PECO Keywords.xlsx')
find_one_B_one(keywords,wait_sleep_time)
#concatenate_file(path)