基于MVP模式開發的帶緩存網絡爬蟲,采用最流行框架搭建,干貨多多

FreeBook

引言

基于MVP模式開發的帶緩存網絡爬蟲,采用最流行框架搭建,干貨多多

網絡爬蟲很多同學都不陌生,但是在Android上玩這個還是比較少的

集中最近流行的框架和思維以一種簡單易理解的方式實現,方便大家學習

至于需求什么的我就不說了,對于準備畢業項目的同學學會這個你會覺得畢業項目非常easy

源碼下載地址 程序還在持續維護中 下一步打算開發在線閱讀 有興趣的可以一起 喜歡的朋友歡迎star

APK下載 免費下載小說 學習的同時 還能拿到不少的干貨

需求

網絡爬蟲能干嘛?

  • 獲取對應網頁某些有價值數據保存到自己服務器自己使用(很多公司的數據部這么玩 不過人家叫采集 哈哈 多么溫馨的字眼)

  • 還有就是上面說的畢業項目 不需要搭建后臺也不需要數據支持用別人的就好了

  • 對于有些壞壞的人來說還可以做些羞羞的軟件LCRapidDevelop里面集成了視頻播放 拿去用 我是雷鋒

首先讓大家看看效果 No picture you say a jb? gif錄制效果不好 將就點看

首頁

書庫

搜索

下載

下載管理

還有就是項目的結構 看看我是怎么玩轉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);
            }
        });
    }
}

好了是不是覺得特別簡單 當然我只是帶你們入門 真正想玩轉想拓展 還是要好好的多了解了解 有吃瓜群眾要問了 這個緩存是以什么形式緩存的啊 不說話 看圖


緩存數據

接下來就是MVP了

最近mvp真的是火的不行啊,了解后確實覺得非常不錯,解決了View和Model的耦合,對于后期的維護以及邏輯的清晰度的來說還是非常不錯的

有吃瓜群眾要說了 為毛一步可以搞定的事情非要分三步

MVP的優點:
  • 模型與視圖完全分離,我們可以修改視圖而不影響模型

  • 可以更高效地使用模型,因為所有的交互都發生在一個地方——Presenter內部

  • 我們可以將一個Presenter用于多個視圖,而不需要改變Presenter的邏輯。這個特性非常的有用,因為視圖的變化總是比模型的變化頻繁。

  • 如果我們把邏輯放在Presenter中,那么我們就可以脫離用戶接口來測試這些邏輯(單元測試)

MVP一種編程模式 沒有必要照搬 按照自己的理解去寫就好 下面我例舉這個APP里的書籍詳情頁面 來幫助大家理解

首頁是Model層:業務邏輯和實體模型 由于我不是很關注實體模型 所以model層我只放業務邏輯 實體統一丟 entity

/**
 * 獲取書籍詳情數據
 */
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);
            }
        });
    }
    public interface OnLoadDataListListener<T> {
    void onSuccess(T data);
    void onFailure(Throwable e);
}
}

然后是我們的view層: View 對應于Activity||fragment,負責View的繪制以及與用戶交互

/**
 * 定義一個接口來控制Activity相關操作
 */
public interface BookInfoView {
    //顯示加載頁
    void showProgress();
    //關閉加載頁
    void hideProgress();
    //數據加載成功
    void newData(BookInfoDto data);
    //顯示加載失敗
    void showLoadFailMsg();
}

最后就是Presenter 負責完成View于Model間的交互 一個正宗的媒婆 不應該說中介 大哥你三環的房子賣嗎?

public class BookInfoPresenter implements OnLoadDataListListener<BookInfoDto>{
    private BookInfoView mView;
    private BookInfoModel mModel;

    public BookInfoPresenter(BookInfoView mView) {
        this.mView = mView;
        mModel=new BookInfoModel();
    }

    public void loadData(String bookUrl,String bookName){
        mModel.loadData(bookUrl,bookName,this);
        mView.showProgress();
    }

    @Override
    public void onSuccess(BookInfoDto data) {
        if(data.getBookName().equals("")){
            mView.showLoadFailMsg();
        }else{
            mView.newData(data);
            mView.hideProgress();
        }
    }

    @Override
    public void onFailure(Throwable e) {
        mView.showLoadFailMsg();
    }
}

然而不管我們套路有多深 最終我們還是得回到Activity

public class BookInfoActivity extends BaseActivity implements BookInfoView {

    @BindView
    ......
    @BindView(R.id.book_info_textview_download)
    TextView bookInfoTextviewDownload;
    private String bookurl, bookname;
    private BookInfoDto bookInfoDto;
    private BookInfoPresenter presenter;

    @Override
    protected void loadViewLayout() {
        setContentView(R.layout.activity_book_info);
    }

    @Override
    protected void findViewById() {
        Intent intent = getIntent();
        bookurl = intent.getStringExtra("bookurl");
        bookname = intent.getStringExtra("bookname");
    }

    public void initview(BookInfoDto data) {
        bookInfoDto = data;
        Glide.with(mContext)
                .load(data.getBookImageUrl())
                .crossFade()
                .placeholder(R.mipmap.image_error)
                .into(bookInfoImageviewBookurl);
        bookInfoTextviewName.setText(data.getBookName());
        bookInfoTextviewAuthor.setText(data.getBookAuthor());
        bookInfoTextviewType.setText(data.getBookType());
        bookInfoTextviewLength.setText(data.getBookLength());
        bookInfoTextviewProgress.setText(data.getBookProgress());
        bookInfoTextviewUpdatetime.setText(data.getBookUpdateTime());
        bookInfoTextviewIntroduction.setText(Html.fromHtml(data.getBookIntroduction()));
    }

    @Override
    protected void setListener() {

    }

    @Override
    protected void processLogic() {
        presenter = new BookInfoPresenter(this);
        presenter.loadData(bookurl, bookname);
    }

    @Override
    protected Context getActivityContext() {
        return this;
    }

    @Override
    public void showProgress() {
        bookInfoProgress.showLoading();
    }

    @Override
    public void hideProgress() {
        bookInfoProgress.showContent();
    }

    @Override
    public void newData(BookInfoDto data) {
        initview(data);
    }

    @Override
    public void showLoadFailMsg() {
        toError();
    }

    public void toError() {
        bookInfoProgress.showError(getResources().getDrawable(R.mipmap.load_error), Constant.ERROR_TITLE, Constant.ERROR_CONTEXT, Constant.ERROR_BUTTON, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bookInfoProgress.showLoading();
                //重試
                presenter.loadData(bookurl, bookname);
            }
        });
    }
}

在APP里出現的加載頁面 無數據頁面 網絡錯誤頁面 下拉刷新 自動加載 加載動畫 viewpage的頭部動畫等等
看似很高級 其實就是很輕量級的框架啦 LCRapidDevelop是我前段時間整合的一個快速開發框架 歡迎大家star

網絡上形形色色的教程非常之多 講的也非常細致 細致到 看著看著我都不想看了 我呢 只是教大家怎么去入門

先把東西玩起來再去細致的了解 會比你先詳細的了解在開發要輕松的多

目前在線閱讀還沒有開發 感興趣的朋友可以一起

聯系郵箱mychinalance@gmail.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容