selenium爬虫入门

记录一下写的一个简单爬虫

简介

selenium是基于浏览器DevTool模拟用户态行为的爬虫框架,与传统爬虫相比selenium最大的优点就是可以通过模拟行为绕过大部分的反爬,同时因为其本身基于浏览器,在对抗加密方面有天然的优势

常用API

webDriver.Chrome.Options() : Option

返回一个 option 对象,用于在之后为浏览器提供选项参数,最常见的用法如下

1
2
options = webdriver.ChromeOptions()
options.add_argument("disable-blink-features=AutomationControlled")

这段代码为option添加了一个参数将浏览器的 webdriver 属性置为false,该属性常用于检测浏览器是否处于调试模式,常见的反爬会检测该属性,该属性为true则会触发反爬
此外还可以添加包括但不限于下列的属性,但用处都不大

1
2
3
--disable-gpu     # 禁用gpu加速
--headless # 将浏览器设置为无头浏览器
--windows-size=1920,1080 #设置浏览器窗口大小

webdriver.Chrome(options : Option ) : WebDriver

创建一个浏览器对象,所有与浏览器的交互都会通过这个对象进行

1
driver = webdriver.Chrome(options=options)

driver.get(url : str ) : void

打开一个页面

driver.find_element(by: str , value : str ) : WebElement

在webdriver目前聚焦的页面中寻找对应html元素,并返回一个可以对找到的元素进行交互的对象, byselenium.webdriver.common.by 中的枚举类, value 是搜索对应的值或表达式,这里只介绍 By.CSS_SELECTORBy.LINK_TEXT,个人认为算是最通用的方法。
这个方法只返回找到的第一个元素,且找不到则抛出错误,另一个类似的方法是 driver.find_elements 返回所有找到的元素,如果找不到就返回空列表

By.CSS_SELECTOR 利用CSS选择器查找元素,value 应为一个CSS选择器表达式
下面是一些示例

1
2
3
4
5
6
7
8
9
10
11
12
13
value = "div"   # 单独的名字表示筛选特定tag的元素
value = ".targetClass" # class名前带.表示筛选带特定类的元素
value = "#ID" # ID前带#表示筛选含特定id的元素
value = "[attribute]" # 筛选带具备特定属性的元素
value = '[attribute="value"]' # 筛选带特定属性且属性值为value的元素,其中""不可省略和替换
value = "div.targetClass#ID" # 选择含多个条件的元素则将所有条件不加空格地组合在一起

value = "div#fatherDiv div#sonDiv" # 如果要选择某个元素的后代元素则将两个元素的筛选条件以空格隔开

value = "div#faterDiv > div#directSonDiv" # 如果要选择直属于某个元素的子元素则在两个元素的筛选条件之间额外加入 >

value = ":not()" # 选择不符合特定条件的元素,()中可以嵌套另一个CSS选择器表达式
# 比如 "div:not(.disabled)" ,选择不为disable类的div元素

By.LINK_TEXT 筛选文本为 value 的<a>元素即超链接元素,执行严格匹配,另一个类似的方法是 By.PARTIAL_LINK_TEXT,只需提供的 value 是目标的子串就行,此方法用于快捷定位特定超链接对应的元素,方便爬虫在页面间进行跳转

1
Page = driver.find_element(by=By.LINK_TEXT, value="下一页") # 这样我们就定位到了一个翻页按钮,可以用click之类的方法让我们的爬虫翻页

ActionChains(driver : WebDriver ) : ActionChains

这是selenium提供的动作链类,用于模拟用户的操作输入

1
2
3
4
5
6
7
actions = ActionChains(driver)  # 指定所属的webdriver,还可以指定duration属性来控制每次操作的时间,用来绕过针对仿真行为的反爬
actions.key_down(Keys.CONTROL).click(
Page).key_up(Keys.CONTROL).perform()
# 这个操作就模拟了ctrl+左键,即在新页面打开对应链接
# 使用连续调用来设计动作链,最后调用perform()执行
# click之类的要指定具体点击的webElement,否则点击鼠标指针所在位置
# 按键类的动作可以用selenium.webdriver.common.keys中的Keys枚举类来模拟按下ctrl之类的功能键,其他按键直接输入对应字符即可

driver.switch_to.window(window_name : str ) : void

将浏览器的焦点切换至另一个页面,其中 window_name 可以通过 driver.window_handles 获取,这是webdriver类的一个列表成员,储存了当前浏览器所有窗口的 window_name

1
2
driver.switch_to.window(driver.window_handles[-1]) 
# 切换至最右边的窗口,事实上这个列表里句柄的排列顺序是按窗口打开顺序排列的

driver.close() : void

当一个页面的数据爬完了可能要关闭该页面,用close方法就能把当前页面关掉,但是注意driver并 不会自动切换聚焦到新窗口 ,所以close后要调用switch_to.window把焦点转移到新窗口

driver.quit() : void

直接关闭浏览器

项目实战

项目地址 => 基于selenium框架爬取avd.aliyun.com上的漏洞报告

阿里云的漏洞网站用了很恶心的加密算法,把本机的时间戳套了一堆加密做成token放进payload里做验证,如果验证不通过就无法进入漏洞库和漏洞报告,我们直接上selenium,只要我们全程模拟用户态行为就不用管加密,这点和逆向工程中的动态调试非常像,本质上是利用了应用自加密/自解密的性质

下面直接上代码

1
2
3
4
5
6
7
8
#utility.py
def init_driver(url: str):
options = webdriver.ChromeOptions()
options.add_argument("disable-blink-features=AutomationControlled")
driver = webdriver.Chrome(options=options)
driver.get(url)
return driver

他的加密算法会在加载时就读取 webdriver 标记的值,并且这个参数会被用于加密过程,如果是undefined或者true加密跑出来就是错的,而且非常恶心的是他并不直接从 navigator.webdriver 实时读取,而是加载时检测到 webdriver 不为false就让加密胡乱输出,所以我们加一个参数在网页加载前就把 webdriver 标记设置成false

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
#Crawler.py
def runCrawler(catalog: str, number: int = 0, debug: bool = 0, sleepSecond: float = 1):
'''
sleepSecond为每跳转一个页面的等待时长,过短可能会触发限流或反爬\n
如果cve的命名不符合windows文件命名规范则会转换为url命名输出\n
catalog为要爬取的漏洞库的按钮文本{"CVE 漏洞库","非CVE漏洞库","高危漏洞"}\n
number为爬取的记录数量,若不指定则在调用函数时提示输入
'''
driver = init_driver("https://avd.aliyun.com/")
Page = driver.find_element(by=By.LINK_TEXT, value=catalog)
if debug:
print("in "+driver.title+" at "+driver.current_url)
# input("continue")
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).click(
Page).key_up(Keys.CONTROL).perform() # 进入漏洞库列表
sleep(sleepSecond*1.5) # 等待加载
if debug:
print("sleep finished")

driver.switch_to.window(driver.window_handles[-1]) # 切换至漏洞库窗口

links = driver.find_elements(By.TAG_NAME, "a")
# print(len(links))
if debug:
# print(driver.window_handles)
print("in "+driver.title+" at "+driver.current_url)
# input("continue")
total = driver.find_element(
by=By.CSS_SELECTOR, value="div.py-3.bg-light > div.container.vuln-list-container > div.py-3 > div.d-flex.justify-content-between.align-items-center > span.text-muted").text
if debug:
print(total[total.find("总计 ")+3: total.find(" 条记录")])
total = int(total[total.find("总计 ")+3: total.find(" 条记录")])
print("正在爬取 {}".format(catalog))
toDo = number
if toDo == 0:
toDo = int(input("输入要爬取的记录条数,范围为 <={}\n".format(total)))
hasNextPage = 1
cnt = 0
while hasNextPage and cnt < toDo:
if debug:
print("in"+driver.current_url)
for link in links: # 遍历所有超链接
if "detail" in link.get_attribute("href"):
link.click() # 进入cve
sleep(sleepSecond)
driver.switch_to.window(
driver.window_handles[-1]) # 切换至新窗口
button = driver.find_elements(
by=By.CSS_SELECTOR, value=".btn.btn-link.text-muted")
if len(button):
button[0].click()
genRawDoc(driver, debug)
cnt += 1
print("进度 {} / {}".format(cnt, toDo))
if debug:
print("in "+driver.title+" at "+driver.current_url)
input("continue")
driver.close()
# input("closed")
driver.switch_to.window(driver.window_handles[-1])
if debug:
print("in "+driver.title+" at "+driver.current_url)
# input("continue")
if cnt == toDo:
break
nextPageButton = driver.find_elements(
by=By.CSS_SELECTOR, value=".px-3.btn.btn-sm.btn-outline-secondary.btn-bd-primary:not(.disabled)")
if debug:
print(len(nextPageButton))
hasNextPage = 0
for button in nextPageButton:
if "下一页" in button.text:
actions.key_down(Keys.CONTROL).click(
button).key_up(Keys.CONTROL).perform()
# driver.close()
sleep(sleepSecond)
driver.close()
driver.switch_to.window(driver.window_handles[-1])
hasNextPage = 1
break
driver.quit()

这段非常长,因为爬虫定位元素基本靠手工定位,有大量重复代码,我们看关键的部分

1
2
3
4
5
driver = init_driver("https://avd.aliyun.com/")
Page = driver.find_element(by=By.LINK_TEXT, value=catalog)
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).click(Page).key_up(Keys.CONTROL).perform() # 进入漏洞库列表
sleep(sleepSecond*1.5) # 等待加载


这里我们打开网站,然后网站首页有三个链接分别跳进不同分类的漏洞库,我们直接用文本定位然后在新页面打开,并且等待加载,这里不直接点击在原地跳转的原因是不知道为什么原地跳转会导致加密算法输出不正确,然后等待加载也很重要,如果不等页面完全加载完就跳转也会让加密算法输出不正确,尚不清楚原因

1
2
driver.switch_to.window(driver.window_handles[-1])  # 切换至漏洞库窗
links = driver.find_elements(By.TAG_NAME, "a")

这段就是把这个页面里所有的超链接元素都抓下来

1
2
3
total = driver.find_element(
by=By.CSS_SELECTOR, value="div.py-3.bg-light > div.container.vuln-list-container > div.py-3 > div.d-flex.justify-content-between.align-items-center > span.text-muted").text
total = int(total[total.find("总计 ")+3: total.find(" 条记录")])

这段是找下总共有多少条记录,不是很重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while hasNextPage and cnt < toDo:
#....
nextPageButton = driver.find_elements(
by=By.CSS_SELECTOR, value=".px-3.btn.btn-sm.btn-outline-secondary.btn-bd-primary:not(.disabled)")
if debug:
print(len(nextPageButton))
hasNextPage = 0
for button in nextPageButton:
if "下一页" in button.text:
actions.key_down(Keys.CONTROL).click(
button).key_up(Keys.CONTROL).perform()
# driver.close()
sleep(sleepSecond)
driver.close() #关闭当前页
driver.switch_to.window(driver.window_handles[-1]) #前往下一页
hasNextPage = 1
break

因为记录有很多页,我们要一页一页爬,先找到翻页按钮,然后发现能按和不能按的按钮有一个.disable类的区别,我们就一直翻直到”下一页”变成disable为止
然后每次点开一个新的就把老的关了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for link in links:  # 遍历所有超链接
if "detail" in link.get_attribute("href"):
link.click() # 进入cve
sleep(sleepSecond)
driver.switch_to.window(
driver.window_handles[-1]) # 切换至新窗口
button = driver.find_elements(
by=By.CSS_SELECTOR, value=".btn.btn-link.text-muted")
if len(button):
button[0].click()
genRawDoc(driver, debug)
cnt += 1
print("进度 {} / {}".format(cnt, toDo))
if debug:
print("in "+driver.title+" at "+driver.current_url)
input("continue")
driver.close()
# input("closed")
driver.switch_to.window(driver.window_handles[-1])
if debug:
print("in "+driver.title+" at "+driver.current_url)
# input("continue")
if cnt == toDo:
break


页面里有一堆超链接,我们肯定不能每个都跳进去,发现漏洞详情的超链接里有detail关键字,我们就只找含这个关键字的跳,也是点开,转移焦点,爬数据,关闭,把焦点切回来,同样注意每次等待一会,可以防止访问量过大导致限流或者直接被封ip
然后跳进去后发现参考链接如果数量过多会只显示部分,要先点一下 .btn.btn-link.text-muted 的按钮把所有东西都展开
genRawDoc里的内容就没什么营养了,目前已经进入具体报告界面,直接定位对应内容,保存下来写入文件就行

Author

SGSG

Posted on

2025-03-03

Updated on

2025-04-18

Licensed under