1. 程式人生 > >FreeBook 基於 MVP 模式開發的帶快取網路爬蟲,採用最流行框架搭建,乾貨多多

FreeBook 基於 MVP 模式開發的帶快取網路爬蟲,採用最流行框架搭建,乾貨多多

用到的主流框架

  • RxJava+Retrofit2+Okhttp+RxCache 實現 API 資料請求以及快取(快取不區分 GET&POST 快取策略可根據自己要求修改)

  • RxJava+jsoup+RxCache 實現 HTMl 頁面爬蟲資料的請求以及快取 快取實現與 API 一致 不需要另寫邏輯

  • glide 載入圖片

  • LCRapidDevelop 下拉重新整理 狀態頁 RecyclerView 介面卡 RecyclerView 載入動畫 等等感興趣的自行了解 傳送門

  • bga-banner 首頁的 Banner 實現無限迴圈 還不錯 整合簡單

功能點

  • 首頁 banner 以及推薦資料 根據後臺介面更新(總要有點自己可控的元素嘛 比如加個廣告什麼的 哈哈 比如說)

  • 書庫類別 以及類別的 HTML 地址等資料 通過後臺介面控制 (如果哪天我覺得這個網站的資源不是很豐富 我可以很任性的直接在後臺換一個)

  • 資料快取 請求 HTML 網頁再從網頁上抓取想要的資料其實相對 API 來說耗時會比較大 快取就顯得非常重要了

  • 檔案下載統一管理 並且呼叫系統支援的程式開啟檔案

首先詳細講解一下 RxJava+Retrofit2+Okhttp+RxCache 的使用 五部曲

第一步:導包

    compile 'io.reactivex:rxjava:1.1.8'
    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
    compile 'com.github.VictorAlbertos.RxCache:core:1.4.6'

第二步:新建 API 介面

/**
 * API 介面 
 * 因為使用 RxCache 作為快取策略 所以這裡不需要寫快取資訊
 */
public interface MovieService {

    //獲取書庫分類資訊
    @GET("freebook/typeconfigs.json")
    Observable<List<BookTypeDto>> getBookTypes();

    //獲得首頁 banner 以及書籍資料
    @GET("freebook/home.json")
    Observable<HomeDto> getHomeInfo();

    //獲得搜尋標籤
    @GET("freebook/search_lable.json")
    Observable<List<String>> getSearchLable();
}

第三步:新建快取介面(Html 爬蟲共用)

/**
 * 快取 API 介面
 * @LifeCache 設定快取過期時間. 如果沒有設定@LifeCache , 資料將被永久快取理除非你使用了 EvictProvider, EvictDynamicKey or EvictDynamicKeyGroup .
 * EvictProvider 可以明確地清理清理所有快取資料.
 * EvictDynamicKey 可以明確地清理指定的資料 DynamicKey.
 * EvictDynamicKeyGroup 允許明確地清理一組特定的資料. DynamicKeyGroup.
 * DynamicKey 驅逐與一個特定的鍵使用 EvictDynamicKey 相關的資料。比如分頁,排序或篩選要求
 * DynamicKeyGroup。驅逐一組與 key 關聯的資料,使用 EvictDynamicKeyGroup。比如分頁,排序或篩選要求
 */
public interface CacheProviders {
    //獲取書庫對應類別列表  快取時間 1 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<List<BookInfoListDto>>> getStackTypeList(Observable<List<BookInfoListDto>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //獲取書庫分類資訊快取資料 快取時間 永久
    Observable<Reply<List<BookTypeDto>>> getBookTypes(Observable<List<BookTypeDto>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //獲取首頁配置資料 banner 最熱 最新  快取時間 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<HomeDto>> getHomeInfo(Observable<HomeDto> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //獲取搜尋標籤  快取時間 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<List<String>>> getSearchLable(Observable<List<String>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //獲取書籍詳情  快取時間 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<BookInfoDto>> getBookInfo(Observable<BookInfoDto> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);
}

第四步:新建 retrofit 抽象類


/**
 *封裝一個 retrofit 整合 0kHttp3 的抽象基類
 */
public abstract class RetrofitUtils {

    private static Retrofit mRetrofit;
    private static OkHttpClient mOkHttpClient;
    /**
     * 獲取 Retrofit 物件
     *
     * @return
     */
    protected static Retrofit getRetrofit() {

        if (null == mRetrofit) {

            if (null == mOkHttpClient) {
                mOkHttpClient = new OkHttpClient.Builder().build();
            }

            //Retrofit2 後使用 build 設計模式
            mRetrofit = new Retrofit.Builder()
                    //設定伺服器路徑
                    .baseUrl(Constant.API_SERVER + "/")
                    //新增轉化庫,預設是 Gson
                    .addConverterFactory(GsonConverterFactory.create())
                    //添加回調庫,採用 RxJava
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    //設定使用 okhttp 網路請求
                    .client(mOkHttpClient)
                    .build();
        }

        return mRetrofit;
    }

}

第五步:新建 HttpData 類 用於統一管理請求

/*
 *所有的請求資料的方法集中地
 * 根據 MovieService 的定義編寫合適的方法
 * 其中 observable 是獲取 API 資料
 * observableCahce 獲取快取資料
 * new EvictDynamicKey(false) false 使用快取  true 載入資料不使用快取
 */
public class HttpData extends RetrofitUtils {

    private static File cacheDirectory = FileUtil.getcacheDirectory();
    private static final CacheProviders providers = new RxCache.Builder()
            .persistence(cacheDirectory)
            .using(CacheProviders.class);
    protected static final MovieService service = getRetrofit().create(MovieService.class);

    //在訪問 HttpMethods 時建立單例
    private static class SingletonHolder {
        private static final HttpData INSTANCE = new HttpData();
    }

    //獲取單例
    public static HttpData getInstance() {
        return SingletonHolder.INSTANCE;
    }

    //獲取 app 書本類別
    public void getBookTypes(Observer<List<BookTypeDto>> observer){
        Observable observable=service.getBookTypes();
        Observable observableCahce=providers.getBookTypes(observable,new DynamicKey("書本類別"),new EvictDynamicKey(false)).map(new HttpResultFuncCcche<List<BookTypeDto>>());
        setSubscribe(observableCahce,observer);
    }
    //獲取 app 首頁配置資訊  banner  最新 最熱
    public void getHomeInfo(Observer<HomeDto> observer){
        Observable observable=service.getHomeInfo();
        Observable observableCache=providers.getHomeInfo(observable,new DynamicKey("首頁配置"),new EvictDynamicKey(false)).map(new HttpResultFuncCcche<HomeDto>());
        setSubscribe(observableCache,observer);
    }
    //獲得搜尋熱門標籤
    public void getSearchLable(Observer<List<String>> observer){
        Observable observable=service.getSearchLable();
        Observable observableCache=providers.getSearchLable(observable,new DynamicKey("搜尋熱門標籤"), new EvictDynamicKey(false)).map(new HttpResultFuncCcche<List<String>>());
        setSubscribe(observableCache,observer);
    }
    /**
     * 插入觀察者
     *
     * @param observable
     * @param observer
     * @param <T>
     */
    public static <T> void setSubscribe(Observable<T> observable, Observer<T> observer) {
        observable.subscribeOn(Schedulers.io())
                .subscribeOn(Schedulers.newThread())//子執行緒訪問網路
                .observeOn(AndroidSchedulers.mainThread())//回撥到主執行緒
                .subscribe(observer);
    }

    /**
     * 用來統一處理 RxCacha 的結果
     */
    private  class HttpResultFuncCcche<T> implements Func1<Reply<T>, T> {

        @Override
        public T call(Reply<T> httpResult) {
            return httpResult.getData();
        }
    }

}

RxJava+Retrofit2+Okhttp+RxCache 的搭建就是這麼簡單的五步就完成了,剩下的就是怎麼去使用了 我來舉個栗子 像這樣請求資料肯定是需要寫到 Model 裡面的

/**
 * 獲得類別資料
 */
public class HomeStackFragmentModel {

    public void LoadData(final OnLoadDataListListener listener){
        HttpData.getInstance().getBookTypes(new Observer<List<BookTypeDto>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                listener.onFailure(e);
            }

            @Override
            public void onNext(List<BookTypeDto> bookTypeDtos) {
                listener.onSuccess(bookTypeDtos);
            }
        });
    }
}

想要的資料已經拿到了,故事到這裡結束了,但是新的故事又開始了,吃瓜群眾們你們準備好瓜子了嗎?

RxJava+jsoup+RxCache 實現 HTMl 頁面爬蟲資料的請求以及快取 四部曲

第一步:導包 還是熟悉的套路

compile 'org.jsoup:jsoup:1.9.2'

第二步:其實就是 RxJava+Retrofit2+Okhttp+RxCache 的第三步 新建快取介面 第三步:新建自定義 OnSubscribe 用於解析 Html 獲得自己資料

/**
 * 其實這裡面的玩法還很多
 * 這是 jsop 的中文文件 http://www.open-open.com/jsoup/  再牛逼的資料都能抓取
 * 其實 doc.select(".bookcover h1:eq(1)");  ()裡面的資料完全可以通過介面定義  達到完全控制的效果
 * 我是懶得寫了  但是這個需求還是提一下  很 nice 的  裝逼必備啊
 */
public class BookInfoHtmlOnSubscribe<T> implements Observable.OnSubscribe<T> {
    private String url;

    public BookInfoHtmlOnSubscribe(String url) {
        //獲取到需要解析 html 地址
        this.url = url;
    }

    @Override
    public void call(Subscriber<? super T> subscriber) {
        try {
            //開始瘋狂的資料抓取啦 這個我就不解釋了  大家去看看文件  http://www.open-open.com/jsoup/
            Document doc = Jsoup.connect(url).get();
            Elements bookIntroduction = doc.select(".con");
            Elements bookname = doc.select(".bookcover h1:eq(1)");
            Elements bookImageUrl = doc.select(".bookcover img");
            Elements bookAuthor = doc.select(".bookcover p:eq(2)");
            Elements bookType = doc.select(".bookcover p:eq(3)");
            Elements bookLength = doc.select(".bookcover p:eq(4)");
            Elements bookProgress = doc.select(".bookcover p:eq(5)");
            Elements bookUpdateTime = doc.select(".bookcover p:eq(6)");
            String[] strs=url.split("/");
            String bookDownload="http://www.txt99.cc/home/down/txt/id/"+((strs[strs.length-1]));
            T bookInfoDto= (T) new BookInfoDto(bookImageUrl.attr("src"),bookname.text(),bookAuthor.text(),bookType.text(),bookLength.text(),bookProgress.text(),bookUpdateTime.text(),bookDownload,bookIntroduction.html());
            subscriber.onNext(bookInfoDto);
            subscriber.onCompleted();
        } catch (IOException e) {
            throw new ApiException("ERROR:資料解析錯誤");
        }
    }
}

第四步:新建 HtmlData 類 和上面的非常相似 哎 就不解釋了 就是這麼 666

/**
 * Created by Administrator on 2016/9/14. */
public class HtmlData {
    //這裡是設定一個快取地址 如果地址不存在就新建一個
    private static File cacheDirectory = FileUtil.getcacheDirectory();
    //新增快取提供者
    private static final CacheProviders providers = new RxCache.Builder()
            .persistence(cacheDirectory)
            .using(CacheProviders.class);

    //在訪問 HttpMethods 時建立單例
    private static class SingletonHolder {
        private static final HtmlData INSTANCE = new HtmlData();
    }

    //獲取單例
    public static HtmlData getInstance() {
        return SingletonHolder.INSTANCE;
    }

    //根據型別獲取書籍集合
    public void getStackTypeHtml(BookTypeDto bookType, int pageIndex, Observer<List<BookInfoListDto>> observer) {
        Observable observable = Observable.create(new StackTypeHtmlOnSubscribe<BookInfoListDto>(bookType.getBookTypeUrl().replace("{Page}",pageIndex+"")));
        Observable observableCache=providers.getStackTypeList(observable,new DynamicKey("getStackTypeHtml"+bookType.getBookTypeName()+pageIndex), new EvictDynamicKey(false)).map(new HttpResultFuncCache<List<BookInfoListDto>>());
        setSubscribe(observableCache, observer);
    }
    //根據關鍵字搜尋書籍
    public void getSearchList(String key,Observer<List<BookInfoListDto>> observer){
        try {
            //中文記得轉碼  不然會亂碼  搜尋不出想要的效果
            key = URLEncoder.encode(key, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        Observable observable=Observable.create(new StackTypeHtmlOnSubscribe<BookInfoListDto>(Constant.API_SEARCH.replace("{Key}",key)));
        Observable observableCache=providers.getStackTypeList(observable,new DynamicKey("getSearchList&"+key), new EvictDynamicKey(false)).map(new HttpResultFuncCache<List<BookInfoListDto>>());
        setSubscribe(observableCache, observer);
    }
    //獲得書籍的詳情
    public void getBookInfo(String bookUrl,String bookName, Observer<BookInfoDto> observer){
        Observable observable=Observable.create(new BookInfoHtmlOnSubscribe<BookInfoDto>(bookUrl));
        Observable observableCache=providers.getBookInfo(observable,new DynamicKey(bookName),new EvictDynamicKey(false)).map(new HttpResultFuncCache<BookInfoDto>());
        setSubscribe(observableCache, observer);
    }

    /**
     * 插入觀察者
     *
     * @param observable
     * @param observer
     * @param <T>
     */
    public static <T> void setSubscribe(Observable<T> observable, Observer<T> observer) {
        observable.subscribeOn(Schedulers.io())
                .subscribeOn(Schedulers.newThread())//子執行緒訪問網路
                .observeOn(AndroidSchedulers.mainThread())//回撥到主執行緒
                .subscribe(observer);
    }
    private  class HttpResultFuncCache<T> implements Func1<Reply<T>, T> {

        @Override
        public T call(Reply<T> httpResult) {
            return httpResult.getData();
        }
    }
}

使用方式和 RxJava+Retrofit2+Okhttp+RxCache 一致 我也舉個栗子好了

/**
 * 獲取書籍詳情資料
 */
public class BookInfoModel {
    public void loadData(String bookUrl,String bookName, final OnLoadDataListListener listener){
        HtmlData.getInstance().getBookInfo(bookUrl,bookName, new Observer<BookInfoDto>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                listener.onFailure(e);
            }

            @Override
            public void onNext(BookInfoDto bookInfoDto) {
                listener.onSuccess(bookInfoDto);
            }
        });
    }
}

好了是不是覺得特別簡單 當然我只是帶你們入門 真正想玩轉想拓展 還是要好好的多瞭解瞭解 有吃瓜群眾要問了 

快取資料