多应用+插件架构,代码干净,支持一键云编译,码云点赞13K star,4.8-4.12 预售价格198元 广告
script(脚本节点)用js代码写,用于解析获取到的数据并转换为目标数据。本节分节点对漫画插件示例的xml代码部分和js代码部分讲解(输出格式说明)。节尾赠送小说插件输出格式说明,其他类型插件输出格式说明请见[插件开放平台http://sited.noear.org/dev/](http://sited.noear.org/dev/) 里面的官方开发文档PDF。 <br> ## 2.4.1 hots、updates节点 ```xml <hots cache="1d" showImg="1" w="1" h="1" title="hots节点" method="get" parse="hots_parse" url="https://m.comic.naver.com/webtoon/weekday.nhn"/> <updates cache="1d" showImg="1" w="1" h="1" title="updates节点" method="get" parse="updates_parse" url="https://m.comic.naver.com/webtoon/weekday.nhn"/> ``` ```javascript function hots_parse(url, html) { var $ = cheerio.load(html); var list = []; $('.toon_lst li .lst a').each(function () { var item = $(this); var bm = {}; bm.name = item.find('.toon_name').text(); bm.url = urla(item.attr('href')).replace(/&week=.+$/, ''); bm.logo = item.find('img').attr('src'); list.push(bm); }); return JSON.stringify(list); } function updates_parse(url, html) { var $ = cheerio.load(html); var list = []; $('.toon_lst li .lst a').each(function () { var item = $(this); var bm = {}; bm.name = item.find('.toon_name').text(); bm.url = urla(item.attr('href')).replace(/&week=.+$/, ''); bm.logo = item.find('img').attr('src'); bm.newSection = ''; bm.updateTime = item.find('.sub_info').eq(0).text(); list.push(bm); }); return JSON.stringify(list); } function urla(u){var host="https://m.comic.naver.com";if(u.indexOf("http")<0){if(u.substr(0,2)!="//"){if(u.substr(0,1)!="/"){u=host+"/"+u}else{u=host+u}}else{u="https:"+u}}return encodeURI(u)}; ``` 漫画(或有目录图片)输出格式[dtype=1]如下 |处理事件|输出格式(字符串)|说明| |:-|:-|:-| |>login.check|"1"或"0"|详见高级篇插件加login节点教程| |>hots.parse|[{name:"",url:"",logo:""}]|| |>updates.parse|[{name:"",url:"",logo?:"",newSection:"",updateTime:"yyyy-MM-dd"}]|| hots节点通过hots.parse向用户展示热门漫画列表,上面示例js部分function hots_parse(url,html){}对应xml部分hots节点的@parse属性、@url属性(目标网址),所有parse(url,html)函数的html参数表示url对应的源代码文本(为电脑浏览器Ctrl+u后显示的不加载js的html源代码,而不是F12开发者工具Elements面板中显示的加载js后的html代码)。hots.parse输出含有name(漫画名字)、url(该漫画主页)、logo(漫画封面,实在没有就留空)的数组(字符串格式,后面节点的也是字符串格式)。urla函数是补全网址给任何节点调用,因为源网站上面漫画网址可能是相对路径,要识别补全协议、域名。<br> updates节点通过updates.parse展示最新漫画列表,updates_parse(url,html)及输出类似hots_parse的,只是输出数组里多出newSection(最新章节)、updateTime(更新时间)。 <br> ## 2.4.2 tags、tag节点 ```xml <tags title="分类" cache="1d" method="get" parse="tags_parse" url="https://m.comic.naver.com/webtoon/genre.nhn"/> …… <tag> <tag cache="10m" showImg="1" w="1" h="1" method="get" parse="tag_parse" expr="webtoon\/genre.nhn"/> </tag> ``` ```javascript function tags_parse(url, html) { var $ = cheerio.load(html); var list = []; list.push({ 'group': '正式漫画' }); $('#webtoonGenreTab ul li a').each(function () { var item = $(this); var bm = {}; bm.title = item.text(); bm.url = urla(item.attr('href')) + '&sort=NEW&page=@page'; list.push(bm); }); return JSON.stringify(list); } function tag_parse(url, html) { var $ = cheerio.load(html); var list = []; $('.toon_lst li').each(function () { var item = $(this); var bm = {}; bm.name = item.find('.toon_name').text(); bm.url = urla(item.find('a').attr('href')); bm.logo = item.find('img').attr('src'); bm.author = ''; bm.status = '正式漫画'; bm.newSection = ''; bm.updateTime = ''; list.push(bm); }); return JSON.stringify(list); } ``` |>tags.parse|[{title:"",group:"",url:""}]| |:-|:-| |>tag.parse|[{name:"",url:"",logo:"",author:"",status:"",newSection:"",updateTime:""}]| tags节点展示漫画分类列表(是展示所有分类标签),如果不用item静态配置就用tags.parse解析,tags.parse输出含有title(某一分类名字)、url(该分类的网址)、group(该分类的所属分组)的数组。注:group为非必须项(只能在分组的第一项里出现),比如输出数组有国产漫画、日本漫画、少女漫画、少男漫画的,源网站有写是按产地、按受众性别分类的,可以在数组第一项、第三项里加上group:"产地"、group:"受众性别"。<br> tag节点通过tag.parse展示某一个分类的漫画列表,tag.parse输出含有name(漫画名字)、url(该漫画主页)、logo(漫画封面)、author(漫画作者)、status(完本/连载状态)、newSection(新章节的名字)、updateTime(更新时间)的数组,后4个值中有几个在多多猫界面排斥占用同一行位置(多多猫展示模板决定的),所以tag和search节点的后4个值我会把想写的几个值都写在updateTime值里面(字符串拼接)。如果某一个分类的漫画列表第1、2、3页是`xx.com/p1、xx.com/p2、xx.com/p3`,想要tag_parse(url,html)对`xx.com/p1、xx.com/p2、xx.com/p3`解析,必须要在tags节点输出该分类的网址为`xx.com/p@page`,第一次进入tag节点会对`xx.com/p1`解析,用户在tag界面每次上拉进入下一页,tag节点会每次对@page属性加1替换@page为用户当前页数。 <br> ## 2.4.3 search节点 ```xml <search cache="1d" method="get" parse="search_parse" url="https://m.comic.naver.com/search/result.nhn?keyword=@key&amp;searchType=WEBTOON"/> ``` ```javascript function search_parse(url, html) { var $ = cheerio.load(html); var list = []; $('.lst').slice(0, 10).each(function () { var item = $(this); var bm = {}; bm.name = item.find('.toon_name').text(); bm.url = urla(item.children('a').attr('href')); bm.logo = item.find('img').attr('src'); bm.author = item.find('.sub_info').eq(0).text(); bm.status = '正式漫画'; bm.newSection = ''; bm.updateTime = ''; bm.btag = "正式漫画"; if (!bm.url.match(/javascript:goPcPage/i)) list.push(bm); }); return JSON.stringify(list); } ``` |>search.parse|[{name:"",url:"",logo:"",author:"",status:"",newSection:"",updateTime:"",btag:""}]| |:-|:-| search节点通过search.parse展示关键字@key的搜索结果列表,如果xml部分@method属性等于get的,@key写在@url属性里如`abc.com/@key`,用户填写搜索xx时,search_parse(url,html)的url参数自动变成`abc.com/xx`;如果xml部分@method属性等于post的(对于搜索页面post请求常见),@key写在@args属性里。search.parse输出类似tag.parse的,(author、status、newSection、updateTime也像tag节点排斥占用情况),只是search.parse输出数组里多出btag(即对搜索结果项业务类型的真实描述如漫画,不会显示在多多猫界面)。 <br> ## 2.4.4book、sections节点 ```xml <book cache="1d" method="get" buildUrl="book_buildUrl" parse="book_parse" expr="\/list\.nhn" > <sections cache="1d" method="get" buildUrl="sections_buildUrl" parseUrl="sections_parseUrl" parse="sections_parse" /> </book> ``` ```javascript function book_buildUrl(url) { return url.replace(/^http.+?\/\/[^\/]+/i, 'https://m.comic.naver.com'); } function book_parse(url, html) { var $ = cheerio.load(html); var data = {}; data.name = $('.info_in .title').text(); data.author = $('.info_in .nm').text(); data.logo = $('.toon_info .im_br img').attr('src'); data.intro = $('.info_in .info_cont').text(); data.updateTime = ''; data.isSectionsAsc = 0; if ($('.lst>a').length > 1) { if ($('.lst>a').eq(0).attr('href').match(/no=(\d+)/i)) { var a0 = $('.lst>a').eq(0).attr('href').match(/no=(\d+)/i)[1]; } else { var a0 = $('.lst').eq(0).parent().attr('data-no'); } if ($('.lst>a').eq(1).attr('href').match(/no=(\d+)/i)) { var a1 = $('.lst>a').eq(1).attr('href').match(/no=(\d+)/i)[1]; } else { var a1 = $('.lst').eq(1).parent().attr('data-no'); } if (Number(a0) < Number(a1)) data.isSectionsAsc = 1; } data.sections = []; return JSON.stringify(data); } function sections_buildUrl(url) { return url.replace(/^http.+?\/\/[^\/]+/i, 'https://m.comic.naver.com'); } function sections_parseUrl(url, html) { var $ = cheerio.load(html); var urls = url; if ($('.current_pg').text().match(/\d+\D+(\d+)/i)) { var pages = Number($('.current_pg').text().match(/\d+\D+(\d+)/i)[1]); for (i = 2; i <= pages; i++) { urls = urls + ';' + url + '&page=' + i; } } return urls; } function sections_parse(url, html) { var $ = cheerio.load(html); var data = {}; data.sections = []; $('.lst>a').each(function () { if ($(this).attr('href') && $(this).attr('href').match(/titleId/i)) { var bm = { name: $(this).find('.toon_name').text().trim(), url: urla($(this).attr('href')) }; } else { var bm = { name: $(this).find('.toon_name').text().trim() + '-要登陆源网站观看', url: '' }; } data.sections.push(bm); }); return JSON.stringify(data); } ``` 如果一个节点xml部分有@parse、@parseUrl、@buildUrl属性,js最先运行buildUrl函数,中间运行parseUrl,最后运行parse。上面示例代码中我用`function book_buildUrl(url){}`来转换接收的url参数域名为手机网页版域名,效果是用户在多多猫首页输入电脑网页版或者手机网页版的漫画主页,被能book.expr属性牵引入book节点变成手机网页版解析(也可以改成电脑网页版),方便用户输入任意版本的漫画主页就能直接进入book节点界面。<br> 上面示例代码中`function book_buildUrl(url){}`另一个作用是兼容旧版本插件不同域名的收藏。比如有个插件以前网址是`a.com`,用户收藏了漫画`a.com/book1.html`,后来源网页`a.com/book1.html`已关闭了并且迁移为`b.com/book1.html`。打开a网站插件收藏的漫画被book.expr(比如book\d+.html)牵引后默认解析`a.com/book1.html`,因为源网页打不开会解析失败,此时只要通过buildUrl变成`b.com/book1.html`后就能提供b网站漫画网址给后面parseUrl或parse环节来解析a网站插件收藏漫画的数据(还要在meta.expr节点里同时匹配新旧两个域名比如`a\.com|b\.com`;meta.url节点内容也不能改,以兼容用户在旧域名的收藏;并且在main节点增加durl属性为变化后网站首页)(因为book节点S按钮打开的是book节点最先接收的url网址,所以用户点击a网站插件收藏漫画进入book界面后,点击S按钮会默认进入浏览器访问已关闭的`a.com/book1.html`,建议通过`function buildWeb(url){}`来修改S按钮访问网址为`b.com/book1.html`,buildWeb函数大括号里可以直接复制用于兼容旧版本插件收藏的buildUrl函数里面代码,此时book节点的xml部分要对应加上buildWeb属性)。 |>book.parseUrl?|url或CALL::url(多个时用";"隔开)<br>url=目标url(其请求结果转到parse处理)<br>CALL::url = 中转解析url(其请求结果仍回到parseUrl处理)<br>目标url需要中转解析,或分布在多个url上时使用。|| |:-|:-|:-| |>book.parse|{name:"",author:"",intro:"",logo:"",updateTime:"yyyy-MM-dd",isSectionsAsc:1或0,sections:[{name:"",url:""}]}|book节点(dtype等于1时)通过book.parse展示某一本漫画书的介绍和目录,book.parse输出含有name(漫画名字)、author(该漫画作者)、intro(漫画介绍)、logo(漫画封面)、updateTime(漫画更新时间)、isSectionsAsc(章节排序,1为源网页是升序,0为降序,如果不写该属性就是0的效果)、sections(即每一话目录数组,name为每一话名字,url为该话网址。url为空时,name则为分组)的对象。| 当book.parse接入url和源网站存放该漫画目录数据的网址不一样时(比如后者分为几页网址)可以如上面示例使用sections节点,sections节点必须含buildUrl和parse属性,有需要时可用parseUrl属性但(截止v35引擎)要运行buildUrl才能进入parseUrl。sections节点buildUrl和parseUrl的xml属性和js输出格式和book节点的类似。<br> sections_buildUrl(url)的url参数就是book上级节点输出的url(不一定是book_parse的url参数,有book_parseUrl就是parseUrl的url参数,有book_buildUrl则是buildUrl的url参数),sections_parseUrl(url,html)输出全话目录所分布的url(多个时用";"隔开)。sections.parse输出含有name(每一话漫画名字)、url(该话网址)的数组,如果xml部分有sections节点,js部分book_parse里面data.sections变量直接为空白数组不填内容。book节点存储的全局变量数据在sections的buildUrl读取不了,需要在后面的parseUrl或parse环节读取。 <br> ## 2.4.5 section节点(dtype=1) ```xml <section cache="1d" method="get" options="0,0,0,1" parseUrl="section_parseUrl" parse="section_parse" header="referer"/> ``` ```javascript function section_parseUrl(url, html) { var $ = cheerio.load(html); if ($('#id_area').length || $('#toonLayer').length || $('#mflick').length) { return url; } else { curl = html.match(/effecttoonContent[\s\S]+?imageUrl.+?['"\s]+(http.+?)['"\s]/i)[1] + '/'; return urla(html.match(/documentURL\s*:\s*['"]\s*(.+?)\s*['"]/i)[1]); } } function section_parse(url, html) { var list = []; if (url.match(/detail.nhn\?titleId=/i) || url.match(/nid.naver.com\/nidlogin/i) || url.match(/detail.nhn\?titleId=.+?#nafullscreen/i)) { var $ = cheerio.load(html); if ($('#toonLayer').length) { $('#toonLayer ul li img').each(function () { list.push($(this).attr('data-src')); }); } else if ($('#mflick').length) { $('#mflick>.swiper-wrapper>.swiper-slide>img').each(function () { list.push($(this).attr('data-src')); }); } else {} } else { var json = JSON.parse(html); for (var i in json.pages) { for (var j in json.pages[i].layers) { var item = json.pages[i].layers[j].asset.replace('image/', ''); var imageurl = json.assets.image[item]; if (imageurl.match(/\.jpg/i)) list.push(curl + imageurl); } } } return JSON.stringify(list); } ``` |>section.parseUrl?|url;url;url(目标url字符串;多个时用";"隔开)<br>section的数据分布在多个网页;或目标url需要中转解析时使用|如果能满足需要,可以用buildUrl(url)不用parseUrl(url,html)函数,区别是前者通过上级节点输出的url生成新的目标url(不读取html源代码) |:-|:-|:-| |>section.parse|["",""](图片地址的数组)<br>或者,带背景音乐的{bg:"",list:["",""]}需引擎v25支持)<br>或者,带背景音乐+带时间位置的{bg:"",list:[{url:"",time:xxx},{...}]}(xxx毫秒)<br>bg=背景音乐url网址,用于支持有声漫画|section节点(dtype等于1)通过section.parse展示漫画某一话所有图片。上面示例源网站需要referer验证才能显示图片,所以在xml部分加上header属性(值为"referer")| ## 2.4.6 section节点(dtype=2) |>section.parse|[{d:"xxx",t:1,c:"#999999",b:1,i:1,u:1,w:10,h:10}]<br>d:数据<br>t:类型(1,文本;8,视频;9,图片;10,原始大小图片[居中])<br>c?:颜色<br>b?:是否加粗<br>i?:是否斜体<br>u?:是否下划线<br>w?:视图宽(当t=8或10时有效)<br>h?:视图高(当t=8或10时有效)|dtype1漫画和dtype2小说的格式唯一区别在于section_parse的输出,前者输出图片(可带音频),后者输出含有文本可带图片、视频(比较少见)的数组。| |:-|:-|:-| (完)