pull/32/head
kunfei 5 years ago
parent ce4d2853f7
commit 1f9acf5ae4
  1. 3
      app/src/main/java/io/legado/app/constant/AppConst.kt
  2. 8
      app/src/main/java/io/legado/app/constant/Pattern.kt
  3. 290
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  4. 110
      app/src/main/java/io/legado/app/utils/NetworkUtils.kt
  5. 22
      app/src/main/java/io/legado/app/utils/StringExtensions.kt

@ -2,6 +2,7 @@ package io.legado.app.constant
import io.legado.app.App
import io.legado.app.R
import javax.script.ScriptEngineManager
object AppConst {
const val channelIdDownload = "channel_download"
@ -11,6 +12,8 @@ object AppConst {
const val APP_TAG = "Legado"
const val RC_IMPORT_YUEDU_DATA = 100
val SCRIPT_ENGINE = ScriptEngineManager().getEngineByName("rhino")
val NOT_AVAILABLE = App.INSTANCE.getString(R.string.not_available)
}

@ -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)
}
}

@ -1,5 +1,7 @@
package io.legado.app.utils
import android.text.TextUtils
// import org.apache.commons.text.StringEscapeUtils
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
@ -7,6 +9,26 @@ fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
fun String.isAbsUrl() = this.startsWith("http://", true)
|| this.startsWith("https://", true)
fun String.isJson(): Boolean = kotlin.run {
var result = false
if (!TextUtils.isEmpty(this)) {
val str = this.trim()
if (str.startsWith("{") && str.endsWith("}")) {
result = true
} else if (str.startsWith("[") && str.endsWith("]")) {
result = true
}
}
return result
}
fun String.htmlFormat(): String = if (TextUtils.isEmpty(this)) "" else
this.replace("(?i)<(br[\\s/]*|/*p.*?|/*div.*?)>".toRegex(), "\n")// 替换特定标签为换行符
.replace("<[script>]*.*?>|&nbsp;".toRegex(), "")// 删除script标签对和空格转义符
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字
.replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字
.replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行
fun String.splitNotBlank(delim: String) = if (!this.contains(delim)) sequenceOf(this) else
this.split(delim).asSequence().map { it.trim() }.filterNot { it.isBlank() }

Loading…
Cancel
Save