pull/32/head
parent
1f9acf5ae4
commit
c615aef925
@ -0,0 +1,466 @@ |
||||
package io.legado.app.model.analyzeRule |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.text.TextUtils |
||||
import androidx.annotation.Keep |
||||
import io.legado.app.constant.AppConst.SCRIPT_ENGINE |
||||
import io.legado.app.constant.Pattern.EXP_PATTERN |
||||
import io.legado.app.constant.Pattern.JS_PATTERN |
||||
import io.legado.app.data.entities.BaseBook |
||||
import io.legado.app.utils.* |
||||
import java.util.* |
||||
import java.util.regex.Pattern |
||||
import javax.script.SimpleBindings |
||||
|
||||
|
||||
/** |
||||
* Created by REFGD. |
||||
* 统一解析接口 |
||||
*/ |
||||
@Keep |
||||
class AnalyzeRule(private var book: BaseBook? = null) { |
||||
private var `object`: Any? = null |
||||
private var isJSON: Boolean? = false |
||||
private var baseUrl: String? = null |
||||
|
||||
private var analyzeByXPath: AnalyzeByXPath? = null |
||||
private var analyzeByJSoup: AnalyzeByJSoup? = null |
||||
private var analyzeByJSonPath: AnalyzeByJSonPath? = null |
||||
|
||||
private var objectChangedXP = false |
||||
private var objectChangedJS = false |
||||
private var objectChangedJP = false |
||||
|
||||
fun setBook(book: BaseBook) { |
||||
this.book = book |
||||
} |
||||
|
||||
@JvmOverloads |
||||
fun setContent(body: Any?, baseUrl: String? = null): AnalyzeRule { |
||||
if (body == null) throw AssertionError("Content cannot be null") |
||||
isJSON = body.toString().isJson() |
||||
`object` = body |
||||
this.baseUrl = baseUrl?.split("\n".toRegex(), 1)?.get(0) |
||||
objectChangedXP = true |
||||
objectChangedJS = true |
||||
objectChangedJP = true |
||||
return this |
||||
} |
||||
|
||||
/** |
||||
* 获取XPath解析类 |
||||
*/ |
||||
private fun getAnalyzeByXPath(o: Any?): AnalyzeByXPath { |
||||
return if (o != null) { |
||||
AnalyzeByXPath().parse(o) |
||||
} else getAnalyzeByXPath() |
||||
} |
||||
|
||||
private fun getAnalyzeByXPath(): AnalyzeByXPath { |
||||
if (analyzeByXPath == null || objectChangedXP) { |
||||
analyzeByXPath = AnalyzeByXPath() |
||||
analyzeByXPath!!.parse(`object`!!) |
||||
objectChangedXP = false |
||||
} |
||||
return analyzeByXPath as AnalyzeByXPath |
||||
} |
||||
|
||||
/** |
||||
* 获取JSOUP解析类 |
||||
*/ |
||||
private fun getAnalyzeByJSoup(o: Any?): AnalyzeByJSoup { |
||||
return if (o != null) { |
||||
AnalyzeByJSoup().parse(o) |
||||
} else getAnalyzeByJSoup() |
||||
} |
||||
|
||||
private fun getAnalyzeByJSoup(): AnalyzeByJSoup { |
||||
if (analyzeByJSoup == null || objectChangedJS) { |
||||
analyzeByJSoup = AnalyzeByJSoup() |
||||
analyzeByJSoup!!.parse(`object`!!) |
||||
objectChangedJS = false |
||||
} |
||||
return analyzeByJSoup as AnalyzeByJSoup |
||||
} |
||||
|
||||
/** |
||||
* 获取JSON解析类 |
||||
*/ |
||||
private fun getAnalyzeByJSonPath(o: Any?): AnalyzeByJSonPath { |
||||
return if (o != null) { |
||||
AnalyzeByJSonPath().parse(o) |
||||
} else getAnalyzeByJSonPath() |
||||
} |
||||
|
||||
private fun getAnalyzeByJSonPath(): AnalyzeByJSonPath { |
||||
if (analyzeByJSonPath == null || objectChangedJP) { |
||||
analyzeByJSonPath = AnalyzeByJSonPath() |
||||
analyzeByJSonPath!!.parse(`object`!!) |
||||
objectChangedJP = false |
||||
} |
||||
return analyzeByJSonPath as AnalyzeByJSonPath |
||||
} |
||||
|
||||
/** |
||||
* 获取文本列表 |
||||
*/ |
||||
@Throws(Exception::class) |
||||
@JvmOverloads |
||||
fun getStringList(rule: String, isUrl: Boolean = false): List<String>? { |
||||
if (TextUtils.isEmpty(rule)) return null |
||||
val ruleList = splitSourceRule(rule) |
||||
return getStringList(ruleList, isUrl) |
||||
} |
||||
|
||||
|
||||
@Throws(Exception::class) |
||||
fun getStringList(ruleList: List<SourceRule>, isUrl: Boolean): List<String>? { |
||||
var result: Any? = null |
||||
for (rule in ruleList) { |
||||
when (rule.mode) { |
||||
AnalyzeRule.Mode.Js -> { |
||||
if (result == null) result = `object` |
||||
result = evalJS(rule.rule, result) |
||||
} |
||||
AnalyzeRule.Mode.JSon -> result = getAnalyzeByJSonPath(result).getStringList(rule.rule) |
||||
AnalyzeRule.Mode.XPath -> result = getAnalyzeByXPath(result).getStringList(rule.rule) |
||||
else -> result = getAnalyzeByJSoup(result).getStringList(rule.rule) |
||||
} |
||||
} |
||||
if (result == null) return ArrayList() |
||||
if (result is String) { |
||||
result = Arrays.asList((result as String?)?.htmlFormat()?.split("\n")) |
||||
} |
||||
baseUrl?.let { |
||||
if (isUrl && !TextUtils.isEmpty(it)) { |
||||
val urlList = ArrayList<String>() |
||||
for (url in (result as List<String>?)!!) { |
||||
val absoluteURL = NetworkUtils.getAbsoluteURL(it, url) |
||||
if (!urlList.contains(absoluteURL)) { |
||||
urlList.add(absoluteURL) |
||||
} |
||||
} |
||||
return urlList |
||||
} |
||||
} |
||||
return result as List<String>? |
||||
} |
||||
|
||||
/** |
||||
* 获取文本 |
||||
*/ |
||||
@Throws(Exception::class) |
||||
fun getString(rule: String): String? { |
||||
return getString(rule, false) |
||||
} |
||||
|
||||
@Throws(Exception::class) |
||||
fun getString(ruleStr: String, isUrl: Boolean): String? { |
||||
if (TextUtils.isEmpty(ruleStr)) return null |
||||
val ruleList = splitSourceRule(ruleStr) |
||||
return getString(ruleList, isUrl) |
||||
} |
||||
|
||||
@Throws(Exception::class) |
||||
@JvmOverloads |
||||
fun getString(ruleList: List<SourceRule>, isUrl: Boolean = false): String { |
||||
var result: Any? = null |
||||
for (rule in ruleList) { |
||||
if (rule.rule.isNotBlank()) { |
||||
when (rule.mode) { |
||||
AnalyzeRule.Mode.Js -> { |
||||
if (result == null) result = `object` |
||||
result = evalJS(rule.rule, result) |
||||
} |
||||
AnalyzeRule.Mode.JSon -> result = getAnalyzeByJSonPath(result).getString(rule.rule) |
||||
AnalyzeRule.Mode.XPath -> result = getAnalyzeByXPath(result).getString(rule.rule) |
||||
AnalyzeRule.Mode.Default -> result = if (isUrl && !TextUtils.isEmpty(baseUrl)) { |
||||
getAnalyzeByJSoup(result).getString0(rule.rule) |
||||
} else { |
||||
getAnalyzeByJSoup(result).getString(rule.rule) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if (result == null) return "" |
||||
baseUrl?.let { |
||||
return if (isUrl) { |
||||
NetworkUtils.getAbsoluteURL(it, result.toString()) |
||||
} else result.toString() |
||||
} |
||||
return result.toString() |
||||
} |
||||
|
||||
/** |
||||
* 获取Element |
||||
*/ |
||||
@Throws(Exception::class) |
||||
fun getElement(ruleStr: String): Any? { |
||||
if (TextUtils.isEmpty(ruleStr)) return null |
||||
var result: Any? = null |
||||
val ruleList = splitSourceRule(ruleStr) |
||||
for (rule in ruleList) { |
||||
when (rule.mode) { |
||||
AnalyzeRule.Mode.Js -> { |
||||
if (result == null) result = `object` |
||||
result = evalJS(rule.rule, result) |
||||
} |
||||
AnalyzeRule.Mode.JSon -> result = getAnalyzeByJSonPath(result).getObject(rule.rule) |
||||
AnalyzeRule.Mode.XPath -> result = getAnalyzeByXPath(result).getElements(rule.rule) |
||||
else -> result = getAnalyzeByJSoup(result).getElements(rule.rule) |
||||
} |
||||
} |
||||
return result |
||||
} |
||||
|
||||
/** |
||||
* 获取列表 |
||||
*/ |
||||
@Throws(Exception::class) |
||||
fun getElements(ruleStr: String): List<Any> { |
||||
var result: Any? = null |
||||
val ruleList = splitSourceRule(ruleStr) |
||||
for (rule in ruleList) { |
||||
when (rule.mode) { |
||||
AnalyzeRule.Mode.Js -> { |
||||
if (result == null) result = `object` |
||||
result = evalJS(rule.rule, result) |
||||
} |
||||
AnalyzeRule.Mode.JSon -> result = getAnalyzeByJSonPath(result).getList(rule.rule) |
||||
AnalyzeRule.Mode.XPath -> result = getAnalyzeByXPath(result).getElements(rule.rule) |
||||
else -> result = getAnalyzeByJSoup(result).getElements(rule.rule) |
||||
} |
||||
} |
||||
result?.let { |
||||
return it as List<Any> |
||||
} |
||||
return ArrayList() |
||||
} |
||||
|
||||
/** |
||||
* 保存变量 |
||||
*/ |
||||
@Throws(Exception::class) |
||||
private fun putRule(map: Map<String, String>) { |
||||
for ((key, value) in map) { |
||||
getString(value)?.let { |
||||
book?.putVariable(key, it) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 分离并执行put规则 |
||||
*/ |
||||
@Throws(Exception::class) |
||||
private fun splitPutRule(ruleStr: String): String { |
||||
var ruleStr = ruleStr |
||||
val putMatcher = putPattern.matcher(ruleStr) |
||||
while (putMatcher.find()) { |
||||
ruleStr = ruleStr.replace(putMatcher.group(), "") |
||||
val map = GSON.fromJson<Map<String, String>>(putMatcher.group(1)) |
||||
putRule(map) |
||||
} |
||||
return ruleStr |
||||
} |
||||
|
||||
/** |
||||
* 替换@get |
||||
*/ |
||||
fun replaceGet(ruleStr: String): String { |
||||
var vRuleStr = ruleStr |
||||
val getMatcher = getPattern.matcher(vRuleStr) |
||||
while (getMatcher.find()) { |
||||
var value = "" |
||||
book?.variableMap?.get(getMatcher.group(1))?.let { |
||||
value = it |
||||
} |
||||
vRuleStr = vRuleStr.replace(getMatcher.group(), value) |
||||
} |
||||
return vRuleStr |
||||
} |
||||
|
||||
/** |
||||
* 替换JS |
||||
*/ |
||||
@SuppressLint("DefaultLocale") |
||||
@Throws(Exception::class) |
||||
private fun replaceJs(ruleStr: String): String { |
||||
var ruleStr = ruleStr |
||||
if (ruleStr.contains("{{") && ruleStr.contains("}}")) { |
||||
var jsEval: Any |
||||
val sb = StringBuffer(ruleStr.length) |
||||
val expMatcher = EXP_PATTERN.matcher(ruleStr) |
||||
while (expMatcher.find()) { |
||||
jsEval = evalJS(expMatcher.group(1), `object`) |
||||
if (jsEval is String) { |
||||
expMatcher.appendReplacement(sb, jsEval) |
||||
} else if (jsEval is Double && jsEval % 1.0 == 0.0) { |
||||
expMatcher.appendReplacement(sb, String.format("%.0f", jsEval)) |
||||
} else { |
||||
expMatcher.appendReplacement(sb, jsEval.toString()) |
||||
} |
||||
} |
||||
expMatcher.appendTail(sb) |
||||
ruleStr = sb.toString() |
||||
} |
||||
return ruleStr |
||||
} |
||||
|
||||
/** |
||||
* 分解规则生成规则列表 |
||||
*/ |
||||
@Throws(Exception::class) |
||||
fun splitSourceRule(ruleStr: String): List<SourceRule> { |
||||
var vRuleStr = ruleStr |
||||
val ruleList = ArrayList<SourceRule>() |
||||
if (TextUtils.isEmpty(vRuleStr)) return ruleList |
||||
//检测Mode |
||||
val mode: Mode |
||||
if (vRuleStr.startsWith("@XPath:", true)) { |
||||
mode = Mode.XPath |
||||
vRuleStr = vRuleStr.substring(7) |
||||
} else if (vRuleStr.startsWith("@JSon:", true)) { |
||||
mode = Mode.JSon |
||||
vRuleStr = vRuleStr.substring(6) |
||||
} else { |
||||
mode = if (isJSON!!) { |
||||
Mode.JSon |
||||
} else { |
||||
Mode.Default |
||||
} |
||||
} |
||||
//分离put规则 |
||||
vRuleStr = splitPutRule(vRuleStr) |
||||
//替换get值 |
||||
vRuleStr = replaceGet(vRuleStr) |
||||
//替换js |
||||
vRuleStr = replaceJs(vRuleStr) |
||||
//拆分为列表 |
||||
var start = 0 |
||||
var tmp: String |
||||
val jsMatcher = JS_PATTERN.matcher(vRuleStr) |
||||
while (jsMatcher.find()) { |
||||
if (jsMatcher.start() > start) { |
||||
tmp = vRuleStr.substring(start, jsMatcher.start()).replace("\n".toRegex(), "").trim { it <= ' ' } |
||||
if (!TextUtils.isEmpty(tmp)) { |
||||
ruleList.add(SourceRule(tmp, mode)) |
||||
} |
||||
} |
||||
ruleList.add(SourceRule(jsMatcher.group(), Mode.Js)) |
||||
start = jsMatcher.end() |
||||
} |
||||
if (vRuleStr.length > start) { |
||||
tmp = vRuleStr.substring(start).replace("\n".toRegex(), "").trim { it <= ' ' } |
||||
if (!TextUtils.isEmpty(tmp)) { |
||||
ruleList.add(SourceRule(tmp, mode)) |
||||
} |
||||
} |
||||
return ruleList |
||||
} |
||||
|
||||
/** |
||||
* 规则类 |
||||
*/ |
||||
inner class SourceRule internal constructor(ruleStr: String, mainMode: Mode) { |
||||
internal var mode: Mode |
||||
internal var rule: String |
||||
|
||||
init { |
||||
this.mode = mainMode |
||||
if (mode == Mode.Js) { |
||||
if (ruleStr.startsWith("<js>")) { |
||||
rule = ruleStr.substring(4, ruleStr.lastIndexOf("<")) |
||||
} else { |
||||
rule = ruleStr.substring(4) |
||||
} |
||||
} else { |
||||
if (ruleStr.startsWith("@XPath:", true)) { |
||||
mode = Mode.XPath |
||||
rule = ruleStr.substring(7) |
||||
} else if (ruleStr.startsWith("//")) {//XPath特征很明显,无需配置单独的识别标头 |
||||
mode = Mode.XPath |
||||
rule = ruleStr |
||||
} else if (ruleStr.startsWith("@JSon:", true)) { |
||||
mode = Mode.JSon |
||||
rule = ruleStr.substring(6) |
||||
} else if (ruleStr.startsWith("$.")) { |
||||
mode = Mode.JSon |
||||
rule = ruleStr |
||||
} else { |
||||
rule = ruleStr |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
enum class Mode { |
||||
XPath, JSon, Default, Js |
||||
} |
||||
|
||||
fun put(key: String, value: String): String { |
||||
if (book != null) { |
||||
book!!.putVariable(key, value) |
||||
} |
||||
return value |
||||
} |
||||
|
||||
operator fun get(key: String): String? { |
||||
return book?.variableMap?.get(key) |
||||
} |
||||
|
||||
/** |
||||
* 执行JS |
||||
*/ |
||||
@Throws(Exception::class) |
||||
private fun evalJS(jsStr: String, result: Any?): Any { |
||||
val bindings = SimpleBindings() |
||||
bindings["java"] = this |
||||
bindings["result"] = result |
||||
bindings["baseUrl"] = baseUrl |
||||
return SCRIPT_ENGINE.eval(jsStr, bindings) |
||||
} |
||||
|
||||
/** |
||||
* js实现跨域访问,不能删 |
||||
*/ |
||||
// fun ajax(urlStr: String): String? { |
||||
// try { |
||||
// val analyzeUrl = AnalyzeUrl(urlStr) |
||||
// val response = BaseModelImpl.getInstance().getResponseO(analyzeUrl) |
||||
// .blockingFirst() |
||||
// return response.body() |
||||
// } catch (e: Exception) { |
||||
// return e.localizedMessage |
||||
// } |
||||
// |
||||
// } |
||||
|
||||
/** |
||||
* js实现解码,不能删 |
||||
*/ |
||||
// fun base64Decoder(base64: String): String { |
||||
// return StringUtils.base64Decode(base64) |
||||
// } |
||||
|
||||
/** |
||||
* 章节数转数字 |
||||
*/ |
||||
// fun toNumChapter(s: String?): String? { |
||||
// if (s == null) { |
||||
// return null |
||||
// } |
||||
// val pattern = Pattern.compile("(第)(.+?)(章)") |
||||
// val matcher = pattern.matcher(s) |
||||
// return if (matcher.find()) { |
||||
// matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3) |
||||
// } else s |
||||
// } |
||||
|
||||
companion object { |
||||
private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE) |
||||
private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE) |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue