最近需要每天填写一个表单,表单很复杂但是每天填的内容没有变化,而且忘了填或者填错会被卷……之前在学校填类似的表格的时候直接用的是直接从浏览器导出的curl脚本,因为学校的数据收集系统是自建的,所以很好糊弄。但是单位没能力自建这一套系统,所以用的是商业化的数据收集系统,看了一下POST的数据看不懂,好像还有一个sha256,这就没法用简单的curl糊弄的了……

目前看来有两个思路,一个是仔细看一下前端的js代码,研究一下其提交的逻辑,然后用requests模拟请求,但这样太过麻烦,而且那些js文件是经过压缩后的,根本不是人读的东西。最后决定采取最终的解决手段,使用Selenium与一个无头的浏览器来彻底解决问题。

Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。测试系统功能——创建回归测试检验软件功能和用户需求。

Selenium是用来进行Web应用程序测试的,但并不妨碍我们把它作为一个简单的自动化浏览器工具。

安装与配置

Selenium安装很简单,直接在pip3 install selenium即可。但是有了selenium还不够,还需要一个可以正常使用的浏览器和相应的驱动程序。

Selenium支持多个操作系统上的多种主流的浏览器,包括Chrome(Chromium)FirefoxEdge等,毕竟作为一个专门的web测试工具需要尽量模拟真实的用户操作。

有了浏览器,还需要专门的与浏览器相应的驱动程序(webdriver)。与chrome相对应的webdriver是chromedriver,与firefox对应的是geckodriver。具体选择哪一种浏览器需要结合实际情况,需要注意的是chromium(chrome)的webdriver的版本号需要与浏览器的版本号一致才能工作,否则会出现各种奇奇怪怪的问题。由于我的chromium是用snap安装的,会自动更新,所以用Chromedriver可能会比较麻烦,所以最后采用firefox方案。

具体的配置webdriver很简单,去对应的网站下载对应平台的对应二进制文件,然后放在系统的可执行文件的搜索路径($PATH)即可。

使用方法

先是引入必要的模块

from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.select import Select

启动浏览器,其他浏览器的启动方式也差不多

d = webdriver.Firefox()

这个时候应该会弹出一个火狐浏览器窗口

浏览器提示已被远程控制
浏览器提示已被远程控制

然后打开需要填写的表单页面

d.get("https://jinshuju.net/f/bPYGhY")

相应页面已被打开
相应页面已被打开

利用selenium的页面元素选择器来找到相应的输入框

i3 = d.find_element_by_name("field_7")
i3.send_keys("12345677654")

其中相应输入框的定位方法可以用浏览器的开发工具来寻找,找到对应的输入框之后使用send_keys()来模拟输入字符串

selenium的选择元素的简单方法单数形式的只返回第一个匹配项,而复数形式的会返回一个列表。

find_element_by_id()
find_element_by_name()
find_element_by_class_name()
find_element_by_tag_name()
find_element_by_link_text()
find_element_by_partial_link_text()
find_element_by_xpath()
find_element_by_css_selector()

find_elements_by_id()
find_elements_by_name()
find_elements_by_class_name()
find_elements_by_tag_name()
find_elements_by_link_text()
find_elements_by_partial_link_text()
find_elements_by_xpath()
find_elements_by_css_selector()

对于选择框,可以在找到对应元素之后使用click()方法来模拟点击

s8 = d.find_element_by_name("field_90")
s8.click()
s9 = d.find_elements_by_name("field_91")
s9[1].click()

对于行政区划选择框,则可以通过遍历所有选项,匹配正确的选项的方式来模拟点击

selects = d.find_elements_by_class_name("ant-cascader-picker-label")
selects[0].text
selects[0].click()
selects = d.find_elements_by_class_name("ant-cascader-menu-item-expand")
for ls in selects:
    print(ls.text)
    if ls.text == "山东省":
        ls.click()
        break
selects = d.find_elements_by_class_name("ant-cascader-menu-item-expand")
for ls in selects:
    print(ls.text)
    if ls.text == "济南市":
        ls.click()
        break
selects = d.find_elements_by_class_name("ant-cascader-menu-item")
for ls in selects:
    print(ls.text)
    if ls.text == "长清区":
        ls.click()
        break

最后找到提交按钮,模拟点击并提交,提交之后等几秒钟截图以查看结果并固定证据。

s = driver.find_element_by_css_selector("button.ant-btn")
s.click()
time.sleep(3)
print("填报完成,开始截图")
d.save_screenshot("r.png")

成功后保存的截图
成功后保存的截图

cookie处理

以自动化方式启动的浏览器默认是不保留cookie等有效信息的,每次启动都是一个全新的浏览器,如果需要登录并持久化,可以通过每次启动时指定一个个人数据保存目录的方式。或者是直接在浏览器关闭前导出cookie并保存,在下次打开浏览器时加载的方式来实现登录状态持久化。

导出cookie到文件

l = open("ck.txt","w")
ck = d.get_cookies()
st=json.dumps(ck)
l.write(st)
l.close()

从文件导入cookie

d.get("https://jinshuju.net")
cck=open("ck.txt","r")
llo=cck.read()
ckk=json.loads(llo)
for losdog in ckk:
    d.add_cookie(losdog)
cck.close()

注意:由于cookie的同源要求,浏览器只能在cookie内的url与地址栏一致时才接受导入,所以在刚打开浏览器显示空白页时并不能导入,需要先访问目标页面或者与目标页面的关联页面后再导入cookie。

服务器端部署与无头浏览器

以上的操作全部都在一个有图形界面的ubuntu上进行的,但最终的目标应该是在一个没有图形界面的服务器上运行这个自动化脚本,这就需要浏览器以无头(headless)方式运行,这就需要在脚本启动时将无头参数传递给浏览器。

options = Options()
options.add_argument('-headless')
d = webdriver.Firefox(firefox_options=options)

脚本安排好了之后,可以安排上定时任务周期性执行,在使用crontab等定时工具时要注意,定时任务的PATH可能与shell中的并不一致,尤其是Selenium需要到PATH路径中找webdriver,而webdriver也要到PATH路径中找浏览器,务必确保定时脚本执行环境中的PATH包含相应的二进制文件或者其链接。

完整代码

from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.select import Select
import time
import json


options = Options()
options.add_argument('-headless')
d = webdriver.Firefox(options=options)
print("开始自动化填报")
d.get("https://jinshuju.net")
cck=open("ck.txt","r")
llo=cck.read()
ckk=json.loads(llo)
for losdog in ckk:
    d.add_cookie(losdog)
cck.close()

d.implicitly_wait(20)
d.get("https://jinshuju.net/home")
time.sleep(5)
print(d.title)
driver = d
if d.current_url !=  "https://jinshuju.net/home":
    print("需要登录")
    inputter = d.find_element_by_id('auth_key')
    inputter.send_keys("neko@moe.neko.red")
    button = driver.find_element_by_name('button')
    time.sleep(2)
    button.click()
    inputter = driver.find_element_by_id('password')
    inputter.send_keys("password")
    button = driver.find_element_by_name('button')
    time.sleep(2)
    button.click()
    time.sleep(2)
else:
    print("无需登录")

d.get("https://jinshuju.net/f/bPYGhY")
time.sleep(10)
s1 = d.find_element_by_name("field_38")
s1.click()
i2 = d.find_element_by_name("field_1")
i2.send_keys("dogcraft")
i3 = d.find_element_by_name("field_7")
i3.send_keys("053188395114")
s4 = d.find_element_by_name("field_88")
s4.click()
selects = driver.find_elements_by_css_selector("div.pretty-select")
a = selects[0]
a.click()
d.find_element_by_id("react-select-2-option-7").click()
b = selects[1]
b.click()
d.find_element_by_id("react-select-3-option-1").click()
i5 = d.find_element_by_name("field_89")
i5.send_keys("201605100109")
s6 = d.find_element_by_xpath(
    "/html/body/div[2]/div[3]/div/form/div[3]/div[1]/div[16]/div/div[2]/div/div/span/div/div[3]")
s6.click()
i7 = d.find_element_by_name("field_100")
i7.send_keys("370902199803170426")
s8 = d.find_element_by_name("field_90")
s8.click()
s9 = d.find_elements_by_name("field_91")
s9[1].click()
selects = driver.find_elements_by_class_name("ant-cascader-picker-label")
selects[0].text
selects[0].click()
selects = driver.find_elements_by_class_name("ant-cascader-menu-item-expand")
for ls in selects:
    print(ls.text)
    if ls.text == "山东省":
        ls.click()
        break
selects = driver.find_elements_by_class_name("ant-cascader-menu-item-expand")
for ls in selects:
    print(ls.text)
    if ls.text == "济南市":
        ls.click()
        break
selects = driver.find_elements_by_class_name("ant-cascader-menu-item")
for ls in selects:
    print(ls.text)
    if ls.text == "历城区":
        ls.click()
        break
i11 = d.find_element_by_class_name("address-input")
i11.send_keys("山大南路27号")
s12 = d.find_elements_by_name("field_97")
s12[2].click()
s13 = d.find_elements_by_name("field_98")
s13[0].click()
s14 = d.find_elements_by_name("field_99")
s14[0].click()
s15 = d.find_elements_by_name("field_5")
s15[0].click()
s16 = d.find_elements_by_name("field_75")
s16[2].click()
s17 = d.find_elements_by_name("field_76")
s17[0].click()
d.save_screenshot("p.png")
s = driver.find_element_by_css_selector("button.ant-btn")
s.click()
time.sleep(3)
print("填报完成,开始截图")
d.save_screenshot("r.png")
l = open("ck.txt","w")
ck = d.get_cookies()
st=json.dumps(ck)
l.write(st)
l.close()
d.quit()