pull/32/head
parent
bf0f6438f9
commit
5437bf824a
@ -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 |
||||
|
Loading…
Reference in new issue