# qn_read_rule
**Repository Path**: qbrid/qn_read_rule
## Basic Information
- **Project Name**: qn_read_rule
- **Description**: 青鸟站点规则库
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 3
- **Forks**: 0
- **Created**: 2023-07-06
- **Last Updated**: 2025-04-02
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 青鸟站点规则
>以下为青鸟站点的解析规则
>方便大家自行添加自己想适配的站点
>本手册分为 [环境准备]、[站点规则]、[使用案例] 三个章节
## 环境准备
微光阅读App,版本 V1.0.0 + 120及以上。
### 站点规则
______
青鸟站点规则分为三类
+ JSON文本站点规则
使用类似阅读的源规则进行声明的JSON文本规则,适用于简单及复杂站点
+ JavaScript站点规则
通过实现规则阅读接口,使用JS语言进行编程的高级动态规则
+ 动态Dart站点规则(存在部分缺陷,暂时不开放)
通过实现规则阅读接口,使用Dart语言进行编程的高级动态规则
## 文本站点规则
文本站点规则,格式为Json,范例如下:
```json
{
"siteId": "http://m.duqu.com", // 站点ID,站点唯一标识,每个站点不可重复
"info": {
"type": "book", // 站点类型。 书籍为 book 、听书为 listenbook 、漫画为 comic
"showName": "读趣", // 站点显示名称
"group": "微光", // 站点分组
"desc": "", // 站点描述
"updateTime": 1710552872211, // 站点更新时间,精确到毫秒的时间戳
"versionNumb": "103", // 站点版本
"supportSearch": true, // 是否支持 搜索
"supportExplore": true, // 是否支持 发现
"isEncrypt": false, // 是否支持 站点加密
"enable": true, // 站点是否可用
"vpnWebsite": "" // 是否需要VPN支持
},
"exploreUrl": "玄幻::http://m.duqu.net/sort/1/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n武侠修真::http://m.duqu.net/sort/2/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n都市言情::http://m.duqu.net/sort/3/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n历史军事::http://m.duqu.net/sort/4/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n侦探推理::http://m.duqu.net/sort/5/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n网游动漫::http://m.duqu.net/sort/6/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n科幻小说::http://m.duqu.net/sort/7/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n恐怖灵异::http://m.duqu.net/sort/8/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n言情小说::http://m.duqu.net/sort/9/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n其它类型::http://m.duqu.net/sort/10/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n子部::http://m.duqu.net/sort/13/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}", // 发现页标签列表,格式为 TAG::URL
"ruleExplore": {
"bookList": "//*[@id='jieqi_page_contents']/div // 发现页 获取书籍列表
"bookName": "//h4/text()", // 书名 ,获取得到的书名不能为空,否则本书将被过滤。
"author": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$1}}#,,,#.*\\|(.*)#$1#", // 作者
"cover": "//img/@src", // 封面
"desc": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$2}}#", // 描述
"tags": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$1}}#,,,#(.*)\\|(.*)#$1#", // 标签
"newestChapter": "",// 最新章节
"wordCount": "", // 字数,格式为数字。
"star": "", // 评分,格式为浮点数,取值应介于 0~10.0之间,默认为 8.5
"detailUrl": "//a/@href", // 详情页URL,本书的唯一标识,每本书的detailUrl不能重复
"tocUrl": "//a/@href,,,@js:\nvar preV = QB.getPreV();\nvar newS = preV.replace('m','www').replace('book','html');\nvar index = newS.indexOf('html/');\nvar id = newS.substring(index + 5,newS.length - 1)\nvar subId = id.substring(0,3);\nnewS = newS.substring(0,index) + 'html/' + subId + '/' + id + '/';\nreturn newS;" // 目录页URL
},
"searchUrl": "http://m.duqu.net/modules/article/search.php,{\"method\":\"post\",\"headers\":{\"Content-Type\":\"application/x-www-form-urlencoded\" },\"requestData\":\"searchkey={{QB.utf8($.key)}}&searchtype=all&b_btnsearch=\",\"needCookies\":false}", // 书籍搜索地址
"ruleSearch": {
"bookList": "//div[@id='jieqi_page_contents']/div", // 搜索 获取书籍列表
"bookName": "//h4/text()", // 书名 ,获取得到的书名不能为空,否则本书将被过滤。
"author": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$1}}#,,,#(.*)\\|(.*)#$1#", // 作者
"cover": "//img/@src", // 封面
"desc": "", // 描述
"tags": "",// 标签
"newestChapter": "",// 最新章节
"wordCount": "", // 字数,格式为数字。
"star": "", // 评分,格式为浮点数,取值应介于 0~10.0之间,默认为 8.5
"detailUrl": "//a/@href", // 详情页URL,本书的唯一标识,每本书的detailUrl不能重复
"tocUrl": "//a/@href,,,@js:\nvar preV = QB.getPreV();\nvar newS = preV.replace('m','www').replace('book','html');\nvar index = newS.indexOf('html/');\nvar id = newS.substring(index + 5,newS.length - 1)\nvar subId = id.substring(0,3);\nnewS = newS.substring(0,index) + 'html/' + subId + '/' + id + '/';\nreturn newS;" // 目录页URL
},
"ruleDetail": {
"bookName": "", // 书名
"author": "", // 作者
"cover": "", // 封面
"desc": "", // 描述
"tags": "",// 标签
"newestChapter": "", // 最新章节
"wordCount": "",// 字数,格式为数字。
"star": "", // 评分,格式为浮点数,取值应介于 0~10.0之间,默认为 8.5
"detailUrl": "",// 详情页URL,本书的唯一标识,每本书的detailUrl不能重复
"tocUrl": "" // 目录页URL
},
"ruleToc": {
"transform":"", // 转换规则,可对传递过来的tocUrl进行二次处理,得到新的Url。支持JS。
"tocList": "//div[@class='index']//li", // 目录页列表
"title": "/a/text()", // 章节标题
"url": "/a/@href",// 章节URL
"nextTocUrl": "", // 下一页URL规则,如此目录存在下一页,可通过此字段进行配置。
"needVip": "0",// 是否需要VIP,0为否,1为是
"wordCount": "0" //章节字数
},
"ruleContent": {
"transform":"", // 转换规则,可对传递过来的contentUrl进行二次处理,得到新的Url。支持JS。
"content": "//div[@id='acontent']/text()", // 获取章节内容
"nextUrls":"", // 下一页URL规则。如此章节存在多页内容,可通过此字段进行配置。
"nextJoin":"" // 多页内容连接符,默认为空字符串。
},
"verify": {
"searchKey": "元尊归来", // 验证搜索词,不能为空
"bookDetailPrefix": "http://m.duqu.net" // 验证详情页Url前缀,不能为空
},
"g": {
"debugSearchKey": "我的", // 调试搜索词。
"headers":"", // 全局请求头
"exploreGenerator": "gggg@@@######test1. 99999\ntest2. xxx\n\ntest4 sss\ntest3 ssasas\n\n123 444", // 发现Tag批量生成器
"fffff": "ssssss", // 自定义字段1
"kkk": "bbbb" // 自定义字段2
}
}
```
_____
### 字段规则
文本规则中可划分为 4 类
+ 网络调用
+ 字段取值
#### **网络调用**
整体规则中, `exploreUrl`、`searchUrl`、`detailUrl`、`tocUrl`以及目录章节中的 `contentUrl` 会在特定时机进行网络调用。
网络调用的完整格式为:
```
http://www.test.com/index,{"m":"get","h":{"token":"123"},"d":"","rCharset":"utf8","webview":0,"waitTime":500,"needCookies": 0, "cacheTime":0}
```
其中,`http://www.test.com/index`为url请求地址,`,`为分割符,后面接入的参数为Json字符串,描述了 url的调用规则。
参数详解:
* `m`描述了http调用方法,可取值为 `get`、`post`、`put`、`delete`、`options`,默认为`get`,为默认值时,可省略。
* `d`描述了http请求体,只在调用方法为 `post`、`delete`、`put`时生效,默认为空,可省略。
* `h`描述了http请求头,默认为空,可省略。
* `rCharset`描述了请求体的解码规则,可选值为`utf8`或`gbk`,默认为`utf8`,可省略。
注意,`gbk`兼容`gb2312`,当文本编码格式为`gb2312`时,`responseCharset`设置为`gbk`即可解析。
* `webview`描述了是否启用web容器进行网络请求,可取值为 `1`或者`0`,默认为`0`,可省略。
一般无需设置,当调用的网页需要让网页自身调用自身的JS逻辑时,可设置为`1`
* `waitTime`配合`webview`为1时进行使用,代表web容器的加载时间,单位为毫秒,默认为 500毫秒,可省略
* `needCookies`描述网络请求是否使用cookies,默认为不启用cookies。
* `cacheTime`描述了网络请求的数据缓存时间,单位为毫秒。默认为0,默认为不启用数据缓存。
因此 `http://www.test.com/index`等价于`http://www.test.com/index,{"m":"get","h":{"token":"123"},"d":"","rCharset":"utf8","webview":0,"waitTime":500,"needCookies": 0, "cacheTime":0}`
#### **字段取值**
`{{xx}}`规则中使用`{{}}`进行取值操作。
取值操作支持 `XPath`、`JsonPath`、`Css`、`App方法调用`、`书籍数据`、`章节数据`、`过程记录数据` 、`字符串取值`
+ `XPath`取值以`/`开头,如`{{/div[@class="test"]/p/text()}}`
+ `JsonPath`取值以`$`开头,如`$.id`
+ Css取值以 css: 开头,如 css: #content@text ,Css本身只能选择到标签,取值可用 @text 获取文本值,或者 @属性 获取 到属性值。
+ `App方法调用`取值以`QB.方法(参数1,,参数2,,参数3)`开头。
如 `{{QB.gbk($.key)}}`计算为 取得参数`key`,并对`key`进行`gbk`编码
+ `书籍数据`取值以`book.`开头,如`{{book.bookId}}`取值为`书籍`的`bookId`,`book`对象拥有如下属性
> + bookId 书籍ID
> + siteId 站点ID
> + siteVersion 站点版本
> + bookName 书名
> + detailUrl 详情页URL
> + tags 标签,数组,对个标签以`\n`分割
> + star 评分
> + wordCount 字数
> + newestChapter 最新章节
> + tocUrl 目录页URL
> + author 作者
> + cover 封面图
> + desc 简介
+ `章节数据`取值以`chapter.`开头,如`{{chapter.title}}`取值为`章节`的`title`,`chapter`对象拥有如下属性
> + title 标题
> + url 章节URL(也是内容页的URL)
> + needVip 是否需要VIP
> + wordCount 章节字数
> **注意 `chapter`取值只在详情页可用。**
+ `过程记录数据`指取得计算过程中的记录值,关键字为 `preV`,例如 http://baidu.com,,,{{preV}} 计算值为 http://baidu.com 。
关于`,,,`的用法后续介绍。
________
+ `字符串取值`取值指,当取值中的字符,无法匹配上述规则时,则会被当成字符串处理。
如 `{{this is a data}}`计算值为`this is a data`。
> 为便于大家理解,以下为相关范例。
> 假定现在返回的字符串为 `{"id":"10086","pId":"10324","bookName":"圣墟","tag":"玄幻","tag1":"仙侠"}`
> + 如需取值 书名,可写为 `"{{$.id}}"`,可简写为 `"$.id"` ,计算值为 `"10086"`
> + 如需取标签,可写为 `"{{$.tag}},{{$.tag1}}"`,计算值为 `"玄幻,仙侠"`
> **注意,可以对对个取值进行拼接**
> + 如需取详情页地址,可写为`"http://test.com/{{$.id}}/{{$.pId}}.html"`,计算值为`"http://test.com/10086/10324.html"`
#### 赋值及取值操作
赋值操作以`@put(key,value)`格式执行。如进行赋值后,后续可通过`{{G_key}}`进行取值操作。
以下为范例
> 假定现在返回的字符串为 `{"id":"10086","pId":"10324","bookName":"圣墟","tag":"玄幻","tag1":"仙侠"}`
> `@put(bookId,$.id),,,http://test.com/{{G_bookId}}`计算值为 http://test.com/10086 ,关于`,,,`分割后后续介绍
#### 正则替换操作
正则操作格式分为两种。
+ #正则表达式#替换的值# ---单次匹配
+ #正则表达式#替换的值## ---全部匹配
正则匹配的输入值为 `上一操作的过程值`,即`{{preV}}`
为便于大家理解,范例如下:
(单次匹配)表达式`"http://test.com/10086/10086.html,,,#10086#-#"`取值为 http://test.com/-/10086.html
(全部匹配)表达式`"http://test.com/10086/10086.html,,,#10086#-##"`取值为 http://test.com/-/-.html
以上述单次匹配表达式进行执行拆解。
`"http://test.com/10086/10086.html,,,#10086#-#"` 执行时被分拆为 `http://test.com/10086/10086.html` ,`,,,`,`#10086#-#`,其中 `http://test.com/10086/10086.html`的执行结果为 匹配为字符串http://test.com/10086/10086 , `,,,`为表达式分隔符,会将前一步骤的计算结果保存到`preV`变量中,然后执行第三部分正则替换`#10086#-#`
**正则表达式取值关键词**
`$0`只在正则表达式中生效。代表获取正则匹配的值。
为便于大家理解,以下为相关范例:
+ 范例一:返回值为`"这个是反馈的数据"`,表达式为`#.*#{{$0}}#`,取值为`这个是反馈的数据`
+ 范例二:返回值为`"这个是反馈的数据"`,表达式为`#这个是(.*)#{{$0}}`,取值为`这个是反馈的数据`
`$(index)`只在正则表达式中生效,类似`$1`、`$2`、`$3` ... ,代表取`第一个`、`第二个`、`第三个分组`、... 的数据。
以下为相关范例:
+ 范例一: 返回值为`"这个是反馈的数据"`,表达式为`#这个是反馈的(.*)#{{$1}}#`,取值为`数据`
+ 范例二: 返回值为`"这个是反馈的数据"`,表达式为`#这个是(.*)的(.*)#{{$0}}--{{$1}}--{{$2}}`,取值为`这个是反馈的数据--反馈--数据`。
_______________
#### 操作符详解
**列表取值**
列表取值不支持`{{}}`取值操作。
`ruleExplore -> bookList`、`ruleSearch -> bookList`及`ruleToc -> tocList`这三个字段为列表取值操作。目前只支持 `&&` 、`||`、`%%`操作。
+ `&&` 为取合集,格式为 `A&&B&&C`
+ `||` 为取或集合,格式为 `A||B`,计算时,当`A`存在值,且不为空时,直接返回结果,不进行`B`的计算,否则返回`B`的结果
+ `%%` 为轮询插入,格式为 `A%%B%%C`,计算是,组装列表,会先从 `A`取第一个元素,再取`B`的第一个元素,再取`C`的第一个元素。
为便于大家理解,范例如下:
假定当前返回值为
```json
{
"dataList_1":["AAA","BBB","CCC"],
"dataList_2":["111","222","333"],
"dataList_3":["aaa","bbb","ccc"],
}
```
基于上述反馈值,计算结果如下:
* `$.dataList_1&&$.dataList_2&&$.dataList_3`计算值为 `["AAA","BBB","CCC","111","222","333","aaa","bbb","ccc"]`
* `$.dataList_1||$.dataList_2`计算值为 `["AAA","BBB","CCC"]`
* `$.dataList_1%%$.dataList_2`计算值为 `["AAA","111","BBB","222","CCC","333"]`
**字段取值**
字段取值支持 `{{}}`、`&&`、`||`、`,,,`、`#正则#替换#`、`@put(key,value)`、`字符拼接`等操作规范。
`{{}}`取值在上述章节已有描述,在此不进行重复描述。
`字符拼接` 格式为 `"字符串{{key1}}字符串{{key2}}字符串"`,相关范例如下:`http://test.com/{{$.id}}.html`,在计算过程中,会计算为 `http://test.com/` + `{{$.id}}` + `.html`
`,,,` 为多计算分隔符,顶级运算符,最高优先级运算符。并声明`preV`为前次计算的结果。
**为便于大家理解,以下为相关范例。**
+ 范例一:`http://test.com,,,{{preV}}`,取值结果为 `http://test.com`,这里注意,如想取值`上一次操作的计算结果`,使用`{{preV}}`,不能使用`preV`,单纯的`preV`会被当做字符串解析。
以下为解析过程:`http://test.com,,,{{preV}}`被`,,,`分割为`http://test.com`与`{{preV}}`两部分,从左到右执行,`http://test.com`的计算结果为 `http://test.com`,因被`,,,`分割,将`http://test.com`的结果赋值到`preV`局域计算变量中,后续的`{{preV}}`则执行`preV变量`的取值操作。
+ 范例二:`http://test.com,,,abc`,取值结果为`abc`。
+ 范例三: `http://test.com,,,@put(url,preV)`,取值结果为 `http://test.com`,并且插入自定义字段`url`,值为`http://test.com`
+ 范例四: `http://test.com,,,@put(url,abc)`,取值结果为 `http://test.com`,并且插入自定义字段`url`,值为`abc`
+ **`@put`** 操作不会对计算结果产生影响,只做值传递。并且`@put(key,value)`中的value取值不需要带`{{}}`。
+ 范例五:`http://test.com/{{$.id}},,,@put(bId,$.id),,,#{{preV}}}#replace#` 取值为 `replace`,并且插入值`bId`为`$.id`
+ 范例六: 输入值为`{"id":"10086","pId":"1"}`,计算表达式为`http://test.com/{{$.id}}-new/{{$.pId}}-new.html,,,@put(bId,$.id),,,@put(pId,$.pId),,,#http://test.com/(.*)/(.*)\.html#{{$1}}&{{$2}}#`,计算值为 `10086-new&1-new`
`&&`为字段组合运算符,类比`+`,以下为相关实例:
* 范例一:返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id&&$.name`取值为`1元尊`,也可类比为 `{{$.id}}{{$.name}}`
* 范例二:返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id&&---&&$.name`取值为`1元尊`,也可类比为 `{{$.id}}---{{$.name}}`,以及`{{$.id}}{{---}}{{$.name}}`
* 范例三: 返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id&&$.name,,,{{preV}}`取值为`1元尊`。
**`,,,`** 的优先级高于 **`&&`**
`||`为字段组合运算符,类比`或运算`,以下为相关实例:
* 范例一:返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id||$.name`取值为`1`
* 范例二:返回结果为`{"id":1,"name":"元尊"}`,表达式`http://test/{{$.id||$.name}}`此表达式无法计算,`{{}}`为取值操作,不支持`||`运算,正确写法应为`$.id||$.name,,,http://test/{{preV}}`或者`$.id||$.name,,,#.*#http://test/{{$0}}`
_____________
**特殊字段处理**
`发现页Url`-`exploreUrl`特殊规则
发现页在App操作时,会触发翻页,因此在取值时会注入一个Json字符串`{"page":"1"}`,因此`exploreUrl`需获取页码时,应写成 `http://explore.test/{{$.page}}`,页码默认为0。
`exploreUrl`支持App内置方法。
+ 比如,站点的页码`从1开始`,那么可使用表达式`http://explore.test/{{QB.plus(page,,1)}}`,`QB.plus(page,,1)`会执行 `page` `+` `(1)`
+ 比如,站点的间隔为`页码的10倍`,那么可使用表达式`http://explore.test/{{QB.muti(page,,10)}}`,`QB.muti(page,,10)`会执行`page` `*` `10`
+ 比如,站点的间隔为`页码的10倍+12的偏移量`,那么可使用表达式`http://explore/test/{{QB.plus(QB.muti(page,,10),,12)}}`,`QB.plus(QB.muti(page,,10),,12)`会执行`(page * 10) + 12`
+ 比如,站点的第一页为`http://explore/test/`,第二页为`http://explore/test/2`,那么可使用表达式`http://explore/test/{{QB.emptyOr(page,,0)}}`
`QB.emptyOr(A,,B)`表达式的计算逻辑为:当`A == B`时,输出`空字符串`,否则输出`A`,等价于三目运算符 ` A == B ? "" : A`
**特别注意,在规则内调用 App中的方法时,多参数之间直接使用,,进行分割。如 QB.plus(page ,, 1)**
关于规则内调用App内置方法,青鸟提供了一系列的方法调用,具体如下:
- 如 下列描述的`timestamp`方法,调用写法为 `QB.`timestamp()
- 如 下列描述的 `gbk`方法,调用写法为 `QB.gbk('这个是要转gbk的字符串')`
- 如 下列描述的 `plus` 方法,调用写法为 `QB.plus(1,,2,,3)` ,**再次提醒,调用内置方法,多参数间使用 `,,`进行分割**
```dart
static final Map syncHandlerMap = {
/// 返回当前时间戳,精确到毫秒
'timestamp': (List args) =>
DateTime
.now()
.millisecondsSinceEpoch
.toString(),
/// gbk 编码, 输入为 字符串, 返回字符串 hex16
"gbk": (List args) => AppInnerUtils.gbk(args[0]),
/// utf8 编码, 输入为 字符串, 返回字符串 hex16
"utf8": (List args) => AppInnerUtils.utf(args[0]),
/// utf8 编码,输入为 字符串, 返回 bytes数组
"utf8_bytes": (List args) => AppInnerUtils.utf8Bytes(args[0]),
/// utf8 解码,输入为 bytes数字, 返回 字符串
"utf8_decode": (List args) {
if (args[0] is List) {
return AppInnerUtils.utf8Decode(args[0]);
} else {
return AppInnerUtils.utf8Decode(
(args[0] as List).map((e) => e as int).toList());
}
},
/// utf8 解码,输入为 字符串, 返回 bas64编码
"base64": (List args) => AppInnerUtils.base64(args[0]),
/// md5 签名,输入为 字符串, 返回 md5值
"md5": (List args) => AppInnerUtils.md5String(args[0]),
/// url编码 ,输入为 字符串, 返回 字符串
"urlEncode": (List args) => AppInnerUtils.urlEncode(args[0]),
/// 执行2次url编码 ,输入为 字符串, 返回 字符串
"urlEncode2": (List args) => AppInnerUtils.urlEncode2(args[0]),
/// 加法,支持多个字符串,返回字符串,类比 1 + 2 + 3 ,返回 字符串 6
"plus": (List args) => AppInnerUtils.add(args),
/// 乘法 ,支持多个字符串,返回字符串,类比 1 * 2 * 3 ,返回 字符串 6
"mult": (List args) => AppInnerUtils.mult(args),
/// 类似三目运算符。 args[0] == args[1] ? '' : args[0],当两值相关,返回'',否则返回args[0]
"emptyOr": (List args) => AppInnerUtils.emptyOr(args[0], args[1]),
/// 连接,可实现 左右连接,如 QB.connect(A,,B) 输出 字符串 AB
"connect": (List args) => AppInnerUtils.connect(args[0], args[1]),
"slice": (List args) => AppInnerUtils.slice(args[0], args[1]),
/// unicode解码 ,输入字符串,输出字符串
"unicode_decode": (List args) =>
AppInnerUtils.unicodeDecode(args[0]),
/// base解码 ,输入字符串,输出字符串(utf8)
"base64Decode": (List args) =>
AppInnerUtils.base64DecodeUTF8(args[0]),
/// 对称解密(DESede) ,输入 参数1:加密字符,参数2:秘钥,参数3:iv 输出:字符串(utf8)
"DESede": (List args) =>
AppInnerUtils.DESedeDecode(args[0], args[1], args[2]),
/// 非对称加密(rsa) ,输入 参数1:待加密字符,参数2:秘钥,参数3:类型,支持 PKCS1与OAEP 输出:bytes数组
"rsaEncrypt": (List args) {
if (args[1] is List) {
return AppInnerUtils.rsaEncrypt(args[0], args[1], args[2]);
} else {
return AppInnerUtils.rsaEncrypt(args[0],
(args[1] as List).map((e) => e as int).toList(), args[2]);
}
},
// bytes转Hex输出
"hexCode": (List args) {
if (args[0] is List) {
return AppInnerUtils.hexCode(args[0]);
} else {
return AppInnerUtils.hexCode(
(args[0] as List).map((e) => e as int).toList());
}
},
/// url解密
"urlDecode": (List args) {
return AppInnerUtils.urlDecode(args[0]);
},
/// aes加密,String source,String key,String iv,String padding,String mode
/// 参数0 待加密字符串
/// 参数1 加密Key
/// 参数2 加密IV
/// 参数3 加密Padding类型,例如 PKCS5 / PKCS7
/// 参数4 加密模式,例如 ecb,sic
"aesEncrypt": (List args) {
return AppInnerUtils.aesEncrypt(
args[0], args[1], args[2], args[3], args[4]);
},
/// aes解密,String source,String key,String iv,String padding,String mode
/// 参数0 加密字符串
/// 参数1 解密Key
/// 参数2 解密IV
/// 参数3 解密Padding类型,例如 PKCS5 / PKCS7
/// 参数4 解密模式,例如 ecb,sic
"aesDecrypt": (List args) {
return AppInnerUtils.aesDecrypt(
args[0], args[1], args[2], args[3], args[4]);
},
};
```
`搜索页`-`searchUrl`特殊规则
App在进行搜索时,会将用户搜索的书名以Json形式`{"key":"元尊"}`的形式注入。因此`searchUrl`在获取书名进行搜索时,应写成:
+ 范例一: `http://example/search?bookName={{$.key}}`
+ 范例二: `http://example/search,{"m":"post","d":"type=bookName&&searchKey={{$.key}}"}`
`内容页`-`content`特殊规则
因书源支持 `书籍(book)`、`听书(listenbook)`、`漫画(comic)`等多种类型。
+ 其中 如果`info -> type`取值为`书籍(book)`时,`content`返回`内容文本`即可。
+ 其中 如果`info -> type`取值为`听书(listenbook)`时,`content`返回`音频http地址`即可,格式如下:
> 1. http://music.test/test.mp3
> 2. http://music.test/test.mp3,{"headers":{"refer":"http://music.test"},"method":"get"}
+ 其中 如果`info -> type`取值为`漫画(comic)`时,`content`返回`漫画http地址数组`即可,数组以`\n`分割,格式如下:
> 1. `"http://comic.test/1.jpg\nhttp://comic.test/2.jpg\nhttp://comic.test/3.jpg"`
> 2. `http://comic.test/1.jpg,{"headers":{"refer":"http://comic.test"}}\nhttp://comic.test/2.jpg,{"headers":{"refer":"http://comic.test"}}\nhttp://comic.test/3.jpg,{"headers":{"refer":"http://comic.test"}}`
------
#### 关于JS调用
规则内支持JS调用。
使用 `@js:`进行声明。在JS脚本中,会内置注入 相应参数
- 会内置注入 `result`参数,值为前一个运算所获取的结果。
- 会内置注入 `bookId`参数,值为此书的唯一标识符。
- 在获取章节内容时,会注入 chapterUrl 参数。代表章节ID
```
数据源为 { "id": 123 , "bookName": "圣墟" }
范例一:
取值表达式为 $.id,,,@js: return 'http://baidu.com/index/' + result;
结果为 http://baidu.com/index123
-----
范例二:
取值表达式为 $.id,,,@js: var url = 'http://baidu.com/index/' + result; QB.putKey(bookId,'sUrl',url); ,,,{{G_sUrl}}
结果为 http://baidu.com/index123
其中 QB.putKey方法为JS中的@put赋值操作写法。
```
- 关于JS方法调用,青鸟提供了一系列的方法调用,具体如下:
```javascript
class QB{
// 日志打印
// level 可赋值 "debug" 或者 "error"
static log(log, level)
// 获取当前时间戳,精确到毫秒
static timestamp()
// @put操作
static putKey(bookId, k, v)
// @get取值,类似 G_ 取值
static getKey(bookId, k)
// 获取当前的page页码
static getExplorePage()
// 获取当前的搜索词 key
static getSearchKey()
// 获取本次的网络调用源数据
static networkData()
// 获取书籍详情
static getBook(bookId)
// 获取章节详情
static getChapter(chapterUrl)
// 通过 xpath解析单个字段(一般在Json站点无需关注)
static xpathQuerySingle(rule, node, join, nodeTag)
// 通过 xpath解析数据列表(一般在Json站点无需关注)
static xpathQueryList(rule, node, nodeTag)
// 通过 xpath获取解析的数据列表总数 (一般在Json站点无需关注)
static xpathQueryListTagCount(rule, node, nodeTag)
// 通过 xpath中的tag获取数据内容(一般在Json站点无需关注)
static queryDataByNodeTag(tag)
// md5加密
static md5String(str)
// md5加密 与 md5String等效
static md5(str)
// 3des解密
static DESedeDecode(secretStr, key, iv)
// 3des加密
static DESedeEncode(source, key, iv)
// aes加密
static aesEncode(source, key, iv, padding, mode)
// aes解密
static aesDecode(secretStr, key, iv, padding, mode)
// rsa加密
static rasEncode(source, key, type)
// 获取字符串的hashCode
static hashCode(str)
// 字符串转Bytes
static strToBytes(str, charset)
// bytes转字符串
static bytesToStr(bytes, charset)
// base64解密为字符串
static base64Decode(base64, charset)
// base64解密为bytes
static base64DecodeToByteArray(base64)
// base64加密
static base64Encode(str)
// base64加密,输入为bytes
static base64EncodeBytes(bytes)
// hex转bytes
static hexDecodeToByteArray(hex)
// hex转字符串
static hexDecodeToString(hex, charset)
// 字符串转hex
static strEncodeToHex(str)
// utf8转gbk
static utf8ToGbk(str)
// url encode
static encodeURI(str, enc)
// url decode
static decodeURI(str, enc)
// 暂未实现
static t2s(text)
// 暂未实现
static s2t(text)
// 暂未实现
static toast(text)
// 暂未实现
static longToast(text)
// 暂未实现
static getFile(path)
// 暂未实现
static readFile(path)
// 暂未实现
static readTxtFile(path, charset)
// 暂未实现
static deleteFile(path)
// 暂未实现
static unzipFile(zipPath)
// 暂未实现
static un7zFile(zipPath)
// 暂未实现
static unrarFile(rarPath)
// 暂未实现
static unArchiveFile(zipPath)
// 添加 toc章节 (一般在Json站点无需关注)
static addInnerToc(innerToc)
// 添加 content内容 (一般在Json站点无需关注)
static addContent(qnContentModel)
// 添加 书籍列表 (一般在Json站点无需关注)
static addAllBook(list)
// 添加 书籍 (一般在Json站点无需关注)
static addBook(qnBookModel)
// 添加 书籍目录 (一般在Json站点无需关注)
static addBookToc(tocModel)
}
```
### 书源范例
+ 范例一:
```json
{
"siteId": "http://m.duqu.com", // 站点ID,站点唯一标识,每个站点不可重复
"info": {
"type": "book", // 站点类型。 书籍为 book 、听书为 listenbook 、漫画为 comic
"showName": "读趣", // 站点显示名称
"group": "微光", // 站点分组
"desc": "", // 站点描述
"updateTime": 1710552872211, // 站点更新时间,精确到毫秒的时间戳
"versionNumb": "103", // 站点版本
"supportSearch": true, // 是否支持 搜索
"supportExplore": true, // 是否支持 发现
"isEncrypt": false, // 是否支持 站点加密
"enable": true, // 站点是否可用
"vpnWebsite": "" // 是否需要VPN支持
},
"exploreUrl": "玄幻::http://m.duqu.net/sort/1/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n武侠修真::http://m.duqu.net/sort/2/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n都市言情::http://m.duqu.net/sort/3/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n历史军事::http://m.duqu.net/sort/4/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n侦探推理::http://m.duqu.net/sort/5/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n网游动漫::http://m.duqu.net/sort/6/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n科幻小说::http://m.duqu.net/sort/7/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n恐怖灵异::http://m.duqu.net/sort/8/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n言情小说::http://m.duqu.net/sort/9/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n其它类型::http://m.duqu.net/sort/10/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}\n子部::http://m.duqu.net/sort/13/{{QB.plus(page,,1)}}.html,{\"cacheTime\":60000}", // 发现页标签列表,格式为 TAG::URL
"ruleExplore": {
"bookList": "//*[@id='jieqi_page_contents']/div // 发现页 获取书籍列表
"bookName": "//h4/text()", // 书名 ,获取得到的书名不能为空,否则本书将被过滤。
"author": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$1}}#,,,#.*\\|(.*)#$1#", // 作者
"cover": "//img/@src", // 封面
"desc": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$2}}#", // 描述
"tags": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$1}}#,,,#(.*)\\|(.*)#$1#", // 标签
"newestChapter": "",// 最新章节
"wordCount": "", // 字数,格式为数字。
"star": "", // 评分,格式为浮点数,取值应介于 0~10.0之间,默认为 8.5
"detailUrl": "//a/@href", // 详情页URL,本书的唯一标识,每本书的detailUrl不能重复
"tocUrl": "//a/@href,,,@js:\nvar preV = QB.getPreV();\nvar newS = preV.replace('m','www').replace('book','html');\nvar index = newS.indexOf('html/');\nvar id = newS.substring(index + 5,newS.length - 1)\nvar subId = id.substring(0,3);\nnewS = newS.substring(0,index) + 'html/' + subId + '/' + id + '/';\nreturn newS;" // 目录页URL
},
"searchUrl": "http://m.duqu.net/modules/article/search.php,{\"method\":\"post\",\"headers\":{\"Content-Type\":\"application/x-www-form-urlencoded\" },\"requestData\":\"searchkey={{QB.utf8($.key)}}&searchtype=all&b_btnsearch=\",\"needCookies\":false}", // 书籍搜索地址
"ruleSearch": {
"bookList": "//div[@id='jieqi_page_contents']/div", // 搜索 获取书籍列表
"bookName": "//h4/text()", // 书名 ,获取得到的书名不能为空,否则本书将被过滤。
"author": "//p[@class='gray']/text(),,,#(.*) (.*)#{{$1}}#,,,#(.*)\\|(.*)#$1#", // 作者
"cover": "//img/@src", // 封面
"desc": "", // 描述
"tags": "",// 标签
"newestChapter": "",// 最新章节
"wordCount": "", // 字数,格式为数字。
"star": "", // 评分,格式为浮点数,取值应介于 0~10.0之间,默认为 8.5
"detailUrl": "//a/@href", // 详情页URL,本书的唯一标识,每本书的detailUrl不能重复
"tocUrl": "//a/@href,,,@js:\nvar preV = QB.getPreV();\nvar newS = preV.replace('m','www').replace('book','html');\nvar index = newS.indexOf('html/');\nvar id = newS.substring(index + 5,newS.length - 1)\nvar subId = id.substring(0,3);\nnewS = newS.substring(0,index) + 'html/' + subId + '/' + id + '/';\nreturn newS;" // 目录页URL
},
"ruleDetail": {
"bookName": "", // 书名
"author": "", // 作者
"cover": "", // 封面
"desc": "", // 描述
"tags": "",// 标签
"newestChapter": "", // 最新章节
"wordCount": "",// 字数,格式为数字。
"star": "", // 评分,格式为浮点数,取值应介于 0~10.0之间,默认为 8.5
"detailUrl": "",// 详情页URL,本书的唯一标识,每本书的detailUrl不能重复
"tocUrl": "" // 目录页URL
},
"ruleToc": {
"transform":"", // 转换规则,可对传递过来的tocUrl进行二次处理,得到新的Url。支持JS。
"tocList": "//div[@class='index']//li", // 目录页列表
"title": "/a/text()", // 章节标题
"url": "/a/@href",// 章节URL
"nextTocUrl": "", // 下一页URL规则,如此目录存在下一页,可通过此字段进行配置。
"needVip": "0",// 是否需要VIP,0为否,1为是
"wordCount": "0" //章节字数
},
"ruleContent": {
"transform":"", // 转换规则,可对传递过来的contentUrl进行二次处理,得到新的Url。支持JS。
"content": "//div[@id='acontent']/text()", // 获取章节内容
"nextUrls":"", // 下一页URL规则。如此章节存在多页内容,可通过此字段进行配置。
"nextJoin":"" // 多页内容连接符,默认为空字符串。
},
"verify": {
"searchKey": "元尊归来", // 验证搜索词,不能为空
"bookDetailPrefix": "http://m.duqu.net" // 验证详情页Url前缀,不能为空
},
"g": {
"debugSearchKey": "我的", // 调试搜索词。
"headers":"", // 全局请求头
"exploreGenerator": "gggg@@@######test1. 99999\ntest2. xxx\n\ntest4 sss\ntest3 ssasas\n\n123 444", // 发现Tag批量生成器
"fffff": "ssssss", // 自定义字段1
"kkk": "bbbb" // 自定义字段2
}
}
```
## 动态JS站点规则
无法使用文本规则适配的站点可以通过动态JS站点规则进行适配。
具体规则,查看如下实例:
```javascript
// SourceRule_kumange 全局唯一,不可重复,无实际意义
class SourceRule_kumange {
// 站点ID,不可重复,站点唯一标识
siteId = "http://www.kumange1.com"
// 站点版本号
currentSiteVersion = 204
// 站点更新信息
siteUpdateInfo = {
100: "初始化",
}
// 站点基础信息
info = {
siteId: this.siteId,
type: "comic",
showName: "酷漫阁",
group: "搜狗",
desc: "",
updateTime: 1234,
versionNumb: this.currentSiteVersion,
supportSearchBookName: true,
supportExplore: true,
enable: false
}
// 站点验证词
verifyBookConfig = {
searchKey: "重生之都市修仙",
bookDetailPrefix: "http://www.kumange1.com",
}
// 站点迁移
migrate = function (oldVersion) {
return 0;
};
// 发现列表
exploreModel = function () {
return {
boy: [
{
tag: "武侠格斗",
url: "http://www.kumange1.com/sort/2-1.html",
},
],
girl: [],
}
}
// 查询发现页
queryExplore = async function (exploreUrl) {
// 获取网络
let res = await QBAsync.network({
url: exploreUrl,
m: 'get',
rCharset: 'utf8'
});
let dataTag = res.dataTag
// 进行 xpath解析
let dataCount = QB.xpathQueryListTagCount('//div[@class="mult-body"]/ul/li', null, dataTag)
let bookList = [];
for (let index = 0; index < dataCount; index++) {
let itemTag = dataTag + '-' + index
let link = QB.xpathQuerySingle('//a/@href', null, '', itemTag);
let detailUrl = `http://www.kumange1.com${link}`;
bookList.push({
bookId: QB.hashCode(detailUrl), // QB.hashCode 可以把 detailUrl 转化为 bookId
bookName: QB.xpathQuerySingle('//a/div[2]/text()', null, '', itemTag),
cover: QB.xpathQuerySingle('//a/div[1]/img/@data-src', null, '', itemTag),
author:QB.xpathQuerySingle('', null, '', itemTag),
newestChapter: QB.xpathQuerySingle('//a/div[1]/div/text()', null, '', itemTag),
detailUrl: detailUrl,
tocUrl: link,
});
}
return bookList;
};
// 搜索
searchBookName = async function (key) {
QB.log('searchBookName------ ' + key);
let searchUrl = `http://www.kumange1.com/search?searword=${key}`;
let res = await QBAsync.network({
url: searchUrl,
m: "get",
rCharset: "utf8",
ext: {
needRspHeader: 1
}
});
QB.log('res -> ' + JSON.stringify(res))
let dataTag = res.dataTag
QB.log('dataTag------ ' + dataTag);
let dataCount = QB.xpathQueryListTagCount('//div[@class="mult-body"]/ul/li', null, dataTag)
let bookList = [];
for (let index = 0; index < dataCount; index++) {
let itemTag = dataTag + '-' + index
let link = QB.xpathQuerySingle('//a/@href', null, '', itemTag);
let detailUrl = `http://www.kumange1.com${link}`;
bookList.push({
bookId: QB.hashCode(detailUrl),
bookName: QB.xpathQuerySingle('//a/div[2]/text()', null, '', itemTag),
cover: QB.xpathQuerySingle('//a/div[1]/img/@data-src', null, '', itemTag),
newestChapter: QB.xpathQuerySingle('//a/div[1]/div/text()', null, '', itemTag),
detailUrl: detailUrl,
tocUrl: link,
});
}
return bookList
};
// 查询书籍详情
queryBookDetail = async function (model) {
QB.log('queryBookDetail------ ' + JSON.stringify(model));
//qnLog('queryBookDetail --> ' + JSON.stringify(model))
let res = await QBAsync.network({
url: model.detailUrl,
rCharset: "utf8",
});
let dataTag = res.dataTag
let desc = QB.xpathQuerySingle('//div[@class="introd"]/text()', null, '', dataTag);
let tag = QB.xpathQuerySingle('//div[@class="bookinfo"]/p[2]/text()', null, '', dataTag)
.replace(/分 类:/g, "")
.trim();
let status = QB.xpathQuerySingle('//div[@class="bookinfo"]/p[4]/text()', null, '', dataTag)
.replace(/状 态:/g, "")
.trim();
let author = QB.xpathQuerySingle('//div[@class="bookinfo"]/p[1]/text()', null, '', dataTag)
.replace(/作 者:/g, "")
.trim();
model["desc"] = desc;
model["tags"] = [tag, status];
model["author"] = author;
return model;
};
// 查询书籍目录
queryBookToc = async function (model) {
QB.log('queryBookToc -->' + JSON.stringify(model))
let bookId = model.tocUrl.replace(/\//g, "");
let res_1 = await QBAsync.network({
url: `http://www.kumange1.com/${bookId}/`,
rCharset: "utf8",
});
// JSON 格式,我们通过设置 ext: { needDataTag:0 } 返回真实数据
let res_2 = await QBAsync.network({
url: 'http://www.kumange1.com/chapterlist/',
m: 'post',
d: 'id=' + bookId,
h: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.51",
'Referer': model.detailUrl,
},
rCharset: "utf8",
ext: {
needDataTag: 0
}
});
let chapterList = [];
let res_1Tag = res_1.dataTag
let data_1Count = QB.xpathQueryListTagCount('//ul[@class="chapterlist"]/li', null, res_1Tag)
for (let index = 0; index < data_1Count; index++) {
let arrTag = res_1Tag + '-' + index
let title = QB.xpathQuerySingle('//a/text()', null, '', arrTag)
let url = QB.xpathQuerySingle('//a/@href', null, '', arrTag)
chapterList.push({
title: title,
url: url,
needVip: false,
})
}
let dataList_2 = JSON.parse(res_2.data);
for (let items of dataList_2.data) {
chapterList.push({
title: items.name,
url: 'http://www.kumange1.com/' + bookId + '/' + items.id + '.html',
needVip: false,
});
}
QB.log('chapterList.length -->' + chapterList.length)
return chapterList.reverse();
};
// 查询章节详情
queryBookContent = async function (model, contentKey) {
let res = await QBAsync.network({
url: contentKey,
ext: {
needDataTag: 0,
},
});
let dataId = QB.xpathQuerySingle(
'//div[@class="imgView readerContainer"]/@data-id',
res.data
);
let script = res.data.match("eval(.*)")[1];
let base64Str = eval(script).match(/"(.*)"/)[1];
let imgStr = this.decContent(base64Str, dataId);
let comicList = [];
for (let index in imgStr) {
let url = imgStr[index];
comicList.push({
url: url,
headers: {}, // 如果请求此Url需要设置Headers
w: 0, // 如果可以获取 宽度
h: 0, // 如果可以获取 高度
});
}
//QB.log("comicList =========> " + JSON.stringify(comicList));
return { contentKey: contentKey, comicPicList: comicList };
};
decContent = function (content, dataId) {
let keys = [
"smkhy258",
"smkd95fv",
"md496952",
"cdcsdwq",
"vbfsa256",
"cawf151c",
"cd56cvda",
"8kihnt9",
"dso15tlo",
"5ko6plhy",
][dataId];
let enContent = QB.base64DecodeToByteArray(content);
let deContent = Array.from(Uint8Array.from(enContent), (char, index) => {
let k = index % keys.length;
return String.fromCharCode(char ^ keys.charCodeAt(k));
}).join("");
let deData = QB.base64Decode(deContent, "utf-8");
return JSON.parse(deData);
};
}
```