pull/32/head
kunfei 6 years ago
parent bf0f6438f9
commit 5437bf824a
  1. 1
      app/build.gradle
  2. 435
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  3. 160
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt
  4. 2
      app/src/main/java/io/legado/app/model/analyzeRule/analyzeJsoup.kt
  5. 5
      app/src/main/java/io/legado/app/utils/StringExtensions.kt

@ -115,6 +115,7 @@ dependencies {
// //
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.code.gson:gson:2.8.5'
implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.jsoup:jsoup:1.12.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
// implementation 'org.apache.commons:commons-text:1.6' // implementation 'org.apache.commons:commons-text:1.6'
// Default json parser for jsonpath // Default json parser for jsonpath
implementation 'net.minidev:json-smart:2.3' implementation 'net.minidev:json-smart:2.3'

@ -0,0 +1,435 @@
package io.legado.app.model.analyzeRule
import android.text.TextUtils.isEmpty
import android.text.TextUtils.join
import io.legado.app.utils.startWithIgnoreCase
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.select.Collector
import org.jsoup.select.Elements
import org.jsoup.select.Evaluator
import org.seimicrawler.xpath.JXNode
import java.util.*
/**
* Created by GKF on 2018/1/25.
* 书源规则解析
*/
class AnalyzeByJSoup {
private var element: Element? = null
fun parse(doc: Any): AnalyzeByJSoup {
if (doc is Element) {
element = doc
} else if (doc is JXNode) {
if (doc.isElement) {
element = doc.asElement()
} else {
element = Jsoup.parse(doc.value().toString())
}
} else {
element = Jsoup.parse(doc.toString())
}
return this
}
/**
* 获取列表
*/
internal fun getElements(rule: String): Elements {
return getElements(element, rule)
}
/**
* 合并内容列表,得到内容
*/
internal fun getString(ruleStr: String): String? {
if (isEmpty(ruleStr)) {
return null
}
val textS = getStringList(ruleStr)
return if (textS.isEmpty()) {
null
} else join("\n", textS).trim { it <= ' ' }
}
/**
* 获取一个字符串
*/
internal fun getString0(ruleStr: String): String {
val urlList = getStringList(ruleStr)
return if (urlList.isNotEmpty()) {
urlList[0]
} else ""
}
/**
* 获取所有内容列表
*/
internal fun getStringList(ruleStr: String): List<String> {
val textS = ArrayList<String>()
if (isEmpty(ruleStr)) {
return textS
}
//拆分规则
val sourceRule = SourceRule(ruleStr)
if (isEmpty(sourceRule.elementsRule)) {
textS.add(element!!.data())
} else {
val elementsType: String
val ruleStrS: Array<String>
when {
sourceRule.elementsRule.contains("&") -> {
elementsType = "&"
ruleStrS =
sourceRule.elementsRule.split("&+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
sourceRule.elementsRule.contains("%%") -> {
elementsType = "%"
ruleStrS =
sourceRule.elementsRule.split("%%".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
else -> {
elementsType = "|"
ruleStrS = if (sourceRule.isCss) {
sourceRule.elementsRule.split("\\|\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else {
sourceRule.elementsRule.split("\\|+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
}
}
val results = ArrayList<List<String>>()
for (ruleStrX in ruleStrS) {
val temp: List<String>?
temp = if (sourceRule.isCss) {
val lastIndex = ruleStrX.lastIndexOf('@')
getResultLast(
element!!.select(ruleStrX.substring(0, lastIndex)),
ruleStrX.substring(lastIndex + 1)
)
} else {
getResultList(ruleStrX)
}
if (temp != null && temp.isNotEmpty()) {
results.add(temp)
if (results.isNotEmpty() && elementsType == "|") {
break
}
}
}
if (results.size > 0) {
if ("%" == elementsType) {
for (i in 0 until results[0].size) {
for (temp in results) {
if (i < temp.size) {
textS.add(temp[i])
}
}
}
} else {
for (temp in results) {
textS.addAll(temp)
}
}
}
}
if (!isEmpty(sourceRule.replaceRegex)) {
val tempList = ArrayList(textS)
textS.clear()
for (text in tempList) {
val tmpText = text.replace(sourceRule.replaceRegex.toRegex(), sourceRule.replacement)
if (tmpText.isNotEmpty()) {
textS.add(tmpText)
}
}
}
return textS
}
/**
* 获取Elements
*/
private fun getElements(temp: Element?, rule: String): Elements {
val elements = Elements()
if (temp == null || isEmpty(rule)) {
return elements
}
val sourceRule = SourceRule(rule)
val elementsType: String
val ruleStrS: Array<String>
when {
sourceRule.elementsRule.contains("&") -> {
elementsType = "&"
ruleStrS = sourceRule.elementsRule.split("&+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
sourceRule.elementsRule.contains("%") -> {
elementsType = "%"
ruleStrS = sourceRule.elementsRule.split("%+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
else -> {
elementsType = "|"
ruleStrS = if (sourceRule.isCss) {
sourceRule.elementsRule.split("\\|\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else {
sourceRule.elementsRule.split("\\|+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
}
}
val elementsList = ArrayList<Elements>()
if (sourceRule.isCss) {
for (ruleStr in ruleStrS) {
val tempS = temp.select(ruleStr)
elementsList.add(tempS)
if (tempS.size > 0 && elementsType == "|") {
break
}
}
} else {
for (ruleStr in ruleStrS) {
val tempS = getElementsSingle(temp, ruleStr)
elementsList.add(tempS)
if (tempS.size > 0 && elementsType == "|") {
break
}
}
}
if (elementsList.size > 0) {
if ("%" == elementsType) {
for (i in 0 until elementsList[0].size) {
for (es in elementsList) {
if (i < es.size) {
elements.add(es[i])
}
}
}
} else {
for (es in elementsList) {
elements.addAll(es)
}
}
}
return elements
}
private fun filterElements(elements: Elements, rules: Array<String>?): Elements {
if (rules == null || rules.size < 2) return elements
val selectedEls = Elements()
for (ele in elements) {
var isOk = false
when (rules[0]) {
"class" -> isOk = ele.getElementsByClass(rules[1]).size > 0
"id" -> isOk = ele.getElementById(rules[1]) != null
"tag" -> isOk = ele.getElementsByTag(rules[1]).size > 0
"text" -> isOk = ele.getElementsContainingOwnText(rules[1]).size > 0
}
if (isOk) {
selectedEls.add(ele)
}
}
return selectedEls
}
/**
* 获取Elements按照一个规则
*/
private fun getElementsSingle(temp: Element, rule: String): Elements {
val elements = Elements()
try {
val rs = rule.trim { it <= ' ' }.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (rs.size > 1) {
elements.add(temp)
for (rl in rs) {
val es = Elements()
for (et in elements) {
es.addAll(getElements(et, rl))
}
elements.clear()
elements.addAll(es)
}
} else {
val rulePcx = rule.split("!".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val rulePc =
rulePcx[0].trim { it <= ' ' }.split(">".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val rules =
rulePc[0].trim { it <= ' ' }.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
var filterRules: Array<String>? = null
var needFilterElements = rulePc.size > 1 && !isEmpty(rulePc[1].trim { it <= ' ' })
if (needFilterElements) {
filterRules = rulePc[1].trim { it <= ' ' }.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
filterRules[0] = filterRules[0].trim { it <= ' ' }
val validKeys = listOf("class", "id", "tag", "text")
if (filterRules.size < 2 || !validKeys.contains(filterRules[0]) || isEmpty(filterRules[1].trim { it <= ' ' })) {
needFilterElements = false
}
filterRules[1] = filterRules[1].trim { it <= ' ' }
}
when (rules[0]) {
"children" -> {
var children = temp.children()
if (needFilterElements)
children = filterElements(children, filterRules)
elements.addAll(children)
}
"class" -> {
var elementsByClass = temp.getElementsByClass(rules[1])
if (rules.size == 3) {
val index = Integer.parseInt(rules[2])
if (index < 0) {
elements.add(elementsByClass[elementsByClass.size + index])
} else {
elements.add(elementsByClass[index])
}
} else {
if (needFilterElements)
elementsByClass = filterElements(elementsByClass, filterRules)
elements.addAll(elementsByClass)
}
}
"tag" -> {
var elementsByTag = temp.getElementsByTag(rules[1])
if (rules.size == 3) {
val index = Integer.parseInt(rules[2])
if (index < 0) {
elements.add(elementsByTag[elementsByTag.size + index])
} else {
elements.add(elementsByTag[index])
}
} else {
if (needFilterElements)
elementsByTag = filterElements(elementsByTag, filterRules)
elements.addAll(elementsByTag)
}
}
"id" -> {
var elementsById = Collector.collect(Evaluator.Id(rules[1]), temp)
if (rules.size == 3) {
val index = Integer.parseInt(rules[2])
if (index < 0) {
elements.add(elementsById[elementsById.size + index])
} else {
elements.add(elementsById[index])
}
} else {
if (needFilterElements)
elementsById = filterElements(elementsById, filterRules)
elements.addAll(elementsById)
}
}
"text" -> {
var elementsByText = temp.getElementsContainingOwnText(rules[1])
if (needFilterElements)
elementsByText = filterElements(elementsByText, filterRules)
elements.addAll(elementsByText)
}
else -> elements.addAll(temp.select(rulePcx[0]))
}
if (rulePcx.size > 1) {
val rulePcs = rulePcx[1].split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (pc in rulePcs) {
val pcInt = Integer.parseInt(pc)
if (pcInt < 0 && elements.size + pcInt >= 0) {
elements[elements.size + pcInt] = null
} else if (Integer.parseInt(pc) < elements.size) {
elements[Integer.parseInt(pc)] = null
}
}
val es = Elements()
es.add(null)
elements.removeAll(es)
}
}
} catch (ignore: Exception) {
}
return elements
}
/**
* 获取内容列表
*/
private fun getResultList(ruleStr: String): List<String>? {
if (isEmpty(ruleStr)) {
return null
}
var elements = Elements()
elements.add(element)
val rules = ruleStr.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (i in 0 until rules.size - 1) {
val es = Elements()
for (elt in elements) {
es.addAll(getElementsSingle(elt, rules[i]))
}
elements.clear()
elements = es
}
return if (elements.isEmpty()) {
null
} else getResultLast(elements, rules[rules.size - 1])
}
/**
* 根据最后一个规则获取内容
*/
private fun getResultLast(elements: Elements, lastRule: String): List<String> {
val textS = ArrayList<String>()
try {
when (lastRule) {
"text" -> for (element in elements) {
val text = element.text()
textS.add(text)
}
"textNodes" -> for (element in elements) {
val contentEs = element.textNodes()
for (i in contentEs.indices) {
val temp = contentEs[i].text().trim { it <= ' ' }
if (!isEmpty(temp)) {
textS.add(temp)
}
}
}
"ownText", "html" -> {
elements.select("script").remove()
val html = elements.html()
textS.add(html)
}
else -> for (element in elements) {
val url = element.attr(lastRule)
if (!isEmpty(url) && !textS.contains(url)) {
textS.add(url)
}
}
}
} catch (ignore: Exception) {
}
return textS
}
internal inner class SourceRule(ruleStr: String) {
var isCss = false
var elementsRule: String
var replaceRegex = ""
var replacement = ""
init {
if (startWithIgnoreCase(ruleStr, "@CSS:")) {
isCss = true
elementsRule = ruleStr.substring(5).trim { it <= ' ' }
} else {
val ruleStrS: Array<String> =
ruleStr.trim { it <= ' ' }.split("#".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
//分离正则表达式
elementsRule = ruleStrS[0]
if (ruleStrS.size > 1) {
replaceRegex = ruleStrS[1]
}
if (ruleStrS.size > 2) {
replacement = ruleStrS[2]
}
}
}
}
}

@ -0,0 +1,160 @@
package io.legado.app.model.analyzeRule
import android.text.TextUtils
import org.jsoup.nodes.Document
import org.seimicrawler.xpath.JXDocument
import org.seimicrawler.xpath.JXNode
import java.util.*
class AnalyzeByXPath {
private var jxDocument: JXDocument? = null
fun parse(doc: Any): AnalyzeByXPath {
if (doc is Document) {
jxDocument = JXDocument.create(doc)
} else {
var html = doc.toString()
// 给表格标签添加完整的框架结构,否则会丢失表格标签;html标准不允许表格标签独立在table之外
if (html.endsWith("</td>")) {
html = String.format("<tr>%s</tr>", html)
}
if (html.endsWith("</tr>") || html.endsWith("</tbody>")) {
html = String.format("<table>%s</table>", html)
}
jxDocument = JXDocument.create(html)
}
return this
}
internal fun getElements(xPath: String): List<JXNode>? {
if (TextUtils.isEmpty(xPath)) {
return null
}
val jxNodes = ArrayList<JXNode>()
val elementsType: String
val rules: Array<String>
if (xPath.contains("&&")) {
rules = xPath.split("&&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "&"
} else if (xPath.contains("%%")) {
rules = xPath.split("%%".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "%"
} else {
rules = xPath.split("\\|\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "|"
}
if (rules.size == 1) {
return jxDocument!!.selN(rules[0])
} else {
val results = ArrayList<List<JXNode>>()
for (rl in rules) {
val temp = getElements(rl)
if (temp != null && temp.isNotEmpty()) {
results.add(temp)
if (temp.isNotEmpty() && elementsType == "|") {
break
}
}
}
if (results.size > 0) {
if ("%" == elementsType) {
for (i in 0 until results[0].size) {
for (temp in results) {
if (i < temp.size) {
jxNodes.add(temp[i])
}
}
}
} else {
for (temp in results) {
jxNodes.addAll(temp)
}
}
}
}
return jxNodes
}
internal fun getStringList(xPath: String): List<String> {
val result = ArrayList<String>()
val elementsType: String
val rules: Array<String>
if (xPath.contains("&&")) {
rules = xPath.split("&&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "&"
} else if (xPath.contains("%%")) {
rules = xPath.split("%%".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "%"
} else {
rules = xPath.split("\\|\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "|"
}
if (rules.size == 1) {
val jxNodes = jxDocument!!.selN(xPath)
for (jxNode in jxNodes) {
/*if(jxNode.isString()){
result.add(String.valueOf(jxNode));
}*/
result.add(jxNode.toString())
}
return result
} else {
val results = ArrayList<List<String>>()
for (rl in rules) {
val temp = getStringList(rl)
if (temp.isNotEmpty()) {
results.add(temp)
if (temp.isNotEmpty() && elementsType == "|") {
break
}
}
}
if (results.size > 0) {
if ("%" == elementsType) {
for (i in 0 until results[0].size) {
for (temp in results) {
if (i < temp.size) {
result.add(temp[i])
}
}
}
} else {
for (temp in results) {
result.addAll(temp)
}
}
}
}
return result
}
fun getString(rule: String): String? {
val rules: Array<String>
val elementsType: String
if (rule.contains("&&")) {
rules = rule.split("&&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "&"
} else {
rules = rule.split("\\|\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
elementsType = "|"
}
if (rules.size == 1) {
/*Object object = jxDocument.selOne(rule);
result = object instanceof Element ? ((Element) object).html() : (String) object;*/
val jxNodes = jxDocument!!.selN(rule) ?: return null
return TextUtils.join(",", jxNodes)
} else {
val sb = StringBuilder()
for (rl in rules) {
val temp = getString(rl)
if (!TextUtils.isEmpty(temp)) {
sb.append(temp)
if (elementsType == "|") {
break
}
}
}
return sb.toString()
}
}
}

@ -1,2 +0,0 @@
package io.legado.app.model.analyzeRule

@ -10,4 +10,7 @@ fun String.isAbsUrl() = this.startsWith("http://", true)
fun String.splitNotBlank(delim: String) = if (!this.contains(delim)) sequenceOf(this) else fun String.splitNotBlank(delim: String) = if (!this.contains(delim)) sequenceOf(this) else
this.split(delim).asSequence().map { it.trim() }.filterNot { it.isBlank() } this.split(delim).asSequence().map { it.trim() }.filterNot { it.isBlank() }
// fun String.unescapeJson() = StringEscapeUtils.unescapeJson(this) fun startWithIgnoreCase(src: String?, obj: String?): Boolean {
if (src == null || obj == null) return false
return if (obj.length > src.length) false else src.substring(0, obj.length).equals(obj, ignoreCase = true)
}
Loading…
Cancel
Save