selenium 学习、工作 记录,附常见异常和工具方法

  • 基础配置
  • 2020.03.31
    • 开发者模式
    • 设置有认证的http代理
    • 让selenium接管人为打开的chrome
  • 2020.07.23(79+版本消除webdriver特征)
  • 2020.08.12(增加chromeDriver参数,解决大部分特征识别,看代码里的注释。。)
  • 2020.08.13(火狐参数)
  • 2020.08.17
    • 常见异常
    • 常见方法(工具方法)
  • 2020.09.17
  • 2020.09.21 (记一次火狐主页绑定 ~ 因为360)
  • 2020.09.22 (记一次火狐记住密码不自动填写)
  • 2020.09.27(记如何拦截selenium请求)
  • 2020.11.02(记连接远程selenium)
  • 2021.04.12(记selenium跨域问题)
  • 2021.04.26(记selenium的sessionId能干的事)
    • 退出 相当于quit()
    • 创建,打开浏览器
    • 获取元素
    • 获取元素文本
    • 根据sessionId接管已经被selenium打开的浏览器 java版本(包你满意)
  • 2022-03-04(新增浏览器安全参数)

基础配置

说一下chrome和firefox的基础配置,首先

chromedriver和chrome的版本对应关系看 :https://www.cnblogs.com/reform999/p/10685946.html

firefox和geckodriver的对应关系: https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html

chromedriver的下载地址在: http://npm.taobao.org/mirrors/chromedriver/

geckodriver的下载地址是: https://github.com/mozilla/geckodriver/releases

chrome和火狐的下载地址就不说了,百度一下,官网那个就是。

下载对应版本的驱动和浏览器后:

java:

去 maven仓库 里找对应版本的依赖,记住千万别忘了 guava 这个依赖。不然你可能会经历 找不到方法,找不到类,等等一系列各种各样奇怪的问题。

配置好selenium的依赖和驱动以及浏览器(浏览器的安装地址建议不要变动,让它自己安装就行了)后,设置系统变量,也就是把程序要的东西(驱动的地址),放到系统变量里,它会从系统变量里取。

//chrome:
System.setProperty("webdriver.chrome.driver", "chromedriver.exe的路径");
//firefox:
System.setProperty("webdriver.gecko.driver", "geckodriver.exe的路径");
//如果浏览器安装地址变了
//chrome:
System.setProperty("webdriver.chrome.bin","chrome安装地址,精确到chrome.exe");
//firefox:
System.setProperty("webdriver.firefox.bin","firefox安装地址,精确到firefox.exe")

这段代码在new WebDriver之前执行,现在可以试试:

public static void main(String[] args) {System.setProperty("webdriver.chrome.driver", "chromedriver.exe的路径");ChromeDriver driver = null;try{driver = new ChromeDriver();driver.get("http://www.baidu.com");((JavascriptExecutor) driver).executeScript("document.body.style.fontSize = '30px';document.body.innerHTML='<p1>hello world~<br/><span id=\"close\">5</span>秒后页面关闭</p1>';setInterval(()=>{let time=document.getElementById('close');time.innerText=(time.innerText-1)},1000);");Thread.sleep(5000L);}catch(Exception e){e.printStackTrace();}finally{//养成良好的习惯,用完后quit掉。if(null!=driver){driver.quit();}}}

python:

pip install selenium
然后就能用了(驱动和浏览器还是要下载的)。。

if __name__ == '__main__':driver = Nonetry:driver = webdriver.Chrome(executable_path='chromedriver.exe的路径')driver.get("http://www.baidu.com")driver.execute_script("document.body.style.fontSize = '30px';document.body.innerHTML='<p1>hello world~<br/><span id=\"close\">5</span>秒后页面关闭</p1>';setInterval(()=>{let time=document.getElementById('close');time.innerText=(time.innerText-1)},1000);")time.sleep(5)except Exception as e:print(e)finally:# 养成好习惯,用完后quit掉driver.quit()

2020.03.31

开发者模式

该模式在chrome老版本可用,79+版本navigator.webdriver仍为true。(新版本消除navigator.webdriver方法参考目录上的2020.07.23(79+版本消除webdriver特征)

ChromeOptions options = new ChromeOptions();
options.setExperimentalOption("excludeSwitches", Collections.singletonList("enable-automation"));
WebDriver driver = new ChromeDriver(options);

设置有认证的http代理

目的是让chrome打开时加载浏览器插件,所以要先生成浏览器插件。(Proxy是封装的一个类,只需要有ip,端口,用户名和密码就行了)
由于没有sock5代理,sock5的认证代理没法测试,这里用的是http代理,公司亲测此种方法可行,用了2年多,平时也在用。

private static ChromeOptions setProxyChrome(Proxy proxy) throws Exception{ChromeOptions options = new ChromeOptions();if(null==proxy||StringUtils.isEmpty(proxy.getIp())){return options;}FileWriter fileWriter1 = null;FileWriter fileWriter2 = null;String driverPath = getDriverPath();String jsonFilePath = driverPath + "manifest.json";String jsFilePath = driverPath + "background.js";String zipFilePath = driverPath + "proxy.zip";File jsonFile = new File(jsonFilePath);File zipFile = new File(zipFilePath);File jsFile = new File(jsFilePath);try {String js = "var config={mode:\"fixed_servers\",rules:{singleProxy:{scheme:\"http\",host:\"" + proxy.getIp() + "\",port:" + proxy.getPort() + "},bypassList:[\"\"]}};chrome.proxy.settings.set({value:config,scope:\"regular\"},function(){});function callbackFn(details){return{authCredentials:{username:\"" + proxy.getProxyUser() + "\",password:\"" + proxy.getProxyPass() + "\"}};}chrome.webRequest.onAuthRequired.addListener(callbackFn,{urls:[\"<all_urls>\"]},['blocking']);";String json = "{\"version\":\"1.0.0\",\"manifest_version\":2,\"name\":\"Chrome Proxy\",\"permissions\":[\"proxy\",\"tabs\",\"unlimitedStorage\",\"storage\",\"<all_urls>\",\"webRequest\",\"webRequestBlocking\"],\"background\":{\"scripts\":[\"background.js\"]},\"minimum_chrome_version\":\"22.0.0\"}";if(!jsFile.exists()){jsFile.createNewFile();}if(!jsonFile.exists()){jsonFile.createNewFile();}fileWriter1 = new FileWriter(jsFilePath);fileWriter2 = new FileWriter(jsonFilePath);fileWriter1.write(js);fileWriter2.write(json);}catch (Exception e){e.printStackTrace();}finally {try{if(null!=fileWriter1){fileWriter1.flush();fileWriter1.close();}if(null!=fileWriter2){fileWriter2.flush();fileWriter2.close();}}catch (Exception e){e.printStackTrace();}}try {File[] files = new File[]{jsFile, jsonFile};zipFile(files, zipFile);options.addExtensions(zipFile);}catch (Exception e){e.printStackTrace();}return options;}private static void zipFile(File[] srcFiles, File zipFile){if (!zipFile.exists()) {try {zipFile.createNewFile();} catch (IOException e) {e.printStackTrace();}}FileOutputStream fileOutputStream = null;ZipOutputStream zipOutputStream = null;FileInputStream fileInputStream = null;try {fileOutputStream = new FileOutputStream(zipFile);zipOutputStream = new ZipOutputStream(fileOutputStream);ZipEntry zipEntry = null;for (int i = 0; i < srcFiles.length; i++) {fileInputStream = new FileInputStream(srcFiles[i]);zipEntry = new ZipEntry(srcFiles[i].getName());zipOutputStream.putNextEntry(zipEntry);int len;byte[] buffer = new byte[1024];while ((len = fileInputStream.read(buffer)) > 0) {zipOutputStream.write(buffer, 0, len);}}} catch (IOException e) {e.printStackTrace();}finally {try{if(null!=zipOutputStream){zipOutputStream.close();}if(null!=fileOutputStream){fileOutputStream.close();}if(null!=fileInputStream){fileInputStream.close();}}catch (Exception e){e.printStackTrace();}}}

让selenium接管人为打开的chrome

接管selenium打开的浏览器看这里

这里参考了python控制已经打开chrome的做法,具体做法参考:
https://www.cnblogs.com/lovealways/p/9813059.html
java代码:

     ChromeOptions options = new ChromeOptions();options.setExperimentalOption("debuggerAddress","127.0.0.1:9222");ChromeDriver driver = new ChromeDriver(options);

2020.07.23(79+版本消除webdriver特征)

更新,开发者模式在79版本以前能用,详情点击这里,参考这个问答,79版本以后消除navigator.webdriver使用

ChromeOptions options = new ChromeOptions();
options.addArguments("--disable-blink-features=AutomationControlled");
WebDriver driver = new ChromeDriver(options);

2020.08.12(增加chromeDriver参数,解决大部分特征识别,看代码里的注释。。)

这里再增加几个常用的参数
关于个人用户文件夹地址:
chrome:地址栏输入 chrome://version/ 个人资料路径 最后一个文件夹之前的就是。
firefox:地址栏输入 about:profiles 正在使用的那个就是 一般默认default-release

java:

//下载配置
HashMap<String, Object> chromePrefs = new HashMap<>();
chromePrefs.put("download.default_directory", "文件下载路径");
chromePrefs.put("profile.default_content_settings.popups", 0);//下载不弹框
chromePrefs.put("profile.default_content_setting_values.images", 2);//禁止显示图片
chromePrefs.put("browser.download.useDownloadDir", true);//使用用户下载文件夹
chromePrefs.put("profile.default_content_setting_values.automatic_downloads",1);//多文件下载总是允许
chromePrefs.put("profile.default_content_setting_values.media_stream_mic",1);//允许麦克风
chromePrefs.put("profile.default_content_setting_values.media_stream_camera",1);//允许摄像头
//消除特征配置
ChromeOptions options = new ChromeOptions();
options.addArguments("--disable-blink-features=AutomationControlled");
//使用平时打开的用户数据文件夹打开chrome(这样有些网站不用登录,有些网站会记录密码,而且配合消除特性的参数,大概率不会触发滑块)
options.addArguments("--user-data-dir=C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\User Data");
options.setExperimentalOption("prefs", chromePrefs);

python:

options = webdriver.ChromeOptions()
# 下载配置
prefs = {"download.default_directory": "文件下载路径","profile.default_content_settings.popups": 0,"profile.default_content_setting_values.images": 2,"browser.download.useDownloadDir": true,"profile.default_content_setting_values.automatic_downloads": 1,"profile.default_content_setting_values.media_stream_mic": 1,"profile.default_content_setting_values.media_stream_camera": 1
}
options.add_experimental_option("prefs", prefs)
# 设置用户数据文件夹 chrome打开时加载的文件夹
options.add_argument("--user-data-dir=C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\User Data")
# 消除特征
options.add_argument("--disable-blink-features=AutomationControlled")
driver = webdriver.Chrome(chrome_options = options)

2020.08.13(火狐参数)

增加几个火狐的参数(我已经弃用火狐,限制太多了):

final FirefoxOptions fopts = new FirefoxOptions();
ProfilesIni allProfiles = new ProfilesIni();
/**
* 获取默认文件夹的方法:
* 打开常用的火狐,地址栏输入about:profiles ,回车 ,找到正在使用的配置文件 后面的default,default-release就是用户配置文件夹
*/
FirefoxProfile firefoxProfile = allProfiles.getProfile("用户文件夹");//默认default
firefoxProfile.setPreference("dom.webdriver.enabled", false);//消除navigator.webdriver
firefoxProfile.setPreference("browser.download.folderList", 2);//下载到用户配置文件夹
firefoxProfile.setPreference("browser.download.manager.showWhenStarting",false);//下载是否弹框
firefoxProfile.setPreference("browser.download.dir", "下载文件夹路径");
firefoxProfile.setPreference("permissions.default.image", 2);//禁用图片
fopts.setProfile(firefoxProfile);
WebDriver driver = new FirefoxDriver(fopts);

以下内容来自 https://blog.csdn.net/qq_37931379/article/details/104595450 (仅供参考)

 FirefoxOptions options = new FirefoxOptions();//声明一个profile对象FirefoxProfile profile = new FirefoxProfile();//设置Firefox的“broswer.download.folderList”属性为2/*** 如果没有进行设定,则使用默认值 1,表示下载文件保存在“下载”文件夹中* 设定为0,则下载文件会被保存在用户的桌面上* 设定为2,则下载的文件会被保存的用户指定的文件夹中*/profile.setPreference("browser.download.folderList", 2);//browser.download.manager.showWhenStarting的属性默认值为true//设定为 true , 则在用户启动下载时显示Firefox浏览器的文件下载窗口//设定为false,则在用户启动下载时不显示Firefox浏览器的文件下载窗口profile.setPreference("browser.download.manager.showWhenStarting", false);//设定文件保存的目录profile.setPreference("browser.download.dir", downloadFilePath);//browser.helperApps.neverAsk.openFile 表示直接打开下载文件,不显示确认框//默认值.exe类型的文件,"application/excel"表示Excel类型的文件// application/x-msdownloadprofile.setPreference("browser.helperApps.neverAsk.openFile", "application/x-msdownload");//browser.helperApps.never.saveToDisk 设置是否直接保存 下载文件到磁盘中默认值为空字符串,厦航代码行设定了多种温江的MIME类型profile.setPreference("browser.helperApps.neverAsk.saveToDisk", "application/x-msdownload");//browser.helperApps.alwaysAsk.force 针对位置的MIME类型文件会弹出窗口让用户处理,默认值为true ,设定为false 表示不会记录打开未知MIME类型文件profile.setPreference("browser.helperApps.alwaysAsk.force", true);//下载.exe文件弹出窗口警告,默认值是true ,设定为false 则不会弹出警告框profile.setPreference("browser.download.manager.alertOnEXEOpen", false);//browser.download.manager.focusWhenStarting设定下载框在下载时会获取焦点profile.setPreference("browser.download.manager.focusWhenStarting", true);//browser.download.manager.useWindow 设定下载是否现在下载框,默认值为true,设定为false 会把下载框隐藏profile.setPreference("browser.download.manager.useWindow", false);//browser.download.manager.showAlertOnComplete 设定下载文件结束后是否显示下载完成的提示框,默认值为true,//设定为false表示下载完成后,现在下载完成提示框profile.setPreference("browser.download.manager.showAlertOnComplete", false);//browser.download.manager.closeWhenDone 设定下载结束后是否自动关闭下载框,默认值为true 设定为false 表示不关闭下载管理器profile.setPreference("browser.download.manager.closeWhenDone", false);

增加参考文章:https://www.cnblogs.com/wpjamer/articles/3873769.html

2020.08.17

常见异常

下面记录几个java关于selenium的异常:


/**未配置系统变量,即chromedriver.exe的位置* 配置方法如下:(最上面有详细的说明)* chrome:* System.setProperty("webdriver.chrome.driver", "chromedriver.exe的位置");* firefox:* System.setProperty("webdriver.gecko.driver", "geckodriver.exe的位置");* phantomjs:* System.setProperty("phantomjs.binary.path", "phantomjs.exe的位置");*/
java.lang.IllegalStateException: The path to the driver executable must be set by the webdriver.chrome.driver system property;/*** 这个出现的情况有很多,举个例子,如果你使用driver.get("www.baidu.com");就会出现这个报错,因为这个地址不对,要使用https://www.baidu.com这个完整路径。*/
org.openqa.selenium.InvalidArgumentException: invalid argument/*** 这个是比较常见的错误,比如打开百度首页后,直接* driver.findElements(By.id("testest"));* 就会报这个错误,因为页面上没有这个元素。* 首先反思一下选择器有没有写错,浏览器console先试试能不能拿到对应的元素,浏览器能拿到,但是selenium拿不到的话,* 看一下是不是元素在iframe内,在iframe的情况,selenium是要切换句柄的。* 然后看一下是不是页面或者元素需要某个操作(比如点击)才会出现,或者你刚点击,元素还没加载出来就开始获取了。* 还有  不要直接获取这个元素。* 正确的做法是:先判断有没有这个元素,如果没有,执行没有的逻辑(等待,刷新,或者抛出异常等等)* 一般来说,在获取这个元素之前,都会先用* new WebDriverWait(driver, 5).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(cssSelector)));* 这个方法的意思是5秒内元素不出来就不往下走,5秒后还没出来就抛出异常*/
org.openqa.selenium.NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"xxx"}/*** 要点击或者操作的元素已经不在当前页面的环境内了,例如打开一个网站后,获取了一个select下的option列表* 循环点击这个option列表,但是每点击一次,页面就会跳转到新的页面,那么在第二次循环的时候就会报这个异常,也就是说,你要操作的元素是你上一个页面的(或者已经消失的)元素,你在这个页面想点击一个不存在,但是你恰好还有WebElement实例的元素,就会报这个错。*/
org.openqa.selenium.StaleElementReferenceException: stale element reference: element is not attached to the page document/*** 当前元素不可交互,比如说,点击的元素不在页面可见范围内,如果是点击操作,可能点击上方还有别的元素挡住        * 了,也可能是当前元素不可点击。或者给一个disabled属性的input进行sendKeys操作。* 一般来说,点击都是通过执行js来实现的,但是执行js不一定能真正点击(具体原因还不清楚),sendKeys有时候会比执行js来的好用。*/
org.openqa.selenium.ElementNotInteractableException: element not interactable/*** 类似这种报错,有几种情况,用户文件夹被占用,chrome版本和chromedriver版本对不上*/
org.openqa.selenium.InvalidArgumentException: invalid argument: user data directory is already in use, please specify a unique value for --user-data-dir argument, or don't use --user-data-dir
remote stacktrace: Backtrace:Ordinal0 [0x007FC013+3194899]Ordinal0 [0x006E6021+2056225]Ordinal0 [0x0057F608+587272]Ordinal0 [0x004FF28A+62090]Ordinal0 [0x004FC64A+50762]Ordinal0 [0x00521EE9+204521]Ordinal0 [0x00521D0D+204045]Ordinal0 [0x0051FC1B+195611]Ordinal0 [0x00503B7F+80767]Ordinal0 [0x00504B4E+84814]Ordinal0 [0x00504AD9+84697]Ordinal0 [0x006FCE64+2149988]GetHandleVerifier [0x0096BE95+1400773]GetHandleVerifier [0x0096BB61+1399953]GetHandleVerifier [0x009731FA+1430314]GetHandleVerifier [0x0096C69F+1402831]Ordinal0 [0x006F3D61+2112865]Ordinal0 [0x006FE5CB+2155979]Ordinal0 [0x006FE6F5+2156277]Ordinal0 [0x0070F26E+2224750]BaseThreadInitThunk [0x76A56359+25]RtlGetAppContainerNamedObjectPath [0x77127C24+228]RtlGetAppContainerNamedObjectPath [0x77127BF4+180]

常见方法(工具方法)

划到页面底部

System.setProperty("webdriver.chrome.driver", "E://drivers//chromedriver.exe");
WebDriver driver = null;
try {driver = new ChromeDriver();driver.get("https://blog.csdn.net/weixin_42525191/article/details/105216417");Actions actions = new Actions(driver);Thread.sleep(2000);//看效果记得打断点。actions.sendKeys(Keys.END).perform(); //方法一Thread.sleep(2000);((JavascriptExecutor) driver).executeScript("window.scrollTo(0,document.body.scrollHeight)");//方法二
}catch (Exception e){e.printStackTrace();
}finally {if(null!=driver){driver.quit();}
}

工具方法(java)。使用时,需要用到该工具类的方法直接继承该工具类即可

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.remote.*;
import org.openqa.selenium.remote.http.W3CHttpCommandCodec;
import org.openqa.selenium.remote.http.W3CHttpResponseCodec;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Collections;
import java.util.List;/*** selenium 工具方法*/
public class WebDriverUtils {/*** 等待元素出现时间*/private static final long WAIT_TIME_SECONDS = 5;/*** 延迟按键的基本睡眠时间*/private static final Long SLEEP_TIME = 100L;/*** 延迟按键的随机睡眠时间*/private static final Long SLEEP_RANDOM_TIME = 100L;/*** 用css选择option* @param driver 驱动* @param cssSelector css选择器* @param value 值* @return 返回执行结果*/public static Object setValueByCssSelector(WebDriver driver, String cssSelector, String value) {String varName = "current_ele_" + System.currentTimeMillis();String script = "var " + varName + " = document.querySelector('" + cssSelector + "');" + varName + ".value='"+ value + "';" + varName + ".dispatchEvent(new UIEvent('change'));" + varName+ ".dispatchEvent(new InputEvent('input'));";return ((JavascriptExecutor) driver).executeScript(script);}/*** 获取单个元素* @param driver 元素* @param cssSelector css选择器* @return 返回元素*/public static WebElement getEle(WebDriver driver, String cssSelector){if(null==driver || StringUtils.isEmpty(cssSelector)){throw new RuntimeException("驱动或者css选择为空");}try{new WebDriverWait(driver, WAIT_TIME_SECONDS).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(cssSelector)));return driver.findElement(By.cssSelector(cssSelector));}catch (Exception e){throw new RuntimeException(e);}}/*** 延迟按键* @param driver 驱动* @param cssSelector css选择器* @param keys 待输入文本*/public static void sendKeys(WebDriver driver, String cssSelector, String keys){if(null == driver || StringUtils.isEmpty(cssSelector) || StringUtils.isEmpty(keys)){throw new RuntimeException("驱动或者css选择器,或者待输入值为空");}if(!judgeEle(driver, cssSelector)){throw new RuntimeException("元素不存在");}WebElement ele = getEle(driver, cssSelector);sendKeys(ele, keys);}/*** 延迟按键* @param ele 待输入元素* @param keys 待输入文本*/public static void sendKeys(WebElement ele, String keys){if(null == ele || StringUtils.isEmpty(keys)){throw new RuntimeException("待输入元素为空,或者待输入值为空");}ele.clear();int length = keys.length();for (int i = 0; i < length; i++) {char tempChar = keys.charAt(i);ele.sendKeys(String.valueOf(tempChar));sleep((int)(Math.floor(Math.random()*SLEEP_RANDOM_TIME)+SLEEP_TIME));}}/*** 获取元素内部的元素* @param ele 元素* @param cssSelector css选择器* @return 返回元素*/public static WebElement getEle(WebElement ele, String cssSelector){if(null==ele || StringUtils.isEmpty(cssSelector)){throw new RuntimeException("驱动或者css选择为空");}try{return ele.findElement(By.cssSelector(cssSelector));}catch (Exception e){throw new RuntimeException(e);}}/*** 获取元素列表* @param driver 驱动* @param cssSelector css选择器* @return 返回元素列表*/public static List<WebElement> getElements(WebDriver driver, String cssSelector){if(null==driver || StringUtils.isEmpty(cssSelector)){throw new RuntimeException("驱动或者css选择为空");}try{new WebDriverWait(driver, WAIT_TIME_SECONDS).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(cssSelector)));return driver.findElements(By.cssSelector(cssSelector));}catch (Exception e){throw new RuntimeException(e);}}/*** 获取元素内部的元素列表* @param element 元素* @param cssSelector css选择器* @return 返回元素列表*/public static List<WebElement> getElements(WebElement element, String cssSelector){if(null==element || StringUtils.isEmpty(cssSelector)){throw new RuntimeException("元素或者css选择为空");}try{return element.findElements(By.cssSelector(cssSelector));}catch (Exception e){throw new RuntimeException(e);}}/*** 根据css选择器单击元素* @param driver 驱动* @param cssSelector css选择器* @return 返回是否点击成功*/public static boolean clickEle(WebDriver driver, String cssSelector){if(null==driver || StringUtils.isEmpty(cssSelector)){return false;}try{new WebDriverWait(driver, WAIT_TIME_SECONDS).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(cssSelector)));((JavascriptExecutor)driver).executeScript("arguments[0].click()", driver.findElement(By.cssSelector(cssSelector)));}catch (Exception e){e.printStackTrace();return false;}return true;}/*** 根据元素单击元素* @param driver 驱动* @param ele 元素* @return 返回是否点击成功*/public static boolean clickEle(WebDriver driver, WebElement ele){if(null == driver || null==ele){return false;}try{((JavascriptExecutor)driver).executeScript("arguments[0].click()", ele);}catch (Exception e){e.printStackTrace();return false;}return true;}/*** 根据css选择器双击元素* @param driver 驱动* @param cssSelector css选择器* @return 返回是否点击成功*/public static boolean doubleClickEle(WebDriver driver, String cssSelector){if(null==driver || StringUtils.isEmpty(cssSelector)){return false;}try{new WebDriverWait(driver, WAIT_TIME_SECONDS).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(cssSelector)));Actions actions = new Actions(driver);actions.doubleClick(driver.findElement(By.cssSelector(cssSelector))).perform();}catch (Exception e){e.printStackTrace();return false;}return true;}/*** 根据元素双击元素* @param driver 驱动* @param element 元素* @return 返回是否点击成功*/public static boolean doubleClickEle(WebDriver driver, WebElement element){if(null==driver || null == element){return false;}try{Actions actions = new Actions(driver);actions.doubleClick(element).perform();}catch (Exception e){e.printStackTrace();return false;}return true;}/*** 判断元素是否存在,传驱动* @param driver 驱动* @param cssSelector css选择器* @return 返回是否存在*/public static boolean judgeEle(WebDriver driver, String cssSelector){if(null==driver || StringUtils.isEmpty(cssSelector)){return false;}try{new WebDriverWait(driver, 2*WAIT_TIME_SECONDS).until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(cssSelector)));}catch (Exception e){return false;}return true;}/*** 判断元素内部是否存在某元素* @param ele 元素* @param cssSelector css选择器* @return 返回是否存在*/public static boolean judgeEle(WebElement ele, String cssSelector){if(null==ele || StringUtils.isEmpty(cssSelector)){return false;}try{ele.findElement(By.cssSelector(cssSelector));return true;}catch (Exception e){return false;}}/*** 根据document.readyState 判断页面是否加载完成** @param driver 驱动*/public static boolean judgePageIsComplete(WebDriver driver) {if (null == driver) {throw new RuntimeException("驱动为空");}while (true) {try {String isComplete = ((JavascriptExecutor) driver).executeScript("return document.readyState").toString();if ("complete".equalsIgnoreCase(isComplete)) {return true;}sleep(500L);} catch (Exception e) {e.printStackTrace();return false;}}}/*** 获取input的value* @param ele input* @return input的value属性*/public static String getInputValue(WebElement ele){if(null==ele){return null;}return ele.getAttribute("value");}/*** 网页元素截图* @param driver 驱动* @param element 元素* @return 返回图片base64编码*/public static String shotWebElement(WebDriver driver, WebElement element){if(null==driver||null==element){return null;}Point location = element.getLocation();Dimension size = element.getSize();ByteArrayOutputStream bos = null;String result = "";try {WebDriver augmentedDriver = new Augmenter().augment(driver);byte[] screenshotAs = ((TakesScreenshot) augmentedDriver).getScreenshotAs(OutputType.BYTES);BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(screenshotAs));BufferedImage croppedImage = originalImage.getSubimage(location.getX(), location.getY(), size.getWidth(), size.getHeight());bos = new ByteArrayOutputStream();ImageIO.write(croppedImage, "PNG", bos);result = Base64.encodeBase64String(bos.toByteArray()).replaceAll("\n", "");} catch (Exception e) {throw new RuntimeException(e);} finally {if (null != bos) {try {bos.close();} catch (IOException e) {e.printStackTrace();}}}return result;}/*** 根据sessionId 和 url 获取 driver* @param sessionId sessionId* @param url url* @return driver*/public static RemoteWebDriver createDriverFromSession(final String sessionId, URL url){CommandExecutor executor = new HttpCommandExecutor(url) {@Overridepublic Response execute(Command command) throws IOException {Response response;if ("newSession".equals(command.getName())) {response = new Response();response.setSessionId(sessionId);response.setStatus(0);response.setValue(Collections.<String, String>emptyMap());try {Field commandCodec = this.getClass().getSuperclass().getDeclaredField("commandCodec");commandCodec.setAccessible(true);commandCodec.set(this, new W3CHttpCommandCodec());Field responseCodec = this.getClass().getSuperclass().getDeclaredField("responseCodec");responseCodec.setAccessible(true);responseCodec.set(this, new W3CHttpResponseCodec());} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();}} else {response = super.execute(command);}return response;}};return new RemoteWebDriver(executor, new DesiredCapabilities());}/*** 睡眠* @param time 睡眠时间/ms*/private static void sleep(long time){try {Thread.sleep(time);}catch (Exception e){e.printStackTrace();}}}

2020.09.17

qq群:330405140(selenium学习之路),非教育非推销,单纯的测试技术群(主要是selenium)。
群里有各种 小姐姐 解答问题,当然 小姐姐没看到问题就没办法了 -_-

2020.09.21 (记一次火狐主页绑定 ~ 因为360)

今天碰到一个很恶心的绑定主页,记录一下,火狐浏览器一直打开都很正常,今天打开突然指向了
http://upan.000g.com (会重定向到360主页,别在浏览器打开)
尝试了网上的一些办法,甚至重装了好几次火狐浏览器,结果再打开还是这个主页地址。这里说一下我的解决方法,由于我是安装在虚拟机上的firefox,虚拟机上安装有360,打开360,点击功能大全,找到主页防护,点一下,解锁所有的浏览器主页绑定,然后把浏览器主页设置为你想要的页面即可。

2020.09.22 (记一次火狐记住密码不自动填写)

手动打开火狐 about:preferences 进入隐私与安全,找到 登录信息与密码 除了使用主密码以外全勾上以后,关闭火狐,再打开查看配置,勾被取消了。
如此一来,就算配置用户文件夹 user-data-dir,也没自动填写密码。
我的做法是,打开表单页面,判断是否自动填写,若没有,则进入 about:preferences 页面,点击相应的按钮。java代码如下(参考上面的工具方法):

WebElement email = getEle(driver, "#ap_email");
if(StringUtils.isEmpty(email.getAttribute("value"))){//进入火狐密码设置页面driver.get("about:preferences#privacy");//点击 向您询问是否保存网站的登录名和密码(R)if(judgeEle(driver, "#savePasswords")){clickEle(driver, "#savePasswords");sleep(1000L);}//点击 自动填写登录名和密码(I)if(judgeEle(driver, "#passwordAutofillCheckbox")){clickEle(driver, "#passwordAutofillCheckbox");}//返回表单填写页面driver.get(loginUrl);//判断页面是否加载完if (!judgePageIsComplete(driver)) {throw new RuntimeException("页面加载异常");}
}

注意,勾选之后返回之前的页面,页面是有刷新的,如果要开新窗口设置相关参数,返回原窗口的时候要刷新。

2020.09.27(记如何拦截selenium请求)

在看下面的方法之前,首先思考一下你是否需要这么做

这里先问几个问题。
第一,拦截selenium请求的原因是否是因为页面上的元素不是固定,不能直接通过元素选择器获取,需要解析请求的响应体来获取数据?

第二,是否是因为需要新增请求头或者修改没有加密或很好解决的加密的请求参数?

第三,是否是因为解析页面太慢,需要直接获取请求来达到快速获取数据的目的?

以上只是部分原因,如果是因为这样,考虑用别的方式解决问题。例如:自己构造请求,发送数据返回给程序提取页面源码,使用beautifulsoup4,jsoup来解析元素
给出使用selenium发送请求的demo:

function getData() {let x = '';$.ajax({url: "https://blog.csdn.net//phoenix/web/v1/article/like",type: "post",async: false,data: "articleId=105216417",success: function(result) {x = result;}})return x;
}
//将以上函数代码修改后压缩,给selenium执行js,js末尾加上return getData();//如果页面没有jquery
function getData() {let url = "https://blog.csdn.net//phoenix/web/v1/article/like";let xhr = new XMLHttpRequest();//getxhr.open("get", url, false);xhr.send(url);//postxhr.open('post',url,false);
//    xhr.send('要发送的数据,例如 e=2&b=1 没有就是空字符串')xhr.send("articleId=105216417");let res = '';if (xhr.readyState == 4 && xhr.status == 200) {res = xhr.responseText}return res
}
//将以上函数代码修改后压缩,给selenium执行js,js末尾加上return getData();

以下是拦截请求的部分代码,亲自试验过,似乎没法拿到响应体,网上有人通过其他方式拿到响应体,但是不完美,这里就不拿响应体了。想拿也不是没办法,拿到请求头,请求url,请求体,自己发送就好了。

先给出浏览器插件的webRequest的部分方法:

//这个方法能获取请求体
chrome.webRequest.onBeforeRequest.addListener(function (details) {console.info(details);/*** 通过请求体details拿到的requestBody有两种格式,formData的就不说了* 这里说一下ArrayBuffer的转化方法:* decodeURIComponent(escape(String.fromCodePoint.apply(null, new Uint8Array(d.requestBody.raw[0].bytes))))*/
},{urls: ["<all_urls>"]}, ["blocking",'requestBody'])//这个方法能获取请求头
chrome.webRequest.onResponseStarted.addListener(function(details){console.info(details);//给每个请求添加 test:test 请求头details.requestHeaders.push({"name":"test","value":"test"})//如果要修改某个请求头,判断details.url,再循环改details.requestHeaders的属性就好了。return { requestHeaders: details.requestHeaders };;
},{urls:["<all_urls>"]},["responseHeaders"])//这个是google官方插件User-Agent Switcher for Chrome的改变user agent的函数 replaceHeader// Given a UserAgent object, will replace the "User-Agent" header in the
// map provided as requestHeaders.
function replaceHeader(user_agent, requestHeaders) {if (!user_agent || !user_agent.ua_string)return requestHeaders;var newHeaders = [];for (var i = 0; i < requestHeaders.length; i++) {if (requestHeaders[i].name != "User-Agent") {newHeaders.push(requestHeaders[i]);} else {var new_value = requestHeaders[i].value;if (user_agent.ua_string != "")new_value = (user_agent.append_to_default_ua ? new_value + " " + user_agent.ua_string : user_agent.ua_string);newHeaders.push({"name"  : "User-Agent","value" : new_value});}}return newHeaders;
}

以下是 java demo(zipFile方法参考上面的 设置有认证的代理):

/*** 根据demo自行修改* @return* @throws Exception*/
private static ChromeOptions setChromeIntercept() throws Exception{ChromeOptions options = new ChromeOptions();FileWriter fileWriter1 = null;FileWriter fileWriter2 = null;String driverPath = "C:/";//浏览器插件存放文件目录String jsonFilePath = driverPath + "manifest.json";String jsFilePath = driverPath + "background.js";String zipFilePath = driverPath + "intercept.zip";File jsonFile = new File(jsonFilePath);File zipFile = new File(zipFilePath);File jsFile = new File(jsFilePath);try {String js = "chrome.webRequest.onBeforeSendHeaders.addListener(function(details){details.requestHeaders.push({\"name\":\"test\",\"value\":\"test\"});return{requestHeaders:details.requestHeaders}},{urls:[\"<all_urls>\"]},[\"blocking\",\"requestHeaders\"]);";String json = "{\"version\":\"1.0.0\",\"manifest_version\":2,\"name\":\"Chrome Intercept\",\"permissions\":[\"tabs\",\"unlimitedStorage\",\"storage\",\"<all_urls>\",\"webRequest\",\"webRequestBlocking\"],\"background\":{\"scripts\":[\"js/background.js\"]},\"minimum_chrome_version\":\"22.0.0\"}";if(!jsFile.exists()){jsFile.createNewFile();}if(!jsonFile.exists()){jsonFile.createNewFile();}fileWriter1 = new FileWriter(jsFilePath);fileWriter2 = new FileWriter(jsonFilePath);fileWriter1.write(js);fileWriter2.write(json);}catch (Exception e){e.printStackTrace();}finally {try{if(null!=fileWriter1){fileWriter1.flush();fileWriter1.close();}if(null!=fileWriter2){fileWriter2.flush();fileWriter2.close();}}catch (Exception e){e.printStackTrace();}}try {File[] files = new File[]{jsFile, jsonFile};//zipFile方法参考上面的 设置有认证的代理。zipFile(files, zipFile);options.addExtensions(zipFile);}catch (Exception e){e.printStackTrace();}return options;
}

这个是生成的文件结构目录:

群里有人说还有通过代理的方式拦截请求,具体去问群里的其他人吧。

2020.11.02(记连接远程selenium)

tips:远程连接火狐的时候,用户文件夹似乎不能设置。尝试了几种办法,最后发现他是把文件夹内容发送出去了,远程服务器接收到以后也没有打开火狐。似乎是有点问题。chrome配置用户文件夹是传地址,所以chrome可以远程配置用户文件夹。
下面是步骤:

  1. 首先下载jar包:https://www.selenium.dev/downloads/
    下载最新稳定版本(Latest stable version)。

  2. 远程服务器上安装java环境,并配置环境变量,将jar包放入远程服务器上,将chromedriver的目录写入环境变量。

  3. 在jar包所在的文件夹打开命令行,输入

    java -jar xxxxxx.jar -port 8888
    

    xxx.jar 是你下载的jar包 , 8888 是你指定打开程序的端口,可以自己设置,如果看到打印

     Selenium Server is up and running on port 8888
    

    ,在本地尝试打开 http://服务器ip:8888/wd/hub/注意开放端口

如上图所示,则启动成功。

  1. 远程服务器jar包启动成功后,编写代码连接远程服务器,为远程打开selenium添加参数:

    @Test
    public void RemoteTest() throws Exception{DesiredCapabilities chrome = DesiredCapabilities.chrome();ChromeOptions options = new ChromeOptions();options.addArguments("xxx参数");chrome.merge(options);chrome.setCapability("key","value");WebDriver driver = null;try{driver = new RemoteWebDriver(new URL("http://远程ip:8888/wd/hub/"),chrome);driver.get("https://www.baidu.com/s?wd=selenium")Thread.sleep(2000L);}catch(Exception e){e.printStackTrace();}finally{if(null!=driver){driver.quit();}}
    }

tips:当需要为远程机器的 file inupt框sendkeys本地文件时,在初始化RemoteWebdriver后要加上:

RemoteWebDriver driver = new RemoteWebDriver(new URL("http://ip:port/wd/hub"), caps);
driver.setFileDetector(new LocalFileDetector());

2021.04.12(记selenium跨域问题)

首先说方法,如果不考虑环境安全性,可以直接使用参数:

options.addArguments("--disable-web-security");

使用这个参数打开的chrome,浏览器不会考虑跨域问题。

正常情况下,直接加上上面那个参数就好了,如果考虑环境安全,还是用别的方式。因为referer并没有变化,从百度出去的,referer就是百度。

2021.04.26(记selenium的sessionId能干的事)

首先要明确selenium grid的工作原理。它是服务端调用客户端的方式,服务端通过给客户端发送指令来控制客户端操作浏览器,通过阅读它的源码,抓包等方式,可以知道它其实是通过http请求来通信的。

下面是一些模拟服务端给客户端发送消息操控客户端浏览器的请求示例。

注意! !!!如果是grid的形式,则链接里包含/wd/hub,如果是本地,则没有!!!

退出 相当于quit()

服务关了,但是驱动没有退出怎么办?
长久以来的问题,终于能解决了。我目前的方法是,驱动初始化后,记录驱动的sessionId(最好是持久化到数据库或者文件),当程序异常停止(或者运行的时候手动停止),通过获取之前记录的sessionId的方式关闭驱动(就算驱动已经关闭,也不会有什么影响)

通过请求的方式创建selenium。方法如下,注意是delete方式

DELETE  http://ip:port/wd/hub/session/sessionId

创建,打开浏览器

通过请求的方式创建selenium。方法如下:

POST  http://ip:port/wd/hub/session
content-type: application/json

请求体示例:

{"desiredCapabilities": {"browserName": "chrome","sessionId": "9d9cbf48ab3b0afe67afa63c143ee692","goog:chromeOptions": {"args": [],"extensions": []}},"capabilities": {"firstMatch": [{"browserName": "chrome","goog:chromeOptions": {"args": [],"extensions": []}}]}
}


tips:
1.请求体具体参数属性可以在 ProtocolHandshake 类,createSession 方法里打断点,观察payload属性,通过构造不同的参数来确定json的格式
2.sessionId已它返回的sessionId为准

获取元素

获取元素信息(grid有/wd/hub,本地没有,此处演示是本地)

POST /session/0ccdfc35e328efb5592371d18938a57c/element
Content-Type    application/json; charset=utf-8

请求体示例:

tips:
请求体可以在 AbstractHttpCommandCodec类,encode方法里打断点,观察this.json.toJson(parameters);的结果

获取元素文本

获取元素文本(grid有/wd/hub,本地没有,此处演示是本地)

GET      /session/0ccdfc35e328efb5592371d18938a57c/element/元素ID/text

请求示例:

根据sessionId接管已经被selenium打开的浏览器 java版本(包你满意)

之前一直说不可以,今天google的时候偶然看到了一个可行的办法。先上代码。操作方法写在注释里。

import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.*;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.W3CHttpCommandCodec;
import org.openqa.selenium.remote.http.W3CHttpResponseCodec;import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Collections;
import java.util.Map;/*** @author admin* 2021-06-22 13:51*/public class SimpleWebDriver{public static RemoteWebDriver createDriverFromSession(final String sessionId, URL url){CommandExecutor executor = new HttpCommandExecutor(url) {@Overridepublic Response execute(Command command) throws IOException {Response response;if ("newSession".equals(command.getName())) {response = new Response();response.setSessionId(sessionId);response.setStatus(0);response.setValue(Collections.<String, String>emptyMap());try {Field commandCodec;commandCodec = this.getClass().getSuperclass().getDeclaredField("commandCodec");commandCodec.setAccessible(true);commandCodec.set(this, new W3CHttpCommandCodec());Field responseCodec;responseCodec = this.getClass().getSuperclass().getDeclaredField("responseCodec");responseCodec.setAccessible(true);responseCodec.set(this, new W3CHttpResponseCodec());} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();}} else {response = super.execute(command);}return response;}};return new RemoteWebDriver(executor, new DesiredCapabilities());}//测试用public static void main(String [] args) throws Exception{//填驱动位置System.setProperty("webdriver.chrome.driver", "E:\\drivers\\chromedriver_win32 -- 91\\chromedriver.exe");RemoteWebDriver driver = null;try {//如果是本地,直接用new ChromeDriver(options);,火狐没试过。driver = new RemoteWebDriver(new URL("http://ip:port/wd/hub"), new ChromeOptions());driver.get("https://blog.csdn.net/weixin_42525191/article/details/105216417");HttpCommandExecutor executor = (HttpCommandExecutor) driver.getCommandExecutor();//这里如果是本地,端口是程序启动时的端口,如果是远程,直接是上面的 http://ip:port/wd/hubURL url = executor.getAddressOfRemoteServer();System.out.println("url:"+url.toString());//第一次打开时的sessionIdString sessionId = driver.getSessionId().toString();System.out.println("sessionId:"+sessionId);//假设此时程序崩了。你可以手动记录sessionId,在此关掉程序,注释掉上述的几行代码,将sessionId写死,解开下面的4行代码,运行看看结果。//没有url的,url = new URL("上面打印的url.toString()");/*RemoteWebDriver driver2 = createDriverFromSession(sessionId, url);driver2.get("http://www.baidu.com");System.out.println(driver2.getSessionId());//打断点用System.out.println();*/}catch (Exception e){e.printStackTrace();}finally {if(null != driver){//为了测试接管能力,这里注释掉
//                driver.quit();}}}
}

这里重写了 HttpCommandExecutor 这个类的excute方法(上述的创建新ChromeDriver和退出都在这个方法里),通过伪造newSession请求的响应来获取ChromeDriver,因为 commandCodec 和 responseCodec 这两个变量是私有的,所以通过反射获取这两个变量,伪造了一个响应对象。这个方法可以归到工具方法里。
对比一下原方法:

2022-03-04(新增浏览器安全参数)

学到一个参数,http网站chrome会限制它的权限,包括剪切板权限,麦克风权限,加上这个参数后,就能解除这些限制 具体详见

chrome://flags/#unsafely-treat-insecure-origin-as-secure
--unsafely-treat-insecure-origin-as-secure=http网站,多个用逗号隔开

selenium 学习、工作 记录,附常见异常和工具方法相关推荐

  1. 串口服务器工作方式及常见异常故障问题排除方法介绍

    串口设备联网服务器就像一台带CPU.实时操作系统和TCP/IP协议的微型电脑,方便在串口和网络设备中传输数据.您可以在世界任何位置通过网络,用您的计算机来存取,管理和配置远程的设备.但是我们在实际使用 ...

  2. Android ExceptionThrowable 常见异常和解决方法 奔溃日志上报 monkey异常修改

    java将所有的错误封装为一个对象,其根本父类为Throwable, Throwable有两个子类:Error和Exception. 注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理. ...

  3. springboot mybatis常见异常及处理方法

    springboot mybatis常见异常及处理方法 参考文章: (1)springboot mybatis常见异常及处理方法 (2)https://www.cnblogs.com/gunduzi/ ...

  4. python学习自记录(2)开发工具的pycharm安装使用,编写的第一个应声虫程序

    python学习自记录(2)开发工具的pycharm安装使用 1.下载安装 链接:https://pan.baidu.com/s/18ARXYybcoMrRi96gfIh6Zg 密码:qydc 下方注 ...

  5. 串口服务器常见异常情况排除方法介绍

    串口服务器就像一台带CPU.实时操作系统和TCP/IP协议的微型电脑,方便在串口和网络设备中传输数据.在使用串口服务器的过程中,一般按照操作手册进行操作基本上可以解决问题,但是,在实际操作中还是会出现 ...

  6. Maven常见异常及解决方法

    异常1: [ERROR] Failed to execute goal on project biz_zhuhai: Could not resolve dependencies for projec ...

  7. linux学习工作记录----配置基于ip的虚拟主机

    查看自己的ip信息: [root@slave5 conf]# ifconfig eth0 Link encap:Ethernet HWaddr E0:CB:4E:D0:EC:B2 inet addr: ...

  8. Github标星超7k!从零开始,最简明扼要的数据科学学习路径(附高效免费小工具)...

    点击上方"涛哥聊Python",选择"星标"公众号 重磅干货,第一时间送达 来源:大数据文摘 大数据文摘出品 作者:蒋宝尚 试图入门一个新话题时,多数人会感到不 ...

  9. python常见异常以及处理方法

    一.常见的异常 1.NameError 未定义变量异常 print(a) # 输出:NameError: name 'a' is not defined 2.IndexError 下标越界异常 lis ...

最新文章

  1. 不愿说再见 | 自动化系2019年毕业典礼发言
  2. MyBatis Plus自定义SQL使用条件构造器QueryWrapper
  3. MySQL索引如何优化?二十条铁则送你!!!
  4. 云服务器与传统服务器的优劣对比_为什么选择海外云服务器和香港云服务器
  5. matlab 垂直边缘检测,matlab 边缘检测
  6. java并发:初探消费者和生产者模式
  7. mysql正删改查返回值_MySQL增删改查
  8. matlab 贝叶斯信息标准_Matlab中贝叶斯(bayes)分类器实现分类
  9. php数据入库流程,php数据库操作
  10. MTK 开机logo 修改
  11. 综述:三维点云深度学习技术
  12. 土方回填施工方案范本_土方回填施工方案范例(模板)
  13. html实现简单动画,编写自己的代码库(css3常用动画的实现)
  14. kettle-3(linux环境调度kjb并配置定时读取)
  15. 关于装msdn网站纯净版win7正版授权问题(已解决)
  16. foxipdf和adobe_过去和将来的活动:Adobe Max North America和CFCAMP澳大利亚
  17. 根据药物名找华法林的代谢通路并可视化KGML文件
  18. CMake生成多个.so文件
  19. 浅析静态规划和动态规划
  20. 钉钉在线表格下载后子表内容空白无数据

热门文章

  1. 软件工程毕业设计课题(25)基于JAVA毕业设计JAVA房产中介看房预约系统毕设作品项目
  2. UFFI-制作OVMF并成功运行qemu虚拟机
  3. zabbix php代码,Zabbix php分布式系统监视 v5.2.5
  4. 2019春招前端实习面经
  5. python 进程,线程,协程篇
  6. java 接口 抽象类 继承 重载 重写 多态
  7. Windows系统用Docker搭建私有仓库
  8. 思灵机器人与您相约德国慕尼黑展
  9. 基于springboot的论坛系统
  10. 小程序如何生成海报分享朋友圈,android移动开发技术与应用