Flutter 全栈开发体验——爬虫与服务端

栏目: IOS · Android · 发布时间: 5年前

内容简介:在学习或开发Flutter应用时,很多人会在app中硬编码很多假数据,用以调试界面,实际上我认为是完全没有必要的,Flutter使用Dart语言编程,而Dart语言作为一种全栈语言,其语法可以甩JavaScript几条街,我们是很有必要真正的将这种语言的能力发挥出来的。这里我就讲讲如何使用Dart语言编写爬虫获取数据,如何使用Dart语言编写编写简单服务器后端。首先我们花十分钟来编写一个简易爬虫。

在学习或开发Flutter应用时,很多人会在app中硬编码很多假数据,用以调试界面,实际上我认为是完全没有必要的,Flutter使用Dart语言编程,而Dart语言作为一种全栈语言,其语法可以甩JavaScript几条街,我们是很有必要真正的将这种语言的能力发挥出来的。

这里我就讲讲如何使用Dart语言编写爬虫获取数据,如何使用Dart语言编写编写简单服务器后端。

Dart 爬虫开发

首先我们花十分钟来编写一个简易爬虫。

环境准备

关于Dart 服务端SDK环境搭建,请阅读我的另一篇文章 Dart语言——45分钟快速入门(上)

我们完全手动创建一个 Dart 工程还是略显麻烦,因此我们需要安装一个脚手架,自动生成一个合乎规范的 Dart 工程项目,执行以下命令安装 stagehand

pub global activate stagehand
复制代码

完成安装后直接使用 stagehand 命令: stagehand -h 可能会报找不到错误,这时候我们有两种办法解决

  1. 配置环境变量

    打开 cmd 命令行,输入如下命令

    echo %APPDATA%\Pub\Cache\bin
    复制代码

    这时可以看到,命令行输出了 stagehand 命令所在的路径,只需要将该路径加入到系统的 Path 环境变量即可

  2. 使用 pub 工具调用

    除了配置环境变量,还可以使用 pub global run 去调用,由于我本机配置了各种各样的开发语言和工具,命令实在太多,我已经不太喜欢配置环境变量,这里就先使用该方式演示。执行以下命令可以查看一下帮助

    pub global run stagehand -h
    复制代码

创建工程

新建一个文件夹 spidercd 到该目录下,运行以下命令,会在 spider 下自动生成一个命令行项目

pub global run stagehand console-full
复制代码

使用 vscode 打开该项目目录

编辑配置文件 pubspec.yaml ,避免不必要的下载,删除默认添加的 test 库依赖,配置如下依赖库

dependencies:
  http: ^0.12.0+2
  html: ^0.14.0+2
复制代码

这里 http 库主要用于处理http请求,html库用于处理 html 内容的解析与提取,它们都是Dart官方提供的非标准库,GitHub链接如下

在项目下执行命令,下载依赖

pub get
复制代码

本文主要做Demo演示,不会对爬虫知识进行讲解。这里主要爬取了一个妹子图网站,大家可以根据自己的实际需要选择目标。如果对爬虫不太了解,请查找资料进行学习,也可以阅读本人的CSDN博客了解爬虫,这里默认大家都掌握爬虫技术。

我的个人博客

Flutter 全栈开发体验——爬虫与服务端
编辑项目中 lib/spider.dart

文件

import 'package:http/http.dart' as http;
import 'package:html/parser.dart' show parse;
import 'package:html/dom.dart';
import 'dart:convert';
import 'dart:io';

// 数据实体
class ItemEntity{
  final String title;
  final String imgUrl;

  ItemEntity({this.title,this.imgUrl});

  Map<String, dynamic> toJson(){
     return {
        'title': title,
        'imgUrl': imgUrl,
      };
   }
}

// 构造请求头
var header = {
  'user-agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) '+
  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
};

// 数据的请求
request_data() async{
  var url = "https://www.mzitu.com/";

  var response = await http.get(url,headers: header);
  if (response.statusCode == 200) {
    return response.body;
  } 
  return '<html>error! status:${response.statusCode}</html>';
}

// 数据的解析
html_parse() async{
  var html = await request_data();
  Document document = parse(html);
  // 这里使用css选择器语法提取数据
  List<Element> images = document.querySelectorAll('#pins > li > a > img');
  List<ItemEntity> data = [];
  if(images.isNotEmpty){
    data = List.generate(images.length, (i){
      return ItemEntity(
        title: images[i].attributes['alt'],
        imgUrl: images[i].attributes['data-original']);
    });
  }
  return data;
}

// 数据的储存
void save_data() async{
  var data = await html_parse();

  var json_str = json.encode({'items':data});
  // 将json写入文件中
  await File('data.json').writeAsString(json_str,flush: true);
}
复制代码

编辑项目下的 bin/main.dart 文件

import 'package:spider/spider.dart' as spider;

main(List<String> arguments) {
  spider.save_data();
}
复制代码

从篇幅考虑。本文省略数据库相关操作,用一个 data.json 文件替代。以上代码中 save_data 函数即处理数据的持久化储存工作,对于小型爬虫而言,推荐使用 Sqlite3 作为数据库,大型爬虫推荐 MongoDB 数据库,我个人认为, MongoDB 是对爬虫最亲和的数据库。

运行以上程序,即可生成 data.json 文件

Flutter 全栈开发体验——爬虫与服务端

总结: 就我个人感觉,使用Dart语言写爬虫肯定是没有 Python 顺手高效的,Python在爬虫这块的 工具 过于强大、简洁、高效。

Dart 服务端

使用Dart语言的原生API开发HTTP服务器仍显得过于繁琐,因此我们需要一个HTTP服务器框架,这样我们就只需要关注业务逻辑的处理。如果大家使用过任何一款成熟的HTTP服务器框架,那么对于新框架上手就会易如反掌,因为绝大多数服务器框架的概念都是相同的,主要就是 ORM 、路由映射、模板渲染、中间件等等这些东西。

根据我所知的,目前可用的仍在维护的Dart的HTTP服务器框架主要有四个,依次按照star最多的从上到下来排序:

其中排第一的 aqueduct 是功能、文档、示例最完善的,因此我们就以此框架做演示

安装

pub global activate aqueduct
复制代码

创建项目

执行命令,生成项目 api_server

pub global run aqueduct create api_server
复制代码

最简示例——hello world

其中 bin/main.dart 下的入口文件可以不用修改,主要修改 lib/channel.dart ,删除多余注释,代码如下

import 'package:api_server/controller.dart';

import 'api_server.dart';

class ApiServerChannel extends ApplicationChannel {
  @override
  Future prepare() async {
    logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
  }

  @override
  Controller get entryPoint {
    final router = Router();
    router
      .route("/")
      .linkFunction((request) async {
        return Response.ok('hello world!');
      });
    return router;
  }
}
复制代码

简单说一下,这里有两个实现,其中 prepare() 方法一般用于预处理,例如连接数据库等,我们暂时用不到,不需理会。 entryPoint 方法是我们真正需要关注的方法,它的执行在 prepare() 方法之后,当有请求到来时,就会被回调。我们在该方法中注册路由,这里注册一个根路径,并设置一个响应请求的匿名回调方法。当我们打开浏览器访问 http://localhost:8888 时,它返回一个响应,即向浏览器打印一句 hello world!

cd 到项目根路径下,执行以下命令启动服务

dart bin/main.dart
复制代码

在浏览器访问 http://localhost:8888 ,可以看输出 hello world!

实现后台API服务

Router 除了可以注册回调方法,还可以关联一个 Controller 用于处理来自客户端的请求。

在lib目录下新建 controller.dart 文件,自定义一个 Controller 。它需要继承自框架的 Controller 类,并实现一个 handle 方法。

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:aqueduct/aqueduct.dart';

class ItemsController extends Controller {

  @override
  Future<RequestOrResponse> handle(Request request) async {
    final content = await File('asset/data.json').readAsString();
    return Response.ok(json.decode(content));
  }
}
复制代码

在项目根路径下新建 asset 目录,将我们之前爬取的数据文件 data.json 拷贝进去。然后修改 entryPoint 方法,再注册一个新的 url

@override
  Controller get entryPoint {
    final router = Router();
    router
      .route("/")
      .linkFunction((request) async {
        return Response.ok('hello world!');
      });

	// 注册一个新的url,并关联到我们自定义的Controller上
    router
    .route('/api/all')
    .link(() => ItemsController());

    return router;
  }
复制代码

重新启动服务器

dart bin/main.dart
复制代码

浏览器访问 http://localhost:8888/api/all ,成功获取数据

Flutter 全栈开发体验——爬虫与服务端

创建Flutter项目演示

Flutter环境准备这里就省略了。先创建一个Flutter 工程用于演示

代码结构如下

Flutter 全栈开发体验——爬虫与服务端
这里主要是三个文件 list_dao.dartitem_model.dartmain.dart

首先配置依赖文件 pubspec.yaml ,主要用到了两个库 dioflutter_staggered_grid_view

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  dio: 2.1.4
  flutter_staggered_grid_view: "^0.2.7"
复制代码

下载依赖完成,编辑以下文件 list_dao.dart

import 'package:flutter_demo/model/item_model.dart';
import 'package:dio/dio.dart';

class ListDao {
  //这里配置自己的实际域名或IP地址
  static const Host = 'http://192.168.1.102:8888';
  
  static const header = {
    'User-Agent':
        'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0',
    "Referer": "https://www.mzitu.com"
  };

  static Future<ItemModel> fetch() async {
    try {
      Response response = await Dio().get("$Host/api/all");

      if (response.statusCode == 200) {
        return ItemModel.fromJson(response.data);
      } else {
        throw Exception("StatusCode: ${response.statusCode}");
      }
    } catch (e) {
      print(e);
      return null;
    }
  }
}
复制代码

实体类 item_model.dart

class ItemModel {
  List<Items> items;

  ItemModel({this.items});

  ItemModel.fromJson(Map<String, dynamic> json) {
    if (json['items'] != null) {
      items = new List<Items>();
      json['items'].forEach((v) {
        items.add(new Items.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.items != null) {
      data['items'] = this.items.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class Items {
  String title;
  String imgUrl;

  Items({this.title, this.imgUrl});

  Items.fromJson(Map<String, dynamic> json) {
    title = json['title'];
    imgUrl = json['imgUrl'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['title'] = this.title;
    data['imgUrl'] = this.imgUrl;
    return data;
  }
}
复制代码

最后就是实际的UI代码了

import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'dao/list_dao.dart';
import 'model/item_model.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.pink,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<ItemModel> mFuture;

  @override
  void initState() {
    loadData();
    super.initState();
  }

  loadData() {
    mFuture = ListDao.fetch();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("美女图"),
      ),
      body: FutureBuilder(
          future: mFuture,
          builder: (ctx, snapshot) {
            switch (snapshot.connectionState) {
              case ConnectionState.none:
              case ConnectionState.active:
              case ConnectionState.waiting:
                return Center(child: CircularProgressIndicator());
              case ConnectionState.done:
                if (snapshot.hasError)
                  return Center(child: Text('Error: ${snapshot.error}'));
                return _buildList(snapshot.data);
            }
            return null;
          }),
    );
  }

  // 创建GridView
  Widget _buildList(ItemModel data) {
    return Container(
      color: Color(0xfff5f6f7),
      padding: EdgeInsets.only(top: 12, left: 10, right: 10),
      child: StaggeredGridView.countBuilder(
        primary: false,
        crossAxisCount: 4,
        itemCount: data?.items == null ? 0 : data.items.length,
        itemBuilder: (ctx, i) {
          return Container(
            decoration: BoxDecoration(
                color: Colors.white, borderRadius: BorderRadius.circular(8)),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                ClipRRect(		// 处理圆角图片
                    borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(8),
                        topRight: Radius.circular(8)),
                    child: Image.network(data.items[i].imgUrl,
                        headers: ListDao.header)),
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(
                    data.items[i].title,
                    style: TextStyle(fontSize: 16),
                  ),
                )
              ],
            ),
          );
        },
        staggeredTileBuilder: (int index) => StaggeredTile.fit(2),
        mainAxisSpacing: 10.0,
        crossAxisSpacing: 8.0,
      ),
    );
  }
}
复制代码

确认我们之前写的服务器后端已经启动,然后启动本机模拟器,运行起Flutter App

Flutter 全栈开发体验——爬虫与服务端

总结

我认为使用Dart语言开发服务端,并结合Nginx用于生成环境下,使Flutter开发人员真正承包整个项目的业务逻辑是非常可行的,这条路才是真正的全栈之路!Dart的语法优势是胜过JavaScript的,即使 Java 与之相比也显得冗余臃肿。至于是否好用,就待大家自行体会了。

关于Dart的服务端框架 aqueduct ,大家有兴趣可以查看官方文档深入学习,本文主要省略了数据库方面的处理,实际上数据库是独立的知识内容,与框架的关系不是太大。而且该框架也提供了一个ORM模块,大家直接查看文档学习Aqueduct ORM ,但目前似乎只支持PostgreSQL数据库,如果想要使用其他数据库,安装相应的驱动,链接如下


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript & jQuery

JavaScript & jQuery

David Sawyer McFarland / O Reilly / 2011-10-28 / USD 39.99

You don't need programming experience to add interactive and visual effects to your web pages with JavaScript. This Missing Manual shows you how the jQuery library makes JavaScript programming fun, ea......一起来看看 《JavaScript & jQuery》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具