Flutter 小说爬虫示例

作者: 向日花开 | 来源:发表于2020-03-19 14:33 被阅读0次

    Flutter 尽管没有 Python 那样有强大的爬虫框架,但本身自己却是有一套自己解析 HTML5 语言的插件。我们可以用他解析一些网页,也算是简易版的爬虫吧。

    我就用笔趣阁的网站介绍一下这个框架,我们需要在 pubspec.yaml 中添加引用:

    html: ^0.14.0+3
    

    分析网站

    以笔趣阁搜索"元尊"为例。


    从上图中,我们可以看出,很多信息,我们想要爬取的网站域名,关键字,以及网址。

    • 域名
    static const String baseImgUrl = "http://www.xbiqige.com";
    static const String baseUrl = "http://www.xbiqige.com/";
    
    • 网址
     //搜索小说的接口
    static const searchBook = "search.html?searchtype=novelname&searchkey=";
    
    
    • 我们要展示的实体类信息

    假如我们要在 App 中显示网页搜索条目结果,我们可以根据网页上的信息构造我们自己的实体类。

    class BookSearchItem {
      //书名
      final String bookName;
    
      //图书地址
      final String bookUrl;
      //作者
      final String author;
    
      //最新章节url
      final String lastUrl;
    
      //最新章节标题
      final String lastTitle;
    
      //文章类型
      final String type;
    
      //图书封面
      final String bookCover;
    
      BookSearchItem(
          this.author, this.lastUrl, this.lastTitle, this.type, this.bookCover, this.bookName, this.bookUrl);
    
      @override
      String toString() {
        return 'BookSearchItem{bookName: $bookName, bookUrl: $bookUrl, author: $author, lastUrl: $lastUrl, lastTitle: $lastTitle, type: $type, bookCover: $bookCover}';
      }
    }
    
    

    解析网页

    鼠标右键,查看源码,书读百遍其义自见,就一直看,虽然不能背下来,但也能看出一点端倪吧,嘿嘿,说不定你就把它背下来了。。。

    网络请求类

    万丈高楼,始于基地,数据的请求一直都是基层工作。下面构造我们的网络请求类。

    请记得引入 dio 框架。

    
    import 'package:dio/dio.dart';
    
    class DioFactory {
      static DioFactory get instance => _getInstance();
    
      static DioFactory _instance;
    
      Dio _dio;
    
      BaseOptions _baseOptions;
    
      DioFactory._internal(
          {String basUrl = Config.baseUrl,
          Map<String, dynamic> header = Config.headers}) {
        _baseOptions = new BaseOptions(
          baseUrl: basUrl,
          connectTimeout: Config.connectTimeout,
          responseType: ResponseType.json,
          receiveTimeout: Config.receiveTimeout,
          //headers: header
        );
        _dio = new Dio(_baseOptions);
      }
    
      static _getInstance() {
        if (null == _instance) {
          _instance = new DioFactory._internal();
        }
    
        return _instance;
      }
    
    
      /**
       * 新添加,只用于返回字符串,而不是map类型
       *
       */
      Future<String> getString(url, {options, cancelToken, data}) async {
        print("get==>:$url,body:$data");
    
        Response response;
        try {
          response = await _dio.get(url, cancelToken: cancelToken);
        } on DioError catch (e) {
          if (CancelToken.isCancel(e)) {
            print('get请求取消! ' + e.message);
          } else {
            print('get请求发生错误:$e');
          }
        }
        //print(response.data.toString());
        return response == null ? "" : response.data.toString();
      }
    
    }
    
    class Config {
      static const String baseImgUrl = "http://www.xbiqige.com";
      static const String baseUrl = "http://www.xbiqige.com/";
    
      ///链接超时时间
      static const int connectTimeout = 8000;
    
      ///  响应流上前后两次接受到数据的间隔,单位为毫秒。如果两次间隔超过[receiveTimeout],
      ///  [Dio] 将会抛出一个[DioErrorType.RECEIVE_TIMEOUT]的异常.
      ///  注意: 这并不是接收数据的总时限.
      static const int receiveTimeout = 3000;
    
      ///普通格式的header
      static const Map<String, dynamic> headers = {
        "Accept": "application/json",
      };
    
      ///json格式的header
      static const Map<String, dynamic> headersJson = {
        "Accept": "application/json",
        "Content-Type": "application/json; charset=UTF-8",
      };
    }
    
    

    html 插件解析代码

    我们首先应该分析一下页面上的代码,看有没有什么逻辑可循。

     //搜索书籍的接口,为List赋值
      Future<List<BookSearchItem>> fetchSearchBook(String bookName) async {
        var response;
        List<BookSearchItem> books = new List();
        try {
          //获取到整个网页的代码数据
          response = await net.getString("${StringApi.searchBook}$bookName");
          var document = parse(response);//将String 转换为document对象
          var content = document.querySelector(".librarylist");//找到标签中librarylist的节点,类选择器节点的查找前面要加个.
          var lefts = content.querySelectorAll(".pt-ll-l");//找到所有pt-ll-l节点
          var rights = content.querySelectorAll(".pt-ll-r");//找到所有pt-ll-r 节点
          int count = lefts.length > rights.length ? rights.length : lefts.length;//取最短数据,这里是为了保险,防止数组越界
          for (int i = 0; i < count; i++) {
            //在pt-ll-l,pt-ll-r 节点下找到目标数据
            BookSearchItem item = new BookSearchItem(
                rights[i].querySelectorAll(".info>span")[1].text.trim(),//第二个span元素值,获取作者
                rights[i].querySelector(".last>a").attributes["href"].trim(),//href 属性值,最后一章的Url
                rights[i].querySelector(".last>a").text.trim(),//元素值,获取标题
                rights[i].querySelectorAll(".info>span")[2].text.trim(),//第三个span元素值,获取小说分类
                lefts[i].querySelector("div>a>img").attributes['src'].trim(),//获取小说图片
                lefts[i].querySelector("div>a>img").attributes['alt'].trim(),//获取书名
                lefts[i].querySelector("div>a").attributes['href'].trim());//获取书URL
            books.add(item);
            print(item.toString());
          }
          return books;
        } catch (e) {
          print(e);
        }
    
        return books;
      }
    
    

    页面展示

    class DemoBiqugePage extends StatefulWidget {
      @override
      _DemoBiqugePageState createState() => _DemoBiqugePageState();
    }
    
    class _DemoBiqugePageState extends State<DemoBiqugePage> {
      Api _api = new Api();
    
      List<BookSearchItem> _books = new List();
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        _api.fetchSearchBook("元尊").then((data) {
          setState(() {
            _books.clear();
            _books.addAll(data);
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("笔趣阁爬虫示例"),
          ),
          body: _books.length == 0
              ? Center(
                  child: Text("正在加载数据..."),
                )
              : Container(
                  margin: EdgeInsets.all(10),
                  child: ListView.separated(
                      itemBuilder: (BuildContext context, int index) {
                        return item_book_search(context, _books[index]);
                      },
                      separatorBuilder: (BuildContext context, int index) {
                        return Divider(
                          height: 2,
                          color: Theme.of(context).primaryColor,
                        );
                      },
                      itemCount: _books.length),
                ),
        );
      }
    }
    
    Widget item_book_search(BuildContext context, BookSearchItem book) {
      return Container(
          margin: EdgeInsets.only(left: 10, right: 10, top: 10),
          child: InkWell(
            borderRadius: BorderRadius.circular(20),
            onTap: () {},
            child: Container(
              child: Container(
                  height: 140,
                  margin: EdgeInsets.all(10),
                  child: Row(
                    children: <Widget>[
                      Container(
                        height: 120,
                        width: 80,
                        child: CachedNetworkImage(
                            imageUrl: Config.baseImgUrl + book.bookCover),
                      ),
                      Expanded(
                          child: Container(
                        margin: EdgeInsets.only(left: 30),
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: <Widget>[
                            Container(
                              margin: EdgeInsets.only(bottom: 20),
                              child: Text(
                                book.bookName,
                                style: TextStyle(
                                    fontWeight: FontWeight.w500,
                                    color: Colors.black,
                                    fontSize: 18.0),
                                overflow: TextOverflow.ellipsis,
                                maxLines: 1,
                              ),
                            ),
                            Row(
                              children: <Widget>[
                                Container(
                                  margin: EdgeInsets.only(top: 0),
                                  child: Text(
                                    "类型:" + book.type.split(':')[1],
                                    style: TextStyle(
                                        fontWeight: FontWeight.w300,
                                        color: Colors.black,
                                        fontSize: 12.0),
                                    overflow: TextOverflow.ellipsis,
                                    maxLines: 1,
                                  ),
                                ),
                                Container(
                                  margin: EdgeInsets.only(left: 20),
                                  child: Text(
                                    book.author,
                                    style: TextStyle(
                                        fontWeight: FontWeight.w300,
                                        color: Colors.black,
                                        fontSize: 12.0),
                                    overflow: TextOverflow.ellipsis,
                                    maxLines: 2,
                                  ),
                                )
                              ],
                            ),
                            Container(
                              margin: EdgeInsets.only(top: 20),
                              child: Text(
                                book.lastTitle,
                                style: TextStyle(
                                    fontWeight: FontWeight.w400,
                                    color: Colors.black,
                                    fontSize: 16.0),
                                overflow: TextOverflow.ellipsis,
                                maxLines: 2,
                              ),
                            ),
                          ],
                        ),
                      ))
                    ],
                  )),
            ),
          ));
    }
    
    

    效果图

    总结

    以上用到的插件有:

      dio: ^2.1.10
      cached_network_image: ^2.0.0
      html: ^0.14.0+3
    

    以上的代码直接复制,粘贴到自己的 Demo 中即可运行,如果你对比着笔趣阁网站页面的 H5 代码看,效果会更好。Flutter 的解析框架肯定没有 Python 的强大,但是用来用来匹配字符串的最要还是正则表达式,要从茫茫码海中提取出自己想要的数据,还是不容易。且行且珍惜。

    最后

    贴一张自己学习Flutter的公众号,感兴趣的小伙伴可以一起学习哦。。。

    相关文章

      网友评论

        本文标题:Flutter 小说爬虫示例

        本文链接:https://www.haomeiwen.com/subject/zgqlyhtx.html