mirror of
https://gitee.com/g1879/DrissionPage.git
synced 2024-12-10 04:00:23 +08:00
4.0.4.9修复mhtml问题;js中var改为let
This commit is contained in:
parent
29d0886975
commit
690fb96fd0
@ -722,7 +722,7 @@ class ChromiumElement(DrissionElement):
|
||||
def _get_ele_path(self, mode):
|
||||
"""返获取绝对的css路径或xpath路径"""
|
||||
if mode == 'xpath':
|
||||
txt1 = 'var tag = el.nodeName.toLowerCase();'
|
||||
txt1 = 'let tag = el.nodeName.toLowerCase();'
|
||||
txt3 = ''' && sib.nodeName.toLowerCase()==tag'''
|
||||
txt4 = '''
|
||||
if(nth>1){path = '/' + tag + '[' + nth + ']' + path;}
|
||||
@ -741,10 +741,10 @@ class ChromiumElement(DrissionElement):
|
||||
js = '''function(){
|
||||
function e(el) {
|
||||
if (!(el instanceof Element)) return;
|
||||
var path = '';
|
||||
let path = '';
|
||||
while (el.nodeType === Node.ELEMENT_NODE) {
|
||||
''' + txt1 + '''
|
||||
var sib = el, nth = 0;
|
||||
let sib = el, nth = 0;
|
||||
while (sib) {
|
||||
if(sib.nodeType === Node.ELEMENT_NODE''' + txt3 + '''){nth += 1;}
|
||||
sib = sib.previousSibling;
|
||||
@ -1372,8 +1372,8 @@ else{return e.singleNodeValue;}'''
|
||||
# 按顺序获取所有元素、节点或属性
|
||||
elif type_txt == '7':
|
||||
for_txt = """
|
||||
var a=new Array();
|
||||
for(var i = 0; i <e.snapshotLength ; i++){
|
||||
let a=new Array();
|
||||
for(let i = 0; i <e.snapshotLength ; i++){
|
||||
if(e.snapshotItem(i).constructor.name=="Text"){a.push(e.snapshotItem(i).data);}
|
||||
else if(e.snapshotItem(i).constructor.name=="Attr"){a.push(e.snapshotItem(i).nodeValue);}
|
||||
else if(e.snapshotItem(i).constructor.name=="Comment"){a.push(e.snapshotItem(i).nodeValue);}
|
||||
@ -1388,7 +1388,7 @@ else{a.push(e.snapshotItem(i));}}"""
|
||||
return_txt = 'return e.singleNodeValue;'
|
||||
|
||||
xpath = xpath.replace(r"'", r"\'")
|
||||
js = f'function(){{var e=document.evaluate(\'{xpath}\',{node_txt},null,{type_txt},null);\n{for_txt}\n{return_txt}}}'
|
||||
js = f'function(){{let e=document.evaluate(\'{xpath}\',{node_txt},null,{type_txt},null);\n{for_txt}\n{return_txt}}}'
|
||||
|
||||
return js
|
||||
|
||||
|
@ -97,7 +97,7 @@ def location_in_viewport(page, loc_x, loc_y):
|
||||
:param loc_y: 页面绝对坐标y
|
||||
:return: bool
|
||||
"""
|
||||
js = f'''function(){{var x = {loc_x}; var y = {loc_y};
|
||||
js = f'''function(){{let x = {loc_x}; let y = {loc_y};
|
||||
const scrollLeft = document.documentElement.scrollLeft;
|
||||
const scrollTop = document.documentElement.scrollTop;
|
||||
const vWidth = document.documentElement.clientWidth;
|
||||
@ -342,10 +342,10 @@ def get_blob(page, url, as_bytes=True):
|
||||
js = """
|
||||
function fetchData(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'blob';
|
||||
xhr.onload = function() {
|
||||
var reader = new FileReader();
|
||||
let reader = new FileReader();
|
||||
reader.onloadend = function(){resolve(reader.result);}
|
||||
reader.readAsDataURL(xhr.response);
|
||||
};
|
||||
|
@ -753,42 +753,16 @@ class ChromiumBase(BasePage):
|
||||
:param item: 要获取的项,不设置则返回全部
|
||||
:return: sessionStorage一个或所有项内容
|
||||
"""
|
||||
if item:
|
||||
js = f'sessionStorage.getItem("{item}");'
|
||||
return self.run_js_loaded(js, as_expr=True)
|
||||
else:
|
||||
js = '''
|
||||
var dp_ls_len = sessionStorage.length;
|
||||
var dp_ls_arr = new Array();
|
||||
for(var i = 0; i < dp_ls_len; i++) {
|
||||
var getKey = sessionStorage.key(i);
|
||||
var getVal = sessionStorage.getItem(getKey);
|
||||
dp_ls_arr[i] = {'key': getKey, 'val': getVal}
|
||||
}
|
||||
return dp_ls_arr;
|
||||
'''
|
||||
return {i['key']: i['val'] for i in self.run_js_loaded(js)}
|
||||
js = f'sessionStorage.getItem("{item}")' if item else 'sessionStorage'
|
||||
return self.run_js_loaded(js, as_expr=True)
|
||||
|
||||
def local_storage(self, item=None):
|
||||
"""返回localStorage信息,不设置item则获取全部
|
||||
:param item: 要获取的项目,不设置则返回全部
|
||||
:return: localStorage一个或所有项内容
|
||||
"""
|
||||
if item:
|
||||
js = f'localStorage.getItem("{item}");'
|
||||
return self.run_js_loaded(js, as_expr=True)
|
||||
else:
|
||||
js = '''
|
||||
var dp_ls_len = localStorage.length;
|
||||
var dp_ls_arr = new Array();
|
||||
for(var i = 0; i < dp_ls_len; i++) {
|
||||
var getKey = localStorage.key(i);
|
||||
var getVal = localStorage.getItem(getKey);
|
||||
dp_ls_arr[i] = {'key': getKey, 'val': getVal}
|
||||
}
|
||||
return dp_ls_arr;
|
||||
'''
|
||||
return {i['key']: i['val'] for i in self.run_js_loaded(js)}
|
||||
js = f'localStorage.getItem("{item}")' if item else 'localStorage'
|
||||
return self.run_js_loaded(js, as_expr=True)
|
||||
|
||||
def get_screenshot(self, path=None, name=None, as_bytes=None, as_base64=None,
|
||||
full_page=False, left_top=None, right_bottom=None):
|
||||
@ -1233,7 +1207,7 @@ def get_mhtml(page, path=None, name=None):
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
name = make_valid_name(name or page.title)
|
||||
with open(f'{path}{sep}{name}.mhtml', 'w', encoding='utf-8') as f:
|
||||
f.write(r)
|
||||
f.write(r.replace('\r\n', '\n'))
|
||||
return r
|
||||
|
||||
|
||||
|
@ -146,7 +146,7 @@ class PageScroller(Scroller):
|
||||
txt = 'true' if center else 'false'
|
||||
ele.run_js(f'this.scrollIntoViewIfNeeded({txt});')
|
||||
if center or (center is not False and ele.states.is_covered):
|
||||
ele.run_js('''function getWindowScrollTop() {var scroll_top = 0;
|
||||
ele.run_js('''function getWindowScrollTop() {let scroll_top = 0;
|
||||
if (document.documentElement && document.documentElement.scrollTop) {
|
||||
scroll_top = document.documentElement.scrollTop;
|
||||
} else if (document.body) {scroll_top = document.body.scrollTop;}
|
||||
|
154
java/pom.xml
Normal file
154
java/pom.xml
Normal file
@ -0,0 +1,154 @@
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.ll</groupId>
|
||||
<artifactId>DrissonPage</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<name>Magic - DrissonPage</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!--忽略大小写-->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version> <!-- 将版本号更新为最新版本 -->
|
||||
</dependency>
|
||||
<!--文本替换-->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
<version>1.11.0</version>
|
||||
</dependency>
|
||||
<!--ini读取-->
|
||||
<dependency>
|
||||
<groupId>org.ini4j</groupId>
|
||||
<artifactId>ini4j</artifactId>
|
||||
<version>0.5.4</version>
|
||||
</dependency>
|
||||
<!--json序列化-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.83</version>
|
||||
</dependency>
|
||||
<!-- JNA 依赖 调用 Windows API -->
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>5.10.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna-platform</artifactId>
|
||||
<version>5.10.0</version>
|
||||
</dependency>
|
||||
<!-- <!– https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/websocket-client –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.eclipse.jetty.websocket</groupId>-->
|
||||
<!-- <artifactId>websocket-client</artifactId>-->
|
||||
<!-- <version>9.4.54.v20240208</version>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!-- <!–websocket–>
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.5.1</version>
|
||||
</dependency>-->
|
||||
|
||||
<!--okhttp3-->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.10.0</version>
|
||||
</dependency>
|
||||
<!--maven:com.squareup.okio:okio-jvm:3.0.漏洞改成3.4.0-->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio-jvm</artifactId>
|
||||
<version>3.4.0</version>
|
||||
</dependency>
|
||||
<!--可关闭的请求-->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpasyncclient</artifactId>
|
||||
<version>4.1.5</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.16.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-model</artifactId>
|
||||
<version>3.8.1</version> <!-- 使用最新版本 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-core</artifactId>
|
||||
<version>3.8.1</version> <!-- 使用最新版本 -->
|
||||
</dependency>
|
||||
<!--依赖漏洞修补-->
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.shared</groupId>
|
||||
<artifactId>maven-shared-utils</artifactId>
|
||||
<version>3.3.4</version>
|
||||
</dependency>
|
||||
<!--依赖漏洞修补-->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<!--依赖漏洞修补-->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.1.2-jre</version>
|
||||
</dependency>
|
||||
<!--SessionElement-->
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.16.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>RELEASE</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<!-- Apache POI -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
<!--maven:org.apache.commons:commons-compress:1.21-->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.21</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
76
java/src/main/java/com/ll/DataRecorder/BaseRecorder.java
Normal file
76
java/src/main/java/com/ll/DataRecorder/BaseRecorder.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public abstract class BaseRecorder extends OriginalRecorder {
|
||||
protected String encoding;
|
||||
protected Object before;
|
||||
protected Object after;
|
||||
protected String table;
|
||||
|
||||
public BaseRecorder() {
|
||||
}
|
||||
|
||||
public BaseRecorder(Integer cacheSize) {
|
||||
super(cacheSize);
|
||||
}
|
||||
|
||||
public BaseRecorder(Path path) {
|
||||
super(path);
|
||||
}
|
||||
|
||||
public BaseRecorder(Path path, Integer cacheSize) {
|
||||
super(path, cacheSize);
|
||||
}
|
||||
|
||||
public BaseRecorder(String path) {
|
||||
super(path);
|
||||
}
|
||||
|
||||
public BaseRecorder(String path, Integer cacheSize) {
|
||||
super(path, cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置属性的对象
|
||||
*/
|
||||
@Override
|
||||
public BaseSetter<?> set() {
|
||||
if (super.setter == null) super.setter = new BaseSetter<>(this);
|
||||
return (BaseSetter<?>) super.setter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前before内容
|
||||
*/
|
||||
public Object before() {
|
||||
return this.before;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return 返回当前before内容
|
||||
*/
|
||||
public Object after() {
|
||||
return this.after;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回默认表名
|
||||
*/
|
||||
public String table() {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回编码格式
|
||||
*/
|
||||
public String encoding() {
|
||||
return this.encoding;
|
||||
}
|
||||
|
||||
}
|
155
java/src/main/java/com/ll/DataRecorder/BaseSetter.java
Normal file
155
java/src/main/java/com/ll/DataRecorder/BaseSetter.java
Normal file
@ -0,0 +1,155 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class BaseSetter<R extends BaseRecorder> extends OriginalSetter<R> {
|
||||
public BaseSetter(R recorder) {
|
||||
super(recorder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认表名
|
||||
*
|
||||
* @param name 表名
|
||||
*/
|
||||
public void table(String name) {
|
||||
recorder.table = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据前面补充的列
|
||||
*
|
||||
* @param before 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void before(List<String> before) {
|
||||
before(before, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据前面补充的列
|
||||
*
|
||||
* @param before 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void before(Map<?, ?> before) {
|
||||
before(before, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据前面补充的列
|
||||
*
|
||||
* @param before 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void before(String before) {
|
||||
before(before, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据前面补充的列
|
||||
*
|
||||
* @param before 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void before(String[] before) {
|
||||
before(before, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据前面补充的列
|
||||
*
|
||||
* @param before 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
private void before(Object before, boolean ignoredI) {
|
||||
if (before == null || "".equals(before)) {
|
||||
this.recorder.before = null;
|
||||
} else if (before instanceof List) {
|
||||
this.recorder.before = before;
|
||||
} else if (before instanceof Map) {
|
||||
this.recorder.before = before;
|
||||
|
||||
} else if (before instanceof String[]) {
|
||||
this.recorder.before = before;
|
||||
} else {
|
||||
this.recorder.before = Collections.singletonList(before);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据后面补充的列
|
||||
*
|
||||
* @param after 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void after(List<String> after) {
|
||||
after(after, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据后面补充的列
|
||||
*
|
||||
* @param after 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void after(Map<?, ?> after) {
|
||||
after(after, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据后面补充的列
|
||||
*
|
||||
* @param after 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void after(String after) {
|
||||
after(after, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据后面补充的列
|
||||
*
|
||||
* @param after 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
public void after(String[] after) {
|
||||
after(after, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在数据后面补充的列
|
||||
*
|
||||
* @param after 列表、数组或字符串,为字符串时则补充一列
|
||||
*/
|
||||
private void after(Object after, boolean ignoredI) {
|
||||
if (after == null || "".equals(after)) {
|
||||
this.recorder.after = null;
|
||||
} else if (after instanceof List) {
|
||||
this.recorder.after = after;
|
||||
} else if (after instanceof Map) {
|
||||
this.recorder.after = after;
|
||||
|
||||
} else if (after instanceof String[]) {
|
||||
this.recorder.after = after;
|
||||
} else {
|
||||
this.recorder.after = Collections.singletonList(after);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编码
|
||||
*
|
||||
* @param encoding 编码格式
|
||||
*/
|
||||
public void encoding(String encoding) {
|
||||
this.recorder.encoding = Charset.forName(encoding).name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编码
|
||||
*
|
||||
* @param encoding 编码格式
|
||||
*/
|
||||
public void encoding(Charset encoding) {
|
||||
this.recorder.encoding = encoding.name();
|
||||
}
|
||||
}
|
150
java/src/main/java/com/ll/DataRecorder/ByteRecorder.java
Normal file
150
java/src/main/java/com/ll/DataRecorder/ByteRecorder.java
Normal file
@ -0,0 +1,150 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class ByteRecorder extends OriginalRecorder {
|
||||
private static final long[] END = {0, 2};
|
||||
protected List<ByteData> data;
|
||||
|
||||
/**
|
||||
* 用于记录字节数据的工具
|
||||
*/
|
||||
public ByteRecorder() {
|
||||
this("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于记录字节数据的工具
|
||||
*
|
||||
* @param path 保存的文件路径
|
||||
*/
|
||||
public ByteRecorder(Path path) {
|
||||
this(path, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于记录字节数据的工具
|
||||
*
|
||||
* @param cacheSize 每接收多少条记录写入文件,0为不自动写入
|
||||
*/
|
||||
public ByteRecorder(Integer cacheSize) {
|
||||
this("", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于记录字节数据的工具
|
||||
*
|
||||
* @param path 保存的文件路径
|
||||
* @param cacheSize 每接收多少条记录写入文件,0为不自动写入
|
||||
*/
|
||||
public ByteRecorder(Path path, Integer cacheSize) {
|
||||
super(path == null ? null : path.toAbsolutePath().toString(), cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于记录字节数据的工具
|
||||
*
|
||||
* @param path 保存的文件路径
|
||||
*/
|
||||
public ByteRecorder(String path) {
|
||||
super(path, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于记录字节数据的工具
|
||||
*
|
||||
* @param path 保存的文件路径
|
||||
* @param cacheSize 每接收多少条记录写入文件,0为不自动写入
|
||||
*/
|
||||
public ByteRecorder(String path, Integer cacheSize) {
|
||||
super("".equals(path) ? null : path, cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data 类型只能为byte[]
|
||||
*/
|
||||
@Override
|
||||
public void addData(Object data) {
|
||||
if (data instanceof byte[]) addData((byte[]) data, null);
|
||||
else throw new IllegalArgumentException("data类型只能为byte[]为了兼容");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加一段二进制数据
|
||||
*
|
||||
* @param data bytes类型数据
|
||||
* @param seek 在文件中的位置,None表示最后
|
||||
*/
|
||||
public void addData(byte[] data, Long seek) {
|
||||
while (this.pauseAdd) { //等待其它线程写入结束
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (seek != null && seek < 0) throw new IllegalArgumentException("seek参数只能接受null或大于等于0的整数。");
|
||||
this.data.add(new ByteData(data, seek));
|
||||
this.dataCount++;
|
||||
if (0 < this.cacheSize() && this.cacheSize() <= this.dataCount) this.record();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前保存在缓存的数据
|
||||
*/
|
||||
public List<ByteData> data() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据到文件
|
||||
*/
|
||||
protected void _record() {
|
||||
Path filePath = Paths.get(path);
|
||||
if (!Files.exists(filePath)) {
|
||||
try {
|
||||
Files.createFile(filePath);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
try (SeekableByteChannel fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
|
||||
long[] previous = null;
|
||||
for (ByteData entry : data) {
|
||||
|
||||
long[] loc = (entry.seek == null ? ByteRecorder.END : new long[]{entry.seek, 0});
|
||||
if (!(previous != null && previous[0] == loc[0] && previous[1] == loc[1] && ByteRecorder.END[0] == loc[0] && ByteRecorder.END[1] == loc[1])) {
|
||||
fileChannel.position(loc[0] + loc[1]);
|
||||
previous = loc;
|
||||
}
|
||||
fileChannel.write(ByteBuffer.wrap(entry.data));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class ByteData {
|
||||
private byte[] data;
|
||||
private Long seek;
|
||||
}
|
||||
}
|
20
java/src/main/java/com/ll/DataRecorder/DBRecorder.java
Normal file
20
java/src/main/java/com/ll/DataRecorder/DBRecorder.java
Normal file
@ -0,0 +1,20 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import javax.naming.NoPermissionException;
|
||||
|
||||
/**
|
||||
* 用于存储数据到sqlite的工具
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class DBRecorder extends BaseRecorder {
|
||||
@Override
|
||||
public void addData(Object data) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void _record() throws NoPermissionException {
|
||||
|
||||
}
|
||||
}
|
236
java/src/main/java/com/ll/DataRecorder/OriginalRecorder.java
Normal file
236
java/src/main/java/com/ll/DataRecorder/OriginalRecorder.java
Normal file
@ -0,0 +1,236 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import javax.naming.NoPermissionException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* 记录器的基类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public abstract class OriginalRecorder {
|
||||
protected int cache;
|
||||
protected String path;
|
||||
protected String type;
|
||||
protected List<Object> data;
|
||||
protected Lock lock = new ReentrantLock();//线程锁
|
||||
protected boolean pauseAdd = false;
|
||||
protected boolean pauseWrite = false;
|
||||
public boolean showMsg = true;
|
||||
protected OriginalSetter<?> setter;
|
||||
protected int dataCount = 0;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public OriginalRecorder() {
|
||||
this("");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cacheSize 每接收多少条记录写入文件,0为不自动写入
|
||||
*/
|
||||
public OriginalRecorder(Integer cacheSize) {
|
||||
this("", cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path 保存的文件路径
|
||||
*/
|
||||
public OriginalRecorder(Path path) {
|
||||
this(path.toString(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path 保存的文件路径
|
||||
* @param cacheSize 每接收多少条记录写入文件,0为不自动写入
|
||||
*/
|
||||
public OriginalRecorder(Path path, Integer cacheSize) {
|
||||
this(path.toString(), cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path 保存的文件路径
|
||||
*/
|
||||
public OriginalRecorder(String path) {
|
||||
this(path, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path 保存的文件路径
|
||||
* @param cacheSize 每接收多少条记录写入文件,0为不自动写入
|
||||
*/
|
||||
public OriginalRecorder(String path, Integer cacheSize) {
|
||||
this.set().path(path);
|
||||
this.cache = cacheSize != null ? cacheSize : 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置属性的对象
|
||||
*/
|
||||
public OriginalSetter<?> set() {
|
||||
if (this.setter == null) this.setter = new OriginalSetter<>(this);
|
||||
return this.setter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回缓存大小
|
||||
*/
|
||||
public int cacheSize() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回文件路径
|
||||
*/
|
||||
public String path() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回文件类型
|
||||
*/
|
||||
public String type() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前保存在缓存的数据
|
||||
*/
|
||||
public Object data() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据,可保存到新文件
|
||||
*
|
||||
* @return 文件路径
|
||||
*/
|
||||
public String record() {
|
||||
return record("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据,可保存到新文件
|
||||
*
|
||||
* @param newPath 文件另存为的路径,会保存新文件
|
||||
* @return 文件路径
|
||||
*/
|
||||
|
||||
public String record(Path newPath) {
|
||||
return record(newPath.toString());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录数据,可保存到新文件
|
||||
*
|
||||
* @param newPath 文件另存为的路径,会保存新文件
|
||||
* @return 文件路径
|
||||
* @throws IOException 读写文件时可能发生IOException
|
||||
*/
|
||||
public String record(String newPath) {
|
||||
if ("".equals(newPath)) newPath = null;
|
||||
// 具体功能由_record()实现,本方法实现自动重试及另存文件功能
|
||||
String originalPath = path;
|
||||
String returnPath = path;
|
||||
if (newPath != null && !newPath.isEmpty()) {
|
||||
newPath = Tools.getUsablePath(newPath).toString();
|
||||
returnPath = path = newPath;
|
||||
|
||||
Path originalFilePath = Paths.get(originalPath);
|
||||
if (Files.exists(originalFilePath)) {
|
||||
Path newPathObject = Paths.get(newPath);
|
||||
try {
|
||||
Files.copy(originalFilePath, newPathObject, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
return returnPath;
|
||||
}
|
||||
|
||||
if (path == null || path.isEmpty()) {
|
||||
throw new IllegalArgumentException("保存路径为空。");
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
pauseAdd = true; // 写入文件前暂缓接收数据
|
||||
if (showMsg) {
|
||||
System.out.println(path + " 开始写入文件,切勿关闭进程。");
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(Paths.get(path).getParent());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
while (pauseWrite) { // 等待其它线程写入结束
|
||||
Thread.sleep(100);
|
||||
}
|
||||
pauseWrite = true;
|
||||
this._record();
|
||||
break;
|
||||
|
||||
} catch (NoPermissionException e) {
|
||||
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
Files.write(Paths.get("failed_data.txt"), (data.toString() + "\n").getBytes());
|
||||
System.out.println("保存失败的数据已保存到failed_data.txt。");
|
||||
} catch (IOException ioException) {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
pauseWrite = false;
|
||||
}
|
||||
|
||||
Thread.sleep(300);
|
||||
}
|
||||
|
||||
if (newPath != null && !newPath.isEmpty()) {
|
||||
path = originalPath;
|
||||
}
|
||||
|
||||
if (showMsg) {
|
||||
System.out.println(path + " 写入文件结束。");
|
||||
}
|
||||
clear();
|
||||
dataCount = 0;
|
||||
pauseAdd = false;
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return returnPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存中的数据
|
||||
*/
|
||||
public void clear() {
|
||||
if (this.data != null) this.data.clear();
|
||||
}
|
||||
|
||||
public abstract void addData(Object data);
|
||||
|
||||
protected abstract void _record() throws NoPermissionException;
|
||||
}
|
58
java/src/main/java/com/ll/DataRecorder/OriginalSetter.java
Normal file
58
java/src/main/java/com/ll/DataRecorder/OriginalSetter.java
Normal file
@ -0,0 +1,58 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class OriginalSetter<R extends OriginalRecorder> {
|
||||
protected final R recorder;
|
||||
|
||||
|
||||
/**
|
||||
* 设置缓存大小
|
||||
*
|
||||
* @param size 缓存大小
|
||||
*/
|
||||
public void cacheSize(int size) {
|
||||
if (size < 0) return;
|
||||
this.recorder.cache = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件路径
|
||||
*
|
||||
* @param path 文件路径
|
||||
*/
|
||||
public void path(String path) {
|
||||
if (this.recorder.path() != null) this.recorder.record();
|
||||
this.recorder.path = path;
|
||||
this.recorder.data = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件路径
|
||||
*
|
||||
* @param path 文件路径
|
||||
*/
|
||||
public void path(Path path) {
|
||||
if (this.recorder.path() != null) this.recorder.record();
|
||||
this.recorder.path = path.toString();
|
||||
this.recorder.data = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否显示运行信息
|
||||
*
|
||||
* @param onOff 开关
|
||||
*/
|
||||
|
||||
public void showMsg(boolean onOff) {
|
||||
this.recorder.showMsg = onOff;
|
||||
}
|
||||
}
|
269
java/src/main/java/com/ll/DataRecorder/Recorder.java
Normal file
269
java/src/main/java/com/ll/DataRecorder/Recorder.java
Normal file
@ -0,0 +1,269 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Recorder extends BaseRecorder {
|
||||
|
||||
private String delimiter = ",";
|
||||
private String quoteChar = "\"";
|
||||
private boolean followStyles = false;
|
||||
private Float colHeight = null;
|
||||
private String style = null;
|
||||
private boolean fitHead = false;
|
||||
|
||||
// 其他属性和方法的声明
|
||||
public Recorder(String path) {
|
||||
this(path, null);
|
||||
}
|
||||
|
||||
public Recorder(Path path) {
|
||||
this(path.toString(), null);
|
||||
}
|
||||
|
||||
public Recorder(String path, Integer cacheSize) {
|
||||
super(path, cacheSize);
|
||||
}
|
||||
|
||||
// 其他方法和属性的具体实现
|
||||
|
||||
public RecorderSetter set() {
|
||||
if (setter == null) {
|
||||
setter = new RecorderSetter(this);
|
||||
}
|
||||
return (RecorderSetter) setter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addData(Object data) {
|
||||
this.addData(data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回csv文件分隔符
|
||||
*/
|
||||
public String delimiter() {
|
||||
return delimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回csv文件引用符
|
||||
*/
|
||||
public String quoteChar() {
|
||||
return quoteChar;
|
||||
}
|
||||
|
||||
public void addData(Object data, String table) {
|
||||
while (this.pauseAdd) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (!(data instanceof List) && !(data instanceof Map)) {
|
||||
data = Collections.singletonList(data);
|
||||
}
|
||||
|
||||
if (data instanceof List<?> && ((List<?>) data).isEmpty()) {
|
||||
data = new ArrayList<>();
|
||||
this.dataCount++;
|
||||
}
|
||||
|
||||
if (!"xlsx".equals(type)) {
|
||||
if (data instanceof List<?>) {
|
||||
this.data.addAll((List<?>) data);
|
||||
}
|
||||
} else {
|
||||
if (table == null) {
|
||||
table = this.table;
|
||||
} else if (table.equals("false")) {
|
||||
table = null;
|
||||
}
|
||||
|
||||
// this.data.add(table, k -> new ArrayList<>()).addAll((List<?>) data);
|
||||
}
|
||||
|
||||
if (0 < this.cache && cache <= dataCount) {
|
||||
record();
|
||||
}
|
||||
}
|
||||
|
||||
protected void _record() {
|
||||
// if ("csv".equals(type)) {
|
||||
// toCsv();
|
||||
// } else if ("xlsx".equals(type)) {
|
||||
// toXlsx();
|
||||
// } else if ("json".equals(type)) {
|
||||
// toJson();
|
||||
// } else if ("txt".equals(type)) {
|
||||
// toTxt();
|
||||
// }
|
||||
}
|
||||
|
||||
// protected void toXlsx() {
|
||||
// Path filePath = Paths.get(path);
|
||||
// boolean newFile = filePath.toFile().exists();
|
||||
// Workbook wb;
|
||||
// if (newFile) {
|
||||
// try {
|
||||
// wb = WorkbookFactory.create(filePath.toFile());
|
||||
// } catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// return;
|
||||
// }
|
||||
// } else {
|
||||
// wb = new XSSFWorkbook();
|
||||
// }
|
||||
//
|
||||
// List<String> tables = new ArrayList<>();
|
||||
// data.forEach((table) -> {
|
||||
// boolean newSheet = false;
|
||||
// Sheet sheet;
|
||||
// if (table == null) {
|
||||
// sheet = wb.getSheetAt(0);
|
||||
// } else if (tables.contains(table)) {
|
||||
// sheet = wb.getSheet(table);
|
||||
// } else if (newFile) {
|
||||
// sheet = wb.getSheetAt(0);
|
||||
// tables.remove(sheet.getSheetName());
|
||||
// sheet.getWorkbook().setSheetName(sheet.getWorkbook().getSheetIndex(sheet), table);
|
||||
// } else {
|
||||
// sheet = wb.createSheet(table);
|
||||
// tables.add(table);
|
||||
// newSheet = true;
|
||||
// }
|
||||
//
|
||||
// if (newFile || newSheet) {
|
||||
// List<String> title = getTitle(data, before(), after());
|
||||
// if (title != null) {
|
||||
// Row titleRow = sheet.createRow(sheet.getPhysicalNumberOfRows());
|
||||
// title.forEach(t -> titleRow.createCell(titleRow.getPhysicalNumberOfCells()).setCellValue(t));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Float colHeight = null;
|
||||
// List<CellStyle> rowStyles = null;
|
||||
// if (newFile || newSheet) {
|
||||
// if (this.colHeight != null || followStyles || style != null || _data.size() > 1) {
|
||||
// wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (newFile || newSheet) {
|
||||
// if (this.colHeight != null || followStyles) {
|
||||
// int lastRowNum = sheet.getLastRowNum();
|
||||
// Row lastRow = sheet.getRow(lastRowNum);
|
||||
// if (lastRow != null) {
|
||||
// colHeight = lastRow.getHeightInPoints();
|
||||
// if (followStyles) {
|
||||
// rowStyles = new ArrayList<>();
|
||||
// for (Cell cell : lastRow) {
|
||||
// rowStyles.add(new CellBase(cell.getCellStyle()));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (newFile || newSheet) {
|
||||
// if (fitHead && _head.get(sheet.getSheetName()) == null) {
|
||||
// _head.put(sheet.getSheetName(), getTitle(data.get(0), _before, _after));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (fitHead && _head.get(sheet.getSheetName()) != null) {
|
||||
// data.forEach(d -> {
|
||||
// if (d instanceof Map) {
|
||||
// d = ((Map<?, ?>) d).values();
|
||||
// }
|
||||
// Row row = sheet.createRow(sheet.getPhysicalNumberOfRows());
|
||||
// _head.get(sheet.getSheetName()).forEach(h -> row.createCell(row.getPhysicalNumberOfCells()).setCellValue(processContent(((Map<?, ?>) d).get(h))));
|
||||
// setStyle(colHeight, rowStyles, row);
|
||||
// });
|
||||
// } else {
|
||||
// data.forEach(d -> {
|
||||
// if (d instanceof Map) {
|
||||
// d = ((Map<?, ?>) d).values();
|
||||
// }
|
||||
// Row row = sheet.createRow(sheet.getPhysicalNumberOfRows());
|
||||
// ((List<?>) d).forEach(value -> row.createCell(row.getPhysicalNumberOfCells()).setCellValue(processContent(value)));
|
||||
// setStyle(colHeight, rowStyles, row);
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// try {
|
||||
// wb.write(filePath.toFile());
|
||||
// wb.close();
|
||||
// } catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// protected void setStyle(Float colHeight, List<CellStyleCopier> rowStyles, Row row) {
|
||||
// if (colHeight != null) {
|
||||
// row.setHeightInPoints(colHeight);
|
||||
// }
|
||||
//
|
||||
// if (rowStyles != null) {
|
||||
// for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
|
||||
// Cell cell = row.getCell(i);
|
||||
// CellStyleCopier styleCopier = rowStyles.get(i);
|
||||
// styleCopier.setToCell(cell);
|
||||
// }
|
||||
// } else if (style != null) {
|
||||
// for (Cell cell : row) {
|
||||
// setStyle(style, cell);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// protected void setStyle(String style, Table.Cell cell) {
|
||||
// // 实现设置样式的逻辑
|
||||
// }
|
||||
//
|
||||
// protected List<String> getTitle(Object data, Object before, Object after) {
|
||||
// if (data instanceof List) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// List<String> returnList = new ArrayList<>();
|
||||
// List<String> beforeList = getList(before);
|
||||
// List<String> afterList = getList(after);
|
||||
//
|
||||
// for (Object obj : List.of(beforeList, data, afterList)) {
|
||||
// if (obj instanceof Map) {
|
||||
// returnList.addAll(((Map<?, ?>) obj).keySet());
|
||||
// } else if (obj == null) {
|
||||
// // Do nothing
|
||||
// } else if (obj instanceof List) {
|
||||
// ((List<?>) obj).forEach(o -> returnList.add(""));
|
||||
// } else {
|
||||
// returnList.add("");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return returnList;
|
||||
// }
|
||||
//
|
||||
// protected List<String> getList(Object obj) {
|
||||
// if (obj instanceof List) {
|
||||
// return (List<String>) obj;
|
||||
// } else if (obj instanceof Map) {
|
||||
// return new ArrayList<>(((Map<?, ?>) obj).keySet());
|
||||
// } else {
|
||||
// return List.of("");
|
||||
// }
|
||||
// }
|
||||
|
||||
// 其他方法和属性的具体实现
|
||||
}
|
11
java/src/main/java/com/ll/DataRecorder/RecorderSetter.java
Normal file
11
java/src/main/java/com/ll/DataRecorder/RecorderSetter.java
Normal file
@ -0,0 +1,11 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class RecorderSetter<R extends Recorder> extends SheetLikeSetter<R> {
|
||||
public RecorderSetter(R recorder) {
|
||||
super(recorder);
|
||||
}
|
||||
}
|
12
java/src/main/java/com/ll/DataRecorder/SheetLikeSetter.java
Normal file
12
java/src/main/java/com/ll/DataRecorder/SheetLikeSetter.java
Normal file
@ -0,0 +1,12 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class SheetLikeSetter<R extends BaseRecorder> extends BaseSetter<R> {
|
||||
public SheetLikeSetter(R recorder) {
|
||||
super(recorder);
|
||||
}
|
||||
|
||||
}
|
104
java/src/main/java/com/ll/DataRecorder/Tools.java
Normal file
104
java/src/main/java/com/ll/DataRecorder/Tools.java
Normal file
@ -0,0 +1,104 @@
|
||||
package com.ll.DataRecorder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Tools {
|
||||
|
||||
|
||||
/**
|
||||
* 检查文件或文件夹是否有重名,并返回可以使用的路径
|
||||
* @param path 文件或文件夹路径
|
||||
* @return 可用的路径,Path对象
|
||||
*/
|
||||
public static Path getUsablePath(String path) {
|
||||
return getUsablePath(path, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件或文件夹是否有重名,并返回可以使用的路径
|
||||
*
|
||||
* @param path 文件或文件夹路径
|
||||
* @param isFile 目标是文件还是文件夹
|
||||
* @param createParents 是否创建目标路径
|
||||
* @return 可用的路径,Path对象
|
||||
*/
|
||||
public static Path getUsablePath(String path, boolean isFile, boolean createParents) {
|
||||
Path filePath = Paths.get(path).toAbsolutePath();
|
||||
Path parent = filePath.getParent();
|
||||
if (createParents) parent.toFile().mkdirs();
|
||||
String name = makeValidName(filePath.getFileName().toString());
|
||||
int num;
|
||||
String srcName;
|
||||
boolean firstTime = true;
|
||||
|
||||
while (Files.exists(filePath) && (Files.isRegularFile(filePath) == isFile)) {
|
||||
Matcher matcher = Pattern.compile("(.*)_(\\d+)$").matcher(name);
|
||||
if (!matcher.find() || (matcher.find() && firstTime)) {
|
||||
srcName = name;
|
||||
num = 1;
|
||||
} else {
|
||||
srcName = matcher.group(1);
|
||||
num = Integer.parseInt(matcher.group(2)) + 1;
|
||||
}
|
||||
name = srcName + "_" + num;
|
||||
filePath = parent.resolve(name);
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的文件名
|
||||
*
|
||||
* @param fullName 文件名
|
||||
* @return 可用的文件名
|
||||
*/
|
||||
public static String makeValidName(String fullName) {
|
||||
fullName = fullName.trim();
|
||||
|
||||
String name;
|
||||
String ext;
|
||||
int extLong;
|
||||
|
||||
Matcher matcher = Pattern.compile("(.*)(\\.[^.]+$)").matcher(fullName);
|
||||
if (matcher.find()) {
|
||||
name = matcher.group(1);
|
||||
ext = matcher.group(2);
|
||||
extLong = ext.length();
|
||||
} else {
|
||||
name = fullName;
|
||||
ext = "";
|
||||
extLong = 0;
|
||||
}
|
||||
|
||||
while (getLong(name) > 255 - extLong) {
|
||||
name = name.substring(0, name.length() - 1);
|
||||
}
|
||||
|
||||
fullName = name + ext;
|
||||
|
||||
return fullName.replaceAll("[<>/\\\\|:*?\\n]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回字符串中字符个数(一个汉字是2个字符)
|
||||
*
|
||||
* @param txt 字符串
|
||||
* @return 字符个数
|
||||
*/
|
||||
public static int getLong(String txt) {
|
||||
int txtLen = txt.length();
|
||||
return (txt.getBytes().length - txtLen) / 2 + txtLen;
|
||||
}
|
||||
|
||||
|
||||
}
|
1294
java/src/main/java/com/ll/DownloadKit/DownloadKit.java
Normal file
1294
java/src/main/java/com/ll/DownloadKit/DownloadKit.java
Normal file
File diff suppressed because it is too large
Load Diff
46
java/src/main/java/com/ll/DownloadKit/FileExists.java
Normal file
46
java/src/main/java/com/ll/DownloadKit/FileExists.java
Normal file
@ -0,0 +1,46 @@
|
||||
package com.ll.DownloadKit;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用于设置存在同名文件时处理方法
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class FileExists {
|
||||
private final Setter setter;
|
||||
|
||||
public void set(FileMode fileMode) {
|
||||
this.setter.downloadKit.fileMode = fileMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为跳过
|
||||
*/
|
||||
public void skip() {
|
||||
this.setter.downloadKit.fileMode = FileMode.SKIP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为重命名,文件名后加序号
|
||||
*/
|
||||
public void rename() {
|
||||
this.setter.downloadKit.fileMode = FileMode.rename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为覆盖
|
||||
*/
|
||||
public void overwrite() {
|
||||
this.setter.downloadKit.fileMode = FileMode.overwrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为追加
|
||||
*/
|
||||
public void add() {
|
||||
this.setter.downloadKit.fileMode = FileMode.add;
|
||||
}
|
||||
}
|
18
java/src/main/java/com/ll/DownloadKit/FileMode.java
Normal file
18
java/src/main/java/com/ll/DownloadKit/FileMode.java
Normal file
@ -0,0 +1,18 @@
|
||||
package com.ll.DownloadKit;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public enum FileMode {
|
||||
ADD("add"), add(ADD.value), SKIP("skip"), skip(SKIP.value), RENAME("rename"), rename(RENAME.value), OVERWRITE("overwrite"), overwrite(OVERWRITE.value), a(ADD.value), A(ADD.value), S(SKIP.value), s(SKIP.value), r(RENAME.value), R(RENAME.value), o(OVERWRITE.value), O(OVERWRITE.value);
|
||||
private final String value;
|
||||
|
||||
FileMode(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
86
java/src/main/java/com/ll/DownloadKit/LogSet.java
Normal file
86
java/src/main/java/com/ll/DownloadKit/LogSet.java
Normal file
@ -0,0 +1,86 @@
|
||||
package com.ll.DownloadKit;
|
||||
|
||||
import com.ll.DataRecorder.Recorder;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* 用于设置信息打印和记录日志方式
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class LogSet {
|
||||
private final Setter setter;
|
||||
|
||||
/**
|
||||
* 设置日志文件路径
|
||||
*
|
||||
* @param path 文件路径,可以是str或Path
|
||||
*/
|
||||
public void path(String path) {
|
||||
if (this.setter.downloadKit.getLogger() != null) this.setter.downloadKit.getLogger().record();
|
||||
this.setter.downloadKit.logger= new Recorder(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志文件路径
|
||||
*
|
||||
* @param path 文件路径,可以是str或Path
|
||||
*/
|
||||
|
||||
public void path(Path path) {
|
||||
if (this.setter.downloadKit.getLogger() != null) this.setter.downloadKit.getLogger().record();
|
||||
this.setter.downloadKit.logger= new Recorder(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印所有信息
|
||||
*/
|
||||
public void printAll() {
|
||||
this.setter.downloadKit.printMode= "all";
|
||||
}
|
||||
|
||||
/**
|
||||
* 只有在下载失败时打印信息
|
||||
*/
|
||||
public void printFailed() {
|
||||
this.setter.downloadKit.printMode= "failed";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 不打印任何信息
|
||||
*/
|
||||
public void printNull() {
|
||||
this.setter.downloadKit.printMode= null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录所有信息
|
||||
*/
|
||||
public void logAll() {
|
||||
if (this.setter.downloadKit.getLogger() == null) throw new RuntimeException("请先用logPath()设置log文件路径。");
|
||||
this.setter.downloadKit.logMode= "all";
|
||||
}
|
||||
|
||||
/**
|
||||
* 只记录下载失败的信息
|
||||
*/
|
||||
public void logFailed() {
|
||||
if (this.setter.downloadKit.getLogger() == null) throw new RuntimeException("请先用logPath()设置log文件路径。");
|
||||
this.setter.downloadKit.logMode= "failed";
|
||||
}
|
||||
|
||||
/**
|
||||
* 不进行记录
|
||||
*/
|
||||
public void logNull() {
|
||||
if (this.setter.downloadKit.getLogger() == null) throw new RuntimeException("请先用logPath()设置log文件路径。");
|
||||
this.setter.downloadKit.logMode= null;
|
||||
}
|
||||
|
||||
|
||||
}
|
197
java/src/main/java/com/ll/DownloadKit/Setter.java
Normal file
197
java/src/main/java/com/ll/DownloadKit/Setter.java
Normal file
@ -0,0 +1,197 @@
|
||||
package com.ll.DownloadKit;
|
||||
|
||||
import com.ll.DrissonPage.base.BasePage;
|
||||
import com.ll.DrissonPage.config.SessionOptions;
|
||||
import com.ll.DrissonPage.page.SessionPage;
|
||||
import com.ll.DrissonPage.page.WebPage;
|
||||
import com.ll.DrissonPage.units.HttpClient;
|
||||
import lombok.AllArgsConstructor;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.http.Header;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class Setter {
|
||||
protected final DownloadKit downloadKit;
|
||||
|
||||
|
||||
/**
|
||||
* -
|
||||
* 设置Session对象
|
||||
*
|
||||
* @param driver Session对象或DrissionPage的页面对象
|
||||
*/
|
||||
|
||||
public void driver(Object driver) {
|
||||
if (driver == null) {
|
||||
this.downloadKit.session = new OkHttpClient();
|
||||
return;
|
||||
}
|
||||
if (driver instanceof OkHttpClient) {
|
||||
this.downloadKit.session = (OkHttpClient) driver;
|
||||
return;
|
||||
}
|
||||
if (driver instanceof SessionOptions) {
|
||||
HttpClient httpClient = ((SessionOptions) driver).makeSession();
|
||||
Collection<? extends Header> headers = httpClient.getHeaders();
|
||||
this.downloadKit.session = this.downloadKit.session().newBuilder().addInterceptor(new Interceptor() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Request.Builder builder1 = request.newBuilder();
|
||||
if (!headers.isEmpty()) headers.forEach((a) -> builder1.addHeader(a.getName(), a.getValue()));
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}).build();
|
||||
} else if (driver instanceof BasePage) {
|
||||
if (driver instanceof SessionPage) this.downloadKit.session = ((SessionPage) driver).session();
|
||||
else if (driver instanceof WebPage) this.downloadKit.session = ((WebPage) driver).session();
|
||||
else this.downloadKit.session = new OkHttpClient();
|
||||
this.downloadKit.page = ((BasePage<?>) driver);
|
||||
} else {
|
||||
throw new IllegalArgumentException("类型只能为OkHttpClient SessionOptions BasePage");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置可同时运行的线程数
|
||||
*
|
||||
* @param num 线程数量
|
||||
*/
|
||||
public void roads(int num) {
|
||||
if (this.downloadKit.isRunning()) {
|
||||
System.out.println("有任务未完成时不能改变roads。");
|
||||
return;
|
||||
}
|
||||
if (num != this.downloadKit.roads()) {
|
||||
this.downloadKit.roads = num;
|
||||
this.downloadKit.getThreads().setMaximumPoolSize(num);
|
||||
this.downloadKit.threadMap = new HashMap<>(num);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接失败时重试次数
|
||||
*
|
||||
* @param times 重试次数
|
||||
*/
|
||||
public void retry(int times) {
|
||||
if (times < 0) throw new IllegalArgumentException("times参数过于小");
|
||||
this.downloadKit.retry = times;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接失败时重试间隔
|
||||
*
|
||||
* @param seconds 连接失败时重试间隔(秒)
|
||||
*/
|
||||
|
||||
public void interval(double seconds) {
|
||||
if (seconds < 0) throw new IllegalArgumentException("seconds参数过于小");
|
||||
this.downloadKit.interval = seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接超时时间
|
||||
*
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public void timeout(double timeout) {
|
||||
if (timeout < 0) throw new IllegalArgumentException("timeout参数过于小");
|
||||
this.downloadKit.timeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件保存路径
|
||||
*
|
||||
* @param path 文件路径,可以是str或Path
|
||||
*/
|
||||
public void goalPath(Path path) {
|
||||
this.downloadKit.goalPath = path.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件保存路径
|
||||
*
|
||||
* @param path 文件路径,可以是str或Path
|
||||
*/
|
||||
public void goalPath(String path) {
|
||||
this.downloadKit.goalPath = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置大文件是否分块下载
|
||||
*
|
||||
* @param onOff 代表开关
|
||||
*/
|
||||
public void split(boolean onOff) {
|
||||
this.downloadKit.split = onOff;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分块大小
|
||||
*
|
||||
* @param size 单位为字节,可用'K'、'M'、'G'为单位,如'50M'
|
||||
*/
|
||||
public void blockSize(String size) {
|
||||
this.downloadKit.blockSize = Utils.blockSizeSetter(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分块大小
|
||||
*
|
||||
* @param size 单位为字节,可用'K'、'M'、'G'为单位,如'50M'
|
||||
*/
|
||||
public void blockSize(int size) {
|
||||
this.downloadKit.blockSize = Utils.blockSizeSetter(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置代理地址及端口,例:'127.0.0.1:1080' 创建方式 new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",80))
|
||||
*/
|
||||
public void proxy(Proxy proxy) {
|
||||
OkHttpClient.Builder builder = this.downloadKit.session.newBuilder();
|
||||
builder.setProxy$okhttp(proxy);
|
||||
this.downloadKit.session = builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编码
|
||||
*
|
||||
* @param encoding 编码名称
|
||||
*/
|
||||
public void encoding(Charset encoding) {
|
||||
this.downloadKit.encoding = encoding.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编码
|
||||
*
|
||||
* @param encoding 编码名称 使用Charset.forName去校验
|
||||
*/
|
||||
public void encoding(String encoding) {
|
||||
this.downloadKit.encoding = Charset.forName(encoding).name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编码 为空
|
||||
*/
|
||||
public void encoding() {
|
||||
this.downloadKit.encoding = null;
|
||||
}
|
||||
}
|
329
java/src/main/java/com/ll/DownloadKit/Utils.java
Normal file
329
java/src/main/java/com/ll/DownloadKit/Utils.java
Normal file
@ -0,0 +1,329 @@
|
||||
package com.ll.DownloadKit;
|
||||
|
||||
import com.ll.DataRecorder.Tools;
|
||||
import okhttp3.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Utils {
|
||||
private static final Map<String, Long> blockSizeMap;
|
||||
private static final Map<String, String> FILE_EXISTS_MODE;
|
||||
|
||||
static {
|
||||
blockSizeMap = new HashMap<>();
|
||||
blockSizeMap.put("b", 1L);
|
||||
blockSizeMap.put("k", 1024L);
|
||||
blockSizeMap.put("m", 1048576L);
|
||||
blockSizeMap.put("g", 21_474_836_480L);
|
||||
|
||||
FILE_EXISTS_MODE = new HashMap<>();
|
||||
FILE_EXISTS_MODE.put("rename", "rename");
|
||||
FILE_EXISTS_MODE.put("overwrite", "overwrite");
|
||||
FILE_EXISTS_MODE.put("skip", "skip");
|
||||
FILE_EXISTS_MODE.put("add", "add");
|
||||
FILE_EXISTS_MODE.put("r", "rename");
|
||||
FILE_EXISTS_MODE.put("o", "overwrite");
|
||||
FILE_EXISTS_MODE.put("s", "skip");
|
||||
FILE_EXISTS_MODE.put("a", "add");
|
||||
}
|
||||
|
||||
public static long blockSizeSetter(Object val) {
|
||||
if (val instanceof Integer || val instanceof Long) {
|
||||
if (Long.parseLong(val.toString()) > 0) {
|
||||
return (int) val;
|
||||
} else {
|
||||
throw new IllegalArgumentException(val + "数字需要大于0");
|
||||
}
|
||||
}
|
||||
if (val instanceof String && val.toString().length() >= 2) {
|
||||
String string = val.toString();
|
||||
long num = Long.parseLong(string.substring(0, string.length() - 2));
|
||||
Long unit = blockSizeMap.get(String.valueOf(string.charAt(string.length() - 1)).toLowerCase());
|
||||
if (unit != null && num > 0) {
|
||||
return unit * num;
|
||||
} else {
|
||||
throw new IllegalArgumentException("单位只支持B、K、M、G,数字必须为大于0的整数。");
|
||||
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("只能传入int或str,数字必须为大于0的整数。");
|
||||
}
|
||||
}
|
||||
|
||||
public static String pathSetter(Object path) {
|
||||
if (path instanceof String) return (String) path;
|
||||
if (path instanceof Path) return ((Path) path).toAbsolutePath().toString();
|
||||
throw new IllegalArgumentException("只能传入Path或str");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置Response对象的编码
|
||||
*
|
||||
* @param response Response对象
|
||||
* @param encoding 指定的编码格式
|
||||
* @return 设置编码后的Response对象
|
||||
*/
|
||||
public static Response setCharset(Response response, String encoding) {
|
||||
if (encoding != null && !encoding.isEmpty()) {
|
||||
response = setEncoding(response, encoding);
|
||||
return response;
|
||||
}
|
||||
|
||||
// 在headers中获取编码
|
||||
String contentType = response.headers("content-type").toString().toLowerCase();
|
||||
if (!contentType.endsWith(";")) {
|
||||
contentType += ";";
|
||||
}
|
||||
|
||||
String charset = findCharset(contentType);
|
||||
if (charset != null) {
|
||||
response = setEncoding(response, charset);
|
||||
return response;
|
||||
}
|
||||
|
||||
// 在headers中获取不到编码,且如果是网页
|
||||
if (contentType.replace(" ", "").startsWith("text/html")) {
|
||||
Matcher matcher = null;
|
||||
try {
|
||||
if (response.body() != null) {
|
||||
matcher = Pattern.compile("<meta.*?charset=[ \\\\'\"]*([^\"\\\\' />]+).*?>").matcher(response.body().string());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (matcher != null && matcher.find()) {
|
||||
charset = matcher.group(1);
|
||||
}
|
||||
|
||||
response = setEncoding(response, charset);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static Response setEncoding(Response response, String charset) {
|
||||
if (charset != null && !charset.isEmpty()) {
|
||||
Response.Builder build = response.newBuilder();
|
||||
ResponseBody body = response.body();
|
||||
if (body != null) if (body.contentType() != null)
|
||||
Objects.requireNonNull(body.contentType()).charset(Charset.forName(charset));
|
||||
return build.build();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String findCharset(String contentType) {
|
||||
Matcher matcher = Pattern.compile("charset[=: ]*(.*)?;?").matcher(contentType);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息,大小单位为byte
|
||||
* 包括:size、path、skip
|
||||
*
|
||||
* @param response Response对象
|
||||
* @param goalPath 目标文件夹
|
||||
* @param rename 重命名
|
||||
* @param suffix 重命名后缀名
|
||||
* @param fileExists 存在重名文件时的处理方式
|
||||
* @param encoding 编码格式
|
||||
* @param lock 线程锁
|
||||
* @return 文件名、文件大小、保存路径、是否跳过
|
||||
*/
|
||||
public static Map<String, Object> getFileInfo(Response response, String goalPath, String rename, String suffix, FileMode fileExists, String encoding, Lock lock) {
|
||||
// ------------获取文件大小------------
|
||||
long fileSize = Optional.ofNullable(response.headers().get("Content-Length")).map(Long::parseLong).orElse(-1L);
|
||||
|
||||
// ------------获取网络文件名------------
|
||||
String fileName = getFileName(response, encoding);
|
||||
|
||||
// ------------获取保存路径------------
|
||||
Path goalPathObj = Paths.get(goalPath);
|
||||
goalPath = goalPathObj.getRoot() + goalPathObj.subpath(1, goalPathObj.getNameCount()).toString().replaceAll("[*:|<>?\"]", "").trim();
|
||||
|
||||
Path goalPathAbsolute = Paths.get(goalPath).toAbsolutePath();
|
||||
goalPathAbsolute.toFile().mkdirs();
|
||||
goalPath = goalPathAbsolute.toString();
|
||||
|
||||
// ------------获取保存文件名------------
|
||||
// -------------------重命名-------------------
|
||||
String fullFileName;
|
||||
if (rename != null) {
|
||||
if (suffix != null) {
|
||||
fullFileName = suffix.isEmpty() ? rename : rename + "." + suffix;
|
||||
} else {
|
||||
String[] tmp = fileName.split("\\.", 2);
|
||||
String extName = tmp.length > 1 ? "." + tmp[1] : "";
|
||||
tmp = rename.split("\\.", 2);
|
||||
String extRename = tmp.length > 1 ? "." + tmp[1] : "";
|
||||
fullFileName = extRename.equals(extName) ? rename : rename + extName;
|
||||
}
|
||||
} else if (suffix != null) {
|
||||
String[] tmp = fileName.split("\\.", 2);
|
||||
fullFileName = suffix.isEmpty() ? tmp[0] : tmp[0] + "." + suffix;
|
||||
} else {
|
||||
fullFileName = fileName;
|
||||
}
|
||||
|
||||
fullFileName = Tools.makeValidName(fullFileName);
|
||||
|
||||
// -------------------生成路径-------------------
|
||||
boolean skip = false;
|
||||
boolean create = true;
|
||||
Path fullPath = Paths.get(goalPath, fullFileName);
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
if (Files.exists(fullPath)) {
|
||||
if (FileMode.RENAME.equals(fileExists)) {
|
||||
fullPath = Tools.getUsablePath(fullPath.toString());
|
||||
} else if (FileMode.SKIP.equals(fileExists)) {
|
||||
skip = true;
|
||||
create = false;
|
||||
} else if (FileMode.OVERWRITE.equals(fileExists)) {
|
||||
try {
|
||||
Files.delete(fullPath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else if (FileMode.ADD.equals(fileExists)) {
|
||||
create = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (create) {
|
||||
try {
|
||||
Files.createFile(fullPath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
Map<String, Object> fileInfo = new HashMap<>();
|
||||
fileInfo.put("size", fileSize);
|
||||
fileInfo.put("path", fullPath);
|
||||
fileInfo.put("skip", skip);
|
||||
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从headers或url中获取文件名,如果获取不到,生成一个随机文件名
|
||||
*
|
||||
* @param response 返回的response
|
||||
* @param encoding 在headers获取时指定编码格式
|
||||
* @return 下载文件的文件名
|
||||
*/
|
||||
private static String getFileName(Response response, String encoding) {
|
||||
String fileName = "";
|
||||
String charset = "utf-8";
|
||||
|
||||
String contentDisposition = Objects.requireNonNull(response.header("content-disposition", "")).replace(" ", "");
|
||||
|
||||
if (!contentDisposition.isEmpty()) {
|
||||
String txt = matchFilename(contentDisposition);
|
||||
if (txt != null) {
|
||||
if (txt.contains("''")) {
|
||||
String[] parts = txt.split("''", 2);
|
||||
charset = parts[0];
|
||||
fileName = parts[1];
|
||||
} else {
|
||||
fileName = txt;
|
||||
}
|
||||
} else {
|
||||
txt = matchFilename(contentDisposition, "filename");
|
||||
if (txt != null) {
|
||||
fileName = txt;
|
||||
if (response.body() != null) {
|
||||
charset = encoding != null ? encoding : Objects.requireNonNull(Objects.requireNonNull(response.body().contentType()).charset()).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileName = fileName.replace("'", "");
|
||||
}
|
||||
|
||||
if (fileName.isEmpty()) {
|
||||
Paths.get(response.request().url().encodedPath());
|
||||
fileName = Paths.get(response.request().url().encodedPath()).getFileName().toString().split("\\?")[0];
|
||||
}
|
||||
|
||||
if (fileName.isEmpty()) {
|
||||
fileName = "untitled_" + System.currentTimeMillis() + "_" + ThreadLocalRandom.current().nextInt(100);
|
||||
}
|
||||
|
||||
charset = charset.isEmpty() ? "utf-8" : charset;
|
||||
try {
|
||||
return URLDecoder.decode(fileName, charset);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String matchFilename(String contentDisposition) {
|
||||
Matcher matcher = Pattern.compile("filename\\*?=\"?([^\";]+)\"?").matcher(contentDisposition);
|
||||
if (matcher.find()) {
|
||||
String[] parts = matcher.group(1).split("''", 2);
|
||||
return parts.length == 2 ? parts[0] + parts[1] : parts[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String matchFilename(String contentDisposition, String pattern) {
|
||||
Matcher matcher = Pattern.compile(pattern + "=\"?([^\";]+)\"?").matcher(contentDisposition);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置OkHttpClient.Builder对象的cookies
|
||||
*
|
||||
* @param builder OkHttpClient.Builder对象
|
||||
* @param cookies cookies信息
|
||||
*/
|
||||
public static void setSessionCookies(OkHttpClient.Builder builder, List<Cookie> cookies) {
|
||||
for (Cookie cookie : cookies) {
|
||||
builder.setCookieJar$okhttp(new CookieJar() {
|
||||
@Override
|
||||
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
|
||||
list.add(cookie);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
131
java/src/main/java/com/ll/DownloadKit/mission/BaseTask.java
Normal file
131
java/src/main/java/com/ll/DownloadKit/mission/BaseTask.java
Normal file
@ -0,0 +1,131 @@
|
||||
package com.ll.DownloadKit.mission;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 任务类基类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class BaseTask {
|
||||
protected final static String DONE = "done";
|
||||
public final static Map<String, String> RESULT_TEXTS = Map.of("success", "成功", "skipped", "跳过", "canceled", "取消", "false", "失败", "null", "未知");
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
private final String id;
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
protected String state = "waiting"; // 'waiting'、'running'、'done'
|
||||
@Getter
|
||||
protected String result = "null"; //'success'、'skipped'、'canceled'、'false'、'null'
|
||||
@Getter
|
||||
@Setter
|
||||
protected String info = "等待下载"; // 信息
|
||||
|
||||
/**
|
||||
* @param id 任务id
|
||||
*/
|
||||
public BaseTask(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回任务或子任务id
|
||||
*/
|
||||
public String id() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回任务数据
|
||||
*/
|
||||
|
||||
public MissionData data() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回任务是否结束
|
||||
*/
|
||||
public boolean isDone() {
|
||||
return "done".equalsIgnoreCase(this.state) || "cancel".equalsIgnoreCase(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务结果值
|
||||
*/
|
||||
public void setStates() {
|
||||
setStates(result, info, "done");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务结果值
|
||||
*
|
||||
* @param result 结果:'success'、'skipped'、'canceled'、false、null
|
||||
*/
|
||||
public void setStates(String result) {
|
||||
setStates(result, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务结果值
|
||||
*
|
||||
* @param result 结果:'success'、'skipped'、'canceled'、false、null
|
||||
* @param info 任务信息
|
||||
*/
|
||||
public void setStates(String result, String info) {
|
||||
setStates(result, info, "done");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务结果值
|
||||
*
|
||||
* @param result 结果:'success'、'skipped'、'canceled'、false、null
|
||||
* @param info 任务信息
|
||||
* @param state 任务状态:'waiting'、'running'、'done'
|
||||
*/
|
||||
public void setStates(String result, String info, String state) {
|
||||
if (result == null) result = "null";
|
||||
result = result.toLowerCase().trim();
|
||||
switch (result) {
|
||||
case "success":
|
||||
this.result = "success";
|
||||
break;
|
||||
case "skipped":
|
||||
this.result = "skipped";
|
||||
break;
|
||||
case "canceled":
|
||||
this.result = "canceled";
|
||||
break;
|
||||
case "false":
|
||||
this.result = "False";
|
||||
break;
|
||||
case "null":
|
||||
this.result = "null";
|
||||
break;
|
||||
default:
|
||||
this.result = "null";
|
||||
break;
|
||||
}
|
||||
this.info = info;
|
||||
if (state == null) state = "done";
|
||||
state = state.toLowerCase().trim();
|
||||
switch (state) {
|
||||
case "running":
|
||||
this.state = "running";
|
||||
break;
|
||||
case "waiting":
|
||||
this.state = "waiting";
|
||||
break;
|
||||
default:
|
||||
this.state = "done";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
401
java/src/main/java/com/ll/DownloadKit/mission/Mission.java
Normal file
401
java/src/main/java/com/ll/DownloadKit/mission/Mission.java
Normal file
@ -0,0 +1,401 @@
|
||||
package com.ll.DownloadKit.mission;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.ll.DataRecorder.ByteRecorder;
|
||||
import com.ll.DownloadKit.DownloadKit;
|
||||
import com.ll.DownloadKit.FileMode;
|
||||
import com.ll.DownloadKit.Utils;
|
||||
import kotlin.Pair;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.*;
|
||||
import org.apache.commons.collections4.map.CaseInsensitiveMap;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 任务类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Mission extends BaseTask {
|
||||
|
||||
@Setter
|
||||
protected String fileName;
|
||||
private final MissionData data;
|
||||
private String path;
|
||||
private ByteRecorder recorder;
|
||||
@Setter
|
||||
private Long size;
|
||||
private int doneTasksCount = 0;
|
||||
@Setter
|
||||
private int tasksCount = 1;
|
||||
@Setter
|
||||
@Getter
|
||||
private List<Task> tasks;
|
||||
/**
|
||||
* 所属DownloadKit对象
|
||||
*/
|
||||
private final DownloadKit downloadKit;
|
||||
@Getter
|
||||
private OkHttpClient session;
|
||||
@Getter
|
||||
private CaseInsensitiveMap<String, Object> headers;
|
||||
@Getter
|
||||
private String method;
|
||||
@Getter
|
||||
|
||||
private String encoding;
|
||||
|
||||
/**
|
||||
* @param id 任务id
|
||||
* @param downloadKit 所属DownloadKit对象
|
||||
* @param fileUrl 文件网址
|
||||
* @param goalPath 保存文件夹路径
|
||||
* @param rename 重命名
|
||||
* @param suffix 重命名后缀名
|
||||
* @param fileExists 存在同名文件处理方式
|
||||
* @param split 是否分块下载
|
||||
* @param encoding 编码格式
|
||||
* @param params 连接参数
|
||||
*/
|
||||
public Mission(String id, DownloadKit downloadKit, String fileUrl, String goalPath, String rename, String suffix, FileMode fileExists, boolean split, String encoding, Map<String, Object> params) {
|
||||
super(id);
|
||||
this.downloadKit = downloadKit;
|
||||
this.size = null;
|
||||
this.tasks = new ArrayList<>();
|
||||
this.encoding = encoding;
|
||||
this.setSession();
|
||||
params = this.handleParams(fileUrl, params);
|
||||
this.data = new MissionData(fileUrl, goalPath, rename, suffix, fileExists, split, params, 0L);
|
||||
this.method = this.data.params.get("data") != null || this.data.params.get("json") != null ? "post" : "get";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 任务id
|
||||
* @param downloadKit 所属DownloadKit对象
|
||||
* @param fileUrl 文件网址
|
||||
* @param goalPath 保存文件夹路径
|
||||
* @param rename 重命名
|
||||
* @param suffix 重命名后缀名
|
||||
* @param fileExists 存在同名文件处理方式
|
||||
* @param split 是否分块下载
|
||||
* @param encoding 编码格式
|
||||
* @param params 连接参数
|
||||
*/
|
||||
public Mission(String id, DownloadKit downloadKit, String fileUrl, Path goalPath, String rename, String suffix, FileMode fileExists, boolean split, String encoding, Map<String, Object> params) {
|
||||
super(id);
|
||||
this.downloadKit = downloadKit;
|
||||
this.size = null;
|
||||
this.tasks = new ArrayList<>();
|
||||
this.encoding = encoding;
|
||||
this.setSession();
|
||||
params = this.handleParams(fileUrl, params);
|
||||
this.data = new MissionData(fileUrl, goalPath, rename, suffix, fileExists, split, params, 0L);
|
||||
this.method = this.data.params.get("data") != null || this.data.params.get("json") != null ? "post" : "get";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<Mission " + this.id() + " " + this.info + " " + this.fileName + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回任务数据
|
||||
*/
|
||||
@Override
|
||||
public MissionData data() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回文件保存路径
|
||||
*/
|
||||
public String path() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回记录器对象
|
||||
*/
|
||||
public ByteRecorder recorder() {
|
||||
if (this.recorder == null) {
|
||||
this.recorder = new ByteRecorder("", 100);
|
||||
this.recorder.showMsg = false;
|
||||
}
|
||||
return this.recorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回下载进度百分比
|
||||
*/
|
||||
public Float rate() {
|
||||
if (this.size == null) return null;
|
||||
int c = 0;
|
||||
for (Task task : this.tasks) c += task.downloadedSize;
|
||||
return new BigDecimal(c * 100).divide(new BigDecimal(this.size), 2, RoundingMode.FLOOR).floatValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消该任务,停止未下载完的task
|
||||
*/
|
||||
public void cancel() {
|
||||
this._breakMission("canceled", "已取消");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 删除下载的文件
|
||||
*/
|
||||
public boolean delFile() {
|
||||
if (this.path != null && Paths.get(this.path).toFile().exists()) {
|
||||
try {
|
||||
return Paths.get(this.path).toFile().delete();
|
||||
} catch (Exception ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待当前任务完成
|
||||
*
|
||||
* @return 任务结果和信息组成的数组
|
||||
*/
|
||||
public String[] waits() {
|
||||
return wait(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待当前任务完成
|
||||
*
|
||||
* @param show 是否显示下载进度
|
||||
* @return 任务结果和信息组成的数组
|
||||
*/
|
||||
public String[] wait(boolean show) {
|
||||
return wait(show, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待当前任务完成
|
||||
*
|
||||
* @param timeout 超时时间
|
||||
* @return 任务结果和信息组成的数组
|
||||
*/
|
||||
public String[] wait(double timeout) {
|
||||
return wait(true, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待当前任务完成
|
||||
*
|
||||
* @param show 是否显示下载进度
|
||||
* @param timeout 超时时间
|
||||
* @return 任务结果和信息组成的数组
|
||||
*/
|
||||
public String[] wait(boolean show, double timeout) {
|
||||
if (show) {
|
||||
System.out.println("url:" + this.data().url);
|
||||
long t2 = System.currentTimeMillis();
|
||||
while (this.fileName == null && System.currentTimeMillis() - t2 < 4000) {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
System.out.println("file:" + this.fileName);
|
||||
System.out.println("filePath:" + this.fileName);
|
||||
if (this.size == null) System.out.println("Unknown size");
|
||||
}
|
||||
long t1 = System.currentTimeMillis();
|
||||
while (!this.isDone() && (System.currentTimeMillis() - t1 < timeout * 1000 || timeout == 0)) {
|
||||
if (show && this.size != null) {
|
||||
try {
|
||||
long rate = Files.size(Paths.get(this.path()));
|
||||
System.out.println("\r" + new BigDecimal(rate * 100).divide(new BigDecimal(this.size), 2, RoundingMode.FLOOR) + "%");
|
||||
} catch (IOException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (show) {
|
||||
String result = this.result.trim().toLowerCase();
|
||||
switch (result) {
|
||||
case "false":
|
||||
System.out.println("下载失败 " + this.info);
|
||||
break;
|
||||
case "success":
|
||||
System.out.println("100%");
|
||||
System.out.println("下载成功 " + this.info);
|
||||
break;
|
||||
case "skipped":
|
||||
System.out.println("已跳过 " + this.info);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new String[]{super.result, super.info};
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制Session对象,并设置cookies
|
||||
*/
|
||||
private void setSession() {
|
||||
OkHttpClient.Builder builder = this.downloadKit.session().newBuilder();
|
||||
CaseInsensitiveMap<String, Object> headers = new CaseInsensitiveMap<>();
|
||||
/*
|
||||
* 使用拦截器去获取请求头参数
|
||||
*/
|
||||
builder.addInterceptor(new Interceptor() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Headers headers1 = request.headers();
|
||||
for (Pair<? extends String, ? extends String> pair : headers1)
|
||||
headers.put(String.valueOf(pair), headers1.get(String.valueOf(pair)));
|
||||
return chain.proceed(request.newBuilder().headers(Headers.of()).build());
|
||||
}
|
||||
});
|
||||
if (this.downloadKit.getPage() != null) {
|
||||
Utils.setSessionCookies(builder, this.downloadKit.getPage().cookies());
|
||||
try {
|
||||
Field header;
|
||||
header = this.downloadKit.getPage().getClass().getField("headers");
|
||||
header.setAccessible(true);
|
||||
Object o = header.get(this.downloadKit.getPage());
|
||||
if (o instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) o;
|
||||
// 检查泛型参数是否为<String, Object>
|
||||
if (map.keySet().stream().allMatch(key -> key instanceof String) && map.values().stream().allMatch(Objects::nonNull))
|
||||
map.forEach((a, b) -> headers.put((String) a, b));
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException ignored) {
|
||||
}
|
||||
headers.put("User-Agent", this.downloadKit.getPage().userAgent());
|
||||
}
|
||||
this.session = builder.build();
|
||||
this.headers = headers;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理接收到的参数
|
||||
*
|
||||
* @param url 要访问的url
|
||||
* @param params 传入的参数map
|
||||
* @return 处理后的参数map
|
||||
*/
|
||||
private Map<String, Object> handleParams(String url, Map<String, Object> params) {
|
||||
if (!params.containsKey("timeout")) params.put("timeout", this.downloadKit.timeout());
|
||||
Map<String, Object> headers = params.containsKey("headers") ? new CaseInsensitiveMap<>(JSON.parseObject(params.get("headers").toString())) : new CaseInsensitiveMap<>();
|
||||
URI uri = URI.create(url);
|
||||
String hostName = uri.getHost();
|
||||
String scheme = uri.getScheme();
|
||||
if (!(headers.containsKey("Referer") || this.headers.containsKey("Referer")))
|
||||
headers.put("Referer", this.downloadKit.getPage() != null && this.downloadKit.getPage().url() != null ? this.downloadKit.getPage().url() : scheme + "://" + hostName);
|
||||
if (!(headers.containsKey("Host") || this.headers.containsKey("Host"))) headers.put("Host", hostName);
|
||||
params.put("headers", headers);
|
||||
return params;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件保存路径
|
||||
*/
|
||||
public void _setPath(Object path) {
|
||||
Path path1;
|
||||
if (path instanceof Path) path1 = (Path) path;
|
||||
else if (path instanceof String) {
|
||||
path1 = Paths.get((String) path);
|
||||
} else throw new IllegalArgumentException("path只能是String或者Path");
|
||||
this.fileName = path1.toAbsolutePath().getFileName().toString();
|
||||
this.path = path1.toAbsolutePath().toString();
|
||||
this.recorder.set().path(path1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个任务为done状态
|
||||
*
|
||||
* @param result 结果:'success'、'skipped'、'canceled'、False、None
|
||||
* @param info 任务信息
|
||||
*/
|
||||
public void _setDone(String result, String info) {
|
||||
switch (result) {
|
||||
case "skipped":
|
||||
this.setStates(result, info, Mission.DONE);
|
||||
break;
|
||||
case "canceled":
|
||||
case "false":
|
||||
this.recorder.clear();
|
||||
this.setStates(result, info, Mission.DONE);
|
||||
break;
|
||||
case "success":
|
||||
this.recorder.record();
|
||||
try {
|
||||
if (this.size != null && Files.size(Paths.get(this.path)) < this.size) {
|
||||
this.delFile();
|
||||
this.setStates("false", "下载失败", Mission.DONE);
|
||||
} else {
|
||||
this.setStates("success", info, Mission.DONE);
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
this.downloadKit._whenMissionDone(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当一个task完成时调用
|
||||
*
|
||||
* @param isSuccess 该task是否成功
|
||||
* @param info 该task传入的信息
|
||||
*/
|
||||
protected void aTaskDone(boolean isSuccess, String info) {
|
||||
if (this.isDone()) return;
|
||||
if (!isSuccess) this._breakMission("false", info);
|
||||
if (++this.doneTasksCount == this.tasksCount) this._setDone("success", info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止该任务,停止未下载完的task
|
||||
*
|
||||
* @param result 结果:'success'、'skipped'、'canceled'、false、None
|
||||
* @param info 任务信息
|
||||
*/
|
||||
public void _breakMission(String result, String info) {
|
||||
if (this.isDone()) return;
|
||||
this.tasks.stream().filter(task -> !task.isDone()).forEach(task -> task.setStates(result, info, "cancel"));
|
||||
this.tasks.stream().filter(task -> !task.isDone()).forEach(task -> {
|
||||
try {
|
||||
Thread.sleep(300);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
this._setDone(result, info);
|
||||
this.delFile();
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package com.ll.DownloadKit.mission;
|
||||
|
||||
import com.ll.DownloadKit.FileMode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 保存任务数据的对象
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class MissionData {
|
||||
/**
|
||||
* 下载文件url
|
||||
*/
|
||||
@Getter
|
||||
|
||||
protected String url;
|
||||
/**
|
||||
* 保存文件夹
|
||||
*/
|
||||
@Getter
|
||||
protected String goalPath;
|
||||
/**
|
||||
* 文件重命名
|
||||
*/
|
||||
@Getter
|
||||
protected String rename;
|
||||
/**
|
||||
* 文件重命名后缀名
|
||||
*/
|
||||
@Getter
|
||||
protected String suffix;
|
||||
/**
|
||||
* 存在重名文件时处理方式
|
||||
*/
|
||||
@Getter
|
||||
protected FileMode fileExists;
|
||||
/**
|
||||
* 是否允许分块下载
|
||||
*/
|
||||
@Getter
|
||||
protected boolean split;
|
||||
/**
|
||||
* requests其它参数
|
||||
*/
|
||||
@Getter
|
||||
protected Map<String, Object> params;
|
||||
/**
|
||||
* 文件存储偏移量
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
protected Long offset;
|
||||
|
||||
public MissionData(String url, String goalPath, String rename, String suffix, FileMode fileExists, boolean split, Map<String, Object> params, Long offset) {
|
||||
if (url != null) {
|
||||
//版本兼容
|
||||
try {
|
||||
this.url = URLEncoder.encode(url, "utf-8").replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'").replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
this.goalPath = goalPath;
|
||||
this.rename = rename;
|
||||
this.suffix = suffix;
|
||||
this.fileExists = fileExists;
|
||||
this.split = split;
|
||||
this.params = params;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public MissionData(String url, Path goalPath, String rename, String suffix, FileMode fileExists, boolean split, Map<String, Object> params, Long offset) {
|
||||
this(url, goalPath.toAbsolutePath().toString(),rename,suffix,fileExists,split,params,offset);
|
||||
}
|
||||
}
|
104
java/src/main/java/com/ll/DownloadKit/mission/Task.java
Normal file
104
java/src/main/java/com/ll/DownloadKit/mission/Task.java
Normal file
@ -0,0 +1,104 @@
|
||||
package com.ll.DownloadKit.mission;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 子任务类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Task extends BaseTask {
|
||||
|
||||
@Getter
|
||||
private Mission mission;
|
||||
@Getter
|
||||
List<Object> range;
|
||||
protected int downloadedSize;
|
||||
private Long size;
|
||||
|
||||
/**
|
||||
* @param id 任务id
|
||||
*/
|
||||
public Task(Mission mission, List<Object> range, String id, long size) {
|
||||
super(id);
|
||||
this.mission = mission;
|
||||
this.range = range;
|
||||
this.size = size;
|
||||
this.downloadedSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回父任务id
|
||||
*/
|
||||
public String mid() {
|
||||
return this.mission.id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回任务数据对象
|
||||
*/
|
||||
public MissionData data() {
|
||||
return this.mission.data();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回文件保存路径
|
||||
*/
|
||||
|
||||
public String path() {
|
||||
return this.mission.path();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return 返回文件名
|
||||
*/
|
||||
public String fileName() {
|
||||
return this.mission.fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回下载进度百分比
|
||||
*/
|
||||
|
||||
public Float rate() {
|
||||
return this.size == null ? null : new BigDecimal(this.downloadedSize * 100).divide(new BigDecimal(this.size), 2, RoundingMode.FLOOR).floatValue();
|
||||
}
|
||||
|
||||
|
||||
public void addData(byte[] data) {
|
||||
addData(data, null);
|
||||
}
|
||||
|
||||
public void addData(byte[] data, Long seek) {
|
||||
this.downloadedSize += data.length;
|
||||
this.mission.recorder().addData(data, seek);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除以接收但未写入硬盘的缓存
|
||||
*/
|
||||
public void clearCache() {
|
||||
this.mission.recorder().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个子任务为done状态
|
||||
*
|
||||
* @param result 结果:'success'、'skipped'、'canceled'、'false'、'null'
|
||||
* @param info 任务信息
|
||||
*/
|
||||
public void _setDone(String result, String info) {
|
||||
this.setStates(result, info, Task.DONE);
|
||||
this.mission.aTaskDone(!Objects.equals(result, "false"), info);
|
||||
}
|
||||
|
||||
|
||||
}
|
242
java/src/main/java/com/ll/DrissonPage/base/BaseElement.java
Normal file
242
java/src/main/java/com/ll/DrissonPage/base/BaseElement.java
Normal file
@ -0,0 +1,242 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import com.ll.DrissonPage.error.extend.ElementNotFoundError;
|
||||
import com.ll.DrissonPage.functions.Settings;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 各元素类的基类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public abstract class BaseElement<P extends BasePage<?>, T extends BaseElement<?, ?>> extends BaseParser<T> {
|
||||
private final P owner;
|
||||
|
||||
public BaseElement(P page) {
|
||||
this.owner = page;
|
||||
this.setType("BaseElement");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回元素标签名
|
||||
*/
|
||||
public abstract String tag();
|
||||
|
||||
/**
|
||||
* 返回上面某一级父元素 用查询语法定位
|
||||
*
|
||||
* @param by 查询选择器
|
||||
* @return 上级元素对象
|
||||
*/
|
||||
public T parent(By by) {
|
||||
return parent(by, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上面某一级父元素 用查询语法定位
|
||||
*
|
||||
* @param by 查询选择器
|
||||
* @param index 选择第几个结果
|
||||
* @return 上级元素对象
|
||||
*/
|
||||
public abstract T parent(By by, Integer index);
|
||||
|
||||
/**
|
||||
* 获取上级父元素
|
||||
*
|
||||
* @return 上级元素对象
|
||||
*/
|
||||
public T parent() {
|
||||
return parent(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上面某一级父元素,指定层数
|
||||
*
|
||||
* @param level 第几级父元素
|
||||
* @return 上级元素对象
|
||||
*/
|
||||
public abstract T parent(Integer level);
|
||||
|
||||
/**
|
||||
* 获取指定定位的第一个父元素
|
||||
*
|
||||
* @param loc 定位语法
|
||||
* @return 上级元素对象
|
||||
*/
|
||||
public T parent(String loc) {
|
||||
return parent(loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上面某一级父元素 用查询语法定位
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @param index 选择第几个结果
|
||||
* @return 上级元素对象
|
||||
*/
|
||||
public abstract T parent(String loc, Integer index);
|
||||
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(By by) {
|
||||
return next(by, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(By by, Integer index) {
|
||||
return next(by, index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 查找节点的超时时间(秒)
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(By by, Integer index, Double timeout) {
|
||||
return next(by, index, timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 查找节点的超时时间(秒)
|
||||
* @param eleOnly 是否只获取元素,为False时把文本、注释节点也纳入
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public abstract T next(By by, Integer index, Double timeout, Boolean eleOnly);
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next() {
|
||||
return next("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(String loc) {
|
||||
return next(loc.isEmpty() ? null : loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(String loc, Integer index) {
|
||||
return next(loc, index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 查找节点的超时时间(秒)
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(String loc, Integer index, Double timeout) {
|
||||
return next(loc, index, timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 查找节点的超时时间(秒)
|
||||
* @param eleOnly 是否只获取元素,为False时把文本、注释节点也纳入
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public abstract T next(String loc, Integer index, Double timeout, Boolean eleOnly);
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
|
||||
public T next(Integer index) {
|
||||
return next(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 查找节点的超时时间(秒)
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public T next(Integer index, Double timeout) {
|
||||
return next(index, timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
*
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 查找节点的超时时间(秒)
|
||||
* @param eleOnly 是否只获取元素,为False时把文本、注释节点也纳入
|
||||
* @return 兄弟元素或节点文本
|
||||
*/
|
||||
public abstract T next(Integer index, Double timeout, Boolean eleOnly);
|
||||
|
||||
|
||||
@Override
|
||||
public List<T> _ele(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr, String method) {
|
||||
return __ele(this.findElements(by, timeout, index, relative, raiseErr), by.getValue(), index, raiseErr, method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> _ele(String str, Double timeout, Integer index, Boolean relative, Boolean raiseErr, String method) {
|
||||
return __ele(this.findElements(str, timeout, index, relative, raiseErr), str, index, raiseErr, method);
|
||||
}
|
||||
|
||||
private List<T> __ele(List<T> elements, String str, Integer index, Boolean raiseErr, String method) {
|
||||
//如果index为空则说明是查找多元素,如果不为空,则说明是单元素查找,直接判断是否为空,如果不为空则说明单元素找到了
|
||||
if (index == null || !elements.isEmpty()) return elements;
|
||||
//如果是单元素,是否抛出异常
|
||||
if (Settings.raiseWhenEleNotFound || raiseErr != null && raiseErr) {
|
||||
Map<String, Object> locOrStr = new HashMap<>();
|
||||
locOrStr.put("loc_or_str", str);
|
||||
locOrStr.put("index", index);
|
||||
throw new ElementNotFoundError(null, method, locOrStr);
|
||||
}
|
||||
//如果不抛出异常则直接创建个空的
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
}
|
300
java/src/main/java/com/ll/DrissonPage/base/BasePage.java
Normal file
300
java/src/main/java/com/ll/DrissonPage/base/BasePage.java
Normal file
@ -0,0 +1,300 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DownloadKit.DownloadKit;
|
||||
import com.ll.DrissonPage.error.extend.ElementNotFoundError;
|
||||
import com.ll.DrissonPage.functions.Settings;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.Cookie;
|
||||
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 页面类的基类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
//页面类的基类
|
||||
public abstract class BasePage<T extends BaseParser<?>> extends BaseParser<T> {
|
||||
protected String url = null;
|
||||
/**
|
||||
* 查找元素时等待的秒数
|
||||
*/
|
||||
private Double timeout = 10.0;
|
||||
/**
|
||||
* 当前访问的url有效性
|
||||
*/
|
||||
@Setter
|
||||
private Boolean urlAvailable = null;
|
||||
/**
|
||||
* 重试次数
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
private Integer retryTimes = 3;
|
||||
/**
|
||||
* 重试间隔
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
private Double retryInterval = 2.0;
|
||||
/**
|
||||
* 默认下载路径
|
||||
*/
|
||||
@Setter
|
||||
private String downloadPath = null;
|
||||
/**
|
||||
* 下载器对象
|
||||
*/
|
||||
@Getter
|
||||
private DownloadKit downloadKit = null;
|
||||
/**
|
||||
* ele返回值
|
||||
*/
|
||||
@Setter
|
||||
private Boolean noneEleReturnValue = Boolean.FALSE;
|
||||
@Setter
|
||||
private Object noneEleValue = null;
|
||||
|
||||
public BasePage() {
|
||||
this.setType("BasePage");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回网页的标题title
|
||||
*/
|
||||
public String title() {
|
||||
T title = this._ele("xpath://title", null, null, null, false, "title").get(0);
|
||||
return title instanceof DrissionElement ? ((DrissionElement<?, ?>) title).text() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回查找元素时等待的秒数
|
||||
*/
|
||||
public Double timeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查找元素时等待的秒数
|
||||
*
|
||||
* @param timeout 秒
|
||||
*/
|
||||
public void setTimeout(Double timeout) {
|
||||
if (timeout != null && timeout >= 0) this.timeout = timeout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 连接前的准备
|
||||
*
|
||||
* @param url 要访问的url
|
||||
* @param retry 重试次数
|
||||
* @param interval 重试间隔
|
||||
* @return 重试次数和间隔
|
||||
*/
|
||||
protected BeforeConnect beforeConnect(String url, Integer retry, Double interval) {
|
||||
boolean isFile = false;
|
||||
|
||||
try {
|
||||
if (Paths.get(url).toFile().exists() || (!url.contains("://") && !url.contains(":\\\\"))) {
|
||||
Path p = Paths.get(url);
|
||||
if (p.toFile().exists()) {
|
||||
url = p.toAbsolutePath().toString();
|
||||
isFile = true;
|
||||
}
|
||||
}
|
||||
} catch (InvalidPathException ignored) {
|
||||
|
||||
}
|
||||
this.url = url;
|
||||
retry = retry == null ? this.getRetryTimes() : retry;
|
||||
interval = interval == null ? this.getRetryInterval() : interval;
|
||||
return new BeforeConnect(retry, interval, isFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前访问的url有效性
|
||||
*/
|
||||
public Boolean urlAvailable() {
|
||||
return this.urlAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回默认下载路径
|
||||
*/
|
||||
public String downloadPath() {
|
||||
return this.downloadPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回下载器对象
|
||||
*/
|
||||
public DownloadKit download() {
|
||||
if (this.downloadKit == null) this.downloadKit = new DownloadKit(this.downloadPath, null, null, this);
|
||||
return this.downloadKit;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @return 返回当前访问url
|
||||
*/
|
||||
public abstract String url();
|
||||
/**
|
||||
* @return 用于被WebPage覆盖
|
||||
*/
|
||||
protected String browserUrl() {
|
||||
return this.url();
|
||||
}
|
||||
|
||||
public abstract JSONObject json();
|
||||
|
||||
/**
|
||||
* @return 返回user agent
|
||||
*/
|
||||
public abstract String userAgent();
|
||||
|
||||
/**
|
||||
* 返回cookies
|
||||
*/
|
||||
public List<Cookie> cookies() {
|
||||
return cookies(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回cookies
|
||||
*
|
||||
* @param asMap 为True时返回由{name: value}键值对组成的map,为false时返回list且allInfo无效
|
||||
*/
|
||||
public List<Cookie> cookies(boolean asMap) {
|
||||
return cookies(asMap, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回cookies
|
||||
*
|
||||
* @param asMap 为True时返回由{name: value}键值对组成的map,为false时返回list且allInfo无效
|
||||
* @param allDomains 是否返回所有域的cookies
|
||||
*/
|
||||
public List<Cookie> cookies(boolean asMap, boolean allDomains) {
|
||||
return cookies(asMap, allDomains, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回cookies
|
||||
*
|
||||
* @param asMap 为True时返回由{name: value}键值对组成的map,为false时返回list且allInfo无效
|
||||
* @param allDomains 是否返回所有域的cookies
|
||||
* @param allInfo 是否返回所有信息,为False时只返回name、value、domain
|
||||
*/
|
||||
public abstract List<Cookie> cookies(boolean asMap, boolean allDomains, boolean allInfo);
|
||||
|
||||
public Boolean get(String url) {
|
||||
return get(url, false);
|
||||
}
|
||||
|
||||
|
||||
public Boolean get(String url, boolean showErrMsg) {
|
||||
return get(url, showErrMsg, retryTimes, retryInterval, timeout);
|
||||
}
|
||||
|
||||
public Boolean get(String url, boolean showErrMsg, Integer retry, Double interval, Double timeout) {
|
||||
return get(url, showErrMsg, retry, interval, timeout, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用get请求跳转到url,可输入文件路径
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为null时使用页面对象retryTimes属性值
|
||||
* @param interval 重试间隔(秒),为null时使用页面对象retryInterval属性值
|
||||
* @param timeout 连接超时时间(秒),为null时使用页面对象timeouts.pageLoad属性值
|
||||
* @param params 连接参数 s模式专用
|
||||
* @return url是否可用
|
||||
*/
|
||||
public abstract Boolean get(String url, boolean showErrMsg, Integer retry, Double interval, Double timeout, Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @param method 方法名称
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
@Override
|
||||
public List<T> _ele(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr, String method) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("str", "");
|
||||
map.put("index", index);
|
||||
if (by == null) throw new ElementNotFoundError(null, method, map);
|
||||
List<T> elements = this.findElements(by, timeout, index, relative, raiseErr);
|
||||
//如果index为空则说明是查找多元素,如果不为空,则说明是单元素查找,直接判断是否为空,如果不为空则说明单元素找到了
|
||||
if (index == null || !elements.isEmpty()) return elements;
|
||||
//如果是单元素,是否抛出异常
|
||||
if (Settings.raiseWhenEleNotFound || raiseErr != null && raiseErr)
|
||||
throw new ElementNotFoundError(null, method, map);
|
||||
//如果不抛出异常则直接创建个空的
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc 定位符
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @param method 方法名称
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
@Override
|
||||
public List<T> _ele(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr, String method) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("str", "");
|
||||
map.put("index", index);
|
||||
if (loc == null) throw new ElementNotFoundError(null, method, map);
|
||||
List<T> elements = this.findElements(loc, timeout, index, relative, raiseErr);
|
||||
//如果index为空则说明是查找多元素,如果不为空,则说明是单元素查找,直接判断是否为空,如果不为空则说明单元素找到了
|
||||
if (index == null || !elements.isEmpty()) return elements;
|
||||
//如果是单元素,是否抛出异常
|
||||
if (Settings.raiseWhenEleNotFound || raiseErr != null && raiseErr)
|
||||
throw new ElementNotFoundError(null, method, map);
|
||||
//如果不抛出异常则直接创建个空的
|
||||
return new ArrayList<>();
|
||||
}
|
||||
/**
|
||||
* 执行元素查找
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
protected abstract List<T> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr);
|
||||
|
||||
|
||||
/**
|
||||
* 执行元素查找
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
protected abstract List<T> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr);
|
||||
}
|
235
java/src/main/java/com/ll/DrissonPage/base/BaseParser.java
Normal file
235
java/src/main/java/com/ll/DrissonPage/base/BaseParser.java
Normal file
@ -0,0 +1,235 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import com.ll.DrissonPage.element.SessionElement;
|
||||
import com.ll.DrissonPage.error.extend.ElementNotFoundError;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 所有页面、元素类的基类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
|
||||
@Getter
|
||||
public abstract class BaseParser<T extends BaseParser<?>> {
|
||||
@Setter
|
||||
private String type;
|
||||
|
||||
|
||||
/**
|
||||
* 返回当前元素下级符合条件的一个元素、属性或节点文本
|
||||
*
|
||||
* @param by 查询元素
|
||||
*/
|
||||
public T ele(By by) {
|
||||
return ele(by, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素下级符合条件的一个元素、属性或节点文本
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param index 获取第几个元素,下标从1开始可传入负数获取倒数第几个
|
||||
*/
|
||||
public T ele(By by, int index) {
|
||||
return ele(by, index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素下级符合条件的一个元素、属性或节点文本
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找元素超时时间(秒),默认与元素所在页面等待时间一致
|
||||
*/
|
||||
public T ele(By by, Double timeout) {
|
||||
return ele(by, 1, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素下级符合条件的一个元素、属性或节点文本
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param index 获取第几个元素,下标从1开始可传入负数获取倒数第几个
|
||||
* @param timeout 查找元素超时时间(秒),默认与元素所在页面等待时间一致
|
||||
*/
|
||||
public T ele(By by, int index, Double timeout) {
|
||||
try {
|
||||
return this._ele(by, timeout, index, null, null, "ele()").get(0);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
Map<String, Object> map = new java.util.HashMap<>();
|
||||
map.put("by", by);
|
||||
map.put("index", index);
|
||||
new ElementNotFoundError("ele()", map).printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个元素
|
||||
*
|
||||
* @param loc 参数,字符串
|
||||
*/
|
||||
public T ele(String loc) {
|
||||
return ele(loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个元素
|
||||
*
|
||||
* @param loc 参数,字符串
|
||||
* @param index 获取第几个元素,下标从0开始可传入负数获取倒数第几个
|
||||
*/
|
||||
public T ele(String loc, int index) {
|
||||
return ele(loc, index, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回当前元素下级符合条件的一个元素、属性或节点文本
|
||||
*
|
||||
* @param loc 参数,字符串
|
||||
* @param timeout 查找元素超时时间(秒),默认与元素所在页面等待时间一致
|
||||
*/
|
||||
public T ele(String loc, Double timeout) {
|
||||
return ele(loc, 1, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个元素
|
||||
*
|
||||
* @param loc 参数,字符串
|
||||
* @param index 获取第几个元素,下标从0开始可传入负数获取倒数第几个
|
||||
* @param timeout 查找元素超时时间(秒),默认与元素所在页面等待时间一致
|
||||
*/
|
||||
public T ele(String loc, int index, Double timeout) {
|
||||
try {
|
||||
List<T> ts = this._ele(loc, timeout, index, null, null, "ele()");
|
||||
if (ts == null) return null;
|
||||
return ts.get(0);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
Map<String, Object> map = new java.util.HashMap<>();
|
||||
map.put("loc", loc);
|
||||
map.put("index", index);
|
||||
new ElementNotFoundError("ele()", map).printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<T> eles(By by) {
|
||||
return eles(by, null);
|
||||
}
|
||||
|
||||
public List<T> eles(By by, Double timeout) {
|
||||
return this._ele(by, timeout, null, null, null, null);
|
||||
}
|
||||
|
||||
public List<T> eles(String loc) {
|
||||
return eles(loc, null);
|
||||
}
|
||||
|
||||
public List<T> eles(String loc, Double timeout) {
|
||||
return this._ele(loc, timeout, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面数据
|
||||
*/
|
||||
public abstract String html();
|
||||
|
||||
/**
|
||||
* @param by 查询元素
|
||||
* @return 元素对象
|
||||
*/
|
||||
public SessionElement sEle(By by) {
|
||||
return sEle(by, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param by 查询元素
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @return 元素对象
|
||||
*/
|
||||
public abstract SessionElement sEle(By by, Integer index);
|
||||
|
||||
/**
|
||||
* @param loc 定位符
|
||||
* @return 元素对象
|
||||
*/
|
||||
public SessionElement sEle(String loc) {
|
||||
return sEle(loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc 定位符
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @return 元素对象
|
||||
*/
|
||||
public abstract SessionElement sEle(String loc, Integer index);
|
||||
|
||||
/**
|
||||
* @param by 查询元素
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
public abstract List<SessionElement> sEles(By by);
|
||||
|
||||
/**
|
||||
* @param loc 定位符
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
public abstract List<SessionElement> sEles(String loc);
|
||||
|
||||
|
||||
/**
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @param method 方法名称
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
protected abstract List<T> _ele(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr, String method);
|
||||
|
||||
/**
|
||||
* @param loc 定位符
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @param method 方法名称
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
protected abstract List<T> _ele(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr, String method);
|
||||
|
||||
/**
|
||||
* 执行元素查找
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
protected abstract List<T> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr);
|
||||
|
||||
|
||||
/**
|
||||
* 执行元素查找
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象组成的列表
|
||||
*/
|
||||
protected abstract List<T> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr);
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class BeforeConnect {
|
||||
private Integer retry;
|
||||
private Double interval;
|
||||
private boolean isFile;
|
||||
}
|
470
java/src/main/java/com/ll/DrissonPage/base/Browser.java
Normal file
470
java/src/main/java/com/ll/DrissonPage/base/Browser.java
Normal file
@ -0,0 +1,470 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.config.ChromiumOptions;
|
||||
import com.ll.DrissonPage.error.extend.PageDisconnectedError;
|
||||
import com.ll.DrissonPage.functions.Tools;
|
||||
import com.ll.DrissonPage.page.ChromiumPage;
|
||||
import com.ll.DrissonPage.page.ChromiumBase;
|
||||
import com.ll.DrissonPage.units.downloader.DownloadManager;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 浏览器
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
|
||||
public class Browser implements Occupant {
|
||||
public static final String __ERROR__ = "error";
|
||||
private static final Map<String, Browser> BROWSERS = new ConcurrentHashMap<>();
|
||||
@Getter
|
||||
private final ChromiumBase page;
|
||||
private final Map<String, Driver> drivers;
|
||||
private final Map<String, Set<Driver>> allDrivers;
|
||||
@Getter
|
||||
private BrowserDriver driver;
|
||||
@Getter
|
||||
private String id;
|
||||
@Getter
|
||||
private String address;
|
||||
@Getter
|
||||
private Map<String, String> frames;
|
||||
@Getter
|
||||
private DownloadManager dlMgr;
|
||||
/**
|
||||
* 浏览器进程id
|
||||
*/
|
||||
@Getter
|
||||
private Integer processId;
|
||||
private boolean connected;
|
||||
|
||||
/**
|
||||
* 浏览器
|
||||
*
|
||||
* @param address 浏览器地址
|
||||
* @param browserId 浏览器id
|
||||
* @param page ChromiumPage对象
|
||||
*/
|
||||
private Browser(String address, String browserId, ChromiumBase page) {
|
||||
this.page = page;
|
||||
this.address = address;
|
||||
this.driver = BrowserDriver.getInstance(browserId, "browser", address, this);
|
||||
this.id = browserId;
|
||||
this.frames = new HashMap<>();
|
||||
this.drivers = new HashMap<>();
|
||||
this.allDrivers = new HashMap<>();
|
||||
this.connected = false;
|
||||
this.processId = null;
|
||||
JSONArray processInfoArray = JSON.parseObject(runCdp("SystemInfo.getProcessInfo")).getJSONArray("processInfo");
|
||||
if (processInfoArray == null) processInfoArray = new JSONArray();
|
||||
for (Object processInfoObject : processInfoArray) {
|
||||
JSONObject processInfo = JSON.parseObject(processInfoObject.toString());
|
||||
if ("browser".equals(processInfo.getString("type"))) {
|
||||
this.processId = processInfo.getInteger("id");
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.runCdp("Target.setDiscoverTargets", Map.of("discover", true));
|
||||
driver.setCallback("Target.targetDestroyed", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onTargetDestroyed(getMessage());
|
||||
}
|
||||
});
|
||||
driver.setCallback("Target.targetCreated", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onTargetCreated(getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param address 浏览器地址
|
||||
* @param browserId 浏览器id
|
||||
* @param page ChromiumPage对象
|
||||
*/
|
||||
public static Browser getInstance(String address, String browserId, ChromiumBase page) {
|
||||
return BROWSERS.computeIfAbsent(browserId, key -> new Browser(address, browserId, page));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应tab id的Driver
|
||||
*
|
||||
* @param tabId 标签页id
|
||||
* @return Driver对象
|
||||
*/
|
||||
public Driver getDriver(String tabId) {
|
||||
return getDriver(tabId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应tab id的Driver
|
||||
*
|
||||
* @param tabId 标签页id
|
||||
* @param occupant 使用该驱动的对象
|
||||
* @return Driver对象
|
||||
*/
|
||||
public Driver getDriver(String tabId, Occupant occupant) {
|
||||
Driver driver = Objects.requireNonNullElseGet(drivers.remove(tabId), () -> new Driver(tabId, "page", this.getAddress(), occupant));
|
||||
HashSet<Driver> value = new HashSet<>();
|
||||
value.add(driver);
|
||||
this.allDrivers.put(tabId, value);
|
||||
return driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页创建时执行
|
||||
*
|
||||
* @param message 回调参数
|
||||
*/
|
||||
private void onTargetCreated(Object message) {
|
||||
try {
|
||||
JSONObject jsonObject = JSON.parseObject(message.toString());
|
||||
String type = jsonObject.getJSONObject("targetInfo").getString("type");
|
||||
if ("page".equals(type) || "webview".equals(type) && !jsonObject.getJSONObject("targetInfo").getString("url").startsWith("devtools://")) {
|
||||
String targetId = jsonObject.getJSONObject("targetInfo").getString("targetId");
|
||||
Driver driver = new Driver(targetId, "page", address);
|
||||
drivers.put(targetId, driver);
|
||||
this.allDrivers.computeIfAbsent(targetId, k -> new HashSet<>()).add(driver);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页关闭时执行
|
||||
*
|
||||
* @param message 回调参数
|
||||
*/
|
||||
private void onTargetDestroyed(Object message) {
|
||||
JSONObject jsonObject = JSON.parseObject(message.toString());
|
||||
String tabId = jsonObject.getString("targetId");
|
||||
if (this.dlMgr != null) this.dlMgr.clearTabInfo(tabId);
|
||||
if (frames != null) frames.values().removeIf(value -> value.equals(tabId));
|
||||
allDrivers.forEach((a, b) -> {
|
||||
if (a.equals(tabId)) b.forEach(Driver::stop);
|
||||
});
|
||||
allDrivers.remove(tabId);
|
||||
drivers.forEach((a, b) -> {
|
||||
if (a.equals(tabId)) b.stop();
|
||||
});
|
||||
drivers.remove(tabId);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行page相关的逻辑
|
||||
*/
|
||||
public void connectToPage() {
|
||||
if (!connected) {
|
||||
this.dlMgr = new DownloadManager(this);
|
||||
connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public String runCdp(String cmd) {
|
||||
return runCdp(cmd, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Chrome DevTools Protocol语句
|
||||
*
|
||||
* @param cmd 协议项目
|
||||
* @param cmdArgs 参数
|
||||
* @return 执行的结果
|
||||
*/
|
||||
public String runCdp(String cmd, Map<String, Object> cmdArgs) {
|
||||
cmdArgs = new HashMap<>(cmdArgs == null ? new HashMap<>() : cmdArgs);
|
||||
Object ignore = cmdArgs.remove("_ignore");
|
||||
String result = driver.run(cmd, cmdArgs).toString();
|
||||
JSONObject result1 = JSONObject.parseObject(result);
|
||||
if (result1.containsKey(__ERROR__)) Tools.raiseError(result1, ignore);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回标签页数量
|
||||
*/
|
||||
public int tabsCount() {
|
||||
JSONArray targetInfos = JSON.parseObject(this.runCdp("Target.getTargets")).getJSONArray("targetInfos");
|
||||
return (int) targetInfos.stream().filter(targetInfo -> {
|
||||
JSONObject jsonObject = JSON.parseObject(targetInfo.toString());
|
||||
String type = jsonObject.getString("type");
|
||||
String url = jsonObject.getString("url");
|
||||
return ("page".equals(type) || "webview".equals(type)) && !url.startsWith("devtools://");
|
||||
}).count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有标签页id组成的列表
|
||||
*/
|
||||
public List<String> tabs() {
|
||||
JSONArray jsonArray = JSON.parseArray(JSONObject.toJSONString(driver.get("http://" + address + "/json")));
|
||||
return jsonArray.stream().filter(targetInfo -> {
|
||||
JSONObject jsonObject = JSON.parseObject(targetInfo.toString());
|
||||
String type = jsonObject.getString("type");
|
||||
String url = jsonObject.getString("url");
|
||||
return ("page".equals(type) || "webview".equals(type)) && !url.startsWith("devtools://");
|
||||
}).map(obj -> ((JSONObject) obj).getString("id")).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs() {
|
||||
return findTabs(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title) {
|
||||
return findTabs(title, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @param url 要匹配url的文本
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title, String url) {
|
||||
return findTabs(title, url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @param url 要匹配url的文本
|
||||
* @param tabType tab类型,可用列表输入多个
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title, String url, List<String> tabType) {
|
||||
return findTabs(title, url, tabType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @param url 要匹配url的文本
|
||||
* @param tabType tab类型,可用列表输入多个
|
||||
* @param single 是否返回首个结果的id,为False返回所有信息
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title, String url, List<String> tabType, boolean single) {
|
||||
Object parse = JSON.parse(JSONObject.toJSONString(this.driver.get("http://" + this.address + "/json")));
|
||||
JSONArray tabs;
|
||||
if (parse instanceof String) {
|
||||
tabs = new JSONArray(List.of(parse));
|
||||
} else if (parse instanceof List || parse instanceof Set || parse instanceof String[]) {
|
||||
tabs = JSON.parseArray(parse.toString());
|
||||
} else {
|
||||
throw new IllegalArgumentException("tab_type类型不对" + parse.toString());
|
||||
}
|
||||
|
||||
List<String> result = tabs.stream().filter(targetInfo -> {
|
||||
JSONObject jsonObject = JSON.parseObject(targetInfo.toString());
|
||||
return (title == null || jsonObject.getString("title").contains(title)) && (url == null || jsonObject.getString("url").contains(url)) && (tabType == null || tabType.contains(jsonObject.getString("type")));
|
||||
}).map(tab -> ((JSONObject) tab).getString("id")).collect(Collectors.toList());
|
||||
return single ? (result.isEmpty() ? null : List.of(result.get(0))) : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭标签页
|
||||
*
|
||||
* @param tabId 标签页id
|
||||
*/
|
||||
public void closeTab(String tabId) {
|
||||
this.onTargetDestroyed(JSON.toJSONString(Map.of("targetId", tabId)));
|
||||
this.driver.run("Target.closeTarget", Map.of("targetId", tabId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止一个Driver
|
||||
*
|
||||
* @param driver Driver对象
|
||||
*/
|
||||
public void stopDiver(Driver driver) {
|
||||
driver.stop();
|
||||
Set<Driver> set = this.allDrivers.get(driver.getId());
|
||||
if (set != null) set.remove(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使标签页变为活动状态
|
||||
*
|
||||
* @param tabId 标签页id
|
||||
*/
|
||||
public void activateTab(String tabId) {
|
||||
this.runCdp("Target.activateTarget", Map.of("targetId", tabId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回浏览器窗口位置和大小信息
|
||||
*
|
||||
* @return 窗口大小字典
|
||||
*/
|
||||
public JSONObject getWindowBounds() {
|
||||
return getWindowBounds(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回浏览器窗口位置和大小信息
|
||||
*
|
||||
* @param tabId 标签页id 不传入默认使用本身的id
|
||||
*/
|
||||
public JSONObject getWindowBounds(String tabId) {
|
||||
return JSON.parseObject(runCdp("Browser.getWindowForTarget", Map.of("targetId", tabId == null || tabId.isEmpty() ? this.id : tabId))).getJSONObject("bounds");
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开重连
|
||||
*/
|
||||
public void reconnect() {
|
||||
this.driver.stop();
|
||||
BrowserDriver.BROWSERS.remove(this.id);
|
||||
this.driver = BrowserDriver.getInstance(this.id, "browser", this.address, this);
|
||||
this.runCdp("Target.setDiscoverTargets", Map.of("discover", true));
|
||||
this.driver.setCallback("Target.targetDestroyed", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onTargetDestroyed(getMessage());
|
||||
}
|
||||
});
|
||||
this.driver.setCallback("Target.targetCreated", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onTargetCreated(getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*/
|
||||
public void quit() {
|
||||
quit(5.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*
|
||||
* @param timeout 等待浏览器关闭超时时间
|
||||
*/
|
||||
public void quit(double timeout) {
|
||||
quit(timeout, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*
|
||||
* @param timeout 等待浏览器关闭超时时间
|
||||
* @param force 是否立刻强制终止进程
|
||||
*/
|
||||
public void quit(double timeout, boolean force) {
|
||||
List<Integer> pids = JSON.parseArray(this.runCdp("SystemInfo.getProcessInfo")).stream().map(o -> JSON.parseObject(o.toString()).getInteger("id")).collect(Collectors.toList());
|
||||
|
||||
|
||||
for (Set<Driver> value : this.allDrivers.values()) for (Driver driver1 : value) driver1.stop();
|
||||
|
||||
if (force) {
|
||||
for (Integer pid : pids)
|
||||
try {
|
||||
ProcessHandle.allProcesses().filter(process -> process.info().command().isPresent()).filter(process -> process.info().command().map(s -> s.contains(Integer.toString(pid))).orElse(false)).forEach(process -> {
|
||||
if (process.isAlive()) process.destroy();
|
||||
});
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
this.runCdp("Browser.close");
|
||||
this.driver.stop();
|
||||
} catch (PageDisconnectedError e) {
|
||||
this.driver.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (force) {
|
||||
String[] ipPort = address.split(":");
|
||||
if (!("127.0.0.1".equals(ipPort[0]) || "localhost".equals(ipPort[0]))) {
|
||||
return;
|
||||
}
|
||||
Tools.stopProcessOnPort(Integer.parseInt(ipPort[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
if (processId != null) {
|
||||
String txt = System.getProperty("os.name").toLowerCase().contains("win") ? "tasklist | findstr " + processId : "ps -ef | grep " + processId;
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new ProcessBuilder(txt.split("\\s+")).start().getInputStream()))) {
|
||||
try {
|
||||
if (!reader.readLine().contains(processId.toString())) {
|
||||
return;
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
// Handle null pointer exception, if needed
|
||||
}
|
||||
Thread.sleep(100);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
this.page.onDisconnect();
|
||||
BROWSERS.remove(id);
|
||||
if (page instanceof ChromiumPage) {
|
||||
ChromiumOptions chromiumOptions = ((ChromiumPage) page).getChromiumOptions();
|
||||
if (chromiumOptions.isAutoPort() && chromiumOptions.getUserDataPath() != null) {
|
||||
Path path = Paths.get(chromiumOptions.getUserDataPath());
|
||||
long endTime = System.currentTimeMillis() + 7000;
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
if (!Files.exists(path)) break;
|
||||
try (Stream<Path> walk = Files.walk(path, FileVisitOption.FOLLOW_LINKS)) {
|
||||
walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.ll.DrissonPage.utils.CloseableHttpClientUtils;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 浏览器驱动
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
|
||||
public class BrowserDriver extends Driver {
|
||||
protected static final Map<String, BrowserDriver> BROWSERS = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
private BrowserDriver(String tabId, String tabType, String address, Occupant occupant) {
|
||||
super(tabId, tabType, address, occupant);
|
||||
}
|
||||
|
||||
public static BrowserDriver getInstance(String tabId, String tabType, String address, Occupant occupant) {
|
||||
|
||||
return BROWSERS.computeIfAbsent(tabId, key -> new BrowserDriver(tabId, tabType, address, occupant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<BrowserDriver " + this.getId() + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求
|
||||
*
|
||||
* @param url 请求地址
|
||||
* @return 返回值可能是List<Map> 或者Map
|
||||
*/
|
||||
public Object get(String url) {
|
||||
HttpGet request = new HttpGet(url);
|
||||
request.setHeader("Connection", "close");
|
||||
String text = CloseableHttpClientUtils.sendRequestJson(request);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
|
||||
}
|
91
java/src/main/java/com/ll/DrissonPage/base/By.java
Normal file
91
java/src/main/java/com/ll/DrissonPage/base/By.java
Normal file
@ -0,0 +1,91 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class By {
|
||||
private BySelect name;
|
||||
private String value;
|
||||
|
||||
private By(BySelect name, String value) {
|
||||
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static By NULL() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static By id(String value) {
|
||||
return new By(BySelect.ID, Objects.requireNonNullElse(value, "Cannot find elements when id is null."));
|
||||
}
|
||||
|
||||
public static By className(String value) {
|
||||
return new By(BySelect.CLASS_NAME, Objects.requireNonNullElse(value, "Cannot find elements when the class name expression is null."));
|
||||
}
|
||||
|
||||
public static By tag(String value) {
|
||||
return new By(BySelect.TAG_NAME, Objects.requireNonNullElse(value, "Cannot find elements when the tag name is null."));
|
||||
}
|
||||
|
||||
public static By name(String value) {
|
||||
return new By(BySelect.NAME, Objects.requireNonNullElse(value, "Cannot find elements when name text is null."));
|
||||
}
|
||||
|
||||
public static By css(String value) {
|
||||
return new By(BySelect.CSS_SELECTOR, Objects.requireNonNullElse(value, "Cannot find elements when the css selector is null."));
|
||||
}
|
||||
|
||||
public static By xpath(String value) {
|
||||
return new By(BySelect.XPATH, Objects.requireNonNullElse(value, "Cannot find elements when the XPath is null."));
|
||||
}
|
||||
|
||||
public static By linkText(String value) {
|
||||
return new By(BySelect.LINK_TEXT, Objects.requireNonNullElse(value, "Cannot find elements when the link text is null."));
|
||||
}
|
||||
|
||||
public static By partialLinkText(String value) {
|
||||
return new By(BySelect.PARTIAL_LINK_TEXT, Objects.requireNonNullElse(value, "Cannot find elements when the partial link text is null."));
|
||||
}
|
||||
|
||||
public static By text(String value) {
|
||||
return new By(BySelect.TEXT, Objects.requireNonNullElse(value, "Cannot find elements when the text is null."));
|
||||
}
|
||||
|
||||
public static By partialText(String value) {
|
||||
return new By(BySelect.PARTIAL_TEXT, Objects.requireNonNullElse(value, "Cannot find elements when the partial text is null."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof By)) return false;
|
||||
By by = (By) o;
|
||||
return Objects.equals(getName().getName(), by.getName().getName()) && Objects.equals(getValue(), by.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getName(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "By{" +
|
||||
"name=" + name.getName() +
|
||||
", value='" + value + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
}
|
18
java/src/main/java/com/ll/DrissonPage/base/BySelect.java
Normal file
18
java/src/main/java/com/ll/DrissonPage/base/BySelect.java
Normal file
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public enum BySelect {
|
||||
NAME("name"), ID("id"), CLASS_NAME("class name"), TAG_NAME("tag name"), CSS_SELECTOR("css selector"),
|
||||
XPATH("xpath"), LINK_TEXT("link text"), PARTIAL_LINK_TEXT("partial link text"), TEXT("text"), PARTIAL_TEXT("partial text");
|
||||
private final String name;
|
||||
|
||||
BySelect(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
1270
java/src/main/java/com/ll/DrissonPage/base/DrissionElement.java
Normal file
1270
java/src/main/java/com/ll/DrissonPage/base/DrissionElement.java
Normal file
File diff suppressed because it is too large
Load Diff
450
java/src/main/java/com/ll/DrissonPage/base/Driver.java
Normal file
450
java/src/main/java/com/ll/DrissonPage/base/Driver.java
Normal file
@ -0,0 +1,450 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.functions.Settings;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.*;
|
||||
import okio.ByteString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 驱动 okHTTP框架
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class Driver {
|
||||
/**
|
||||
* 标签id
|
||||
*/
|
||||
@Getter
|
||||
private final String id;
|
||||
/**
|
||||
* 浏览器连接地址
|
||||
*/
|
||||
@Getter
|
||||
private final String address;
|
||||
/**
|
||||
* 标签页类型
|
||||
*/
|
||||
@Getter
|
||||
private final String type;
|
||||
@Setter
|
||||
@Getter
|
||||
private boolean debug;
|
||||
private final String websocketUrl;
|
||||
private final AtomicInteger curId;
|
||||
/**
|
||||
* 会话返回值
|
||||
*/
|
||||
private final AtomicReference<String> webSocketMsg = new AtomicReference<>(null);
|
||||
private final Thread recvThread;
|
||||
private final Thread handleEventThread;
|
||||
@Getter
|
||||
private AtomicBoolean stopped;
|
||||
private final BlockingQueue<Map<String, Object>> eventQueue;
|
||||
private final BlockingQueue<Map<String, Object>> immediateEventQueue;
|
||||
private final ConcurrentHashMap<String, MyRunnable> eventHandlers;
|
||||
private final ConcurrentHashMap<String, MyRunnable> immediateEventHandlers;
|
||||
private final ConcurrentHashMap<Integer, BlockingQueue<Map<String, Object>>> methodResults;
|
||||
/**
|
||||
* 创建这个驱动的对象
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private Occupant occupant;
|
||||
private boolean alertFlag;
|
||||
private static final OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.callTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.SECONDS)
|
||||
.pingInterval(5, TimeUnit.SECONDS).build();
|
||||
/**
|
||||
* 会话驱动
|
||||
*/
|
||||
private WebSocket ws;
|
||||
private Thread handleImmediateEventThread;
|
||||
|
||||
public Driver(String tabId, String tabType, String address) {
|
||||
this(tabId, tabType, address, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驱动
|
||||
*
|
||||
* @param tabId 标签id
|
||||
* @param tabType 标签页类型
|
||||
* @param address 浏览器连接地址
|
||||
*/
|
||||
public Driver(String tabId, String tabType, String address, Occupant occupant) {
|
||||
this.id = tabId;
|
||||
this.address = address;
|
||||
this.type = tabType;
|
||||
this.occupant = occupant;
|
||||
this.debug = false;
|
||||
this.alertFlag = false;
|
||||
this.websocketUrl = "ws://" + address + "/devtools/" + tabType + "/" + tabId;
|
||||
this.curId = new AtomicInteger(0);
|
||||
this.ws = null;
|
||||
|
||||
|
||||
this.recvThread = new Thread(this::recvLoop);
|
||||
this.handleEventThread = new Thread(this::handleEventLoop);
|
||||
this.recvThread.setDaemon(true);
|
||||
this.handleEventThread.setDaemon(true);
|
||||
this.handleImmediateEventThread = null;
|
||||
|
||||
this.stopped = new AtomicBoolean();
|
||||
|
||||
this.eventHandlers = new ConcurrentHashMap<>();
|
||||
this.immediateEventHandlers = new ConcurrentHashMap<>();
|
||||
this.methodResults = new ConcurrentHashMap<>();
|
||||
this.eventQueue = new LinkedBlockingQueue<>();
|
||||
this.immediateEventQueue = new LinkedBlockingQueue<>();
|
||||
start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息到浏览器,并返回浏览器返回的信息
|
||||
*
|
||||
* @param message 发送给浏览器的数据
|
||||
* @param timeout 超时时间,为null表示无时间
|
||||
* @return 浏览器返回的数据
|
||||
*/
|
||||
private JSONObject send(Map<String, Object> message, double timeout) {
|
||||
message = new HashMap<>(message);
|
||||
int wsId = curId.incrementAndGet();
|
||||
message.put("id", wsId);
|
||||
String messageJson = JSON.toJSONString(message);
|
||||
|
||||
if (this.debug) System.out.println("发->" + messageJson);
|
||||
//计算等待时间
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000L);
|
||||
LinkedBlockingQueue<Map<String, Object>> value = new LinkedBlockingQueue<>();
|
||||
methodResults.put(wsId, value);
|
||||
try {
|
||||
ws.send(messageJson);
|
||||
if (timeout == 0) {
|
||||
methodResults.remove(wsId);
|
||||
return new JSONObject(Map.of("id", wsId, "result", Map.of()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
methodResults.remove(wsId);
|
||||
return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
}
|
||||
int i = 5;
|
||||
long endTimes = System.currentTimeMillis() + 1000L;
|
||||
while (!stopped.get()) {
|
||||
try {
|
||||
Map<String, Object> result = methodResults.get(wsId).poll(5, TimeUnit.SECONDS);
|
||||
if (result == null && System.currentTimeMillis() < endTimes) continue;
|
||||
if (result == null && i > 0 && System.currentTimeMillis() > endTimes) {
|
||||
i--;
|
||||
endTimes = System.currentTimeMillis() + 1000L;
|
||||
if (debug) System.out.println("超时或者丢包,重新发送:->" + messageJson);
|
||||
ws.send(messageJson);
|
||||
continue;
|
||||
}
|
||||
methodResults.remove(wsId);
|
||||
if (result == null) throw new NullPointerException();
|
||||
return new JSONObject(result);
|
||||
} catch (InterruptedException | NullPointerException | IllegalArgumentException e) {
|
||||
// e.printStackTrace();
|
||||
String string = message.get("method").toString();
|
||||
if (alertFlag && string.startsWith("Input.") || string.startsWith("Runtime.")) {
|
||||
return new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists"));
|
||||
}
|
||||
if (timeout > 0 && System.currentTimeMillis() > endTime) {
|
||||
methodResults.remove(wsId);
|
||||
return alertFlag ? new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists")) : new JSONObject(Map.of("error", Map.of("message", "timeout"), "type", "timeout"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收浏览器信息的守护线程方法
|
||||
*/
|
||||
private void recvLoop() {
|
||||
while (!stopped.get()) {
|
||||
JSONObject msg;
|
||||
try {
|
||||
String andSet = webSocketMsg.getAndSet(null);
|
||||
if (andSet != null) {
|
||||
msg = JSONObject.parseObject(andSet);
|
||||
} else continue;
|
||||
} catch (Exception e) {
|
||||
if (stop()) return;
|
||||
return;
|
||||
|
||||
}
|
||||
if (this.debug) System.out.println("<-收" + msg);
|
||||
|
||||
if (msg.containsKey("method")) {
|
||||
if (msg.getString("method").startsWith("Page.javascriptDialog")) {
|
||||
alertFlag = msg.getString("method").endsWith("Opening");
|
||||
}
|
||||
MyRunnable function = immediateEventHandlers.get(msg.getString("method"));
|
||||
if (function != null) {
|
||||
this.handleImmediateEvent(function, msg.getOrDefault("params", new HashMap<>()));
|
||||
} else {
|
||||
try {
|
||||
eventQueue.put(msg);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int i = 1000;
|
||||
Integer integer = msg.getInteger("id");
|
||||
while (i-- > 0 && integer != null && !methodResults.containsKey(integer)) {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (methodResults.containsKey(integer)) {
|
||||
try {
|
||||
methodResults.get(integer).put(msg);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else if (this.debug) {
|
||||
System.out.println("未知错误->" + msg);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当接收到浏览器信息,执行已绑定的方法
|
||||
*/
|
||||
private void handleEventLoop() {
|
||||
while (!stopped.get()) {
|
||||
Map<String, Object> event;
|
||||
try {
|
||||
event = eventQueue.poll(2, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
MyRunnable function = eventHandlers.get(event.get("method").toString());
|
||||
if (function != null) {
|
||||
function.setMessage(event.get("params"));
|
||||
function.run();
|
||||
}
|
||||
}
|
||||
this.eventQueue.poll();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void handleImmediateEventLoop() {
|
||||
while (!stopped.get() && !immediateEventQueue.isEmpty()) {
|
||||
Map<String, Object> event;
|
||||
try {
|
||||
event = immediateEventQueue.poll(2, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
if (event != null) {
|
||||
MyRunnable function = immediateEventHandlers.get(event.get("method").toString());
|
||||
if (function != null) {
|
||||
function.setMessage(event.get("params"));
|
||||
function.run();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理立即执行的动作
|
||||
*
|
||||
* @param function 要运行下方法
|
||||
* @param params 方法参数
|
||||
*/
|
||||
private void handleImmediateEvent(MyRunnable function, Object params) {
|
||||
Map<String, Object> func = new HashMap<>();
|
||||
func.put("method", function);
|
||||
func.put("params", params);
|
||||
immediateEventQueue.add(func);
|
||||
|
||||
if (handleImmediateEventThread == null || !handleImmediateEventThread.isAlive()) {
|
||||
handleImmediateEventThread = new Thread(this::handleImmediateEventLoop);
|
||||
handleImmediateEventThread.setDaemon(true);
|
||||
handleImmediateEventThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行cdp方法
|
||||
*
|
||||
* @param method 方法
|
||||
* @return 执行结果
|
||||
*/
|
||||
public Object run(String method) {
|
||||
return run(method, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行cdp方法
|
||||
*
|
||||
* @param method 方法
|
||||
* @param params 参数
|
||||
* @return 执行结果
|
||||
*/
|
||||
public Object run(String method, Map<String, Object> params) {
|
||||
if (stopped.get()) return Map.of("error", "connection disconnected", "type", "connection_error");
|
||||
params = new HashMap<>(params);
|
||||
Object timeout1 = params.remove("_timeout");
|
||||
double timeout = timeout1 != null ? Float.parseFloat(timeout1.toString()) : Settings.cdpTimeout;
|
||||
|
||||
JSONObject result = this.send(Map.of("method", method, "params", params), timeout);
|
||||
if (!result.containsKey("result") && result.containsKey("error")) {
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("error", result.getJSONObject("error").get("message"));
|
||||
map.put("type", result.getOrDefault("type", "call_method_error"));
|
||||
map.put("method", method);
|
||||
map.put("args", params);
|
||||
map.put("timeout", timeout);
|
||||
return JSON.toJSONString(map);
|
||||
} else {
|
||||
return JSON.toJSONString(result.get("result"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动连接
|
||||
*/
|
||||
private void start() {
|
||||
this.stopped.set(false);
|
||||
try {
|
||||
Request build = new Request(new HttpUrl("socket", "", "", "", 80, new ArrayList<>(), null, null, this.websocketUrl), "GET", Headers.of(), null, new HashMap<>()).newBuilder().url(this.websocketUrl).build();
|
||||
ws = okHttpClient.newWebSocket(build, new WebSocketListener() {
|
||||
@Override
|
||||
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||
// 关闭事件处理
|
||||
stop();
|
||||
super.onClosed(webSocket, code, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||
super.onClosing(webSocket, code, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, Response response) {
|
||||
super.onFailure(webSocket, t, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
|
||||
webSocketMsg.set(text);
|
||||
super.onMessage(webSocket, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
|
||||
super.onMessage(webSocket, bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
|
||||
super.onOpen(webSocket, response);
|
||||
}
|
||||
|
||||
});
|
||||
recvThread.start();
|
||||
handleEventThread.start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断连接
|
||||
*/
|
||||
public boolean stop() {
|
||||
stop1();
|
||||
while (this.recvThread.isAlive() || this.handleEventThread.isAlive()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断连接
|
||||
*/
|
||||
private void stop1() {
|
||||
if (debug) System.out.println("关闭");
|
||||
if (stopped.get()) return;
|
||||
stopped.set(!stopped.get());
|
||||
if (ws != null) {
|
||||
ws.close(1000, "");
|
||||
ws = null;
|
||||
}
|
||||
try {
|
||||
while (!eventQueue.isEmpty()) {
|
||||
Map<String, Object> event = eventQueue.poll();
|
||||
MyRunnable method = eventHandlers.get(event.get("method").toString());
|
||||
if (method != null) {
|
||||
method.setMessage(event.get("params"));
|
||||
method.run();
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
eventHandlers.clear();
|
||||
methodResults.clear();
|
||||
eventQueue.clear();
|
||||
if (occupant != null) occupant.onDisconnect();
|
||||
}
|
||||
|
||||
public void setCallback(String event, MyRunnable callback) {
|
||||
setCallback(event, callback, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定cdp event和回调方法
|
||||
*
|
||||
* @param event 方法名称
|
||||
* @param callback 绑定到cdp event的回调方法
|
||||
* @param immediate 是否要立即处理的动作
|
||||
*/
|
||||
public void setCallback(String event, MyRunnable callback, boolean immediate) {
|
||||
Map<String, MyRunnable> handler = immediate ? immediateEventHandlers : eventHandlers;
|
||||
if (callback != null) handler.put(event, callback);
|
||||
else handler.remove(event);
|
||||
}
|
||||
}
|
423
java/src/main/java/com/ll/DrissonPage/base/Driver2.java
Normal file
423
java/src/main/java/com/ll/DrissonPage/base/Driver2.java
Normal file
@ -0,0 +1,423 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
/**
|
||||
* 驱动
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class Driver2 {
|
||||
// /**
|
||||
// * 标签id
|
||||
// */
|
||||
// @Getter
|
||||
// private final String id;
|
||||
// /**
|
||||
// * 浏览器连接地址
|
||||
// */
|
||||
// @Getter
|
||||
// private final String address;
|
||||
// /**
|
||||
// * 标签页类型
|
||||
// */
|
||||
// @Getter
|
||||
// private final String type;
|
||||
// private final boolean debug;
|
||||
// private final String websocketUrl;
|
||||
// private final AtomicInteger curId;
|
||||
// /**
|
||||
// * 会话返回值
|
||||
// */
|
||||
// private final AtomicReference<String> webSocketMsg = new AtomicReference<>(null);
|
||||
// private final Thread recvThread;
|
||||
// private final Thread handleEventThread;
|
||||
// @Getter
|
||||
//
|
||||
// private final AtomicBoolean stopped;
|
||||
// private final BlockingQueue<Map<String, Object>> eventQueue;
|
||||
// private final BlockingQueue<Map<String, Object>> immediateEventQueue;
|
||||
// private final Map<String, MyRunnable> eventHandlers;
|
||||
// private final Map<String, MyRunnable> immediateEventHandlers;
|
||||
// private final Map<Integer, BlockingQueue<Map<String, Object>>> methodResults;
|
||||
// /**
|
||||
// * 创建这个驱动的对象
|
||||
// */
|
||||
// @Getter
|
||||
// @Setter
|
||||
// private Occupant occupant;
|
||||
// private boolean alertFlag;
|
||||
// /**
|
||||
// * 会话驱动
|
||||
// */
|
||||
// private WebSocket ws;
|
||||
// private Thread handleImmediateEventThread;
|
||||
//
|
||||
// public Driver2(String tabId, String tabType, String address) {
|
||||
// this(tabId, tabType, address, null);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 驱动
|
||||
// *
|
||||
// * @param tabId 标签id
|
||||
// * @param tabType 标签页类型
|
||||
// * @param address 浏览器连接地址
|
||||
// */
|
||||
// public Driver2(String tabId, String tabType, String address, Occupant occupant) {
|
||||
// this.id = tabId;
|
||||
// this.address = address;
|
||||
// this.type = tabType;
|
||||
// this.occupant = occupant;
|
||||
// this.debug = true;
|
||||
// this.alertFlag = false;
|
||||
// this.websocketUrl = "ws://" + address + "/devtools/" + tabType + "/" + tabId;
|
||||
// this.curId = new AtomicInteger(0);
|
||||
// this.ws = null;
|
||||
//
|
||||
//
|
||||
// this.recvThread = new Thread(this::recvLoop);
|
||||
// this.handleEventThread = new Thread(this::handleEventLoop);
|
||||
// this.recvThread.setDaemon(true);
|
||||
// this.handleEventThread.setDaemon(true);
|
||||
// this.handleImmediateEventThread = null;
|
||||
//
|
||||
// this.stopped = new AtomicBoolean();
|
||||
//
|
||||
// this.eventHandlers = new ConcurrentHashMap<>();
|
||||
// this.immediateEventHandlers = new ConcurrentHashMap<>();
|
||||
// this.methodResults = new ConcurrentHashMap<>();
|
||||
// this.eventQueue = new LinkedBlockingQueue<>();
|
||||
// this.immediateEventQueue = new LinkedBlockingQueue<>();
|
||||
// start();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 发送信息到浏览器,并返回浏览器返回的信息
|
||||
// *
|
||||
// * @param message 发送给浏览器的数据
|
||||
// * @param timeout 超时时间,为null表示无时间
|
||||
// * @return 浏览器返回的数据
|
||||
// */
|
||||
// private JSONObject send(Map<String, Object> message, double timeout) {
|
||||
// message = new HashMap<>(message);
|
||||
// int wsId = curId.incrementAndGet();
|
||||
// message.put("id", wsId);
|
||||
// String messageJson = JSON.toJSONString(message);
|
||||
//
|
||||
// if (this.debug) System.out.println("发->" + messageJson);
|
||||
// //计算等待时间
|
||||
// long endTime = (long) (System.currentTimeMillis() + timeout * 1000L);
|
||||
// LinkedBlockingQueue<Map<String, Object>> value = new LinkedBlockingQueue<>();
|
||||
// methodResults.put(wsId, value);
|
||||
// try {
|
||||
// ws.send(messageJson);
|
||||
// if (timeout == 0) {
|
||||
// methodResults.remove(wsId);
|
||||
// return new JSONObject(Map.of("id", wsId, "result", Map.of()));
|
||||
// }
|
||||
// } catch (WebsocketNotConnectedException e) {
|
||||
// e.printStackTrace();
|
||||
// methodResults.remove(wsId);
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
// }
|
||||
// int i = 5;
|
||||
// long endTimes = System.currentTimeMillis() + 1000L;
|
||||
// while (!stopped.get()) {
|
||||
// try {
|
||||
// Map<String, Object> result = methodResults.get(wsId).poll(200, TimeUnit.MILLISECONDS);
|
||||
// if (result == null && System.currentTimeMillis() < endTimes) continue;
|
||||
// if (result == null && i > 0 && System.currentTimeMillis() > endTimes) {
|
||||
// i--;
|
||||
// endTimes = System.currentTimeMillis() + 1000L;
|
||||
// ws.send(messageJson);
|
||||
// continue;
|
||||
// }
|
||||
// methodResults.remove(wsId);
|
||||
// if (result == null) throw new NullPointerException();
|
||||
// return new JSONObject(result);
|
||||
// } catch (InterruptedException | NullPointerException | IllegalArgumentException e) {
|
||||
//// e.printStackTrace();
|
||||
// String string = message.get("method").toString();
|
||||
// if (alertFlag && string.startsWith("Input.") || string.startsWith("Runtime.")) {
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists"));
|
||||
// }
|
||||
// if (timeout > 0 && System.currentTimeMillis() > endTime) {
|
||||
// methodResults.remove(wsId);
|
||||
// return alertFlag ? new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists")) : new JSONObject(Map.of("error", Map.of("message", "timeout"), "type", "timeout"));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 接收浏览器信息的守护线程方法
|
||||
// */
|
||||
// private void recvLoop() {
|
||||
// while (!stopped.get()) {
|
||||
// JSONObject msg;
|
||||
// try {
|
||||
// String andSet = webSocketMsg.getAndSet(null);
|
||||
// if (andSet != null) {
|
||||
// msg = JSONObject.parseObject(andSet);
|
||||
// } else continue;
|
||||
// } catch (Exception e) {
|
||||
// if (stop()) return;
|
||||
// return;
|
||||
//
|
||||
// }
|
||||
// if (this.debug) System.out.println("<-收" + msg);
|
||||
//
|
||||
// if (msg.containsKey("method")) {
|
||||
// if (msg.getString("method").startsWith("Page.javascriptDialog")) {
|
||||
// alertFlag = msg.getString("method").endsWith("Opening");
|
||||
// }
|
||||
// MyRunnable function = immediateEventHandlers.get(msg.getString("method"));
|
||||
// if (function != null) {
|
||||
// this.handleImmediateEvent(function, msg.getOrDefault("params", new HashMap<>()));
|
||||
// } else {
|
||||
// try {
|
||||
// eventQueue.put(msg);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// int i = 1000;
|
||||
// Integer integer = msg.getInteger("id");
|
||||
// while (i-- > 0 && integer != null && !methodResults.containsKey(integer)) {
|
||||
// try {
|
||||
// Thread.sleep(10);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// if (methodResults.containsKey(integer)) {
|
||||
// try {
|
||||
// methodResults.get(integer).put(msg);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// } else if (this.debug) {
|
||||
// System.out.println("未知错误->" + msg);
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 当接收到浏览器信息,执行已绑定的方法
|
||||
// */
|
||||
// private void handleEventLoop() {
|
||||
// while (!stopped.get()) {
|
||||
// Map<String, Object> event;
|
||||
// try {
|
||||
// event = eventQueue.poll(1, TimeUnit.SECONDS);
|
||||
// } catch (InterruptedException e) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (event != null) {
|
||||
// MyRunnable function = eventHandlers.get(event.get("method").toString());
|
||||
// if (function != null) {
|
||||
// function.setMessage(event.get("params"));
|
||||
// function.run();
|
||||
// }
|
||||
// }
|
||||
// this.eventQueue.poll();
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void handleImmediateEventLoop() {
|
||||
// while (!stopped.get() && !immediateEventQueue.isEmpty()) {
|
||||
// Map<String, Object> event;
|
||||
// try {
|
||||
// event = immediateEventQueue.poll(1, TimeUnit.SECONDS);
|
||||
// } catch (InterruptedException e) {
|
||||
// e.printStackTrace();
|
||||
// continue;
|
||||
// }
|
||||
// if (event != null) {
|
||||
// MyRunnable function = immediateEventHandlers.get(event.get("method").toString());
|
||||
// if (function != null) {
|
||||
// function.setMessage(event.get("params"));
|
||||
// function.run();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理立即执行的动作
|
||||
// *
|
||||
// * @param function 要运行下方法
|
||||
// * @param params 方法参数
|
||||
// */
|
||||
// private void handleImmediateEvent(MyRunnable function, Object params) {
|
||||
// Map<String, Object> func = new HashMap<>();
|
||||
// func.put("method", function);
|
||||
// func.put("params", params);
|
||||
// immediateEventQueue.add(func);
|
||||
//
|
||||
// if (handleImmediateEventThread == null || !handleImmediateEventThread.isAlive()) {
|
||||
// handleImmediateEventThread = new Thread(this::handleImmediateEventLoop);
|
||||
// handleImmediateEventThread.setDaemon(true);
|
||||
// handleImmediateEventThread.start();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行cdp方法
|
||||
// *
|
||||
// * @param method 方法
|
||||
// * @return 执行结果
|
||||
// */
|
||||
// public Object run(String method) {
|
||||
// return run(method, new HashMap<>());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行cdp方法
|
||||
// *
|
||||
// * @param method 方法
|
||||
// * @param params 参数
|
||||
// * @return 执行结果
|
||||
// */
|
||||
// public Object run(String method, Map<String, Object> params) {
|
||||
// if (stopped.get()) return Map.of("error", "connection disconnected", "type", "connection_error");
|
||||
// params = new HashMap<>(params);
|
||||
// Object timeout1 = params.remove("_timeout");
|
||||
// double timeout = timeout1 != null ? Float.parseFloat(timeout1.toString()) : 30.0;
|
||||
//
|
||||
// JSONObject result = this.send(Map.of("method", method, "params", params), timeout);
|
||||
// if (!result.containsKey("result") && result.containsKey("error")) {
|
||||
// HashMap<String, Object> map = new HashMap<>();
|
||||
// map.put("error", result.getJSONObject("error").get("message"));
|
||||
// map.put("type", result.getOrDefault("type", "call_method_error"));
|
||||
// map.put("method", method);
|
||||
// map.put("args", params);
|
||||
// map.put("timeout", timeout);
|
||||
// return JSON.toJSONString(map);
|
||||
// } else {
|
||||
// return JSON.toJSONString(result.get("result"));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 启动连接
|
||||
// */
|
||||
// private void start() {
|
||||
// this.stopped.set(false);
|
||||
// try {
|
||||
// Request build = new Request(new HttpUrl("socket","","","",80,new ArrayList<>(),null,null,this.websocketUrl), "GET", Headers.of(), null, new HashMap<>()).newBuilder().url(this.websocketUrl).build();
|
||||
// OkHttpClient okHttpClient = new OkHttpClient();
|
||||
// ws = okHttpClient.newWebSocket(build, new WebSocketListener() {
|
||||
// @Override
|
||||
// public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||
// // 关闭事件处理
|
||||
// stop();
|
||||
// super.onClosed(webSocket, code, reason);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||
// super.onClosing(webSocket, code, reason);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
|
||||
// super.onFailure(webSocket, t, response);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
|
||||
// webSocketMsg.set(text);
|
||||
// super.onMessage(webSocket, text);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
|
||||
// super.onMessage(webSocket, bytes);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
|
||||
// super.onOpen(webSocket, response);
|
||||
// }
|
||||
//
|
||||
// });
|
||||
// recvThread.start();
|
||||
// handleEventThread.start();
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// stop();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 中断连接
|
||||
// */
|
||||
// public boolean stop() {
|
||||
// stop1();
|
||||
// while (this.recvThread.isAlive() || this.handleEventThread.isAlive()) {
|
||||
// try {
|
||||
// Thread.sleep(100);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 中断连接
|
||||
// */
|
||||
// private void stop1() {
|
||||
// if (stopped.get()) return;
|
||||
// stopped.set(true);
|
||||
// if (ws != null) {
|
||||
// ws.close(1000, "");
|
||||
// ws = null;
|
||||
// }
|
||||
// try {
|
||||
// while (!eventQueue.isEmpty()) {
|
||||
// Map<String, Object> event = eventQueue.poll();
|
||||
// MyRunnable method = eventHandlers.get(event.get("method").toString());
|
||||
// if (method != null) {
|
||||
// method.setMessage(event.get("params"));
|
||||
// method.run();
|
||||
// }
|
||||
// }
|
||||
// } catch (Exception ignored) {
|
||||
// }
|
||||
// eventHandlers.clear();
|
||||
// methodResults.clear();
|
||||
// eventQueue.clear();
|
||||
// if (occupant != null) occupant.onDisconnect();
|
||||
// }
|
||||
//
|
||||
// public void setCallback(String event, MyRunnable callback) {
|
||||
// setCallback(event, callback, false);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 绑定cdp event和回调方法
|
||||
// *
|
||||
// * @param event 方法名称
|
||||
// * @param callback 绑定到cdp event的回调方法
|
||||
// * @param immediate 是否要立即处理的动作
|
||||
// */
|
||||
// public void setCallback(String event, MyRunnable callback, boolean immediate) {
|
||||
// Map<String, MyRunnable> handler = immediate ? immediateEventHandlers : eventHandlers;
|
||||
// if (callback != null) handler.put(event, callback);
|
||||
// else handler.remove(event);
|
||||
// }
|
||||
}
|
426
java/src/main/java/com/ll/DrissonPage/base/Driver3.java
Normal file
426
java/src/main/java/com/ll/DrissonPage/base/Driver3.java
Normal file
@ -0,0 +1,426 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
/**
|
||||
* 驱动
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class Driver3 {
|
||||
// /**
|
||||
// * 标签id
|
||||
// */
|
||||
// @Getter
|
||||
// private final String id;
|
||||
// /**
|
||||
// * 浏览器连接地址
|
||||
// */
|
||||
// @Getter
|
||||
// private final String address;
|
||||
// /**
|
||||
// * 标签页类型
|
||||
// */
|
||||
// @Getter
|
||||
// private final String type;
|
||||
// private final boolean debug;
|
||||
// private final String websocketUrl;
|
||||
// private final AtomicInteger curId;
|
||||
// /**
|
||||
// * 会话返回值
|
||||
// */
|
||||
// private final AtomicReference<String> webSocketMsg = new AtomicReference<>(null);
|
||||
// private final Thread recvThread;
|
||||
// private final Thread handleEventThread;
|
||||
// @Getter
|
||||
//
|
||||
// private final AtomicBoolean stopped;
|
||||
// private final BlockingQueue<Map<String, Object>> eventQueue;
|
||||
// private final BlockingQueue<Map<String, Object>> immediateEventQueue;
|
||||
// private final Map<String, MyRunnable> eventHandlers;
|
||||
// private final Map<String, MyRunnable> immediateEventHandlers;
|
||||
// private final Map<Integer, BlockingQueue<Map<String, Object>>> methodResults;
|
||||
// /**
|
||||
// * 创建这个驱动的对象
|
||||
// */
|
||||
// @Getter
|
||||
// @Setter
|
||||
// private Occupant occupant;
|
||||
// private boolean alertFlag;
|
||||
// /**
|
||||
// * 会话驱动
|
||||
// */
|
||||
// private WebSocketClient ws;
|
||||
// private Future<Session> webSocketSession;
|
||||
// private Thread handleImmediateEventThread;
|
||||
//
|
||||
// public Driver3(String tabId, String tabType, String address) {
|
||||
// this(tabId, tabType, address, null);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 驱动
|
||||
// *
|
||||
// * @param tabId 标签id
|
||||
// * @param tabType 标签页类型
|
||||
// * @param address 浏览器连接地址
|
||||
// */
|
||||
// public Driver3(String tabId, String tabType, String address, Occupant occupant) {
|
||||
// this.id = tabId;
|
||||
// this.address = address;
|
||||
// this.type = tabType;
|
||||
// this.occupant = occupant;
|
||||
// this.debug = true;
|
||||
// this.alertFlag = false;
|
||||
// this.websocketUrl = "ws://" + address + "/devtools/" + tabType + "/" + tabId;
|
||||
// this.curId = new AtomicInteger(0);
|
||||
// this.ws = null;
|
||||
// this.recvThread = new Thread(this::recvLoop);
|
||||
// this.handleEventThread = new Thread(this::handleEventLoop);
|
||||
// this.recvThread.setDaemon(true);
|
||||
// this.handleEventThread.setDaemon(true);
|
||||
// this.handleImmediateEventThread = null;
|
||||
//
|
||||
// this.stopped = new AtomicBoolean();
|
||||
//
|
||||
// this.eventHandlers = new ConcurrentHashMap<>();
|
||||
// this.immediateEventHandlers = new ConcurrentHashMap<>();
|
||||
// this.methodResults = new ConcurrentHashMap<>();
|
||||
// this.eventQueue = new LinkedBlockingQueue<>();
|
||||
// this.immediateEventQueue = new LinkedBlockingQueue<>();
|
||||
// start();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 发送信息到浏览器,并返回浏览器返回的信息
|
||||
// *
|
||||
// * @param message 发送给浏览器的数据
|
||||
// * @param timeout 超时时间,为null表示无时间
|
||||
// * @return 浏览器返回的数据
|
||||
// */
|
||||
// private JSONObject send(Map<String, Object> message, double timeout) {
|
||||
// message = new HashMap<>(message);
|
||||
// int wsId = curId.incrementAndGet();
|
||||
// message.put("id", wsId);
|
||||
// String messageJson = JSON.toJSONString(message);
|
||||
//
|
||||
// if (this.debug) System.out.println("发->" + messageJson);
|
||||
// //计算等待时间
|
||||
// long endTime = (long) (System.currentTimeMillis() + timeout * 1000L);
|
||||
// LinkedBlockingQueue<Map<String, Object>> value = new LinkedBlockingQueue<>();
|
||||
// methodResults.put(wsId, value);
|
||||
// try {
|
||||
// if (ws.isStopping()) return new JSONObject();
|
||||
// webSocketSession.get().getRemote().sendString(messageJson);
|
||||
// if (timeout == 0) {
|
||||
// methodResults.remove(wsId);
|
||||
// return new JSONObject(Map.of("id", wsId, "result", Map.of()));
|
||||
// }
|
||||
// } catch (WebsocketNotConnectedException | IOException | InterruptedException | ExecutionException e) {
|
||||
// e.printStackTrace();
|
||||
// methodResults.remove(wsId);
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
// }
|
||||
// int i = 5;
|
||||
// long endTimes = System.currentTimeMillis() + 1000L;
|
||||
// while (!stopped.get()) {
|
||||
// try {
|
||||
// Map<String, Object> result = methodResults.get(wsId).poll(2000, TimeUnit.MILLISECONDS);
|
||||
// if (result == null && System.currentTimeMillis() < endTimes) continue;
|
||||
// if (result == null && i > 0 && System.currentTimeMillis() > endTimes) {
|
||||
// System.out.println("丢包:->" + messageJson);
|
||||
// i--;
|
||||
// endTimes = System.currentTimeMillis() + 1000L;
|
||||
// if (ws.isStopping()) return new JSONObject();
|
||||
// webSocketSession.get().getRemote().sendString(messageJson);
|
||||
// continue;
|
||||
// }
|
||||
// methodResults.remove(wsId);
|
||||
// if (result == null) throw new NullPointerException();
|
||||
// return new JSONObject(result);
|
||||
// } catch (InterruptedException | NullPointerException | IllegalArgumentException | IOException |
|
||||
// ExecutionException e) {
|
||||
//// e.printStackTrace();
|
||||
// String string = message.get("method").toString();
|
||||
// if (alertFlag && string.startsWith("Input.") || string.startsWith("Runtime.")) {
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists"));
|
||||
// }
|
||||
// if (timeout > 0 && System.currentTimeMillis() > endTime) {
|
||||
// methodResults.remove(wsId);
|
||||
// return alertFlag ? new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists")) : new JSONObject(Map.of("error", Map.of("message", "timeout"), "type", "timeout"));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 接收浏览器信息的守护线程方法
|
||||
// */
|
||||
// private void recvLoop() {
|
||||
// while (!stopped.get()) {
|
||||
// JSONObject msg;
|
||||
// try {
|
||||
// String andSet = webSocketMsg.getAndSet(null);
|
||||
// if (andSet != null) {
|
||||
// msg = JSONObject.parseObject(andSet);
|
||||
// } else continue;
|
||||
// } catch (Exception e) {
|
||||
// if (stop()) return;
|
||||
// return;
|
||||
//
|
||||
// }
|
||||
// if (this.debug) System.out.println("<-收" + msg);
|
||||
//
|
||||
// if (msg.containsKey("method")) {
|
||||
// if (msg.getString("method").startsWith("Page.javascriptDialog")) {
|
||||
// alertFlag = msg.getString("method").endsWith("Opening");
|
||||
// }
|
||||
// MyRunnable function = immediateEventHandlers.get(msg.getString("method"));
|
||||
// if (function != null) {
|
||||
// this.handleImmediateEvent(function, msg.getOrDefault("params", new HashMap<>()));
|
||||
// } else {
|
||||
// try {
|
||||
// eventQueue.put(msg);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// int i = 1000;
|
||||
// Integer integer = msg.getInteger("id");
|
||||
// while (i-- > 0 && integer != null && !methodResults.containsKey(integer)) {
|
||||
// try {
|
||||
// Thread.sleep(10);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// if (methodResults.containsKey(integer)) {
|
||||
// try {
|
||||
// methodResults.get(integer).put(msg);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// } else if (this.debug) {
|
||||
// System.out.println("未知错误->" + msg);
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 当接收到浏览器信息,执行已绑定的方法
|
||||
// */
|
||||
// private void handleEventLoop() {
|
||||
// while (!stopped.get()) {
|
||||
// Map<String, Object> event;
|
||||
// try {
|
||||
// event = eventQueue.poll(1, TimeUnit.SECONDS);
|
||||
// } catch (InterruptedException e) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (event != null) {
|
||||
// MyRunnable function = eventHandlers.get(event.get("method").toString());
|
||||
// if (function != null) {
|
||||
// function.setMessage(event.get("params"));
|
||||
// function.run();
|
||||
// }
|
||||
// }
|
||||
// this.eventQueue.poll();
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void handleImmediateEventLoop() {
|
||||
// while (!stopped.get() && !immediateEventQueue.isEmpty()) {
|
||||
// Map<String, Object> event;
|
||||
// try {
|
||||
// event = immediateEventQueue.poll(1, TimeUnit.SECONDS);
|
||||
// } catch (InterruptedException e) {
|
||||
// e.printStackTrace();
|
||||
// continue;
|
||||
// }
|
||||
// if (event != null) {
|
||||
// MyRunnable function = immediateEventHandlers.get(event.get("method").toString());
|
||||
// if (function != null) {
|
||||
// function.setMessage(event.get("params"));
|
||||
// function.run();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理立即执行的动作
|
||||
// *
|
||||
// * @param function 要运行下方法
|
||||
// * @param params 方法参数
|
||||
// */
|
||||
// private void handleImmediateEvent(MyRunnable function, Object params) {
|
||||
// Map<String, Object> func = new HashMap<>();
|
||||
// func.put("method", function);
|
||||
// func.put("params", params);
|
||||
// immediateEventQueue.add(func);
|
||||
//
|
||||
// if (handleImmediateEventThread == null || !handleImmediateEventThread.isAlive()) {
|
||||
// handleImmediateEventThread = new Thread(this::handleImmediateEventLoop);
|
||||
// handleImmediateEventThread.setDaemon(true);
|
||||
// handleImmediateEventThread.start();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行cdp方法
|
||||
// *
|
||||
// * @param method 方法
|
||||
// * @return 执行结果
|
||||
// */
|
||||
// public Object run(String method) {
|
||||
// return run(method, new HashMap<>());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行cdp方法
|
||||
// *
|
||||
// * @param method 方法
|
||||
// * @param params 参数
|
||||
// * @return 执行结果
|
||||
// */
|
||||
// public Object run(String method, Map<String, Object> params) {
|
||||
// if (stopped.get()) return Map.of("error", "connection disconnected", "type", "connection_error");
|
||||
// params = new HashMap<>(params);
|
||||
// Object timeout1 = params.remove("_timeout");
|
||||
// double timeout = timeout1 != null ? Float.parseFloat(timeout1.toString()) : 30.0;
|
||||
//
|
||||
// JSONObject result = this.send(Map.of("method", method, "params", params), timeout);
|
||||
// if (!result.containsKey("result") && result.containsKey("error")) {
|
||||
// HashMap<String, Object> map = new HashMap<>();
|
||||
// map.put("error", result.getJSONObject("error").get("message"));
|
||||
// map.put("type", result.getOrDefault("type", "call_method_error"));
|
||||
// map.put("method", method);
|
||||
// map.put("args", params);
|
||||
// map.put("timeout", timeout);
|
||||
// return JSON.toJSONString(map);
|
||||
// } else {
|
||||
// return JSON.toJSONString(result.get("result"));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 启动连接
|
||||
// */
|
||||
// private void start() {
|
||||
// this.stopped.set(false);
|
||||
// try {
|
||||
//// Request build = new Request(new HttpUrl("socket","","","",80,new ArrayList<>(),null,null,this.websocketUrl), "GET", Headers.of(), null, new HashMap<>()).newBuilder().url(this.websocketUrl).build();
|
||||
//// OkHttpClient okHttpClient = new OkHttpClient();
|
||||
// ws = new WebSocketClient();
|
||||
// ws.setConnectTimeout(60_000);
|
||||
// ws.start();
|
||||
// webSocketSession = ws.connect(new WebSocketAdapter() {
|
||||
// @Override
|
||||
// public void onWebSocketClose(int statusCode, String reason) {
|
||||
// stop();
|
||||
// super.onWebSocketClose(statusCode, reason);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onWebSocketConnect(Session sess) {
|
||||
// super.onWebSocketConnect(sess);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onWebSocketError(Throwable cause) {
|
||||
// super.onWebSocketError(cause);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public boolean isConnected() {
|
||||
// return super.isConnected();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onWebSocketText(String message) {
|
||||
// webSocketMsg.set(message);
|
||||
// }
|
||||
//
|
||||
// }, URI.create(this.websocketUrl));
|
||||
// recvThread.start();
|
||||
// handleEventThread.start();
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// stop();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 中断连接
|
||||
// */
|
||||
// public boolean stop() {
|
||||
// stop1();
|
||||
// while (this.recvThread.isAlive() || this.handleEventThread.isAlive()) {
|
||||
// try {
|
||||
// Thread.sleep(100);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 中断连接
|
||||
// */
|
||||
// private void stop1() {
|
||||
// if (stopped.get()) return;
|
||||
// stopped.set(true);
|
||||
// if (ws != null) {
|
||||
// try {
|
||||
// ws.stop();
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// ws = null;
|
||||
// }
|
||||
// try {
|
||||
// while (!eventQueue.isEmpty()) {
|
||||
// Map<String, Object> event = eventQueue.poll();
|
||||
// MyRunnable method = eventHandlers.get(event.get("method").toString());
|
||||
// if (method != null) {
|
||||
// method.setMessage(event.get("params"));
|
||||
// method.run();
|
||||
// }
|
||||
// }
|
||||
// } catch (Exception ignored) {
|
||||
// }
|
||||
// eventHandlers.clear();
|
||||
// methodResults.clear();
|
||||
// eventQueue.clear();
|
||||
// if (occupant != null) occupant.onDisconnect();
|
||||
// }
|
||||
//
|
||||
// public void setCallback(String event, MyRunnable callback) {
|
||||
// setCallback(event, callback, false);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 绑定cdp event和回调方法
|
||||
// *
|
||||
// * @param event 方法名称
|
||||
// * @param callback 绑定到cdp event的回调方法
|
||||
// * @param immediate 是否要立即处理的动作
|
||||
// */
|
||||
// public void setCallback(String event, MyRunnable callback, boolean immediate) {
|
||||
// Map<String, MyRunnable> handler = immediate ? immediateEventHandlers : eventHandlers;
|
||||
// if (callback != null) handler.put(event, callback);
|
||||
// else handler.remove(event);
|
||||
// }
|
||||
}
|
@ -0,0 +1,424 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
/**
|
||||
* 驱动 org.java-websocket
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class Driver_org_webSocket {
|
||||
// /**
|
||||
// * 标签id
|
||||
// */
|
||||
// @Getter
|
||||
// private final String id;
|
||||
// /**
|
||||
// * 浏览器连接地址
|
||||
// */
|
||||
// @Getter
|
||||
// private final String address;
|
||||
// /**
|
||||
// * 标签页类型
|
||||
// */
|
||||
// @Getter
|
||||
// private final String type;
|
||||
// private final boolean debug;
|
||||
// private final String websocketUrl;
|
||||
// private final AtomicInteger curId;
|
||||
// /**
|
||||
// * 会话返回值
|
||||
// */
|
||||
// private final AtomicReference<String> webSocketMsg = new AtomicReference<>(null);
|
||||
// private final Thread recvThread;
|
||||
// private final Thread handleEventThread;
|
||||
// @Getter
|
||||
//
|
||||
// private final AtomicBoolean stopped;
|
||||
// private final BlockingQueue<Map<String, Object>> eventQueue;
|
||||
// private final BlockingQueue<Map<String, Object>> immediateEventQueue;
|
||||
// private final Map<String, MyRunnable> eventHandlers;
|
||||
// private final Map<String, MyRunnable> immediateEventHandlers;
|
||||
// private final Map<Integer, BlockingQueue<Map<String, Object>>> methodResults;
|
||||
// /**
|
||||
// * 创建这个驱动的对象
|
||||
// */
|
||||
// @Getter
|
||||
// @Setter
|
||||
// private Occupant occupant;
|
||||
// private boolean alertFlag;
|
||||
// /**
|
||||
// * 会话驱动
|
||||
// */
|
||||
// private WebSocketClient ws;
|
||||
// private Thread handleImmediateEventThread;
|
||||
//
|
||||
// public Driver(String tabId, String tabType, String address) {
|
||||
// this(tabId, tabType, address, null);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 驱动
|
||||
// *
|
||||
// * @param tabId 标签id
|
||||
// * @param tabType 标签页类型
|
||||
// * @param address 浏览器连接地址
|
||||
// */
|
||||
// public Driver(String tabId, String tabType, String address, Occupant occupant) {
|
||||
// this.id = tabId;
|
||||
// this.address = address;
|
||||
// this.type = tabType;
|
||||
// this.occupant = occupant;
|
||||
// this.debug = true;
|
||||
// this.alertFlag = false;
|
||||
// this.websocketUrl = "ws://" + address + "/devtools/" + tabType + "/" + tabId;
|
||||
// this.curId = new AtomicInteger(0);
|
||||
// this.ws = null;
|
||||
//
|
||||
//
|
||||
// this.recvThread = new Thread(this::recvLoop);
|
||||
// this.handleEventThread = new Thread(this::handleEventLoop);
|
||||
// this.recvThread.setDaemon(true);
|
||||
// this.handleEventThread.setDaemon(true);
|
||||
// this.handleImmediateEventThread = null;
|
||||
//
|
||||
// this.stopped = new AtomicBoolean();
|
||||
//
|
||||
// this.eventHandlers = new ConcurrentHashMap<>();
|
||||
// this.immediateEventHandlers = new ConcurrentHashMap<>();
|
||||
// this.methodResults = new ConcurrentHashMap<>();
|
||||
// this.eventQueue = new LinkedBlockingQueue<>();
|
||||
// this.immediateEventQueue = new LinkedBlockingQueue<>();
|
||||
// start();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 发送信息到浏览器,并返回浏览器返回的信息
|
||||
// *
|
||||
// * @param message 发送给浏览器的数据
|
||||
// * @param timeout 超时时间,为null表示无时间
|
||||
// * @return 浏览器返回的数据
|
||||
// */
|
||||
// private JSONObject send(Map<String, Object> message, double timeout) {
|
||||
// message = new HashMap<>(message);
|
||||
// int wsId = curId.incrementAndGet();
|
||||
// message.put("id", wsId);
|
||||
// String messageJson = JSON.toJSONString(message);
|
||||
//
|
||||
// if (this.debug) System.out.println("发->" + messageJson);
|
||||
// //计算等待时间
|
||||
// long endTime = (long) (System.currentTimeMillis() + timeout * 1000L);
|
||||
// LinkedBlockingQueue<Map<String, Object>> value = new LinkedBlockingQueue<>();
|
||||
// methodResults.put(wsId, value);
|
||||
// try {
|
||||
// ws.send(messageJson);
|
||||
// if (timeout == 0) {
|
||||
// methodResults.remove(wsId);
|
||||
// return new JSONObject(Map.of("id", wsId, "result", Map.of()));
|
||||
// }
|
||||
// } catch (WebsocketNotConnectedException e) {
|
||||
// e.printStackTrace();
|
||||
// methodResults.remove(wsId);
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
// }
|
||||
// int i = 5;
|
||||
// long endTimes = System.currentTimeMillis() + 1000L;
|
||||
// while (!stopped.get()) {
|
||||
// try {
|
||||
// Map<String, Object> result = methodResults.get(wsId).poll(10_000, TimeUnit.MILLISECONDS);
|
||||
// if (result == null && System.currentTimeMillis() < endTimes) continue;
|
||||
// if (result == null && i > 0 && System.currentTimeMillis() > endTimes) {
|
||||
// i--;
|
||||
// endTimes = System.currentTimeMillis() + 1000L;
|
||||
// System.out.println("超时丢包:->" + messageJson);
|
||||
// ws.send(messageJson);
|
||||
// continue;
|
||||
// }
|
||||
// methodResults.remove(wsId);
|
||||
// if (result == null) throw new NullPointerException();
|
||||
// return new JSONObject(result);
|
||||
// } catch (InterruptedException | NullPointerException | IllegalArgumentException e) {
|
||||
//// e.printStackTrace();
|
||||
// String string = message.get("method").toString();
|
||||
// if (alertFlag && string.startsWith("Input.") || string.startsWith("Runtime.")) {
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists"));
|
||||
// }
|
||||
// if (timeout > 0 && System.currentTimeMillis() > endTime) {
|
||||
// methodResults.remove(wsId);
|
||||
// return alertFlag ? new JSONObject(Map.of("error", Map.of("message", "alert exists."), "type", "alert_exists")) : new JSONObject(Map.of("error", Map.of("message", "timeout"), "type", "timeout"));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new JSONObject(Map.of("error", Map.of("message", "connection disconnected"), "type", "connection_error"));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 接收浏览器信息的守护线程方法
|
||||
// */
|
||||
// private void recvLoop() {
|
||||
// while (!stopped.get()) {
|
||||
// JSONObject msg;
|
||||
// try {
|
||||
// String andSet = webSocketMsg.getAndSet(null);
|
||||
// if (andSet != null) {
|
||||
// msg = JSONObject.parseObject(andSet);
|
||||
// } else continue;
|
||||
// } catch (Exception e) {
|
||||
// if (stop()) return;
|
||||
// return;
|
||||
//
|
||||
// }
|
||||
// if (this.debug) System.out.println("<-收" + msg);
|
||||
//
|
||||
// if (msg.containsKey("method")) {
|
||||
// if (msg.getString("method").startsWith("Page.javascriptDialog")) {
|
||||
// alertFlag = msg.getString("method").endsWith("Opening");
|
||||
// }
|
||||
// MyRunnable function = immediateEventHandlers.get(msg.getString("method"));
|
||||
// if (function != null) {
|
||||
// this.handleImmediateEvent(function, msg.getOrDefault("params", new HashMap<>()));
|
||||
// } else {
|
||||
// try {
|
||||
// eventQueue.put(msg);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// int i = 1000;
|
||||
// Integer integer = msg.getInteger("id");
|
||||
// while (i-- > 0 && integer != null && !methodResults.containsKey(integer)) {
|
||||
// try {
|
||||
// Thread.sleep(10);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// if (methodResults.containsKey(integer)) {
|
||||
// try {
|
||||
// methodResults.get(integer).put(msg);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// } else if (this.debug) {
|
||||
// System.out.println("未知错误->" + msg);
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 当接收到浏览器信息,执行已绑定的方法
|
||||
// */
|
||||
// private void handleEventLoop() {
|
||||
// while (!stopped.get()) {
|
||||
// Map<String, Object> event;
|
||||
// try {
|
||||
// event = eventQueue.poll(1, TimeUnit.SECONDS);
|
||||
// } catch (InterruptedException e) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (event != null) {
|
||||
// MyRunnable function = eventHandlers.get(event.get("method").toString());
|
||||
// if (function != null) {
|
||||
// function.setMessage(event.get("params"));
|
||||
// function.run();
|
||||
// }
|
||||
// }
|
||||
// this.eventQueue.poll();
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void handleImmediateEventLoop() {
|
||||
// while (!stopped.get() && !immediateEventQueue.isEmpty()) {
|
||||
// Map<String, Object> event;
|
||||
// try {
|
||||
// event = immediateEventQueue.poll(1, TimeUnit.SECONDS);
|
||||
// } catch (InterruptedException e) {
|
||||
// e.printStackTrace();
|
||||
// continue;
|
||||
// }
|
||||
// if (event != null) {
|
||||
// MyRunnable function = immediateEventHandlers.get(event.get("method").toString());
|
||||
// if (function != null) {
|
||||
// function.setMessage(event.get("params"));
|
||||
// function.run();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理立即执行的动作
|
||||
// *
|
||||
// * @param function 要运行下方法
|
||||
// * @param params 方法参数
|
||||
// */
|
||||
// private void handleImmediateEvent(MyRunnable function, Object params) {
|
||||
// Map<String, Object> func = new HashMap<>();
|
||||
// func.put("method", function);
|
||||
// func.put("params", params);
|
||||
// immediateEventQueue.add(func);
|
||||
//
|
||||
// if (handleImmediateEventThread == null || !handleImmediateEventThread.isAlive()) {
|
||||
// handleImmediateEventThread = new Thread(this::handleImmediateEventLoop);
|
||||
// handleImmediateEventThread.setDaemon(true);
|
||||
// handleImmediateEventThread.start();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行cdp方法
|
||||
// *
|
||||
// * @param method 方法
|
||||
// * @return 执行结果
|
||||
// */
|
||||
// public Object run(String method) {
|
||||
// return run(method, new HashMap<>());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行cdp方法
|
||||
// *
|
||||
// * @param method 方法
|
||||
// * @param params 参数
|
||||
// * @return 执行结果
|
||||
// */
|
||||
// public Object run(String method, Map<String, Object> params) {
|
||||
// if (stopped.get()) return Map.of("error", "connection disconnected", "type", "connection_error");
|
||||
// params = new HashMap<>(params);
|
||||
// Object timeout1 = params.remove("_timeout");
|
||||
// double timeout = timeout1 != null ? Float.parseFloat(timeout1.toString()) : 30.0;
|
||||
//
|
||||
// JSONObject result = this.send(Map.of("method", method, "params", params), timeout);
|
||||
// if (!result.containsKey("result") && result.containsKey("error")) {
|
||||
// HashMap<String, Object> map = new HashMap<>();
|
||||
// map.put("error", result.getJSONObject("error").get("message"));
|
||||
// map.put("type", result.getOrDefault("type", "call_method_error"));
|
||||
// map.put("method", method);
|
||||
// map.put("args", params);
|
||||
// map.put("timeout", timeout);
|
||||
// return JSON.toJSONString(map);
|
||||
// } else {
|
||||
// return JSON.toJSONString(result.get("result"));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 启动连接
|
||||
// */
|
||||
// private void start() {
|
||||
// this.stopped.set(false);
|
||||
// try {
|
||||
// ws = new WebSocketClient(new URI(websocketUrl)) {
|
||||
// @Override
|
||||
// public void onOpen(ServerHandshake handshakeData) {
|
||||
// // 处理 WebSocket 打开事件
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onMessage(String message) {
|
||||
// //处理返回数据
|
||||
// webSocketMsg.set(message);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// @Override
|
||||
// public void onClose(int code, String reason, boolean remote) {
|
||||
//
|
||||
// System.out.println("关闭" + reason);
|
||||
// // 关闭事件处理
|
||||
// stop();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onError(Exception ex) {
|
||||
// System.out.println("错误" + ex.getMessage());
|
||||
// // 错误事件处理
|
||||
//// stop();
|
||||
// }
|
||||
// };
|
||||
// ws.setConnectionLostTimeout(60);
|
||||
// ws.connect();
|
||||
// //需要睡0.1秒让其等待
|
||||
// while (ws != null && !ws.getReadyState().equals(ReadyState.OPEN)) {
|
||||
// Thread.sleep(10);
|
||||
// }
|
||||
// recvThread.start();
|
||||
// handleEventThread.start();
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// stop();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 中断连接
|
||||
// */
|
||||
// public boolean stop() {
|
||||
// stop1();
|
||||
// while (this.recvThread.isAlive() || this.handleEventThread.isAlive()) {
|
||||
// try {
|
||||
// Thread.sleep(100);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 中断连接
|
||||
// */
|
||||
// private void stop1() {
|
||||
// if (stopped.get()) {
|
||||
// return;
|
||||
// }
|
||||
// stopped.set(true);
|
||||
// if (ws != null) {
|
||||
// ws.close();
|
||||
// ws = null;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// while (!eventQueue.isEmpty()) {
|
||||
// Map<String, Object> event = eventQueue.poll();
|
||||
// MyRunnable method = eventHandlers.get(event.get("method").toString());
|
||||
// if (method != null) {
|
||||
// method.setMessage(event.get("params"));
|
||||
// method.run();
|
||||
// }
|
||||
// }
|
||||
// } catch (Exception ignored) {
|
||||
// }
|
||||
// eventHandlers.clear();
|
||||
// methodResults.clear();
|
||||
// eventQueue.clear();
|
||||
// if (occupant != null) occupant.onDisconnect();
|
||||
// }
|
||||
//
|
||||
// public void setCallback(String event, MyRunnable callback) {
|
||||
// setCallback(event, callback, false);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 绑定cdp event和回调方法
|
||||
// *
|
||||
// * @param event 方法名称
|
||||
// * @param callback 绑定到cdp event的回调方法
|
||||
// * @param immediate 是否要立即处理的动作
|
||||
// */
|
||||
// public void setCallback(String event, MyRunnable callback, boolean immediate) {
|
||||
// Map<String, MyRunnable> handler = immediate ? immediateEventHandlers : eventHandlers;
|
||||
// if (callback != null) handler.put(event, callback);
|
||||
// else handler.remove(event);
|
||||
// }
|
||||
}
|
17
java/src/main/java/com/ll/DrissonPage/base/ElePathMode.java
Normal file
17
java/src/main/java/com/ll/DrissonPage/base/ElePathMode.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public enum ElePathMode {
|
||||
C("css"), CSS("css"), X("xpath"), XPATH("xpath");
|
||||
private final String mode;
|
||||
|
||||
ElePathMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
14
java/src/main/java/com/ll/DrissonPage/base/MyRunnable.java
Normal file
14
java/src/main/java/com/ll/DrissonPage/base/MyRunnable.java
Normal file
@ -0,0 +1,14 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
public abstract class MyRunnable implements Runnable {
|
||||
private Object message;
|
||||
}
|
10
java/src/main/java/com/ll/DrissonPage/base/Occupant.java
Normal file
10
java/src/main/java/com/ll/DrissonPage/base/Occupant.java
Normal file
@ -0,0 +1,10 @@
|
||||
package com.ll.DrissonPage.base;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public interface Occupant {
|
||||
default void onDisconnect() {
|
||||
}
|
||||
}
|
@ -0,0 +1,747 @@
|
||||
package com.ll.DrissonPage.config;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import lombok.Getter;
|
||||
import org.ini4j.Wini;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
@Getter
|
||||
public class ChromiumOptions {
|
||||
private final String iniPath; //ini文件路径
|
||||
private final Map<String, Double> timeouts; // 返回timeouts设置
|
||||
private final List<String> arguments; // 返回浏览器命令行设置列表
|
||||
private final List<String> extensions; // 以list形式返回要加载的插件路径
|
||||
private final Map<String, Object> pres; // 返回用户首选项配置
|
||||
private final List<String> presToDel; //删除用户配置文件中已设置的项
|
||||
private final Map<String, String> flags; // 返回实验项配置
|
||||
private String downloadPath; // 默认下载路径文件路径
|
||||
private String browserPath; // 浏览器启动文件路径
|
||||
private String userDataPath; // 返回用户数据文件夹路径
|
||||
private String tmpPath; // 返回临时文件夹路径
|
||||
private String user; // 返回用户配置文件夹名称
|
||||
private String loadMode; // 返回页面加载策略,'normal', 'eager', 'none'
|
||||
private String proxy; // 返回代理设置
|
||||
private String address; // 返回浏览器地址,ip:port
|
||||
private boolean systemUserPath; // 返回是否使用系统安装的浏览器所使用的用户数据文件夹
|
||||
|
||||
private boolean existingOnly; // 返回是否只接管现有浏览器方式
|
||||
|
||||
private boolean autoPort; // 返回是否使用自动端口和用户文件
|
||||
|
||||
private int retryTimes; // 返回连接失败时的重试次数
|
||||
|
||||
private int retryInterval; // 返回连接失败时的重试间隔(秒)
|
||||
private boolean clearFileFlags;// 删除浏览器配置文件中已设置的实验项
|
||||
private boolean headless;//设置是否隐藏浏览器界面
|
||||
|
||||
public ChromiumOptions() {
|
||||
this(true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param readFile 是否从默认ini文件中读取配置信息
|
||||
* @param iniPath ini文件路径,为None则读取默认ini文件
|
||||
*/
|
||||
public ChromiumOptions(boolean readFile, String iniPath) {
|
||||
// 构造方法实现部分
|
||||
this.userDataPath = null;
|
||||
this.user = "Default";
|
||||
this.presToDel = new ArrayList<>();
|
||||
this.clearFileFlags = false;
|
||||
this.headless = false;
|
||||
if (readFile) {
|
||||
// 从文件中读取配置信息的实现部分
|
||||
OptionsManager om = new OptionsManager(iniPath);
|
||||
this.iniPath = om.getIniPath();
|
||||
// 从OptionsManager获取配置信息
|
||||
Wini ini = om.getIni();
|
||||
this.downloadPath = ini.get("paths", "download_path");
|
||||
this.tmpPath = ini.get("paths", "tmp_path");
|
||||
this.arguments = JSON.parseArray(ini.get("chromium_options", "arguments"), String.class);
|
||||
this.browserPath = ini.get("chromium_options", "browser_path");
|
||||
this.extensions = JSON.parseArray(ini.get("chromium_options", "extensions"), String.class);
|
||||
this.pres = JSON.parseObject(ini.get("chromium_options", "prefs"), new TypeReference<>() {
|
||||
});
|
||||
this.flags = JSON.parseObject(ini.get("chromium_options", "flags"), new TypeReference<>() {
|
||||
});
|
||||
this.address = ini.get("chromium_options", "address");
|
||||
String s = ini.get("chromium_options", "load_mode");
|
||||
this.loadMode = s != null ? s : "normal";
|
||||
s = ini.get("chromium_options", "system_user_path");
|
||||
this.systemUserPath = Boolean.parseBoolean(s);
|
||||
s = ini.get("chromium_options", "existing_only");
|
||||
this.existingOnly = Boolean.parseBoolean(s);
|
||||
s = ini.get("proxies", "http");
|
||||
this.proxy = s != null ? s : ini.get("proxies", "https");
|
||||
|
||||
boolean userPathSet = false;
|
||||
boolean userSet = false;
|
||||
|
||||
for (String arg : this.arguments) {
|
||||
if (arg.startsWith("--user-data-dir=")) {
|
||||
setPaths(arg.substring(16));
|
||||
userPathSet = true;
|
||||
}
|
||||
if (arg.startsWith("--profile-directory=")) {
|
||||
setUser(arg.substring(20));
|
||||
userSet = true;
|
||||
}
|
||||
if (userSet && userPathSet) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
s = ini.get("timeouts", "base");
|
||||
String s1 = ini.get("timeouts", "page_load");
|
||||
String s2 = ini.get("timeouts", "script");
|
||||
this.timeouts = Map.of("base", s1 != null ? Double.parseDouble(s) : 10.0, "pageLoad", s1 != null ? Double.parseDouble(s1) : 20.0, "script", s2 != null ? Double.parseDouble(s2) : 30.0);
|
||||
s = ini.get("chromium_options", "auto_port");
|
||||
this.autoPort = Boolean.parseBoolean(s);
|
||||
if (this.autoPort) {
|
||||
// 使用自动端口和用户文件
|
||||
PortFinder.PortInfo portInfo = new PortFinder().getPort();
|
||||
this.address = "127.0.0.1:" + portInfo.getPort();
|
||||
setArgument("--user-data-dir", portInfo.getPath());
|
||||
}
|
||||
|
||||
this.retryTimes = Integer.parseInt(ini.get("others").getOrDefault("retry_times", "3"));
|
||||
this.retryInterval = Integer.parseInt(ini.get("others").getOrDefault("retry_interval", "2"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认值初始化
|
||||
this.iniPath = null;
|
||||
this.browserPath = "chrome";
|
||||
this.arguments = new ArrayList<>();
|
||||
this.downloadPath = null;
|
||||
this.tmpPath = null;
|
||||
this.extensions = new ArrayList<>();
|
||||
this.pres = new HashMap<>();
|
||||
this.flags = new HashMap<>();
|
||||
this.timeouts = new HashMap<>();
|
||||
this.timeouts.put("base", 10.0);
|
||||
this.timeouts.put("pageLoad", 20.0);
|
||||
this.timeouts.put("script", 30.0);
|
||||
this.address = "127.0.0.1:9222";
|
||||
this.loadMode = "normal";
|
||||
this.proxy = null;
|
||||
this.autoPort = false;
|
||||
this.systemUserPath = false;
|
||||
this.existingOnly = false;
|
||||
this.retryTimes = 3;
|
||||
this.retryInterval = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接失败时的重试操作
|
||||
*
|
||||
* @param times 重试次数
|
||||
* @param interval 重试间隔
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setRetry(Integer times, Integer interval) {
|
||||
if (times != null && times >= 0) this.retryTimes = times;
|
||||
if (interval != null && interval >= 0) this.retryInterval = interval;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器配置的 argument 属性
|
||||
*
|
||||
* @param arg 属性名
|
||||
*/
|
||||
public ChromiumOptions setArgument(String arg) {
|
||||
// 返回当前对象
|
||||
return setArgument(arg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器配置的 argument 属性
|
||||
*
|
||||
* @param arg 属性名
|
||||
* @param value 属性值,有值的属性传入值,没有的传入 null,如传入 false,删除该项
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setArgument(String arg, String value) {
|
||||
// 调用 removeArgument 方法删除已有的同名属性
|
||||
removeArgument(arg);
|
||||
if (value == null && arg.equals("--headless")) {
|
||||
// 如果属性是 "--headless" 且值为 null,则将 "--headless=new" 添加到 _arguments 列表中
|
||||
arguments.add("--headless=new");
|
||||
} else {
|
||||
// 否则,根据是否有值构造属性字符串,添加到 _arguments 列表中
|
||||
arguments.add(value != null ? arg + "=" + value : arg);
|
||||
}
|
||||
|
||||
// 返回当前对象
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个 argument 项
|
||||
*
|
||||
* @param value 设置项名,有值的设置项传入设置名称即可
|
||||
* @return 本身
|
||||
*/
|
||||
public ChromiumOptions removeArgument(String value) {
|
||||
List<String> delList = new ArrayList<>();
|
||||
for (String argument : arguments)
|
||||
if (argument.equals(value) || argument.startsWith(value + "=")) delList.add(argument);
|
||||
arguments.removeAll(delList);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加插件
|
||||
*
|
||||
* @param path 插件路径,可指向文件夹
|
||||
*/
|
||||
public ChromiumOptions addExtension(String path) throws IOException {
|
||||
Path extensionPath = Paths.get(path);
|
||||
if (!Files.exists(extensionPath)) throw new IOException("插件路径不存在。");
|
||||
extensions.add(extensionPath.toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有插件
|
||||
*/
|
||||
public ChromiumOptions removeExtensions() {
|
||||
extensions.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Preferences文件中的用户设置项
|
||||
*
|
||||
* @param key 设置项名称
|
||||
* @param value 设置项值
|
||||
*/
|
||||
public ChromiumOptions setPref(String key, Object value) {
|
||||
this.pres.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户首选项设置,不能删除已设置到文件中的项
|
||||
*
|
||||
* @param key 设置项名称
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions removePref(String key) {
|
||||
this.pres.remove(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户配置文件中已设置的项
|
||||
*
|
||||
* @param arg 设置项名称
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions removePrefFromFile(String arg) {
|
||||
this.presToDel.add(arg);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实验项
|
||||
*
|
||||
* @param flag 设置项名称
|
||||
* @param value 设置项的值,为null则删除该项
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setFlag(String flag, String value) {
|
||||
if (value == null) flags.remove(flag);
|
||||
else flags.put(flag, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除浏览器配置文件中已设置的实验项
|
||||
*
|
||||
* @return 返回当前对象
|
||||
*/
|
||||
public ChromiumOptions clearFlagsInFile() {
|
||||
clearFileFlags = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空本对象已设置的argument参数
|
||||
*
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions clearArguments() {
|
||||
this.arguments.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空本对象已设置的pref参数
|
||||
*
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions clearPrefs() {
|
||||
this.pres.clear();
|
||||
;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置超时时间,单位为秒
|
||||
*
|
||||
* @param base 默认超时时间
|
||||
* @param pageLoad 页面加载超时时间
|
||||
* @param script 脚本运行超时时间
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setTimeouts(Double base, Double pageLoad, Double script) {
|
||||
// 设置超时时间,单位为秒
|
||||
if (base != null && base >= 0) timeouts.put("base", base);
|
||||
if (pageLoad != null && pageLoad >= 0) timeouts.put("pageLoad", pageLoad);
|
||||
if (script != null && script >= 0) timeouts.put("script", script);
|
||||
|
||||
// 返回当前对象
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置使用哪个用户配置文件夹
|
||||
*
|
||||
* @param user 用户文件夹名称
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setUser(String user) {
|
||||
setArgument("--profile-directory", user);
|
||||
this.user = user;
|
||||
|
||||
// 返回当前对象
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChromiumOptions headless() {
|
||||
return headless(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否隐藏浏览器界面
|
||||
*
|
||||
* @param onOff 是否开启
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions headless(boolean onOff) {
|
||||
this.headless = onOff;
|
||||
String value = onOff ? "new" : null;
|
||||
return setArgument("--headless", value);
|
||||
}
|
||||
|
||||
public ChromiumOptions noImg() {
|
||||
return noImg(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否加载图片
|
||||
*
|
||||
* @param onOff 是否开启
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions noImg(boolean onOff) {
|
||||
return onOff ? setArgument("--blink-settings=imagesEnabled=false") : this;
|
||||
}
|
||||
|
||||
public ChromiumOptions noJs() {
|
||||
return noJs(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否禁用js
|
||||
*
|
||||
* @param onOff 是否开启
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions noJs(boolean onOff) {
|
||||
return onOff ? setArgument("--disable-javascript") : this;
|
||||
}
|
||||
|
||||
public ChromiumOptions mute() {
|
||||
return mute(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否静音
|
||||
*
|
||||
* @param onOff 是否开启
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions mute(boolean onOff) {
|
||||
return onOff ? setArgument("--mute-audio") : this;
|
||||
}
|
||||
|
||||
public ChromiumOptions incognito() {
|
||||
return incognito(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置是否使用无痕模式启动
|
||||
*
|
||||
* @param onOff 是否开启
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions incognito(boolean onOff) {
|
||||
return onOff ? setArgument("--incognito") : this;
|
||||
}
|
||||
|
||||
public ChromiumOptions ignoreCertificateErrors() {
|
||||
return ignoreCertificateErrors(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否忽略证书错误
|
||||
*
|
||||
* @param onOff 是否开启
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions ignoreCertificateErrors(boolean onOff) {
|
||||
return onOff ? setArgument("--ignore-certificate-errors") : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置user agent
|
||||
*
|
||||
* @param userAgent user agent文本
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setUserAgent(String userAgent) {
|
||||
return setArgument("--user-agent", userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置代理
|
||||
*
|
||||
* @param proxy 代理
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setProxy(String proxy) {
|
||||
if (Pattern.matches(".*?:.*?@.*?\\..*", proxy)) {
|
||||
System.out.println("你似乎在设置使用账号密码的代理,暂时不支持这种代理,可自行用插件实现需求。");
|
||||
}
|
||||
if (proxy.toLowerCase().startsWith("socks")) {
|
||||
System.out.println("你似乎在设置使用socks代理,暂时不支持这种代理,可自行用插件实现需求。");
|
||||
}
|
||||
this.proxy = proxy;
|
||||
return setArgument("--proxy-server", proxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置load_mode 可接收 'normal', 'eager', 'none'
|
||||
* normal:默认情况下使用, 等待所有资源下载完成
|
||||
* eager:DOM访问已准备就绪, 但其他资源 (如图像) 可能仍在加载中
|
||||
* none:完全不阻塞
|
||||
*
|
||||
* @param value 可接收 'normal', 'eager', 'none'
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setLoadMode(String value) {
|
||||
//
|
||||
String lowerCase = value == null ? null : value.trim().toLowerCase();
|
||||
if (!List.of("normal", "eager", "none").contains(lowerCase)) {
|
||||
throw new IllegalArgumentException("只能选择 'normal', 'eager', 'none'。");
|
||||
}
|
||||
this.loadMode = lowerCase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChromiumOptions setPaths(String browserPath) {
|
||||
return setPaths(browserPath, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷的路径设置函数
|
||||
*
|
||||
* @param browserPath 浏览器可执行文件路径
|
||||
* @param localPort 本地端口号
|
||||
* @param address 调试浏览器地址,例:127.0.0.1:9222
|
||||
* @param downloadPath 下载文件路径
|
||||
* @param userDataPath 用户数据路径
|
||||
* @param cachePath 缓存路径
|
||||
* @param debuggerAddress 调试浏览器地址
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setPaths(String browserPath, Integer localPort, String address, String downloadPath, String userDataPath, String cachePath, String debuggerAddress) {
|
||||
// 快捷的路径设置函数
|
||||
address = (address != null) ? address : debuggerAddress;
|
||||
if (browserPath != null) {
|
||||
setBrowserPath(browserPath);
|
||||
}
|
||||
|
||||
if (localPort != null) {
|
||||
setLocalPort(localPort);
|
||||
}
|
||||
|
||||
if (address != null) {
|
||||
setAddress(address);
|
||||
}
|
||||
|
||||
if (downloadPath != null) {
|
||||
setDownloadPath(downloadPath);
|
||||
}
|
||||
|
||||
if (userDataPath != null) {
|
||||
setUserDataPath(userDataPath);
|
||||
}
|
||||
|
||||
if (cachePath != null) {
|
||||
setCachePath(cachePath);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置本地启动端口
|
||||
*
|
||||
* @param port 端口号
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setLocalPort(Integer port) {
|
||||
this.address = String.format("127.0.0.1:%04d", port);
|
||||
this.autoPort = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器地址,格式'ip:port'
|
||||
*
|
||||
* @param address 浏览器地址
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setAddress(String address) {
|
||||
address = address.replace("localhost", "127.0.0.1").replace("http://", "").replace("https://", "");
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器可执行文件路径
|
||||
*
|
||||
* @param path 浏览器路径
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setBrowserPath(String path) {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
// 设置浏览器可执行文件路径
|
||||
this.browserPath = path;
|
||||
this.autoPort = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下载文件保存路径
|
||||
*
|
||||
* @param path 下载路径
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setDownloadPath(String path) {
|
||||
if (path != null && !path.isEmpty()) this.downloadPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置临时文件文件保存路径
|
||||
*
|
||||
* @param path 用户文件夹路径
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setTmpPath(String path) {
|
||||
if (path != null && !path.isEmpty()) this.tmpPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户文件夹路径
|
||||
*
|
||||
* @param path 用户文件夹路径
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setUserDataPath(String path) {
|
||||
// 设置用户文件夹路径
|
||||
if (path != null && !path.isEmpty()) {
|
||||
setArgument("--user-data-dir", path);
|
||||
this.userDataPath = path;
|
||||
this.autoPort = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置缓存路径
|
||||
*
|
||||
* @param path 缓存路径
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions setCachePath(String path) {
|
||||
if (path != null && !path.isEmpty()) setArgument("--disk-cache-dir", path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChromiumOptions useSystemUserPath() {
|
||||
return useSystemUserPath(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否使用系统安装的浏览器默认用户文件夹
|
||||
*
|
||||
* @param onOff 开或关
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions useSystemUserPath(boolean onOff) {
|
||||
//
|
||||
this.systemUserPath = onOff;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动获取可用端口
|
||||
*
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions autoPort() {
|
||||
return autoPort(null);
|
||||
}
|
||||
|
||||
public ChromiumOptions autoPort(String tmpPath) {
|
||||
return autoPort(true, tmpPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动获取可用端口
|
||||
*
|
||||
* @param onOff 开或关
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions autoPort(boolean onOff, String tmpPath) {
|
||||
if (onOff) {
|
||||
this.autoPort = true;
|
||||
if (tmpPath != null && !tmpPath.isEmpty()) this.tmpPath = tmpPath;
|
||||
} else {
|
||||
this.autoPort = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChromiumOptions existingOnly() {
|
||||
return existingOnly(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置只接管已有浏览器,不自动启动新的
|
||||
*
|
||||
* @param onOff 开或关
|
||||
* @return 当前对象
|
||||
*/
|
||||
public ChromiumOptions existingOnly(boolean onOff) {
|
||||
//
|
||||
this.existingOnly = onOff;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前配置到默认ini文件
|
||||
*
|
||||
* @param path ini文件的路径, None 保存到当前读取的配置文件,传入 'default' 保存到默认ini文件
|
||||
* @return 保存文件的绝对路径
|
||||
*/
|
||||
public String save(String path) throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
// 保存设置到文件
|
||||
URL resource = getClass().getResource("/configs.ini");
|
||||
if (resource == null) throw new FileNotFoundException();
|
||||
String pathStr = Paths.get(resource.getPath()).toAbsolutePath().toString();
|
||||
|
||||
|
||||
if ("default".equals(path)) {
|
||||
path = pathStr;
|
||||
} else if (path == null) {
|
||||
if (this.iniPath != null) {
|
||||
path = Paths.get(this.iniPath).toAbsolutePath().toString();
|
||||
} else {
|
||||
path = pathStr;
|
||||
}
|
||||
} else {
|
||||
path = Paths.get(path).toAbsolutePath().toString();
|
||||
}
|
||||
path = path + File.separator + "config.ini";
|
||||
OptionsManager om;
|
||||
if (new File(path).exists()) {
|
||||
om = new OptionsManager(path);
|
||||
} else {
|
||||
om = new OptionsManager(this.iniPath != null ? this.iniPath : pathStr);
|
||||
}
|
||||
// 设置chromium_options
|
||||
String[] attrs = {"address", "browserPath", "arguments", "extensions", "user", "loadMode", "autoPort", "systemUserPath", "existingOnly", "flags"};
|
||||
for (String i : attrs) {
|
||||
om.setItem("chromium_options", i, this.getClass().getDeclaredField("_" + i).get(this));
|
||||
}
|
||||
|
||||
// 设置代理
|
||||
om.setItem("proxies", "http", this.proxy);
|
||||
om.setItem("proxies", "https", this.proxy);
|
||||
|
||||
// 设置路径
|
||||
om.setItem("paths", "downloadPath", this.downloadPath != null ? this.downloadPath : "");
|
||||
om.setItem("paths", "tmpPath", this.tmpPath != null ? this.tmpPath : "");
|
||||
|
||||
// 设置timeout
|
||||
om.setItem("timeouts", "base", this.timeouts.get("base"));
|
||||
om.setItem("timeouts", "pageLoad", this.timeouts.get("pageLoad"));
|
||||
om.setItem("timeouts", "script", this.timeouts.get("script"));
|
||||
|
||||
// 设置重试
|
||||
om.setItem("others", "retryTimes", this.retryTimes);
|
||||
om.setItem("others", "retryInterval", this.retryInterval);
|
||||
|
||||
// 设置prefs
|
||||
om.setItem("chromium_options", "prefs", this.pres);
|
||||
|
||||
om.save(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前配置到默认ini文件
|
||||
*
|
||||
* @return 保存文件的绝对路径
|
||||
*/
|
||||
public String saveToDefault() throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
return this.save("default");
|
||||
}
|
||||
public ChromiumOptions copy(){
|
||||
return JSON.parseObject(JSON.toJSONString(this),ChromiumOptions.class);
|
||||
}
|
||||
}
|
163
java/src/main/java/com/ll/DrissonPage/config/OptionsManager.java
Normal file
163
java/src/main/java/com/ll/DrissonPage/config/OptionsManager.java
Normal file
@ -0,0 +1,163 @@
|
||||
package com.ll.DrissonPage.config;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.ll.DrissonPage.error.extend.loadFileError;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ini4j.Profile;
|
||||
import org.ini4j.Wini;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* ini配置文件加载
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
@Getter
|
||||
public class OptionsManager {
|
||||
/**
|
||||
* 配置文件路径
|
||||
*/
|
||||
private final String iniPath;
|
||||
/**
|
||||
* 配置文件参数
|
||||
*/
|
||||
private final Wini ini;
|
||||
|
||||
/**
|
||||
* 使用默认初始化参数
|
||||
*/
|
||||
public OptionsManager() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化参数
|
||||
*
|
||||
* @param iniPath 配置文件路径
|
||||
*/
|
||||
public OptionsManager(String iniPath) {
|
||||
this("configs.ini", iniPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化参数
|
||||
*
|
||||
* @param fileName 初始化值
|
||||
* @param iniPath 配置文件路径
|
||||
*/
|
||||
public OptionsManager(String fileName, String iniPath) {
|
||||
this.iniPath = iniPath;
|
||||
//加载配置文件中的数据
|
||||
this.ini = loadIni(fileName, iniPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置文件,使用的是map的putAll
|
||||
*
|
||||
* @param fileName 源位置
|
||||
* @param path 重新加载的文件
|
||||
* @return 返回map集合
|
||||
*/
|
||||
private Wini loadIni(String fileName, String path) {
|
||||
//加载内部资源configs.ini
|
||||
Wini wini;
|
||||
try (InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(fileName)) {
|
||||
wini = new Wini(resourceAsStream);
|
||||
} catch (IOException e) {
|
||||
throw new loadFileError(e);
|
||||
}
|
||||
//加载外部资源configs.ini
|
||||
if (StringUtils.isNotEmpty(path)) {
|
||||
Wini externalWini = null;
|
||||
try {
|
||||
externalWini = new Wini(new File(path));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
if (externalWini != null) {
|
||||
wini.putAll(externalWini);
|
||||
}
|
||||
}
|
||||
return wini;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
*
|
||||
* @param section 配置项名称
|
||||
* @return 配置项
|
||||
*/
|
||||
public Profile.Section getOption(String section) {
|
||||
return ini.get(section);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置的值
|
||||
*
|
||||
* @param section 段名
|
||||
* @param key 项名
|
||||
* @return 项值
|
||||
*/
|
||||
public String getValue(String section, String key) {
|
||||
Profile.Section option = getOption(section);
|
||||
return option != null ? option.get(key) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置项的值
|
||||
*
|
||||
* @param section 配置项
|
||||
* @param item 配置
|
||||
* @param value 值
|
||||
*/
|
||||
public void setItem(String section, String item, Object value) {
|
||||
ini.add(section, item, value);
|
||||
}
|
||||
|
||||
// 删除配置项
|
||||
public String removeItem(String sectionName, String optionName) {
|
||||
Profile.Section section = ini.get(sectionName);
|
||||
return section != null ? section.remove(optionName) : null;
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
public void save(String path) throws IOException {
|
||||
Path filePath;
|
||||
if ("default".equals(path)) {
|
||||
// 如果保存路径为'default',则使用默认的configs.ini
|
||||
filePath = Paths.get(getClass().getResource("/configs.ini").getFile()).toAbsolutePath();
|
||||
} else if (path == null) {
|
||||
// 如果保存路径为null,则使用当前配置文件路径
|
||||
filePath = Paths.get(iniPath).toAbsolutePath();
|
||||
} else {
|
||||
// 使用指定的保存路径
|
||||
filePath = Paths.get(path).toAbsolutePath();
|
||||
}
|
||||
|
||||
Files.write(filePath, ini.toString().getBytes());
|
||||
System.out.println("配置已保存到文件:" + filePath);
|
||||
if (filePath.equals(Paths.get(getClass().getResource("/configs.ini").getFile()).toAbsolutePath())) {
|
||||
System.out.println("以后程序可自动从文件加载配置.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置到默认文件
|
||||
*/
|
||||
public void saveToDefault() throws IOException {
|
||||
save("default");
|
||||
}
|
||||
|
||||
public void show() {
|
||||
System.out.println(JSON.toJSONString(ini));
|
||||
}
|
||||
}
|
98
java/src/main/java/com/ll/DrissonPage/config/PortFinder.java
Normal file
98
java/src/main/java/com/ll/DrissonPage/config/PortFinder.java
Normal file
@ -0,0 +1,98 @@
|
||||
package com.ll.DrissonPage.config;
|
||||
|
||||
import com.ll.DrissonPage.functions.Tools;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class PortFinder {
|
||||
private static final Map<Integer, String> usedPort = new HashMap<>();
|
||||
private static final Lock lock = new ReentrantLock();
|
||||
|
||||
private final Path tmpDir;
|
||||
|
||||
public PortFinder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path 临时文件保存路径,为None时使用系统临时文件夹
|
||||
*/
|
||||
public PortFinder(String path) {
|
||||
Path tmp = (path != null) ? Paths.get(path) : Paths.get(System.getProperty("java.io.tmpdir")).resolve("DrissionPage");
|
||||
this.tmpDir = tmp.resolve("UserTempFolder");
|
||||
try {
|
||||
Files.createDirectories(this.tmpDir);
|
||||
if (usedPort.isEmpty()) {
|
||||
Tools.cleanFolder(this.tmpDir.toAbsolutePath().toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error initializing PortFinder", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanDirectory(Path directory) throws IOException {
|
||||
Tools.deleteDirectory(directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找一个可用端口
|
||||
*
|
||||
* @return 可以使用的端口和用户文件夹路径组成的元组
|
||||
*/
|
||||
public synchronized PortInfo getPort() {
|
||||
try {
|
||||
lock.lock();
|
||||
for (int i = 9600; i < 19600; i++) {
|
||||
if (usedPort.containsKey(i)) {
|
||||
continue;
|
||||
} else if (Tools.portIsUsing("127.0.0.1", i)) {
|
||||
usedPort.put(i, null);
|
||||
continue;
|
||||
}
|
||||
String path = Files.createTempDirectory(this.tmpDir, "tmp").toString();
|
||||
usedPort.put(i, path);
|
||||
return new PortInfo(i, path);
|
||||
}
|
||||
|
||||
for (int i = 9600; i < 19600; i++) {
|
||||
if (Tools.portIsUsing("127.0.0.1", i)) {
|
||||
continue;
|
||||
}
|
||||
cleanDirectory(Paths.get(usedPort.get(i)));
|
||||
return new PortInfo(i, Files.createTempDirectory(this.tmpDir, "tmp").toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
throw new RuntimeException("No available port found.");
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class PortInfo {
|
||||
private final int port;
|
||||
private final String path;
|
||||
|
||||
public PortInfo(int port, String path) {
|
||||
this.port = port;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
442
java/src/main/java/com/ll/DrissonPage/config/SessionOptions.java
Normal file
442
java/src/main/java/com/ll/DrissonPage/config/SessionOptions.java
Normal file
@ -0,0 +1,442 @@
|
||||
package com.ll.DrissonPage.config;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import com.ll.DrissonPage.units.HttpClient;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.*;
|
||||
import org.apache.commons.collections4.map.CaseInsensitiveMap;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.ini4j.Profile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* requests的Session对象配置类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class SessionOptions {
|
||||
private String iniPath;
|
||||
/**
|
||||
* 返回默认下载路径属性信息
|
||||
*/
|
||||
private String downloadPath;
|
||||
/**
|
||||
* 返回timeout属性信息
|
||||
*/
|
||||
private Double timeout = 10.0;
|
||||
/**
|
||||
* 记录要从ini文件删除的参数
|
||||
*/
|
||||
private Set<String> delSet = new HashSet<>();
|
||||
/**
|
||||
* 返回headers设置信息
|
||||
*/
|
||||
private Map<String, String> headers;
|
||||
/**
|
||||
* 以list形式返回cookies
|
||||
*/
|
||||
private List<Cookie> cookies;
|
||||
/**
|
||||
* 返回认证设置信息
|
||||
*/
|
||||
private List<Object> auth;
|
||||
/**
|
||||
* 返回proxies设置信息
|
||||
*/
|
||||
private Map<String, String> proxies;
|
||||
/**
|
||||
* 返回回调方法
|
||||
*/
|
||||
private Map<String, Object> hooks;
|
||||
/**
|
||||
* 返回连接参数设置信息
|
||||
*/
|
||||
private Map<String, String> params;
|
||||
/**
|
||||
* 返回是否验证SSL证书设置
|
||||
*/
|
||||
private Boolean verify;
|
||||
/**
|
||||
* 返回SSL证书设置信息
|
||||
*/
|
||||
private String cert;
|
||||
/**
|
||||
* 返回适配器设置信息
|
||||
*/
|
||||
private List<String> adapters;
|
||||
/**
|
||||
* 返回是否使用流式响应内容设置信息
|
||||
*/
|
||||
private Boolean stream;
|
||||
/**
|
||||
* 返回是否信任环境设置信息
|
||||
*/
|
||||
private Boolean trustEnv;
|
||||
/**
|
||||
* 返回最大重定向次数
|
||||
*/
|
||||
private Integer maxRedirects;
|
||||
/**
|
||||
* 返回连接失败时的重试次数
|
||||
*/
|
||||
private int retryTimes = 3;
|
||||
/**
|
||||
* 返回连接失败时的重试间隔(秒)
|
||||
*/
|
||||
private int retryInterval = 2;
|
||||
|
||||
public SessionOptions(boolean readFile, String iniPath) {
|
||||
headers = new CaseInsensitiveMap<>();
|
||||
auth = new ArrayList<>();
|
||||
cookies = new ArrayList<>();
|
||||
proxies = new HashMap<>();
|
||||
hooks = new HashMap<>();
|
||||
params = new HashMap<>();
|
||||
if (!readFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
iniPath = iniPath != null ? iniPath : "";
|
||||
OptionsManager om = new OptionsManager(iniPath);
|
||||
this.iniPath = om.getIniPath();
|
||||
|
||||
Profile.Section options = om.getIni().get("session_options");
|
||||
if (options.get("headers") != null) {
|
||||
setHeaders(JSON.parseObject(options.get("headers"), new TypeReference<>() {
|
||||
}));
|
||||
}
|
||||
|
||||
if (options.containsKey("cookies")) {
|
||||
setCookies(JSON.parseObject(options.get("cookies"), new TypeReference<>() {
|
||||
}));
|
||||
}
|
||||
|
||||
if (options.containsKey("auth")) {
|
||||
this.auth = JSON.parseArray(options.get("auth"));
|
||||
}
|
||||
|
||||
if (options.containsKey("params")) {
|
||||
this.params = JSON.parseObject(options.get("params"), new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
|
||||
if (options.containsKey("verify")) {
|
||||
this.verify = Boolean.parseBoolean(options.get("verify"));
|
||||
}
|
||||
|
||||
if (options.containsKey("cert")) {
|
||||
this.cert = options.get("cert");
|
||||
}
|
||||
|
||||
if (options.containsKey("stream")) {
|
||||
this.stream = Boolean.parseBoolean(options.get("stream"));
|
||||
}
|
||||
|
||||
if (options.containsKey("trust_env")) {
|
||||
this.trustEnv = Boolean.parseBoolean(options.get("trust_env"));
|
||||
}
|
||||
|
||||
if (options.containsKey("max_redirects")) {
|
||||
this.maxRedirects = Integer.parseInt("max_redirects");
|
||||
}
|
||||
|
||||
setProxies(om.getIni().get("proxies", "http"), om.getIni().get("proxies", "https"));
|
||||
String s = om.getIni().get("timeouts", "base");
|
||||
if (s != null) this.timeout = Double.parseDouble(om.getIni().get("timeouts", "base"));
|
||||
this.downloadPath = om.getIni().get("paths", "download_path");
|
||||
Profile.Section others = om.getIni().get("others");
|
||||
s = others.get("retry_times");
|
||||
this.retryTimes = s != null ? Integer.parseInt(s) : 3;
|
||||
s = others.get("retry_interval");
|
||||
this.retryInterval = s != null ? Integer.parseInt(s) : 2;
|
||||
}
|
||||
|
||||
public static Map<String, Object> sessionOptionsToMap(Map<String, Object> options) {
|
||||
if (options == null) return new SessionOptions(false, null).asMap();
|
||||
if (!options.isEmpty()) return options;
|
||||
String[] attrs = {"headers", "cookies", "proxies", "params", "verify", "stream", "trustEnv", "cert", "maxRedirects", "timeout", "downloadPath"};
|
||||
options = new HashMap<>();
|
||||
for (String attr : attrs) {
|
||||
Object val;
|
||||
try {
|
||||
val = options.getClass().getField(attr).get(options);
|
||||
} catch (IllegalAccessException | NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (val != null) options.put(attr, val);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
public void setProxies(String http, String https) {
|
||||
this.proxies.put("http", http);
|
||||
this.proxies.put("https", https);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接失败时的重试操作
|
||||
*
|
||||
* @param times 重试次数
|
||||
* @param interval 重试间隔
|
||||
* @return 当前对象
|
||||
*/
|
||||
public SessionOptions setRetry(Integer times, Integer interval) {
|
||||
if (times != null) this.retryTimes = times;
|
||||
if (interval != null) this.retryInterval = interval;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置headers参数
|
||||
*
|
||||
* @param headers 参数值,传入null可在ini文件标记删除
|
||||
* @return 返回当前对象
|
||||
*/
|
||||
public SessionOptions setHeaders(Map<String, String> headers) {
|
||||
if (headers == null) {
|
||||
this.headers = null;
|
||||
this.delSet.add("headers");
|
||||
} else {
|
||||
this.headers = new CaseInsensitiveMap<>(headers.size());
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
this.headers.put(entry.getKey().toLowerCase(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置headers中一个项
|
||||
*
|
||||
* @param attr 设置名称
|
||||
* @param value 设置值
|
||||
* @return 返回当前对象
|
||||
*/
|
||||
public SessionOptions setHeader(String attr, String value) {
|
||||
if (this.headers == null) this.headers = new CaseInsensitiveMap<>();
|
||||
this.headers.put(attr.toLowerCase(), value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从headers中删除一个设置
|
||||
*
|
||||
* @param attr 要删除的设置
|
||||
* @return 返回当前对象
|
||||
*/
|
||||
public SessionOptions removeHeader(String attr) {
|
||||
if (this.headers != null) {
|
||||
this.headers.remove(attr);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> adapters() {
|
||||
if (this.adapters == null) this.adapters = new ArrayList<>();
|
||||
return this.adapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给属性赋值或标记删除
|
||||
*
|
||||
* @param arg 属性名称
|
||||
* @param val 参数值
|
||||
*/
|
||||
private void sets(String arg, Object val) {
|
||||
try {
|
||||
Field field = this.getClass().getDeclaredField(arg);
|
||||
field.setAccessible(true);
|
||||
|
||||
if (val == null) {
|
||||
field.set(this, null);
|
||||
delSet.add(arg);
|
||||
} else {
|
||||
field.set(this, val);
|
||||
delSet.remove(arg);
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
e.printStackTrace(); // Handle the exception according to your needs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String save(String path) throws URISyntaxException, IOException {
|
||||
if ("default".equals(path)) {
|
||||
path = Path.of(Objects.requireNonNull(getClass().getResource("configs.ini")).toURI()).toAbsolutePath().toString();
|
||||
} else if (path == null) {
|
||||
path = iniPath != null ? Path.of(iniPath).toAbsolutePath().toString() : Path.of(Objects.requireNonNull(getClass().getResource("configs.ini")).toURI()).toAbsolutePath().toString();
|
||||
} else {
|
||||
path = Path.of(path).toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
Path filePath = path.endsWith("config.ini") ? Path.of(path) : Path.of(path, "config.ini");
|
||||
|
||||
OptionsManager om = filePath.toFile().exists() ? new OptionsManager(filePath.toString()) : new OptionsManager(iniPath != null ? iniPath : getClass().getResource("configs.ini").toURI().toString());
|
||||
|
||||
Map<String, Object> options = sessionOptionsToMap(JSON.parseObject(JSON.toJSONString(this)));
|
||||
|
||||
for (Map.Entry<String, Object> entry : options.entrySet()) {
|
||||
String i = entry.getKey();
|
||||
if (!List.of("downloadPath", "timeout", "proxies").contains(i)) {
|
||||
om.setItem("sessionOptions", i, entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
om.setItem("paths", "downloadPath", downloadPath != null ? downloadPath : "");
|
||||
om.setItem("timeouts", "base", timeout);
|
||||
om.setItem("proxies", "http", proxies.get("http") != null ? proxies.get("http") : "");
|
||||
om.setItem("proxies", "https", proxies.get("https") != null ? proxies.get("https") : "");
|
||||
om.setItem("others", "retryTimes", retryTimes);
|
||||
om.setItem("others", "retryInterval", retryInterval);
|
||||
|
||||
for (String i : delSet) {
|
||||
if ("downloadPath".equals(i)) {
|
||||
om.setItem("paths", "downloadPath", "");
|
||||
} else if ("proxies".equals(i)) {
|
||||
om.setItem("proxies", "http", "");
|
||||
om.setItem("proxies", "https", "");
|
||||
} else {
|
||||
om.removeItem("sessionOptions", i);
|
||||
}
|
||||
}
|
||||
|
||||
om.save(filePath.toString());
|
||||
|
||||
return filePath.toString();
|
||||
}
|
||||
|
||||
public String saveToDefault() throws URISyntaxException, IOException {
|
||||
return save("default");
|
||||
}
|
||||
|
||||
public Map<String, Object> asMap() {
|
||||
return sessionOptionsToMap(JSON.parseObject(JSON.toJSONString(this)));
|
||||
}
|
||||
|
||||
public HttpClient makeSession() {
|
||||
List<Header> headers = new ArrayList<>();
|
||||
this.headers.forEach((a, b) -> headers.add(new BasicHeader(a, b)));
|
||||
OkHttpClient.Builder builder = new OkHttpClient().newBuilder().readTimeout(120, TimeUnit.SECONDS);
|
||||
|
||||
builder.addInterceptor(new Interceptor() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Request.Builder builder1 = request.newBuilder();
|
||||
if (!headers.isEmpty()) headers.forEach((a) -> builder1.addHeader(a.getName(), a.getValue()));
|
||||
return chain.proceed(request);
|
||||
}
|
||||
});
|
||||
//设置缓存
|
||||
if (!this.cookies.isEmpty()) {
|
||||
builder.setCookieJar$okhttp(new CookieJar() {
|
||||
@Override
|
||||
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
|
||||
list.addAll(cookies);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
});
|
||||
}
|
||||
//设置代理
|
||||
if (!this.proxies.isEmpty()) {
|
||||
String https = this.proxies.get("https");
|
||||
String http = this.proxies.get("http");
|
||||
if (!https.isEmpty()) {
|
||||
String[] split = https.split(":");
|
||||
builder.setProxy$okhttp(split.length == 2 ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(split[0], Integer.parseInt(split[1]))) : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(https, 80)));
|
||||
}
|
||||
if (!http.isEmpty()) {
|
||||
String[] split = http.split(":");
|
||||
builder.setProxy$okhttp(split.length == 2 ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(split[0], Integer.parseInt(split[1]))) : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(http, 80)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.verify != null) {
|
||||
builder.setHostnameVerifier$okhttp((s, sslSession) -> this.verify);
|
||||
}
|
||||
if (this.maxRedirects != null) {
|
||||
builder.setConnectionPool$okhttp(new ConnectionPool(this.maxRedirects, 5, TimeUnit.MINUTES));
|
||||
}
|
||||
return new HttpClient(builder.build(), headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Session对象中读取配置
|
||||
*
|
||||
* @param session Session对象
|
||||
* @param headers headers
|
||||
* @return 当前对象
|
||||
*/
|
||||
public SessionOptions fromSession(OkHttpClient session, Map<String, String> headers) {
|
||||
headers = headers == null ? new CaseInsensitiveMap<>() : new CaseInsensitiveMap<>(headers);
|
||||
|
||||
Map<String, String> finalHeaders = headers;
|
||||
OkHttpClient.Builder builder = session.newBuilder();
|
||||
builder.addInterceptor(new Interceptor() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Headers headers1 = request.headers();
|
||||
for (String name : headers1.names()) {
|
||||
finalHeaders.put(name, headers1.get(name));
|
||||
}
|
||||
return chain.proceed(request);
|
||||
}
|
||||
});
|
||||
this.headers = headers;
|
||||
builder.setCookieJar$okhttp(new CookieJar() {
|
||||
@Override
|
||||
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
|
||||
cookies = list;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
});
|
||||
Proxy proxy$okhttp = builder.getProxy$okhttp();
|
||||
if (proxy$okhttp != null) {
|
||||
this.proxies = new HashMap<>();
|
||||
this.proxies.put(proxy$okhttp.type().toString(), proxy$okhttp.address().toString());
|
||||
}
|
||||
this.maxRedirects = builder.getConnectionPool$okhttp().connectionCount();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionOptions copy() {
|
||||
return JSON.parseObject(JSON.toJSONString(this), SessionOptions.class);
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public static class Adapter {
|
||||
private String url;
|
||||
}
|
||||
}
|
2755
java/src/main/java/com/ll/DrissonPage/element/ChromiumElement.java
Normal file
2755
java/src/main/java/com/ll/DrissonPage/element/ChromiumElement.java
Normal file
File diff suppressed because it is too large
Load Diff
26
java/src/main/java/com/ll/DrissonPage/element/Pseudo.java
Normal file
26
java/src/main/java/com/ll/DrissonPage/element/Pseudo.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.ll.DrissonPage.element;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class Pseudo {
|
||||
private final ChromiumElement chromiumElement;
|
||||
|
||||
/**
|
||||
* @return 返回当前元素的::before伪元素内容
|
||||
*/
|
||||
public String before() {
|
||||
return chromiumElement.style("content", "before");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前元素的::after伪元素内容
|
||||
*/
|
||||
public String after() {
|
||||
return chromiumElement.style("content", "after");
|
||||
}
|
||||
}
|
701
java/src/main/java/com/ll/DrissonPage/element/SelectElement.java
Normal file
701
java/src/main/java/com/ll/DrissonPage/element/SelectElement.java
Normal file
@ -0,0 +1,701 @@
|
||||
package com.ll.DrissonPage.element;
|
||||
|
||||
import com.ll.DrissonPage.base.By;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用于处理 select 标签
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class SelectElement {
|
||||
private final ChromiumElement ele;
|
||||
|
||||
public SelectElement(ChromiumElement ele) {
|
||||
if (!Objects.equals(ele.tag(), "select"))
|
||||
throw new IllegalArgumentException("select方法只能在<select>元素使用。");
|
||||
this.ele = ele;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定下拉列表中子元素
|
||||
*
|
||||
* @param textOrIndex 根据文本、值选或序号择选项,若允许多选,传入集合或数组可多选
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return null
|
||||
*/
|
||||
public boolean select(Object textOrIndex, Double timeout) {
|
||||
String paraType = textOrIndex instanceof Integer ? "index" : "text";
|
||||
timeout = timeout == null ? this.ele.getOwner().timeout() : timeout;
|
||||
return this.select(textOrIndex, paraType, false, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回是否多选表单
|
||||
*/
|
||||
public boolean isMulti() {
|
||||
String multiple = this.ele.attr("multiple");
|
||||
return multiple != null && !multiple.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有选项元素组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> options() {
|
||||
return this.ele.eles("xpath://option");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个被选中的option元素
|
||||
*
|
||||
* @return ChromiumElement对象或null
|
||||
*/
|
||||
public ChromiumElement selectOption() {
|
||||
Object o = this.ele.runJs("return this.options[this.selectedIndex];");
|
||||
if (o instanceof List<?>) {
|
||||
try {
|
||||
List<ChromiumElement> o1 = (List<ChromiumElement>) o;
|
||||
if (!o1.isEmpty()) return o1.get(0);
|
||||
} catch (ClassCastException ignored) {
|
||||
}
|
||||
} else if (o instanceof ChromiumElement) {
|
||||
return (ChromiumElement) o;
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有被选中的option元素列表
|
||||
*
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> selectOptions() {
|
||||
List<ChromiumElement> options = this.options();
|
||||
options.removeIf(option -> !option.states().isSelected());
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选
|
||||
*
|
||||
* @return 是否成功
|
||||
*/
|
||||
public boolean all() {
|
||||
if (!this.isMulti()) throw new IllegalArgumentException("只能在多选菜单执行此操作.");
|
||||
return this._byLoc("tag:option", 1.0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反选
|
||||
*/
|
||||
public void invert() {
|
||||
if (!this.isMulti()) throw new IllegalArgumentException("只能对多项选框执行反选.");
|
||||
boolean change = false;
|
||||
for (ChromiumElement option : this.options()) {
|
||||
change = true;
|
||||
String mode = option.states().isSelected() ? "false" : "true";
|
||||
option.runJs("this.selected=" + mode + ";");
|
||||
}
|
||||
if (change) this.dispatchChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有已选项
|
||||
*
|
||||
* @return 是否成功
|
||||
*/
|
||||
public boolean clear() {
|
||||
if (!this.isMulti()) throw new IllegalArgumentException("只能对多项选框执行反选.");
|
||||
return this._byLoc("tag:option", 1.0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text text属性值,传入集合或数组 可选择多项
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byText(Object text) {
|
||||
return this.byText(text, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text text属性值,传入集合或数组 可选择多项
|
||||
* @param timeout 超时时间,为null默认使用页面超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byText(Object text, Double timeout) {
|
||||
return this.select(text, "text", false, timeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text value属性值,传入集合或数组 可选择多项
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byValue(Object text) {
|
||||
return this.byValue(text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text value属性值,传入集合或数组 可选择多项
|
||||
* @param timeout 超时时间,为null默认使用页面超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byValue(Object text, Double timeout) {
|
||||
return this.select(text, "value", false, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可选择多项
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byIndex(int index) {
|
||||
return this.byIndex(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可选择多项
|
||||
* @param timeout 超时时间,为null默认使用页面超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byIndex(int index, Double timeout) {
|
||||
return this.byIndex(Integer.valueOf(index), timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可选择多项
|
||||
* @param timeout 超时时间,为null默认使用页面超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean byIndex(Integer index, Double timeout) {
|
||||
return this.select(index, "index", false, timeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可选择多项
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byIndex(int[] index) {
|
||||
return this.byIndex(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可选择多项
|
||||
* @param timeout 超时时间,为null默认使用页面超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byIndex(int[] index, Double timeout) {
|
||||
return byIndex(Arrays.stream(index).boxed().toArray(Integer[]::new), timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可选择多项
|
||||
* @param timeout 超时时间,为null默认使用页面超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean byIndex(Integer[] index, Double timeout) {
|
||||
return this.select(index, "index", false, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符选择指定的项
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byLoc(String loc) {
|
||||
return byLoc(loc, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符选择指定的项
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byLoc(String loc, Double timeout) {
|
||||
return _byLoc(loc, timeout, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符选择指定的项
|
||||
*
|
||||
* @param by 定位符
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byLoc(By by) {
|
||||
return byLoc(by, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符选择指定的项
|
||||
*
|
||||
* @param by 定位符
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean byLoc(By by, Double timeout) {
|
||||
return _byLoc(by, timeout, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中单个或多个option元素
|
||||
*
|
||||
* @param option option元素或它们组成的列表
|
||||
*/
|
||||
public void byOption(ChromiumElement option) {
|
||||
ArrayList<ChromiumElement> option1 = new ArrayList<>();
|
||||
option1.add(option);
|
||||
byOption(option1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中单个或多个option元素
|
||||
*
|
||||
* @param option option元素或它们组成的列表
|
||||
*/
|
||||
public void byOption(List<ChromiumElement> option) {
|
||||
this.selectOptions(option, "true");
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text 传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByText(String text) {
|
||||
return this.cancelByText(text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text 传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByText(String text, Double timeout) {
|
||||
return this.select(text, "text", true, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text 传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByText(String[] text) {
|
||||
return this.cancelByText(text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text 传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByText(String[] text, Double timeout) {
|
||||
return this.select(text, "text", true, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text 传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByText(List<String> text) {
|
||||
return this.cancelByText(text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param text 传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByText(List<String> text, Double timeout) {
|
||||
return this.select(text, "text", true, timeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param value value属性值,传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByValue(String value) {
|
||||
return this.cancelByText(value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param value value属性值,传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByValue(String value, Double timeout) {
|
||||
return this.select(value, "value", true, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param value value属性值,传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByValue(String[] value) {
|
||||
return this.cancelByValue(value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param value value属性值,传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByValue(String[] value, Double timeout) {
|
||||
return this.select(value, "value", true, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param value value属性值,传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByValue(List<String> value) {
|
||||
return this.cancelByValue(value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param value value属性值,传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByValue(List<String> value, Double timeout) {
|
||||
return this.select(value, "value", true, timeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByIndex(int index) {
|
||||
return this.cancelByIndex(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByIndex(int index, Double timeout) {
|
||||
return this.select(index, "index", true, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByIndex(int[] index) {
|
||||
return this.cancelByIndex(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByIndex(int[] index, Double timeout) {
|
||||
return this.select(Arrays.stream(index).boxed().toArray(Integer[]::new), "index", true, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可取消多项
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByIndex(List<Integer> index) {
|
||||
return this.cancelByIndex(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或数组
|
||||
*
|
||||
* @param index 序号,从1开始,可传入负数获取倒数第几个,传入集合或数组可取消多项
|
||||
* @param timeout 超时时间,不输入默认实用页面超时时间
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
public boolean cancelByIndex(List<Integer> index, Double timeout) {
|
||||
return this.select(index, "index", true, timeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用定位符取消选择指定的项
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean cancelByLoc(String loc) {
|
||||
return cancelByLoc(loc, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符取消选择指定的项
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean cancelByLoc(String loc, Double timeout) {
|
||||
return _byLoc(loc, timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符取消选择指定的项
|
||||
*
|
||||
* @param by 定位符
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean cancelByLoc(By by) {
|
||||
return cancelByLoc(by, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符取消选择指定的项
|
||||
*
|
||||
* @param by 定位符
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
public boolean cancelByLoc(By by, Double timeout) {
|
||||
return _byLoc(by, timeout, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 取消选中单个或多个option元 素
|
||||
*
|
||||
* @param option option元素或它们组成的列表
|
||||
*/
|
||||
public void cancelByOption(ChromiumElement option) {
|
||||
ArrayList<ChromiumElement> option1 = new ArrayList<>();
|
||||
option1.add(option);
|
||||
cancelByOption(option1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消选中单个或多个option元 素
|
||||
*
|
||||
* @param option option元素或它们组成的列表
|
||||
*/
|
||||
public void cancelByOption(ChromiumElement[] option) {
|
||||
this.selectOptions(List.of(option), "false");
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消选中单个或多个option元 素
|
||||
*
|
||||
* @param option option元素或它们组成的列表
|
||||
*/
|
||||
public void cancelByOption(List<ChromiumElement> option) {
|
||||
this.selectOptions(option, "false");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用定位符取消选择指定的项
|
||||
*
|
||||
* @param loc 定位符
|
||||
* @param timeout 超时时间
|
||||
* @param cancel 是否取消选择
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean _byLoc(String loc, Double timeout, boolean cancel) {
|
||||
return __byLoc(cancel, this.ele.eles(loc, timeout));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用定位符取消选择指定的项
|
||||
*
|
||||
* @param by 定位符
|
||||
* @param timeout 超时时间
|
||||
* @param cancel 是否取消选择
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean _byLoc(By by, Double timeout, boolean cancel) {
|
||||
return __byLoc(cancel, this.ele.eles(by, timeout));
|
||||
}
|
||||
|
||||
private boolean __byLoc(boolean cancel, List<ChromiumElement> elements) {
|
||||
if (elements == null || elements.isEmpty()) return false;
|
||||
this.selectOptions(this.isMulti() ? elements : Collections.singletonList(elements.get(0)), cancel ? "false" : "true");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定或取消选定下拉列表中子元素
|
||||
*
|
||||
* @param condition 根据文本、值选或序号择选项,若允许多选,传入集合或数组可多选
|
||||
* @param paraType 参数类型,可选 'text'、'value'、'index'
|
||||
* @param cancel 是否取消选择
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
|
||||
private boolean select(Object condition, String paraType, boolean cancel, Double timeout) {
|
||||
if (!this.isMulti() && (condition instanceof Collection || condition instanceof String[] || condition instanceof Integer[]))
|
||||
throw new IllegalArgumentException("单选列表只能传入str格式.");
|
||||
String mode = cancel ? "false" : "true";
|
||||
timeout = timeout != null ? timeout : this.ele.getOwner().timeout();
|
||||
Collection<String> objects = new ArrayList<>();
|
||||
if (!(condition instanceof Collection || condition instanceof String[] || condition instanceof Integer[]))
|
||||
objects.add(condition.toString());
|
||||
else if (condition instanceof Collection) ((List<?>) condition).forEach(a -> objects.add(a.toString()));
|
||||
else if (condition instanceof String[]) Collections.addAll(objects, (String[]) condition);
|
||||
else Collections.addAll(objects, Arrays.toString(((Integer[]) condition)));
|
||||
if ("text".equals(paraType) || "value".equals(paraType))
|
||||
return this.textValue(Collections.singleton(objects), paraType, mode, timeout);
|
||||
else if ("index".equals(paraType)) return this.index(objects, mode, timeout, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行text和value搜索
|
||||
*
|
||||
* @param condition 条件
|
||||
* @param paraType 参数类型,可选 'text'、'value'
|
||||
* @param mode 'true' 或 'false'
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean textValue(Collection<Object> condition, String paraType, String mode, double timeout) {
|
||||
boolean ok = false;
|
||||
int textLen = condition.size();
|
||||
List<ChromiumElement> elements = new ArrayList<>();
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
if (Objects.equals(paraType, "text"))
|
||||
this.options().stream().filter(i -> condition.toString().contains(i.text())).forEachOrdered(elements::add);
|
||||
else if (Objects.equals(paraType, "value"))
|
||||
this.options().stream().filter(i -> condition.toString().contains(i.attr("value"))).forEachOrdered(elements::add);
|
||||
if (elements.size() >= textLen) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
this.selectOptions(elements, mode);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行index搜索
|
||||
*
|
||||
* @param condition 条件
|
||||
* @param mode 'true' 或 'false'
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean index(Collection<String> condition, String mode, double timeout, Integer ignored) {
|
||||
return index(condition.stream().map(Integer::parseInt).collect(Collectors.toList()), mode, timeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行index搜索
|
||||
*
|
||||
* @param condition 条件
|
||||
* @param mode 'true' 或 'false'
|
||||
* @param timeout 超时时间
|
||||
* @return 是否选择成功
|
||||
*/
|
||||
private boolean index(Collection<Integer> condition, String mode, double timeout) {
|
||||
boolean ok = false;
|
||||
int textLen = Math.abs(condition.stream().mapToInt(Math::abs).max().orElse(0));
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
if (this.options().size() >= textLen) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
List<ChromiumElement> elements = options();
|
||||
selectOptions(condition.stream().mapToInt(i -> i).mapToObj(i -> i > 0 ? elements.get(i - 1) : elements.get(i)).collect(Collectors.toList()), mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中或取消某个选项
|
||||
*
|
||||
* @param option options元素对象
|
||||
* @param mode 选中还是取消
|
||||
*/
|
||||
private void selectOptions(List<ChromiumElement> option, String mode) {
|
||||
if (this.isMulti() && option.size() > 1) option = List.of(option.get(0));
|
||||
for (ChromiumElement chromiumElement : option) {
|
||||
chromiumElement.runJs("this.selected=" + mode + ";");
|
||||
this.dispatchChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发修改动作
|
||||
*/
|
||||
private void dispatchChange() {
|
||||
this.ele.runJs("this.dispatchEvent(new Event(\"change\", {bubbles: true}));");
|
||||
}
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
package com.ll.DrissonPage.element;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.ll.DrissonPage.base.*;
|
||||
import com.ll.DrissonPage.functions.Locator;
|
||||
import com.ll.DrissonPage.functions.Web;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Attribute;
|
||||
import org.jsoup.nodes.Attributes;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class SessionElement extends DrissionElement<BasePage<?>, SessionElement> {
|
||||
private final Element innerEle;
|
||||
|
||||
public SessionElement(Element ele) {
|
||||
this(ele, null);
|
||||
}
|
||||
|
||||
public SessionElement(Element ele, BasePage<?> page) {
|
||||
super(page);
|
||||
this.innerEle = ele;
|
||||
this.setType("SessionElement");
|
||||
}
|
||||
|
||||
public static List<SessionElement> makeSessionEle(String html, String loc, Integer index) {
|
||||
return _makeSessionEle(html, loc, index);
|
||||
}
|
||||
|
||||
public static List<SessionElement> makeSessionEle(BaseParser<?> ele, String loc, Integer index) {
|
||||
return _makeSessionEle(ele, loc, index);
|
||||
|
||||
}
|
||||
|
||||
public static List<SessionElement> makeSessionEle(String html, By by, Integer index) {
|
||||
return _makeSessionEle(html, by, index);
|
||||
|
||||
}
|
||||
|
||||
public static List<SessionElement> makeSessionEle(BaseParser<?> ele, By by, Integer index) {
|
||||
return _makeSessionEle(ele, by, index);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 从接收到的对象或html文本中查找元素,返回SessionElement对象
|
||||
* 如要直接从html生成SessionElement而不在下级查找,loc输入None即可
|
||||
*
|
||||
* @param htmlOrEle html文本、BaseParser对象
|
||||
* @param loc by或字符串,为None时不在下级查找,返回根元素
|
||||
* @param index 获取第几个元素,从1开始,可传入负数获取倒数第几个,None获取所有
|
||||
* @return 返回SessionElement元素或列表
|
||||
*/
|
||||
private static List<SessionElement> _makeSessionEle(Object htmlOrEle, Object loc, Integer index) {
|
||||
//----------------------------------处理定位符------------------------------------------
|
||||
By locs;
|
||||
if (loc == null) {
|
||||
if (htmlOrEle instanceof SessionElement) {
|
||||
ArrayList<SessionElement> sessionElements = new ArrayList<>();
|
||||
sessionElements.add((SessionElement) htmlOrEle);
|
||||
return sessionElements;
|
||||
}
|
||||
locs = By.xpath(".");
|
||||
} else if (loc instanceof By || loc instanceof String) {
|
||||
locs = Locator.getLoc(loc);
|
||||
} else {
|
||||
throw new ClassCastException("定位符必须为str或By");
|
||||
}
|
||||
//---------------根据传入对象类型获取页面对象和lxml元素对象---------------
|
||||
//直接传入html文本
|
||||
BasePage<?> page;
|
||||
if (htmlOrEle instanceof String) {
|
||||
page = null;
|
||||
htmlOrEle = Jsoup.parse((String) htmlOrEle);
|
||||
} else if (htmlOrEle instanceof SessionElement) {
|
||||
page = ((SessionElement) htmlOrEle).getOwner();
|
||||
String str = locs.getValue();
|
||||
if (locs.getName().equals(BySelect.XPATH) && str.stripLeading().startsWith("/")) {
|
||||
str = "." + str;
|
||||
htmlOrEle = ((SessionElement) htmlOrEle).innerEle();
|
||||
//若css以>开头,表示找元素的直接子元素,要用page以绝对路径才能找到
|
||||
} else if (locs.getName().equals(BySelect.CSS_SELECTOR) && str.stripLeading().startsWith(">")) {
|
||||
str = ((SessionElement) htmlOrEle).cssPath() + str;
|
||||
if (((SessionElement) htmlOrEle).getOwner() != null) {
|
||||
htmlOrEle = Jsoup.parse(((SessionElement) htmlOrEle).getOwner().html());
|
||||
} else { //接收html文本,无page的情况
|
||||
htmlOrEle = Jsoup.parse(((SessionElement) htmlOrEle).ele("xpath:/ancestor::*").html());
|
||||
}
|
||||
} else {
|
||||
htmlOrEle = ((SessionElement) htmlOrEle).innerEle();
|
||||
}
|
||||
locs.setValue(str);
|
||||
} else if (htmlOrEle instanceof ChromiumElement) {
|
||||
String str = locs.getValue();
|
||||
if (locs.getName().equals(BySelect.XPATH) && str.stripLeading().startsWith("/")) {
|
||||
str = "." + str;
|
||||
} else if (locs.getName().equals(BySelect.CSS_SELECTOR) && str.stripLeading().startsWith(">")) {
|
||||
str = ((ChromiumElement) htmlOrEle).cssPath() + str;
|
||||
}
|
||||
locs.setValue(str);
|
||||
//获取整个页面html再定位到当前元素,以实现查找上级元素
|
||||
page = ((ChromiumElement) htmlOrEle).getOwner();
|
||||
String xpath = ((ChromiumElement) htmlOrEle).xpath();
|
||||
//ChromiumElement,兼容传入的元素在iframe内的情况
|
||||
String html;
|
||||
if (((ChromiumElement) htmlOrEle).getDocId() != null) {
|
||||
html = JSON.parseObject(((ChromiumElement) htmlOrEle).getOwner().runCdp("DOM.getOuterHTML", Map.of("objectId", ((ChromiumElement) htmlOrEle).getDocId())).toString()).getString("outerHTML");
|
||||
} else {
|
||||
html = ((ChromiumElement) htmlOrEle).getOwner().html();
|
||||
}
|
||||
htmlOrEle = Jsoup.parse(html);
|
||||
htmlOrEle = ((Document) htmlOrEle).selectXpath(xpath).get(0);
|
||||
} else if (htmlOrEle instanceof BasePage) { //各种页面对象
|
||||
page = (BasePage<?>) htmlOrEle;
|
||||
String html = ((BasePage<?>) htmlOrEle).html();
|
||||
if (html.startsWith("<?xml ")) {
|
||||
html = html.replaceAll("<\\?xml.*?>", "");
|
||||
}
|
||||
htmlOrEle = Jsoup.parse(html);
|
||||
} else if (htmlOrEle instanceof BaseElement) {
|
||||
page = ((BaseElement<?, ?>) htmlOrEle).getOwner();
|
||||
htmlOrEle = Jsoup.parse(((BaseElement<?, ?>) htmlOrEle).html());
|
||||
} else {
|
||||
throw new ClassCastException("html_or_ele参数只能是元素、页面对象或html文本。");
|
||||
}
|
||||
// ---------------执行查找-----------------
|
||||
Elements elements;
|
||||
//用lxml内置方法获取lxml的元素对象列表
|
||||
if (locs.getName().equals(BySelect.XPATH)) {
|
||||
elements = ((Element) htmlOrEle).selectXpath(locs.getValue());
|
||||
} else {
|
||||
elements = ((Element) htmlOrEle).select(locs.getValue());
|
||||
}
|
||||
//把lxml元素对象包装成SessionElement对象并按需要返回一个或全部
|
||||
ArrayList<SessionElement> sessionElements = new ArrayList<>();
|
||||
if (index != null) {
|
||||
int count = elements.size();
|
||||
if (count == 0 || Math.abs(index) > count) return null;
|
||||
if (index < 0) index = count + index + 1;
|
||||
Element ele = elements.get(index - 1);
|
||||
sessionElements.add(new SessionElement(ele, page));
|
||||
} else {
|
||||
for (Element element : elements) {
|
||||
sessionElements.add(new SessionElement(element, page));
|
||||
}
|
||||
}
|
||||
return sessionElements;
|
||||
}
|
||||
|
||||
public Element innerEle() {
|
||||
return innerEle;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "<SessionElement " + this.tag() + " " + JSON.toJSONString(this.attrs()) + '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回元素类型
|
||||
*/
|
||||
@Override
|
||||
public String tag() {
|
||||
return this.innerEle.tagName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回outerHTML文本
|
||||
*/
|
||||
@Override
|
||||
public String html() {
|
||||
return this.innerEle.outerHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回元素innerHTML文本
|
||||
*/
|
||||
public String innerHtml() {
|
||||
return this.innerEle.html();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回元素所有属性及值
|
||||
*/
|
||||
@Override
|
||||
public Map<String, String> attrs() {
|
||||
Attributes attributes = this.innerEle.attributes();
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (Attribute attribute : attributes) {
|
||||
String key = attribute.getKey();
|
||||
map.put(key, this.attr(key));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String text() {
|
||||
return Web.getEleTxt(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String rawText() {
|
||||
return this.innerEle.wholeText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionElement sEle(By by, Integer index) {
|
||||
List<SessionElement> sessionElements = this._ele(by, null, index, false, null, "s_ele()");
|
||||
return !sessionElements.isEmpty() ? sessionElements.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionElement sEle(String loc, Integer index) {
|
||||
List<SessionElement> sessionElements = this._ele(loc, null, index, false, null, "s_ele()");
|
||||
return !sessionElements.isEmpty() ? sessionElements.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionElement> sEles(By by) {
|
||||
return this._ele(by, null, null, false, null, "s_eles()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionElement> sEles(String loc) {
|
||||
return this._ele(loc, null, null, false, null, "s_eles()");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒) 无效参数
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数 无效参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置 无效参数
|
||||
* @return SessionElement对象
|
||||
*/
|
||||
@Override
|
||||
protected List<SessionElement> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
return makeSessionEle(this, by, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc 查询元素
|
||||
* @param timeout 查找超时时间(秒) 无效参数
|
||||
* @param index 获取第几个,从0开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数 无效参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置 无效参数
|
||||
* @return SessionElement对象
|
||||
*/
|
||||
@Override
|
||||
protected List<SessionElement> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
return makeSessionEle(this, loc, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String attr(String attr) {
|
||||
if (Objects.equals(attr, "href")) {
|
||||
String href = this.innerEle.attr("href");
|
||||
if (href.toLowerCase().startsWith("javascript:") || href.toLowerCase().startsWith("mailto:")) {
|
||||
return href;
|
||||
} else {
|
||||
return Web.makeAbsoluteLink(href, this.getOwner() != null ? this.getOwner().url() : null);
|
||||
}
|
||||
} else if (Objects.equals(attr, "src")) {
|
||||
return Web.makeAbsoluteLink(this.innerEle.attr("src"), this.getOwner() != null ? this.getOwner().url() : null);
|
||||
} else if (Objects.equals(attr, "text")) {
|
||||
return this.text();
|
||||
} else if (Objects.equals(attr, "innerText")) {
|
||||
return this.rawText();
|
||||
} else if (Objects.equals(attr, "html") || Objects.equals(attr, "outerHTML")) {
|
||||
return this.html();
|
||||
} else if (Objects.equals(attr, "innerHTML")) {
|
||||
return this.innerHtml();
|
||||
} else {
|
||||
return this.innerEle.attr(attr);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getElePath(ElePathMode mode) {
|
||||
StringBuilder pathStr = new StringBuilder();
|
||||
SessionElement ele = this;
|
||||
int brothers;
|
||||
while (ele != null) {
|
||||
if ("css".equalsIgnoreCase(mode.getMode())) {
|
||||
brothers = ele.eles("xpath:./preceding-sibling::*").size();
|
||||
pathStr.insert(0, ">" + ele.tag() + ":nth-child(" + (brothers + 1) + ")");
|
||||
} else {
|
||||
brothers = ele.eles("xpath:./preceding-sibling::" + ele.tag()).size();
|
||||
pathStr = new StringBuilder(brothers > 0 ? "/" + ele.tag() + "[" + (brothers + 1) + "]" + pathStr : "/" + ele.tag() + pathStr);
|
||||
}
|
||||
ele = ele.parent();
|
||||
}
|
||||
return "css".equalsIgnoreCase(mode.getMode()) ? pathStr.substring(1) : pathStr.toString();
|
||||
}
|
||||
|
||||
}
|
868
java/src/main/java/com/ll/DrissonPage/element/ShadowRoot.java
Normal file
868
java/src/main/java/com/ll/DrissonPage/element/ShadowRoot.java
Normal file
@ -0,0 +1,868 @@
|
||||
package com.ll.DrissonPage.element;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.ll.DrissonPage.base.BaseElement;
|
||||
import com.ll.DrissonPage.base.By;
|
||||
import com.ll.DrissonPage.base.BySelect;
|
||||
import com.ll.DrissonPage.error.extend.ElementNotFoundError;
|
||||
import com.ll.DrissonPage.functions.Locator;
|
||||
import com.ll.DrissonPage.functions.Settings;
|
||||
import com.ll.DrissonPage.page.ChromiumBase;
|
||||
import com.ll.DrissonPage.units.states.ShadowRootStates;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class ShadowRoot extends BaseElement<ChromiumBase, ChromiumElement> {
|
||||
@Getter
|
||||
private String objId;
|
||||
@Getter
|
||||
private Integer backendId;
|
||||
private Integer nodeId;
|
||||
private final ChromiumElement parentEle;
|
||||
private ShadowRootStates states;
|
||||
|
||||
/**
|
||||
* @param parentEle shadow root 所在父元素
|
||||
* @param objId js中的object id
|
||||
* @param backendId cdp中的backend id
|
||||
*/
|
||||
public ShadowRoot(ChromiumElement parentEle, String objId, Integer backendId) {
|
||||
super(parentEle.getOwner());
|
||||
this.parentEle = parentEle;
|
||||
if (backendId != null) {
|
||||
this.backendId = backendId;
|
||||
this.objId = this.getObjId(backendId);
|
||||
this.nodeId = this.getNodeId(this.objId);
|
||||
} else if (objId != null) {
|
||||
this.objId = objId;
|
||||
this.nodeId = this.getNodeId(objId);
|
||||
this.backendId = this.getBackendId(this.nodeId);
|
||||
}
|
||||
this.states = null;
|
||||
super.setType("ShadowRoot");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ShadowRoot || obj instanceof ChromiumElement)
|
||||
if (obj instanceof ShadowRoot) return Objects.equals(this.backendId, ((ShadowRoot) obj).getBackendId());
|
||||
else return Objects.equals(this.backendId, ((ChromiumElement) obj).getBackendId());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回元素标签名
|
||||
*/
|
||||
@Override
|
||||
public String tag() {
|
||||
return "shadow-root";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回outerHTML文本
|
||||
*/
|
||||
@Override
|
||||
public String html() {
|
||||
return "<shadow_root>" + this.innerHtml() + "</shadow_root>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回内部的html文本
|
||||
*/
|
||||
public String innerHtml() {
|
||||
return this.runJs("return this.innerHTML;").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于获取元素状态的对象
|
||||
*/
|
||||
public ShadowRootStates states() {
|
||||
if (this.states == null) this.states = new ShadowRootStates(this);
|
||||
return this.states;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @return 运行的结果
|
||||
*/
|
||||
public Object runJs(String js) {
|
||||
return runJs(js, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @param params 参数,按顺序在js文本中对应arguments[0]、arguments[1]...
|
||||
* @return 运行的结果
|
||||
*/
|
||||
public Object runJs(String js, List<Object> params) {
|
||||
return runJs(js, null, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @param timeout js超时时间(秒),为None则使用页面timeouts.script设置
|
||||
* @param params 参数,按顺序在js文本中对应arguments[0]、arguments[1]...
|
||||
* @return 运行的结果
|
||||
*/
|
||||
public Object runJs(String js, Double timeout, List<Object> params) {
|
||||
return runJs(js, false, timeout, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @param asExpr 是否作为表达式运行,为True时args无效
|
||||
* @param timeout js超时时间(秒),为None则使用页面timeouts.script设置
|
||||
* @param params 参数,按顺序在js文本中对应arguments[0]、arguments[1]...
|
||||
* @return 运行的结果
|
||||
*/
|
||||
public Object runJs(String js, Boolean asExpr, Double timeout, List<Object> params) {
|
||||
return ChromiumElement.runJs(this, js, asExpr, timeout != null ? timeout : this.getOwner().getTimeouts().getScript(), params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
*/
|
||||
public void runAsyncJs(String js) {
|
||||
runAsyncJs(js, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @param params 参数,按顺序在js文本中对应arguments[0]、arguments[1]...
|
||||
*/
|
||||
public void runAsyncJs(String js, List<Object> params) {
|
||||
runAsyncJs(js, 0.0, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @param params 参数,按顺序在js文本中对应arguments[0]、arguments[1]...
|
||||
* @param timeout js超时时间(秒),为None则使用页面timeouts.script设置
|
||||
*/
|
||||
public void runAsyncJs(String js, Double timeout, List<Object> params) {
|
||||
runAsyncJs(js, false, params, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本元素执行javascript代码
|
||||
*
|
||||
* @param js js文本,文本中用this表示本元素
|
||||
* @param asExpr 是否作为表达式运行,为True时args无效
|
||||
* @param params 参数,按顺序在js文本中对应arguments[0]、arguments[1]...
|
||||
* @param timeout js超时时间(秒),为None则使用页面timeouts.script设置
|
||||
*/
|
||||
public void runAsyncJs(String js, Boolean asExpr, List<Object> params, Double timeout) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ChromiumElement.runJs(this, js, asExpr, timeout == null ? 0 : timeout, params);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChromiumElement parent(By by, Integer index) {
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String loc = "xpath:./ancestor-or-self::" + by.getValue().replaceFirst("^[.\\s/]+", "") + "[" + index + "]";
|
||||
return this.parentEle._ele(loc, 0.0, null, true, false, "parent()").get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChromiumElement parent(Integer level) {
|
||||
String loc = "xpath:./ancestor-or-self::*[" + level + "]";
|
||||
return this.parentEle._ele(loc, 0.0, null, true, false, "parent()").get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChromiumElement parent(String loc, Integer index) {
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
loc = "xpath:./ancestor-or-self::" + by.getValue().replaceFirst("^[.\\s/]+", "") + "[" + index + "]";
|
||||
return this.parentEle._ele(loc, 0.0, null, true, false, "parent()").get(0);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回直接子元素元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement child() {
|
||||
return this.child("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回直接子元素元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement child(String loc) {
|
||||
return this.child(loc == null || loc.isEmpty() ? null : loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回直接子元素元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,1开始
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement child(String loc, Integer index) {
|
||||
String value;
|
||||
if (loc == null || loc.isEmpty()) value = "*";
|
||||
else {
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
}
|
||||
value = "xpath:./" + value;
|
||||
List<ChromiumElement> list = this._ele(value, null, index, true, null, null);
|
||||
if (list != null && !list.isEmpty()) return list.get(0);
|
||||
if (Settings.raiseWhenEleNotFound)
|
||||
throw new ElementNotFoundError("child()", Map.of("filter_loc", loc == null ? "" : loc, "index", index));
|
||||
else return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回直接子元素元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement child(By loc) {
|
||||
return this.child(loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回直接子元素元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param index 第几个查询结果,1开始
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement child(By by, Integer index) {
|
||||
String loc = by.getName().getName() + ":" + by.getValue();
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
List<ChromiumElement> list = this._ele(value, null, index, true, null, null);
|
||||
if (list != null && !list.isEmpty()) return list.get(0);
|
||||
if (Settings.raiseWhenEleNotFound) {
|
||||
throw new ElementNotFoundError("child()", Map.of("filter_loc", loc, "index", index));
|
||||
} else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(By by, Integer index) {
|
||||
return this.next(by, index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 无效参数
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(By by, Integer index, Double timeout) {
|
||||
return this.next(by, index, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 无效参数
|
||||
* @param eleOnly 无效参数
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(By by, Integer index, Double timeout, Boolean eleOnly) {
|
||||
String loc = by.getName().getName() + ":" + by.getValue();
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
List<ChromiumElement> list = this.parentEle._ele(value, null, index, true, null, null);
|
||||
if (list != null && !list.isEmpty()) return list.get(0);
|
||||
if (Settings.raiseWhenEleNotFound) {
|
||||
throw new ElementNotFoundError("next()", Map.of("filter_loc", loc, "index", index));
|
||||
} else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(String loc, Integer index) {
|
||||
return this.next(loc, index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 无效参数
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(String loc, Integer index, Double timeout) {
|
||||
return this.next(loc, index, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 无效参数
|
||||
* @param eleOnly 无效参数
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(String loc, Integer index, Double timeout, Boolean eleOnly) {
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
List<ChromiumElement> list = this.parentEle._ele(value, null, index, true, false, null);
|
||||
if (list != null && !list.isEmpty()) return list.get(0);
|
||||
if (Settings.raiseWhenEleNotFound) {
|
||||
throw new ElementNotFoundError("next()", Map.of("filter_loc", loc, "index", index));
|
||||
} else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(Integer index) {
|
||||
return next(index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 无效参数
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(Integer index, Double timeout) {
|
||||
return next(index, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index 第几个查询结果,0开始
|
||||
* @param timeout 无效参数
|
||||
* @param eleOnly 无效参数
|
||||
* @return ChromiumElement对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumElement next(Integer index, Double timeout, Boolean eleOnly) {
|
||||
return next("", index);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement before() {
|
||||
return this.before("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement before(String loc) {
|
||||
return this.before(loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 第几个查询结果,1开始
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement before(String loc, Integer index) {
|
||||
String value;
|
||||
if (loc == null) loc = "";
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./preceding::" + value;
|
||||
List<ChromiumElement> list = this.parentEle._ele(value, null, index, true, null, null);
|
||||
if (list != null && !list.isEmpty()) return list.get(0);
|
||||
if (Settings.raiseWhenEleNotFound)
|
||||
throw new ElementNotFoundError("before()", Map.of("filter_loc", loc == null ? "" : loc, "index", index));
|
||||
else return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement before(By by) {
|
||||
return this.before(by, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param index 第几个查询结果,1开始
|
||||
* @return 直接子元素
|
||||
*/
|
||||
|
||||
public ChromiumElement before(By by, Integer index) {
|
||||
String loc = by.getName().getName() + ":" + by.getValue();
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./preceding::" + value;
|
||||
List<ChromiumElement> list = this.parentEle._ele(value, null, index, true, null, null);
|
||||
if (list != null && !list.isEmpty()) return list.get(0);
|
||||
if (Settings.raiseWhenEleNotFound)
|
||||
throw new ElementNotFoundError("child()", Map.of("filter_loc", loc, "index", index));
|
||||
else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中此当前元素后面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return 本元素后面的某个元素
|
||||
*/
|
||||
public ChromiumElement after(By by) {
|
||||
return after(by, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中此当前元素后面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @param index 后面第几个查询结果,1开始
|
||||
* @return 本元素后面的某个元素
|
||||
*/
|
||||
public ChromiumElement after(By by, Integer index) {
|
||||
String loc = by.getName().getName() + ":" + by.getValue();
|
||||
List<ChromiumElement> afters = this.afters(by);
|
||||
if (afters != null && !afters.isEmpty()) {
|
||||
index -= 1;
|
||||
if (index < 0) index = afters.size() + index;
|
||||
return afters.get(index);
|
||||
}
|
||||
if (Settings.raiseWhenEleNotFound)
|
||||
throw new ElementNotFoundError("after(()", Map.of("filter_loc", loc, "index", index));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中此当前元素后面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @return 本元素后面的某个元素
|
||||
*/
|
||||
public ChromiumElement after() {
|
||||
return after("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中此当前元素后面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 本元素后面的某个元素
|
||||
*/
|
||||
public ChromiumElement after(String loc) {
|
||||
return after(loc, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中此当前元素后面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @param index 后面第几个查询结果,1开始
|
||||
* @return 本元素后面的某个元素
|
||||
*/
|
||||
public ChromiumElement after(String loc, Integer index) {
|
||||
List<ChromiumElement> afters = this.afters(loc);
|
||||
if (afters != null && !afters.isEmpty()) {
|
||||
index -= 1;
|
||||
if (index < 0) index = afters.size() + index;
|
||||
return afters.get(index);
|
||||
}
|
||||
if (Settings.raiseWhenEleNotFound)
|
||||
throw new ElementNotFoundError("after(()", Map.of("filter_loc", loc, "index", index));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素符合条件的直接子元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> children(By by) {
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
return this._ele(value, null, null, true, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素符合条件的直接子元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> children() {
|
||||
return this.children("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素符合条件的直接子元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> children(String loc) {
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
return this._ele(value, null, null, true, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面符合条件的同级元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> nexts(By by) {
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
return this.parentEle._ele(value, null, null, true, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面符合条件的同级元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> nexts() {
|
||||
return nexts("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前元素后面符合条件的同级元素或节点组成的列表,可用查询语法筛选
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return ChromiumElement对象组成的列表
|
||||
*/
|
||||
public List<ChromiumElement> nexts(String loc) {
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./" + value;
|
||||
return this.parentEle._ele(value, null, null, true, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的元素或节点组成的列表,可用查询语法筛选
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return 本元素前面的元素
|
||||
*/
|
||||
public List<ChromiumElement> befores(By by) {
|
||||
by = Locator.getLoc(by, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./preceding::" + value;
|
||||
return this.parentEle._ele(value, null, null, true, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的元素或节点组成的列表,可用查询语法筛选
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @return 本元素前面的元素
|
||||
*/
|
||||
public List<ChromiumElement> befores() {
|
||||
return this.befores("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素前面符合条件的元素或节点组成的列表,可用查询语法筛选
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 本元素前面的元素
|
||||
*/
|
||||
public List<ChromiumElement> befores(String loc) {
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR))
|
||||
throw new IllegalArgumentException("此css selector语法不受支持,请换成xpath。");
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./preceding::" + value;
|
||||
return this.parentEle._ele(value, null, null, true, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素后面符合条件的元素或节点组成的列表,可用查询语法筛选
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param by 用于筛选的查询语法
|
||||
* @return 本元素后面的元素
|
||||
*/
|
||||
public List<ChromiumElement> afters(By by) {
|
||||
List<ChromiumElement> next = this.nexts(by);
|
||||
by = Locator.getLoc(by, true, false);
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./following::" + value;
|
||||
List<ChromiumElement> list = this.parentEle._ele(value, null, null, true, null, null);
|
||||
List<ChromiumElement> returns = new ArrayList<>();
|
||||
if (next != null) returns.addAll(next);
|
||||
if (list != null) returns.addAll(list);
|
||||
return returns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素后面符合条件的元素或节点组成的列表,可用查询语法筛选
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @return 本元素后面的元素
|
||||
*/
|
||||
public List<ChromiumElement> afters() {
|
||||
return this.afters("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文档中当前元素后面符合条件的元素或节点组成的列表,可用查询语法筛选
|
||||
* 查找范围不限同级元素,而是整个DOM文档
|
||||
*
|
||||
* @param loc 用于筛选的查询语法
|
||||
* @return 本元素后面的元素
|
||||
*/
|
||||
public List<ChromiumElement> afters(String loc) {
|
||||
List<ChromiumElement> next = this.nexts(loc);
|
||||
By by = Locator.getLoc(loc, true, false);
|
||||
String value = by.getValue().replaceAll("^[.\\s/]+", "");
|
||||
value = "xpath:./following::" + value;
|
||||
List<ChromiumElement> list = this.parentEle._ele(value, null, null, true, null, null);
|
||||
List<ChromiumElement> returns = new ArrayList<>();
|
||||
if (next != null) returns.addAll(next);
|
||||
if (list != null) returns.addAll(list);
|
||||
return returns;
|
||||
}
|
||||
|
||||
|
||||
public ChromiumElement ele(By by, int index, Double timeout) {
|
||||
List<ChromiumElement> list = this._ele(by, timeout, index, null, null, "ele()");
|
||||
return !list.isEmpty() ? list.get(0) : null;
|
||||
}
|
||||
|
||||
public ChromiumElement ele(String loc, int index, Double timeout) {
|
||||
List<ChromiumElement> list = this._ele(loc, timeout, index, null, null, "ele()");
|
||||
return !list.isEmpty() ? list.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChromiumElement> eles(By by, Double timeout) {
|
||||
return this._ele(by, timeout, null, null, null, "ele()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChromiumElement> eles(String loc, Double timeout) {
|
||||
return this._ele(loc, timeout, null, null, null, "ele()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionElement sEle(By by, Integer index) {
|
||||
List<SessionElement> sessionElements = SessionElement.makeSessionEle(this, by, index);
|
||||
return sessionElements != null && !sessionElements.isEmpty() ? sessionElements.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionElement sEle(String loc, Integer index) {
|
||||
List<SessionElement> sessionElements = SessionElement.makeSessionEle(this, loc, index);
|
||||
return sessionElements != null && !sessionElements.isEmpty() ? sessionElements.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionElement> sEles(By by) {
|
||||
return SessionElement.makeSessionEle(this, by, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionElement> sEles(String loc) {
|
||||
return SessionElement.makeSessionEle(this, loc, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ChromiumElement> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
return _findElement(Locator.getLoc(by, false, false), timeout, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ChromiumElement> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
return _findElement(Locator.getLoc(loc, false, false), timeout, index);
|
||||
}
|
||||
|
||||
private List<ChromiumElement> _findElement(By by, Double timeout, Integer index) {
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR) && by.getValue().startsWith(":root")) {
|
||||
by.setValue(by.getValue().substring(5));
|
||||
}
|
||||
timeout = timeout == null ? this.getOwner().timeout() : timeout;
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
List<ChromiumElement> result = doFind(by, index);
|
||||
while (result == null && System.currentTimeMillis() <= endTime) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
result = doFind(by, index);
|
||||
}
|
||||
if (result != null) return result;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private List<ChromiumElement> doFind(By by, Integer index) {
|
||||
if (by.getName().equals(BySelect.CSS_SELECTOR)) {
|
||||
if (index != null && index == 1) {
|
||||
Object nodeId = JSON.parseObject(this.getOwner().runCdp("DOM.querySelector", Map.of("nodeId", this.nodeId, "selector", by.getValue())).toString()).get("nodeId");
|
||||
if (nodeId != null && !nodeId.toString().isEmpty()) {
|
||||
return ChromiumElement.makeChromiumEles(this.getOwner(), nodeId, 1, false);
|
||||
}
|
||||
} else {
|
||||
Object nodId = JSON.parseObject(this.getOwner().runCdp("DOM.querySelectorAll", Map.of("nodeId", this.nodeId, "selector", by.getValue())).toString()).get("nodeId");
|
||||
return ChromiumElement.makeChromiumEles(this.getOwner(), nodId, 1, false);
|
||||
}
|
||||
} else {
|
||||
List<SessionElement> sessionElements = SessionElement.makeSessionEle(this.html(), "", 1).get(0).eles(by);
|
||||
if (sessionElements == null || sessionElements.isEmpty()) return null;
|
||||
List<String> css = new ArrayList<>();
|
||||
for (SessionElement ele : sessionElements) css.add(ele.cssPath().substring(0, 61));
|
||||
if (index != null) {
|
||||
Object nodeId;
|
||||
try {
|
||||
index -= 1;
|
||||
if (index < 0) index = css.size() + index;
|
||||
nodeId = JSON.parseObject(this.getOwner().runCdp("DOM.querySelector", Map.of("nodeId", this.nodeId, "selector", css.get(index))).toString()).get("nodeId");
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
return null;
|
||||
}
|
||||
return ChromiumElement.makeChromiumEles(this.getOwner(), nodeId, 1, false);
|
||||
} else {
|
||||
List<Object> nodeIds = new ArrayList<>();
|
||||
for (String s : css)
|
||||
nodeIds.add(JSON.parseObject(this.getOwner().runCdp("DOM.querySelector", Map.of("nodeId", this.nodeId, "selector", s)).toString()).get("nodeId"));
|
||||
if (nodeIds.contains(0)) return null;
|
||||
return ChromiumElement.makeChromiumEles(this.getOwner(), nodeId, 1, false);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据传入object id或backend id获取cdp中的node id
|
||||
*
|
||||
* @param objId backend id
|
||||
* @return cdp中的node id
|
||||
*/
|
||||
private Integer getNodeId(String objId) {
|
||||
return JSON.parseObject(this.getOwner().runCdp("DOM.requestNode", Map.of("objectId", objId)).toString()).getInteger("nodeId");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据传入node id或backend id获取js中的object id
|
||||
*
|
||||
* @param backendId backend id
|
||||
* @return js中的object id
|
||||
*/
|
||||
private String getObjId(Integer backendId) {
|
||||
return JSON.parseObject(this.getOwner().runCdp("DOM.describeNode", Map.of("backendNodeId", backendId)).toString()).getJSONObject("object").getString("objectId");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据传入node id获取backend id
|
||||
*
|
||||
* @param nodeId js中的nodeId
|
||||
* @return backend id
|
||||
*/
|
||||
private Integer getBackendId(Integer nodeId) {
|
||||
return JSON.parseObject(this.getOwner().runCdp("DOM.describeNode", Map.of("nodeId", nodeId)).toString()).getJSONObject("node").getInteger("backendNodeId");
|
||||
}
|
||||
|
||||
}
|
20
java/src/main/java/com/ll/DrissonPage/error/BaseError.java
Normal file
20
java/src/main/java/com/ll/DrissonPage/error/BaseError.java
Normal file
@ -0,0 +1,20 @@
|
||||
package com.ll.DrissonPage.error;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class BaseError extends RuntimeException {
|
||||
private final String info;
|
||||
|
||||
public BaseError(String info) {
|
||||
super(info);
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return info;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class AlertExistsError extends BaseError {
|
||||
public AlertExistsError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public AlertExistsError() {
|
||||
super("存在未处理的提示框");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class BrowserConnectError extends BaseError {
|
||||
public BrowserConnectError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public BrowserConnectError() {
|
||||
super("浏览器连接失败。");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class CDPError extends BaseError {
|
||||
public CDPError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public CDPError() {
|
||||
super("方法调用错误.");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class CanNotClickError extends BaseError {
|
||||
public CanNotClickError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public CanNotClickError() {
|
||||
super("该元素无法滚动到视口或被遮挡,无法点击。");
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class ContextLostError extends BaseError {
|
||||
public ContextLostError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public ContextLostError() {
|
||||
super("页面被刷新,请操作前尝试等待页面刷新或加载完成。");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class CookieFormatError extends BaseError {
|
||||
public CookieFormatError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public CookieFormatError() {
|
||||
super("cookie格式不正确.");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class ElementLostError extends BaseError {
|
||||
public ElementLostError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public ElementLostError() {
|
||||
super("元素对象已失效。可能是页面整体刷新,或js局部刷新把元素替换或去除了。");
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class ElementNotFoundError extends BaseError {
|
||||
private String method;
|
||||
private Map<String, Object> arguments;
|
||||
|
||||
public ElementNotFoundError(String method, Map<String, Object> arguments) {
|
||||
this();
|
||||
this.method = method;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ElementNotFoundError(String errorInfo, String method, Map<String, Object> arguments) {
|
||||
super(errorInfo);
|
||||
this.method = method;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ElementNotFoundError(String errorInfo) {
|
||||
super(errorInfo);
|
||||
}
|
||||
|
||||
public ElementNotFoundError() {
|
||||
this("没有找到该元素");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + ((method != null && !method.isEmpty()) ? "\nmethod: " + method : "") + ((arguments != null && !arguments.isEmpty()) ? "\nargs: " + arguments : "");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class GetDocumentError extends BaseError {
|
||||
public GetDocumentError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public GetDocumentError() {
|
||||
super("获取文档失败。");
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class InvalidSelectorError extends BaseError {
|
||||
public InvalidSelectorError(String info) {
|
||||
super(info);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class JavaScriptError extends BaseError {
|
||||
public JavaScriptError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public JavaScriptError() {
|
||||
super("JavaScript运行错误.");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class NoRectError extends BaseError {
|
||||
public NoRectError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public NoRectError() {
|
||||
super("该元素没有位置及大小");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class NoResourceError extends BaseError {
|
||||
public NoResourceError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public NoResourceError() {
|
||||
super("该元素无可保存的内容或保存失败。");
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class PageDisconnectedError extends BaseError {
|
||||
|
||||
public PageDisconnectedError() {
|
||||
super("与页面的连接已断开");
|
||||
}
|
||||
|
||||
public PageDisconnectedError(String info) {
|
||||
super(info);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
public class StorageError extends BaseError {
|
||||
public StorageError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public StorageError() {
|
||||
super("无法操作当前存储数据。");
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class WaitTimeoutError extends BaseError {
|
||||
public WaitTimeoutError(String info) {
|
||||
super(info);
|
||||
}
|
||||
|
||||
public WaitTimeoutError() {
|
||||
super("等待失败。");
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
import com.ll.DrissonPage.error.BaseError;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class WrongURLError extends BaseError {
|
||||
|
||||
|
||||
public WrongURLError() {
|
||||
super("无效的url。");
|
||||
}
|
||||
|
||||
public WrongURLError(String info) {
|
||||
super(info);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.ll.DrissonPage.error.extend;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class loadFileError extends RuntimeException {
|
||||
public loadFileError() {
|
||||
super("load file error");
|
||||
}
|
||||
|
||||
public loadFileError(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public loadFileError(Throwable cause) {
|
||||
super("load file error", cause);
|
||||
}
|
||||
|
||||
public loadFileError(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,474 @@
|
||||
package com.ll.DrissonPage.functions;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.config.ChromiumOptions;
|
||||
import com.ll.DrissonPage.config.OptionsManager;
|
||||
import com.ll.DrissonPage.error.extend.BrowserConnectError;
|
||||
import com.ll.DrissonPage.utils.CloseableHttpClientUtils;
|
||||
import com.sun.jna.platform.win32.Advapi32Util;
|
||||
import com.sun.jna.platform.win32.WinReg;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class BrowserUtils {
|
||||
/**
|
||||
* 连接或启动浏览器
|
||||
*
|
||||
* @param options ChromiumOptions对象
|
||||
* @return 返回是否接管的浏览器
|
||||
*/
|
||||
public static boolean connectBrowser(ChromiumOptions options) {
|
||||
if (options == null) throw new NullPointerException("必须传入ChromiumOptions对象");
|
||||
String address = options.getAddress().replace("localhost", "127.0.0.1").replace("http://", "").replace("https://", "");
|
||||
String browserPath = options.getBrowserPath();
|
||||
String[] split = address.split(":");
|
||||
String ip = split[0];
|
||||
String port = split[1];
|
||||
if (!ip.equals("127.0.0.1") || Tools.portIsUsing(ip, Integer.parseInt(port)) || options.isExistingOnly()) {
|
||||
testConnect(ip, port);
|
||||
for (String argument : options.getArguments()) {
|
||||
if (argument.startsWith("--headless") && argument.endsWith("=false")) {
|
||||
options.headless(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ----------创建浏览器进程----------
|
||||
List<String> args = getLaunchArgs(options);
|
||||
setPrefs(options);
|
||||
setFlags(options);
|
||||
try {
|
||||
runBrowser(port, browserPath, args);
|
||||
} catch (Exception e) {
|
||||
browserPath = getChromePath();
|
||||
if (browserPath == null) {
|
||||
throw new RuntimeException("未找到浏览器,请手动指定浏览器可执行文件路径。");
|
||||
}
|
||||
runBrowser(port, browserPath, args);
|
||||
}
|
||||
testConnect(ip, port);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从ChromiumOptions获取命令行启动参数
|
||||
*
|
||||
* @param options ChromiumOptions
|
||||
* @return 启动参数列表
|
||||
*/
|
||||
public static List<String> getLaunchArgs(ChromiumOptions options) {
|
||||
Set<String> result = new HashSet<>();
|
||||
boolean hasUserPath = false;
|
||||
Boolean headless = null;
|
||||
|
||||
for (String arg : options.getArguments()) {
|
||||
if (arg.startsWith("--load-extension=") || arg.startsWith("--remote-debugging-port=")) {
|
||||
continue;
|
||||
} else if (arg.startsWith("--user-data-dir") && !options.isSystemUserPath()) {
|
||||
result.add("--user-data-dir=" + Path.of(arg.substring(16)).toAbsolutePath());
|
||||
hasUserPath = true;
|
||||
continue;
|
||||
} else if (arg.startsWith("--headless")) {
|
||||
if ("--headless=false".equals(arg)) {
|
||||
headless = false;
|
||||
continue;
|
||||
} else if ("--headless".equals(arg)) {
|
||||
arg = "--headless=new";
|
||||
headless = true;
|
||||
} else {
|
||||
headless = true;
|
||||
}
|
||||
}
|
||||
|
||||
result.add(arg);
|
||||
}
|
||||
|
||||
if (!hasUserPath && !options.isSystemUserPath()) {
|
||||
String port = options.getAddress() != null ? options.getAddress().split(":")[1] : "0";
|
||||
Path p = options.getTmpPath() != null && !options.getTmpPath().isEmpty() ? Path.of(options.getTmpPath()) : Paths.get(System.getProperty("java.io.tmpdir")).resolve("DrissionPage");
|
||||
Path path = p.resolve("userData_" + port);
|
||||
path.toFile().mkdirs();
|
||||
options.setUserDataPath(path.toString());
|
||||
result.add("--user-data-dir=" + path);
|
||||
}
|
||||
|
||||
if (headless == null && System.getProperty("os.name").toLowerCase().contains("linux")) {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec("systemctl list-units | grep graphical.target");
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
if (reader.lines().noneMatch(line -> line.contains("graphical.target"))) {
|
||||
headless = true;
|
||||
result.add("--headless=new");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
List<String> resultList = new ArrayList<>(result);
|
||||
options.headless(Boolean.TRUE.equals(headless));
|
||||
|
||||
// 处理插件extensions
|
||||
List<String> extensions = options.getExtensions().stream().map(e -> Path.of(e).toAbsolutePath().toString()).distinct().collect(Collectors.toList());
|
||||
|
||||
if (!extensions.isEmpty()) {
|
||||
String ext = String.join(",", extensions);
|
||||
result.add("--load-extension=" + ext);
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理启动配置中的prefs项,目前只能对已存在文件夹配置
|
||||
*
|
||||
* @param options ChromiumOptions
|
||||
*/
|
||||
public static void setPrefs(ChromiumOptions options) {
|
||||
if (options == null) throw new NullPointerException("必须传入ChromiumOptions对象");
|
||||
if (options.getUserDataPath() == null || options.getPres() == null && options.getPresToDel() == null) return;
|
||||
Map<String, Object> pres = options.getPres();
|
||||
List<String> delList = options.getPresToDel();
|
||||
String user = "Default";
|
||||
for (String argument : options.getArguments())
|
||||
if (argument.startsWith("--profile-directory")) {
|
||||
String[] split = argument.split("=");
|
||||
user = split[split.length - 1].strip();
|
||||
break;
|
||||
}
|
||||
Path prefsFile = Paths.get(options.getUserDataPath()).resolve(user).resolve("Preferences");
|
||||
if (!Files.exists(prefsFile)) {
|
||||
try {
|
||||
// 创建父目录(如果不存在)
|
||||
Files.createDirectories(prefsFile.getParent());
|
||||
// 创建文件并写入空的 JSON 对象
|
||||
Files.write(prefsFile, "{}".getBytes(), StandardOpenOption.CREATE);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 处理异常,例如文件创建失败
|
||||
}
|
||||
}
|
||||
|
||||
// 读取属性文件
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try (BufferedReader reader = Files.newBufferedReader(prefsFile, StandardCharsets.UTF_8)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
builder = new StringBuilder("{}");
|
||||
}
|
||||
JSONObject targetDict = JSON.parseObject(builder.toString());
|
||||
// 设置新的属性值
|
||||
for (String pref : pres.keySet()) {
|
||||
Object value = pres.get(pref);
|
||||
String[] prefArray = pref.split("\\.");
|
||||
|
||||
List<String> list = Arrays.asList(prefArray);
|
||||
makeLeaveInMap(targetDict, list, 0, prefArray.length);
|
||||
setValueToMap(targetDict, list, value);
|
||||
}
|
||||
// 删除属性
|
||||
for (String pref : delList) {
|
||||
removeArgFromDict(targetDict, pref);
|
||||
}
|
||||
|
||||
// 写入属性文件
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(prefsFile, StandardCharsets.UTF_8)) {
|
||||
writer.write(targetDict.toJSONString());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理启动配置中的prefs项,目前只能对已存在文件夹配置
|
||||
*
|
||||
* @param options ChromiumOptions
|
||||
*/
|
||||
public static void setFlags(ChromiumOptions options) {
|
||||
if (options == null) throw new NullPointerException("必须传入ChromiumOptions对象");
|
||||
if (options.getUserDataPath() == null || options.getPres() == null && options.getPresToDel() == null) return;
|
||||
Path stateFile = Paths.get(options.getUserDataPath()).resolve("Local State");
|
||||
if (!Files.exists(stateFile)) {
|
||||
try {
|
||||
// 创建父目录(如果不存在)
|
||||
Files.createDirectories(stateFile.getParent());
|
||||
// 创建文件并写入空的 JSON 对象
|
||||
Files.write(stateFile, "{}".getBytes(), StandardOpenOption.CREATE);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 处理异常,例如文件创建失败
|
||||
}
|
||||
}
|
||||
// 读取属性文件
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try (BufferedReader reader = Files.newBufferedReader(stateFile, StandardCharsets.UTF_8)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
builder = new StringBuilder("{}");
|
||||
}
|
||||
JSONObject jsonObject;
|
||||
try {
|
||||
jsonObject = JSON.parseObject(builder.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
jsonObject = JSON.parseObject("{}");
|
||||
|
||||
}
|
||||
if (!options.isClearFileFlags()) {
|
||||
jsonObject.put("browser", Objects.requireNonNullElseGet(jsonObject.get("browser"), HashMap::new));
|
||||
jsonObject.put("enabled_labs_experiments", Objects.requireNonNullElseGet(jsonObject.get("enabled_labs_experiments"), HashMap::new));
|
||||
}
|
||||
Map<String, Object> flagsMap = new HashMap<>(options.getFlags());
|
||||
List<String> value = new ArrayList<>();
|
||||
flagsMap.forEach((k, v) -> value.add(v == null ? k : k + "@" + v));
|
||||
jsonObject.getJSONObject("browser").put("enabled_labs_experiments", value);
|
||||
// 写入属性文件
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(stateFile, StandardCharsets.UTF_8)) {
|
||||
writer.write(jsonObject.toJSONString());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试浏览器是否可用默认三十秒
|
||||
*
|
||||
* @param ip 浏览器ip
|
||||
* @param port 浏览器端口
|
||||
*/
|
||||
public static void testConnect(String ip, String port) {
|
||||
testConnect(ip, port, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试浏览器是否可用
|
||||
*
|
||||
* @param ip 浏览器ip
|
||||
* @param port 浏览器端口
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static void testConnect(String ip, String port, Integer timeout) {
|
||||
long endTime = System.currentTimeMillis() + (long) (timeout * 1000);
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
try {
|
||||
HttpGet request = new HttpGet("http://" + ip + ":" + port + "/json");
|
||||
request.setHeader("Connection", "close");
|
||||
request.setConfig(RequestConfig.custom().setConnectTimeout(20).build());
|
||||
String text = CloseableHttpClientUtils.sendRequestJson(request);
|
||||
if (text != null) {
|
||||
for (Object o : JSON.parseArray(text)) {
|
||||
String type = JSON.parseObject(o.toString()).get("type").toString();
|
||||
if ("page".contains(type) || "webview".contains(type)) return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new BrowserConnectError("\n" + ip + ":" + port + "浏览器无法链接。\n请确认:\n1、该端口为浏览器\n" + "2、已添加'--remote-debugging-port=" + port + "'启动项\n" + "3、用户文件夹没有和已打开的浏览器冲突\n" + "4、如为无界面系统,请添加'--headless=new '参数\n" + "5、如果是Linux系统,可能还要添加'--no-sandbox '启动参数\n" + "可使用ChromiumOptions设置端口和用户文件夹路径。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建chrome进程
|
||||
*
|
||||
* @param port 端口号
|
||||
* @param path 浏览器路径
|
||||
* @param args 启动参数
|
||||
*/
|
||||
private static void runBrowser(String port, String path, List<String> args) {
|
||||
File executable = new File(path);
|
||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
||||
// 设置命令和参数
|
||||
processBuilder.command(executable.getAbsolutePath(), "--remote-debugging-port=" + port);
|
||||
processBuilder.command().addAll(args);
|
||||
|
||||
// 设置工作目录
|
||||
processBuilder.directory(executable.getParentFile());
|
||||
try {
|
||||
// 启动进程
|
||||
processBuilder.start();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("未找到浏览器,请手动指定浏览器可执行文件路径。", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把prefs中a.b.c形式的属性转为a['b']['c']形式
|
||||
*
|
||||
* @param targetDict 要处理的map
|
||||
* @param src 属性层级列表[a, b, c]
|
||||
* @param num 当前处理第几个
|
||||
* @param end src长度
|
||||
*/
|
||||
public static void makeLeaveInMap(JSONObject targetDict, List<String> src, int num, int end) {
|
||||
if (num == end) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = src.get(num);
|
||||
|
||||
if (!targetDict.containsKey(key)) {
|
||||
targetDict.put(key, new JSONObject());
|
||||
}
|
||||
|
||||
num++;
|
||||
try {
|
||||
makeLeaveInMap(targetDict.getJSONObject(key), src, num, end);
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void setValueToMap(JSONObject targetDict, List<String> src, Object value) {
|
||||
setNestedValue(targetDict, src, value, 0);
|
||||
}
|
||||
|
||||
private static void setNestedValue(JSONObject currentDict, List<String> src, Object value, int index) {
|
||||
// 当前层级是最后一层,设置值
|
||||
if (index == src.size() - 1) currentDict.put(src.get(index), value);
|
||||
else {
|
||||
// 当前层级不是最后一层,继续递归
|
||||
JSONObject nestedDict = JSON.parseObject(currentDict.getOrDefault(src.get(index), new HashMap<>()).toString());
|
||||
currentDict.put(src.get(index), nestedDict);
|
||||
setNestedValue(nestedDict, src, value, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeArgFromDict(JSONObject targetDict, String arg) {
|
||||
List<String> args = List.of(arg.split("\\."));
|
||||
removeNestedArg(targetDict, args, 0);
|
||||
}
|
||||
|
||||
private static void removeNestedArg(JSONObject currentDict, List<String> args, int index) {
|
||||
if (index == args.size() - 1) {
|
||||
// 当前层级是最后一层,删除值
|
||||
currentDict.remove(args.get(index));
|
||||
} else {
|
||||
// 当前层级不是最后一层,继续递归
|
||||
JSONObject nestedDict = JSON.parseObject(currentDict.get(args.get(index)).toString());
|
||||
if (nestedDict != null) {
|
||||
removeNestedArg(nestedDict, args, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 从ini文件或系统变量中获取chrome可执行文件的路径
|
||||
*/
|
||||
public static String getChromePath() {
|
||||
// 从ini文件中获取
|
||||
String path = new OptionsManager().getValue("chromium_options", "browser_path");
|
||||
if (path != null && new File(path).isFile()) return path;
|
||||
|
||||
// 使用which获取
|
||||
String[] candidates = {"chrome", "chromium", "google-chrome", "google-chrome-stable", "google-chrome-unstable", "google-chrome-beta"};
|
||||
for (String candidate : candidates) {
|
||||
path = findExecutable(candidate);
|
||||
if (path != null) return path;
|
||||
}
|
||||
|
||||
// 从MAC和Linux默认路径获取
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("mac") || os.contains("darwin")) {
|
||||
path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||
return new File(Paths.get(path).toString()).exists() ? path : null;
|
||||
} else if (os.contains("linux")) {
|
||||
String[] paths = {"/usr/bin/google-chrome", "/opt/google/chrome/google-chrome", "/usr/lib/chromium-browser/chromium-browser"};
|
||||
for (String p : paths) {
|
||||
if (new File(p).exists()) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else if (!os.contains("windows")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从注册表中获取
|
||||
String registryPath = getRegistryPath();
|
||||
if (registryPath != null) {
|
||||
return registryPath;
|
||||
}
|
||||
// 从系统变量中获取
|
||||
return getPathFromSystemVariable();
|
||||
}
|
||||
|
||||
private static String findExecutable(String executable) {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec("which " + executable);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
return reader.readLine();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取注册表中谷歌浏览器的地址
|
||||
*/
|
||||
public static String getRegistryPath() {
|
||||
try {
|
||||
// 使用 Advapi32Util.registryGetStringValue 获取注册表项的字符串值
|
||||
String chromePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe", "");
|
||||
|
||||
if (chromePath != null && !chromePath.isEmpty()) {
|
||||
return chromePath;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取系统变量谷歌浏览器的地址
|
||||
*/
|
||||
private static String getPathFromSystemVariable() {
|
||||
String paths = System.getenv("PATH").toLowerCase();
|
||||
Pattern pattern = Pattern.compile("[^;]*chrome[^;]*");
|
||||
Matcher matcher = pattern.matcher(paths.toLowerCase());
|
||||
if (matcher.find()) {
|
||||
String path = matcher.group(0);
|
||||
if (path.contains("chrome.exe")) {
|
||||
return Paths.get(path).toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
254
java/src/main/java/com/ll/DrissonPage/functions/Keys.java
Normal file
254
java/src/main/java/com/ll/DrissonPage/functions/Keys.java
Normal file
File diff suppressed because one or more lines are too long
577
java/src/main/java/com/ll/DrissonPage/functions/Locator.java
Normal file
577
java/src/main/java/com/ll/DrissonPage/functions/Locator.java
Normal file
@ -0,0 +1,577 @@
|
||||
package com.ll.DrissonPage.functions;
|
||||
|
||||
import com.ll.DrissonPage.base.By;
|
||||
import com.ll.DrissonPage.base.BySelect;
|
||||
import com.ll.cssselectortoxpath.CssToXpath;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 字符串选择器
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Locator {
|
||||
public static boolean isLoc(String text) {
|
||||
return startsWithAny(text, ".", "#", "@", "t:", "t=", "tag:", "tag=", "tx:", "tx=", "tx^", "tx$", "text:", "text=", "text^", "text$", "xpath:", "xpath=", "x:", "x=", "css:", "css=", "c:", "c=");
|
||||
}
|
||||
|
||||
public static By getLoc(Object str) {
|
||||
return getLoc(str, false);
|
||||
}
|
||||
|
||||
public static By getLoc(Object str, boolean cssMode) {
|
||||
return getLoc(str, false, cssMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收本库定位语法或By,转换为标准定位元组,
|
||||
*
|
||||
* @param str 本库定位语法或By定位元组
|
||||
* @param translateCss 是否翻译css selector为xpath
|
||||
* @param cssMode 是否尽量用css selector方式
|
||||
* @return DrissionPage中By定位元素
|
||||
*/
|
||||
public static By getLoc(Object str, boolean translateCss, boolean cssMode) {
|
||||
By byLoc;
|
||||
if (str == null || str.toString().trim().isEmpty()) str = "";
|
||||
if (str instanceof By) {
|
||||
byLoc = cssMode ? translateCssLoc((By) str) : translateLoc((By) str);
|
||||
} else if (str instanceof String) {
|
||||
byLoc = cssMode ? strToCssLoc((String) str) : strToXPathLoc((String) str);
|
||||
} else {
|
||||
throw new IllegalArgumentException("loc参数只能是By选择器或str。");
|
||||
}
|
||||
if (byLoc.getName().equals(BySelect.CSS_SELECTOR) && translateCss) {
|
||||
try {
|
||||
// 使用你的方法将 CSS Selector 转换为 XPath
|
||||
String s = CssToXpath.convertCssSelectorToXpath(byLoc.getValue());
|
||||
if (!s.equals(byLoc.getValue())) byLoc = By.xpath(s);
|
||||
} catch (Exception e) {
|
||||
// 处理异常
|
||||
}
|
||||
}
|
||||
return byLoc;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理元素查找语句
|
||||
*
|
||||
* @param loc 查找语法字符串
|
||||
* @return By查询对象
|
||||
*/
|
||||
public static By strToXPathLoc(String loc) {
|
||||
|
||||
By by = By.xpath("");
|
||||
String locStr;
|
||||
|
||||
if (loc.startsWith(".")) {
|
||||
loc = startsWithAny(loc, ".=", ".:", ".^", ".$") ? loc.replaceFirst(".", "@class") : loc.replaceFirst(".", "@class=");
|
||||
} else if (loc.startsWith("#")) {
|
||||
loc = startsWithAny(loc, "#=", "#:", "#^", "#$") ? loc.replaceFirst("#", "@id") : loc.replaceFirst("#", "@id=");
|
||||
} else if (loc.startsWith("t:") || loc.startsWith("t=")) {
|
||||
loc = "tag:" + loc.substring(2);
|
||||
} else if (loc.startsWith("tx:") || loc.startsWith("tx=") || loc.startsWith("tx^") || loc.startsWith("tx$")) {
|
||||
loc = "text" + loc.substring(2);
|
||||
}
|
||||
|
||||
// 多属性查找
|
||||
if ((loc.startsWith("@@") || loc.startsWith("@|") || loc.startsWith("@!")) && !("@@".equals(loc) || "@|".equals(loc) || "@!".equals(loc))) {
|
||||
locStr = makeMultiXPathStr("*", loc).getValue();
|
||||
}
|
||||
// 单属性查找
|
||||
else if (loc.startsWith("@") && !"@".equals(loc)) {
|
||||
locStr = makeSingleXPathStr("*", loc).getValue();
|
||||
}
|
||||
// 根据tag name查找
|
||||
else if ((loc.startsWith("tag:") || loc.startsWith("tag=")) && !("tag:".equals(loc) || "tag=".equals(loc))) {
|
||||
int atInd = loc.indexOf('@');
|
||||
if (atInd == -1) {
|
||||
locStr = "//*[name()='" + loc.substring(4) + "']";
|
||||
} else {
|
||||
String substring = loc.substring(atInd);
|
||||
locStr = substring.startsWith("@@") || substring.startsWith("@|") || substring.startsWith("@!") ? makeMultiXPathStr(loc.substring(4, atInd), substring).getValue() : makeSingleXPathStr(loc.substring(4, atInd), substring).getValue();
|
||||
}
|
||||
}
|
||||
// 根据文本查找
|
||||
else if (loc.startsWith("text=")) {
|
||||
locStr = "//*[text()=" + makeSearchStr(loc.substring(5)) + "]";
|
||||
} else if (loc.startsWith("text:") && !"text:".equals(loc)) {
|
||||
locStr = "//*/text()[contains(., " + makeSearchStr(loc.substring(5)) + ")]/..";
|
||||
} else if (loc.startsWith("text^") && !"text^".equals(loc)) {
|
||||
locStr = "//*/text()[starts-with(., " + makeSearchStr(loc.substring(5)) + ")]/..";
|
||||
} else if (loc.startsWith("text$") && !"text$".equals(loc)) {
|
||||
locStr = "//*/text()[substring(., string-length(.) - string-length(" + makeSearchStr(loc.substring(5)) + ") +1) = " + makeSearchStr(loc.substring(5)) + "]/..";
|
||||
}
|
||||
|
||||
// 用xpath查找
|
||||
else if ((loc.startsWith("xpath:") || loc.startsWith("xpath=")) && !("xpath:".equals(loc) || "xpath=".equals(loc))) {
|
||||
locStr = loc.substring(6);
|
||||
} else if ((loc.startsWith("x:") || loc.startsWith("x=")) && !("x:".equals(loc) || "x=".equals(loc))) {
|
||||
locStr = loc.substring(2);
|
||||
}
|
||||
|
||||
// 用css selector查找
|
||||
else if ((loc.startsWith("css:") || loc.startsWith("css=")) && !("css:".equals(loc) || "css=".equals(loc))) {
|
||||
by.setName(BySelect.CSS_SELECTOR);
|
||||
locStr = loc.substring(4);
|
||||
} else if ((loc.startsWith("c:") || loc.startsWith("c=")) && !("c:".equals(loc) || "c=".equals(loc))) {
|
||||
by.setName(BySelect.CSS_SELECTOR);
|
||||
locStr = loc.substring(2);
|
||||
}
|
||||
|
||||
// 根据文本模糊查找
|
||||
else if (!loc.isEmpty()) {
|
||||
locStr = "//*/text()[contains(., " + makeSearchStr(loc) + ")]/..";
|
||||
} else {
|
||||
locStr = "//*";
|
||||
}
|
||||
by.setValue(locStr);
|
||||
return by;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理元素查找语句
|
||||
*
|
||||
* @param loc 查找语法字符串
|
||||
* @return By查询对象
|
||||
*/
|
||||
public static By strToCssLoc(String loc) {
|
||||
|
||||
By by = By.css("");
|
||||
if (loc.startsWith(".")) {
|
||||
if (startsWithAny(loc, ".=", ".:", ".^", ".$")) {
|
||||
loc = loc.replaceFirst(".", "@class");
|
||||
} else {
|
||||
loc = loc.replaceFirst(".", "@class=");
|
||||
}
|
||||
} else if (loc.startsWith("#")) {
|
||||
if (startsWithAny(loc, "#=", "#:", "#^", "#$")) {
|
||||
loc = loc.replaceFirst("#", "@id");
|
||||
} else {
|
||||
loc = loc.replaceFirst("#", "@id=");
|
||||
}
|
||||
} else if (loc.startsWith("t:") || loc.startsWith("t=")) {
|
||||
loc = "tag:" + loc.substring(2);
|
||||
} else if (loc.startsWith("tx:") || loc.startsWith("tx=") || loc.startsWith("tx^") || loc.startsWith("tx$")) {
|
||||
loc = "text" + loc.substring(2);
|
||||
}
|
||||
|
||||
// 多属性查找
|
||||
if ((loc.startsWith("@@") || loc.startsWith("@|") || loc.startsWith("@!")) && !("@@".equals(loc) || "@|".equals(loc) || "@!".equals(loc))) {
|
||||
by.setValue(makeMultiCssStr("*", loc).getValue());
|
||||
}
|
||||
// 单属性查找
|
||||
else if (loc.startsWith("@") && !"@".equals(loc)) {
|
||||
By by1 = makeSingleCssStr("*", loc);
|
||||
by.setName(by1.getName());
|
||||
by.setValue(by1.getValue());
|
||||
}
|
||||
// 根据tag name查找
|
||||
else if ((loc.startsWith("tag:") || loc.startsWith("tag=")) && !("tag:".equals(loc) || "tag=".equals(loc))) {
|
||||
int atInd = loc.indexOf('@');
|
||||
if (atInd == -1) {
|
||||
by.setValue(loc.substring(4));
|
||||
} else if (loc.substring(atInd).startsWith("@@") || loc.substring(atInd).startsWith("@|") || loc.substring(atInd).startsWith("@!")) {
|
||||
|
||||
By by1 = makeMultiCssStr(loc.substring(4, atInd), loc.substring(atInd));
|
||||
by.setName(by1.getName());
|
||||
by.setValue(by1.getValue());
|
||||
} else {
|
||||
|
||||
By by1 = makeSingleCssStr(loc.substring(4, atInd), loc.substring(atInd));
|
||||
by.setName(by1.getName());
|
||||
by.setValue(by1.getValue());
|
||||
}
|
||||
}
|
||||
// 根据文本查找
|
||||
else if (startsWithAny(loc, "text=", "text:", "text^", "text$", "xpath=", "xpath:", "x=", "x:")) {
|
||||
by = strToXPathLoc(loc);
|
||||
}
|
||||
// 用css selector查找
|
||||
else if ((loc.startsWith("css:") || loc.startsWith("css=")) && !("css:".equals(loc) || "css=".equals(loc))) {
|
||||
by.setValue(loc.substring(4));
|
||||
} else if ((loc.startsWith("c:") || loc.startsWith("c=")) && !("c:".equals(loc) || "c=".equals(loc))) {
|
||||
by.setValue(loc.substring(2));
|
||||
}
|
||||
// 根据文本模糊查找
|
||||
else if (!loc.isEmpty()) {
|
||||
by = strToXPathLoc(loc);
|
||||
} else {
|
||||
by.setValue("*");
|
||||
}
|
||||
|
||||
return by;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单属性xpath语句
|
||||
*
|
||||
* @param tag 标签名
|
||||
* @param text 待处理的字符串
|
||||
* @return By 对象
|
||||
*/
|
||||
private static By makeSingleXPathStr(String tag, String text) {
|
||||
String argStr = "";
|
||||
String txtStr = "";
|
||||
List<String> argList = new ArrayList<>();
|
||||
|
||||
if (!tag.equals("*")) {
|
||||
argList.add("name()=\"" + tag + "\"");
|
||||
}
|
||||
|
||||
if (text.equals("@")) {
|
||||
argStr = "not(@*)";
|
||||
} else {
|
||||
Pattern pattern = Pattern.compile("(.*?)([:=$^])(.*)");
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
String[] r = new String[0];
|
||||
if (matcher.find()) {
|
||||
int i = matcher.groupCount();
|
||||
r = new String[i];
|
||||
for (int i1 = 0; i1 < i; i1++) r[i1] = matcher.group(i1 + 1);
|
||||
}
|
||||
int lenR = r.length;
|
||||
int lenR0 = r[0].length();
|
||||
|
||||
if (lenR == 3 && lenR0 > 1) {
|
||||
String symbol = r[1];
|
||||
switch (symbol) {
|
||||
case "=": // 精确查找
|
||||
String arg = r[0].equals("@text()") || r[0].equals("@tx()") ? "." : r[0];
|
||||
argStr = arg + "=" + makeSearchStr(r[2]);
|
||||
break;
|
||||
case "^": // 匹配开头
|
||||
if (r[0].equals("@text()") || r[0].equals("@tx()")) {
|
||||
txtStr = "/text()[starts-with(., " + makeSearchStr(r[2]) + ")]/..";
|
||||
argStr = "";
|
||||
} else {
|
||||
argStr = "starts-with(" + r[0] + "," + makeSearchStr(r[2]) + ")";
|
||||
}
|
||||
break;
|
||||
case "$": // 匹配结尾
|
||||
if (r[0].equals("@text()") || r[0].equals("@tx()")) {
|
||||
txtStr = "/text()[substring(., string-length(.) - string-length(" + makeSearchStr(r[2]) + ") +1) = " + makeSearchStr(r[2]) + "]/..";
|
||||
argStr = "";
|
||||
} else {
|
||||
argStr = "substring(" + r[0] + ", string-length(" + r[0] + ") - string-length(" + makeSearchStr(r[2]) + ") +1) = " + makeSearchStr(r[2]);
|
||||
}
|
||||
break;
|
||||
case ":": // 模糊查找
|
||||
if (r[0].equals("@text()") || r[0].equals("@tx()")) {
|
||||
txtStr = "/text()[contains(., " + makeSearchStr(r[2]) + ")]/..";
|
||||
argStr = "";
|
||||
} else {
|
||||
argStr = "contains(" + r[0] + "," + makeSearchStr(r[2]) + ")";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("符号不正确:" + symbol);
|
||||
}
|
||||
} else if (lenR != 3 && lenR0 > 1) {
|
||||
argStr = r[0].equals("@text()") || r[0].equals("@tx()") ? "normalize-space(text())" : r[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!argStr.isEmpty()) {
|
||||
argList.add(argStr);
|
||||
}
|
||||
|
||||
argStr = String.join(" and ", argList);
|
||||
String xpath = "//*[" + argStr + "]" + txtStr;
|
||||
return By.xpath(xpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成多属性查找的xpath语句
|
||||
*
|
||||
* @param tag 标签名
|
||||
* @param text 待处理的字符串
|
||||
* @return By 对象
|
||||
*/
|
||||
private static By makeMultiXPathStr(String tag, String text) {
|
||||
List<String> argList = new ArrayList<>();
|
||||
String[] args = text.split("@!|@@|@\\|", -1);
|
||||
args = Arrays.copyOfRange(args, 1, args.length);
|
||||
boolean and;
|
||||
if (ArrayUtils.contains(args, "@@") && ArrayUtils.contains(args, "@|")) {
|
||||
throw new IllegalArgumentException("@@和@|不能同时出现在一个定位语句中。");
|
||||
} else and = ArrayUtils.contains(args, "@@");
|
||||
|
||||
for (int k = 0; k < args.length - 1; k += 2) {
|
||||
String[] r = args[k + 1].split("([:=$^])", 2);
|
||||
String argStr;
|
||||
int lenR = r.length;
|
||||
|
||||
if (r[0].isEmpty()) { // 不查询任何属性
|
||||
argStr = "not(@*)";
|
||||
} else {
|
||||
boolean ignore = args[k].equals("@!"); // 是否去除某个属性
|
||||
if (lenR != 3) { // 只有属性名没有属性内容,查询是否存在该属性
|
||||
argStr = r[0].equals("text()") || r[0].equals("tx()") ? "normalize-space(text())" : "@" + r[0];
|
||||
} else { // 属性名和内容都有
|
||||
String arg = r[0].equals("text()") || r[0].equals("tx()") ? "." : "@" + r[0];
|
||||
String symbol = r[1];
|
||||
switch (symbol) {
|
||||
case "=":
|
||||
argStr = arg + "=" + makeSearchStr(r[2]);
|
||||
break;
|
||||
case ":":
|
||||
argStr = "contains(" + arg + "," + makeSearchStr(r[2]) + ")";
|
||||
break;
|
||||
case "^":
|
||||
argStr = "starts-with(" + arg + "," + makeSearchStr(r[2]) + ")";
|
||||
break;
|
||||
case "$":
|
||||
argStr = "substring(" + arg + ", string-length(" + arg + ") - string-length(" + makeSearchStr(r[2]) + ") +1) = " + makeSearchStr(r[2]);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("符号不正确:" + symbol);
|
||||
}
|
||||
}
|
||||
if (ignore) argStr = "not(" + argStr + ")";
|
||||
}
|
||||
|
||||
argList.add(argStr);
|
||||
}
|
||||
|
||||
String argStr = and ? String.join(" and ", argList) : String.join(" or ", argList);
|
||||
if (!tag.equals("*")) {
|
||||
String condition = !argStr.isEmpty() ? " and (" + argStr + ")" : "";
|
||||
argStr = "name()=\"" + tag + "\"" + condition;
|
||||
}
|
||||
|
||||
String xpath = "//*[" + argStr + "]";
|
||||
return By.xpath(xpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将"转义,不知何故不能直接用 \ 来转义
|
||||
*
|
||||
* @param searchStr 查询字符串
|
||||
* @return 把"转义后的字符串
|
||||
*/
|
||||
private static String makeSearchStr(String searchStr) {
|
||||
|
||||
String[] parts = searchStr.split("\"");
|
||||
int partsNum = parts.length;
|
||||
StringBuilder result = new StringBuilder("concat(");
|
||||
|
||||
for (int key = 0; key < partsNum; key++) {
|
||||
result.append("\"").append(parts[key]).append("\"");
|
||||
if (key < partsNum - 1) result.append(",'\"',");
|
||||
}
|
||||
|
||||
result.append(",\"\")");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成多属性查找的css selector语句
|
||||
*
|
||||
* @param tag 标签名
|
||||
* @param text 待处理的字符串
|
||||
* @return By 对象
|
||||
*/
|
||||
private static By makeMultiCssStr(String tag, String text) {
|
||||
|
||||
List<String> argList = new ArrayList<>();
|
||||
String[] args = text.split("@!|@@|@\\|", -1);
|
||||
args = Arrays.copyOfRange(args, 1, args.length);
|
||||
boolean and;
|
||||
// @|
|
||||
if (ArrayUtils.contains(args, "@@") && ArrayUtils.contains(args, "@|")) {
|
||||
throw new IllegalArgumentException("@@和@|不能同时出现在一个定位语句中。");
|
||||
} else and = ArrayUtils.contains(args, "@@");
|
||||
|
||||
for (int k = 0; k < args.length - 1; k += 2) {
|
||||
String[] r = args[k + 1].split("([:=$^])", 2);
|
||||
if (r[0].isEmpty() || r[0].startsWith("text()") || r[0].startsWith("tx()")) {
|
||||
return makeMultiXPathStr(tag, text);
|
||||
}
|
||||
|
||||
String argStr = "";
|
||||
int lenR = r.length;
|
||||
boolean ignore = args[k].equals("@!"); // 是否去除某个属性
|
||||
if (lenR != 3) { // 只有属性名没有属性内容,查询是否存在该属性
|
||||
argStr = "[" + r[0] + "]";
|
||||
} else { // 属性名和内容都有
|
||||
Map<String, String> d = Map.of("=", "", "^", "^", "$", "$", ":", "*");
|
||||
argStr = "[" + r[0] + d.get(r[1]) + "=" + cssTrans(r[2]) + "]";
|
||||
}
|
||||
|
||||
if (ignore) {
|
||||
argStr = ":not(" + argStr + ")";
|
||||
}
|
||||
|
||||
argList.add(argStr);
|
||||
}
|
||||
|
||||
String argStr = and ? String.join("", argList) : String.join(",", argList);
|
||||
return By.css(tag + argStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单属性css selector语句
|
||||
*
|
||||
* @param tag 标签名
|
||||
* @param text 待处理的字符串
|
||||
* @return By 对象
|
||||
*/
|
||||
private static By makeSingleCssStr(String tag, String text) {
|
||||
|
||||
if (text.equals("@") || text.startsWith("@text()") || text.startsWith("@tx()")) {
|
||||
return makeSingleXPathStr(tag, text);
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile("(.*?)([:=$^])(.*)");
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
String[] r = new String[0];
|
||||
if (matcher.find()) {
|
||||
int i = matcher.groupCount();
|
||||
r = new String[i];
|
||||
for (int i1 = 0; i1 < i; i1++) r[i1] = matcher.group(i1 + 1);
|
||||
}
|
||||
String argStr;
|
||||
if (r.length == 3) {
|
||||
Map<String, String> d = Map.of("=", "", "^", "^", "$", "$", ":", "*");
|
||||
argStr = "[" + r[0].substring(1) + d.get(r[1]) + "=" + cssTrans(r[2]) + "]";
|
||||
} else {
|
||||
argStr = "[" + cssTrans(r[0].substring(1)) + "]";
|
||||
}
|
||||
|
||||
String cssSelector = tag + argStr;
|
||||
return By.css(cssSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把By类型转换为css selector或xpath类型的 先转xpath如果xpath无法转换则是css
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @return css selector或xpath查询元素
|
||||
*/
|
||||
public static By translateLoc(By by) {
|
||||
if (by == null) throw new NullPointerException("by is not null");
|
||||
switch (by.getName()) {
|
||||
case XPATH:
|
||||
case CSS_SELECTOR:
|
||||
break;
|
||||
case ID:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[@id=\"" + by.getValue() + "\"]");
|
||||
break;
|
||||
case CLASS_NAME:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[@class=\"" + by.getValue() + "\"]");
|
||||
break;
|
||||
case LINK_TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//a[text()=\"" + by.getValue() + "\"]");
|
||||
break;
|
||||
case NAME:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[@name=\"" + by.getValue() + "\"]");
|
||||
break;
|
||||
case TAG_NAME:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[name()=\"" + by.getValue() + "\"]");
|
||||
break;
|
||||
case PARTIAL_LINK_TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//a[contains(text(),\"" + by.getValue() + "\")]");
|
||||
case TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[text()=\"" + by.getValue() + "\")]");
|
||||
case PARTIAL_TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[contains(text(),\"" + by.getValue() + "\")]");
|
||||
default:
|
||||
throw new IllegalArgumentException(by.getName().name() + " Type does not exist,value is " + by.getValue());
|
||||
}
|
||||
return by;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把By类型转换为css selector或xpath类型的 先转css如果css无法转换则是xpath
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @return css selector或xpath查询元素
|
||||
*/
|
||||
public static By translateCssLoc(By by) {
|
||||
if (by == null) throw new NullPointerException("by is not null");
|
||||
switch (by.getName()) {
|
||||
case XPATH:
|
||||
case CSS_SELECTOR:
|
||||
break;
|
||||
case ID:
|
||||
by.setName(BySelect.CSS_SELECTOR);
|
||||
by.setValue("#" + cssTrans(by.getValue()));
|
||||
break;
|
||||
case CLASS_NAME:
|
||||
by.setName(BySelect.CSS_SELECTOR);
|
||||
by.setValue("." + cssTrans(by.getValue()));
|
||||
break;
|
||||
case LINK_TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//a[text()=\"" + by.getValue() + "\"]");
|
||||
break;
|
||||
case NAME:
|
||||
by.setName(BySelect.CSS_SELECTOR);
|
||||
by.setValue("*[@name=" + cssTrans(by.getValue()) + "]");
|
||||
break;
|
||||
case TAG_NAME:
|
||||
by.setName(BySelect.CSS_SELECTOR);
|
||||
break;
|
||||
case PARTIAL_LINK_TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//a[contains(text(),\"" + by.getValue() + "\")]");
|
||||
case TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[text()=\"" + by.getValue() + "\")]");
|
||||
case PARTIAL_TEXT:
|
||||
by.setName(BySelect.XPATH);
|
||||
by.setValue("//*[contains(text(),\"" + by.getValue() + "\")]");
|
||||
default:
|
||||
throw new IllegalArgumentException(by.getName().name() + " Type does not exist,value is " + by.getValue());
|
||||
}
|
||||
return by;
|
||||
}
|
||||
|
||||
private static boolean startsWithAny(String str, String... prefixes) {
|
||||
for (String prefix : prefixes) {
|
||||
if (str.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本转换为CSS选择器中的转义形式
|
||||
*
|
||||
* @param txt 输入文本
|
||||
* @return 转义后的文本
|
||||
*/
|
||||
public static String cssTrans(String txt) {
|
||||
char[] c = {'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '`', ',', '{', '|', '}', '~', ' '};
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char i : txt.toCharArray()) {
|
||||
if (indexOf(c, i) != -1) result.append('\\');
|
||||
result.append(i);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static int indexOf(char[] array, char target) {
|
||||
for (int i = 0; i < array.length; i++) if (array[i] == target) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.ll.DrissonPage.functions;
|
||||
|
||||
/**
|
||||
* 设置
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class Settings {
|
||||
public static boolean raiseWhenEleNotFound = false;
|
||||
public static boolean raiseWhenClickFailed = false;
|
||||
public static boolean raiseWhenWaitFailed = false;
|
||||
public static boolean singletonTabObj = true;
|
||||
|
||||
public static Double cdpTimeout = 10.0;
|
||||
}
|
293
java/src/main/java/com/ll/DrissonPage/functions/Tools.java
Normal file
293
java/src/main/java/com/ll/DrissonPage/functions/Tools.java
Normal file
@ -0,0 +1,293 @@
|
||||
package com.ll.DrissonPage.functions;
|
||||
|
||||
import com.ll.DrissonPage.config.OptionsManager;
|
||||
import com.ll.DrissonPage.error.extend.*;
|
||||
import com.ll.DrissonPage.page.ChromiumPage;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.platform.win32.WinDef;
|
||||
import org.apache.maven.project.MavenProject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 工具
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
|
||||
//wait_until
|
||||
public class Tools {
|
||||
/**
|
||||
* 检查端口是否被占用
|
||||
*
|
||||
* @param ip 浏览器地址
|
||||
* @param port 浏览器端口
|
||||
* @return 是否占用
|
||||
*/
|
||||
public static boolean portIsUsing(String ip, int port) {
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.connect(new InetSocketAddress(ip, port), 100);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanFolder(String folderPath) {
|
||||
cleanFolder(folderPath, new String[]{});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空一个文件夹,除了 ignore 里的文件和文件夹
|
||||
*
|
||||
* @param folderPath 要清空的文件夹路径
|
||||
* @param ignore 忽略列表
|
||||
*/
|
||||
public static void cleanFolder(String folderPath, String[] ignore) {
|
||||
Path path = Paths.get(folderPath);
|
||||
try {
|
||||
Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (!contains(ignore, file.getFileName().toString())) {
|
||||
Files.delete(file);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
if (!contains(ignore, dir.getFileName().toString())) {
|
||||
Files.delete(dir);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行显示或隐藏浏览器窗口
|
||||
*
|
||||
* @param page ChromePage对象
|
||||
*/
|
||||
public static void showOrHideBrowser(ChromiumPage page) {
|
||||
showOrHideBrowser(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行显示或隐藏浏览器窗口
|
||||
*
|
||||
* @param page ChromePage对象
|
||||
* @param hide 是否隐藏
|
||||
*/
|
||||
public static void showOrHideBrowser(ChromiumPage page, boolean hide) {
|
||||
if (!page.getAddress().startsWith("127.0.0.1") && !page.getAddress().startsWith("localhost")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
|
||||
throw new UnsupportedOperationException("该方法只能在Windows系统使用。");
|
||||
}
|
||||
|
||||
try {
|
||||
Integer processId = page.processId();
|
||||
if (processId != null) {
|
||||
User32 user32 = User32.INSTANCE;
|
||||
WinDef.HWND hWnd = user32.FindWindow(null, page.title()); // 替换为实际的窗口标题
|
||||
|
||||
if (hWnd != null) {
|
||||
int nCmdShow = hide ? 0 : 1; // 0 表示隐藏,1 表示显示
|
||||
user32.ShowWindow(hWnd, nCmdShow);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static Long getBrowserProcessId(String address) {
|
||||
return getBrowserProcessId(null, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器进程id
|
||||
*
|
||||
* @param process 已知的进程对象,没有时传入null
|
||||
* @param address 浏览器管理地址,含端口
|
||||
* @return 进程id
|
||||
*/
|
||||
public static Long getBrowserProcessId(Process process, String address) {
|
||||
if (process != null) {
|
||||
return process.pid();
|
||||
}
|
||||
|
||||
String port = address.split(":")[1];
|
||||
String command = "netstat -nao | findstr :" + port;
|
||||
|
||||
try {
|
||||
Process netstatProcess = Runtime.getRuntime().exec(command);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(netstatProcess.getInputStream()));
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.contains("LISTENING")) {
|
||||
String[] parts = line.split(" ");
|
||||
return Long.parseLong(parts[parts.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制关闭某个端口内的进程
|
||||
*
|
||||
* @param port 端口号
|
||||
*/
|
||||
public static void stopProcessOnPort(int port) {
|
||||
|
||||
try {
|
||||
ProcessHandle.allProcesses().filter(process -> process.info().command().isPresent()).filter(process -> process.info().command().map(s -> s.contains(Integer.toString(port))).orElse(false)).forEach(process -> {
|
||||
if (process.isAlive()) process.destroy();
|
||||
});
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean contains(String[] array, String target) {
|
||||
for (String s : array) {
|
||||
if (s.equals(target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void raiseError(Map<String, Object> result, Object ignore) {
|
||||
Object o = result.get("error");
|
||||
if (o == null) return;
|
||||
String error = o.toString();
|
||||
if ("Cannot find context with specified id".equals(error) || "Inspected target navigated or closed".equals(error)) {
|
||||
throw new ContextLostError();
|
||||
} else if ("Could not find node with given id".equals(error) || "Could not find object with given id".equals(error) || "No node with given id found".equals(error) || "Node with given id does not belong to the document".equals(error) || "No node found for given backend id".equals(error)) {
|
||||
throw new ElementLostError();
|
||||
} else if ("connection disconnected".equals(error) || "No target with given id found".equals(error)) {
|
||||
throw new PageDisconnectedError();
|
||||
} else if ("alert exists.".equals(error)) {
|
||||
throw new AlertExistsError();
|
||||
} else if ("Node does not have a layout object".equals(error) || "Could not compute box model.".equals(error)) {
|
||||
throw new NoRectError();
|
||||
} else if ("Cannot navigate to invalid URL".equals(error)) {
|
||||
throw new WrongURLError("无效的url:" + result.get("args.url") + "。也许要加上\"http://\"?");
|
||||
} else if ("Frame corresponds to an opaque origin and its storage key cannot be serialized".equals(error)) {
|
||||
throw new StorageError();
|
||||
} else if ("Sanitizing cookie failed".equals(error)) {
|
||||
throw new CookieFormatError("cookie格式不正确:" + result.get("args"));
|
||||
} else if ("Given expression does not evaluate to a function".equals(error)) {
|
||||
throw new JavaScriptError("传入的js无法解析成函数:\n" + result.get("args.functionDeclaration"));
|
||||
} else if ("call_method_error".equals(result.get("type")) || "timeout".equals(result.get("type"))) {
|
||||
long processTime = System.currentTimeMillis(); // 这里可能需要替换成其他计时方式
|
||||
String txt = "\n错误:" + error + "\nmethod:" + result.get("method") + "\nargs:" + result.get("args") + "\n版本:" + new MavenProject().getVersion() + "\n运行时间:" + processTime + "\n出现这个错误可能意味着程序有bug,请把错误信息和重现方法" + "告知作者,谢谢。\n报告网站:https://gitee.com/g1879/DrissionPage/issues";
|
||||
if ("timeout".equals(result.get("type"))) {
|
||||
throw new WaitTimeoutError(txt);
|
||||
} else {
|
||||
throw new CDPError(txt);
|
||||
}
|
||||
}
|
||||
|
||||
if (ignore == null) {
|
||||
// 此处可能需要替换成适当的异常类
|
||||
throw new RuntimeException(result.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认的ini文件复制到当前目录
|
||||
*
|
||||
* @param saveName 指定文件名,为null则命名为'dp_configs.ini'
|
||||
* @throws IOException 如果复制过程中发生IO异常
|
||||
*/
|
||||
public static void copyConfigsToCurrentDirectory(String saveName) throws IOException {
|
||||
OptionsManager optionsManager = new OptionsManager("default");
|
||||
saveName = (saveName != null) ? saveName + ".ini" : "dp_configs.ini";
|
||||
optionsManager.save(saveName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹以及其子目录
|
||||
*
|
||||
* @param path 删除文件地址
|
||||
* @param fileName 删除文件名也可以是文件夹
|
||||
* @throws IOException 删除异常
|
||||
*/
|
||||
public static void deleteDirectory(String path, String fileName) throws IOException {
|
||||
deleteDirectory(Paths.get(path + "/" + fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹以及其子目录
|
||||
*
|
||||
* @param path 删除文件地址
|
||||
* @throws IOException 删除异常
|
||||
*/
|
||||
public static void deleteDirectory(String path) throws IOException {
|
||||
deleteDirectory(Paths.get(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹以及其子目录
|
||||
*
|
||||
* @param path 删除文件地址
|
||||
* @throws IOException 删除异常
|
||||
*/
|
||||
public static void deleteDirectory(File path) throws IOException {
|
||||
deleteDirectory(Paths.get(path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹以及其子目录
|
||||
*
|
||||
* @param directoryPath 要删除的文件夹路径
|
||||
* @throws IOException 删除异常
|
||||
*/
|
||||
public static void deleteDirectory(Path directoryPath) throws IOException {
|
||||
if (Files.exists(directoryPath)) try (Stream<Path> walk = Files.walk(directoryPath)) {
|
||||
walk.sorted(java.util.Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
|
||||
}
|
||||
}
|
||||
|
||||
public interface User32 extends Library {
|
||||
User32 INSTANCE = Native.load("user32", User32.class);
|
||||
|
||||
boolean ShowWindow(WinDef.HWND hWnd, int nCmdShow);
|
||||
|
||||
WinDef.HWND FindWindow(String lpClassName, String lpWindowName);
|
||||
}
|
||||
}
|
411
java/src/main/java/com/ll/DrissonPage/functions/Web.java
Normal file
411
java/src/main/java/com/ll/DrissonPage/functions/Web.java
Normal file
@ -0,0 +1,411 @@
|
||||
package com.ll.DrissonPage.functions;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import com.ll.DrissonPage.base.DrissionElement;
|
||||
import com.ll.DrissonPage.element.ChromiumElement;
|
||||
import com.ll.DrissonPage.page.ChromiumBase;
|
||||
import com.ll.DrissonPage.units.Coordinate;
|
||||
import okhttp3.Cookie;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Web {
|
||||
/**
|
||||
* 前面无须换行的元素
|
||||
*/
|
||||
private static final List<String> NOWRAP_LIST = Arrays.asList("br", "sub", "sup", "em", "strong", "a", "font", "b", "span", "s", "i", "del", "ins", "img", "td", "th", "abbr", "bdi", "bdo", "cite", "code", "data", "dfn", "kbd", "mark", "q", "rp", "rt", "ruby", "samp", "small", "time", "u", "var", "wbr", "button", "slot", "content");
|
||||
/**
|
||||
* 后面添加换行的元素
|
||||
*/
|
||||
private static final List<String> WRAP_AFTER_LIST = Arrays.asList("p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "ol", "li", "blockquote", "header", "footer", "address", "article", "aside", "main", "nav", "section", "figcaption", "summary");
|
||||
/**
|
||||
* 不获取文本的元素
|
||||
*/
|
||||
private static final List<String> NO_TEXT_LIST = Arrays.asList("script", "style", "video", "audio", "iframe", "embed", "noscript", "canvas", "template");
|
||||
/**
|
||||
* 用/t分隔的元素
|
||||
*/
|
||||
private static final List<String> TAB_LIST = Arrays.asList("td", "th");
|
||||
|
||||
/**
|
||||
* 获取元素内所有文本
|
||||
*
|
||||
* @param e 元素对象
|
||||
* @return 元素内所有文本
|
||||
*/
|
||||
public static String getEleTxt(DrissionElement<?, ?> e) {
|
||||
String tag = e.tag();
|
||||
if (NO_TEXT_LIST.contains(tag)) return e.rawText();
|
||||
|
||||
List<String> reStr = getNodeTxt(e, false);
|
||||
if (!reStr.isEmpty() && reStr.get(reStr.size() - 1).equals("\n")) reStr.remove(reStr.size() - 1);
|
||||
return formatHtml(reStr.stream().map(i -> i != null && !i.equals("true") ? i : "\n").collect(Collectors.joining("")));
|
||||
}
|
||||
|
||||
private static List<String> getNodeTxt(DrissionElement<?, ?> ele, boolean pre) {
|
||||
String tag = ele.tag();
|
||||
if ("br".equals(tag)) return new ArrayList<>(List.of("true"));
|
||||
if (!pre && "pre".equals(tag)) pre = true;
|
||||
|
||||
List<String> strList = new ArrayList<>();
|
||||
//标签内的文本不返回
|
||||
if (NO_TEXT_LIST.contains(tag) && !pre) return strList;
|
||||
|
||||
List<?> eles = ele.eles("xpath:./text() | *");
|
||||
String prevEle = "";
|
||||
for (Object e : eles) {
|
||||
//元素节点
|
||||
DrissionElement<?, ?> el = (DrissionElement<?, ?>) e;
|
||||
//元素间换行的情况
|
||||
if (!NOWRAP_LIST.contains(el.tag()) && !strList.isEmpty() && !"\n".equals(strList.get(strList.size() - 1)))
|
||||
strList.add("\n");
|
||||
//表格的行
|
||||
if (TAB_LIST.contains(el.tag()) && TAB_LIST.contains(prevEle)) strList.add("\t");
|
||||
strList.addAll(getNodeTxt(el, pre));
|
||||
prevEle = el.tag();
|
||||
}
|
||||
if (WRAP_AFTER_LIST.contains(tag) && !strList.isEmpty() && !"\n".equals(strList.get(strList.size() - 1)))
|
||||
strList.add("\n");
|
||||
return strList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTML编码字符
|
||||
*
|
||||
* @param text HTML文本
|
||||
* @return 格式化后的HTML文本
|
||||
*/
|
||||
public static String formatHtml(String text) {
|
||||
return text != null ? StringEscapeUtils.unescapeHtml4(text).replace("\u00a0", " ") : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的坐标是否在视口中
|
||||
*
|
||||
* @param page ChromePage对象
|
||||
* @param coordinate 页面绝对坐标
|
||||
*/
|
||||
public static Boolean locationInViewport(ChromiumBase page, Coordinate coordinate) {
|
||||
return locationInViewport(page, coordinate.getX(), coordinate.getY());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的坐标是否在视口中
|
||||
*
|
||||
* @param page ChromePage对象
|
||||
* @param locX 页面绝对坐标x
|
||||
* @param locY 页面绝对坐标y
|
||||
*/
|
||||
public static Boolean locationInViewport(ChromiumBase page, Integer locX, Integer locY) {
|
||||
String js = "function(){var x = " + locX + "; var y = " + locY + ";\n" + " const scrollLeft = document.documentElement.scrollLeft;\n" + " const scrollTop = document.documentElement.scrollTop;\n" + " const vWidth = document.documentElement.clientWidth;\n" + " const vHeight = document.documentElement.clientHeight;\n" + " if (x< scrollLeft || y < scrollTop || x > vWidth + scrollLeft || y > vHeight + scrollTop){return false;}\n" + " return true;}";
|
||||
return Boolean.parseBoolean(page.runJs(js).toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收元素及偏移坐标,把坐标滚动到页面中间,返回该点在视口中的坐标
|
||||
* 有偏移量时以元素左上角坐标为基准,没有时以click_point为基准
|
||||
*
|
||||
* @param ele 元素对象
|
||||
* @param offset_x 偏移量x
|
||||
* @param offset_y 偏移量y
|
||||
* @return 视口中的坐标
|
||||
*/
|
||||
|
||||
public static Coordinate offsetScroll(ChromiumElement ele, Integer offset_x, Integer offset_y) {
|
||||
Coordinate location = ele.rect().location();
|
||||
int loc_x = location.getX();
|
||||
int loc_y = location.getY();
|
||||
Coordinate clickPoint = ele.rect().clickPoint();
|
||||
int cp_x = clickPoint.getX(); // Assuming click_point is the same as location for x
|
||||
int cp_y = clickPoint.getY(); // Assuming click_point is the same as location for y
|
||||
|
||||
int lx = (offset_x != null && offset_x != 0) ? loc_x + offset_x : cp_x;
|
||||
int ly = (offset_y != null && offset_y != 0) ? loc_y + offset_y : cp_y;
|
||||
|
||||
if (!locationInViewport(ele.getOwner(), lx, ly)) {
|
||||
int clientWidth = Integer.parseInt(ele.getOwner().runJs("return document.body.clientWidth;").toString());
|
||||
int clientHeight = Integer.parseInt(ele.getOwner().runJs("return document.body.clientHeight;").toString());
|
||||
ele.scroll().toLocation(lx - clientWidth / 2, ly - clientHeight / 2);
|
||||
|
||||
}
|
||||
location = ele.rect().viewportLocation();
|
||||
int cl_x = location.getX();
|
||||
int cl_y = location.getY();
|
||||
clickPoint = ele.rect().viewportClickPoint();
|
||||
int ccp_x = clickPoint.getX(); // Assuming viewport_click_point is the same as viewport_location for x
|
||||
int ccp_y = clickPoint.getY(); // Assuming viewport_click_point is the same as viewport_location for y
|
||||
int cx = (offset_x != null && offset_x != 0) ? cl_x + offset_x : ccp_x;
|
||||
int cy = (offset_y != null && offset_y != 0) ? cl_y + offset_y : ccp_y;
|
||||
|
||||
return new Coordinate(cx, cy);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取绝对url
|
||||
*
|
||||
* @param link 超链接
|
||||
* @return 绝对链接
|
||||
*/
|
||||
public static String makeAbsoluteLink(String link) {
|
||||
return makeAbsoluteLink(link, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绝对url
|
||||
*
|
||||
* @param link 超链接
|
||||
* @param baseURI 页面或iframe的url
|
||||
* @return 绝对链接
|
||||
*/
|
||||
public static String makeAbsoluteLink(String link, String baseURI) {
|
||||
if (Objects.isNull(link) || link.trim().isEmpty()) {
|
||||
return link;
|
||||
}
|
||||
|
||||
link = link.trim();
|
||||
URI parsed = URI.create(link);
|
||||
|
||||
// 是相对路径,与页面url拼接并返回
|
||||
if (parsed.getScheme() == null && parsed.getHost() == null) {
|
||||
if (baseURI == null || baseURI.isEmpty()) return link;
|
||||
try {
|
||||
return new URL(new URL(baseURI), link).toString();
|
||||
} catch (Exception e) {
|
||||
// 处理异常,例如URL格式不正确
|
||||
e.printStackTrace();
|
||||
return link;
|
||||
}
|
||||
}
|
||||
// 是绝对路径但缺少协议,从页面url获取协议并修复
|
||||
if (!parsed.isAbsolute() && baseURI != null && !baseURI.isEmpty()) {
|
||||
try {
|
||||
return new URI(baseURI).resolve(parsed).toString();
|
||||
} catch (URISyntaxException e) {
|
||||
// 处理异常,例如URL格式不正确
|
||||
e.printStackTrace();
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
// 绝对路径且不缺协议,直接返回
|
||||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文本是否js函数
|
||||
*/
|
||||
public static boolean isJsFunc(String func) {
|
||||
func = func.trim();
|
||||
return (func.startsWith("function") || func.startsWith("async ")) && func.endsWith("}") || func.contains("=>");
|
||||
}
|
||||
|
||||
|
||||
public static Map<String, Object> cookieToMap(Object cookie) {
|
||||
if (cookie instanceof Cookie) {
|
||||
// 如果是 Cookie 对象
|
||||
return cookieObjectToMap((Cookie) cookie);
|
||||
} else if (cookie instanceof String) {
|
||||
// 如果是字符串
|
||||
return cookieStringToMap((String) cookie);
|
||||
} else if (cookie instanceof Map) {
|
||||
// 如果已经是 Map
|
||||
return JSON.parseObject(JSON.toJSONString(cookie));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid cookie type: " + cookie.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> cookieObjectToMap(Cookie cookie) {
|
||||
Map<String, Object> cookieMap = new HashMap<>();
|
||||
// 根据 Cookie 对象的属性进行填充 cookieMap
|
||||
cookieMap.put("name", cookie.name());
|
||||
cookieMap.put("value", cookie.value());
|
||||
// 其他属性类似,根据需要添加
|
||||
return cookieMap;
|
||||
}
|
||||
|
||||
private static Map<String, Object> cookieStringToMap(String cookieString) {
|
||||
Map<String, Object> cookieMap = new HashMap<>();
|
||||
String[] cookieParts = cookieString.split(";");
|
||||
|
||||
for (String part : cookieParts) {
|
||||
String[] keyValue = part.trim().split("=");
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0].trim();
|
||||
String value = keyValue[1].trim();
|
||||
cookieMap.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return cookieMap;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> cookiesToList(Object cookies) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
if (cookies instanceof List) {
|
||||
List<?> cookieList = (List<?>) cookies;
|
||||
for (Object cookie : cookieList) {
|
||||
if (cookie instanceof String) {
|
||||
Map<String, Object> cookieMap = cookieToMap(cookie);
|
||||
if (cookieMap != null) result.add(cookieMap);
|
||||
} else if (cookie instanceof Map) {
|
||||
result.add((JSON.parseObject(JSON.toJSONString(cookie))));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void setBrowserCookies(ChromiumBase page, Object cookies) {
|
||||
List<Map<String, Object>> mapList = cookiesToList(cookies);
|
||||
for (Map<String, Object> cookieMap : mapList) {
|
||||
if (cookieMap.containsKey("expiry")) {
|
||||
cookieMap.put("expires", Integer.parseInt(cookieMap.get("expiry").toString()));
|
||||
cookieMap.remove("expiry");
|
||||
}
|
||||
|
||||
if (cookieMap.containsKey("expires")) {
|
||||
Object expires = cookieMap.get("expires");
|
||||
if (expires != null) {
|
||||
if (expires.toString().matches("\\d+?")) {
|
||||
cookieMap.put("expires", Integer.parseInt(expires.toString()));
|
||||
} else if (expires.toString().matches("\\d+(\\.\\d+)?"))
|
||||
cookieMap.put("expires", Double.parseDouble(expires.toString()));
|
||||
else {
|
||||
try {
|
||||
cookieMap.put("expires", convertExpiresToTimestamp(expires.toString()));
|
||||
} catch (ParseException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cookieMap.get("value") == null) cookieMap.put("value", "");
|
||||
else if (!(cookieMap.get("value") instanceof String))
|
||||
cookieMap.put("value", String.valueOf(cookieMap.get("value")));
|
||||
if (cookieMap.get("name") != null && ((String) cookieMap.get("name")).startsWith("__Secure-"))
|
||||
cookieMap.put("secure", true);
|
||||
|
||||
if (cookieMap.get("name") != null && ((String) cookieMap.get("name")).startsWith("__Host-")) {
|
||||
cookieMap.put("path", "/");
|
||||
cookieMap.put("secure", true);
|
||||
cookieMap.put("url", page.url());
|
||||
page.runCdpLoaded("Network.setCookie", JSONObject.parseObject(JSONObject.toJSONString(cookieMap)));
|
||||
continue; // 不用设置域名,可退出
|
||||
}
|
||||
if (cookieMap.containsKey("domain")) try {
|
||||
page.runCdpLoaded("Network.setCookie", JSONObject.parseObject(JSONObject.toJSONString(cookieMap)));
|
||||
isCookieInDriver(page, cookieMap);
|
||||
} catch (Exception ignored) {
|
||||
// Handle the exception as needed
|
||||
}
|
||||
URL url;
|
||||
try {
|
||||
url = new URL(page.url());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
List<String> dList = new ArrayList<>(List.of(url.getHost().split("\\.")));
|
||||
List<String> tmp = new ArrayList<>();
|
||||
if (dList.size() > 1) {
|
||||
for (int i = 1; i < dList.size(); i++) {
|
||||
tmp.add(".");
|
||||
tmp.add(dList.get(i));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < tmp.size(); i++) {
|
||||
String d = String.join("", tmp.subList(i, tmp.size()));
|
||||
cookieMap.put("domain", d);
|
||||
page.runCdpLoaded("Network.setCookie", cookieMap);
|
||||
if (isCookieInDriver(page, cookieMap)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static long convertExpiresToTimestamp(String expiresStr) throws ParseException {
|
||||
// 定义日期时间格式
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
|
||||
Date expiresDate = dateFormat.parse(expiresStr);
|
||||
|
||||
// 将日期时间对象转换为 UNIX 时间戳(毫秒)
|
||||
return expiresDate.getTime() / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询cookie是否在浏览器内
|
||||
*
|
||||
* @param page BasePage对象
|
||||
* @param cookie map
|
||||
* @return 是否存在
|
||||
*/
|
||||
public static boolean isCookieInDriver(ChromiumBase page, Map<String, Object> cookie) {
|
||||
Object cookies;
|
||||
List<Map<String, Object>> allCookies;
|
||||
if (cookie.containsKey("domain")) {
|
||||
cookies = page.cookies(true);
|
||||
allCookies = JSON.parseObject(cookies.toString(), new TypeReference<>() {
|
||||
});
|
||||
for (Map<String, Object> c : allCookies) {
|
||||
if (cookie.get("name").equals(c.get("name")) && cookie.get("value").equals(c.get("value")) && cookie.get("domain").equals(c.get("domain"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cookies = page.cookies(true);
|
||||
allCookies = JSON.parseObject(cookies.toString(), new TypeReference<>() {
|
||||
});
|
||||
for (Map<String, Object> c : allCookies) {
|
||||
if (cookie.get("name").equals(c.get("name")) && cookie.get("value").equals(c.get("value"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知道blob资源
|
||||
*
|
||||
* @param page 资源所在页面对象
|
||||
* @param url 资源url
|
||||
* @param asBytes 是否以字节形式返回
|
||||
* @return 资源内容
|
||||
*/
|
||||
public static Object getBlob(ChromiumBase page, String url, boolean asBytes) {
|
||||
if (!url.startsWith("blob")) {
|
||||
throw new IllegalArgumentException("该链接非blob类型。");
|
||||
}
|
||||
|
||||
String js = "function fetchData(url) {\n" + " return new Promise((resolve, reject) => {\n" + " var xhr = new XMLHttpRequest();\n" + " xhr.responseType = 'blob';\n" + " xhr.onload = function() {\n" + " var reader = new FileReader();\n" + " reader.onloadend = function(){resolve(reader.result);}\n" + " reader.readAsDataURL(xhr.response);\n" + " };\n" + " xhr.open('GET', url, true);\n" + " xhr.send();\n" + " });\n" + "}\n";
|
||||
|
||||
Object result;
|
||||
try {
|
||||
result = page.runJs(js, List.of(url));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("无法获取该资源。", e);
|
||||
}
|
||||
|
||||
if (asBytes) {
|
||||
return Base64.getDecoder().decode(result.toString().split(",", 2)[1]);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
26
java/src/main/java/com/ll/DrissonPage/page/Alert.java
Normal file
26
java/src/main/java/com/ll/DrissonPage/page/Alert.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Alert {
|
||||
private Boolean activated = false;
|
||||
private String text = null;
|
||||
private String type = null;
|
||||
private String defaultPrompt = null;
|
||||
private String responseAccept = null;
|
||||
private String responseText = null;
|
||||
private Boolean handleNext = null;
|
||||
private String nextText = null;
|
||||
private Boolean auto = null;
|
||||
}
|
2204
java/src/main/java/com/ll/DrissonPage/page/ChromiumBase.java
Normal file
2204
java/src/main/java/com/ll/DrissonPage/page/ChromiumBase.java
Normal file
File diff suppressed because it is too large
Load Diff
1404
java/src/main/java/com/ll/DrissonPage/page/ChromiumFrame.java
Normal file
1404
java/src/main/java/com/ll/DrissonPage/page/ChromiumFrame.java
Normal file
File diff suppressed because it is too large
Load Diff
705
java/src/main/java/com/ll/DrissonPage/page/ChromiumPage.java
Normal file
705
java/src/main/java/com/ll/DrissonPage/page/ChromiumPage.java
Normal file
@ -0,0 +1,705 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.base.Browser;
|
||||
import com.ll.DrissonPage.config.ChromiumOptions;
|
||||
import com.ll.DrissonPage.config.PortFinder;
|
||||
import com.ll.DrissonPage.error.extend.BrowserConnectError;
|
||||
import com.ll.DrissonPage.functions.BrowserUtils;
|
||||
import com.ll.DrissonPage.units.setter.ChromiumPageSetter;
|
||||
import com.ll.DrissonPage.units.waiter.PageWaiter;
|
||||
import com.ll.DrissonPage.utils.CloseableHttpClientUtils;
|
||||
import lombok.Getter;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* 用于管理浏览器的类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class ChromiumPage extends ChromiumBase {
|
||||
private static final Map<String, ChromiumPage> PAGES = new ConcurrentHashMap<>();
|
||||
private final String browserId;
|
||||
private final boolean isExist;
|
||||
@Getter
|
||||
private final ChromiumOptions chromiumOptions;
|
||||
|
||||
|
||||
private ChromiumPage(String address, String tabId, Double timeout, Object[] objects) {
|
||||
this(handleOptions(address), tabId, timeout, objects);
|
||||
}
|
||||
|
||||
private ChromiumPage(Integer address, String tabId, Double timeout, Object[] objects) {
|
||||
this("127.0.0.1:" + String.format("%04d", Math.max(address, 1000)), tabId, timeout, objects);
|
||||
}
|
||||
|
||||
private ChromiumPage(ChromiumOptions options, String tabId, Double timeout, Object[] objects) {
|
||||
this.chromiumOptions = handleOptions(options);
|
||||
this.isExist = Boolean.parseBoolean(objects[0].toString());
|
||||
this.browserId = objects[1].toString();
|
||||
this.setTimeout(timeout);
|
||||
this.page = this;
|
||||
this.runBrowser();
|
||||
super.init(options.getAddress(), tabId, timeout);
|
||||
this.setType("ChromiumPage");
|
||||
this.set().timeouts(timeout);
|
||||
this.pageInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*/
|
||||
public static ChromiumPage getInstance() {
|
||||
return getInstance("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param address 浏览器地址
|
||||
*/
|
||||
public static ChromiumPage getInstance(String address) {
|
||||
return getInstance("".equals(address) ? null : address, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param options 浏览器配置
|
||||
*/
|
||||
public static ChromiumPage getInstance(ChromiumOptions options) {
|
||||
return getInstance(options, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param address 浏览器地址
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static ChromiumPage getInstance(String address, Double timeout) {
|
||||
return getInstance(address, null, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param port 端口
|
||||
*/
|
||||
public static ChromiumPage getInstance(Integer port) {
|
||||
return getInstance(port, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param options 浏览器配置
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static ChromiumPage getInstance(ChromiumOptions options, Double timeout) {
|
||||
return getInstance(options, null, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param port 端口
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static ChromiumPage getInstance(Integer port, Double timeout) {
|
||||
return getInstance(port, null, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param address 浏览器地址
|
||||
* @param tabId 要控制的标签页id,不指定默认为激活的
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static ChromiumPage getInstance(String address, String tabId, Double timeout) {
|
||||
return getInstance(handleOptions(address), tabId, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param port 端口
|
||||
* @param tabId 要控制的标签页id,不指定默认为激活的
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static ChromiumPage getInstance(Integer port, String tabId, Double timeout) {
|
||||
Object[] objects = runBrowser(handleOptions(port));
|
||||
return PAGES.computeIfAbsent(objects[1].toString(), key -> new ChromiumPage(port, tabId, timeout, objects));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例模式
|
||||
*
|
||||
* @param options 浏览器配置
|
||||
* @param tabId 要控制的标签页id,不指定默认为激活的
|
||||
* @param timeout 超时时间(秒)
|
||||
*/
|
||||
public static ChromiumPage getInstance(ChromiumOptions options, String tabId, Double timeout) {
|
||||
Object[] objects = runBrowser(handleOptions(options));
|
||||
return PAGES.computeIfAbsent(objects[1].toString(), key -> new ChromiumPage(options, tabId, timeout, objects));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器启动属性
|
||||
*
|
||||
* @param port 'port'
|
||||
*/
|
||||
public static ChromiumOptions handleOptions(int port) {
|
||||
return new ChromiumOptions().setLocalPort(port);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器启动属性
|
||||
*
|
||||
* @param options 'ChromiumOptions'
|
||||
*/
|
||||
public static ChromiumOptions handleOptions(ChromiumOptions options) {
|
||||
if (options == null) options = new ChromiumOptions();
|
||||
else {
|
||||
if (options.isAutoPort()) {
|
||||
PortFinder.PortInfo address = new PortFinder(options.getTmpPath()).getPort();
|
||||
options = options.setAddress("127.0.0.1:" + address.getPort()).setUserDataPath(address.getPath()).autoPort();
|
||||
}
|
||||
}
|
||||
return options;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器启动属性
|
||||
*
|
||||
* @param address 'ip:port'
|
||||
*/
|
||||
public static ChromiumOptions handleOptions(String address) {
|
||||
ChromiumOptions options;
|
||||
if (address == null || address.isEmpty()) {
|
||||
options = new ChromiumOptions();
|
||||
} else {
|
||||
options = new ChromiumOptions();
|
||||
options.setAddress(address);
|
||||
}
|
||||
return options;
|
||||
|
||||
}
|
||||
|
||||
//----------挂件----------
|
||||
|
||||
public static Object[] runBrowser(ChromiumOptions options) {
|
||||
boolean isExist = BrowserUtils.connectBrowser(options);
|
||||
String browserId;
|
||||
try {
|
||||
HttpGet request = new HttpGet("http://" + options.getAddress() + "/json/version");
|
||||
request.setHeader("Connection", "close");
|
||||
Object ws = CloseableHttpClientUtils.sendRequestJson(request);
|
||||
if (ws == null || ws.toString().isEmpty()) {
|
||||
throw new BrowserConnectError("\n浏览器连接失败,如使用全局代理,须设置不代理127.0.0.1地址。");
|
||||
}
|
||||
String[] split = JSON.parseObject(ws.toString()).get("webSocketDebuggerUrl").toString().split("/");
|
||||
browserId = split[split.length - 1];
|
||||
} catch (NullPointerException e) {
|
||||
throw new BrowserConnectError("浏览器版本太旧,请升级。");
|
||||
} catch (Exception e) {
|
||||
throw new BrowserConnectError("\n浏览器连接失败,如使用全局代理,须设置不代理127.0.0.1地址。");
|
||||
}
|
||||
return new Object[]{isExist, browserId};
|
||||
}
|
||||
|
||||
private void runBrowser() {
|
||||
this.setBrowser(Browser.getInstance(this.chromiumOptions.getAddress(), this.browserId, this));
|
||||
if (this.isExist && !this.chromiumOptions.isHeadless() && JSON.parseObject(this.getBrowser().runCdp("Browser.getVersion")).getString("userAgent").toLowerCase().contains("headless")) {
|
||||
this.getBrowser().quit(3);
|
||||
BrowserUtils.connectBrowser(this.chromiumOptions);
|
||||
HttpGet request = new HttpGet("http://" + this.chromiumOptions.getAddress() + "/json/version");
|
||||
request.setHeader("Connection", "close");
|
||||
JSONObject obj = JSON.parseObject(CloseableHttpClientUtils.sendRequestJson(request));
|
||||
String[] split = obj.get("webSocketDebuggerUrl").toString().split("/");
|
||||
String ws = split[split.length - 1];
|
||||
this.setBrowser(Browser.getInstance(this.chromiumOptions.getAddress(), ws, this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//----------挂件----------
|
||||
|
||||
@Override
|
||||
protected void dSetRuntimeSettings() {
|
||||
super.timeouts = new Timeout(this, this.chromiumOptions.getTimeouts().get("base"), this.chromiumOptions.getTimeouts().get("pageLoad"), this.chromiumOptions.getTimeouts().get("script"));
|
||||
Double base = this.chromiumOptions.getTimeouts().get("base");
|
||||
if (base != null) this.setTimeout(base);
|
||||
super.setLoadMode(this.chromiumOptions.getLoadMode());
|
||||
this.setDownloadPath(this.chromiumOptions.getDownloadPath() == null ? null : Paths.get(this.chromiumOptions.getDownloadPath()).toFile().getAbsolutePath());
|
||||
|
||||
super.setRetryTimes(this.chromiumOptions.getRetryTimes());
|
||||
super.setRetryInterval((double) this.chromiumOptions.getRetryInterval());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器相关设置
|
||||
*/
|
||||
private void pageInit() {
|
||||
this.getBrowser().connectToPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置的对象
|
||||
*/
|
||||
@Override
|
||||
public ChromiumPageSetter set() {
|
||||
if (super.set == null) {
|
||||
super.set = new ChromiumPageSetter(this);
|
||||
}
|
||||
return (ChromiumPageSetter) super.set;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于等待的对象
|
||||
*/
|
||||
public PageWaiter waits() {
|
||||
if (super.wait == null) this.wait = new PageWaiter(this);
|
||||
return (PageWaiter) super.wait;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于控制浏览器cdp的driver
|
||||
*/
|
||||
public Browser browser() {
|
||||
return this.getBrowser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回标签页数量
|
||||
*/
|
||||
public Integer tabsCount() {
|
||||
return this.getBrowser().tabsCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回所有标签页id组成的列表
|
||||
*/
|
||||
public List<String> tabs() {
|
||||
return this.getBrowser().tabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回最新的标签页id,最新标签页指最后创建或最后被激活的
|
||||
*/
|
||||
public String latestTab() {
|
||||
return this.tabs().get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回浏览器进程id
|
||||
*/
|
||||
|
||||
public Integer processId() {
|
||||
return this.getBrowser().getProcessId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前页面保存为文件,如果path和name参数都为null,只返回文本
|
||||
*
|
||||
* @param path 保存路径,为null且name不为null时保存在当前路径
|
||||
* @param name 文件名,为null且path不为null时用title属性值
|
||||
* @return asPdf为True时返回bytes,否则返回文件文本
|
||||
*/
|
||||
|
||||
public Object save(String path, String name) {
|
||||
return save(path, name, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前页面保存为文件,如果path和name参数都为null,只返回文本
|
||||
*
|
||||
* @param path 保存路径,为null且name不为null时保存在当前路径
|
||||
* @param name 文件名,为null且path不为null时用title属性值
|
||||
* @param asPdf 为Ture保存为pdf,否则为mhtml且忽略params参数
|
||||
* @return asPdf为True时返回bytes,否则返回文件文本
|
||||
*/
|
||||
|
||||
public Object save(String path, String name, boolean asPdf) {
|
||||
return save(path, name, asPdf, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前页面保存为文件,如果path和name参数都为null,只返回文本
|
||||
*
|
||||
* @param path 保存路径,为null且name不为null时保存在当前路径
|
||||
* @param name 文件名,为null且path不为null时用title属性值
|
||||
* @param asPdf 为Ture保存为pdf,否则为mhtml且忽略params参数
|
||||
* @param params pdf生成参数
|
||||
* @return asPdf为True时返回bytes,否则返回文件文本
|
||||
*/
|
||||
|
||||
public Object save(String path, String name, boolean asPdf, Map<String, Object> params) {
|
||||
return asPdf ? ChromiumBase.getPdf(this, path, name, params) : ChromiumBase.getMHtml(this, path, name);
|
||||
}
|
||||
|
||||
public ChromiumTab getTab() {
|
||||
return getTab(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签页对象
|
||||
*
|
||||
* @param id 要获取的标签页id或序号,为null时获取当前tab,序号从0开始,可传入负数获取倒数第几个,不是视觉排列顺序,而是激活顺序
|
||||
* @return 标签页对象
|
||||
*/
|
||||
public ChromiumTab getTab(String id) {
|
||||
if (id == null) return ChromiumTab.getInstance(this, this.tabId());
|
||||
return ChromiumTab.getInstance(this, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签页对象
|
||||
*
|
||||
* @param num 要获取的标签页id或序号,为null时获取当前tab,序号从0开始,可传入负数获取倒数第几个,不是视觉排列顺序,而是激活顺序
|
||||
* @return 标签页对象
|
||||
*/
|
||||
public ChromiumTab getTab(int num) {
|
||||
List<String> tabs = this.tabs();
|
||||
return ChromiumTab.getInstance(this, tabs.get(num >= 0 ? num : tabs.size() + num));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs() {
|
||||
return findTabs(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param single 是否返回首个结果的id,为False返回所有信息
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(Boolean single) {
|
||||
return findTabs(null, single);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @param single 是否返回首个结果的id,为False返回所有信息
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title, Boolean single) {
|
||||
return findTabs(title, null, single);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @param url 要匹配url的文本
|
||||
* @param single 是否返回首个结果的id,为False返回所有信息
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title, String url, Boolean single) {
|
||||
return findTabs(title, url, null, single);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找符合条件的tab,返回它们的id组成的列表
|
||||
*
|
||||
* @param title 要匹配title的文本
|
||||
* @param url 要匹配url的文本
|
||||
* @param tabType tab类型,可用列表输入多个
|
||||
* @param single 是否返回首个结果的id,为False返回所有信息
|
||||
* @return tab id或tab列表
|
||||
*/
|
||||
public List<String> findTabs(String title, String url, List<String> tabType, Boolean single) {
|
||||
return this.getBrowser().findTabs(title, url, tabType, single);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public ChromiumTab newTab(String url) {
|
||||
return newTab(url, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public ChromiumTab newTab(String url, boolean newWindow) {
|
||||
return newTab(url, newWindow, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @param background 是否不激活新标签页,如new_window为True则无效
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public ChromiumTab newTab(String url, boolean newWindow, boolean background) {
|
||||
return newTab(url, newWindow, background, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @param background 是否不激活新标签页,如new_window为True则无效
|
||||
* @param newContext 是否创建新的上下文
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public ChromiumTab newTab(String url, boolean newWindow, boolean background, boolean newContext) {
|
||||
ChromiumTab chromiumTab = ChromiumTab.getInstance(this, this._newTab(newWindow, background, newContext));
|
||||
if (url != null && !url.isEmpty()) chromiumTab.get(url);
|
||||
return chromiumTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @param background 是否不激活新标签页,如new_window为True则无效
|
||||
* @param newContext 是否创建新的上下文
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
protected String _newTab(boolean newWindow, boolean background, boolean newContext) {
|
||||
Object bid = null;
|
||||
if (newContext)
|
||||
bid = JSON.parseObject(this.getBrowser().runCdp("Target.createBrowserContext")).get("browserContextId");
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("url", "");
|
||||
if (newWindow) params.put("newWindow", true);
|
||||
if (background) params.put("background", true);
|
||||
if (bid != null) params.put("browserContextId", bid);
|
||||
return JSON.parseObject(this.getBrowser().runCdp("Target.createTarget", params)).get("targetId").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭Page管理的标签页
|
||||
*/
|
||||
public void close() {
|
||||
this.closeTabs(this.tabId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*/
|
||||
public void closeTabs() {
|
||||
closeTabs(new String[]{});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param ids 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
*/
|
||||
public void closeTabs(String[] ids) {
|
||||
closeTabs(ids, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param ids 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
*/
|
||||
public void closeTabs(String ids) {
|
||||
closeTabs(ids, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param ids 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
* @param others 是否关闭指定标签页之外的
|
||||
*/
|
||||
public void closeTabs(String[] ids, boolean others) {
|
||||
if (ids.length == 0) ids = new String[]{this.tabId()};
|
||||
List<String> tabs = Arrays.asList(ids);
|
||||
closeTabs(others, tabs);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param ids 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
* @param others 是否关闭指定标签页之外的
|
||||
*/
|
||||
public void closeTabs(String ids, boolean others) {
|
||||
if (ids == null) ids = this.tabId();
|
||||
List<String> tabs = Collections.singletonList(ids);
|
||||
closeTabs(others, tabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param chromiumTabs 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
*/
|
||||
public void closeTabs(ChromiumTab[] chromiumTabs) {
|
||||
closeTabs(chromiumTabs, false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param chromiumTab 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
*/
|
||||
public void closeTabs(ChromiumTab chromiumTab) {
|
||||
closeTabs(chromiumTab, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param chromiumTabs 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
* @param others 是否关闭指定标签页之外的
|
||||
*/
|
||||
public void closeTabs(ChromiumTab[] chromiumTabs, boolean others) {
|
||||
List<String> tabs = new ArrayList<>();
|
||||
if (chromiumTabs.length == 0) tabs.add(this.tabId());
|
||||
for (ChromiumTab chromiumTab : chromiumTabs) tabs.add(chromiumTab.tabId());
|
||||
|
||||
closeTabs(others, tabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param chromiumTab 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
* @param others 是否关闭指定标签页之外的
|
||||
*/
|
||||
public void closeTabs(ChromiumTab chromiumTab, boolean others) {
|
||||
List<String> tabs = new ArrayList<>();
|
||||
tabs.add((chromiumTab == null ? this : chromiumTab).tabId());
|
||||
closeTabs(others, tabs);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传入的标签页,默认关闭当前页。可传入多个
|
||||
*
|
||||
* @param others 要关闭的标签页对象或id,可传入列表或元组,为None时关闭当前页
|
||||
* @param tabs 是否关闭指定标签页之外的
|
||||
*/
|
||||
private void closeTabs(boolean others, List<String> tabs) {
|
||||
List<String> allTabs = this.tabs();
|
||||
int size = allTabs.size();
|
||||
if (others) {
|
||||
allTabs.removeAll(tabs);
|
||||
tabs = allTabs;
|
||||
}
|
||||
int endLen = tabs.size() - size;
|
||||
super.driver().stop();
|
||||
if (endLen <= 0) {
|
||||
this.quit();
|
||||
return;
|
||||
}
|
||||
for (String id : tabs) {
|
||||
this.getBrowser().closeTab(id);
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
long endTime = System.currentTimeMillis() + 3000;
|
||||
while (this.tabsCount() != endLen && endTime > System.currentTimeMillis()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*/
|
||||
public void quit() {
|
||||
this.quit(5.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*
|
||||
* @param timeout 等待浏览器关闭超时时间(秒)
|
||||
*/
|
||||
public void quit(double timeout) {
|
||||
this.quit(timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*
|
||||
* @param timeout 等待浏览器关闭超时时间(秒)
|
||||
* @param force 关闭超时是否强制终止进程
|
||||
*/
|
||||
public void quit(double timeout, boolean force) {
|
||||
this.getBrowser().quit(timeout, force);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 克隆新的浏览器
|
||||
*
|
||||
* @param cloneNumber 克隆数量
|
||||
* @return 集合
|
||||
*/
|
||||
public List<ChromiumPage> copy(int cloneNumber) {
|
||||
return IntStream.range(0, cloneNumber < 0 ? 1 : cloneNumber).mapToObj(i -> copy()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆新的浏览器
|
||||
*
|
||||
* @return 单个
|
||||
*/
|
||||
public ChromiumPage copy() {
|
||||
ChromiumOptions chromiumOptions1 = this.chromiumOptions.copy();
|
||||
chromiumOptions1.autoPort(true, chromiumOptions1.getTmpPath() + UUID.randomUUID().toString().substring(0, 5));
|
||||
ChromiumPage instance = ChromiumPage.getInstance(chromiumOptions1);
|
||||
String url1 = this.url();
|
||||
if (url1 != null) instance.get(url1);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
ChromiumPage.PAGES.remove(this.browserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChromiumPage{" + "browser_id=" + this.getBrowser().getId() + "tab_id=" + this.tabId() + '}';
|
||||
}
|
||||
|
||||
|
||||
}
|
152
java/src/main/java/com/ll/DrissonPage/page/ChromiumTab.java
Normal file
152
java/src/main/java/com/ll/DrissonPage/page/ChromiumTab.java
Normal file
@ -0,0 +1,152 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import com.ll.DrissonPage.functions.Settings;
|
||||
import com.ll.DrissonPage.units.setter.TabSetter;
|
||||
import com.ll.DrissonPage.units.waiter.TabWaiter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* 实现浏览器标签页的类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class ChromiumTab extends ChromiumBase {
|
||||
private static final Map<String, ChromiumTab> TAB = new ConcurrentHashMap<>();
|
||||
|
||||
protected ChromiumTab(ChromiumPage page, String tabId) {
|
||||
this.page = page;
|
||||
this.setBrowser(page.getBrowser());
|
||||
super.init(page.getAddress(), tabId, page.timeout());
|
||||
super.rect = null;
|
||||
this.setType("ChromiumTab");
|
||||
}
|
||||
|
||||
public static ChromiumTab getInstance(ChromiumPage page, String tabId) {
|
||||
ChromiumTab chromiumTab = TAB.get(tabId);
|
||||
if (Settings.singletonTabObj && chromiumTab != null) return chromiumTab;
|
||||
chromiumTab = new ChromiumTab(page, tabId);
|
||||
TAB.put(tabId, chromiumTab);
|
||||
return chromiumTab;
|
||||
}
|
||||
|
||||
/***
|
||||
* 重写设置浏览器运行参数方法
|
||||
*/
|
||||
@Override
|
||||
protected void dSetRuntimeSettings() {
|
||||
super.timeouts = this.page.getTimeouts().copy();
|
||||
super.setRetryTimes(this.page.getRetryTimes());
|
||||
super.setRetryInterval(this.page.getRetryInterval());
|
||||
super.setLoadMode(this.page.loadMode());
|
||||
super.setDownloadPath(this.page.downloadPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前标签页
|
||||
*/
|
||||
public void close() {
|
||||
this.page.closeTabs(this.tabId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回总体page对象
|
||||
*/
|
||||
public ChromiumPage page() {
|
||||
return this.page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置的对象
|
||||
*/
|
||||
@Override
|
||||
public TabSetter set() {
|
||||
if (super.set == null) {
|
||||
super.set = new TabSetter(this);
|
||||
}
|
||||
return (TabSetter) super.set;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于等待的对象
|
||||
*/
|
||||
@Override
|
||||
public TabWaiter waits() {
|
||||
if (super.wait == null) this.wait = new TabWaiter(this);
|
||||
return (TabWaiter) super.wait;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前页面保存为文件,如果path和name参数都为null,只返回文本
|
||||
*
|
||||
* @param path 保存路径,为null且name不为null时保存在当前路径
|
||||
* @param name 文件名,为null且path不为null时用title属性值
|
||||
* @return asPdf为True时返回bytes,否则返回文件文本
|
||||
*/
|
||||
|
||||
public Object save(String path, String name) {
|
||||
return save(path, name, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前页面保存为文件,如果path和name参数都为null,只返回文本
|
||||
*
|
||||
* @param path 保存路径,为null且name不为null时保存在当前路径
|
||||
* @param name 文件名,为null且path不为null时用title属性值
|
||||
* @param asPdf 为Ture保存为pdf,否则为mhtml且忽略params参数
|
||||
* @return asPdf为True时返回bytes,否则返回文件文本
|
||||
*/
|
||||
|
||||
public Object save(String path, String name, boolean asPdf) {
|
||||
return save(path, name, asPdf, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前页面保存为文件,如果path和name参数都为null,只返回文本
|
||||
*
|
||||
* @param path 保存路径,为null且name不为null时保存在当前路径
|
||||
* @param name 文件名,为null且path不为null时用title属性值
|
||||
* @param asPdf 为Ture保存为pdf,否则为mhtml且忽略params参数
|
||||
* @param params pdf生成参数
|
||||
* @return asPdf为True时返回bytes,否则返回文件文本
|
||||
*/
|
||||
|
||||
public Object save(String path, String name, boolean asPdf, Map<String, Object> params) {
|
||||
return asPdf ? ChromiumBase.getPdf(this, path, name, params) : ChromiumBase.getMHtml(this, path, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<ChromiumTab " + "browser_id=" + this.browser().getId() + " tab_id=" + this.tabId() + '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @param cloneNumber 克隆数量
|
||||
* @return 集合
|
||||
*/
|
||||
public List<ChromiumTab> copy(int cloneNumber) {
|
||||
return IntStream.range(0, cloneNumber < 0 ? 1 : cloneNumber).mapToObj(i -> copy()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @return 单个
|
||||
*/
|
||||
public ChromiumTab copy() {
|
||||
return this.page.newTab(this.url());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
ChromiumTab.TAB.remove(this.tabId());
|
||||
}
|
||||
}
|
690
java/src/main/java/com/ll/DrissonPage/page/SessionPage.java
Normal file
690
java/src/main/java/com/ll/DrissonPage/page/SessionPage.java
Normal file
@ -0,0 +1,690 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.base.BasePage;
|
||||
import com.ll.DrissonPage.base.BeforeConnect;
|
||||
import com.ll.DrissonPage.base.By;
|
||||
import com.ll.DrissonPage.config.SessionOptions;
|
||||
import com.ll.DrissonPage.element.SessionElement;
|
||||
import com.ll.DrissonPage.units.HttpClient;
|
||||
import com.ll.DrissonPage.units.setter.SessionPageSetter;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.*;
|
||||
import okhttp3.internal.http.RealResponseBody;
|
||||
import okio.Buffer;
|
||||
import org.apache.commons.collections4.map.CaseInsensitiveMap;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* SessionPage封装了页面操作的常用功能,使用requests来获取、解析网页
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class SessionPage extends BasePage<SessionElement> {
|
||||
@Getter
|
||||
@Setter
|
||||
protected Map<String, String> headers;
|
||||
@Setter
|
||||
protected OkHttpClient session;
|
||||
protected SessionOptions sessionOptions;
|
||||
protected Response response;
|
||||
private double timeout;
|
||||
private int retryTimes;
|
||||
private float retryInterval;
|
||||
private SessionPageSetter set;
|
||||
@Setter
|
||||
private Charset encoding;
|
||||
|
||||
/**
|
||||
* @param request 请求工厂
|
||||
*/
|
||||
public SessionPage(OkHttpClient request) {
|
||||
this(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request 请求工厂
|
||||
* @param timeout 连接超时时间
|
||||
*/
|
||||
public SessionPage(OkHttpClient request, Double timeout) {
|
||||
this(request, timeout, false);
|
||||
}
|
||||
|
||||
public SessionPage() {
|
||||
this(new SessionOptions(true, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param option 配置
|
||||
*/
|
||||
public SessionPage(SessionOptions option) {
|
||||
this(option, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param option 配置
|
||||
* @param timeout 连接超时时间
|
||||
*/
|
||||
public SessionPage(SessionOptions option, Double timeout) {
|
||||
this(option, timeout, false);
|
||||
}
|
||||
|
||||
private SessionPage(Object requestOrOption, Double timeout, boolean ignoredFlag) {
|
||||
this.setType("SessionPage");
|
||||
this.sSetStartOptions(requestOrOption);
|
||||
this.sSetRunTimeSettings(timeout);
|
||||
this.createSession();
|
||||
if (timeout != null) this.timeout = timeout;
|
||||
this.headers = new CaseInsensitiveMap<>();
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动配置
|
||||
*/
|
||||
private void sSetStartOptions(Object sessionOrOptions) {
|
||||
if (sessionOrOptions == null || sessionOrOptions instanceof SessionOptions) {
|
||||
this.sessionOptions = sessionOrOptions == null ? new SessionOptions(true, null) : (SessionOptions) sessionOrOptions;
|
||||
} else if (sessionOrOptions instanceof OkHttpClient) {
|
||||
this.sessionOptions = new SessionOptions(true, null);
|
||||
this.headers = new CaseInsensitiveMap<>(this.sessionOptions.getHeaders());
|
||||
this.sessionOptions.setHeaders(null);
|
||||
this.session = (OkHttpClient) sessionOrOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行时用到的属性
|
||||
*/
|
||||
private void sSetRunTimeSettings(Double timeout) {
|
||||
this.timeout = timeout == null || timeout <= 0 ? this.sessionOptions.getTimeout() : timeout;
|
||||
this.setDownloadPath(this.sessionOptions.getDownloadPath() == null ? null : Paths.get(this.sessionOptions.getDownloadPath()).toAbsolutePath().toString());
|
||||
this.retryTimes = this.sessionOptions.getRetryTimes();
|
||||
this.retryInterval = this.sessionOptions.getRetryInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内建Session对象
|
||||
*/
|
||||
protected void createSession() {
|
||||
if (this.session == null) {
|
||||
HttpClient httpClient = this.sessionOptions.makeSession();
|
||||
this.session = httpClient.getClient();
|
||||
this.headers = new CaseInsensitiveMap<>(this.sessionOptions.getHeaders());
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------共有属性和方法-------------------
|
||||
@Override
|
||||
public String title() {
|
||||
List<SessionElement> sessionPages = this._ele("xpath://title", null, null, null, false, null);
|
||||
if (!sessionPages.isEmpty()) return sessionPages.get(0).text();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前访问url
|
||||
*/
|
||||
@Override
|
||||
public String url() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回页面原始数据
|
||||
*/
|
||||
public byte[] rawData() {
|
||||
ResponseBody body = this.response.body();
|
||||
if (body != null) {
|
||||
try {
|
||||
return body.bytes();
|
||||
} catch (IOException e) {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String html() {
|
||||
if (this.response == null) return "";
|
||||
ResponseBody body = this.response.body();
|
||||
if (body != null) {
|
||||
try {
|
||||
return body.string();
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 当返回内容是json格式则返回JSONObject,非json格式时返回None
|
||||
*/
|
||||
|
||||
@Override
|
||||
public JSONObject json() {
|
||||
if (this.response == null) return null;
|
||||
ResponseBody body = this.response.body();
|
||||
if (body != null) {
|
||||
try {
|
||||
return JSON.parseObject(body.string());
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回user agent
|
||||
*/
|
||||
@Override
|
||||
public String userAgent() {
|
||||
String ua = "";
|
||||
for (Map.Entry<String, String> entry : this.headers.entrySet()) {
|
||||
String k = entry.getKey();
|
||||
Object v = entry.getValue();
|
||||
if (Objects.equals(k.toUpperCase(Locale.ROOT), "user-agent")) {
|
||||
ua = v.toString();
|
||||
break;
|
||||
} else if ("useragent".equalsIgnoreCase(k.toUpperCase(Locale.ROOT))) {
|
||||
ua = v.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ua;
|
||||
}
|
||||
|
||||
public OkHttpClient session() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回访问url得到的Response对象
|
||||
*/
|
||||
public Response response() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回设置的编码
|
||||
*/
|
||||
public String encoding() {
|
||||
return this.encoding.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置的对象
|
||||
*/
|
||||
public SessionPageSetter set() {
|
||||
if (this.set == null) this.set = new SessionPageSetter(this);
|
||||
return this.set;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用get方式跳转到url,可输入文件路径
|
||||
*
|
||||
* @param url 目标url,可指定本地文件路径
|
||||
* @return url是否可用
|
||||
*/
|
||||
public Boolean get(Path url) {
|
||||
return get(url, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用get方式跳转到url,可输入文件路径
|
||||
*
|
||||
* @param url 目标url,可指定本地文件路径
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @return url是否可用
|
||||
*/
|
||||
public Boolean get(Path url, boolean showErrMsg) {
|
||||
return get(url, showErrMsg, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用get方式跳转到url,可输入文件路径
|
||||
*
|
||||
* @param url 目标url,可指定本地文件路径
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param interval 重试间隔(秒),为None时使用页面对象retry_interval属性值
|
||||
* @param timeout 连接超时时间(秒),为None时使用页面对象timeout属性值
|
||||
* @return url是否可用
|
||||
*/
|
||||
public Boolean get(Path url, boolean showErrMsg, Integer retry, Double interval, Double timeout) {
|
||||
return get(url, showErrMsg, retry, interval, timeout, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用get方式跳转到url,可输入文件路径
|
||||
*
|
||||
* @param url 目标url,可指定本地文件路径
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param interval 重试间隔(秒),为None时使用页面对象retry_interval属性值
|
||||
* @param timeout 连接超时时间(秒),为None时使用页面对象timeout属性值
|
||||
* @param params 连接参数
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
public Boolean get(@NotNull Path url, boolean showErrMsg, Integer retry, Double interval, Double timeout, Map<String, Object> params) {
|
||||
return get(url.toAbsolutePath().toString(), showErrMsg, retry, interval, timeout, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用get方式跳转到url,可输入文件路径
|
||||
*
|
||||
* @param url 目标url,可指定本地文件路径
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param interval 重试间隔(秒),为None时使用页面对象retry_interval属性值
|
||||
* @param timeout 连接超时时间(秒),为None时使用页面对象timeout属性值
|
||||
* @param params 连接参数
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Boolean get(@NotNull String url, boolean showErrMsg, Integer retry, Double interval, Double timeout, Map<String, Object> params) {
|
||||
if (!url.toLowerCase().startsWith("http")) {
|
||||
if (url.startsWith("file:///")) {
|
||||
url = url.substring(8);
|
||||
}
|
||||
File file = Paths.get(url).toFile();
|
||||
if (file.exists()) {
|
||||
try (FileInputStream fileInputStream = new FileInputStream(file)) {
|
||||
String string = Arrays.toString(fileInputStream.readAllBytes());
|
||||
Response.Builder builder = new Response.Builder();
|
||||
builder.setMessage$okhttp("get");
|
||||
builder.setCode$okhttp(200);
|
||||
builder.setBody$okhttp(new RealResponseBody(string, string.length(), new Buffer()));
|
||||
this.response = builder.build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.sConnect(url, "get", showErrMsg, retry, interval, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url
|
||||
*
|
||||
* @param url 目标url
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
public Boolean post(@NotNull String url) {
|
||||
return this.post(url, null);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param params 连接参数
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
public Boolean post(@NotNull String url, Map<String, Object> params) {
|
||||
return this.post(url, false, params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param params 连接参数
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
public Boolean post(@NotNull String url, boolean showErrMsg, Map<String, Object> params) {
|
||||
return this.post(url, showErrMsg, null, params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param params 连接参数
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
public Boolean post(@NotNull String url, boolean showErrMsg, Integer retry, Map<String, Object> params) {
|
||||
return this.post(url, showErrMsg, retry, null, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param interval 重试间隔(秒),为None时使用页面对象retry_interval属性值
|
||||
* @param params 连接参数
|
||||
* @return url是否可用
|
||||
*/
|
||||
|
||||
public Boolean post(@NotNull String url, boolean showErrMsg, Integer retry, Double interval, Map<String, Object> params) {
|
||||
return this.sConnect(url, "post", showErrMsg, retry, interval, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionElement sEle(By by, Integer index) {
|
||||
if (by == null) {
|
||||
List<SessionElement> sessionElements = SessionElement.makeSessionEle(this.html(), By.NULL(), null);
|
||||
if (!sessionElements.isEmpty()) return sessionElements.get(0);
|
||||
return null;
|
||||
}
|
||||
List<SessionElement> sessionElements = this._ele(by, null, index, null, null, "s_ele()");
|
||||
if (!sessionElements.isEmpty()) return sessionElements.get(0);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionElement sEle(String loc, Integer index) {
|
||||
if (loc == null) {
|
||||
List<SessionElement> sessionElements = SessionElement.makeSessionEle(this.html(), By.NULL(), null);
|
||||
if (!sessionElements.isEmpty()) return sessionElements.get(0);
|
||||
return null;
|
||||
}
|
||||
List<SessionElement> sessionElements = this._ele(loc, null, index, null, null, "s_ele()");
|
||||
if (!sessionElements.isEmpty()) return sessionElements.get(0);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionElement> sEles(By by) {
|
||||
return this._ele(by, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionElement> sEles(String loc) {
|
||||
return this._ele(loc, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SessionElement> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
return SessionElement.makeSessionEle(this, by, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SessionElement> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
return SessionElement.makeSessionEle(this, loc, index);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> cookies(boolean asMap, boolean allDomains, boolean allInfo) {
|
||||
List<Cookie> list;
|
||||
{
|
||||
final var cookies = new List[]{new ArrayList<>()};
|
||||
this.session.newBuilder().setCookieJar$okhttp(new CookieJar() {
|
||||
@Override
|
||||
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
|
||||
if (url != null) {
|
||||
ArrayList<Cookie> src = new ArrayList<>();
|
||||
Collections.copy(list, src);
|
||||
src.removeIf(cookie -> !cookie.domain().isEmpty() || !cookie.domain().contains(url));
|
||||
cookies[0] = src;
|
||||
} else {
|
||||
cookies[0] = list;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
});
|
||||
list = new ArrayList(cookies[0]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
public void close() {
|
||||
if (this.response != null) try {
|
||||
this.response.close();
|
||||
} catch (Exception ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sConnect(String url, String mode, boolean showErrMsg, Integer retry, Double interval, Map<String, Object> params) {
|
||||
BeforeConnect beforeConnect = this.beforeConnect(url, retry, interval);
|
||||
ResponseWrapper responseReturn = this.makResponse(this.url(), mode, beforeConnect.getRetry(), beforeConnect.getInterval(), showErrMsg, params);
|
||||
boolean urlAvailable;
|
||||
if (responseReturn.getResponse() == null) urlAvailable = false;
|
||||
else if (this.response.code() == 200) urlAvailable = true;
|
||||
else {
|
||||
if (showErrMsg) try {
|
||||
throw new ConnectException("状态码:" + this.response.code());
|
||||
} catch (ConnectException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
urlAvailable = false;
|
||||
|
||||
}
|
||||
return urlAvailable;
|
||||
}
|
||||
|
||||
public ResponseWrapper makResponse(String url, String mode, Integer retry, Double interval, boolean showErrMsg, Map<String, Object> params) {
|
||||
Map<String, Object> headersMap = new CaseInsensitiveMap<>();
|
||||
if (params.containsKey("headers"))
|
||||
headersMap.putAll(JSON.parseObject(JSON.toJSONString(params.get("headers"))));
|
||||
|
||||
// Set referer and host values
|
||||
URI uri = URI.create(url);
|
||||
String hostname = uri.getHost();
|
||||
String scheme = uri.getScheme();
|
||||
if (notCheckHeaders(headersMap, headers, "Referer")) {
|
||||
headersMap.put("Referer", this.headers.get("Referer"));
|
||||
if (headersMap.get("Referer") == null) {
|
||||
headersMap.put("Referer", (this.headers.get("scheme") != null ? this.headers.get("scheme") : scheme) + "://" + hostname);
|
||||
}
|
||||
}
|
||||
if (!headersMap.containsKey("Host")) {
|
||||
headersMap.put("Host", hostname);
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
if (notCheckHeaders(params, headers, "timeout")) {
|
||||
params.put("timeout", this.timeout);
|
||||
}
|
||||
|
||||
// Merge headers
|
||||
headersMap.putAll(this.headers);
|
||||
|
||||
// Build request
|
||||
Request.Builder requestBuilder = new Request.Builder();
|
||||
requestBuilder.url(url);
|
||||
for (Map.Entry<String, Object> entry : headersMap.entrySet()) {
|
||||
requestBuilder.addHeader(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
|
||||
Response response = null;
|
||||
retry = retry == null ? this.retryTimes : retry;
|
||||
interval = interval == null ? this.retryInterval : interval;
|
||||
IOException exception = null;
|
||||
for (int i = 0; i <= retry; i++) {
|
||||
try {
|
||||
if (mode.equals("get")) {
|
||||
response = this.session.newCall(requestBuilder.build()).execute();
|
||||
} else if (mode.equals("post")) {
|
||||
RequestBody requestBody = RequestBody.create(params.get("data").toString(), MediaType.parse("application/json"));
|
||||
requestBuilder.post(requestBody);
|
||||
response = this.session.newCall(requestBuilder.build()).execute();
|
||||
}
|
||||
|
||||
if (response != null && response.body() != null) {
|
||||
MediaType mediaType = response.body().contentType();
|
||||
if (mediaType != null && this.encoding != null) {
|
||||
mediaType.charset(this.encoding);
|
||||
return new ResponseWrapper(response, "Success");
|
||||
|
||||
}
|
||||
return new ResponseWrapper(setCharset(response), "Success");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
}
|
||||
long millis = (long) (interval * 1000);
|
||||
if (i < retry) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
if (showErrMsg) {
|
||||
System.out.println("重试 " + url);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showErrMsg) {
|
||||
if (exception != null) {
|
||||
throw new RuntimeException(exception);
|
||||
} else if (response != null) {
|
||||
try {
|
||||
throw new ConnectException("状态码:" + response.code());
|
||||
} catch (ConnectException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
throw new ConnectException("连接失败");
|
||||
} catch (ConnectException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (response != null) {
|
||||
return new ResponseWrapper(response, "状态码:" + response.code());
|
||||
} else {
|
||||
return new ResponseWrapper(null, "连接失败" + (exception != null ? exception.getMessage() : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean notCheckHeaders(Map<String, Object> map, Map<String, String> headers, String key) {
|
||||
return !map.containsKey(key) && !headers.containsKey(key);
|
||||
}
|
||||
|
||||
public static Response setCharset(Response response) {
|
||||
// 在headers中获取编码
|
||||
String s = response.headers().get("content-type");
|
||||
s = s == null ? "" : s;
|
||||
String contentType = s.toLowerCase();
|
||||
if (!contentType.endsWith(";")) contentType += ";";
|
||||
String charset = searchCharset(contentType);
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (charset != null && !charset.isEmpty()) {
|
||||
if (body != null) {
|
||||
MediaType mediaType = body.contentType();
|
||||
if (mediaType != null) {
|
||||
mediaType.charset(Charset.forName(charset));
|
||||
}
|
||||
}
|
||||
} else if (contentType.replace(" ", "").startsWith("text/html")) {
|
||||
String content = "";
|
||||
try {
|
||||
if (body != null) {
|
||||
content = body.string();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
charset = searchCharsetInMeta(content);
|
||||
if (charset == null || !charset.isEmpty()) {
|
||||
MediaType mediaType = null;
|
||||
if (body != null) mediaType = body.contentType();
|
||||
if (mediaType != null) charset = mediaType.type();
|
||||
}
|
||||
Response.Builder builder = new Response.Builder();
|
||||
builder.setBody$okhttp(ResponseBody.create(content, MediaType.get(charset == null ? "utf-8" : charset)));
|
||||
response.close();
|
||||
try (Response r = builder.build()) {
|
||||
response = r;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String searchCharset(String contentType) {
|
||||
Pattern pattern = Pattern.compile("charset[=: ]*(.*?);?");
|
||||
Matcher matcher = pattern.matcher(contentType);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
private static String searchCharsetInMeta(String content) {
|
||||
Pattern pattern = Pattern.compile("<meta.*?charset=[ \\\\'\"]*([^\"\\\\' />]+).*?>", Pattern.DOTALL);
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @param cloneNumber 克隆数量
|
||||
* @return 集合
|
||||
*/
|
||||
public List<SessionPage> copy(int cloneNumber) {
|
||||
return IntStream.range(0, cloneNumber < 0 ? 1 : cloneNumber).mapToObj(i -> copy()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @return 单个
|
||||
*/
|
||||
public SessionPage copy() {
|
||||
return new SessionPage(this.sessionOptions.copy());
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class ResponseWrapper {
|
||||
private Response response;
|
||||
private String message;
|
||||
}
|
||||
}
|
62
java/src/main/java/com/ll/DrissonPage/page/Timeout.java
Normal file
62
java/src/main/java/com/ll/DrissonPage/page/Timeout.java
Normal file
@ -0,0 +1,62 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public class Timeout {
|
||||
private final ChromiumBase page;
|
||||
@Setter
|
||||
private Double base = 10.0;
|
||||
@Setter
|
||||
|
||||
private Double pageLoad = 30.0;
|
||||
@Setter
|
||||
|
||||
private Double script = 30.0;
|
||||
|
||||
public Timeout(ChromiumBase page, Double base, Double pageLoad, Double script) {
|
||||
this.page = page;
|
||||
if (base != null && base >= 0) this.base = base;
|
||||
if (pageLoad != null && pageLoad >= 0) this.pageLoad = pageLoad;
|
||||
if (script != null && script >= 0) this.script = script;
|
||||
}
|
||||
|
||||
public Timeout(ChromiumBase page, Integer base, Integer pageLoad, Integer script) {
|
||||
this.page = page;
|
||||
if (base != null && base >= 0) this.base = Double.valueOf(base);
|
||||
if (pageLoad != null && pageLoad >= 0) this.pageLoad = Double.valueOf(pageLoad);
|
||||
if (script != null && script >= 0) this.script = Double.valueOf(script);
|
||||
}
|
||||
|
||||
public Timeout(ChromiumBase page) {
|
||||
this(page, -1.0, -1.0, -1.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{base=" + base + ", pageLoad=" + pageLoad + ", script=" + script + '}';
|
||||
}
|
||||
|
||||
// 深拷贝方法
|
||||
// 深拷贝方法
|
||||
public Timeout copy() {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos)) {
|
||||
out.writeObject(this);
|
||||
out.flush(); // 在写入对象之前调用 flush
|
||||
|
||||
try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream in = new ObjectInputStream(bis)) {
|
||||
return (Timeout) in.readObject();
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
System.out.println("深拷贝失败,错误原因:" + e.getMessage());
|
||||
return this; // 如果发生异常,返回原始对象
|
||||
}
|
||||
}
|
||||
}
|
16
java/src/main/java/com/ll/DrissonPage/page/WebMode.java
Normal file
16
java/src/main/java/com/ll/DrissonPage/page/WebMode.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
/**
|
||||
* 'd' 或 's',即driver模式和session模式
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public enum WebMode {
|
||||
s("s"), d("d"), S("s"), D("d"), NULL("d");
|
||||
final String mode;
|
||||
|
||||
WebMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
}
|
916
java/src/main/java/com/ll/DrissonPage/page/WebPage.java
Normal file
916
java/src/main/java/com/ll/DrissonPage/page/WebPage.java
Normal file
@ -0,0 +1,916 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.base.BasePage;
|
||||
import com.ll.DrissonPage.base.By;
|
||||
import com.ll.DrissonPage.base.DrissionElement;
|
||||
import com.ll.DrissonPage.config.ChromiumOptions;
|
||||
import com.ll.DrissonPage.config.SessionOptions;
|
||||
import com.ll.DrissonPage.element.ChromiumElement;
|
||||
import com.ll.DrissonPage.element.SessionElement;
|
||||
import com.ll.DrissonPage.functions.Web;
|
||||
import com.ll.DrissonPage.units.setter.WebPageSetter;
|
||||
import lombok.Getter;
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Response;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class WebPage extends BasePage<DrissionElement<?, ?>> {
|
||||
@Getter
|
||||
protected final SessionPage sessionPage;
|
||||
private WebPageSetter setter;
|
||||
@Getter
|
||||
protected ChromiumPage chromiumPage;
|
||||
private WebMode mode;
|
||||
@Getter
|
||||
private boolean hasDriver;
|
||||
@Getter
|
||||
private boolean hasSession;
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*/
|
||||
public WebPage() {
|
||||
this(WebMode.NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
*/
|
||||
public WebPage(WebMode mode) {
|
||||
this(mode, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout) {
|
||||
this(mode, timeout, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout, boolean chromiumOptions) {
|
||||
this(mode, timeout, chromiumOptions, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
* @param sessionOrOptions Session对象或SessionOptions对象,只使用d模式时应传入False
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout, boolean chromiumOptions, boolean sessionOrOptions) {
|
||||
this(mode, timeout, chromiumOptions, sessionOrOptions, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
* @param sessionOrOptions Session对象或SessionOptions对象,只使用d模式时应传入False
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout, boolean chromiumOptions, SessionOptions sessionOrOptions) {
|
||||
this(mode, timeout, chromiumOptions, sessionOrOptions, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout, ChromiumOptions chromiumOptions) {
|
||||
this(mode, timeout, chromiumOptions, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
* @param sessionOrOptions Session对象或SessionOptions对象,只使用d模式时应传入False
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout, ChromiumOptions chromiumOptions, boolean sessionOrOptions) {
|
||||
this(mode, timeout, chromiumOptions, sessionOrOptions, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
* @param sessionOrOptions Session对象或SessionOptions对象,只使用d模式时应传入False
|
||||
*/
|
||||
public WebPage(WebMode mode, Double timeout, ChromiumOptions chromiumOptions, SessionOptions sessionOrOptions) {
|
||||
this(mode, timeout, chromiumOptions, sessionOrOptions, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
*
|
||||
* @param mode 'd' 或 's',即driver模式和session模式
|
||||
* @param timeout 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒
|
||||
* @param chromiumOptions Driver对象,只使用s模式时应传入False
|
||||
* @param sessionOrOptions Session对象或SessionOptions对象,只使用d模式时应传入False
|
||||
*/
|
||||
private WebPage(WebMode mode, Double timeout, Object chromiumOptions, Object sessionOrOptions, boolean ignoredFlag) {
|
||||
this.mode = mode;
|
||||
sessionPage = sessionOrOptions instanceof SessionOptions ? new SessionPage((SessionOptions) sessionOrOptions) : new SessionPage();
|
||||
if (chromiumOptions == null || !Objects.equals(chromiumOptions, false))
|
||||
chromiumOptions = new ChromiumOptions(true, null).setTimeouts(sessionPage.timeout(), null, null).setPaths(sessionPage.downloadPath());
|
||||
ChromiumPage instance;
|
||||
if (chromiumOptions instanceof String) {
|
||||
instance = ChromiumPage.getInstance(String.valueOf(chromiumOptions), timeout);
|
||||
} else if (chromiumOptions instanceof ChromiumOptions) {
|
||||
instance = ChromiumPage.getInstance((ChromiumOptions) chromiumOptions, timeout);
|
||||
} else if (chromiumOptions instanceof Integer) {
|
||||
instance = ChromiumPage.getInstance((Integer) chromiumOptions, timeout);
|
||||
} else if (chromiumOptions == null) {
|
||||
instance = ChromiumPage.getInstance("", timeout);
|
||||
} else {
|
||||
throw new IllegalArgumentException("chromiumOptions类型只能为 String , ChromiumOptions, Integer, null");
|
||||
}
|
||||
this.chromiumPage = instance;
|
||||
this.setType("WebPage");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置的对象
|
||||
*/
|
||||
|
||||
public WebPageSetter set() {
|
||||
if (setter == null) setter = new WebPageSetter(this);
|
||||
return setter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前url
|
||||
*/
|
||||
@Override
|
||||
public String url() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return this.browserUrl();
|
||||
case s:
|
||||
return this.sessionUrl();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回浏览器当前url
|
||||
*/
|
||||
protected String browserUrl() {
|
||||
return this.chromiumPage.url();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前页面title
|
||||
*/
|
||||
public String title() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.title();
|
||||
case s:
|
||||
return sessionPage.title();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回页码原始数据数据
|
||||
*/
|
||||
public Object rawData() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
if (this.hasDriver) {
|
||||
return chromiumPage.html();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case s:
|
||||
return sessionPage.rawData();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String html() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
if (this.hasDriver) {
|
||||
return chromiumPage.html();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case s:
|
||||
return sessionPage.html();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject json() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.json();
|
||||
|
||||
case s:
|
||||
return sessionPage.json();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回 s 模式获取到的 Response 对象
|
||||
*/
|
||||
public Response response() {
|
||||
return sessionPage.response();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前模式,'s'或'd'
|
||||
*/
|
||||
public WebMode mode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回user agent
|
||||
*/
|
||||
public String ua() {
|
||||
return userAgent();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回user agent
|
||||
*/
|
||||
@Override
|
||||
public String userAgent() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.userAgent();
|
||||
case s:
|
||||
return sessionPage.userAgent();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回Session对象,如未初始化则按配置信息创建
|
||||
*/
|
||||
public OkHttpClient session() {
|
||||
if (sessionPage.session == null) this.sessionPage.createSession();
|
||||
return sessionPage.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回 session 保存的url
|
||||
*/
|
||||
private String sessionUrl() {
|
||||
try (Response response = this.sessionPage.response()) {
|
||||
if (response == null) return null;
|
||||
return response.request().url().toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回通用timeout设置
|
||||
*/
|
||||
public Double timeout() {
|
||||
return chromiumPage.timeouts.getBase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean get(String url, boolean showErrMsg, Integer retry, Double interval, Double timeout, Map<String, Object> params) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.get(url, showErrMsg, retry, interval, timeout, params);
|
||||
case s:
|
||||
timeout = timeout == null ? this.hasDriver ? chromiumPage.timeouts.getPageLoad() : this.timeout() : timeout;
|
||||
return sessionPage.get(url, showErrMsg, retry, interval, timeout, params);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url) {
|
||||
return this.post(url, null);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url, Map<String, Object> params) {
|
||||
return this.post(url, false, params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url, boolean showErrMsg, Map<String, Object> params) {
|
||||
return this.post(url, showErrMsg, null, params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
public Object post(@NotNull String url, boolean showErrMsg, Integer retry, Map<String, Object> params) {
|
||||
return this.post(url, showErrMsg, retry, null, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param interval 重试间隔(秒),为None时使用页面对象retry_interval属性值
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url, boolean showErrMsg, Integer retry, Double interval, Map<String, Object> params) {
|
||||
if (Objects.equals(mode, WebMode.d)) {
|
||||
this.cookiesToSession();
|
||||
sessionPage.post(url, showErrMsg, retry, interval, params);
|
||||
return this.response();
|
||||
} else {
|
||||
return sessionPage.post(url, showErrMsg, retry, interval, params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回第一个符合条件的元素、属性或节点文本
|
||||
*
|
||||
* @param by 元素的定位信息,可以是元素对象,by,或查询字符串
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象
|
||||
*/
|
||||
public DrissionElement<?, ?> ele(By by, int index, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.ele(by, index, timeout);
|
||||
case s:
|
||||
return sessionPage.ele(by, index, timeout);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个符合条件的元素、属性或节点文本
|
||||
*
|
||||
* @param locator 元素的定位信息,可以是元素对象,by,或查询字符串
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象
|
||||
*/
|
||||
public DrissionElement<?, ?> ele(String locator, int index, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.ele(locator, index, timeout);
|
||||
case s:
|
||||
return sessionPage.ele(locator, index, timeout);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中所有符合条件的元素
|
||||
*
|
||||
* @param locator 元素的定位信息,可以是by,或查询字符串
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象的列表
|
||||
*/
|
||||
public List<DrissionElement<?, ?>> eles(String locator, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumPage.eles(locator, timeout);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> sessionElements = sessionPage.eles(locator, timeout);
|
||||
return sessionElements != null ? new ArrayList<>(sessionElements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中所有符合条件的元素
|
||||
*
|
||||
* @param by 元素的定位信息,可以是by,或查询字符串
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象的列表
|
||||
*/
|
||||
public List<DrissionElement<?, ?>> eles(By by, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumPage.eles(by, timeout);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> sessionElements = sessionPage.eles(by, timeout);
|
||||
return sessionElements != null ? new ArrayList<>(sessionElements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @return SessionElement对象
|
||||
*/
|
||||
@Override
|
||||
public SessionElement sEle(By by, Integer index) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.sEle(by, index);
|
||||
case s:
|
||||
return sessionPage.sEle(by, index);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param loc 查询元素
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @return SessionElement对象
|
||||
*/
|
||||
@Override
|
||||
public SessionElement sEle(String loc, Integer index) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.sEle(loc, index);
|
||||
case s:
|
||||
return sessionPage.sEle(loc, index);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param by 元素的定位信息 查询元素
|
||||
* @return SessionElement对象集合
|
||||
*/
|
||||
@Override
|
||||
public List<SessionElement> sEles(By by) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.sEles(by);
|
||||
case s:
|
||||
return sessionPage.sEles(by);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param loc 元素的定位信息 查询元素
|
||||
* @return SessionElement对象集合
|
||||
*/
|
||||
@Override
|
||||
public List<SessionElement> sEles(String loc) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.sEles(loc);
|
||||
case s:
|
||||
return sessionPage.sEles(loc);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模式,接收's'或'd',除此以外的字符串会切换为 d 模式
|
||||
* 如copy_cookies为True,切换时会把当前模式的cookies复制到目标模式
|
||||
* 切换后,如果go是True,调用相应的get函数使访问的页面同步
|
||||
*
|
||||
* @param mode 模式
|
||||
*/
|
||||
public void changeMode(WebMode mode) {
|
||||
changeMode(mode, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模式,接收's'或'd',除此以外的字符串会切换为 d 模式
|
||||
* 如copy_cookies为True,切换时会把当前模式的cookies复制到目标模式
|
||||
* 切换后,如果go是True,调用相应的get函数使访问的页面同步
|
||||
*
|
||||
* @param mode 模式
|
||||
* @param go 是否跳转到原模式的url
|
||||
*/
|
||||
public void changeMode(WebMode mode, boolean go) {
|
||||
changeMode(mode, go, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模式,接收's'或'd',除此以外的字符串会切换为 d 模式
|
||||
* 如copy_cookies为True,切换时会把当前模式的cookies复制到目标模式
|
||||
* 切换后,如果go是True,调用相应的get函数使访问的页面同步
|
||||
*
|
||||
* @param mode 模式
|
||||
* @param go 是否跳转到原模式的url
|
||||
* @param copyCookies 是否复制cookies到目标模式
|
||||
*/
|
||||
public void changeMode(WebMode mode, boolean go, boolean copyCookies) {
|
||||
if (mode != null && Objects.equals(mode.mode, this.mode.mode)) return;
|
||||
this.mode = mode;
|
||||
//s模式转d模式
|
||||
if (this.mode.equals(WebMode.d)) {
|
||||
if (this.chromiumPage.driver == null) {
|
||||
this.chromiumPage.connectBrowser(null);
|
||||
this.url = this.hasDriver ? null : sessionPage.url();
|
||||
this.hasDriver = true;
|
||||
}
|
||||
String s = this.sessionUrl();
|
||||
if (s != null) {
|
||||
if (copyCookies) this.cookiesToBrowser();
|
||||
if (go) this.get(this.sessionUrl());
|
||||
}
|
||||
//d模式转s模式
|
||||
} else if (this.mode.equals(WebMode.s)) {
|
||||
this.hasSession = true;
|
||||
this.url = this.sessionUrl();
|
||||
if (this.hasDriver) {
|
||||
if (copyCookies) this.cookiesToSession();
|
||||
if (go && !this.get(sessionPage.url())) {
|
||||
throw new IllegalArgumentException("s模式访问失败,请设置go=False,自行构造连接参数进行访问。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 把driver对象的cookies复制到session对象
|
||||
*/
|
||||
public void cookiesToSession() {
|
||||
cookiesToSession(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把driver对象的cookies复制到session对象
|
||||
*
|
||||
* @param copyUserAgent 是否复制ua信息
|
||||
*/
|
||||
public void cookiesToSession(boolean copyUserAgent) {
|
||||
if (!this.hasSession) return;
|
||||
if (copyUserAgent) {
|
||||
Object o = JSON.parseObject(this.chromiumPage.runCdp("Runtime.evaluate", Map.of("expression", "navigator.userAgent;")).toString()).getJSONObject("result").get("value");
|
||||
this.sessionPage.headers.put("User-Agent", o);
|
||||
}
|
||||
Web.setBrowserCookies(this.getChromiumPage(), chromiumPage.cookies());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 把session对象的cookies复制到浏览器
|
||||
*/
|
||||
public void cookiesToBrowser() {
|
||||
if (!this.hasDriver) return;
|
||||
Web.setBrowserCookies(this.getChromiumPage(), sessionPage.cookies());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> cookies(boolean asMap, boolean allDomains, boolean allInfo) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumPage.cookies(asMap, allDomains, allInfo);
|
||||
case s:
|
||||
return sessionPage.cookies(asMap, allDomains, allInfo);
|
||||
default:
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签页对象
|
||||
*
|
||||
* @return 标签页对象
|
||||
*/
|
||||
public WebPageTab getTab() {
|
||||
return _getTab(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签页对象
|
||||
*
|
||||
* @param number 要获取的标签页id或序号,为null时获取当前tab,序号不是视觉排列顺序,而是激活顺序
|
||||
* @return 标签页对象
|
||||
*/
|
||||
public WebPageTab getTab(int number) {
|
||||
return _getTab(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签页对象
|
||||
*
|
||||
* @param id 要获取的标签页id或序号,为null时获取当前tab,序号不是视觉排列顺序,而是激活顺序
|
||||
* @return 标签页对象
|
||||
*/
|
||||
public WebPageTab getTab(String id) {
|
||||
return _getTab(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签页对象
|
||||
*
|
||||
* @param idOrNum 要获取的标签页id或序号,为null时获取当前tab,序号不是视觉排列顺序,而是激活顺序
|
||||
* @return 标签页对象
|
||||
*/
|
||||
private WebPageTab _getTab(Object idOrNum) {
|
||||
if (idOrNum instanceof String) {
|
||||
return new WebPageTab(this, idOrNum.toString());
|
||||
} else if (idOrNum instanceof Integer) {
|
||||
return new WebPageTab(this, chromiumPage.tabs().get((Integer) idOrNum));
|
||||
} else if (idOrNum == null) {
|
||||
return new WebPageTab(this, chromiumPage.tabId());
|
||||
} else if (idOrNum instanceof WebPageTab) {
|
||||
return (WebPageTab) idOrNum;
|
||||
} else {
|
||||
throw new ClassCastException("id_or_num需传入tab id或序号");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public WebPageTab newTab() {
|
||||
return newTab(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public WebPageTab newTab(String url) {
|
||||
return newTab(url, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public WebPageTab newTab(String url, boolean newWindow) {
|
||||
return newTab(url, newWindow, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @param background 是否不激活新标签页,如newWindow为True则无效
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public WebPageTab newTab(String url, boolean newWindow, boolean background) {
|
||||
return newTab(url, newWindow, background, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个标签页
|
||||
*
|
||||
* @param url 新标签页跳转到的网址
|
||||
* @param newWindow 是否在新窗口打开标签页
|
||||
* @param background 是否不激活新标签页,如newWindow为True则无效
|
||||
* @param newContext 是否创建新的上下文
|
||||
* @return 新标签页对象
|
||||
*/
|
||||
public WebPageTab newTab(String url, boolean newWindow, boolean background, boolean newContext) {
|
||||
WebPageTab webPageTab = new WebPageTab(this, chromiumPage._newTab(newWindow, background, newContext));
|
||||
if (url != null) webPageTab.get(url);
|
||||
return webPageTab;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭driver及浏览器 并且切换到d模式
|
||||
*/
|
||||
public void closeDriver() {
|
||||
if (this.hasDriver) {
|
||||
this.changeMode(WebMode.s);
|
||||
try {
|
||||
this.chromiumPage.runCdp("Browser.close");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
this.chromiumPage.driver.stop();
|
||||
this.chromiumPage.driver = null;
|
||||
this.hasDriver = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭session 并且切换到s模式
|
||||
*/
|
||||
public void closeSession() {
|
||||
if (this.hasDriver) {
|
||||
this.changeMode(WebMode.d);
|
||||
Response response = this.sessionPage.response;
|
||||
if (response != null) try {
|
||||
response.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
this.sessionPage.session = null;
|
||||
this.sessionPage.response = null;
|
||||
this.hasSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭标签页和Session
|
||||
*/
|
||||
|
||||
public void close() {
|
||||
if (this.hasDriver) this.chromiumPage.closeTabs(this.chromiumPage.tabId());
|
||||
if (this.sessionPage.session != null) {
|
||||
Response response = this.sessionPage.response;
|
||||
if (response != null) try {
|
||||
response.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中符合条件的元素、属性或节点文本,默认返回第一个
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象
|
||||
*/
|
||||
@Override
|
||||
protected List<DrissionElement<?, ?>> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumPage.findElements(by, timeout, index, relative, raiseErr);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> elements = sessionPage.findElements(by, timeout, index, relative, raiseErr);
|
||||
return elements != null ? new ArrayList<>(elements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中符合条件的元素、属性或节点文本,默认返回第一个
|
||||
*
|
||||
* @param loc 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象
|
||||
*/
|
||||
@Override
|
||||
protected List<DrissionElement<?, ?>> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumPage.findElements(loc, timeout, index, relative, raiseErr);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> elements = sessionPage.findElements(loc, timeout, index, relative, raiseErr);
|
||||
return elements != null ? new ArrayList<>(elements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器和Session
|
||||
*
|
||||
* @param timeout 等待浏览器关闭超时时间
|
||||
* @param force 关闭超时是否强制终止进程
|
||||
*/
|
||||
public void quit(Double timeout, boolean force) {
|
||||
if (this.hasSession) {
|
||||
this.sessionPage.close();
|
||||
this.sessionPage.session = null;
|
||||
this.sessionPage.response = null;
|
||||
this.hasSession = false;
|
||||
}
|
||||
if (this.hasDriver) {
|
||||
this.chromiumPage.quit(timeout, force);
|
||||
this.chromiumPage.driver = null;
|
||||
this.hasDriver = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<WebPage browserId=" + this.chromiumPage.browser().getId() + " tabId=" + this.chromiumPage.tabId() + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @param cloneNumber 克隆数量
|
||||
* @return 集合
|
||||
*/
|
||||
public List<WebPage> copy(int cloneNumber) {
|
||||
return IntStream.range(0, cloneNumber < 0 ? 1 : cloneNumber).mapToObj(i -> copy()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @return 单个
|
||||
*/
|
||||
public WebPage copy() {
|
||||
ChromiumOptions chromiumOptions = this.chromiumPage.getChromiumOptions().copy();
|
||||
chromiumOptions.autoPort(true, chromiumOptions.getTmpPath() + UUID.randomUUID().toString().substring(0, 5));
|
||||
WebPage webPage = new WebPage(this.mode, this.timeout(), chromiumOptions.copy(), this.sessionPage.sessionOptions.copy());
|
||||
String url1 = this.url();
|
||||
if (url1 != null) webPage.get(url1);
|
||||
return webPage;
|
||||
}
|
||||
}
|
632
java/src/main/java/com/ll/DrissonPage/page/WebPageTab.java
Normal file
632
java/src/main/java/com/ll/DrissonPage/page/WebPageTab.java
Normal file
@ -0,0 +1,632 @@
|
||||
package com.ll.DrissonPage.page;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.base.BasePage;
|
||||
import com.ll.DrissonPage.base.By;
|
||||
import com.ll.DrissonPage.base.DrissionElement;
|
||||
import com.ll.DrissonPage.config.SessionOptions;
|
||||
import com.ll.DrissonPage.element.ChromiumElement;
|
||||
import com.ll.DrissonPage.element.SessionElement;
|
||||
import com.ll.DrissonPage.functions.Web;
|
||||
import com.ll.DrissonPage.units.setter.WebPageTabSetter;
|
||||
import lombok.Getter;
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Response;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class WebPageTab extends BasePage<DrissionElement<?, ?>> {
|
||||
private WebMode mode;
|
||||
@Getter
|
||||
private WebPage page;
|
||||
@Getter
|
||||
private boolean hasDriver;
|
||||
@Getter
|
||||
private boolean hasSession;
|
||||
private final SessionPage sessionPage;
|
||||
private final ChromiumTab chromiumTab;
|
||||
private WebPageTabSetter setter;
|
||||
|
||||
public WebPageTab(WebPage page, String tabId) {
|
||||
this.mode = WebMode.d;
|
||||
this.hasDriver = true;
|
||||
this.hasSession = true;
|
||||
sessionPage = new SessionPage(new SessionOptions(false, null).fromSession(page.session(), page.sessionPage.headers));
|
||||
chromiumTab = new ChromiumTab(page.chromiumPage, tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回用于设置的对象
|
||||
*/
|
||||
public WebPageTabSetter set() {
|
||||
if (setter == null) setter = new WebPageTabSetter(this);
|
||||
return this.setter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String url() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return this.browserUrl();
|
||||
case s:
|
||||
return this.sessionUrl();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回浏览器当前url
|
||||
*/
|
||||
protected String browserUrl() {
|
||||
return this.chromiumTab.url();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前页面title
|
||||
*/
|
||||
public String title() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.title();
|
||||
case s:
|
||||
return sessionPage.title();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回页码原始数据数据
|
||||
*/
|
||||
public Object rawData() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
if (this.hasDriver) {
|
||||
return chromiumTab.html();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case s:
|
||||
return sessionPage.rawData();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String html() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
if (this.hasDriver) {
|
||||
return chromiumTab.html();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case s:
|
||||
return sessionPage.html();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject json() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.json();
|
||||
case s:
|
||||
return sessionPage.json();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回 s 模式获取到的 Response 对象
|
||||
*/
|
||||
public Response response() {
|
||||
return sessionPage.response();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回当前模式,'s'或'd'
|
||||
*/
|
||||
public WebMode mode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回user agent
|
||||
*/
|
||||
public String ua() {
|
||||
return userAgent();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回user agent
|
||||
*/
|
||||
@Override
|
||||
public String userAgent() {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.userAgent();
|
||||
case s:
|
||||
return sessionPage.userAgent();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回Session对象,如未初始化则按配置信息创建
|
||||
*/
|
||||
public OkHttpClient session() {
|
||||
if (sessionPage.session == null) this.sessionPage.createSession();
|
||||
return sessionPage.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回 session 保存的url
|
||||
*/
|
||||
private String sessionUrl() {
|
||||
try (Response response = this.sessionPage.response()) {
|
||||
if (response == null) return null;
|
||||
return response.request().url().toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回通用timeout设置
|
||||
*/
|
||||
public Double timeout() {
|
||||
return chromiumTab.timeouts.getBase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置通用超时时间
|
||||
*
|
||||
* @param second 秒
|
||||
*/
|
||||
public void setTimeout(Double second) {
|
||||
this.set().timeouts(second, null, null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Boolean get(String url, boolean showErrMsg, Integer retry, Double interval, Double timeout, Map<String, Object> params) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.get(url, showErrMsg, retry, interval, timeout, params);
|
||||
case s:
|
||||
timeout = timeout == null ? this.hasDriver ? chromiumTab.timeouts.getPageLoad() : this.timeout() : timeout;
|
||||
return sessionPage.get(url, showErrMsg, retry, interval, timeout, params);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url) {
|
||||
return this.post(url, null);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url, Map<String, Object> params) {
|
||||
return this.post(url, false, params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url, boolean showErrMsg, Map<String, Object> params) {
|
||||
return this.post(url, showErrMsg, null, params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
public Object post(@NotNull String url, boolean showErrMsg, Integer retry, Map<String, Object> params) {
|
||||
return this.post(url, showErrMsg, retry, null, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用post方式跳转到url 会切换到s模式
|
||||
*
|
||||
* @param url 目标url
|
||||
* @param showErrMsg 是否显示和抛出异常
|
||||
* @param retry 重试次数,为None时使用页面对象retry_times属性值
|
||||
* @param interval 重试间隔(秒),为None时使用页面对象retry_interval属性值
|
||||
* @param params 连接参数
|
||||
* @return s模式时返回url是否可用,d模式时返回获取到的Response对象
|
||||
*/
|
||||
|
||||
public Object post(@NotNull String url, boolean showErrMsg, Integer retry, Double interval, Map<String, Object> params) {
|
||||
if (Objects.equals(mode, WebMode.d)) {
|
||||
this.cookiesToSession();
|
||||
sessionPage.post(url, showErrMsg, retry, interval, params);
|
||||
return this.response();
|
||||
} else {
|
||||
return sessionPage.post(url, showErrMsg, retry, interval, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个符合条件的元素、属性或节点文本
|
||||
*
|
||||
* @param by 元素的定位信息,可以是元素对象,by,或查询字符串
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象
|
||||
*/
|
||||
public DrissionElement<?, ?> ele(By by, int index, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.ele(by, index, timeout);
|
||||
case s:
|
||||
return sessionPage.ele(by, index, timeout);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个符合条件的元素、属性或节点文本
|
||||
*
|
||||
* @param locator 元素的定位信息,可以是元素对象,by,或查询字符串
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象
|
||||
*/
|
||||
public DrissionElement<?, ?> ele(String locator, int index, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.ele(locator, index, timeout);
|
||||
case s:
|
||||
return sessionPage.ele(locator, index, timeout);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中所有符合条件的元素
|
||||
*
|
||||
* @param locator 元素的定位信息,可以是by,或查询字符串
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象的列表
|
||||
*/
|
||||
public List<DrissionElement<?, ?>> eles(String locator, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumTab.eles(locator, timeout);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> sessionElements = sessionPage.eles(locator, timeout);
|
||||
return sessionElements != null ? new ArrayList<>(sessionElements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中所有符合条件的元素
|
||||
*
|
||||
* @param by 元素的定位信息,可以是by,或查询字符串
|
||||
* @param timeout 查找元素超时时间(秒),默认与页面等待时间一致
|
||||
* @return 元素对象的列表
|
||||
*/
|
||||
public List<DrissionElement<?, ?>> eles(By by, Double timeout) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumTab.eles(by, timeout);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> sessionElements = sessionPage.eles(by, timeout);
|
||||
return sessionElements != null ? new ArrayList<>(sessionElements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @return SessionElement对象
|
||||
*/
|
||||
@Override
|
||||
public SessionElement sEle(By by, Integer index) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.sEle(by, index);
|
||||
case s:
|
||||
return sessionPage.sEle(by, index);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param loc 查询元素
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个
|
||||
* @return SessionElement对象
|
||||
*/
|
||||
@Override
|
||||
public SessionElement sEle(String loc, Integer index) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.sEle(loc, index);
|
||||
case s:
|
||||
return sessionPage.sEle(loc, index);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param by 元素的定位信息 查询元素
|
||||
* @return SessionElement对象集合
|
||||
*/
|
||||
@Override
|
||||
public List<SessionElement> sEles(By by) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.sEles(by);
|
||||
case s:
|
||||
return sessionPage.sEles(by);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有符合条件的元素以SessionElement形式返回,d模式处理复杂页面时效率很高
|
||||
*
|
||||
* @param loc 元素的定位信息 查询元素
|
||||
* @return SessionElement对象集合
|
||||
*/
|
||||
@Override
|
||||
public List<SessionElement> sEles(String loc) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.sEles(loc);
|
||||
case s:
|
||||
return sessionPage.sEles(loc);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模式,接收's'或'd',除此以外的字符串会切换为 d 模式
|
||||
* 如copy_cookies为True,切换时会把当前模式的cookies复制到目标模式
|
||||
* 切换后,如果go是True,调用相应的get函数使访问的页面同步
|
||||
*
|
||||
* @param mode 模式
|
||||
*/
|
||||
public void changeMode(WebMode mode) {
|
||||
changeMode(mode, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模式,接收's'或'd',除此以外的字符串会切换为 d 模式
|
||||
* 如copy_cookies为True,切换时会把当前模式的cookies复制到目标模式
|
||||
* 切换后,如果go是True,调用相应的get函数使访问的页面同步
|
||||
*
|
||||
* @param mode 模式
|
||||
* @param go 是否跳转到原模式的url
|
||||
*/
|
||||
public void changeMode(WebMode mode, boolean go) {
|
||||
changeMode(mode, go, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模式,接收's'或'd',除此以外的字符串会切换为 d 模式
|
||||
* 如copy_cookies为True,切换时会把当前模式的cookies复制到目标模式
|
||||
* 切换后,如果go是True,调用相应的get函数使访问的页面同步
|
||||
*
|
||||
* @param mode 模式
|
||||
* @param go 是否跳转到原模式的url
|
||||
* @param copyCookies 是否复制cookies到目标模式
|
||||
*/
|
||||
public void changeMode(WebMode mode, boolean go, boolean copyCookies) {
|
||||
if (mode != null && Objects.equals(mode.mode, this.mode.mode)) return;
|
||||
this.mode = mode;
|
||||
//s模式转d模式
|
||||
if (this.mode.equals(WebMode.d)) {
|
||||
if (this.chromiumTab.driver == null) {
|
||||
this.chromiumTab.connectBrowser(null);
|
||||
this.url = this.hasDriver ? null : sessionPage.url();
|
||||
this.hasDriver = true;
|
||||
}
|
||||
String s = this.sessionUrl();
|
||||
if (s != null) {
|
||||
if (copyCookies) this.cookiesToBrowser();
|
||||
if (go) this.get(this.sessionUrl());
|
||||
}
|
||||
//d模式转s模式
|
||||
} else if (this.mode.equals(WebMode.s)) {
|
||||
this.hasSession = true;
|
||||
this.url = this.sessionUrl();
|
||||
if (this.hasDriver) {
|
||||
if (copyCookies) this.cookiesToSession();
|
||||
if (go && !this.get(sessionPage.url())) {
|
||||
throw new IllegalArgumentException("s模式访问失败,请设置go=False,自行构造连接参数进行访问。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把driver对象的cookies复制到session对象
|
||||
*/
|
||||
public void cookiesToSession() {
|
||||
cookiesToSession(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 把driver对象的cookies复制到session对象
|
||||
*
|
||||
* @param copyUserAgent 是否复制ua信息
|
||||
*/
|
||||
public void cookiesToSession(boolean copyUserAgent) {
|
||||
if (!this.hasSession) return;
|
||||
if (copyUserAgent) {
|
||||
String o = JSON.parseObject(this.chromiumTab.runCdp("Runtime.evaluate", Map.of("expression", "navigator.userAgent;")).toString()).getJSONObject("result").getString("value");
|
||||
this.sessionPage.headers.put("User-Agent", o);
|
||||
}
|
||||
Web.setBrowserCookies(this.chromiumTab.page(), chromiumTab.cookies());
|
||||
}
|
||||
|
||||
/**
|
||||
* 把session对象的cookies复制到浏览器
|
||||
*/
|
||||
public void cookiesToBrowser() {
|
||||
if (!this.hasDriver) return;
|
||||
Web.setBrowserCookies(this.chromiumTab.page(), sessionPage.cookies());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> cookies(boolean asMap, boolean allDomains, boolean allInfo) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
return chromiumTab.cookies(asMap, allDomains, allInfo);
|
||||
case s:
|
||||
return sessionPage.cookies(asMap, allDomains, allInfo);
|
||||
default:
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前标签页
|
||||
*/
|
||||
|
||||
public void close() {
|
||||
this.chromiumTab.close();
|
||||
if (this.sessionPage.session != null) {
|
||||
Response response = this.sessionPage.response;
|
||||
if (response != null) try {
|
||||
response.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中符合条件的元素、属性或节点文本,默认返回第一个
|
||||
*
|
||||
* @param by 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象
|
||||
*/
|
||||
@Override
|
||||
protected List<DrissionElement<?, ?>> findElements(By by, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumTab.findElements(by, timeout, index, relative, raiseErr);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> elements = sessionPage.findElements(by, timeout, index, relative, raiseErr);
|
||||
return elements != null ? new ArrayList<>(elements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回页面中符合条件的元素、属性或节点文本,默认返回第一个
|
||||
*
|
||||
* @param loc 查询元素
|
||||
* @param timeout 查找超时时间(秒)
|
||||
* @param index 获取第几个,从1开始,可传入负数获取倒数第几个 如果是null则是返回全部
|
||||
* @param relative WebPage用的表示是否相对定位的参数
|
||||
* @param raiseErr 找不到元素是是否抛出异常,为null时根据全局设置
|
||||
* @return 元素对象
|
||||
*/
|
||||
@Override
|
||||
protected List<DrissionElement<?, ?>> findElements(String loc, Double timeout, Integer index, Boolean relative, Boolean raiseErr) {
|
||||
switch (mode) {
|
||||
case d:
|
||||
List<ChromiumElement> chromiumElements = chromiumTab.findElements(loc, timeout, index, relative, raiseErr);
|
||||
return chromiumElements != null ? new ArrayList<>(chromiumElements) : null;
|
||||
case s:
|
||||
List<SessionElement> elements = sessionPage.findElements(loc, timeout, index, relative, raiseErr);
|
||||
return elements != null ? new ArrayList<>(elements) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @param cloneNumber 克隆数量
|
||||
* @return 集合
|
||||
*/
|
||||
public List<WebPageTab> copy(int cloneNumber) {
|
||||
return IntStream.range(0, cloneNumber < 0 ? 1 : cloneNumber).mapToObj(i -> copy()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆
|
||||
*
|
||||
* @return 单个
|
||||
*/
|
||||
public WebPageTab copy() {
|
||||
return this.page.newTab(this.url());
|
||||
}
|
||||
}
|
1084
java/src/main/java/com/ll/DrissonPage/units/Actions.java
Normal file
1084
java/src/main/java/com/ll/DrissonPage/units/Actions.java
Normal file
File diff suppressed because it is too large
Load Diff
17
java/src/main/java/com/ll/DrissonPage/units/ClickAction.java
Normal file
17
java/src/main/java/com/ll/DrissonPage/units/ClickAction.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.ll.DrissonPage.units;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public enum ClickAction {
|
||||
LEFT("left"), RIGHT("right"), MIDDLE("middle"), BACK("back"), FORWARD("forward");
|
||||
private final String value;
|
||||
|
||||
ClickAction(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
436
java/src/main/java/com/ll/DrissonPage/units/Clicker.java
Normal file
436
java/src/main/java/com/ll/DrissonPage/units/Clicker.java
Normal file
@ -0,0 +1,436 @@
|
||||
package com.ll.DrissonPage.units;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.element.ChromiumElement;
|
||||
import com.ll.DrissonPage.error.extend.AlertExistsError;
|
||||
import com.ll.DrissonPage.error.extend.CDPError;
|
||||
import com.ll.DrissonPage.error.extend.CanNotClickError;
|
||||
import com.ll.DrissonPage.error.extend.NoRectError;
|
||||
import com.ll.DrissonPage.functions.Settings;
|
||||
import com.ll.DrissonPage.functions.Web;
|
||||
import com.ll.DrissonPage.page.ChromiumTab;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class Clicker {
|
||||
private final ChromiumElement ele;
|
||||
|
||||
public Clicker(ChromiumElement ele) {
|
||||
this.ele = ele;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
*
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean click() {
|
||||
return click(1.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
* 如果遇到遮挡,可选择是否用js点击
|
||||
*
|
||||
* @param timeout 模拟点击的超时时间(秒),等待元素可见、可用、进入视口
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean click(Double timeout) {
|
||||
return click(timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
* 如果遇到遮挡,可选择是否用js点击
|
||||
*
|
||||
* @param timeout 模拟点击的超时时间(秒),等待元素可见、可用、进入视口
|
||||
* @param waitStop 是否等待元素运动结束再执行点击
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean click(Double timeout, boolean waitStop) {
|
||||
return left(false, timeout, waitStop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
* 如果遇到遮挡,可选择是否用js点击
|
||||
*
|
||||
* @param byJs 是否用js点击,为None时先用模拟点击,遇到遮挡改用js,为True时直接用js点击,为False时只用模拟点击
|
||||
* @param timeout 模拟点击的超时时间(秒),等待元素可见、可用、进入视口
|
||||
* @param waitStop 是否等待元素运动结束再执行点击
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean click(Boolean byJs, Double timeout, boolean waitStop) {
|
||||
return left(byJs, timeout, waitStop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
*
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean left() {
|
||||
return left(1.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
* 如果遇到遮挡,可选择是否用js点击
|
||||
*
|
||||
* @param timeout 模拟点击的超时时间(秒),等待元素可见、可用、进入视口
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean left(Double timeout) {
|
||||
return left(timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
* 如果遇到遮挡,可选择是否用js点击
|
||||
*
|
||||
* @param timeout 模拟点击的超时时间(秒),等待元素可见、可用、进入视口
|
||||
* @param waitStop 是否等待元素运动结束再执行点击
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
public boolean left(Double timeout, boolean waitStop) {
|
||||
return left(false, timeout, waitStop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击元素
|
||||
* 如果遇到遮挡,可选择是否用js点击
|
||||
*
|
||||
* @param byJs 是否用js点击,为null时先用模拟点击,遇到遮挡改用js,为True时直接用js点击,为False时只用模拟点击
|
||||
* @param timeout 模拟点击的超时时间(秒),等待元素可见、可用、进入视口
|
||||
* @param waitStop 是否等待元素运动结束再执行点击
|
||||
* @return 是否点击成功 如果是select选择器则不返回值
|
||||
*/
|
||||
public Boolean left(Boolean byJs, Double timeout, boolean waitStop) {
|
||||
if (Objects.equals(this.ele.tag(), "option")) {
|
||||
if (this.ele.states().isSelected()) {
|
||||
this.ele.parent("t:select").select().cancelByOption(this.ele);
|
||||
} else {
|
||||
this.ele.parent("t:select").select().byOption(this.ele);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (byJs == null || !byJs) {
|
||||
boolean can_click = false;
|
||||
timeout = timeout == null ? this.ele.getOwner().timeout() : timeout;
|
||||
List<Coordinate> rect = null;
|
||||
if (timeout == 0) try {
|
||||
this.ele.scroll().toSee();
|
||||
if (this.ele.states().isEnabled() && this.ele.states().isDisplayed()) {
|
||||
rect = this.ele.rect().viewportCorners();
|
||||
can_click = true;
|
||||
}
|
||||
} catch (NoRectError e) {
|
||||
if (Boolean.FALSE.equals(byJs)) throw e;
|
||||
}
|
||||
else {
|
||||
rect = this.ele.states().hasRect();
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (rect == null && endTime > System.currentTimeMillis()) {
|
||||
rect = this.ele.states().hasRect();
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (waitStop && rect != null && !rect.isEmpty()) {
|
||||
this.ele.waits().stopMoving(.1, (double) (endTime - System.currentTimeMillis()));
|
||||
}
|
||||
if (rect != null && !rect.isEmpty()) {
|
||||
this.ele.scroll().toSee();
|
||||
rect = this.ele.rect().corners();
|
||||
while (endTime > System.currentTimeMillis()) {
|
||||
if (this.ele.states().isEnabled() && this.ele.states().isDisplayed()) {
|
||||
can_click = true;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} else if (Boolean.FALSE.equals(byJs)) throw new NoRectError();
|
||||
|
||||
}
|
||||
if (can_click && !this.ele.states().isInViewport()) {
|
||||
byJs = true;
|
||||
} else if (can_click && (Boolean.FALSE.equals(byJs) || this.ele.states().isCovered() == null)) {
|
||||
Coordinate coordinate = new Coordinate(rect.get(1).getX() - (rect.get(1).getX() - rect.get(0).getX()) / 2, rect.get(0).getX() + 3);
|
||||
try {
|
||||
JSONObject r = JSON.parseObject(this.ele.getOwner().runCdp("DOM.getNodeForLocation", Map.of("x", coordinate.getX(), "y", coordinate.getY(), "includeUserAgentShadowDOM", true, "ignorePointerEventsNone", true)).toString());
|
||||
if (!Objects.equals(r.getInteger("backendNodeId"), this.ele.getBackendId())) {
|
||||
coordinate = this.ele.rect().viewportMidpoint();
|
||||
} else {
|
||||
coordinate = this.ele.rect().viewportClickPoint();
|
||||
}
|
||||
} catch (CDPError e) {
|
||||
coordinate = this.ele.rect().viewportMidpoint();
|
||||
}
|
||||
this._click(coordinate, ClickAction.LEFT, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(byJs)) {
|
||||
this.ele.runJs("this.click();");
|
||||
return true;
|
||||
}
|
||||
if (Settings.raiseWhenClickFailed) {
|
||||
throw new CanNotClickError();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 右键单击
|
||||
*/
|
||||
public void right() {
|
||||
middle(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 右键点击
|
||||
*
|
||||
* @param count 次数
|
||||
*/
|
||||
public void right(int count) {
|
||||
this.ele.getOwner().scroll().toSee(this.ele);
|
||||
Coordinate coordinate = this.ele.rect().viewportClickPoint();
|
||||
this._click(coordinate, ClickAction.RIGHT, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中键单击
|
||||
*/
|
||||
public void middle() {
|
||||
middle(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中键点击
|
||||
*
|
||||
* @param count 次数
|
||||
*/
|
||||
public void middle(int count) {
|
||||
this.ele.getOwner().scroll().toSee(this.ele);
|
||||
Coordinate coordinate = this.ele.rect().viewportClickPoint();
|
||||
this._click(coordinate, ClickAction.LEFT, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点
|
||||
*/
|
||||
public void at() {
|
||||
at(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点
|
||||
*
|
||||
* @param count 点击次数
|
||||
*/
|
||||
public void at(int count) {
|
||||
at(ClickAction.LEFT, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点
|
||||
*
|
||||
* @param button 点击哪个键,可选 left, middle, right, back, forward
|
||||
* @param count 点击次数
|
||||
*/
|
||||
public void at(ClickAction button, int count) {
|
||||
at(null, null, button, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点
|
||||
*
|
||||
* @param offset_x 相对元素左上角坐标的x轴偏移量
|
||||
* @param offset_y 相对元素左上角坐标的y轴偏移量
|
||||
*/
|
||||
public void at(Integer offset_x, Integer offset_y) {
|
||||
at(offset_x, offset_y, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点
|
||||
*
|
||||
* @param offset_x 相对元素左上角坐标的x轴偏移量
|
||||
* @param offset_y 相对元素左上角坐标的y轴偏移量
|
||||
* @param count 点击次数
|
||||
*/
|
||||
public void at(Integer offset_x, Integer offset_y, int count) {
|
||||
at(offset_x, offset_y, ClickAction.LEFT, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点
|
||||
*
|
||||
* @param offset_x 相对元素左上角坐标的x轴偏移量
|
||||
* @param offset_y 相对元素左上角坐标的y轴偏移量
|
||||
* @param button 点击哪个键,可选 left, middle, right, back, forward
|
||||
* @param count 点击次数
|
||||
*/
|
||||
public void at(Integer offset_x, Integer offset_y, ClickAction button, int count) {
|
||||
this.ele.getOwner().scroll().toSee(this.ele);
|
||||
if (offset_x == null && offset_y == null) {
|
||||
Coordinate size = this.ele.rect().size();
|
||||
offset_x = size.getX() / 2;
|
||||
offset_y = size.getY() / 2;
|
||||
}
|
||||
this._click(Web.offsetScroll(this.ele, offset_x, offset_y), button, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多次点击
|
||||
*/
|
||||
public void multi() {
|
||||
multi(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多次点击
|
||||
*
|
||||
* @param times 默认双击
|
||||
*/
|
||||
public void multi(int times) {
|
||||
this.at(null, null, ClickAction.LEFT, times);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
*/
|
||||
public void toUpload(Path filePaths) {
|
||||
this.toUpload(filePaths, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
* @param byJs 是否用js方式点击,逻辑与click()一致
|
||||
*/
|
||||
public void toUpload(Path filePaths, boolean byJs) {
|
||||
this.ele.getOwner().set().uploadFiles(filePaths);
|
||||
this.left(1.5, byJs);
|
||||
this.ele.getOwner().waits().uploadPathsInputted();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
*/
|
||||
public void toUpload(String filePaths) {
|
||||
this.toUpload(filePaths, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
* @param byJs 是否用js方式点击,逻辑与click()一致
|
||||
*/
|
||||
public void toUpload(String filePaths, boolean byJs) {
|
||||
this.ele.getOwner().set().uploadFiles(filePaths);
|
||||
this.left(1.5, byJs);
|
||||
this.ele.getOwner().waits().uploadPathsInputted();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
*/
|
||||
public void toUpload(String[] filePaths) {
|
||||
this.toUpload(filePaths, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
* @param byJs 是否用js方式点击,逻辑与click()一致
|
||||
*/
|
||||
public void toUpload(String[] filePaths, boolean byJs) {
|
||||
this.ele.getOwner().set().uploadFiles(filePaths);
|
||||
this.left(1.5, byJs);
|
||||
this.ele.getOwner().waits().uploadPathsInputted();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
*/
|
||||
public void toUpload(Collection<String> filePaths) {
|
||||
this.toUpload(filePaths, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发上传文件选择框并自动填入指定路径
|
||||
*
|
||||
* @param filePaths 文件路径,如果上传框支持多文件,可传入列表或字符串,字符串时多个文件用回车分隔
|
||||
* @param byJs 是否用js方式点击,逻辑与click()一致
|
||||
*/
|
||||
public void toUpload(Collection<String> filePaths, boolean byJs) {
|
||||
this.ele.getOwner().set().uploadFiles(filePaths);
|
||||
this.left(1.5, byJs);
|
||||
this.ele.getOwner().waits().uploadPathsInputted();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 点击后等待新tab出现并返回其对象
|
||||
*
|
||||
* @return 新标签页对象,如果没有等到新标签页出现则抛出异常
|
||||
*/
|
||||
public ChromiumTab forNewTab() {
|
||||
return forNewTab(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击后等待新tab出现并返回其对象
|
||||
*
|
||||
* @param byJs 是否使用js点击,逻辑与click()一致
|
||||
* @return 新标签页对象,如果没有等到新标签页出现则抛出异常
|
||||
*/
|
||||
public ChromiumTab forNewTab(boolean byJs) {
|
||||
this.left(1.5, byJs);
|
||||
String tid = this.ele.getOwner().getPage().waits().newTab();
|
||||
if (tid == null) throw new RuntimeException("没有出现新标签页。");
|
||||
return this.ele.getOwner().getPage().getTab(tid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实施点击
|
||||
*
|
||||
* @param coordinate 视口中的坐标
|
||||
* @param button 'left' 'right' 'middle' 'back' 'forward'
|
||||
* @param count 点击次数
|
||||
*/
|
||||
|
||||
private void _click(Coordinate coordinate, ClickAction button, int count) {
|
||||
this.ele.getOwner().runCdp("Input.dispatchMouseEvent", Map.of("type", "mousePressed", "x", coordinate.getX(), "y", coordinate.getY(), "button", button.getValue(), "clickCount", count, "_ignore", new AlertExistsError()));
|
||||
this.ele.getOwner().runCdp("Input.dispatchMouseEvent", Map.of("type", "mouseReleased", "x", coordinate.getX(), "y", coordinate.getY(), "button", button.getValue(), "_ignore", new AlertExistsError()));
|
||||
}
|
||||
|
||||
|
||||
}
|
35
java/src/main/java/com/ll/DrissonPage/units/Coordinate.java
Normal file
35
java/src/main/java/com/ll/DrissonPage/units/Coordinate.java
Normal file
@ -0,0 +1,35 @@
|
||||
package com.ll.DrissonPage.units;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 坐标
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class Coordinate {
|
||||
/**
|
||||
* 横坐标
|
||||
*/
|
||||
private Integer x;
|
||||
/**
|
||||
* 纵坐标
|
||||
*/
|
||||
private Integer y;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Coordinate)) return false;
|
||||
Coordinate that = (Coordinate) o;
|
||||
return Objects.equals(x, that.x) && Objects.equals(y, that.y);
|
||||
}
|
||||
|
||||
}
|
19
java/src/main/java/com/ll/DrissonPage/units/HttpClient.java
Normal file
19
java/src/main/java/com/ll/DrissonPage/units/HttpClient.java
Normal file
@ -0,0 +1,19 @@
|
||||
package com.ll.DrissonPage.units;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.apache.http.Header;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class HttpClient {
|
||||
private OkHttpClient client;
|
||||
private Collection<? extends Header> headers;
|
||||
}
|
16
java/src/main/java/com/ll/DrissonPage/units/PicType.java
Normal file
16
java/src/main/java/com/ll/DrissonPage/units/PicType.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.ll.DrissonPage.units;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PicType {
|
||||
JPG("jpg"), JPEG("jpeg"), PNG("png"), WEBP("webp"), DEFAULT("png");
|
||||
final String value;
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.ll.DrissonPage.units.cookiesSetter;
|
||||
|
||||
import com.ll.DrissonPage.functions.Web;
|
||||
import com.ll.DrissonPage.page.ChromiumBase;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class CookiesSetter {
|
||||
private final ChromiumBase page;
|
||||
|
||||
/**
|
||||
* 删除一个cookie
|
||||
*
|
||||
* @param name cookie的name字段
|
||||
*/
|
||||
public void remove(String name) {
|
||||
remove(name, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个cookie
|
||||
*
|
||||
* @param name cookie的name字段
|
||||
* @param url cookie的url字段,可选
|
||||
*/
|
||||
public void remove(String name, String url) {
|
||||
remove(name, url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个cookie
|
||||
*
|
||||
* @param name cookie的name字段
|
||||
* @param url cookie的url字段,可选
|
||||
* @param domain cookie的domain字段,可选
|
||||
*/
|
||||
public void remove(String name, String url, String domain) {
|
||||
remove(name, url, domain, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个cookie
|
||||
*
|
||||
* @param name cookie的name字段
|
||||
* @param url cookie的url字段,可选
|
||||
* @param domain cookie的domain字段,可选
|
||||
* @param path cookie的path字段,可选
|
||||
*/
|
||||
public void remove(String name, String url, String domain, String path) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", name);
|
||||
if (url != null && !url.isEmpty()) map.put("url", url);
|
||||
if (domain != null && !domain.isEmpty()) map.put("domain", url);
|
||||
if (path != null && !path.isEmpty()) map.put("path", url);
|
||||
this.page.runCdp("Network.deleteCookies", map);
|
||||
}
|
||||
|
||||
public void add(Map<String, Object> cookies) {
|
||||
List<Map<String, Object>> cookies1 = new ArrayList<>();
|
||||
cookies1.add(cookies);
|
||||
Web.setBrowserCookies(this.page, cookies1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除cookies
|
||||
*/
|
||||
public void clear() {
|
||||
this.page.runCdp("Network.clearBrowserCookies");
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.ll.DrissonPage.units.cookiesSetter;
|
||||
|
||||
import com.ll.DrissonPage.page.SessionPage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.CookieJar;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class SessionCookiesSetter {
|
||||
private final SessionPage page;
|
||||
|
||||
/**
|
||||
* 删除一个cookie
|
||||
*
|
||||
* @param name cookie的name字段
|
||||
*/
|
||||
public void remove(String name) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
OkHttpClient.Builder builder = this.page.session().newBuilder();
|
||||
builder.setCookieJar$okhttp(new CookieJar() {
|
||||
@Override
|
||||
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
|
||||
list.stream().filter(cookie -> cookie.name().equals(name)).findFirst().ifPresent(list::remove);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
});
|
||||
this.page.setSession(builder.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除cookies
|
||||
*/
|
||||
public void clear() {
|
||||
OkHttpClient.Builder builder = this.page.session().newBuilder();
|
||||
builder.setCookieJar$okhttp(new CookieJar() {
|
||||
@Override
|
||||
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
|
||||
list.clear();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
});
|
||||
this.page.setSession(builder.build());
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.ll.DrissonPage.units.cookiesSetter;
|
||||
|
||||
import com.ll.DrissonPage.page.WebMode;
|
||||
import com.ll.DrissonPage.page.WebPage;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class WebPageCookiesSetter extends CookiesSetter {
|
||||
private final WebPage page;
|
||||
private final SessionCookiesSetter sessionCookiesSetter;
|
||||
|
||||
public WebPageCookiesSetter(WebPage page) {
|
||||
super(page.getChromiumPage());
|
||||
this.page = page;
|
||||
sessionCookiesSetter = new SessionCookiesSetter(page.getSessionPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个cookie
|
||||
*
|
||||
* @param name cookie的name字段
|
||||
* @param url cookie的url字段,可选 d模式时才有效
|
||||
* @param domain cookie的domain字段,可选 d模式时才有效
|
||||
* @param path cookie的path字段,可选 d模式时才有效
|
||||
*/
|
||||
|
||||
public void remove(String name, String url, String domain, String path) {
|
||||
if (Objects.equals(this.page.mode(), WebMode.d) && this.page.isHasDriver())
|
||||
super.remove(name, url, domain, path);
|
||||
else if (Objects.equals(this.page.mode(), WebMode.s) && this.page.isHasSession())
|
||||
sessionCookiesSetter.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除cookies
|
||||
*/
|
||||
public void clear() {
|
||||
if (Objects.equals(this.page.mode(), WebMode.d) && this.page.isHasDriver()) super.clear();
|
||||
else if (Objects.equals(this.page.mode(), WebMode.s) && this.page.isHasSession()) sessionCookiesSetter.clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,371 @@
|
||||
package com.ll.DrissonPage.units.downloader;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ll.DrissonPage.base.Browser;
|
||||
import com.ll.DrissonPage.base.MyRunnable;
|
||||
import com.ll.DrissonPage.page.ChromiumBase;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
* @original DrissionPage
|
||||
*/
|
||||
public class DownloadManager {
|
||||
private final Browser browser;
|
||||
private ChromiumBase page;
|
||||
/**
|
||||
* 返回所有未完成的下载任务
|
||||
*/
|
||||
@Getter
|
||||
private Map<String, DownloadMission> missions;
|
||||
private Map<String, List<DownloadMission>> tabMissions;
|
||||
private Map<String, Object> flags;
|
||||
private boolean running;
|
||||
private String savePath;
|
||||
private String whenDownloadFileExists;
|
||||
|
||||
|
||||
public DownloadManager(Browser browser) {
|
||||
this.browser = browser;
|
||||
this.page = browser.getPage();
|
||||
this.whenDownloadFileExists = "rename";
|
||||
TabDownloadSettings tabDownloadSettings = TabDownloadSettings.getInstance(this.page.tabId());
|
||||
tabDownloadSettings.path = this.page.downloadPath();
|
||||
this.missions = new HashMap<>();
|
||||
this.tabMissions = new HashMap<>();
|
||||
this.flags = new HashMap<>();
|
||||
String s = this.page.downloadPath();
|
||||
if (s != null && !s.isEmpty()) {
|
||||
this.browser.getDriver().setCallback("Browser.downloadProgress", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onDownloadProgress(getMessage());
|
||||
}
|
||||
});
|
||||
this.browser.getDriver().setCallback("Browser.downloadWillBegin", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onDownloadWillBegin(getMessage());
|
||||
}
|
||||
});
|
||||
String string = this.browser.runCdp("Browser.setDownloadBehavior", Map.of("downloadPath", this.page.downloadPath(), "behavior", "allowAndName", "eventsEnabled", true)).toString();
|
||||
if (JSON.parseObject(string).containsKey("error")) {
|
||||
System.out.println("浏览器版本太低无法使用下载管理功能。");
|
||||
}
|
||||
this.running = true;
|
||||
} else {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab的下载路径
|
||||
*
|
||||
* @param tab 页面对象
|
||||
* @param path 下载路径(绝对路径str)
|
||||
*/
|
||||
public void setPath(ChromiumBase tab, String path) {
|
||||
TabDownloadSettings.getInstance(tab.tabId()).path = path;
|
||||
if (this.page.equals(tab) || !this.running) {
|
||||
this.browser.getDriver().setCallback("Browser.downloadProgress", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onDownloadProgress(getMessage());
|
||||
}
|
||||
});
|
||||
this.browser.getDriver().setCallback("Browser.downloadWillBegin", new MyRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onDownloadWillBegin(getMessage());
|
||||
}
|
||||
});
|
||||
String string = this.browser.runCdp("Browser.setDownloadBehavior", Map.of("downloadPath", this.page.downloadPath(), "behavior", "allowAndName", "eventsEnabled", true)).toString();
|
||||
this.savePath = path;
|
||||
if (JSON.parseObject(string).containsKey("error")) {
|
||||
System.out.println("浏览器版本太低无法使用下载管理功能。");
|
||||
}
|
||||
}
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab的重命名文件名
|
||||
*
|
||||
* @param tabId tab id
|
||||
*/
|
||||
public void setRename(String tabId) {
|
||||
setRename(tabId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab的重命名文件名
|
||||
*
|
||||
* @param tabId tab id
|
||||
* @param rename 文件名,可不含后缀,会自动使用远程文件后缀
|
||||
*/
|
||||
public void setRename(String tabId, String rename) {
|
||||
setRename(tabId, rename, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab的重命名文件名
|
||||
*
|
||||
* @param tabId tab id
|
||||
* @param rename 文件名,可不含后缀,会自动使用远程文件后缀
|
||||
* @param suffix 后缀名,显式设置后缀名,不使用远程文件后缀
|
||||
*/
|
||||
public void setRename(String tabId, String rename, String suffix) {
|
||||
TabDownloadSettings instance = TabDownloadSettings.getInstance(tabId);
|
||||
instance.rename = rename;
|
||||
instance.suffix = suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab下载文件重名时执行的策略
|
||||
*
|
||||
* @param tabId 下载路径
|
||||
* @param mode 下载路径
|
||||
*/
|
||||
public void setFileExists(String tabId, Path mode) {
|
||||
setFileExists(tabId, mode.toAbsolutePath().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab下载文件重名时执行的策略
|
||||
*
|
||||
* @param tabId 下载路径
|
||||
* @param mode 下载路径
|
||||
*/
|
||||
public void setFileExists(String tabId, String mode) {
|
||||
if (mode != null && !mode.isEmpty()) TabDownloadSettings.getInstance(tabId).whenFileExists = mode;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置某个tab的重命名文件名
|
||||
*
|
||||
* @param tabId tab id
|
||||
* @param flag 等待标志
|
||||
*/
|
||||
public void setFlag(String tabId, boolean flag) {
|
||||
this.flags.put(tabId, flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个tab的重命名文件名
|
||||
*
|
||||
* @param tabId tab id
|
||||
* @param flag 等待标志
|
||||
*/
|
||||
public void setFlag(String tabId, DownloadMission flag) {
|
||||
this.flags.put(tabId, flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取tab下载等待标记
|
||||
*
|
||||
* @param tabId tab id
|
||||
* @return 任务对象或False
|
||||
*/
|
||||
public Object getFlag(String tabId) {
|
||||
return this.flags.get(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个tab正在下载的任务
|
||||
*
|
||||
* @param tabId tab id
|
||||
* @return 下载任务组成的列表
|
||||
*/
|
||||
public List<DownloadMission> getTabMissions(String tabId) {
|
||||
return this.tabMissions.getOrDefault(tabId, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务结束
|
||||
*
|
||||
* @param mission 任务对象
|
||||
* @param state 任务状态
|
||||
*/
|
||||
public void setDone(DownloadMission mission, String state) {
|
||||
setDone(mission, state, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务结束
|
||||
*
|
||||
* @param mission 任务对象
|
||||
* @param state 任务状态
|
||||
* @param finalPath 最终路径
|
||||
*/
|
||||
public void setDone(DownloadMission mission, String state, String finalPath) {
|
||||
if (!"canceled".equals(mission.state) && !"skipped".equals(mission.state)) mission.state = state;
|
||||
mission.finalPath = finalPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*
|
||||
* @param mission 任务对象
|
||||
*/
|
||||
public Boolean cancel(DownloadMission mission) {
|
||||
mission.state = "canceled";
|
||||
try {
|
||||
this.browser.runCdp("Browser.cancelDownload", Map.of("guid", mission.id));
|
||||
} catch (Exception ignored) {
|
||||
|
||||
}
|
||||
if (mission.finalPath != null) {
|
||||
return Paths.get(mission.finalPath).toFile().delete();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过任务
|
||||
*
|
||||
* @param mission 任务对象
|
||||
*/
|
||||
public void skip(DownloadMission mission) {
|
||||
mission.state = "skipped";
|
||||
try {
|
||||
this.browser.runCdp("Browser.cancelDownload", Map.of("guid", mission.id));
|
||||
} catch (Exception ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当tab关闭时清除有关信息
|
||||
*
|
||||
* @param tabId 标签页id
|
||||
*/
|
||||
public void clearTabInfo(String tabId) {
|
||||
this.tabMissions.remove(tabId);
|
||||
this.flags.remove(tabId);
|
||||
TabDownloadSettings.TAB_DOWNLOAD_SETTINGS_MAP.remove(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于获取弹出新标签页触发的下载任务
|
||||
*/
|
||||
private void onDownloadWillBegin(Object params) {
|
||||
JSONObject kwargs = JSON.parseObject(params.toString());
|
||||
String guid = (String) kwargs.get("guid");
|
||||
String tabId = this.browser.getFrames().getOrDefault(kwargs.getString("frameId"), this.page.tabId());
|
||||
TabDownloadSettings settings = TabDownloadSettings.getInstance(tabId);
|
||||
String name;
|
||||
if (settings.getRename() != null) {
|
||||
if (settings.getSuffix() != null) {
|
||||
name = settings.getRename() + (settings.getSuffix() != null ? "." + settings.getSuffix() : "");
|
||||
} else {
|
||||
String[] tmp = ((String) kwargs.get("suggestedFilename")).split("\\.");
|
||||
String extName = tmp.length > 1 ? tmp[tmp.length - 1] : "";
|
||||
tmp = settings.getRename().split("\\.");
|
||||
String extRename = tmp.length > 1 ? tmp[tmp.length - 1] : "";
|
||||
name = (extRename.equals(extName) ? settings.getRename() : settings.getRename() + "." + extName);
|
||||
}
|
||||
|
||||
settings.setRename(null);
|
||||
settings.setSuffix(null);
|
||||
|
||||
} else if (settings.getSuffix() != null) {
|
||||
name = ((String) kwargs.get("suggestedFilename")).split("\\.")[0];
|
||||
if (settings.getSuffix() != null) {
|
||||
name += "." + settings.getSuffix();
|
||||
}
|
||||
settings.setSuffix(null);
|
||||
|
||||
} else {
|
||||
name = (String) kwargs.get("suggestedFilename");
|
||||
}
|
||||
|
||||
boolean skip = false;
|
||||
Path goalPath = Paths.get(settings.getPath()).resolve(name);
|
||||
if (goalPath.toFile().exists()) {
|
||||
if ("skip".equals(settings.getWhenFileExists())) {
|
||||
skip = true;
|
||||
} else if ("overwrite".equals(settings.getWhenFileExists())) {
|
||||
goalPath.toFile().delete();
|
||||
}
|
||||
}
|
||||
|
||||
DownloadMission m = new DownloadMission(this, tabId, guid, settings.getPath(), name, (String) kwargs.get("url"), this.savePath);
|
||||
this.missions.put(guid, m);
|
||||
|
||||
if (this.getFlag(tabId).equals(false)) {
|
||||
cancel(m);
|
||||
} else if (skip) {
|
||||
skip(m);
|
||||
} else {
|
||||
this.tabMissions.computeIfAbsent(tabId, k -> new ArrayList<>()).add(m);
|
||||
}
|
||||
|
||||
if (getFlag(tabId) != null) flags.put(tabId, m);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载状态变化时执行
|
||||
*/
|
||||
private void onDownloadProgress(Object kwargs) {
|
||||
JSONObject jsonObject = JSON.parseObject(kwargs.toString());
|
||||
|
||||
if (jsonObject.containsKey("guid") && this.missions.containsKey(jsonObject.getString("guid"))) {
|
||||
DownloadMission mission = this.missions.get(jsonObject.getString("guid"));
|
||||
|
||||
if (jsonObject.containsKey("state")) {
|
||||
String state = jsonObject.getString("state");
|
||||
switch (state) {
|
||||
case "inProgress":
|
||||
mission.receiveBytes = jsonObject.getInteger("receivedBytes");
|
||||
mission.totalBytes = jsonObject.getInteger("totalBytes");
|
||||
break;
|
||||
case "completed":
|
||||
if ("skipped".equals(mission.getState())) {
|
||||
// Perform cleanup for skipped mission
|
||||
// Note: This assumes there is a method like `Path.unlink(true)` for cleanup
|
||||
Path formPath = Paths.get(mission.getSavePath(), mission.getId());
|
||||
formPath.toFile().delete(); // Change to your cleanup logic
|
||||
setDone(mission, "skipped");
|
||||
return;
|
||||
}
|
||||
|
||||
mission.receiveBytes = jsonObject.getInteger("receivedBytes");
|
||||
mission.totalBytes = jsonObject.getInteger("totalBytes");
|
||||
|
||||
// Assuming there are methods like `move` and `getUsablePath` in your code
|
||||
String formPath = mission.getSavePath() + File.separator + mission.getId();
|
||||
String toPath = String.valueOf(com.ll.DataRecorder.Tools.getUsablePath(mission.getPath() + File.separator + mission.getName()));
|
||||
try {
|
||||
Files.move(Path.of(formPath), Path.of(toPath));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
setDone(mission, "completed", toPath);
|
||||
break;
|
||||
|
||||
case "canceled":
|
||||
setDone(mission, "canceled");
|
||||
break;
|
||||
default:
|
||||
// Handle other states if needed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
package com.ll.DrissonPage.units.downloader;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
public class DownloadMission {
|
||||
private final String tabId;
|
||||
private final DownloadManager mgr;
|
||||
private final String url;
|
||||
private final String path;
|
||||
private final String name;
|
||||
protected Integer totalBytes;
|
||||
protected int receiveBytes;
|
||||
private final String savePath;
|
||||
private final Boolean isDone;
|
||||
protected String id;
|
||||
protected String state;
|
||||
protected String finalPath;
|
||||
|
||||
public DownloadMission(DownloadManager mgr, String tabId, String id, String path, String name, String url, String savePath) {
|
||||
this.mgr = mgr;
|
||||
this.url = url;
|
||||
this.tabId = tabId;
|
||||
this.id = id;
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
this.state = "running";
|
||||
this.totalBytes = null;
|
||||
this.receiveBytes = 0;
|
||||
this.finalPath = null;
|
||||
this.savePath = savePath;
|
||||
this.isDone = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以百分比形式返回下载进度
|
||||
*/
|
||||
public Float rate() {
|
||||
return this.totalBytes != null ? new BigDecimal(this.receiveBytes).divide(new BigDecimal(this.totalBytes * 100), 2, RoundingMode.FLOOR).floatValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 返回任务是否在运行中
|
||||
*/
|
||||
public boolean isDone() {
|
||||
return this.isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消该任务,如任务已完成,删除已下载的文件
|
||||
*/
|
||||
public void cancel() {
|
||||
this.mgr.cancel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待任务结束
|
||||
*
|
||||
* @return 等待成功返回完整路径,否则返回null
|
||||
*/
|
||||
|
||||
public String waits() {
|
||||
return wait(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待任务结束
|
||||
*
|
||||
* @param show 是否显示下载信息
|
||||
* @return 等待成功返回完整路径,否则返回null
|
||||
*/
|
||||
|
||||
public String wait(boolean show) {
|
||||
return wait(show, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待任务结束
|
||||
*
|
||||
* @param show 是否显示下载信息
|
||||
* @param timeout 超时时间,为null则无限等待
|
||||
* @return 等待成功返回完整路径,否则返回null
|
||||
*/
|
||||
|
||||
public String wait(boolean show, Double timeout) {
|
||||
return wait(show, timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待任务结束
|
||||
*
|
||||
* @param show 是否显示下载信息
|
||||
* @param timeout 超时时间,为null则无限等待
|
||||
* @param cancelIfTimeout 超时时是否取消任务
|
||||
* @return 等待成功返回完整路径,否则返回null
|
||||
*/
|
||||
|
||||
public String wait(boolean show, Double timeout, boolean cancelIfTimeout) {
|
||||
if (show) {
|
||||
System.out.println("url:" + url);
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (this.name == null && System.currentTimeMillis() < endTime) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
System.out.println("文件名:" + this.name);
|
||||
System.out.println("目标路径:" + this.path);
|
||||
}
|
||||
if (timeout == null) {
|
||||
while (!this.isDone) {
|
||||
if (show) {
|
||||
System.out.println(this.rate() + "%");
|
||||
}
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
if (show) {
|
||||
System.out.println(this.rate() + "%");
|
||||
}
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (!this.isDone && cancelIfTimeout) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
if (show) {
|
||||
switch (this.state) {
|
||||
case "completed":
|
||||
System.out.println("下载完成" + this.finalPath);
|
||||
break;
|
||||
case "canceled":
|
||||
System.out.println("下载取消");
|
||||
break;
|
||||
case "skipped":
|
||||
System.out.println("已跳过");
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
return this.finalPath;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.ll.DrissonPage.units.downloader;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class TabDownloadSettings {
|
||||
protected static final Map<String, TabDownloadSettings> TAB_DOWNLOAD_SETTINGS_MAP = new HashMap<>();
|
||||
protected String rename;
|
||||
protected String suffix;
|
||||
protected String path;
|
||||
protected String whenFileExists;
|
||||
private String tabId;
|
||||
|
||||
private TabDownloadSettings(String tabId) {
|
||||
this.tabId = tabId;
|
||||
this.rename = null;
|
||||
this.suffix = null;
|
||||
this.path = "";
|
||||
this.whenFileExists = "rename";
|
||||
}
|
||||
|
||||
public static TabDownloadSettings getInstance(String tabId) {
|
||||
return TAB_DOWNLOAD_SETTINGS_MAP.computeIfAbsent(tabId, TabDownloadSettings::new);
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package com.ll.DrissonPage.units.listener;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 返回的数据包管理类
|
||||
*
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class DataPacket {
|
||||
private String tabId;
|
||||
private String target;
|
||||
private boolean isFailed;
|
||||
|
||||
protected JSONObject rawRequest;
|
||||
protected JSONObject rawResponse;
|
||||
|
||||
protected String rawPostData;
|
||||
protected String rawBody;
|
||||
protected Map<String, Object> rawFailInfo;
|
||||
protected Boolean base64Body;
|
||||
private Request request;
|
||||
private Response response;
|
||||
private FailInfo failInfo;
|
||||
|
||||
protected String resourceType;
|
||||
protected Map<String, Object> requestExtraInfo;
|
||||
protected Map<String, Object> responseExtraInfo;
|
||||
|
||||
/**
|
||||
* @param tabId 产生这个数据包的tab的id
|
||||
* @param target 监听目标
|
||||
*/
|
||||
public DataPacket(String tabId, String target) {
|
||||
this.tabId = tabId;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public String url() {
|
||||
return this.request.url;
|
||||
}
|
||||
|
||||
public String method() {
|
||||
return this.request.method;
|
||||
}
|
||||
|
||||
public String frameId() {
|
||||
return this.rawRequest.getString("frameId");
|
||||
}
|
||||
|
||||
public Request request() {
|
||||
if (this.request == null)
|
||||
this.request = new Request(this, this.rawRequest.getJSONObject("request"), this.rawPostData);
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public Response response() {
|
||||
if (this.response == null) this.response = new Response(this, this.rawResponse, this.rawBody, this.base64Body);
|
||||
return this.response;
|
||||
}
|
||||
|
||||
public FailInfo failInfo() {
|
||||
if (this.failInfo == null) this.failInfo = new FailInfo(this, this.rawFailInfo);
|
||||
return this.failInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待额外的信息加载完成
|
||||
*
|
||||
* @return 是否等待成功
|
||||
*/
|
||||
public boolean waitExtraInfo() {
|
||||
return waitExtraInfo(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待额外的信息加载完成
|
||||
*
|
||||
* @param timeout 超时时间,null为无限等待
|
||||
* @return 是否等待成功
|
||||
*/
|
||||
public boolean waitExtraInfo(Double timeout) {
|
||||
if (timeout == null) {
|
||||
while (this.requestExtraInfo == null) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
long endTime = (long) (System.currentTimeMillis() + timeout * 1000);
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
if (this.requestExtraInfo != null) return true;
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.ll.DrissonPage.units.listener;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class ExtraInfo {
|
||||
Map<String, Object> extraInfo;
|
||||
|
||||
/**
|
||||
* @return 以map形式返回所有额外信息
|
||||
*/
|
||||
public Map<String, Object> allInfo() {
|
||||
return extraInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param item 获取单独的额外信息
|
||||
*/
|
||||
public Object getInfo(Object item) {
|
||||
return extraInfo.get(item.toString());
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.ll.DrissonPage.units.listener;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 陆
|
||||
* @address <a href="https://t.me/blanksig"/>click
|
||||
*/
|
||||
public class FailInfo {
|
||||
private final DataPacket dataPacket;
|
||||
private final Map<String, Object> failInfo;
|
||||
|
||||
public FailInfo(DataPacket dataPacket, Map<String, Object> failInfo) {
|
||||
this.dataPacket = dataPacket;
|
||||
this.failInfo = failInfo;
|
||||
}
|
||||
|
||||
public Object get(Object item) {
|
||||
if (failInfo != null && !failInfo.isEmpty()) return this.failInfo.get(item.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user