# 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); }; } ```