爬虫教程( 5 ) --- Selenium、PhantomJS、selenium反检测、cdp ( ichrome )、Playwright、DrissionPage、helium

1、Selenium

中式读法:【 瑟林捏幕 】

官方文档:https://selenium-python.readthedocs.io/
中文文档:https://selenium-python-zh.readthedocs.io/en/latest/

Selenium( selenium 中文网:http://www.selenium.org.cn/ )是一个强大的网络数据采集工具,最初是为了网站自动化测试而开发的,被用来测试 Web 应用程序在不同的浏览器和操作系统上运行能力。

浏览器插件:Selenium IDE

简  介

Selenium 是自动化测试工具,通过 Selenium 可以实现使用代码来操作浏览器实现自动化。例如:关键字搜索,点击按钮等等操作。它支持各种浏览器,包括 Chrome,Safari,Firefox 等主流界面式浏览器,如果你在这些浏览器里面安装一个 Selenium 的插件,那么便可以方便地实现Web界面的测试。换句话说 Selenium 可以自动化操作这些浏览器,但是必须下载并设置不同的浏览器驱动。

selenium 是一套完整的web应用程序测试系统,包含了测试的录制(selenium IDE),编写及运行(Selenium Remote Control)和测试的并行处理(Selenium Grid)。Selenium的核心Selenium Core基于JsUnit,完全由JavaScript编写,因此用于任何支持JavaScript的浏览器上。

selenium 可以模拟真实浏览器,自动化测试工具,支持多种浏览器,爬虫中主要用来解决JavaScript渲染问题。

Selenium 支持多种语言开发,比如 Java,C,Ruby、Python 等。用python写爬虫的时候,主要用的是selenium的Webdriver,通过下面代码查看 Selenium.Webdriver 支持哪些浏览器。首先导入 webdriver 模块。然后使用 help 函数

from selenium import webdriver
help(webdriver)

安装、使用

首先安装 Selenium:pip install selenium

安装 浏览器 驱动 webdriver
        1. chromedriver 下载地址:http://chromedriver.chromium.org
            chromedriver 镜像地址 :CNPM Binaries Mirror
        2. Firefox 的驱动 geckodriver 下载地址:Releases · mozilla/geckodriver · GitHub
        3. IE 驱动 下载地址:NuGet Gallery | Selenium.WebDriver.IEDriver 4.8.1

注意:下载解压后,将chromedriver.exe , geckodriver.exe , Iedriver.exe 放到 Python 的安装目录,例如 D:\python 。 然后再将 Python 的安装目录添加到系统环境变量的 Path

爬虫 Selenium Chromium 与 Chromedriver对应版本( 注意是 chromium,不是 Chrome ):
淘宝镜像地址在每个文件夹的 notes.txt 中存有 chromium 和 Chromedriver 的版本对应。

from selenium import webdriver

browser = webdriver.Chrome()
# browser = webdriver.Firefox()
# browser = webdriver.Ie()

browser.get('https://www.baidu.com/')
__input = input("暂停, 按任意键继续")

运行这段代码,会自动打开浏览器,然后访问百度。

如果程序执行错误,浏览器没有打开,那么应该是没有装 Chrome 浏览器或者 Chrome 驱动没有配置在环境变量里。下载驱动,然后将驱动文件路径配置在环境变量即可。

模拟提交

下面的代码实现了“模拟百度提交搜索”的功能。首先等页面加载完成,然后输入关键字到搜索框文本,最后点击提交。

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get("https://www.baidu.com")
print(browser.title)
elem = browser.find_element(By.NAME, value="wd")  # 百度首页搜索框 name="wd"
elem.send_keys("MM")         # 输入关键字
elem.send_keys(Keys.RETURN)  # 模拟 点击 enter键,提交
print(browser.page_source)   # 打印 js 渲染后的网页
__input = input("暂停, 按enter键退出")

其中 driver.get 方法会打开请求的 URL,WebDriver 会等待页面完全加载完成之后才会返回,即程序会等待页面的所有内容加载完成,JS渲染完毕之后才继续往下执行。注意:如果这里用到了特别多的 Ajax 的话,程序可能不知道是否已经完全加载完毕。

WebDriver 提供了许多寻找网页元素的方法,通过 By 类的方法。

然后,输入文本,模拟点击回车。利用 Keys 这个类来模拟键盘输入。就像敲击键盘一样。

注意:获取网页渲染后的源代码。输出 page_source 属性即可。

通过 dir(browser) 查看,browser 对象有那些方法, 

测试用例

import unittest
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By


class PythonOrgSearch(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Chrome()

    def test_search_in_python_org(self):
        driver = self.driver
        driver.get("https://www.python.org")
        self.assertIn("Python", driver.title)
        elem = driver.find_element(By.NAME, value="q")
        elem.send_keys("pycon")
        elem.send_keys(Keys.RETURN)
        assert "No results found." not in driver.page_source

    def tearDown(self):
        self.driver.close()


if __name__ == "__main__":
    unittest.main()

测试用例是继承了 unittest.TestCase 类,继承这个类表明这是一个测试类。setUp方法是初始化的方法,这个方法会在每个测试类中自动调用。每一个测试方法命名都有规范,必须以 test 开头,会自动执行。最后的 tearDown 方法会在每一个测试方法结束之后调用。这相当于最后的析构方法。在这个方法里写的是 close 方法,你还可以写 quit 方法。不过 close 方法相当于关闭了这个 TAB 选项卡,然而 quit 是退出了整个浏览器。当你只开启了一个 TAB 选项卡的时候,关闭的时候也会将整个浏览器关闭。

import time
from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.maximize_window()  # 最大化浏览器
browser.implicitly_wait(20)  # 设置隐式时间等待
url = 'https://www.baidu.com'
browser.get(url)

# 网页的登录按钮
btn_login = browser.find_element(By.ID, value='s-top-loginbtn')
btn_login.click()  # 点击登录按钮
time.sleep(2)  # 显式等待 2s

# 查找所有的 a 标签
all_link = browser.find_element(By.TAG_NAME, value='a')
link = all_link[3]  # 提取 第四个 a 标签
link.click()  # 点击 提取 的 a 标签
input('暂停,按 enter 退出...')

页面操作

访问页面  

运行后自动打开Chrome浏览器,并登陆百度打印百度首页的源代码,然后关闭浏览器

import time
from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get("https://www.baidu.com")
print(browser.page_source)
browser.close()

执行JavaScript

这是一个非常有用的方法,这里就可以直接调用js方法来实现一些操作,下面的例子是通过登录知乎然后通过js翻到页面底部,并弹框提示

from selenium import webdriver

browser = webdriver.Chrome()
browser.get("https://www.zhihu.com/explore")
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
browser.execute_script('alert("To Bottom")')
input('暂停,按 enter 退出')
browser.close()

查找 ( 定位 ) 元素

Selenium常见元素定位方法和操作的学习介绍[python爬虫] Selenium常见元素定位方法和操作的学习介绍_selenium元素定位_Eastmount的博客-CSDN博客

Selenium切换窗口句柄及调用Chrome浏览器:[python爬虫] Selenium切换窗口句柄及调用Chrome浏览器_selenium 窗口句柄_Eastmount的博客-CSDN博客

查找单个元素

import time
from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get("https://www.taobao.com")
input_first = browser.find_element(By.ID, "q")
print(input_first)
input_second = browser.find_element(By.CSS_SELECTOR, "#q")
print(input_second)
input_third = browser.find_element(By.XPATH, '//*[@id="q"]')
print(input_third)
browser.close()

这里通过三种不同的方式获取元素。结果都是相同的。

查找元素的方法

Selenium 提供了8种定位方式:id、name、class name、tag name、link text、partial link text、xpath、css selector。

By 类的一些属性如下

ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"

通过 By 模块 定位元素

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()

browser.get("https://www.taobao.com")
input_first = browser.find_element(By.ID, "q")
print(input_first)
browser.close()

多个元素查找

  • 单个元素是 find_element,
  • 多个元素是 find_elements,结果是获得一个元素列表

示例:lis = browser.find_elements(By.CSS_SELECTOR,'.service-bd li')

页面交互 ( 点击、输入 )

玩转python selenium鼠标键盘操作(ActionChains):https://www.jb51.net/article/92682.htm
Selenium鼠标与键盘事件常用操作方法示例:https://www.jb51.net/article/145502.htm

找到页面元素后,就需要和页面交互,比如:点击,输入等等。

需要导入包:from selenium.webdriver.common.keys import Keys

ele.send_keys("some text") # 文本框输入
ele.send_keys("and some", Keys.ARROW_DOWN)
ele.clear() # 清除 文本框 内容

Selenium 所有的 api 文档:http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.action_chains

交互动作 --- 动作链 (拖动、移动等)

将动作附加到动作链中串行执行

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains


browser = webdriver.Chrome()

url = "https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable"
browser.get(url)
browser.switch_to.frame('iframeResult')

source = browser.find_element(By.CSS_SELECTOR, '#draggable')
target = browser.find_element(By.CSS_SELECTOR, '#droppable')

actions = ActionChains(browser)
actions.drag_and_drop(source, target)
actions.perform()

actions.drag_and_drop_by_offset(source, 400, 0).perform()
input('暂停,按 enter 退出')
browser.close()

更多操作参考:http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.action_chains

元素拖拽

要完成元素的拖拽,首先你需要指定被拖动的元素和拖动目标元素,然后利用 ActionChains 类来实现

element = driver.find_element_by_name("source")
target = driver.find_element_by_name("target")

from selenium.webdriver import ActionChains
action_chains = ActionChains(driver)
action_chains.drag_and_drop(element, target).perform()
actions.drag_and_drop_by_offset(element, 400, 0).perform()

这样就实现了元素从 source 拖动到 target 的操作

chrome 浏览器打开标签页的快捷键是 ctrl+t,那把 ctrl+t 的按键事件传入即可

from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains

browser = webdriver.Chrome()
browser.maximize_window()
url = 'https://www.baidu.com'
browser.get(url)

ac = ActionChains(browser)
ac.key_down(Keys.CONTROL).key_down('t').key_up(Keys.CONTROL).key_up('t').perform()
input("暂停,按 enter 退出...")
browser.close()

import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

browser = webdriver.Chrome() # 默认的火狐浏览器
for i in range(5):
# 这句代码相当于在浏览器窗口下按下ctrl+t打开一个新的标签页
browser.find_element(By.TAG_NAME, 'body').send_keys(Keys.CONTROL + 't')
time.sleep(10) # 等待所有窗口完全打开,10秒够用了, 如果不打开得不到所有句柄,只能得到部分。
handles = browser.window_handles
print(len(handles))
print(handles)
input('暂停,按 enter 退出')
browser.close()

通常要确保页面加载完成,可以使用 selenium.webdriver.support.ui.WebDriverWait 

打开新窗口示例代码:

from selenium import webdriver
# 打开谷歌浏览器
browser = webdriver.Chrome()
 
# 打开窗口
browser.get("https://www.baidu.com/")
# 打开新窗口
newwindow_js = 'window.open("https://www.baidu.com");'  # js 
browser.execute_script(newwindow_js)                    # 执行 js 打开新的页面 
 
# 切换到新的窗口
handles = browser.window_handles        # 得到 所有页面  句柄
browser.switch_to_window(handles[-1])   # 切换焦点到 对应页面

获取元素属性

get_attribute('class')

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
url = 'https://www.zhihu.com/explore'
browser.get(url)
logo = browser.find_element(By.ID, 'zh-top-link-logo')
print(logo)
print(logo.get_attribute('class'))
browser.close()

获取 文本值、ID、位置、标签名

id、location、tag_name、size

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
url = 'https://www.zhihu.com/explore'
browser.get(url)
tag_input = browser.find_element(By.CLASS_NAME, 'zu-top-add-question')
print(tag_input.text)
print(tag_input.id)
print(tag_input.location)
print(tag_input.tag_name)
print(tag_input.size)

下拉 选项卡

下拉选项卡的的处理

tag_select = browser.find_element(By.XPATH, "//select[@name='name']")
all_options = tag_select.find_elements(By.TAG_NAME, "option")
for option in all_options:
    print(f'Value is: {option.get_attribute("value")}')
    option.click()

首先获取了第一个 select 元素,也就是下拉选项卡。然后轮流设置了 select 选项卡中的每一个 option 选项。你可以看到,这并不是一个非常有效的方法

其实 WebDriver 中提供了一个叫 Select 的方法,可以帮助我们完成这些事情。

from selenium.webdriver.support.ui import Select
select = Select(driver.find_element_by_name('name'))
select.select_by_index(index)
select.select_by_visible_text("text")
select.select_by_value(value)

如你所见,它可以根据索引来选择,可以根据值来选择,可以根据文字来选择。是十分方便的。

全部取消选择怎么办呢?很简单
select = Select(driver.find_element_by_id('id'))
select.deselect_all()
这样便可以取消所有的选择。
另外我们还可以通过下面的方法获取所有的已选选项。
select = Select(driver.find_element_by_xpath("xpath"))
all_selected_options = select.all_selected_options
获取所有可选选项是
options = select.options
如果你把表单都填好了,最后肯定要提交表单对吧。怎吗提交呢?很简单
driver.find_element_by_id("submit").click()
这样就相当于模拟点击了 submit 按钮,做到表单提交。
当然你也可以单独提交某个元素
element.submit()
方法,WebDriver 会在表单中寻找它所在的表单,如果发现这个元素并没有被表单所包围,那么程序会抛出 NoSuchElementException 的异常。

Frame

在很多网页中都是有Frame标签,所以我们爬取数据的时候就涉及到切入到frame中以及切出来的问题,通过下面的例子演示
这里常用的是 switch_to.from() 和 switch_to.parent_frame()

import time
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

browser = webdriver.Chrome()
url = 'http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
browser.get(url)
browser.switch_to.frame('iframeResult')
source = browser.find_element_by_css_selector('#draggable')
print(source)
try:
    logo = browser.find_element_by_class_name('logo')
except NoSuchElementException:
    print('NO LOGO')
browser.switch_to.parent_frame()
logo = browser.find_element_by_class_name('logo')
print(logo)
print(logo.text)

页面切换 和 打开新窗口

多窗口之间切换 :https://blog.csdn.net/u011541946/article/details/70132672
打开新窗口,多窗口切换:https://blog.csdn.net/DongGeGe214/article/details/52169761
打开新窗口并实现窗口切换: https://blog.csdn.net/zwq912318834/article/details/79206953

定位以及切换frame(iframe):https://blog.csdn.net/huilan_same/article/details/52200586

关键字:selenium iframe 切换

如果在一个页面上点击一个链接之后,并不是在当前页面上打开,而是重新打开一个新页面;这种情况下如何跳转到新的页面上操作?首先,需要了解的是每个窗口都有句柄的,可以理解为浏览器窗口的唯一标识符,根据这个标识符来确定新打开的窗口。打开新页面后,selenium 的 focus 还是在 原来的页面上,所以需要使用 switch_to.window 方法把 焦点(focus) 切换到新页面上

如果是新打开的 iframe 就使用 switch_to_frame('xxx')
如果是新打开的 tab 就使用 switch_to_window('')

一个浏览器肯定会有很多窗口,所以我们肯定要有方法来实现窗口的切换。切换窗口的方法如下

driver.switch_to_window("windowName")

switch_to_window 方法现在已经废弃,鼠标放在这个方法上提示 使用 switch_to.window 代替

另外你可以使用 window_handles 方法来获取每个窗口的操作对象。例如

for handle in driver.window_handles:
    driver.switch_to_window(handle)

另外切换 frame 的方法如下

driver.switch_to_frame("frameName.0.child")

这样焦点会切换到一个 name 为 child 的 frame 上。

打开新窗口主要使用 JavaScript 实现:

# 新标签页打开这个url
js="window.open("url")"
driver.execute_script(js)
time.sleep(2)

选项卡 管理

通过执行 js 命令实现新开选项卡 window.open()
不同的选项卡是存在列表里 browser.window_handles
通过 browser.window_handles[0] 就可以操作第一个选项卡

import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get('https://www.baidu.com')
browser.execute_script('window.open()')
print(browser.window_handles)
browser.switch_to.window(browser.window_handles[1])
browser.get('https://www.taobao.com')
time.sleep(1)
browser.switch_to.window(browser.window_handles[0])
browser.get('https://python.org')
input('暂停, 按enter退出...')
browser.close()

弹窗处理

页面出现了弹窗

alert = driver.switch_to_alert()

通过上述方法可以获取弹窗对象。

  

历史记录 ( 前进、后退 )

前进 和 后退 针对的是 浏览器浏览的网页 的 历史记录

driver.forward()
driver.back()

Cookies 处理

get_cookies()
delete_all_cookes()
add_cookie()

from selenium import webdriver

browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
print(browser.get_cookies())
browser.add_cookie({'name': 'name', 'domain': 'www.zhihu.com', 'value': 'zhaofan'})
print(browser.get_cookies())
browser.delete_all_cookies()
print(browser.get_cookies())

为页面添加 Cookies,用法如下

# Go to the correct domain
driver.get("http://www.example.com")

# Now set the cookie. This one's valid for the entire domain
cookie = {‘name’ : ‘foo’, ‘value’ : ‘bar’}
driver.add_cookie(cookie)

获取页面 Cookies,用法如下

# Go to the correct domain
driver.get("http://www.example.com")

# And now output all the available cookies for the current URL
driver.get_cookies()

以上便是 Cookies 的处理,同样是非常简单的。

页面 ( 隐式、显示) 等待

Python selenium 三种等待方式详解(必会):https://www.jb51.net/article/92672.htm

        这是非常重要的一部分,现在的网页越来越多采用了 Ajax 技术,这样程序便不能确定何时某个元素完全加载出来了。这会让元素定位困难而且会提高产生 ElementNotVisibleException 的概率。所以 Selenium 提供了两种等待方式,

  • 隐式等待:等待特定的时间。如果 WebDriver没有在 DOM中找到元素,将继续等待,超出设定时间后则抛出找不到元素的异常。默认的时间是0
  • 显式等待:首先指定一个等待条件,并且再指定一个最长等待时间,然后在这个时间段内进行判断是否满足等待条件,如果成立就会立即返回,如果不成立,就会一直等待,直到等待你指定的最长等待时间,如果还是不满足,就会抛出异常,如果满足了就会正常返回
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("http://somedomain/url_that_delays_loading")
try:
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "myDynamicElement"))
    )
finally:
    driver.quit()

程序默认会 500ms 调用一次来查看元素是否已经生成,如果本来元素就是存在的,那么会立即返回。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


browser = webdriver.Chrome()
browser.get('https://www.taobao.com/')

wait = WebDriverWait(browser, 10)
tag_input = wait.until(EC.presence_of_element_located((By.ID, 'q')))
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.btn-search')))
print(input, button)

上述的例子中的条件:EC.presence_of_element_located()是确认元素是否已经出现了
EC.element_to_be_clickable()是确认元素是否是可点击的

下面是一些内置的等待条件,你可以直接调用这些条件,而不用自己写某些等待条件了。

常用的判断条件:
title_is                    标题是某内容
title_contains          标题包含某内容
presence_of_element_located      元素加载出,传入定位元组,如(By.ID, 'p')
visibility_of_element_located       元素可见,传入定位元组
visibility_of                                 可见,传入元素对象
presence_of_all_elements_located            所有元素加载出
text_to_be_present_in_element                某个元素文本包含某文字
text_to_be_present_in_element_value      某个元素值包含某文字
frame_to_be_available_and_switch_to_it frame      加载并切换
invisibility_of_element_located                              元素不可见
element_to_be_clickable                                       元素可点击
staleness_of                        判断一个元素是否仍在DOM,可判断页面是否已经刷新
element_to_be_selected       元素可选择,传元素对象
element_located_to_be_selected 元素可选择,传入定位元组
element_selection_state_to_be 传入元素对象以及状态,相等返回True,否则返回False
element_located_selection_state_to_be 传入定位元组以及状态,相等返回True,否则返回False
alert_is_present 是否出现Alert

更多操作参考:http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.support.expected_conditions

隐式等待

隐式等待比较简单,就是简单地设置一个等待时间,单位为秒。到了一定的时间发现元素还没有加载,则继续等待我们指定的时间,如果超过了我们指定的时间还没有加载就会抛出异常,如果没有需要等待的时候就已经加载完毕就会立即执行

from selenium import webdriver

driver = webdriver.Chrome()
driver.implicitly_wait(10) # seconds
driver.get("http://somedomain/url_that_delays_loading")
myDynamicElement = driver.find_element_by_id("myDynamicElement")

当然如果不设置,默认等待时间为0。

from selenium import webdriver

browser = webdriver.Chrome()
browser.implicitly_wait(10)
browser.get('https://www.zhihu.com/explore')
input = browser.find_element_by_class_name('zu-top-add-question')
print(input)

异常处理

这里的异常比较复杂,官网的参考地址:
http://selenium-python.readthedocs.io/api.html#module-selenium.common.exceptions
这里只进行简单的演示,查找一个不存在的元素

from selenium import webdriver
from selenium.common.exceptions import TimeoutException, NoSuchElementException

browser = webdriver.Chrome()
try:
    browser.get('https://www.baidu.com')
except TimeoutException:
    print('Time Out')
try:
    browser.find_element_by_id('hello')
except NoSuchElementException:
    print('No Element')
finally:
    browser.close()

2、PhantomJS

中式读法:【 饭特姆JS 】

************************* PhantomJS 已经停止更新 *************************

PhantomJS(官网: http://phantomjs.org/ )是一个基于 WebKit 内核、无 UI 界面的浏览器,WebKit 是一个开源的浏览器引擎。比如,主流的 Safari、Google Chrome、傲游3、猎豹浏览器、百度浏览器、opera浏览器 都是基于 Webkit 开发。)

PhantomJS 会把网站数据加载到内存中,并执行页面上的 JavaScript,但不会向用户展示图形界面。

PhantomJS 是一个无界面的,可脚本编程的WebKit浏览器引擎。它原生支持多种web 标准:DOM 操作,CSS选择器,JSON,Canvas 以及SVG。

官方网站:http://phantomjs.org/download.html

Examples:http://phantomjs.org/examples/index.html

安装完成之后命令行输入:phantomjs -v

如果正常显示版本号,那么证明安装成功了。如果提示错误,那么请重新安装。

3、pyppeteer

pyppeteer API Reference:https://miyakogi.github.io/pyppeteer/reference.html
pyppeteer github 地址:https://github.com/miyakogi/pyppeteer
pyppeteer 英文文档地址:https://miyakogi.github.io/pyppeteer/
puppeteer 快速入门:https://blog.csdn.net/freeking101/article/details/91542887
pyppeteer 进阶技巧:https://www.cnblogs.com/dyfblog/p/10887940.html
爬虫、获取cookie、截屏插件、防爬绕过:https://mohen.blog.csdn.net/article/details/107312709
爬虫神器 Pyppeteer 的使用:https://blog.csdn.net/weixin_38819889/article/details/108684254

Pyppeteer  这个项目是非官方的,是基于谷歌官方puppeteer的python版本。chrome 就问题多多,puppeteer也是各种坑,加上pyppeteer是前两者的python版本,也就是产生了只要前两个有一个有bug,那么pyppeteer就会原封不动的继承下来,本来这没什么,但是现在遇到的问题就是 pyppeteer 这个项目已经停止更新,导致很多 bug 根本没人修复。所以,Pyppeteer 已经停止更新,可以使用 playwright-python 代替。

4、selenium 反检测

使用 Selenium 对网页进行爬虫时,如果不做任何处理直接进行爬取,会导致很多特征是暴露的,对一些做了反爬的网站,做了特征检测,用来阻止一些恶意爬虫

直接爬取

目标对象:aHR0cHM6Ly9xaWthbi5jcXZpcC5jb20vUWlrYW4vU2VhcmNoL0FkdmFuY2U=

使用 Selenium 直接爬取目标页面

import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service

chrome_options = Options()

s = Service(r"chromedriver.exe路径")

driver = webdriver.Chrome(service=s, options=chrome_options)

driver.get(url='URL')

driver.save_screenshot('result.png')

# 保存
source = driver.page_source
with open('result.html', 'w') as f:
    f.write(source)

time.sleep(200)

页面明显做了反爬,网页返回直接返回空白内容

利用 Chrome DevTools 协议

CDP 全称为 Chrome Devtools-Protocol:https://chromedevtools.github.io/devtools-protocol/

通过执行 CDP 命令,可以在网页加载前运行一段代码,进而改变浏览器的指纹特征
比如,window.navigator.webdriver 在 Selenium 直接打开网页时返回结果为 true;而手动打开网页时,该对象值为 undefined,因此,我们可以利用 CDP 命令修改该对象的值,达到隐藏指纹特征的目的

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import time

chrome_options = Options()

s = Service(r"chromedriver.exe路径")

driver = webdriver.Chrome(service=s, options=chrome_options)

# 执行cdp命令,修改(window.navigator.webdriver )对象的值
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
            Object.defineProperty(navigator, 'webdriver', {
              get: () => undefined
            })
            """
})

driver.get(url='URL')

driver.save_screenshot('result.png')

# 保存
source = driver.page_source
with open('result.html', 'w', encoding='utf-8') as f:
    f.write(source)

time.sleep(200)

需要指出的是,浏览器的指纹特征很多,使用该方法存在一些局限性。

在 系统环境变量 PATH 里将 chrome的路径 添加进去。

打开cmd,在命令行中输入命令:chrome.exe --remote-debugging-port=9999 --user-data-dir="C:\selenum\AutomationProfile"

对于-remote-debugging-port 值,可以指定任何打开的端口。

对于-user-data-dir 标记,指定创建新 Chrome 配置文件的目录。它是为了确保在单独的配置文件中启动 chrome,不会污染你的默认配置文件。

执行完命令后,会打开一个浏览器页面,我们输入淘宝网址(https://login.taobao.com/member/login.jhtml),输入用户名和密码,登录淘宝后用户信息就保存在 --user-data-dir="C:\selenum\AutomationProfile" 所指定的文件夹中。

执行 js window.open() 打不开窗口时,是因为 chrome 默认不允许弹出窗口,改下 chrome 设置就可以了
在 chrome 浏览器地址栏输入:chrome://settings/content/popups,把 已阻止(推荐)  改成 允许 即可。
或者 chrome -》设置 -》高级 -》隐私设置和安全性 -》网站设置 -》弹出式窗口和重定向,也可以设置。

不要关闭上面浏览器,然后执行 python 代码。在淘宝搜索 "电脑" 关键字,并打印前 5 页 所有 搜索内容

import os
import time
import random
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
 
# from selenium.webdriver.common.action_chains import ActionChains
 
 
def main():        
    # os.system(r'C:\Users\Administrator\AppData\Local\Google\Chrome\Application/chrome.exe --remote-debugging-port=9999 --user-data-dir="C:\selenum\AutomationProfile"')
    chrome_debug_port = 9999
    chrome_options = Options()
    # chrome_options.add_argument('--headless')
    chrome_options.add_experimental_option("debuggerAddress", f"127.0.0.1:{chrome_debug_port}")
 
    browser = webdriver.Chrome(chrome_options=chrome_options)
    wait = WebDriverWait(browser, 5)
    print(browser.title)
 
    # 当前句柄
    current_handle = browser.current_window_handle
 
    # browser.execute_script('window.open("https://login.taobao.com/member/login.jhtml")')
    browser.execute_script('window.open("http://www.baidu.com")')
 
    # 所有句柄
    all_handle = browser.window_handles
    second_handle = all_handle[-1]
 
    # 切回first
    browser.switch_to.window(current_handle)
 
    url = 'https://s.taobao.com/search?q=电脑'
    browser.get(url)
 
    produce_info_xpath = '//div[contains(@class, "J_MouserOnverReq")]//div[@class="row row-2 title"]/a'
    produce_info = browser.find_elements_by_xpath(produce_info_xpath)
    for produce in produce_info:
        print(produce.text.replace(' ', ''))
 
    # 这里是演示,所以只爬了前 5 页
    for page_num in range(2, 6):
        next_page_xpath = '//li[@class="item next"]'
        next_page = browser.find_element_by_xpath(next_page_xpath)
        next_page_enable = False if 'disabled' in next_page.get_attribute('class') else True
        if next_page_enable:
            print('*' * 100)
            print(f'第 {page_num} 页')
            next_page.click()
            # browser.refresh()
            produce_info_xpath = '//div[contains(@class, "J_MouserOnverReq")]//div[@class="row row-2 title"]/a'
 
            wait.until(EC.presence_of_all_elements_located((By.XPATH, produce_info_xpath)))
            time.sleep(random.randint(3, 5))
            produce_info = browser.find_elements_by_xpath(produce_info_xpath)
            for produce in produce_info:
                print(produce.text.replace(' ', ''))
        else:
            break
 
 
if __name__ == '__main__':
    main()
 

代码 2(根据关键字搜索,然后抓取 店铺名,店铺地址,店铺电话,):

# -*- coding: utf-8 -*-
 
 
import time
import random
import parsel
import re
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
 
 
# from selenium.webdriver.common.action_chains import ActionChains
 
class TaoBaoSearch(object):
    def __init__(self):
        super(TaoBaoSearch, self).__init__()
        self.browser = None
        self.wait = None
        self.master_handler = None
        self.slaver_handler = None
        self.temp = None
        self.browser_init()
 
    def browser_init(self):
        chrome_debug_port = 9999
        chrome_options = Options()
        chrome_options.add_experimental_option("debuggerAddress", f"127.0.0.1:{chrome_debug_port}")
        # chrome_options.add_argument('--headless')
 
        self.browser = webdriver.Chrome(chrome_options=chrome_options)
        self.wait = WebDriverWait(self.browser, 5)
 
        all_handler = self.browser.window_handles
        if len(all_handler) >= 1:
            for index in all_handler[1:]:
                self.browser.switch_to.window(index)
                self.browser.close()
 
        # self.master_handler = self.browser.current_window_handle
        self.master_handler = self.browser.window_handles[0]
 
        self.browser.switch_to.window(self.master_handler)
        self.browser.execute_script('window.open()')
        # self.browser.execute_script('window.open("_blank")')
        handlers = self.browser.window_handles
        self.slaver_handler = handlers[-1]
        # print(self.browser.title)
 
    def get_detail_info(self, shop_url=None):
        # 切换到 从 窗口
        self.browser.switch_to.window(self.slaver_handler)
        self.browser.get(shop_url)
        html = self.browser.page_source
        html = html.replace('&lt;', '<').replace('&gt;', '>')
        # print(html)
        s_html = parsel.Selector(text=html)
        shop_keeper_xpath = '//div[@class="extend"]//li[@class="shopkeeper"]//a/text()'
        shop_keeper = s_html.xpath(shop_keeper_xpath).extract_first()
 
        phone_reg = '联系电话:(\d+-?\d+)|联系手机:(\d+)'
        phone = re.findall(phone_reg, html)
        # 处理完后 一定要切换到 主 窗口
        self.browser.switch_to.window(self.master_handler)
        return shop_keeper, phone
 
    def process_item(self, item):
        self.temp = None
        shop_xpath = './/div[@class="shop"]//a'
        local_xpath = './/div[@class="location"]'
        shop = item.find_element_by_xpath(shop_xpath).text
        shop_url = item.find_element_by_xpath(shop_xpath).get_attribute('href')
        local = item.find_element_by_xpath(local_xpath).text
        shop_keeper, phone = self.get_detail_info(shop_url)
        if phone:
            print(f'shop : {shop}')
            print(f'local : {local}')
            print(f'shop_url : {shop_url}')
            print(f'shop_keeper : {shop_keeper}')
            print(f'phone : {phone}')
            with open('./info.txt', 'a+') as f:
                f.write(shop + ',')
                f.write(local + ',')
                f.write(shop_url + ',')
                f.write(shop_keeper + ',')
                f.write(f'{phone}')
                f.write('\n')
 
    def main(self):
        # 切回 主 窗口
        self.browser.switch_to.window(self.master_handler)
        key_word = input('输入淘宝搜索关键字:')
        if not key_word:
            print('没有输入关键字。默认搜索 “手机”')
            key_word = '手机'
        url = f'https://s.taobao.com/search?q={key_word}'
        self.browser.get(url)
        shop_and_local_xpath = '//div[contains(@class, "J_MouserOnverReq")]//div[@class="row row-3 g-clearfix"]'
        shop_and_local = self.browser.find_elements_by_xpath(shop_and_local_xpath)
        for item in shop_and_local:
            self.process_item(item)
 
        # 这里是演示,所以只爬了前 5 页
        for page_num in range(2, 6):
            next_page_xpath = '//li[@class="item next"]'
            next_page = self.browser.find_element_by_xpath(next_page_xpath)
            next_page_enable = False if 'disabled' in next_page.get_attribute('class') else True
            if next_page_enable:
                print('*' * 100)
                print(f'第 {page_num} 页')
                next_page.click()
                # self.browser.refresh()
                self.wait.until(EC.presence_of_all_elements_located((By.XPATH, shop_and_local_xpath)))
                time.sleep(random.randint(3, 5))
                shop_and_local = self.browser.find_elements_by_xpath(shop_and_local_xpath)
                for item in shop_and_local:
                    self.process_item(item)
            else:
                break
 
 
if __name__ == '__main__':
    tb = TaoBaoSearch()
    tb.main()

headless 模式 

上面是一直有浏览器窗口的,没法使用 无头模式,可以使用 --user-data-dir 参数,然后设置无头模式。如果想改变 Chrome 位置,可以设置  chrome_options.binary_location 为 chrome.exe 路径即可。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
 
if __name__ == '__main__':
 
    chrome_options = Options()
 
    # 不使用默认的Chrome安装版本时,可以设置binary_location 指定 Chrome 路径 。
    # chrome 和 Chromium 对应 chromedriver.exe 版本不一样
    chrome_options.binary_location = r'D:\chrome\chrome.exe'
    # chrome_options.binary_location = r'D:\Chromium\chrome.exe'
 
    # chrome_options.add_argument('--headless')
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument('disable-infobars')
    chrome_options.add_argument(r'--user-data-dir=D:\chrome\userdatadir')
    # chrome_options.add_argument(r'--user-data-dir=D:\Chromium\userdatadir')
 
    browser = webdriver.Chrome(
        chrome_options=chrome_options,
        executable_path=r'D:\chrome\chromedriver.exe'
        # executable_path=r'D:\Chromium\chromedriver.exe'
    )
 
    browser.get('https://www.taobao.com/')
    user_name_xpath = '//div[@class="site-nav-user"]/a'
    user_name = browser.find_element_by_xpath(user_name_xpath).text
    print(user_name)
 

可以看到 无头模式下,使用 --user-data-dir 参数,可以登录淘宝。前提需要先手动登录淘宝,拿到登录信息的文件夹。

ichrome

github 地址:https://github.com/ClericPy/ichrome

这里就不放天猫、淘宝的代码了,贴一个药监局的:

( 流程:药品  --->  药品查询  --->  国产药品 ,然后就一直翻页)

import asyncio
from lxml import etree
from ichrome import AsyncChromeDaemon
 
 
async def main():
    async with AsyncChromeDaemon(headless=0, disable_image=False) as cd:
        async with cd.connect_tab(index=0, auto_close=True) as tab:
            url = 'https://www.nmpa.gov.cn/yaopin/index.html'
            wait_timeout = 5
            await tab.goto(url, timeout=wait_timeout)
            await asyncio.sleep(2)
 
            data_query_css_string = '#layer3 > div > a:nth-child(9)'
            await tab.wait_tag(data_query_css_string, max_wait_time=wait_timeout)
            await tab.click(data_query_css_string, timeout=wait_timeout)
            await asyncio.sleep(2)
 
            yao_query_css_string = '[title="国家局批准的药品批准文号信息"]'
            await tab.wait_tag(yao_query_css_string, max_wait_time=wait_timeout)
            await tab.click(yao_query_css_string, timeout=wait_timeout)
            await asyncio.sleep(2)
 
            while True:
                data_link_css_string = '#content table:nth-child(2) > tbody > tr:nth-child(1) > td > p > a'
                await tab.wait_tag(data_link_css_string, timeout=wait_timeout)
 
                html = await tab.get_html(timeout=wait_timeout)
                s_html = etree.HTML(text=html)
                s_table = s_html.xpath('//div[@id="content"]//table')[2]
                s_tr_list = s_table.xpath('.//tr')
                for s_tr in s_tr_list:
                    tag_a = s_tr.xpath('string(.)').strip()
                    print(tag_a)
                    # tag_a_href = s_tr.xpath('.//a/@href')
                    # print(tag_a_href)
 
                btn_next = '[src="images/dataanniu_07.gif"]'
                await tab.click(btn_next, timeout=wait_timeout)
                await asyncio.sleep(2)
 
 
if __name__ == "__main__":
    asyncio.run(main())

chrome 多开:设置不同的 debug_port 和 user_data_dir 可以达到多开 Chrome 

import json
import asyncio
import aiomultiprocess
from loguru import logger
from ichrome import AsyncChromeDaemon
from ichrome.async_utils import Chrome
 
 
async def startup_chrome(dp_port=None):
    """
        设置 chrome 参数,然后启动 chrome
    :param dp_port: 自定义 debug port
    :return:
    """
    logger.info(f'dp_port ---> {dp_port}')
    timeout = 5
    # 也可以给 Chrome 添加代理
    proxy = '127.0.0.1:8080'
    udd= f'c:/chrome_user_data_dir_{dp_port}'
    async with AsyncChromeDaemon(port=dp_port, proxy=proxy, user_data_dir=udd) as cd:
        async with cd.connect_tab(index=0) as tab:
            url = 'https://space.bilibili.com/1904149/'
            await tab.set_url(url, timeout=timeout)
            await asyncio.sleep(5)
            cookie = await tab.get_cookies(url, timeout=timeout)
            cookie_string = json.dumps(cookie, ensure_ascii=False)
            logger.info(f'cookie_string ---> {cookie_string}')
 
 
async def main():
    db_list = [9301 + offset for offset in range(5)]
    async with aiomultiprocess.Pool() as aio_pool:
        await aio_pool.map(startup_chrome, db_list)
    await aio_pool.join()
 
 
if __name__ == "__main__":
    asyncio.run(main())
    pass

深入浅出 CDP

https://www.cnblogs.com/bigben0123/p/15241062.html

常用的几个领域

  1. Page
    1. 简单地理解, 可以把一个 Page 看成一个 Page 类型的 Tab
    2. 对 Tab 的刷新, 跳转, 停止, 激活, 截图等功能都可以找到
    3. 也会有很多有用的事件需要 enable Page 以后才能监听到, 比如 loadEventFired
    4. 多个网站的任务, 可以在同一个浏览器里打开多个 Tab 进行操作, 通过不同的 Websocket 地址进行连接, 相对隔离, 并且托异步模型的福, Chrome 多个标签操作的抗压能力还不错
    5. 然而并发操作多个 Tab 的时候, 可能会出现一点小问题需要注意: 同一个浏览器实例, 对一个域名只能建立 6 个连接, 这个不太好改; 过快生成大量 Tab, 可能会导致有的 Tab 无法正常关闭(zombie tabs)
  2. Network
    1. 和产生网络流量有关系的大都在这个 Domain
    2. 比如 setExtraHTTPHeaders / setUserAgentOverride 对当前标签页的所有请求修改原是参数
    3. 比如对 cookie 的各种操作
    4. 通过 responseReceived + getResponseBody 来监听流量, 只用前者就能嗅探到 mp4 这种特殊类型的 url 了, 而后者可以把流量里已经 base64 化的数据进行其他操作, 比如验证码图片的处理
  3. 其他功能也基本和 devtools 一致

常规姿势

  1. 和某个 Tab 建立连接
  2. 通过 send 发送你想使用的 methods
  3. 通过 recv 监听你发送 methods 产生的事件, 或者其他 enable 的事件, 并执行对应回调

示例代码

from ichrome import AsyncChrome
import asyncio


async def async_operate_tab():
    chrome = AsyncChrome(host='127.0.0.1', port=9222)
    if not await chrome.connect():
        raise RuntimeError
    tab = (await chrome.tabs)[0]
    async with tab():
        # 跳转到 httpbin, 3 秒 loading 超时的话则 stop loading
        await tab.set_url('http://httpbin.org', timeout=3)
        # 注入 js, 并查看返回结果
        result = await tab.js("document.title")
        title = result['result']['result']['value']
        # 打印 title
        print(title)
        # httpbin.org
        # 通过 js 修改 title
        await tab.js("document.title = 'New Title'")
        # click 一个 css 选择器的位置, 跳转到了 Github
        await tab.click('body > a:first-child')
        # 等待加载完成
        await tab.wait_loading(3)

        async def callback_function(request):
            if request:
                # 监听到经过过滤的流量, 等待它加载一会比较保险
                for _ in range(3):
                    result = await tab.get_response(request)
                    if result.get('error'):
                        await tab.wait_loading(1)
                        continue
                    # 拿到整个 html
                    body = result['result']['body']
                    print(body)

        def filter_func(r):
            url = r['params']['response']['url']
            print('received:', url)
            return url == 'https://github.com/'

        # 监听流量, 需要异步处理, 则使用 asyncio.ensure_future 即可
        # 监听 10 秒
        task = asyncio.ensure_future(
            tab.wait_response(
                filter_function=filter_func,
                callback_function=callback_function,
                timeout=10),
            loop=tab.loop)
        # 点击一下左上角的小章鱼则会触发流量
        await tab.click('[href="https://github.com/"]')
        # 等待监听流量
        await task


if __name__ == "__main__":
    asyncio.run(async_operate_tab())

反检测 js:stealth.min.js

该文件包含了常用的浏览器特征,我们只需要读取该文件,然后执行 CDP 命令即可

反检测 js:https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions

github:https://github.com/berstend/puppeteer-extra/tree/stealth-js

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
import time

chrome_options = Options()

# 无头模式
# chrome_options.add_argument("--headless")

# 添加请求头
chrome_options.add_argument(
    'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36')

s = Service(r"chromedriver.exe路径")

driver = webdriver.Chrome(service=s, options=chrome_options)

# 利用stealth.min.js隐藏浏览器指纹特征
# stealth.min.js下载地址:https://github.com/berstend/puppeteer-extra/tree/stealth-js
with open('./stealth.min.js') as f:
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": f.read()
    })

driver.get(url='URL')
# driver.get(url='https://bot.sannysoft.com/')

# 保存图片
driver.save_screenshot('result.png')

time.sleep(200)

操作已开启的浏览器

只需要通过命令行启动一个浏览器

import subprocess

# 1、打开浏览器
# 指定端口号为:1234
# 配置用户数据路径:--user-data-dir
cmd = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe --remote-debugging-port=1234 --user-data-dir="C:\\selenum\\user_data"'

subprocess.run(cmd)

然后,利用 Selenium 直接操作上面的浏览器即可模拟正常操作浏览器的行为

import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service

# 操作上面已经打开的浏览器,进行百度搜索
chrome_options = Options()

# 指定已经打开浏览器的地址及端口号
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:1234")

# 注意:chrome版本与chromedirver驱动要保持一致
# 下载地址:http://chromedriver.storage.googleapis.com/index.html
s = Service(r"chromedriver.exe")

driver = webdriver.Chrome(service=s, options=chrome_options)

# 打开目标网站
driver.get(url="URL")

time.sleep(200)

undetected_chromedriver

这是一个防止浏览器指纹特征被识别的依赖库,可以自动下载驱动配置再运行

github:https://github.com/ultrafunkamsterdam/undetected-chromedriver

github:https://github.com/search?q=undetected-chromedriver

github 搜索 selenium,看看还有没有其他的

pip install  git+https://github.com/ultrafunkamsterdam/undetected-chromedriver.git

安装依赖:pip3 install undetected-chromedriver

然后,通过下面几行代码就能完美隐藏浏览器的指纹特征

from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import time
import undetected_chromedriver as uc

chrome_options = Options()
# chrome_options.add_argument("--headless")

s = Service(r"chromedriver.exe")

driver = uc.Chrome(service=s, options=chrome_options)

driver.get(url='URL')
# driver.get(url='https://bot.sannysoft.com/')

driver.save_screenshot('result.png')
time.sleep(100)

懂车帝 示例

懂车帝对 selenium 反爬挺厉害,通过undetected_chromedriver可轻松搞定。

import ssl
import time
import undetected_chromedriver as uc
from selenium.webdriver.remote.webdriver import By

# ssl._create_unverified_context() 函数创建了一个未经验证的 SSL 上下文
# 禁用 SSL 证书验证
ssl._create_default_https_context = ssl._create_unverified_context
uc.TARGET_VERSION = 91


def main():
    driver = uc.Chrome()
    driver.get('https://www.dongchedi.com/user/53334173333')
    time.sleep(3)
    driver.find_element(By.XPATH, '//input[contains(@class, "search-form")]').send_keys('法拉利')
    time.sleep(3)
    driver.find_element(By.XPATH, '//button[contains(@class, "tw-w-64")]').click()
    time.sleep(50)
    driver.close()


if __name__ == '__main__':
    main()
    pass

github 示例

示例 1:

import time
import selenium.webdriver.support.expected_conditions as EC  # noqa
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.remote.webdriver import By
from selenium.webdriver.support.wait import WebDriverWait

import undetected_chromedriver as uc


def main():
    driver = uc.Chrome()
    driver.get("https://www.google.com")

    # accept the terms
    driver.find_elements(By.XPATH, '//*[contains(text(), "Reject all")]')[-1].click()
    inp_search = driver.find_element(By.XPATH, '//input[@title="Search"]')
    inp_search.send_keys("site:stackoverflow.com undetected chromedriver\n")
    results_container = WebDriverWait(driver, timeout=3).until(
        EC.presence_of_element_located((By.ID, "rso"))
    )

    driver.execute_script(
        """
        let container = document.querySelector('#rso');
        let el = document.createElement('div');
        el.style = 'width:500px;display:block;background:red;color:white;z-index:999;transition:all 2s ease;padding:1em;font-size:1.5em';
        el.textContent = "Excluded from support...!";
        container.insertAdjacentElement('afterBegin', el);
        setTimeout(() => {
            el.textContent = "<<<  OH , CHECK YOUR CONSOLE! >>>"}, 2500)
        
    """
    )
    time.sleep(2)
    for item in results_container.children("a", recursive=True):
        print(item)

    # switching default WebElement for uc.WebElement and do it again
    driver._web_element_cls = uc.UCWebElement

    print("switched to use uc.WebElement. which is more descriptive")
    results_container = driver.find_element(By.ID, "rso")

    # gets only direct children of results_container
    # children is a method unique for undetected chromedriver. it is
    # incompatible when you use regular chromedriver
    for item in results_container.children():
        print(item.tag_name)
        for grandchild in item.children(recursive=True):
            print("\t\t", grandchild.tag_name, "\n\t\t\t", grandchild.text)

    print("lets go to image search")
    inp_search = driver.find_element(By.XPATH, '//input[@name="q"]')
    inp_search.clear()
    inp_search.send_keys("hot girls\n")  # \n as equivalent of ENTER

    body = driver.find_element(By.TAG_NAME, "body")
    body.find_elements(By.XPATH, '//a[contains(text(), "Images")]')[0].click_safe()

    # you can't reuse the body from above, because we are on another page right now
    # so the body above is not attached anymore
    image_search_body = WebDriverWait(driver, 5).until(
        EC.presence_of_element_located((By.TAG_NAME, "body"))
    )

    # gets all images and prints the src
    print("getting image sources data, hold on...")

    for item in image_search_body.children("img", recursive=True):
        print(item.attrs.get("src", item.attrs.get("data-src")), "\n\n")

    USELESS_SITES = [
        "https://www.trumpdonald.org",
        "https://www.isitchristmas.com",
        "https://isnickelbacktheworstbandever.tumblr.com",
        "https://www.isthatcherdeadyet.co.uk",
        "https://whitehouse.gov",
        "https://www.nsa.gov",
        "https://kimjongillookingatthings.tumblr.com",
        "https://instantrimshot.com",
        "https://www.nyan.cat",
        "https://twitter.com",
    ]

    print("opening 9 additinal windows and control them")
    time.sleep(2)  # never use this. this is for demonstration purposes only
    for _ in range(9):
        driver.window_new()

    print("now we got 10 windows")
    time.sleep(2)
    print("using the new windows to open 9 other useless sites")
    time.sleep(2)  # never use this. this is for demonstration purposes only

    for idx in range(1, 10):
        # skip the first handle which is our original window
        print("opening ", USELESS_SITES[idx])
        driver.switch_to.window(driver.window_handles[idx])

        # because of geographical location, (corporate) firewalls and 1001
        # other reasons why a connection could be dropped we will use a try/except clause here.
        try:
            driver.get(USELESS_SITES[idx])
        except WebDriverException as e:
            print(
                (
                    "webdriver exception. this is not an issue in chromedriver, but rather "
                    "an issue specific to your current connection. message:",
                    e.args,
                )
            )
            continue

    for handle in driver.window_handles[1:]:
        driver.switch_to.window(handle)
        print("look. %s is working" % driver.current_url)
        time.sleep(2)  # never use this. it is here only so you can follow along

    print(
        "close windows (including the initial one!), but keep the last new opened window"
    )
    time.sleep(4)  # never use this. wait until nowsecure passed the bot checks

    for handle in driver.window_handles[:-1]:
        driver.switch_to.window(handle)
        print("look. %s is closing" % driver.current_url)
        time.sleep(1)
        driver.close()

    # attach to the last open window
    driver.switch_to.window(driver.window_handles[0])
    print("now we only got ", driver.current_url, "left")

    time.sleep(1)

    driver.get("https://www.nowsecure.nl")

    time.sleep(5)

    print("lets go to UC project page")
    driver.get("https://www.github.com/ultrafunkamsterdam/undetected-chromedriver")

    time.sleep(2)
    driver.quit()


if __name__ == "__main__":
    main()
    pass

示例 2:

import os
import time
from selenium.webdriver.support.wait import WebDriverWait
import selenium.webdriver.support.expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import undetected_chromedriver as uc
from loguru import logger


def main():
    driver = uc.Chrome(
        headless=False,
        browser_executable_path=r'C:\Program Files\Google\Chrome\Application\Chrome.exe'
    )
    driver.get('chrome://version')
    driver.save_screenshot('/home/runner/work/_temp/versioninfo.png')
    driver.get('chrome://settings/help')
    driver.save_screenshot('/home/runner/work/_temp/helpinfo.png')
    driver.get('https://www.google.com')
    driver.save_screenshot('/home/runner/work/_temp/google.com.png')
    driver.get('https://bot.incolumitas.com/#botChallenge')
    pdfdata = driver.execute_cdp_cmd('Page.printToPDF', {})
    if pdfdata:
        if 'data' in pdfdata:
            data = pdfdata['data']
            import base64
            buffer = base64.b64decode(data)
            with open('/home/runner/work/_temp/report.pdf', 'w+b') as f:
                f.write(buffer)
    driver.get('https://www.nowsecure.nl')
    logger.info('current url %s' % driver.current_url)
    try:
        WebDriverWait(driver, 15).until(EC.title_contains('moment'))
    except TimeoutException:
        pass
    logger.info('current page source:\n%s' % driver.page_source)
    logger.info('current url %s' % driver.current_url)
    try:
        WebDriverWait(driver, 15).until(EC.title_contains('nowSecure'))
        logger.info('PASSED CLOUDFLARE!')
    except TimeoutException:
        logger.info('timeout')
        print(driver.current_url)
    logger.info('current page source:\n%s\n' % driver.page_source)
    driver.save_screenshot('/home/runner/work/_temp/nowsecure.png')
    # driver.get('https://imgur.com/upload')
    # driver.find_element('css selector', 'input').send_keys('/home/runner/work/_temp/nowsecure.png')
    # logger.info('current url %s' % driver.current_url)
    # time.sleep(5)
    driver.quit()


if __name__ == "__main__":
    main()

小红书 示例

import re
import json
import time
import requests
from jsonpath import jsonpath
from concurrent.futures import ThreadPoolExecutor
import undetected_chromedriver as uc


def main_1():
    def handle_response(event):
        try:
            req_url = event['params']['response']['url']
            if 'xiaohongshu.com/api/sns/web/v1/search/notes' in req_url:
                print(req_url)
                request_id = event['params']['requestId']
                data_dict = browser.execute_cdp_cmd('Network.getResponseBody', {'requestId': request_id})
                resp_string = data_dict['body']
                resp_dict = json.loads(resp_string)
                # print(f'resp_dict ---> {resp_dict}')
                # https://www.xiaohongshu.com/explore/633296f2000000001703f491
                item_list = resp_dict['data']['items']
                with open('./note_id_list.txt', 'a', encoding='utf-8') as f:
                    for temp in item_list:
                        if 24 == len(temp["id"]):
                            f.write(f'{temp["id"]}\n')
                pass
        except BaseException as be:
            print(be)
            pass

    browser = uc.Chrome(
        headless=False,
        use_subprocess=False,
        user_data_dir='./user_data_dir',
        enable_cdp_events=True
    )
    browser.add_cdp_listener(
        # event_name='Network.dataReceived',  # 数据块接收完成后触发事件
        event_name='Network.responseReceived',  # response 接收完成后触发事件
        callback=handle_response  # 触发事件后, 处理事件的函数
    )

    kw_list = ['美女']
    for index in range(len(kw_list)):
        kw = kw_list[index]
        url = f'https://www.xiaohongshu.com/search_result?keyword={kw}&source=web_explore_feed'
        browser.get(url)
        while True:
            # js_code = "() => window.scrollTo(0,document.body.scrollHeight)"
            js_code = "window.scrollTo(0, document.body.scrollHeight);"
            browser.execute_script(js_code)
            time.sleep(1)
        pass

    browser.close()
    browser.quit()


def main_2():
    def handle_response(event):
        try:
            req_url = event['params']['response']['url']
            if 'www.xiaohongshu.com/explore' in req_url:
                print(req_url)
                request_id = event['params']['requestId']
                data_dict = browser.execute_cdp_cmd('Network.getResponseBody', {'requestId': request_id})
                resp_string = data_dict['body']
                extract_string = re.findall(r'__INITIAL_STATE__=([\s\S]*?)</script>', resp_string)[0]
                temp = re.sub(':undefined', ':null', extract_string)
                extract_dict = json.loads(temp)
                query_list = jsonpath(extract_dict, '$..imageList')
                if query_list and len(query_list):
                    image_list = query_list[0]
                    with open('./image_list.txt', 'a', encoding='utf-8') as f:
                        for temp in image_list:
                            f.write(f'{temp["url"]}\n')
                pass
        except BaseException as be:
            print(be)
            pass

    browser = uc.Chrome(
        headless=False,
        use_subprocess=False,
        user_data_dir='./user_data_dir',
        enable_cdp_events=True
    )
    browser.add_cdp_listener(
        # event_name='Network.dataReceived',  # 数据块接收完成后触发事件
        event_name='Network.responseReceived',  # response 接收完成后触发事件
        callback=handle_response  # 触发事件后, 处理事件的函数
    )

    line_list = []
    with open('./note_id_list.txt', encoding='utf-8') as f1:
        line_list = f1.readlines()
    note_id_list = [x.strip() for x in line_list]
    note_count = len(note_id_list)

    for index in range(note_count):
        note_id = note_id_list[index]
        url = f'https://www.xiaohongshu.com/explore/{note_id}'
        browser.get(url)
        time.sleep(2)
        pass
    browser.close()
    browser.quit()


def download_img():
    line_list = []
    with open('./image_list.txt', encoding='utf-8') as f1:
        line_list = f1.readlines()
    img_url_list = [x.strip() for x in line_list]
    img_url_count = len(img_url_list)
    for index in range(img_url_count):
        if index <= 474:
            continue
        img_url = img_url_list[index]
        img_id = img_url.split('com/')[-1]
        resp = requests.get(img_url)
        if 200 == resp.status_code:
            print(f'[{index}][{img_url_count}] success ---> {img_url}')
            with open(f'./img_list/{img_id}.jpg', 'wb') as f:
                f.write(resp.content)
        else:
            print(f'[{index}][{img_url_count}] fail ---> {img_url}')


if __name__ == '__main__':
    # main_1()
    # main_2()
    download_img()
    pass

面向对象 示例:

import json
import time
import datetime
from loguru import logger
from selenium import webdriver
import undetected_chromedriver as uc
import concurrent
from concurrent.futures import ThreadPoolExecutor


class BrowserSpider(object):

    def __init__(self, *args, **kwargs):
        self.temp = None
        self.browser = self.__generate_webdriver(enable_cdp_events=True)
        self.browser.add_cdp_listener(
            event_name='Network.responseReceived',  # 监听的事件
            callback=self.handle_response  # 当监听到事件发生时,调用函数进行处理
        )
        pass

    def __del__(self):
        try:
            self.browser.close()
            self.browser.quit()
        except BaseException as be:
            pass

    def __generate_webdriver(self, headless=False, proxies=False, enable_cdp_events=False):
        self.temp = None
        chrome_options = webdriver.ChromeOptions()  # Create driver options object
        # chrome_options.add_argument('-incognito')  # 隐身模式
        # chrome_options.add_argument("--disable-geolocation") # 禁用自动地理定位
        chrome_options.add_argument("--disable-extensions")  # 禁用其他扩展
        chrome_options.add_argument("--lang=en-US")  # 更改Chrome语言
        if headless:
            chrome_options.add_argument("--headless")
        if proxies:
            proxy = proxies['https']
            chrome_options.add_argument(f'--proxy-server={proxy}--timeout=120')
        browser = uc.Chrome(
            options=chrome_options,
            enable_cdp_events=enable_cdp_events,
            use_subprocess=False
        )
        return browser

    def handle_response(self, event):
        try:
            # print(f'event ---> {event}')
            request_id = event['params']['requestId']
            cdp_data_dict = self.browser.execute_cdp_cmd('Network.getResponseBody', {'requestId': request_id})
            resp_body = json.loads(cdp_data_dict['body'])
            logger.info(f'resp_body ---> {resp_body}')
        except BaseException as be:
            # logger.error(be)
            pass

    def crawl_data_1(self):
        kw_list = ['藏族']
        for kw in kw_list:
            url = f'https://www.xiaohongshu.com/search_result?keyword={kw}&source=web_explore_feed'
            self.browser.get(url)
            js_code = "window.scrollTo(0, document.body.scrollHeight);"
            start_time = datetime.datetime.now()
            while True:
                self.browser.execute_script(js_code)
                end_time = datetime.datetime.now()
                if (end_time - start_time).seconds > 10:
                    break
            pass

    def crawl_data_2(self, url=None):
        logger.info(f'url ---> {url}')
        self.browser.get(url)
        time.sleep(10)
        pass


def main():
    bs = BrowserSpider()
    bs.crawl_data_1()


if __name__ == '__main__':
    main()
    pass

5、Playwright

要使用 Playwright,需要 Python 3.7 版本及以上,请确保 Python 的版本符合要求。
要安装 Playwright,可以直接使用 pip3,命令:pip3 install playwright
安装完成之后需要进行一些初始化操作(相关浏览器驱动):playwright install
这时候 Playwrigth 会安装 Chromium, Firefox and WebKit 浏览器并配置一些驱动,我们不必关心中间配置的过程,Playwright 会为我们配置好。
具体的安装说明可以参考:https://setup.scrape.center/playwright
安装完成之后,我们便可以使用 Playwright 启动 Chromium 或 Firefox 或 WebKit 浏览器来进行自动化操作了

5.1 示例:同步、异步

基于 playwright 和 pytest 单元测试框架的自动化项目

https://github.com/defnngj/playwright-pro

使用框架图

同步 代码

from playwright.sync_api import sync_playwright

def run(playwright):
    firefox = playwright.firefox
    browser = firefox.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

playwright 支持创建多个Browser contexts,相当于是打开浏览器后,可以创建多个页面上下文,每个上下文做的操作可以不同

from playwright.sync_api import sync_playwright

# 打开两个浏览器上下文
with sync_playwright() as p:
    browser = p.chromium.launch(headless=False, slow_mo=100)  # 打开浏览器
    context1 = browser.new_context()  # 创建浏览器上下文,支持创建多个上下文
    page1 = context1.new_page()  # 新打开一个浏览器标签页
    page1.goto("https://www.baidu.com")
    context2 = browser.new_context()  # 创建浏览器上下文,支持创建多个上下文
    page2 = context2.new_page()  # 新打开一个浏览器标签页
    page2.goto("https://www.bilibili.com")
    browser.close()

运行效果如图。打开两个浏览器实例,

一个浏览器上下文就相当于一个 浏览器 实例,浏览器和上下文都可以使用 new_page() 方法打开一个新的浏览器标签页(选项卡)

browser = p.chromium.launch(headless=False)
page = browser.new_page()

当我们通过点击某些按钮/超链接打开一个新的浏览器标签页时,还需要继续在这个浏览器标签页上继续操作时,那么可以使用以下方式

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False, slow_mo=100)  # 打开浏览器
    context1 = browser.new_context()  # 创建浏览器上下文,支持创建多个上下文
    page1 = context1.new_page()
    page1.goto("https://www.baidu.com")
    with context1.expect_page() as new_page_info:
        page1.click('//a[contains(@href, "https://www.hao123.com")]')  # 在百度首页点击hao123后会打开一个新的选项卡
    new_page = new_page_info.value
    new_page.click('//a[contains(text(), "hao123推荐")]')  # 在hao123点击hao123推荐
pass

参考:https://playwright.dev/python/docs/multi-pages

异步 代码

import asyncio
from playwright.async_api import async_playwright

async def run(playwright):
    firefox = playwright.firefox
    browser = await firefox.launch()
    page = await browser.new_page()
    await page.goto("https://example.com")
    await browser.close()

async def main():
    async with async_playwright() as playwright:
        await run(playwright)
asyncio.run(main())

5.2 Playwright 官网文档

github 搜索:https://github.com/search?q=Playwright

Playwright

scrapy-playwright

scrapy-playwright:https://github.com/scrapy-plugins/scrapy-playwright

playwright-python

官网文档 ( docs、API ):https://playwright.dev/python/docs/intro

特点

  • 支持所有浏览器。Playwright 可以在所有浏览器中实现快速、可靠和强大的自动化测
  • 快速可靠的执行
  • 强大的自动化功能
  • 与你的工作流集成

5.3 为什么选择 Playwright

为什么选择 Playwright:https://www.cnblogs.com/fnng/p/14274960.html

支持所有浏览器

const { chromium, webkit, firefox } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  // const browser = await webkit.launch();
  // const browser = await firefox.launch();

  const page = await browser.newPage();
  await page.goto('http://www.baidu.com/');
  await page.screenshot({ path: `example.png` });
  await browser.close();
})();

测试移动端:通过设置驱动模式可以模拟移动浏览器的效果。

const { webkit, devices } = require('playwright');
const iPhone11 = devices['iPhone 11 Pro'];

(async () => {
  const browser = await webkit.launch();
  const context = await browser.newContext({
    ...iPhone11,
    locale: 'en-US',
    geolocation: { longitude: 12.492507, latitude: 41.889938 },
    permissions: ['geolocation']
  });
  const page = await context.newPage();
  await page.goto('https://m.baidu.com');
  await page.screenshot({ path: 'colosseum-iphone.png' });
  await browser.close();
})();

Headless 和 headful: Playwright支持所有平台和浏览器上使用Headless模式和Headful模式。Headful非常适合调试。Headless运行更快,也可以更方便的在CI/云平台上运行。

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({headless: false});
  // ...
})();

headless 默认开启,设置为false,即为 headful模式,可以看到自动化的过程。

快速可靠的执行

  • 自动等待: Playwright 可以自动等待元素,这将会提高自动化的稳定性,简化测试的编写。
  • 浏览器上下文并行:对具有浏览器上下文的多个并行、隔离的执行环境,重用单个浏览器实例。

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({headless: false, slowMo: 50 });
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('http://www.testpub.cn/login');
  await page.fill("#inputUsername", 'admin');
  await page.fill("#inputPassword", 'admin123456');
  await page.click('"Sign in"');
  await page.close();

  const page2 = await context.newPage();
  await page2.goto("http://www.testpub.cn/guest_manage/")
  await browser.close();
})();

非常方便的 选择元素:Palywright可以依赖面向用户的字符串,如文本内容和可访问性标签来选择元素。这些字符串比与DOM结构紧密耦合的选择器更有弹性。

<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>

例如上面的元素一看就不太好定位,用户到的是一个按钮,名字叫Sign in,那么可以用这个定位方式:await page.click('"Sign in"');

强大的自动化能力

  • 支持多个域、页面和表单: Palywright是一个 进程外(out-of-process) 自动化驱动程序,它不受页内JavaScript执行范围的限制,可以自动处理多个页面的场景。

// Create two pages
const pageOne = await context.newPage();
const pageTwo = await context.newPage();

// Get pages of a brower context
const allPages = context.pages();

  • 强大的网络控制: Palywright引入上下文范围的网络拦截存根和模拟网络请求。

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({headless: false, slowMo: 50 });
  const context = await browser.newContext({ acceptDownloads: true });
  const page = await context.newPage();
  await page.goto('https://pypi.org/project/selenium/#files');
  const [ download ] = await Promise.all([
    page.waitForEvent('download'), 
    page.click('#files > table > tbody > tr:nth-child(2) > th > a')
  ]);
  const path = await download.path();
  console.log("path", path);
  await browser.close();
})();

在 HTTP 认证,文件下载、网络请求响应方面都有很强的控制能力。

  • 覆盖所有场景的功能:支持文件下载、上传,进程外表单,输入、点击,甚至是手机上流行的暗黑模式。

// Create context with dark mode
const context = await browser.newContext({
  colorScheme: 'dark' // or 'light'
});

  • 行命令安装:运行npm i playwright 自动下载浏览器依赖,让你的团队快速上手。
  • 支持TypeScript:Playwright 附带内置的自动完成类型和其他收益。
  • 调试工具:通过 VS Code 完成自动化的调试。
  • 语言绑定:Playwright 支持多种编程语言,这个前面的文章有介绍。
  • 在CI上部署测试:你要可以使用Docker镜像,Playwright默认也是headless模式,你可以在任何环境上执行。

Playwright 反检测

playwright-stealth 是一个 Python 包,通过覆盖特定配置来避免机器人检测。Playwright Stealth 是 puppeteer-extra-plugin-stealth npm 包的移植版,它使用内置规避模块,避免泄露并更改暴露自动化浏览器为机器人的属性。例如,它删除 navigator.webdriver 属性,并从 Chrome 默认的无头模式的用户代理标头中删除“HeadlessChrome”。

Stealth 插件的目标是使自动化无头浏览器实例能够成功通过 sannysoft.com 上的所有机器人检测测试。截至写作时,这一目标已经实现。然而,正如官方文档中所提到的,仍然有检测无头浏览器的方法。所以今天有效的方法,明天可能就无效了。完全绕过所有机器人检测机制是不可能实现的,但该库旨在使这个过程尽可能具有挑战性。

pypi 搜索:https://pypi.org/search/?q=stealth

undetected-playwright:https://pypi.org/project/undetected-playwright/

5.4 爬虫利器 Playwright 的基本用法


爬虫利器 Playwright 的基本用法:https://cuiqingcai.com/36045.html

playwright 教程:https://blog.csdn.net/m0_51156601/article/details/126886040

Playwright 网络爬虫中文教程:https://sql.wang/playwright/scrool-page/

playwright最详细使用教程:https://blog.csdn.net/m0_51156601/article/details/126886040

Playwright 支持两种编写模式,一种是类似 Pyppetter 一样的异步模式,另一种是像 Selenium 的同步模式,我们可以根据实际需要选择使用不同的模式。

同步 示例:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    for browser_type in [p.chromium, p.firefox, p.webkit]:
        browser = browser_type.launch(headless=False)
        page = browser.new_page()
        page.goto('https://www.baidu.com')
        page.screenshot(path=f'screenshot-{browser_type.name}.png')
        print(page.title())
        browser.close()

launch 方法返回的是一个 Browser 对象,我们将其赋值为 browser 变量。然后调用 browser 的 new_page 方法,相当于新建了一个选项卡,返回的是一个 Page 对象,将其赋值为 page,这整个过程其实和 Pyppeteer 非常类似。接着我们就可以调用 page 的一系列 API 来进行各种自动化操作了,比如调用 goto,就是加载某个页面,这里我们访问的是百度的首页。接着我们调用了 page 的 screenshot 方法,参数传一个文件名称,这样截图就会自动保存为该图片名称,这里名称中我们加入了 browser_type 的 name 属性,代表浏览器的类型,结果分别就是 chromium, firefox, webkit。另外我们还调用了 title 方法,该方法会返回页面的标题,即 HTML 中 title 节点中的文字,也就是选项卡上的文字,我们将该结果打印输出到控制台。最后操作完毕,调用 browser 的 close 方法关闭整个浏览器,运行结束。

运行一下,这时候我们可以看到有三个浏览器依次启动并加载了百度这个页面,分别是 Chromium、Firefox 和 Webkit 三个浏览器,页面加载完成之后,生成截图、控制台打印结果就退出了。

异步 示例:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        for browser_type in [p.chromium, p.firefox, p.webkit]:
            browser = await browser_type.launch()
            page = await browser.new_page()
            await page.goto('https://www.baidu.com')
            await page.screenshot(path=f'screenshot-{browser_type.name}.png')
            print(await page.title())
            await browser.close()

asyncio.run(main())

写法和同步模式基本类似,导入的时候使用的是 async_playwright 方法,而不再是 sync_playwright 方法。写法上添加了 async/await 关键字的使用,最后的运行效果是一样的。

另外我们注意到,这例子中使用了 with as 语句,with 用于上下文对象的管理,它可以返回一个上下文管理器,也就对应一个 PlaywrightContextManager 对象,无论运行期间是否抛出异常,它能够帮助我们自动分配并且释放 Playwright 的资源。

代码生成 ( 录制操作 )

Playwright 还有一个强大的功能,那就是可以录制我们在浏览器中的操作并将代码自动生成出来,有了这个功能,我们甚至都不用写任何一行代码,这个功能可以通过 playwright 命令行调用 codegen 来实现,我们先来看看 codegen 命令都有什么参数,输入如下命令:playwright codegen --help

Usage: npx playwright codegen [options] [url]

open page and generate code for user actions

Options:
  -o, --output <file name>     saves the generated script to a file
  --target <language>          language to use, one of javascript, python, python-async, csharp (default: "python")
  -b, --browser <browserType>  browser to use, one of cr, chromium, ff, firefox, wk, webkit (default: "chromium")
  --channel <channel>          Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc
  --color-scheme <scheme>      emulate preferred color scheme, "light" or "dark"
  --device <deviceName>        emulate device, for example  "iPhone 11"
  --geolocation <coordinates>  specify geolocation coordinates, for example "37.819722,-122.478611"
  --load-storage <filename>    load context storage state from the file, previously saved with --save-storage
  --lang <language>            specify language / locale, for example "en-GB"
  --proxy-server <proxy>       specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
  --save-storage <filename>    save context storage state at the end, for later use with --load-storage
  --timezone <time zone>       time zone to emulate, for example "Europe/Rome"
  --timeout <timeout>          timeout for Playwright actions in milliseconds (default: "10000")
  --user-agent <ua string>     specify user agent string
  --viewport-size <size>       specify browser viewport size in pixels, for example "1280, 720"
  -h, --help                   display help for command

Examples:

  $ codegen
  $ codegen --target=python
  $ codegen -b webkit https://example.com

这里有几个选项,比如 -o 代表输出的代码文件的名称;—target 代表使用的语言,默认是 python,即会生成同步模式的操作代码,如果传入 python-async 就会生成异步模式的代码;-b 代表的是使用的浏览器,默认是 Chromium,其他还有很多设置,比如 —device 可以模拟使用手机浏览器,比如 iPhone 11,—lang 代表设置浏览器的语言,—timeout 可以设置页面加载超时时间。

示例:playwright codegen -o script.py -b firefox

这时候就弹出了一个 Firefox 浏览器,同时右侧会输出一个脚本窗口,实时显示当前操作对应的代码。可以在浏览器中做任何操作,比如打开百度,然后点击输入框并输入 mm,然后再点击搜索按钮。

在操作过程中,该窗口中的代码就实时变化,可以看到这里生成了我们一系列操作的对应代码,比如在搜索框中输入 nba,就对应如下代码:page.fill("input[name=\"wd\"]", "nba")

操作完毕之后,关闭浏览器,Playwright 会生成一个 script.py 文件。

所以,有了这个功能,我们甚至都不用编写任何代码

context 变量对应的是一个 BrowserContext 对象,BrowserContext 是一个类似隐身模式的独立上下文环境,其运行资源是单独隔离的,在做一些自动化测试过程中,每个测试用例我们都可以单独创建一个 BrowserContext 对象,这样可以保证每个测试用例之间互不干扰,具体的 API 可以参考 https://playwright.dev/python/docs/api/class-browsercontext

移动端浏览器支持

Playwright 另外一个特色功能就是可以支持移动端浏览器的模拟,比如模拟打开 iPhone 12 Pro Max 上的 Safari 浏览器,然后手动设置定位,并打开百度地图并截图。首先我们可以选定一个经纬度,比如故宫的经纬度是 39.913904, 116.39014,我们可以通过 geolocation 参数传递给 Webkit 浏览器并初始化。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    iphone_12_pro_max = p.devices['iPhone 12 Pro Max']
    browser = p.webkit.launch(headless=False)
    context = browser.new_context(
        **iphone_12_pro_max,
        locale='zh-CN',
        geolocation={'longitude': 116.39014, 'latitude': 39.913904},
        permissions=['geolocation']
    )
    page = context.new_page()
    page.goto('https://amap.com')
    page.wait_for_load_state(state='networkidle')
    page.screenshot(path='location-iphone.png')
    browser.close()

先用 PlaywrightContextManager 对象的 devices 属性指定了一台移动设备,这里传入的是手机的型号,比如 iPhone 12 Pro Max,当然也可以传其他名称,比如 iPhone 8,Pixel 2 等。

现在用 BrowserContext 对象来新建一个页面,还是调用 new_page 方法创建一个新的选项卡,然后跳转到高德地图,并调用了 wait_for_load_state 方法等待页面某个状态完成,这里我们传入的 state 是 networkidle,也就是网络空闲状态。因为在页面初始化和加载过程中,肯定是伴随有网络请求的,所以加载过程中肯定不算 networkidle 状态,所以这里我们传入 networkidle 就可以标识当前页面和数据加载完成的状态。加载完成之后,我们再调用 screenshot 方法获取当前页面截图,最后关闭浏览器。

运行下代码,可以发现这里就弹出了一个移动版浏览器,然后加载了高德地图

选择器 ( 文本、CSS、XPath、节点 )

前面用到了 click 和 fill 等方法都传入了一个字符串,这些字符串有的符合 CSS 选择器的语法,有的又是 text= 开头的,感觉似乎没太有规律的样子,它到底支持怎样的匹配规则呢?下面我们来了解下。

传入的这个字符串,我们可以称之为 Element Selector,它不仅仅支持 CSS 选择器、XPath,Playwright 还扩展了一些方便好用的规则,比如直接根据文本内容筛选,根据节点层级结构筛选等等。

文本选择

文本选择支持直接使用 text= 这样的语法进行筛选,示例:page.click("text=Log in")

这就代表选择文本是 Log in 的节点,并点击。

CSS 选择器

CSS 选择器之前也介绍过了,比如根据 id 或者 class 筛选:

page.click("button")
page.click("#nav-bar .contact-us-item")

根据特定的节点属性筛选:

page.click("[data-test=login-button]")
page.click("[aria-label='Sign in']")

CSS 选择器 + 文本

还可以使用 CSS 选择器结合文本值进行海选,比较常用的就是 has-text 和 text,前者代表包含指定的字符串,后者代表字符串完全匹配,示例如下:

page.click("article:has-text('Playwright')")
page.click("#nav-bar :text('Contact us')")

第一个就是选择文本中包含 Playwright 的 article 节点,第二个就是选择 id 为 nav-bar 节点中文本值等于 Contact us 的节点。

CSS 选择器 + 节点关系

还可以结合节点关系来筛选节点,比如使用 has 来指定另外一个选择器,示例如下:

page.click(".item-description:has(.item-promo-banner)")

比如这里选择的就是选择 class 为 item-description 的节点,且该节点还要包含 class 为 item-promo-banner 的子节点。
另外还有一些相对位置关系,比如 right-of 可以指定位于某个节点右侧的节点,
示例:page.click("input:right-of(:text('Username'))")

这里选择的就是一个 input 节点,并且该 input 节点要位于文本值为 Username 的节点的右侧。

XPath

当然 XPath 也是支持的,不过 xpath 这个关键字需要我们自行制定,

示例:page.click("xpath=//button")

需要在开头指定 xpath= 字符串,代表后面是一个 XPath 表达式。

关于更多选择器的用法和最佳实践,可以参考官方文档:https://playwright.dev/python/docs/locators

用户数据目录

launch_persistent_context:https://playwright.dev/python/docs/api/class-browsertype

import asyncio
from playwright.async_api import async_playwright

# import sys
# sys.path.append('D:\\Project\\my_spiders')
# print(sys.path)

from my_spiders.tools.mongodb_operate import MongoDB

continue_next = False
mongo_url = 'mongodb://admin:admin@172.16.30.180:27017'
mongo_db = MongoDB(url=mongo_url, db='admin')

comment_max = 0


def handle_req(req):
    # mth = req.method
    # url = req.url
    # print(mth)
    pass


async def handle_resp(resp):
    status = resp.status
    url = resp.url
    if 'buildComments' in url:
        try:
            # resp_body = await resp.body()
            # print(f'{url} ---> {resp_body.decode("utf-8")}')
            resp_json = await resp.json()
            # print(f'{url} ---> {resp_json}')
        except BaseException as be:
            return
        comment_list = resp_json.get('data', [])
        global comment_max
        if len(comment_list):
            # mongo_db.add_batch('weibo_comment_test', comment_list)
            comment_max += len(comment_list)
            print(f'comment count ---> {len(comment_list)}')
        if 0 == resp_json.get('max_id', -1):
            print(f'comment_max ---> {comment_max}')
            comment_max = 0
    pass


async def setup_browser(playwright):
    chromium = playwright.chromium
    # browser = await chromium.launch(headless=False, slow_mo=100)
    browser = await chromium.launch_persistent_context(
        headless=False, slow_mo=100,
        user_data_dir='./user_dir'
    )
    page = await browser.new_page()
    # Subscribe to "request" and "response" events.
    page.on("request", handle_req)
    page.on("response", handle_resp)
    # await page.goto("https://m.weibo.cn/detail/4573642094806773")
    await page.goto("https://weibo.com/7418063007/JuTLzkasB#comment")
    while True:
        await page.evaluate("() => window.scrollTo(0,document.body.scrollHeight)")
        await asyncio.sleep(5)
    # await browser.close()


async def main():
    async with async_playwright() as playwright:
        await setup_browser(playwright)


if __name__ == '__main__':
    asyncio.run(main())
    pass

常用操作方法 ( click,输入fill  )

常见的一些 API 如点击 click,输入 fill 等操作,这些方法都是属于 Page 对象的,所以所有的方法都从 Page 对象的 API 文档查找就好了,文档地址:https://playwright.dev/python/docs/api/class-page。

事件监听 ( 拦截 )

Page 对象提供了一个 on 方法,它可以用来监听页面中发生的各个事件,比如 close、console、load、request、response 等等。

示例:监听 response 事件,response 事件可以在每次网络请求得到响应的时候触发,我们可以设置对应的回调方法获取到对应 Response 的全部信息,示例如下:

from playwright.sync_api import sync_playwright

def on_response(response):
    print(f'Statue {response.status}: {response.url}')

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.on('response', on_response)
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    browser.close()

这里我们在创建 Page 对象之后,就开始监听 response 事件,同时将回调方法设置为 on_response,on_response 对象接收一个参数,然后把 Response 的状态码和链接都输出出来了。输出结果其实正好对应浏览器 Network 面板中所有的请求和响应内容

如果数据都是 Ajax 加载的,同时 Ajax 请求中还带有加密参数,不好轻易获取。有了这个方法,这里如果我们想要截获 Ajax 请求,岂不是就非常容易了?

改写一下判定条件,输出对应的 JSON 结果,改写如下:

from playwright.sync_api import sync_playwright

def on_response(response):
    if '/api/movie/' in response.url and response.status == 200:
        print(response.json())

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.on('response', on_response)
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    browser.close()

获取页面源码

要获取页面的 HTML 代码其实很简单,我们直接通过 content 方法获取即可,用法如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    html = page.content()
    print(html)
    browser.close()

页面点击

页面点击的 API 定义如下:page.click(selector, **kwargs)

这里可以看到必传的参数是 selector,其他的参数都是可选的。第一个 selector 就代表选择器,可以用来匹配想要点击的节点,如果传入的选择器匹配了多个节点,那么只会用第一个节点。

这个方法的内部执行逻辑如下:

  • 根据 selector 找到匹配的节点,如果没有找到,那就一直等待直到超时,超时时间可以由额外的 timeout 参数设置,默认是 30 秒。
  • 等待对该节点的可操作性检查的结果,比如说如果某个按钮设置了不可点击,那它会等待该按钮变成了可点击的时候才去点击,除非通过 force 参数设置跳过可操作性检查步骤强制点击。
  • 如果需要的话,就滚动下页面,将需要被点击的节点呈现出来。
  • 调用 page 对象的 mouse 方法,点击节点中心的位置,如果指定了 position 参数,那就点击指定的位置。

click 方法的一些比较重要的参数如下:

  • click_count:点击次数,默认为 1。
  • timeout:等待要点击的节点的超时时间,默认是 30 秒。
  • position:需要传入一个字典,带有 x 和 y 属性,代表点击位置相对节点左上角的偏移位置。
  • force:即使不可点击,那也强制点击。默认是 False。

具体的 API 设置参数可以参考官方文档:https://playwright.dev/python/docs/api/class-page#page-click

文本输入

文本输入对应的方法是 fill,API 定义如下:page.fill(selector, value, **kwargs)

这个方法有两个必传参数,第一个参数也是 selector,第二个参数是 value,代表输入的内容,另外还可以通过 timeout 参数指定对应节点的最长等待时间。

获取节点属性

除了对节点进行操作,我们还可以获取节点的属性,方法就是 get_attribute,API 定义如下:

page.get_attribute(selector, name, **kwargs)

这个方法有两个必传参数,第一个参数也是 selector,第二个参数是 name,代表要获取的属性名称,另外还可以通过 timeout 参数指定对应节点的最长等待时间。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    href = page.get_attribute('a.name', 'href')
    print(href)
    browser.close()

这里我们调用了 get_attribute 方法,传入的 selector 是 a.name,选定了 class 为 name 的 a 节点,然后第二个参数传入了 href,获取超链接的内容,

可以看到对应 href 属性就获取出来了,但这里只有一条结果,因为这里有个条件,那就是如果传入的选择器匹配了多个节点,那么只会用第一个节点。

那怎么获取所有的节点呢?

获取多个节点

获取所有节点可以使用 query_selector_all 方法,它可以返回节点列表,通过遍历获取到单个节点之后,我们可以接着调用单个节点的方法来进行一些操作和属性获取,示例如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    elements = page.query_selector_all('a.name')
    for element in elements:
        print(element.get_attribute('href'))
        print(element.text_content())
    browser.close()

这里我们通过 query_selector_all 方法获取了所有匹配到的节点,每个节点对应的是一个 ElementHandle 对象,然后 ElementHandle 对象也有 get_attribute 方法来获取节点属性,另外还可以通过 text_content 方法获取节点文本。

获取单个节点

获取单个节点也有特定的方法,就是 query_selector,如果传入的选择器匹配到多个节点,那它只会返回第一个节点,示例如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    element = page.query_selector('a.name')
    print(element.get_attribute('href'))
    print(element.text_content())
    browser.close()

网络劫持

最后再介绍一个实用的方法 route,利用 route 方法,我们可以实现一些网络劫持和修改操作,比如修改 request 的属性,修改 response 响应结果等。

from playwright.sync_api import sync_playwright
import re

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    def cancel_request(route, request):
        route.abort()

    page.route(re.compile(r"(\.png)|(\.jpg)"), cancel_request)
    page.goto("https://spa6.scrape.center/")
    page.wait_for_load_state('networkidle')
    page.screenshot(path='no_picture.png')
    browser.close()

这里我们调用了 route 方法,第一个参数通过正则表达式传入了匹配的 URL 路径,这里代表的是任何包含 .png 或 .jpg 的链接,遇到这样的请求,会回调 cancel_request 方法处理,cancel_request 方法可以接收两个参数,一个是 route,代表一个 CallableRoute 对象,另外一个是 request,代表 Request 对象。这里我们直接调用了 route 的 abort 方法,取消了这次请求,所以最终导致的结果就是图片的加载全部取消了。

这个设置有什么用呢?其实是有用的,因为图片资源都是二进制文件,而我们在做爬取过程中可能并不想关心其具体的二进制文件的内容,可能只关心图片的 URL 是什么,所以在浏览器中是否把图片加载出来就不重要了。所以如此设置之后,我们可以提高整个页面的加载速度,提高爬取效率。

另外,利用这个功能,我们还可以将一些响应内容进行修改,比如直接修改 Response 的结果为自定义的文本文件内容。

首先这里定义一个 HTML 文本文件,命名为 custom_response.html,内容如下:

<!DOCTYPE html>
<html>
  <head>
    <title>Hack Response</title>
  </head>
  <body>
    <h1>Hack Response</h1>
  </body>
</html>

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    def modify_response(route, request):
        route.fulfill(path="./custom_response.html")

    page.route('/', modify_response)
    page.goto("https://spa6.scrape.center/")
    browser.close()

这里我们使用 route 的 fulfill 方法指定了一个本地文件,就是上面定义的 HTML 文件

执行程序可以看到,Response 的运行结果就被我们修改了,URL 还是不变的,但是结果已经成了我们修改的 HTML 代码。所以通过 route 方法,我们可以灵活地控制请求和响应的内容,从而在某些场景下达成某些目的。

DrissionPage

github:https://github.com/g1879/DrissionPage

官网:http://g1879.gitee.io/drissionpagedocs/

基于 python 的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。

背景

用 requests 做数据采集面对要登录的网站时,要分析数据包、JS 源码,构造复杂的请求,往往还要应付验证码、JS 混淆、签名参数等反爬手段,门槛较高,开发效率不高。 使用浏览器,可以很大程度上绕过这些坑,但浏览器运行效率不高。

因此,这个库设计初衷,是将它们合而为一,同时实现“写得快”和“跑得快”。能够在不同须要时切换相应模式,并提供一种人性化的使用方法,提高开发和运行效率。
除了合并两者,本库还以网页为单位封装了常用功能,提供非常简便的操作和语句,使用户可减少考虑细节,专注功能实现。 以简单的方式实现强大的功能,使代码更优雅。

入门指南

http://g1879.gitee.io/drissionpagedocs/get_start/installation_and_import/

使用文档

http://g1879.gitee.io/drissionpagedocs/usage_introduction/


 

helium

Helium :https://github.com/mherrmann/helium

Helium 是一个 Python 库,用于自动化浏览器,例如 Chrome 和 Firefox

Helium 是一款 Web 端自动化开源框架,全称是:Selenium-Python-Helium,从名字上就可以看出,Helium 似乎和 Selenium 息息相关

确实,Helium 针对 Selenium 进行了封装,它屏蔽了 Selenium 很多实现细节,提供了更加简洁直观的 API,更方便我们进行 Web 端的自动化

官方表示,要实现同样的功能,Helium 相比 Selenium 要少 30% - 50% 的代码

目前,Helium 仅支持 Chrome 和 FireFox

2.优缺点

Helium 主要包含下面 6 个优点:

  • Helium 自带 WebDriver,不需要下载、配置浏览器驱动
  • 内嵌页面 iframe 页面元素直接操作,不需要使用 switch_to.frame() 切换 iframe
  • 窗体管理更方便,可以直接使用窗口标题或部分标题内容来切换窗体
  • 隐式等待,针对某个元素执行点击操作,Selenium 如果元素没有出现,脚本会执行失败;而 Helium 默认最多等待 10s,等待元素出现后立马执行点击操作
  • 显式等待,Helium 提供更加优雅的 API 来等待页面元素出现
  • API 更简洁直观,代码量少

Helium 主要缺点,体现在:

  • 由于封装,屏蔽了很多细节,所以它不合适二次开发
  • 目前仅支持 Chrome 和 FireFox 浏览器
  • 版本更新慢、遗留 Bug 及文档少

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值