在學習或開發Flutter應用時,很多人會在app中硬編碼很多假資料,用以除錯介面,實際上我認為是完全沒有必要的,Flutter使用Dart語言程式設計,而Dart語言作為一種全棧語言,其語法可以甩JavaScript幾條街,我們是很有必要真正的將這種語言的能力發揮出來的。
這裡我就講講如何使用Dart語言編寫爬蟲獲取資料,如何使用Dart語言編寫編寫簡單伺服器後端。
Dart 爬蟲開發
首先我們花十分鐘來編寫一個簡易爬蟲。
環境準備
關於Dart 服務端SDK環境搭建,請閱讀我的另一篇文章 Dart語言——45分鐘快速入門(上)
我們完全手動建立一個Dart
工程還是略顯麻煩,因此我們需要安裝一個腳手架,自動生成一個合乎規範的Dart
工程專案,執行以下命令安裝stagehand
pub global activate stagehand
複製程式碼
完成安裝後直接使用stagehand
命令:stagehand -h
可能會報找不到錯誤,這時候我們有兩種辦法解決
-
配置環境變數
開啟
cmd
命令列,輸入如下命令echo %APPDATA%\Pub\Cache\bin 複製程式碼
這時可以看到,命令列輸出了
stagehand
命令所在的路徑,只需要將該路徑加入到系統的Path
環境變數即可 -
使用
pub
工具呼叫除了配置環境變數,還可以使用
pub global run
去呼叫,由於我本機配置了各種各樣的開發語言和工具,命令實在太多,我已經不太喜歡配置環境變數,這裡就先使用該方式演示。執行以下命令可以檢視一下幫助pub global run stagehand -h 複製程式碼
建立工程
新建一個資料夾spider
,cd
到該目錄下,執行以下命令,會在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部落格瞭解爬蟲,這裡預設大家都掌握爬蟲技術。
編輯專案中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
檔案
總結: 就我個人感覺,使用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 工程用於演示
程式碼結構如下
這裡主要是三個檔案list_dao.dart
、item_model.dart
、main.dart
首先配置依賴檔案pubspec.yaml
,主要用到了兩個庫dio
和flutter_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
總結
我認為使用Dart語言開發服務端,並結合Nginx用於生成環境下,使Flutter開發人員真正承包整個專案的業務邏輯是非常可行的,這條路才是真正的全棧之路!Dart的語法優勢是勝過JavaScript的,即使Java與之相比也顯得冗餘臃腫。至於是否好用,就待大家自行體會了。
關於Dart的服務端框架aqueduct
,大家有興趣可以檢視官方文件深入學習,本文主要省略了資料庫方面的處理,實際上資料庫是獨立的知識內容,與框架的關係不是太大。而且該框架也提供了一個ORM模組,大家直接檢視文件學習 Aqueduct ORM ,但目前似乎只支援PostgreSQL資料庫,如果想要使用其他資料庫,安裝相應的驅動,連結如下
- Mongo-dart MongoDB
- SQLJocky MySQL
- Dart Redis Redis