多多猫app的siteD引擎长啥样,怎么运行插件的?

在开始前,先讲个故事:

在很久很久以前,一位懵懂的少年用着一款uwp的手机,他很喜欢看漫画,但是他望着这款装着软件都没几个的系统的手机,他很茫然。为此,他联系了很多开发者,希望有人能开发一个看漫画app,结果too young too simple。但是这位少年并没有因此而放弃,他产生了一个“大神由此而诞生”的想法,没错自己写一个。就这样,多多猫app的“前身”就这样诞生了。但是“前身”还只是对一个网站(KuKu漫画)做简单的抓取,这显然是不够的,需要抓取更多的网站,随着网站的增多,app内部也需要一个完善的体系来适配这些“抓取的方法”,这就是历经岁月的摧残而诞生的siteD引擎github

故事讲完了,接下来我们就来大致了解下,这个siteD引擎到底是怎么运行插件的?
siteD引擎结构图

插件导入引擎后,siteD引擎会首先读取插件的xml节点,将它转成引擎内部的SdSource类,之后所有的运行都基于这个类。

<site ver="1" engine="30" schema="1">
    <meta></meta>
    <main></main>
    <script></script>
</site>

对于最外层的site节点:因为他是第一个,引擎并没有通过Name来识别它,可以凭自己的喜好随意更改sitedd,loveyou等等。但是它附带的参数很重要,ver插件版本,engine引擎版本,schema则是引擎的更新中修改了节点name,当你把engine设置成25+时,必须添加schema="1",不然引擎会以旧版的NodeName取抓取子节点。

site中一共包含了三个子节点(meta、main、script):
1.meta

<meta>
        <ua></ua>
        <guid>xxxxxxxxxxxxxxxxxxx</guid>
        <title>733漫画</title>
        <intro>733动漫网_好看的动漫_日本动漫_动漫大全_最新动漫</intro>
        <author>Seiko</author>
        <url>http://www.733dm.net</url>
        <expr>733dm\.net</expr>
        <logo></logo>
        <encode>gb18030</encode>
</meta>

meta里面的数据还是比较明了的,顺便说下这里的url主要起展示作用,这里就简述下guid、expr:
guid——将插件上传到服务器时的唯一凭证,因此写好插件后,需要添加guid才能上传到服务器。
expr——siteD引擎用来判断用哪个插件来解析当前的url。就像你的收藏里有来自57的、汗汗的,总不能拿汗汗的插件来解析57的url。

3.script(main比较复杂,先讲script)

<script>
        <require>
            <item url="http://sited.noear.org/addin/js/cheerio.js" lib="cheerio" />
            <item url="http://sited.noear.org/addin/js/base64.js" />
        </require>
        <code>
        <![CDATA[

var urla = (function() {
    var host = "http://www.733dm.net";

    return function(u) {
        if (u.indexOf("http") < 0) {
            u = host + u;
        }
        return encodeURI(u);
    }
})();

function tg_burl(url, page) {
    if (page > 1) {
        url += "index_" + page + ".html";
    }
    return url;
}


function ht_parse(url, html) {
    var $ = cheerio.load(html);
    var list = [];
    
    $('ul.scroll').find('img').each(function() {
        var img  = $(this);
        
        var bm = {};
        bm.name  = img.attr('alt');
        bm.url   = urla(img.parent().attr('href'));
        bm.logo  = img.attr('src');

        list.push(bm);
    });
    return JSON.stringify(list);
}


function up_parse(url, html) {
    var $ = cheerio.load(html);
    var list = [];
    return JSON.stringify(list);
}


function tg_parse(url, html) {
    var $ = cheerio.load(html);
    var list = [];
    return JSON.stringify(list);
}


function bk_parse(url, html) {
    var $ = cheerio.load(html);
    var data = {};
    return JSON.stringify(data);
}


function sn_parse(url, html) {
    return JSON.stringify(list);
}

        ]]>
        </code>
    </script>

很明显,这个节点就是放js代码的,code放自己写的代码,<require>放一些引用的库,如图上的cheerio和base64。至于lib的作用,siteD引擎里面已经包含了一些js库,lib就是用来识别的,不写也没事,因为第一次加载后,引擎就会缓存。


引擎包含的库

2.main

<main dtype="1">
        <home>
            <hots cache="1d" title="热门" method="get"  parse="ht_parse" url="http://www.733dm.net" />
            <updates cache="1d" title="更新" method="get" parse="up_parse" url="http://www.733dm.net/mh/update.html" />
            <tags  title="分类">
                <item title="国产" url="http://www.733dm.net/mh/guochan/" group="地区" /><item title="日本" url="http://www.733dm.net/mh/riben/" />
                <item title="欧美" url="http://www.733dm.net/mh/oumei/" />
                <item title="韩国" url="http://www.733dm.net/mh/hanguo/" />

                <item title="冒险" url="http://www.733dm.net/mh/maoxian/" group="分类" />
                <item title="魔法" url="http://www.733dm.net/mh/mofa/" />
                <item title="东方神鬼" url="http://www.733dm.net/mh/dongfangshengui/" />
            </tags>
        </home>
        <search  cache="1d" method="get" parse="tg_parse" url="http://www.733dm.net/e/search/index.php?searchget=1&amp;keyboard=@key&amp;myorder=1&amp;orderby=1&amp;show=title,player,playadmin,bieming,pinyin&amp;tbname=mh&amp;tempid=3" />
        <tag     cache="0"  method="get" parse="tg_parse" buildUrl="tg_burl" />
        <book    cache="1d" method="get" parse="bk_parse" />
        <section cache="1"  method="get" parse="sn_parse" header="cookie;referer" />

main里面的结构就比较多了,因为插件的全部内容都在这。main的参数还有很多,具体的可以看开发文档,我这边就讲些我觉得比较重要的。开发文档

dtype:
插件类型,多多猫也是通过这个参数来判断给你漫画界面还是小说界面还是视频界面。注意:不同的插件类型你需要在js里返回的list也是不一样的,具体看开发文档。


插件类型

home:你点开插件时跳转的界面(插件首页),用过多多猫的应该都知道,点开插件最多只有3种界面(热门hots、更新updates、分类tags)。
search:搜索界面
tag:分类界面
book:目录界面(当没有目录时(例:dtype=4),这个就变成了浏览界面)
section:浏览界面(看漫画or小说or视频)

以上5个界面就是多多猫的主要界面,对比app来看,大致的结构还是比较明了的。接下来统一讲下里面的参数。
cache:缓存。0不缓存、1永久缓存、1d缓存一天、60m缓存1小时。
title、url:标题、链接。毕竟有些链接是需要你自己指明的,像hots、updates、search。从这些界面中衍生的则不需要这些参数,像tag、book、section。
method:请求类型。无非get、post,为了应付某些情况引擎加了一个@null(不请求)。
parse:指明你用的哪个方法来解析该节点。比如js代码中function bk_parse(url, html) {}是用来解析目录的,就在book节点里写parse="bk_parse"。
parseUrl:这是一个非常重要的方法,返回一组urls或一个url,对这组url都进行parse。后面对这个做了加强,可以返回一个CALL::GET::+url。这样引擎会在请求后返回parseUrl,不停循环直到抓到你想要的url或urls。

剩下的buildUrl、header等就不细说了。这边就说下siteD的大致请求流程:(以section为例)

从xx传来url->
expr@选择用哪个插件->
开始请求html(每次请求前都会buildUrl,重新生成header、cookie)->
parseUrl@是否需要进入parseUrl(每次parseUrl都会请求html)->
parse@解析当前页面,返回最终结果

插件的简介说的差不多了,下面说下引擎是怎么处理上面的数据的。
引擎中对应的代码

里面的xml就是插件。
doInit() # 解析xml
doLoad() # 将解析后的数据转换成能用的类,怎么转换的就不细说了,去看siteD开源代码吧。(SdNodeSet、SdNode、SdJscript)

1.SdNodeSet是下面的爸爸,例如上面的mian、script、meta。
2.SdNode每个节点,也是解析时需要用的的对象,例如book、section。
3.Sdscript这个是处理js代码的,需要注意的是这个类只起到处理作用,最后的代码都会导入到SdEngine(伟大的J2V8引擎在这个类里)。

插件转换成SdSource后,引用下面的方法,就能开始解析了。(由于siteD代码这块非常复杂,不适合讲解,这边就贴出我的只含有部分功能的简版了,挑战自己的可以去github看源码)

以首页热门为例,上面说到了,

<hots cache="1d" title="热门" method="get"  parse="ht_parse" url="http://www.733dm.net" />

被转成了SdNode类,加上url两个参数,对应解析方法为:

public Flowable<YhPair> doGetNodeViewModel(final SdNode cfg, final String url) {
        return Observable.create(new ObservableOnSubscribe<String>() {
                    @Override
                    public void subscribe(@NonNull ObservableEmitter<String> e) throws Exception {
                        String html = getHtml(cfg, url);      //这里url已经是处理好的,直接请求html(每次请求都会重新判断类型,添加header、cookie)。
                        if (!TextUtils.isEmpty(cfg.parseUrl)) {    //是否进入parseUrl循环
                            String parseUrl = rxParseUrl(cfg, url, html).blockingFirst();  //导入v8引擎跑出结果。

                            while (parseUrl.startsWith(Util.NEXT_CALL)) {       //是否是以"CALL::"开头的,进入循环(尚未测试,可能无效)
                                parseUrl = parseUrl.replace(Util.NEXT_CALL, "");
                                log("doGetNodeViewModel-isNextUrl", parseUrl);
                                String html2 = getHtml(cfg, parseUrl);                         //请求html
                                parseUrl = rxParseUrl(cfg, url, html2).blockingFirst();  //导入v8引擎跑出结果。
                            }

                            String[] urls = parseUrl.split(";");  //将urls转换成数组,并对每个url请求处理
                            for (String u1 : urls) {
                                String html3 = getHtml(cfg, u1);
                                log("doGetNodeViewModel-isParseUrl", html3);
                                e.onNext(html3);
                            }
                        } else {
                            e.onNext(html); //不需要做啥,直接发送html,直接进行parse处理
                        }
                        e.onComplete();
                    }
                })
                .flatMap(new Function<String, ObservableSource<YhPair>>() {
                    @Override
                    public ObservableSource<YhPair> apply(@NonNull String html) throws Exception {
                        String json = rxParse(cfg, url, html);  //进行parse解析
                        log("rxPrase-json", json);
                        return Observable.just(new YhPair(cfg, json)); //将json结果和cfg打包
                    }
                }).subscribeOn(Schedulers.io()).toFlowable(BackpressureStrategy.BUFFER);
    }

引用上面的方法,接着进行处理:

SourceApi.getInstance().rxgetSource(source) //获得插件对应的SdSource
//                .delay(200, TimeUnit.MILLISECONDS)
                .flatMap(new Function<YhSource, Publisher<YhPair>>() {
                    @Override
                    public Publisher<YhPair> apply(@NonNull YhSource sd) throws Exception {
                        return sd.doGetNodeViewModel(sd.Hots(), sd.Hots().url); //由于代码没有全贴,这里简写了。
                    }
                })
                .flatMap(new Function<YhPair, Publisher<List<HotsBean>>>() {
                    @Override
                    public Publisher<List<HotsBean>> apply(@NonNull YhPair pair) throws Exception {
                        List<HotsBean> list = new ArrayList<>();

                        JsonArray array = new JsonParser().parse(pair.getJson()).getAsJsonArray(); //处理v8返回的json数据,这里用的gson工具。
                        for (JsonElement el : array) {
                            JsonObject n = el.getAsJsonObject();
                            String name = getString(n, "name");
                            String logo = getString(n, "logo");
                            String url = getString(n, "url");

                            HotsBean bean = new HotsBean();
                            bean.setName(name);
                            bean.setLogo(logo);
                            bean.setUrl(url);
                            bean.setSource(source);
                            list.add(bean);
                        }
                        return Flowable.just(list);
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<HotsBean>>() {
                    @Override
                    public void accept(@NonNull List<HotsBean> list) throws Exception {
                        if (list.size() > 0) {
                            mView.onSuccess(list); //全部解析好了,发送给界面。
                        } else {
                            mView.onFailed();
                        }
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(@NonNull Throwable throwable) throws Exception {
                        mView.onFailed();
                    }
                });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容