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的公众号,感兴趣的小伙伴可以一起学习哦。。。
网友评论