改进筛选器起始字符位置查找规则,改为逐个比对,先匹配到“(”就拉取“()”平衡,先匹配到“[”就拉取“[]”平衡,避免()和[]嵌套时出现边缘情况。 跳转筛选器匹配优先顺序,从“(”优先改为“[”优先。pull/1100/head
parent
43545b11d5
commit
d0f87aee76
@ -1,496 +1,525 @@ |
|||||||
package io.legado.app.model.analyzeRule |
package io.legado.app.model.analyzeRule |
||||||
|
|
||||||
import android.text.TextUtils.join |
//通用的规则切分处理 |
||||||
import androidx.annotation.Keep |
|
||||||
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. |
|
||||||
* 书源规则解析 |
|
||||||
*/ |
|
||||||
@Keep |
|
||||||
class AnalyzeByJSoup(doc: Any) { |
|
||||||
companion object { |
|
||||||
/** |
|
||||||
* "class", "id", "tag", "text", "children" |
|
||||||
*/ |
|
||||||
val validKeys = arrayOf("class", "id", "tag", "text", "children") |
|
||||||
|
|
||||||
fun parse(doc: Any): Element { |
class RuleAnalyzer(data: String) { |
||||||
return when (doc) { |
|
||||||
is Element -> doc |
private var queue: String = data //被处理字符串 |
||||||
is JXNode -> if (doc.isElement) doc.asElement() else Jsoup.parse(doc.toString()) |
private var pos = 0 //处理到的位置 |
||||||
else -> Jsoup.parse(doc.toString()) |
|
||||||
} |
private var start = 0 //每次处理字段的开始 |
||||||
} |
private var step:Int = 0 //分割字符的长度 |
||||||
|
|
||||||
|
var elementsType = "" |
||||||
|
|
||||||
|
//当前平衡字段 |
||||||
|
fun currBalancedString( stepStart:Int = 1 , stepEnd:Int = 1): String { //stepStart平衡字符的起始分隔字串长度,stepEnd平衡字符的结束分隔字串长度 |
||||||
|
return queue.substring(start+stepStart,pos-stepEnd) //当前平衡字段 |
||||||
|
} |
||||||
|
|
||||||
|
//将pos重置为0,方便复用 |
||||||
|
fun reSetPos() { |
||||||
|
pos = 0 |
||||||
} |
} |
||||||
|
|
||||||
private var element: Element = parse(doc) |
//当前拉取字段 |
||||||
|
fun currString(): String { |
||||||
|
return queue.substring(start,pos) //当前拉取到的字段 |
||||||
|
} |
||||||
|
|
||||||
|
//剩余字串 |
||||||
|
private fun remainingString(): String { |
||||||
|
start = pos |
||||||
|
pos = queue.length |
||||||
|
return queue.substring(start) |
||||||
|
} |
||||||
|
|
||||||
/** |
/** |
||||||
* 获取列表 |
* pos位置回退 |
||||||
*/ |
*/ |
||||||
internal fun getElements(rule: String) = getElements(element, rule) |
fun back(num :Int = 0) { |
||||||
|
if(num == 0)pos = start //回退 |
||||||
|
else pos -= num |
||||||
|
} |
||||||
|
|
||||||
/** |
/** |
||||||
* 合并内容列表,得到内容 |
* pos位置后移 |
||||||
*/ |
*/ |
||||||
internal fun getString(ruleStr: String) = |
fun advance(num :Int = 1) { |
||||||
if(ruleStr.isEmpty()) null |
pos+=num |
||||||
else getStringList(ruleStr).takeIf { it.isNotEmpty() }?.joinToString("\n") |
} |
||||||
|
|
||||||
/** |
/** |
||||||
* 获取一个字符串 |
* 是否已无剩余字符? |
||||||
|
* @return 若剩余字串中已无字符则返回true |
||||||
*/ |
*/ |
||||||
internal fun getString0(ruleStr: String) = getStringList(ruleStr).let{ if ( it.isEmpty() ) "" else it[0] } |
val isEmpty: Boolean |
||||||
|
get() = queue.length - pos == 0 //是否处理到最后 |
||||||
|
|
||||||
/** |
/** |
||||||
* 获取所有内容列表 |
* 检索并返回首字符,但pos不变 |
||||||
|
* @return 首字符:若为空则为 0 号字符 |
||||||
*/ |
*/ |
||||||
internal fun getStringList(ruleStr: String): List<String> { |
fun peek(): Char { //检索首字符 |
||||||
|
return if (isEmpty) 0.toChar() else queue[pos] |
||||||
val textS = ArrayList<String>() |
} |
||||||
|
|
||||||
if (ruleStr.isEmpty()) return textS |
|
||||||
|
|
||||||
//拆分规则 |
|
||||||
val sourceRule = SourceRule(ruleStr) |
|
||||||
|
|
||||||
if (sourceRule.elementsRule.isEmpty()) { |
|
||||||
|
|
||||||
textS.add(element.data() ?: "") |
|
||||||
|
|
||||||
} else { |
|
||||||
|
|
||||||
val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) |
|
||||||
val ruleStrS = ruleAnalyzes.splitRule("&&","||" ,"%%") |
|
||||||
|
|
||||||
val results = ArrayList<List<String>>() |
|
||||||
for (ruleStrX in ruleStrS) { |
|
||||||
|
|
||||||
val temp: List<String>? = |
/** |
||||||
if (sourceRule.isCss) { |
* 消耗剩余字串中一个字符。 |
||||||
val lastIndex = ruleStrX.lastIndexOf('@') |
* @return 返回剩余字串中的下个字符。 |
||||||
getResultLast( |
*/ |
||||||
element.select(ruleStrX.substring(0, lastIndex)), |
fun consume(): Char { |
||||||
ruleStrX.substring(lastIndex + 1) |
return queue[pos++] |
||||||
) |
} |
||||||
} else { |
|
||||||
getResultList(ruleStrX) |
|
||||||
} |
|
||||||
|
|
||||||
if (!temp.isNullOrEmpty()) { |
/** |
||||||
|
* 字串与剩余字串是否匹配,不区分大小写 |
||||||
|
* @param seq 字符串被检查 |
||||||
|
* @return 若下字符串匹配返回 true |
||||||
|
*/ |
||||||
|
fun matches(seq: String): Boolean { |
||||||
|
return queue.regionMatches(pos, seq, 0, seq.length, ignoreCase = true) |
||||||
|
} |
||||||
|
|
||||||
results.add(temp) //!temp.isNullOrEmpty()时,results.isNotEmpty()为true |
/** |
||||||
|
* 从剩余字串中拉出一个字符串,直到但不包括匹配序列,或剩余字串用完。 |
||||||
|
* @param seq 分隔字符 **区分大小写** |
||||||
|
* @return 是否找到相应字段。 |
||||||
|
*/ |
||||||
|
fun consumeTo(seq: String,setStartPos:Boolean = true): Boolean { |
||||||
|
|
||||||
if (ruleAnalyzes.elementsType == "||") break |
if(setStartPos)start = pos //将处理到的位置设置为规则起点 |
||||||
|
val offset = queue.indexOf(seq, pos) |
||||||
|
return if (offset != -1) { |
||||||
|
pos = offset |
||||||
|
true |
||||||
|
} else false |
||||||
|
|
||||||
} |
|
||||||
} |
|
||||||
if (results.size > 0) { |
|
||||||
if ("%%" == ruleAnalyzes.elementsType) { |
|
||||||
for (i in results[0].indices) { |
|
||||||
for (temp in results) { |
|
||||||
if (i < temp.size) { |
|
||||||
textS.add(temp[i]) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} else { |
|
||||||
for (temp in results) { |
|
||||||
textS.addAll(temp) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return textS |
|
||||||
} |
} |
||||||
|
|
||||||
/** |
/** |
||||||
* 获取Elements |
* 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 |
||||||
|
* @param seq 匹配字符串序列 |
||||||
|
* @return 成功返回true并设置间隔,失败则直接返回fasle |
||||||
*/ |
*/ |
||||||
private fun getElements(temp: Element?, rule: String): Elements { |
fun consumeToAny(vararg seq:String): Boolean { |
||||||
|
|
||||||
if (temp == null || rule.isEmpty()) return Elements() |
start = pos |
||||||
|
|
||||||
val elements = Elements() |
while (!isEmpty) { |
||||||
|
|
||||||
val sourceRule = SourceRule(rule) |
for (s in seq) { |
||||||
val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) |
if (matches(s)) { |
||||||
val ruleStrS = ruleAnalyzes.splitRule("&&","||","%%") |
step = s.length //间隔数 |
||||||
|
return true //匹配就返回 true |
||||||
val elementsList = ArrayList<Elements>() |
|
||||||
if (sourceRule.isCss) { |
|
||||||
for (ruleStr in ruleStrS) { |
|
||||||
val tempS = temp.select(ruleStr) |
|
||||||
elementsList.add(tempS) |
|
||||||
if (tempS.size > 0 && ruleAnalyzes.elementsType == "||") { |
|
||||||
break |
|
||||||
} |
} |
||||||
} |
} |
||||||
} else { |
|
||||||
for (ruleStr in ruleStrS) { |
|
||||||
//将原getElementsSingle函数调用的函数的部分代码内联过来,方便简化getElementsSingle函数 |
|
||||||
|
|
||||||
val rsRule = RuleAnalyzer(ruleStr) |
|
||||||
|
|
||||||
if( rsRule.peek() =='@' || rsRule.peek() < '!' ) rsRule.advance() // 修剪当前规则之前的"@"或者空白符 |
|
||||||
|
|
||||||
val rs = rsRule.splitRule("@") |
|
||||||
|
|
||||||
val el = if (rs.size > 1) { |
|
||||||
val el = Elements() |
|
||||||
el.add(temp) |
|
||||||
for (rl in rs) { |
|
||||||
val es = Elements() |
|
||||||
for (et in el) { |
|
||||||
es.addAll(getElements(et, rl)) |
|
||||||
} |
|
||||||
el.clear() |
|
||||||
el.addAll(es) |
|
||||||
} |
|
||||||
el |
|
||||||
}else getElementsSingle(temp,ruleStr) |
|
||||||
|
|
||||||
elementsList.add(el) |
pos++ //逐个试探 |
||||||
if (el.size > 0 && ruleAnalyzes.elementsType == "||") { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
if (elementsList.size > 0) { |
|
||||||
if ("%%" == ruleAnalyzes.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 |
|
||||||
} |
|
||||||
|
|
||||||
/** |
pos = start //匹配失败,位置回退 |
||||||
* 1.支持阅读原有写法,':'分隔索引,!或.表示筛选方式,索引可为负数 |
|
||||||
* |
|
||||||
* 例如 tag.div.-1:10:2 或 tag.div!0:3 |
|
||||||
* |
|
||||||
* 2. 支持与jsonPath类似的[]索引写法 |
|
||||||
* |
|
||||||
* 格式形如 [it,it,。。。] 或 [!it,it,。。。] 其中[!开头表示筛选方式为排除,it为单个索引或区间。 |
|
||||||
* |
|
||||||
* 区间格式为 start:end 或 start:end:step,其中start为0可省略,end为-1可省略。 |
|
||||||
* |
|
||||||
* 索引,区间两端及间隔都支持负数 |
|
||||||
* |
|
||||||
* 例如 tag.div[-1, 3:-2:-10, 2] |
|
||||||
* |
|
||||||
* 特殊用法 tag.div[-1:0] 可在任意地方让列表反向 |
|
||||||
* |
|
||||||
* */ |
|
||||||
|
|
||||||
fun findIndexSet( rule:String ): IndexSet { |
return false |
||||||
|
} |
||||||
val indexSet = IndexSet() |
|
||||||
val rus = rule.trim{ it <= ' '} |
|
||||||
|
|
||||||
var len = rus.length |
|
||||||
var curInt: Int? //当前数字 |
|
||||||
var curMinus = false //当前数字是否为负 |
|
||||||
val curList = mutableListOf<Int?>() //当前数字区间 |
|
||||||
var l = "" //暂存数字字符串 |
|
||||||
|
|
||||||
val head = rus.last() == ']' //是否为常规索引写法 |
|
||||||
|
|
||||||
if(head){ //常规索引写法[index...] |
|
||||||
|
|
||||||
len-- //跳过尾部']' |
|
||||||
|
|
||||||
while (len-- >= 0) { //逆向遍历,可以无前置规则 |
|
||||||
|
|
||||||
var rl = rus[len] |
|
||||||
if (rl == ' ') continue //跳过空格 |
|
||||||
|
|
||||||
if (rl in '0'..'9') l += rl //将数值累接入临时字串中,遇到分界符才取出 |
|
||||||
else if (rl == '-') curMinus = true |
|
||||||
else { |
|
||||||
|
|
||||||
curInt = if (l.isEmpty()) null else if (curMinus) -l.toInt() else l.toInt() //当前数字 |
|
||||||
|
|
||||||
when (rl) { |
/** |
||||||
|
* 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 |
||||||
|
* @param seq 匹配字符序列 |
||||||
|
* @return 返回匹配位置 |
||||||
|
*/ |
||||||
|
private fun findToAny(vararg seq:Char): Int { |
||||||
|
|
||||||
':' -> curList.add(curInt) //区间右端或区间间隔 |
val start = pos //声明新变量记录临时起始位置,不更改类本身的起始位置 |
||||||
|
|
||||||
else -> { |
while (!isEmpty) { |
||||||
|
|
||||||
//为保证查找顺序,区间和单个索引都添加到同一集合 |
for (s in seq) if(queue[pos] == s) return pos //匹配则返回位置 |
||||||
if(curList.isEmpty())indexSet.indexs.add(curInt!!) |
|
||||||
else{ |
|
||||||
|
|
||||||
//列表最后压入的是区间右端,若列表有两位则最先压入的是间隔 |
pos++ //逐个试探 |
||||||
indexSet.indexs.add( Triple(curInt, curList.last(), if(curList.size == 2) curList.first() else 1) ) |
|
||||||
|
|
||||||
curList.clear() //重置临时列表,避免影响到下个区间的处理 |
} |
||||||
|
|
||||||
} |
pos = start //匹配失败,位置回退 |
||||||
|
|
||||||
if(rl == '!'){ |
return -1 |
||||||
indexSet.split='!' |
} |
||||||
do{ rl = rus[--len] } while (len > 0 && rl == ' ')//跳过所有空格 |
|
||||||
} |
|
||||||
|
|
||||||
if(rl == '[') return indexSet.apply { |
//其中js只要符合语法,就不用避开任何阅读关键字,自由发挥 |
||||||
beforeRule = rus.substring(0, len) |
fun chompJsBalanced(f: ((Char) -> Boolean?) = { |
||||||
} //遇到索引边界,返回结果 |
when (it) { |
||||||
|
'{' -> true //开始嵌套一层 |
||||||
|
'}' -> false //闭合一层嵌套 |
||||||
|
else -> null |
||||||
|
} |
||||||
|
} ): Boolean { |
||||||
|
start = pos |
||||||
|
var depth = 0 //嵌套深度 |
||||||
|
var bracketsDepth = 0 //[]嵌套深度 |
||||||
|
|
||||||
|
var inSingleQuote = false //单引号 |
||||||
|
var inDoubleQuote = false //双引号 |
||||||
|
var inOtherQuote = false //js原始字串分隔字符 |
||||||
|
var regex = false //正则 |
||||||
|
var commit = false //单行注释 |
||||||
|
var commits = false //多行注释 |
||||||
|
|
||||||
|
do { |
||||||
|
if (isEmpty) break |
||||||
|
var c = consume() |
||||||
|
if (c != '\\') { //非转义字符 |
||||||
|
if (c == '\'' && !commits && !commit && !regex && !inDoubleQuote && !inOtherQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 |
||||||
|
else if (c == '"' && !commits && !commit && !regex && !inSingleQuote && !inOtherQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 |
||||||
|
else if (c == '`' && !commits && !commit && !regex && !inSingleQuote && !inDoubleQuote) inOtherQuote = !inOtherQuote //匹配具有语法功能的'`' |
||||||
|
else if (c == '/' && !commits && !commit && !regex && !inSingleQuote && !inDoubleQuote && !inOtherQuote) { //匹配注释或正则起点 |
||||||
|
c = consume() |
||||||
|
when(c){ |
||||||
|
'/'->commit=true //匹配单行注释起点 |
||||||
|
'*'->commits=true //匹配多行注释起点 |
||||||
|
else ->regex=true //匹配正则起点 |
||||||
|
} |
||||||
|
} |
||||||
|
else if(commits && c == '*') { //匹配多行注释终点 |
||||||
|
c = consume() |
||||||
|
if(c == '/')commits = false |
||||||
|
} |
||||||
|
else if(regex && c == '/') { //正则的终点或[]平衡 |
||||||
|
|
||||||
if(rl != ',') break //非索引结构,跳出 |
when (c) { |
||||||
|
'/' -> regex = false//匹配正则终点 |
||||||
|
|
||||||
} |
//为了保证当open为( 且 close 为 )时,正则中[(]或[)]的合法性。故对[]这对在任何规则中都平衡的成对符号做匹配。 |
||||||
|
// 注:正则里[(]、[)]、[{]、[}]都是合法的,所以只有[]必须平衡。 |
||||||
|
'[' -> bracketsDepth++ //开始嵌套一层[] |
||||||
|
']' -> bracketsDepth-- //闭合一层嵌套[] |
||||||
} |
} |
||||||
|
|
||||||
l = "" //清空 |
|
||||||
curMinus = false //重置 |
|
||||||
} |
} |
||||||
} |
|
||||||
} else while (len-- >= 0) { //阅读原本写法,逆向遍历,可以无前置规则 |
|
||||||
|
|
||||||
val rl = rus[len] |
if (commits || commit || regex || inSingleQuote || inDoubleQuote || inOtherQuote) continue //语法单元未匹配结束,直接进入下个循环 |
||||||
if (rl == ' ') continue //跳过空格 |
|
||||||
|
|
||||||
if (rl in '0'..'9') l += rl //将数值累接入临时字串中,遇到分界符才取出 |
val fn = f(c) ?: continue |
||||||
else if (rl == '-') curMinus = true |
if (fn) depth++ else depth-- //嵌套或者闭合 |
||||||
else { |
|
||||||
|
|
||||||
if(rl == '!' || rl == '.' || rl == ':') { //分隔符或起始符 |
}else { //转义字符 |
||||||
|
var next = consume() //拉出被转义字符 |
||||||
|
if(commit && next == 'n') commit = false //匹配单行注释终点。当前为\,下个为n,表示换行 |
||||||
|
else if (!commits && !commit && next == '\\') { |
||||||
|
consume() //当前为\,下个为\,双重转义中"\\"表示转义字符本身,根据if条件"\\"字串不在注释中,则只能在字串或正则中 |
||||||
|
next = consume() //拉出下个字符,因为在双重转义的字串或正则中,类似于 \\/ 这样的结构才是转义结构 |
||||||
|
if(next == '\\')consume() //若为转义字符则继续拉出,因为双重转义中转义字符成对存在,即 \\\\ |
||||||
|
} |
||||||
|
} |
||||||
|
} while (depth > 0 || bracketsDepth >0) //拉出全部符合js语法的字段 |
||||||
|
|
||||||
indexSet.indexDefault.add(if (curMinus) -l.toInt() else l.toInt()) // 当前数字追加到列表 |
if(depth > 0 || bracketsDepth >0) start = pos |
||||||
|
|
||||||
if (rl != ':') return indexSet.apply { //rl == '!' || rl == '.' |
return pos > start |
||||||
split = rl |
} |
||||||
beforeRule = rus.substring(0, len) |
|
||||||
} |
|
||||||
|
|
||||||
}else break //非索引结构,跳出循环 |
/** |
||||||
|
* 在双重转义字串中拉出一个规则平衡组 |
||||||
|
*/ |
||||||
|
fun chompRuleBalanced(open: Char = '[', close: Char = ']',f: ((Char) ->Boolean?)? = null ): Boolean { |
||||||
|
start = pos |
||||||
|
var depth = 0 //嵌套深度 |
||||||
|
var otherDepth = 0 //其他对称符合嵌套深度 |
||||||
|
|
||||||
|
var inSingleQuote = false //单引号 |
||||||
|
var inDoubleQuote = false //双引号 |
||||||
|
|
||||||
|
do { |
||||||
|
if (isEmpty) break |
||||||
|
val c = consume() |
||||||
|
if (c != ESC) { //非转义字符 |
||||||
|
if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 |
||||||
|
else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 |
||||||
|
|
||||||
|
if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 |
||||||
|
|
||||||
|
if ( c == open )depth++ //开始嵌套一层 |
||||||
|
else if ( c== close) depth-- //闭合一层嵌套 |
||||||
|
else if(depth == 0 && f != null) { //处于默认嵌套中的非默认字符不需要平衡,仅depth为0时默认嵌套全部闭合,此字符才进行嵌套 |
||||||
|
val fn = f(c) ?: continue |
||||||
|
if (fn) otherDepth++ else otherDepth-- |
||||||
|
} |
||||||
|
|
||||||
l = "" //清空 |
}else { //转义字符 |
||||||
curMinus = false //重置 |
var next = consume() //拉出被转义字符,匹配\/、\"、\'等 |
||||||
|
if (next == ESC) { |
||||||
|
consume() //当前为\,下个为\,双重转义中"\\"表示转义字符本身,根据语法特征当前字段在字串或正则中 |
||||||
|
next = consume() //拉出下个字符,因为在双重转义的字串或正则中,类似于 \\/ 这样的结构才是转义结构 |
||||||
|
if(next == ESC)consume() //若为转义字符则继续拉出,因为双重转义中转义字符成对存在,即 \\\\ |
||||||
|
} |
||||||
} |
} |
||||||
|
} while (depth > 0 || otherDepth > 0) //拉出一个平衡字串 |
||||||
|
|
||||||
} |
return !(depth > 0 || otherDepth > 0) //平衡返回false,不平衡返回true |
||||||
|
|
||||||
return indexSet.apply{ |
|
||||||
split = ' ' |
|
||||||
beforeRule = rus } //非索引格式 |
|
||||||
} |
} |
||||||
|
|
||||||
/** |
/** |
||||||
* 获取Elements按照一个规则 |
* 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 |
||||||
|
* 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 |
||||||
*/ |
*/ |
||||||
private fun getElementsSingle(temp: Element, rule: String): Elements { |
tailrec fun splitRule(vararg split: String): Array<String>{ //首段匹配,elementsType为空 |
||||||
|
|
||||||
var elements = Elements() |
if(split.size == 1) { |
||||||
|
elementsType = split[0] //设置分割字串 |
||||||
|
step = elementsType.length //设置分隔符长度 |
||||||
|
return splitRule(arrayOf()) //仅一个分隔字串时,直接二段解析更快 |
||||||
|
}else if (!consumeToAny(* split)) return arrayOf(queue) //未找到分隔符 |
||||||
|
|
||||||
val fi = findIndexSet(rule) //执行索引列表处理器 |
val st = findToAny( '[','(' ) //查找筛选器 |
||||||
|
|
||||||
val (filterType,ruleStr) = fi //获取操作类型及非索引部分的规则字串 |
if(st == -1) { |
||||||
|
|
||||||
// val rulePc = rulePcx[0].trim { it <= ' ' }.split(">") |
var rule = arrayOf(queue.substring(0, pos)) //压入分隔的首段规则到数组 |
||||||
// jsoup中,当前节点是参与选择的,tag.div 与 tag.div@tag.div 结果相同 |
|
||||||
// 此处">"效果和“@”完全相同,且容易让人误解成选择子节点,实际并不是。以后不允许这种无意义的写法 |
|
||||||
|
|
||||||
val rules = ruleStr.split(".") |
elementsType = queue.substring(pos, pos + step) //设置组合类型 |
||||||
|
pos += step //跳过分隔符 |
||||||
|
|
||||||
elements.addAll( |
while (consumeToAny(* split)) { //循环切分规则压入数组 |
||||||
if(ruleStr.isEmpty()) temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 |
rule += queue.substring(start, pos) |
||||||
else when (rules[0]) { |
pos += step //跳过分隔符 |
||||||
"children" -> temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 |
} |
||||||
"class" -> temp.getElementsByClass(rules[1]) |
|
||||||
"tag" -> temp.getElementsByTag(rules[1]) |
|
||||||
"id" -> Collector.collect(Evaluator.Id(rules[1]), temp) |
|
||||||
"text" -> temp.getElementsContainingOwnText(rules[1]) |
|
||||||
else -> temp.select(ruleStr) |
|
||||||
} ) |
|
||||||
|
|
||||||
val indexSet = fi.getIndexs(elements.size) //传入元素数量,处理负数索引及索引越界问题,生成可用索引集合。 |
rule += queue.substring(pos) //将剩余字段压入数组末尾 |
||||||
|
|
||||||
if(filterType == '!'){ //排除 |
return rule |
||||||
|
} |
||||||
|
|
||||||
for (pcInt in indexSet) elements[pcInt] = null |
val rule = if(st >pos ){ //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 |
||||||
|
|
||||||
elements.removeAll(listOf(null)) //测试过,这样就行 |
var rule = arrayOf(queue.substring(0, pos)) //压入分隔的首段规则到数组 |
||||||
|
|
||||||
}else if(filterType == '.'){ //选择 |
elementsType = queue.substring(pos, pos + step) //设置组合类型 |
||||||
|
pos += step //跳过分隔符 |
||||||
|
|
||||||
val es = Elements() |
while (consumeToAny( * split ) && pos < st ) { //循环切分规则压入数组 |
||||||
|
rule += queue.substring(start, pos) |
||||||
|
pos += step //跳过分隔符 |
||||||
|
} |
||||||
|
|
||||||
for (pcInt in indexSet) es.add(elements[pcInt]) |
rule |
||||||
|
|
||||||
elements = es |
}else null |
||||||
|
|
||||||
} |
pos = st //位置推移到筛选器处 |
||||||
|
val next = if(queue[pos] == '[' ) ']' else ')' //平衡组末尾字符 |
||||||
|
|
||||||
return elements |
return if (rule == null) { //rule为空,首段未匹配完成 |
||||||
} |
|
||||||
|
|
||||||
/** |
if(!chompRuleBalanced(queue[pos],next)) throw Error(queue.substring(0, start)+"后未平衡") //拉出一个筛选器,不平衡则报错 |
||||||
* 获取内容列表 |
splitRule(* split) //递归调用首段匹配 |
||||||
*/ |
|
||||||
private fun getResultList(ruleStr: String): List<String>? { |
|
||||||
|
|
||||||
if (ruleStr.isEmpty()) return null |
} else { |
||||||
|
|
||||||
var elements = Elements() |
val start0 = start //记录当前规则开头位置 |
||||||
|
if(!chompRuleBalanced(queue[pos],next)) throw Error(queue.substring(0, start)+"后未平衡") //拉出一个筛选器,不平衡则报错 |
||||||
|
start = start0 //筛选器的开头不是本段规则开头,故恢复开头设置 |
||||||
|
splitRule(rule) //首段已匹配,但当前段匹配未完成,调用二段匹配 |
||||||
|
|
||||||
elements.add(element) |
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
val rule = RuleAnalyzer(ruleStr) //创建解析 |
@JvmName("splitRuleNext") |
||||||
|
private tailrec fun splitRule(rules:Array<String>): Array<String>{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 |
||||||
|
|
||||||
while( rule.peek() =='@' || rule.peek() < '!' ) rule.advance() // 修剪当前规则之前的"@"或者空白符 |
if (!consumeTo(elementsType,false)) return rules + queue.substring(start) //此处consumeTo(...)开始位置不是规则的开始位置,start沿用上次设置 |
||||||
|
|
||||||
val rules = rule.splitRule("@") // 切割成列表 |
val st = findToAny( '[','(' ) //查找筛选器 |
||||||
|
|
||||||
val last = rules.size - 1 |
if(st == -1) { |
||||||
for (i in 0 until last) { |
var rule = rules + queue.substring(start, pos) //压入本次分隔的首段规则到数组 |
||||||
val es = Elements() |
pos += step //跳过分隔符 |
||||||
for (elt in elements) { |
while (consumeTo(elementsType)) { //循环切分规则压入数组 |
||||||
es.addAll(getElementsSingle(elt, rules[i])) |
rule += queue.substring(start, pos) |
||||||
|
pos += step //跳过分隔符 |
||||||
} |
} |
||||||
elements.clear() |
rule += queue.substring(pos) //将剩余字段压入数组末尾 |
||||||
elements = es |
return rule |
||||||
} |
} |
||||||
return if (elements.isEmpty()) null else getResultLast(elements, rules[last]) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
val rule = if(st > pos ){//先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 |
||||||
* 根据最后一个规则获取内容 |
var rule = rules + queue.substring(start, pos) //压入本次分隔的首段规则到数组 |
||||||
*/ |
pos += step //跳过分隔符 |
||||||
private fun getResultLast(elements: Elements, lastRule: String): List<String> { |
while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 |
||||||
val textS = ArrayList<String>() |
rule += queue.substring(start, pos) |
||||||
try { |
pos += step //跳过分隔符 |
||||||
when (lastRule) { |
} |
||||||
"text" -> for (element in elements) { |
rule |
||||||
textS.add(element.text()) |
}else rules |
||||||
} |
|
||||||
"textNodes" -> for (element in elements) { |
|
||||||
val tn = arrayListOf<String>() |
|
||||||
val contentEs = element.textNodes() |
|
||||||
for (item in contentEs) { |
|
||||||
val temp = item.text().trim { it <= ' ' } |
|
||||||
if (temp.isNotEmpty()) { |
|
||||||
tn.add(temp) |
|
||||||
} |
|
||||||
} |
|
||||||
textS.add(join("\n", tn)) |
|
||||||
} |
|
||||||
"ownText" -> for (element in elements) { |
|
||||||
textS.add(element.ownText()) |
|
||||||
} |
|
||||||
"html" -> { |
|
||||||
elements.select("script").remove() |
|
||||||
elements.select("style").remove() |
|
||||||
val html = elements.outerHtml() |
|
||||||
textS.add(html) |
|
||||||
} |
|
||||||
"all" -> textS.add(elements.outerHtml()) |
|
||||||
else -> for (element in elements) { |
|
||||||
|
|
||||||
val url = element.attr(lastRule) |
pos = st //位置推移到筛选器处 |
||||||
|
val next = if(queue[pos] == '[' ) ']' else ')' //平衡组末尾字符 |
||||||
|
|
||||||
if(url.isEmpty() || textS.contains(url)) break |
val start0 = start //记录当前规则开头位置 |
||||||
|
if(!chompRuleBalanced(queue[pos],next)) throw Error(queue.substring(0, start)+"后未平衡") //拉出一个筛选器,不平衡时返回true,表示未平衡 |
||||||
|
start = start0 //筛选器平衡,但筛选器的开头不是当前规则开头,故恢复开头设置 |
||||||
|
|
||||||
textS.add(url) |
return splitRule(rule) //递归匹配 |
||||||
} |
|
||||||
} |
|
||||||
} catch (e: Exception) { |
|
||||||
e.printStackTrace() |
|
||||||
} |
|
||||||
|
|
||||||
return textS |
|
||||||
} |
} |
||||||
|
|
||||||
data class IndexSet(var split:Char = '.', |
|
||||||
var beforeRule:String = "", |
|
||||||
val indexDefault:MutableList<Int> = mutableListOf(), |
|
||||||
val indexs:MutableList<Any> = mutableListOf()){ |
|
||||||
|
|
||||||
fun getIndexs(len:Int): MutableSet<Int> { |
/** |
||||||
|
* 替换内嵌规则 |
||||||
|
* @param inner 起始标志,如{$. 或 {{ |
||||||
|
* @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 |
||||||
|
* @param endStep 不属于规则部分的后置字符长度,如}}长度为2 |
||||||
|
* @param fr 查找到内嵌规则时,用于解析的函数 |
||||||
|
* |
||||||
|
* */ |
||||||
|
fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->String?): String { |
||||||
|
|
||||||
val indexSet = mutableSetOf<Int>() |
val start0 = pos //规则匹配前起点 |
||||||
|
|
||||||
val lastIndexs = (indexDefault.size - 1).takeIf { it !=-1 } ?: indexs.size -1 |
val st = StringBuilder() |
||||||
|
|
||||||
|
while (!isEmpty && consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true |
||||||
|
|
||||||
if(indexs.isEmpty())for (ix in lastIndexs downTo 0 ){ //indexs为空,表明是非[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 |
val start1 = start //记录拉取前起点 |
||||||
|
|
||||||
val it = indexDefault[ix] |
if (chompRuleBalanced {//拉出一个以[]为默认嵌套、以{}为补充嵌套的平衡字段 |
||||||
if(it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 |
when (it) { |
||||||
else if(it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 |
'{' -> true |
||||||
|
'}' -> false |
||||||
|
else -> null |
||||||
|
} |
||||||
|
}) { |
||||||
|
val frv= fr(currBalancedString(startStep,endStep)) |
||||||
|
if(frv != null) { |
||||||
|
|
||||||
|
st.append(queue.substring(start1,start)+frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 |
||||||
|
continue //获取内容成功,继续选择下个内嵌规则 |
||||||
|
|
||||||
}else for (ix in lastIndexs downTo 0 ){ //indexs不空,表明是[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 |
} |
||||||
|
} |
||||||
|
|
||||||
if(indexs[ix] is Triple<*, *, *>){ //区间 |
start = start1 //拉出字段不平衡,重置起点 |
||||||
|
pos = start + inner.length //拉出字段不平衡,inner只是个普通字串,规则回退到开头,并跳到此inner后继续匹配 |
||||||
|
|
||||||
val (startx, endx, stepx) = indexs[ix] as Triple<Int?, Int?, Int> //还原储存时的类型 |
} |
||||||
|
|
||||||
val start = if (startx == null) 0 //左端省略表示0 |
//匹配前起点与当前规则起点相同,证明无替换成功的内嵌规则,返回空字符串。否则返回替换后的字符串 |
||||||
else if (startx >= 0) if (startx < len) startx else len - 1 //右端越界,设置为最大索引 |
return if(start0 == start) "" else { |
||||||
else if (-startx <= len) len + startx /* 将负索引转正 */ else 0 //左端越界,设置为最小索引 |
st.append(remainingString()) //压入剩余字符串 |
||||||
|
st.toString() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
val end = if (endx == null) len - 1 //右端省略表示 len - 1 |
// /** |
||||||
else if (endx >= 0) if (endx < len) endx else len - 1 //右端越界,设置为最大索引 |
// * 匹配并返回标签中的属性键字串(字母、数字、-、_、:) |
||||||
else if (-endx <= len) len + endx /* 将负索引转正 */ else 0 //左端越界,设置为最小索引 |
// * @return 属性键字串 |
||||||
|
// */ |
||||||
|
// fun consumeAttributeKey(start:Int = pos): String { |
||||||
|
// while (!isEmpty && (Character.isLetterOrDigit(queue[pos]) || matchesAny('-', '_', ':'))) pos++ |
||||||
|
// return queue.substring(start, pos) |
||||||
|
// } |
||||||
|
|
||||||
|
// fun splitRule(query:String,item:String = "other",listItem:String = "allInOne"):String{ |
||||||
|
// |
||||||
|
// val cuurItem = item //当前项类型,list->列表项 mulu->章节列表项 url->链接项 search->搜索链接项 find发现链接列表项 other->其他项 |
||||||
|
// val cuurList = listItem//当前界面总列表项类型,allInOne,json,xml,kotin,java |
||||||
|
// var Reverse = false //是否反转列表 |
||||||
|
// |
||||||
|
// consumeWhitespace() //消耗开头空白 |
||||||
|
// var fisrt = consume() //拉出并消费首字符 |
||||||
|
// |
||||||
|
// when(item){ |
||||||
|
// "search" -> |
||||||
|
// "find" -> |
||||||
|
// "mulu" -> if(fisrt == '-'){ |
||||||
|
// Reverse=true //开启反转 |
||||||
|
// consumeWhitespace() //拉出所有空白符 |
||||||
|
// fisrt = consume() //首字符后移 |
||||||
|
// } |
||||||
|
// else -> |
||||||
|
// |
||||||
|
// } |
||||||
|
// |
||||||
|
// return query |
||||||
|
// } |
||||||
|
|
||||||
if (start == end || stepx >= len) { //两端相同,区间里只有一个数。或间隔过大,区间实际上仅有首位 |
companion object { |
||||||
|
/** |
||||||
|
* 转义字符 |
||||||
|
*/ |
||||||
|
private const val ESC = '\\' |
||||||
|
|
||||||
indexSet.add(start) |
/** |
||||||
continue |
* 阅读共有分隔字串起始部分 |
||||||
|
* "##","@@","{{","{[","<js>", "@js:" |
||||||
|
*/ |
||||||
|
val splitList =arrayOf("##","@@","{{","{[","<js>", "@js:") |
||||||
|
|
||||||
} |
/** |
||||||
|
* 发现‘名称-链接’分隔字串 |
||||||
|
* "::" |
||||||
|
*/ |
||||||
|
const val splitListFaXian = "::" |
||||||
|
|
||||||
val step = if (stepx > 0) stepx else if (-stepx < len) stepx + len else 1 //最小正数间隔为1 |
/** |
||||||
|
* 目录专有起始字符 |
||||||
|
* "-" |
||||||
|
*/ |
||||||
|
const val splitListMulu = "-" |
||||||
|
|
||||||
//将区间展开到集合中,允许列表反向。 |
/** |
||||||
indexSet.addAll(if (end > start) start..end step step else start downTo end step step) |
* 结果为元素列表的 all in one 模式起始字符 |
||||||
|
* "+" |
||||||
|
*/ |
||||||
|
const val splitListTongYi = "+" |
||||||
|
|
||||||
}else{//单个索引 |
/** |
||||||
|
* 结果为元素列表的项的同规则组合结构 |
||||||
|
* "||","&&","%%" |
||||||
|
*/ |
||||||
|
val splitListReSplit = arrayOf("||","&&","%%") |
||||||
|
|
||||||
val it = indexs[ix] as Int //还原储存时的类型 |
/** |
||||||
|
* js脚本结束字串 |
||||||
|
* "</js>" |
||||||
|
*/ |
||||||
|
const val splitListEndJS = "</js>" |
||||||
|
|
||||||
if(it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 |
/** |
||||||
else if(it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 |
*内嵌js结束字串 |
||||||
|
* "}}" |
||||||
|
*/ |
||||||
|
const val splitListEndInnerJS = "}}" |
||||||
|
|
||||||
} |
/** |
||||||
|
* 内嵌规则结束字串 |
||||||
|
* "]}" |
||||||
|
*/ |
||||||
|
const val splitListEndInnerRule = "]}" |
||||||
|
|
||||||
} |
/** |
||||||
|
* '[', ']', '(', ')','{','}' |
||||||
|
*/ |
||||||
|
val splitListPublic = charArrayOf('[', ']', '(', ')','{','}') |
||||||
|
|
||||||
return indexSet |
/** |
||||||
|
* '*',"/","//",":","::","@","|","@xpath:" |
||||||
|
*/ |
||||||
|
val splitListXpath = arrayOf("*","/","//",":","::","@","|","@xpath:") |
||||||
|
|
||||||
} |
/** |
||||||
|
* '*','$',".","..", "@json:" |
||||||
|
*/ |
||||||
|
val splitListJson = arrayOf('*','$',".","..", "@json:") |
||||||
|
|
||||||
} |
/** |
||||||
|
* '*',"+","~",".",",","|","@","@css:",":" |
||||||
|
*/ |
||||||
|
val splitListCss = arrayOf('*',"+","~",".",",","|","@","@css:",":") |
||||||
|
|
||||||
|
/** |
||||||
|
* "-",".","!","@","@@" |
||||||
|
*/ |
||||||
|
val splitListDefault = arrayOf("-",".","!","@","@@") |
||||||
|
|
||||||
internal inner class SourceRule(ruleStr: String) { |
|
||||||
var isCss = false |
|
||||||
var elementsRule: String = if (ruleStr.startsWith("@CSS:", true)) { |
|
||||||
isCss = true |
|
||||||
ruleStr.substring(5).trim { it <= ' ' } |
|
||||||
} else { |
|
||||||
ruleStr |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue