diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eed4fc77..56e6b604 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,9 +37,9 @@ jobs: run: sed -i 's/\/usr\/bin\/env node/node/g' C:/Users/runneradmin/setup-pnpm/node_modules/.pnpm/pnpm@9.4.0/node_modules/pnpm/bin/pnpm.cjs shell: bash - - run: npx changelogithub - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # - run: npx changelogithub + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: pnpm install - run: pnpm run build diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 9996410d..72890f8e 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -31,7 +31,7 @@ export default withMermaid({ nav: [ { text: '指南', link: '/desktop/' }, { text: '规则', link: '/rule/' }, - { text: '规则测试', link: '/play/' } + { text: '在线工具', link: '/play/' } ], search: { provider: 'local' diff --git a/docs/play/index.md b/docs/play/index.md index 3566762c..91dbf5d7 100644 --- a/docs/play/index.md +++ b/docs/play/index.md @@ -1,4 +1,9 @@ -> 开发中, 目前规则部分不支持 `@filter` `@js`, 暂不支持解析流程测试 +# 取文本测试 + +> 开发中, 目前在线工具规则部分不支持 `@filter` `@js`, 暂不支持解析流程测试 +--- + + diff --git a/docs/rule/expression.md b/docs/rule/expression.md new file mode 100644 index 00000000..fd56d6e0 --- /dev/null +++ b/docs/rule/expression.md @@ -0,0 +1,23 @@ +| 特性 | 说明 | 示例 | +| ---------- | :---------------------------------------- | :--------------------------------- | +| `@css` | 使用 css 选择器查找内容 [规则测试][@css] | `@css:.box1 .box2@text` | +| `@json` | 使用 jsonpath 查找内容 [规则测试][@json] | `@json:$.list[:1].title` | +| `@xpath` | 使用 xpath 查找内容 [规则测试][@xpath] | `@xpath://*[@class="box3"]/text()` | +| `@js` | 使用 js 脚本 | `@js:1+1` | +| `@filter` | 模拟浏览器加载地址后匹配指定链接 | `@filter:(?:m3u8\|mp4)` | +| `@replace` | 替换匹配到的内容为空 [规则测试][@replace] | `@replace:\d` | +| `##` | 正则替换 [规则测试][##] | `$.a##2##替换文本` | +| `{‍​‍{}}` | 拼接 [规则测试][​拼接] | `http://www.aaa.com/{‍{$.id}}` | +| `\|\|` | 或, 直到规则匹配成功 [规则测试][或] | `$.a\|\|$.b` | +| `&&` | | | +| 嵌套组合 | | `$.info.body@css:.box1 .box2@text` | + +规则可以省略开头的,**@css**、**@xpath**、**@json**, 因为解析器会尝试自动识别。 + +[@css]: /play/?_t=1#eyJpbnB1dFRleHQiOiI8ZGl2IGNsYXNzPVwiYm94MVwiPjxkaXYgY2xhc3M9XCJib3gyXCI+Y29udGVudDwvZGl2PjwvZGl2PiIsInJ1bGUiOiIuYm94MSAuYm94MkB0ZXh0IiwiaXNMaXN0IjpmYWxzZX0= +[@json]: /play/?_t=2#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IDF9IiwicnVsZSI6IiQuYSIsImlzTGlzdCI6ZmFsc2V9 +[@xpath]: /play/?_t=3#eyJpbnB1dFRleHQiOiI8ZGl2IGNsYXNzPVwiYm94MVwiPjxkaXYgY2xhc3M9XCJib3gyXCI+Y29udGVudDI8L2Rpdj48ZGl2IGNsYXNzPVwiYm94M1wiPmNvbnRlbnQzPC9kaXY+PC9kaXY+IiwicnVsZSI6Ii8vKltAY2xhc3M9XCJib3gzXCJdL3RleHQoKSIsImlzTGlzdCI6ZmFsc2V9 +[@replace]: /play/?_t=6#eyJpbnB1dFRleHQiOiJhMTJiM2MiLCJydWxlIjoiQHJlcGxhY2U6XFxkIiwiaXNMaXN0IjpmYWxzZX0= +[##]: /play/?_t=7#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IFwiMTIzXCJ9IiwicnVsZSI6IiQuYSMjMiMj5pu/5o2i5paH5pysIiwiaXNMaXN0IjpmYWxzZX0= +[​拼接]: /play/?_t=8#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IDF9IiwicnVsZSI6InF7eyQueHx8JC5hfX13IiwiaXNMaXN0IjpmYWxzZX0= +[或]: /play/?_t=9#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IFwiMVwifSIsInJ1bGUiOiIkLmJ8fCQuYSIsImlzTGlzdCI6ZmFsc2V9 diff --git a/docs/rule/index.md b/docs/rule/index.md index ac17283d..3799d1e1 100644 --- a/docs/rule/index.md +++ b/docs/rule/index.md @@ -14,53 +14,6 @@ outline: deep ::: code-group -```json -{ - "id": "xxx-xxx-xxx-xxx-xxx", - "author": "", - "name": "", - "host": "", - "icon": "", - "contentType": 1, - "sort": 0, - "userAgent": "", - "enableDiscover": false, - "discoverUrl": "", - "discoverNextUrl": "", - "discoverList": "", - "discoverTags": "", - "discoverName": "", - "discoverCover": "", - "discoverChapter": "", - "discoverDescription": "", - "discoverResult": "", - "enableSearch": false, - "searchUrl": "", - "searchAuthor": "", - "chapterCover": "", - "chapterTime": "", - "discoverAuthor": "", - "searchList": "", - "searchTags": "", - "searchName": "", - "searchCover": "", - "searchChapter": "", - "searchDescription": "", - "searchResult": "", - "enableMultiRoads": false, - "chapterRoads": "", - "chapterRoadName": "", - "chapterUrl": "", - "chapterNextUrl": "", - "chapterList": "", - "chapterName": "", - "chapterResult": "", - "contentUrl": "", - "contentNextUrl": "", - "contentItems": "" -} -``` - ```typescript export interface Rule { // ===== 通用字段 ===== @@ -142,12 +95,59 @@ enum ContentType { } ``` +```json +{ + "id": "xxx-xxx-xxx-xxx-xxx", + "author": "", + "name": "", + "host": "", + "icon": "", + "contentType": 1, + "sort": 0, + "userAgent": "", + "enableDiscover": false, + "discoverUrl": "", + "discoverNextUrl": "", + "discoverList": "", + "discoverTags": "", + "discoverName": "", + "discoverCover": "", + "discoverChapter": "", + "discoverDescription": "", + "discoverResult": "", + "enableSearch": false, + "searchUrl": "", + "searchAuthor": "", + "chapterCover": "", + "chapterTime": "", + "discoverAuthor": "", + "searchList": "", + "searchTags": "", + "searchName": "", + "searchCover": "", + "searchChapter": "", + "searchDescription": "", + "searchResult": "", + "enableMultiRoads": false, + "chapterRoads": "", + "chapterRoadName": "", + "chapterUrl": "", + "chapterNextUrl": "", + "chapterList": "", + "chapterName": "", + "chapterResult": "", + "contentUrl": "", + "contentNextUrl": "", + "contentItems": "" +} +``` + +::: + > 格式 `eso://:xxxxx` 是压缩后的规则, 软件也会自动识别, 也可以使用 [在线规则编解码工具](/play/comparess) 还原成json > > 并不是每个字段都是必填的, 按需填写既可。 -::: - ## 规则字段类型 ### URL地址规则 @@ -405,39 +405,13 @@ Content-Type: application/json > `discoverUrl` 虽然规则看起来像 `URL地址规则`, 但是用法截然不同, 所以这里单独说明 -## 规则支持情况 - -- ✅ 理论支持 -- ⚠️ 支持部分 -- ❌ 暂不支持 +## 规则表达式 -### URL地址规则 +[在线练习](/play/) -| 特性 | 支持情况 | 示例 | -| ---- | :------: | ---------------------------------------------------------------------------------------------------------- | -| URL | ✅ | `https://xxx.com/search?q=$keyword&pageSize=10` | -| JSON | ✅ | `{"url":"https://xxx.com/search","method":"post","headers":{"token":"111"},"body":{"keyword":"$keyword"}}` | -| @js | ✅ | `@js:(() => { return {url, method, body, headers}; })();` | - -### 规则表达式 - -| 特性 | 支持情况 | 说明 | 示例 | -| ---------- | :------: | -------------------------------- | --------------------------------------- | -| `@css` | ✅ | | `@css:.box1 .box2@text` | -| `@json` | ✅ | | `@json:$.list[:1].title` | -| `@xpath` | ✅ | | `@xpath://*[@class="box3"]/text()` | -| `@js` | ✅ | | | -| `@filter` | ✅ | 模拟浏览器加载地址后匹配指定链接 | `@filter:(?:m3u8\|mp4)(?:$\|/\|\\?\|&)` | -| `@replace` | ✅ | | `@replace:.*?url=\|.*?v=` | -| `##` | ✅ | 正则替换 | `@css:.c2 a@href##\\d+\\.html` | -| `{‍​‍{}}` | ✅ | 使用变量 | `http://www.aaa.com/{‍{$.id}}` | -| 嵌套组合 | ✅ | | `$.info.body@css:.box1 .box2@text` | -| `\|\|` | ✅ | | | -| `&&` | ✅ | | | - -规则可以省略开头的,**@css**、**@xpath**、**@json**, 因为解析器会尝试自动识别。 + -## 规则表达式 +--- ### CSS @@ -474,7 +448,7 @@ Content-Type: application/json > > 如果上一个流程`结果规则`拿到的结果是 `456`, 那么在`取列表规则`字段中 `@js:lastResult` 将输出 `456`, 在`URL地址规则`字段中 `@js:result` 将输出 `456` -内置方法: `CryptoJS`、`fetch`、`xpath` +内置方法: `CryptoJS`、`fetch`、`xpath`、`cheerio` #### CryptoJS diff --git a/packages/core/__test__/analyzer.test.ts b/packages/core/__test__/analyzer.test.ts index c5593f2b..2a779019 100644 --- a/packages/core/__test__/analyzer.test.ts +++ b/packages/core/__test__/analyzer.test.ts @@ -16,6 +16,36 @@ describe('analyzer', () => { expect(content).toEqual('iPhone'); }); + it('|| getString', async () => { + const body = '{"a": "1"}'; + const content = await analyzerManager.getString('$.b||$.a', body); + expect(content).toEqual('1'); + }); + + it('|| getStringList', async () => { + const body = '{"a": "1"}'; + const content = await analyzerManager.getStringList('$.b||$.a', body); + expect(content).toEqual(['1']); + }); + + it('##', async () => { + const body = '{"a": "123"}'; + const content = await analyzerManager.getString('$.a##2##替换文本', body); + expect(content).toEqual('1替换文本3'); + }); + + it('{{}}', async () => { + const body = '{"a": 1}'; + const content = await analyzerManager.getString('q{{$.x||$.a}}w', body); + expect(content).toEqual('q1w'); + }); + + it('@replace', async () => { + const body = '123'; + const content = await analyzerManager.getString('@replace:2', body); + expect(content).toEqual('13'); + }); + it('XPath', async () => { const body = '
content2
content3
'; const content = await analyzerManager.getString('//*[@class="box3"]/text()', body); diff --git a/packages/core/src/analyzer/AnalyzerManager.ts b/packages/core/src/analyzer/AnalyzerManager.ts index 65017c90..ab5316ec 100644 --- a/packages/core/src/analyzer/AnalyzerManager.ts +++ b/packages/core/src/analyzer/AnalyzerManager.ts @@ -158,13 +158,54 @@ export class AnalyzerManager { } async _getString(r: SingleRule, rule?: string): Promise { - const _rule = rule || r.rule; - try { - const res = await r.analyzer.getString(_rule); - return Array.isArray(res) ? res.join('').trim() : res; - } catch (error: any) { - throw new AnalyzerException(error?.message); + if (rule === undefined) { + rule = r.rule; } + + if (r.analyzer instanceof AnalyzerJS) { + const temp = await r.analyzer.getString(rule); + if (temp === null) { + return ''; + } else if (Array.isArray(temp)) { + return temp + .map((s) => typeof s !== 'undefined' && String(s).trim()) + .filter((s) => s) + .join(' '); + } + return String(temp).trim(); + } + + let result = ''; + + if (rule.includes('&&')) { + const rs = []; + for (const rSimple of rule.split('&&')) { + const temp = await this._getString(r, rSimple); + if (temp.length > 0) { + rs.push(temp); + } + } + return rs.join(' '); + } else if (rule.includes('||')) { + for (const rSimple of rule.split('||')) { + const temp = await this._getString(r, rSimple); + if (temp.length > 0) { + return temp; + } + } + } else { + const temp = await r.analyzer.getString(rule); + if (Array.isArray(temp)) { + result = temp + .map((s) => typeof s !== 'undefined' && String(s).trim()) + .filter((s) => s) + .join(' '); + } else if (temp !== null) { + result = String(temp).trim(); + } + } + + return result.length === 0 ? '' : this.replaceSmart(r.replace)(result); } public async getString(rule: string, body: string): Promise { @@ -197,13 +238,44 @@ export class AnalyzerManager { return temp; } - async _getStringList(r: SingleRule, rule?: string): Promise { - const _rule = rule || r.rule; - try { - return await r.analyzer.getStringList(rule || r.rule); - } catch (error: any) { - throw new AnalyzerException(error?.message); + async _getStringList(r: SingleRule, rule?: string): Promise { + if (rule === undefined) { + rule = r.rule; + } + + if (r.analyzer instanceof AnalyzerJS) { + return await r.analyzer.getStringList(rule); + } + + if (rule.includes('&&')) { + const result = []; + for (const rSimple of rule.split('&&')) { + const temp = await this._getStringList(r, rSimple); + if (Array.isArray(temp)) { + result.push(...temp.map((s) => s?.trim()).filter((s) => s)); + } else if (temp && temp.trim()) { + result.push(temp.trim()); + } + } + return result; + } else if (rule.includes('||')) { + let result: string[] = []; + for (const rSimple of rule.split('||')) { + const temp = await this._getStringList(r, rSimple); + if (Array.isArray(temp)) { + result = temp.map((s) => s?.trim()).filter((s) => s); + } else if (temp && temp.trim()) { + result = [temp.trim()]; + } + if (result.length > 0) { + return result; + } + } + } else { + return await r.analyzer.getStringList(rule); } + + return []; } public async getStringList(rule: string, body: string): Promise {