diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt new file mode 100644 index 000000000..5affcb76f --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -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? { + if (TextUtils.isEmpty(rule)) return null + val ruleList = splitSourceRule(rule) + return getStringList(ruleList, isUrl) + } + + + @Throws(Exception::class) + fun getStringList(ruleList: List, isUrl: Boolean): List? { + 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() + for (url in (result as List?)!!) { + val absoluteURL = NetworkUtils.getAbsoluteURL(it, url) + if (!urlList.contains(absoluteURL)) { + urlList.add(absoluteURL) + } + } + return urlList + } + } + return result as List? + } + + /** + * 获取文本 + */ + @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, 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 { + 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 + } + return ArrayList() + } + + /** + * 保存变量 + */ + @Throws(Exception::class) + private fun putRule(map: Map) { + 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>(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 { + var vRuleStr = ruleStr + val ruleList = ArrayList() + 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("")) { + 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) + } + +}