pull/32/head
parent
ce4d2853f7
commit
1f9acf5ae4
@ -0,0 +1,8 @@ |
|||||||
|
package io.legado.app.constant |
||||||
|
|
||||||
|
import java.util.regex.Pattern |
||||||
|
|
||||||
|
object Pattern { |
||||||
|
val JS_PATTERN = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) |
||||||
|
val EXP_PATTERN = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") |
||||||
|
} |
@ -0,0 +1,290 @@ |
|||||||
|
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.utils.NetworkUtils |
||||||
|
import java.net.URLEncoder |
||||||
|
import java.util.* |
||||||
|
import java.util.regex.Pattern |
||||||
|
import javax.script.SimpleBindings |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Created by GKF on 2018/1/24. |
||||||
|
* 搜索URL规则解析 |
||||||
|
*/ |
||||||
|
@Keep |
||||||
|
class AnalyzeUrl @SuppressLint("DefaultLocale") |
||||||
|
@Throws(Exception::class) |
||||||
|
constructor(ruleUrl: String, key: String?, page: Int?, headerMapF: Map<String, String>?, baseUrl: String?) { |
||||||
|
private var baseUrl: String? = null |
||||||
|
var url: String? = null |
||||||
|
private set |
||||||
|
var host: String? = null |
||||||
|
private set |
||||||
|
var path: String? = null |
||||||
|
private set |
||||||
|
var queryStr: String? = null |
||||||
|
private set |
||||||
|
private val queryMap = LinkedHashMap<String, String>() |
||||||
|
private val headerMap = HashMap<String, String>() |
||||||
|
private var charCode: String? = null |
||||||
|
var urlMode = UrlMode.DEFAULT |
||||||
|
private set |
||||||
|
|
||||||
|
val postData: ByteArray |
||||||
|
get() { |
||||||
|
val builder = StringBuilder() |
||||||
|
val keys = queryMap.keys |
||||||
|
for (key in keys) { |
||||||
|
builder.append(String.format("%s=%s&", key, queryMap[key])) |
||||||
|
} |
||||||
|
builder.deleteCharAt(builder.lastIndexOf("&")) |
||||||
|
return builder.toString().toByteArray() |
||||||
|
} |
||||||
|
|
||||||
|
@Throws(Exception::class) |
||||||
|
constructor(urlRule: String) : this(urlRule, null, null, null, null) |
||||||
|
|
||||||
|
@Throws(Exception::class) |
||||||
|
constructor(urlRule: String, headerMapF: Map<String, String>, baseUrl: String) : this( |
||||||
|
urlRule, |
||||||
|
null, |
||||||
|
null, |
||||||
|
headerMapF, |
||||||
|
baseUrl |
||||||
|
) |
||||||
|
|
||||||
|
init { |
||||||
|
var ruleUrl = ruleUrl |
||||||
|
if (!TextUtils.isEmpty(baseUrl)) { |
||||||
|
// this.baseUrl = headerPattern.matcher(baseUrl).replaceAll("") |
||||||
|
} |
||||||
|
//解析Header |
||||||
|
ruleUrl = analyzeHeader(ruleUrl, headerMapF) |
||||||
|
//替换关键字 |
||||||
|
key?.let { |
||||||
|
if (it.isNotBlank()) { |
||||||
|
ruleUrl = ruleUrl.replace("searchKey", it) |
||||||
|
} |
||||||
|
} |
||||||
|
//分离编码规则 |
||||||
|
ruleUrl = splitCharCode(ruleUrl) |
||||||
|
//判断是否有下一页 |
||||||
|
if (page != null && page > 1 && !ruleUrl.contains("searchPage")) |
||||||
|
throw Exception("没有下一页") |
||||||
|
//替换js |
||||||
|
ruleUrl = replaceJs(ruleUrl, baseUrl, page, key) |
||||||
|
//设置页数 |
||||||
|
ruleUrl = analyzePage(ruleUrl, page) |
||||||
|
//执行规则列表 |
||||||
|
val ruleList = splitRule(ruleUrl) |
||||||
|
for (rule in ruleList) { |
||||||
|
ruleUrl = if (rule.startsWith("<js>")) { |
||||||
|
evalJS(rule.substring(4, rule.lastIndexOf("<")), ruleUrl) as String |
||||||
|
} else { |
||||||
|
rule.replace("@result", ruleUrl) |
||||||
|
} |
||||||
|
} |
||||||
|
//分离post参数 |
||||||
|
var ruleUrlS = ruleUrl.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
if (ruleUrlS.size > 1) { |
||||||
|
urlMode = UrlMode.POST |
||||||
|
} else { |
||||||
|
//分离get参数 |
||||||
|
ruleUrlS = ruleUrlS[0].split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
if (ruleUrlS.size > 1) { |
||||||
|
urlMode = UrlMode.GET |
||||||
|
} |
||||||
|
} |
||||||
|
generateUrlPath(ruleUrlS[0]) |
||||||
|
if (urlMode != UrlMode.DEFAULT) { |
||||||
|
analyzeQuery(ruleUrlS[1]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析Header |
||||||
|
*/ |
||||||
|
private fun analyzeHeader(ruleUrl: String, headerMapF: Map<String, String>?): String { |
||||||
|
// var ruleUrl = ruleUrl |
||||||
|
// if (headerMapF != null) { |
||||||
|
// headerMap.putAll(headerMapF) |
||||||
|
// } |
||||||
|
// val matcher = headerPattern.matcher(ruleUrl) |
||||||
|
// if (matcher.find()) { |
||||||
|
// var find = matcher.group(0) |
||||||
|
// ruleUrl = ruleUrl.replace(find, "") |
||||||
|
// find = find.substring(8) |
||||||
|
// try { |
||||||
|
// val map = Gson().fromJson<Map<String, String>>(find) |
||||||
|
// headerMap.putAll(map) |
||||||
|
// } catch (ignored: Exception) { |
||||||
|
// } |
||||||
|
// } |
||||||
|
return ruleUrl |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 分离编码规则 |
||||||
|
*/ |
||||||
|
private fun splitCharCode(rule: String): String { |
||||||
|
val ruleUrlS = rule.split("\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
if (ruleUrlS.size > 1) { |
||||||
|
if (!TextUtils.isEmpty(ruleUrlS[1])) { |
||||||
|
val qtS = ruleUrlS[1].split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
for (qt in qtS) { |
||||||
|
val gz = qt.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
if (gz[0] == "char") { |
||||||
|
charCode = gz[1] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return ruleUrlS[0] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析页数 |
||||||
|
*/ |
||||||
|
private fun analyzePage(ruleUrl: String, searchPage: Int?): String { |
||||||
|
var ruleUrl = ruleUrl |
||||||
|
if (searchPage == null) return ruleUrl |
||||||
|
val matcher = pagePattern.matcher(ruleUrl) |
||||||
|
while (matcher.find()) { |
||||||
|
val pages = matcher.group(1).split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
if (searchPage <= pages.size) { |
||||||
|
ruleUrl = ruleUrl.replace(matcher.group(), pages[searchPage - 1].trim { it <= ' ' }) |
||||||
|
} else { |
||||||
|
ruleUrl = ruleUrl.replace(matcher.group(), pages[pages.size - 1].trim { it <= ' ' }) |
||||||
|
} |
||||||
|
} |
||||||
|
return ruleUrl.replace("searchPage-1", (searchPage - 1).toString()) |
||||||
|
.replace("searchPage+1", (searchPage + 1).toString()) |
||||||
|
.replace("searchPage", searchPage.toString()) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 替换js |
||||||
|
*/ |
||||||
|
@SuppressLint("DefaultLocale") |
||||||
|
@Throws(Exception::class) |
||||||
|
private fun replaceJs(ruleUrl: String, baseUrl: String?, searchPage: Int?, searchKey: String?): String { |
||||||
|
var ruleUrl = ruleUrl |
||||||
|
if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) { |
||||||
|
var jsEval: Any |
||||||
|
val sb = StringBuffer(ruleUrl.length) |
||||||
|
val simpleBindings = object : SimpleBindings() { |
||||||
|
init { |
||||||
|
this["baseUrl"] = baseUrl |
||||||
|
this["searchPage"] = searchPage |
||||||
|
this["searchKey"] = searchKey |
||||||
|
} |
||||||
|
} |
||||||
|
val expMatcher = EXP_PATTERN.matcher(ruleUrl) |
||||||
|
while (expMatcher.find()) { |
||||||
|
jsEval = SCRIPT_ENGINE.eval(expMatcher.group(1), simpleBindings) |
||||||
|
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) |
||||||
|
ruleUrl = sb.toString() |
||||||
|
} |
||||||
|
return ruleUrl |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析QueryMap |
||||||
|
*/ |
||||||
|
@Throws(Exception::class) |
||||||
|
private fun analyzeQuery(allQuery: String) { |
||||||
|
queryStr = allQuery |
||||||
|
val queryS = allQuery.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
for (query in queryS) { |
||||||
|
val queryM = query.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
val value = if (queryM.size > 1) queryM[1] else "" |
||||||
|
if (TextUtils.isEmpty(charCode)) { |
||||||
|
if (NetworkUtils.hasUrlEncoded(value)) { |
||||||
|
queryMap[queryM[0]] = value |
||||||
|
} else { |
||||||
|
queryMap[queryM[0]] = URLEncoder.encode(value, "UTF-8") |
||||||
|
} |
||||||
|
} else if (charCode == "escape") { |
||||||
|
// queryMap[queryM[0]] = StringUtils.escape(value) |
||||||
|
} else { |
||||||
|
queryMap[queryM[0]] = URLEncoder.encode(value, charCode) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 拆分规则 |
||||||
|
*/ |
||||||
|
private fun splitRule(ruleStr: String): List<String> { |
||||||
|
val ruleList = ArrayList<String>() |
||||||
|
val jsMatcher = JS_PATTERN.matcher(ruleStr) |
||||||
|
var start = 0 |
||||||
|
var tmp: String |
||||||
|
while (jsMatcher.find()) { |
||||||
|
if (jsMatcher.start() > start) { |
||||||
|
tmp = ruleStr.substring(start, jsMatcher.start()).replace("\n".toRegex(), "").trim { it <= ' ' } |
||||||
|
if (!TextUtils.isEmpty(tmp)) { |
||||||
|
ruleList.add(tmp) |
||||||
|
} |
||||||
|
} |
||||||
|
ruleList.add(jsMatcher.group()) |
||||||
|
start = jsMatcher.end() |
||||||
|
} |
||||||
|
if (ruleStr.length > start) { |
||||||
|
tmp = ruleStr.substring(start).replace("\n".toRegex(), "").trim { it <= ' ' } |
||||||
|
if (!TextUtils.isEmpty(tmp)) { |
||||||
|
ruleList.add(tmp) |
||||||
|
} |
||||||
|
} |
||||||
|
return ruleList |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 分解URL |
||||||
|
*/ |
||||||
|
private fun generateUrlPath(ruleUrl: String) { |
||||||
|
baseUrl?.let { url = NetworkUtils.getAbsoluteURL(it, ruleUrl) } |
||||||
|
host = NetworkUtils.getBaseUrl(url) |
||||||
|
path = url!!.substring(host!!.length) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 执行JS |
||||||
|
*/ |
||||||
|
@Throws(Exception::class) |
||||||
|
private fun evalJS(jsStr: String, result: Any): Any { |
||||||
|
val bindings = SimpleBindings() |
||||||
|
bindings["result"] = result |
||||||
|
return SCRIPT_ENGINE.eval(jsStr, bindings) |
||||||
|
} |
||||||
|
|
||||||
|
fun getQueryMap(): Map<String, String> { |
||||||
|
return queryMap |
||||||
|
} |
||||||
|
|
||||||
|
fun getHeaderMap(): Map<String, String> { |
||||||
|
return headerMap |
||||||
|
} |
||||||
|
|
||||||
|
enum class UrlMode { |
||||||
|
GET, POST, DEFAULT |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
private val pagePattern = Pattern.compile("\\{(.*?)\\}") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,110 @@ |
|||||||
|
package io.legado.app.utils |
||||||
|
|
||||||
|
import android.text.TextUtils |
||||||
|
import java.net.URL |
||||||
|
import java.util.* |
||||||
|
|
||||||
|
object NetworkUtils { |
||||||
|
|
||||||
|
private val notNeedEncoding: BitSet by lazy { |
||||||
|
val bitSet = BitSet(256) |
||||||
|
var i: Int = 'a'.toInt() |
||||||
|
while (i <= 'z'.toInt()) { |
||||||
|
bitSet.set(i) |
||||||
|
i++ |
||||||
|
} |
||||||
|
i = 'A'.toInt() |
||||||
|
while (i <= 'Z'.toInt()) { |
||||||
|
bitSet.set(i) |
||||||
|
i++ |
||||||
|
} |
||||||
|
i = '0'.toInt() |
||||||
|
while (i <= '9'.toInt()) { |
||||||
|
bitSet.set(i) |
||||||
|
i++ |
||||||
|
} |
||||||
|
bitSet.set('+'.toInt()) |
||||||
|
bitSet.set('-'.toInt()) |
||||||
|
bitSet.set('_'.toInt()) |
||||||
|
bitSet.set('.'.toInt()) |
||||||
|
bitSet.set('$'.toInt()) |
||||||
|
bitSet.set(':'.toInt()) |
||||||
|
bitSet.set('('.toInt()) |
||||||
|
bitSet.set(')'.toInt()) |
||||||
|
bitSet.set('!'.toInt()) |
||||||
|
bitSet.set('*'.toInt()) |
||||||
|
bitSet.set('@'.toInt()) |
||||||
|
bitSet.set('&'.toInt()) |
||||||
|
bitSet.set('#'.toInt()) |
||||||
|
bitSet.set(','.toInt()) |
||||||
|
bitSet.set('['.toInt()) |
||||||
|
bitSet.set(']'.toInt()) |
||||||
|
return@lazy bitSet |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' |
||||||
|
* 0-9a-zA-Z保留 <br></br> |
||||||
|
* ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留 |
||||||
|
* 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F] |
||||||
|
*/ |
||||||
|
fun hasUrlEncoded(str: String): Boolean { |
||||||
|
var needEncode = false |
||||||
|
var i = 0 |
||||||
|
while (i < str.length) { |
||||||
|
val c = str[i] |
||||||
|
if (notNeedEncoding.get(c.toInt())) { |
||||||
|
i++ |
||||||
|
continue |
||||||
|
} |
||||||
|
if (c == '%' && i + 2 < str.length) { |
||||||
|
// 判断是否符合urlEncode规范 |
||||||
|
val c1 = str[++i] |
||||||
|
val c2 = str[++i] |
||||||
|
if (isDigit16Char(c1) && isDigit16Char(c2)) { |
||||||
|
i++ |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
// 其他字符,肯定需要urlEncode |
||||||
|
needEncode = true |
||||||
|
break |
||||||
|
i++ |
||||||
|
} |
||||||
|
|
||||||
|
return !needEncode |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 判断c是否是16进制的字符 |
||||||
|
*/ |
||||||
|
private fun isDigit16Char(c: Char): Boolean { |
||||||
|
return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取绝对地址 |
||||||
|
*/ |
||||||
|
fun getAbsoluteURL(baseURL: String, relativePath: String): String { |
||||||
|
var relativeUrl = relativePath |
||||||
|
if (TextUtils.isEmpty(baseURL)) return relativePath |
||||||
|
try { |
||||||
|
val absoluteUrl = URL(baseURL) |
||||||
|
val parseUrl = URL(absoluteUrl, relativePath) |
||||||
|
relativeUrl = parseUrl.toString() |
||||||
|
return relativeUrl |
||||||
|
} catch (e: Exception) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
return relativeUrl |
||||||
|
} |
||||||
|
|
||||||
|
fun getBaseUrl(url: String?): String? { |
||||||
|
if (url == null || !url.startsWith("http")) return null |
||||||
|
val index = url.indexOf("/", 9) |
||||||
|
return if (index == -1) { |
||||||
|
url |
||||||
|
} else url.substring(0, index) |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue