在開始前瘟裸,先講個(gè)故事:
在很久很久以前客叉,一位懵懂的少年用著一款uwp的手機(jī),他很喜歡看漫畫,但是他望著這款裝著軟件都沒幾個(gè)的系統(tǒng)的手機(jī)兼搏,他很茫然卵慰。為此,他聯(lián)系了很多開發(fā)者佛呻,希望有人能開發(fā)一個(gè)看漫畫app裳朋,結(jié)果too young too simple。但是這位少年并沒有因此而放棄吓著,他產(chǎn)生了一個(gè)“大神由此而誕生”的想法再扭,沒錯(cuò)自己寫一個(gè)。就這樣夜矗,多多貓app的“前身”就這樣誕生了。但是“前身”還只是對一個(gè)網(wǎng)站(KuKu漫畫)做簡單的抓取让虐,這顯然是不夠的紊撕,需要抓取更多的網(wǎng)站,隨著網(wǎng)站的增多赡突,app內(nèi)部也需要一個(gè)完善的體系來適配這些“抓取的方法”对扶,這就是歷經(jīng)歲月的摧殘而誕生的siteD引擎github。
故事講完了惭缰,接下來我們就來大致了解下浪南,這個(gè)siteD引擎到底是怎么運(yùn)行插件的?
插件導(dǎo)入引擎后漱受,siteD引擎會(huì)首先讀取插件的xml節(jié)點(diǎn)络凿,將它轉(zhuǎn)成引擎內(nèi)部的SdSource類,之后所有的運(yùn)行都基于這個(gè)類昂羡。
<site ver="1" engine="30" schema="1">
<meta></meta>
<main></main>
<script></script>
</site>
對于最外層的site節(jié)點(diǎn):因?yàn)樗堑谝粋€(gè)絮记,引擎并沒有通過Name來識(shí)別它,可以憑自己的喜好隨意更改sitedd虐先,loveyou等等怨愤。但是它附帶的參數(shù)很重要,ver插件版本蛹批,engine引擎版本撰洗,schema則是引擎的更新中修改了節(jié)點(diǎn)name,當(dāng)你把engine設(shè)置成25+時(shí)腐芍,必須添加schema="1"差导,不然引擎會(huì)以舊版的NodeName取抓取子節(jié)點(diǎn)。
site中一共包含了三個(gè)子節(jié)點(diǎn)(meta甸赃、main柿汛、script):
1.meta
<meta>
<ua></ua>
<guid>xxxxxxxxxxxxxxxxxxx</guid>
<title>733漫畫</title>
<intro>733動(dòng)漫網(wǎng)_好看的動(dòng)漫_日本動(dòng)漫_動(dòng)漫大全_最新動(dòng)漫</intro>
<author>Seiko</author>
<url>http://www.733dm.net</url>
<expr>733dm\.net</expr>
<logo></logo>
<encode>gb18030</encode>
</meta>
meta里面的數(shù)據(jù)還是比較明了的,順便說下這里的url主要起展示作用,這里就簡述下guid络断、expr:
guid——將插件上傳到服務(wù)器時(shí)的唯一憑證裁替,因此寫好插件后,需要添加guid才能上傳到服務(wù)器貌笨。
expr——siteD引擎用來判斷用哪個(gè)插件來解析當(dāng)前的url弱判。就像你的收藏里有來自57的、汗汗的锥惋,總不能拿汗汗的插件來解析57的url昌腰。
3.script(main比較復(fù)雜,先講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>
很明顯膀跌,這個(gè)節(jié)點(diǎn)就是放js代碼的遭商,code放自己寫的代碼,<require>放一些引用的庫捅伤,如圖上的cheerio和base64劫流。至于lib的作用,siteD引擎里面已經(jīng)包含了一些js庫丛忆,lib就是用來識(shí)別的祠汇,不寫也沒事,因?yàn)榈谝淮渭虞d后熄诡,引擎就會(huì)緩存可很。
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="國產(chǎn)" url="http://www.733dm.net/mh/guochan/" group="地區(qū)" /><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="冒險(xiǎn)" 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&keyboard=@key&myorder=1&orderby=1&show=title,player,playadmin,bieming,pinyin&tbname=mh&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里面的結(jié)構(gòu)就比較多了,因?yàn)椴寮娜績?nèi)容都在這凰浮。main的參數(shù)還有很多我抠,具體的可以看開發(fā)文檔,我這邊就講些我覺得比較重要的导坟。開發(fā)文檔
dtype:
插件類型屿良,多多貓也是通過這個(gè)參數(shù)來判斷給你漫畫界面還是小說界面還是視頻界面。注意:不同的插件類型你需要在js里返回的list也是不一樣的惫周,具體看開發(fā)文檔尘惧。
home:你點(diǎn)開插件時(shí)跳轉(zhuǎn)的界面(插件首頁),用過多多貓的應(yīng)該都知道递递,點(diǎn)開插件最多只有3種界面(熱門hots喷橙、更新updates、分類tags)登舞。
search:搜索界面
tag:分類界面
book:目錄界面(當(dāng)沒有目錄時(shí)(例:dtype=4)贰逾,這個(gè)就變成了瀏覽界面)
section:瀏覽界面(看漫畫or小說or視頻)
以上5個(gè)界面就是多多貓的主要界面,對比app來看菠秒,大致的結(jié)構(gòu)還是比較明了的疙剑。接下來統(tǒng)一講下里面的參數(shù)氯迂。
cache:緩存。0不緩存言缤、1永久緩存嚼蚀、1d緩存一天、60m緩存1小時(shí)管挟。
title轿曙、url:標(biāo)題、鏈接僻孝。畢竟有些鏈接是需要你自己指明的导帝,像hots、updates穿铆、search您单。從這些界面中衍生的則不需要這些參數(shù),像tag荞雏、book睹限、section。
method:請求類型讯檐。無非get、post染服,為了應(yīng)付某些情況引擎加了一個(gè)@null(不請求)别洪。
parse:指明你用的哪個(gè)方法來解析該節(jié)點(diǎn)。比如js代碼中function bk_parse(url, html) {}是用來解析目錄的柳刮,就在book節(jié)點(diǎn)里寫parse="bk_parse"挖垛。
parseUrl:這是一個(gè)非常重要的方法,返回一組urls或一個(gè)url秉颗,對這組url都進(jìn)行parse。后面對這個(gè)做了加強(qiáng)蚕甥,可以返回一個(gè)CALL::GET::+url哪替。這樣引擎會(huì)在請求后返回parseUrl,不停循環(huán)直到抓到你想要的url或urls菇怀。
剩下的buildUrl凭舶、header等就不細(xì)說了。這邊就說下siteD的大致請求流程:(以section為例)
從xx傳來url->
expr@選擇用哪個(gè)插件->
開始請求html(每次請求前都會(huì)buildUrl爱沟,重新生成header帅霜、cookie)->
parseUrl@是否需要進(jìn)入parseUrl(每次parseUrl都會(huì)請求html)->
parse@解析當(dāng)前頁面,返回最終結(jié)果
插件的簡介說的差不多了呼伸,下面說下引擎是怎么處理上面的數(shù)據(jù)的身冀。
里面的xml就是插件。
doInit() # 解析xml
doLoad() # 將解析后的數(shù)據(jù)轉(zhuǎn)換成能用的類,怎么轉(zhuǎn)換的就不細(xì)說了搂根,去看siteD開源代碼吧珍促。(SdNodeSet、SdNode兄墅、SdJscript)
1.SdNodeSet是下面的爸爸踢星,例如上面的mian、script隙咸、meta沐悦。
2.SdNode每個(gè)節(jié)點(diǎn),也是解析時(shí)需要用的的對象五督,例如book藏否、section。
3.Sdscript這個(gè)是處理js代碼的充包,需要注意的是這個(gè)類只起到處理作用副签,最后的代碼都會(huì)導(dǎo)入到SdEngine(偉大的J2V8引擎在這個(gè)類里)。
插件轉(zhuǎn)換成SdSource后基矮,引用下面的方法淆储,就能開始解析了。(由于siteD代碼這塊非常復(fù)雜家浇,不適合講解本砰,這邊就貼出我的只含有部分功能的簡版了,挑戰(zhàn)自己的可以去github看源碼)
以首頁熱門為例钢悲,上面說到了点额,
<hots cache="1d" title="熱門" method="get" parse="ht_parse" url="http://www.733dm.net" />
被轉(zhuǎn)成了SdNode類,加上url兩個(gè)參數(shù)莺琳,對應(yīng)解析方法為:
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已經(jīng)是處理好的还棱,直接請求html(每次請求都會(huì)重新判斷類型,添加header惭等、cookie)珍手。
if (!TextUtils.isEmpty(cfg.parseUrl)) { //是否進(jìn)入parseUrl循環(huán)
String parseUrl = rxParseUrl(cfg, url, html).blockingFirst(); //導(dǎo)入v8引擎跑出結(jié)果。
while (parseUrl.startsWith(Util.NEXT_CALL)) { //是否是以"CALL::"開頭的辞做,進(jìn)入循環(huán)(尚未測試珠十,可能無效)
parseUrl = parseUrl.replace(Util.NEXT_CALL, "");
log("doGetNodeViewModel-isNextUrl", parseUrl);
String html2 = getHtml(cfg, parseUrl); //請求html
parseUrl = rxParseUrl(cfg, url, html2).blockingFirst(); //導(dǎo)入v8引擎跑出結(jié)果。
}
String[] urls = parseUrl.split(";"); //將urls轉(zhuǎn)換成數(shù)組凭豪,并對每個(gè)url請求處理
for (String u1 : urls) {
String html3 = getHtml(cfg, u1);
log("doGetNodeViewModel-isParseUrl", html3);
e.onNext(html3);
}
} else {
e.onNext(html); //不需要做啥焙蹭,直接發(fā)送html,直接進(jìn)行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); //進(jìn)行parse解析
log("rxPrase-json", json);
return Observable.just(new YhPair(cfg, json)); //將json結(jié)果和cfg打包
}
}).subscribeOn(Schedulers.io()).toFlowable(BackpressureStrategy.BUFFER);
}
引用上面的方法嫂伞,接著進(jìn)行處理:
SourceApi.getInstance().rxgetSource(source) //獲得插件對應(yīng)的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數(shù)據(jù),這里用的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); //全部解析好了粪般,發(fā)送給界面。
} else {
mView.onFailed();
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
mView.onFailed();
}
});