小说爬虫真的很简单,但要能优雅地使用却很麻烦。下面让我来诉说一下这几天的肝路历程。整个流程很完整,但不会很深入,主要是讲思路,给想要写类似功能的同学踩点坑。

先奉上项目Github,里面有实现代码以及jar和apk两种软件,几天课余时间肝出来的,有些想的不周到的地方还请见谅。

明确产品需求

  • 最基本的要求,能够搜索小说,然后点击小说进行阅读或者下载。
  • 同时,我们想要能够搜索到各个站点的小说,并且速度不能太慢。
  • Android客户端实现书籍收藏(追更),并自动检查是否有章节更新
  • 下载格式,我们不仅想要生成txt格式的书籍,还想要epub这种带目录图片的格式,最好还要能够支持mobi,然后直接导入kindle。
  • 下载速度不能像市面上普通的小说软件一章一章的下,否则速度太慢会影响体验,最好要能达到宽带的最大速度。
  • 该程序能有较好的可移植性,因为我们想要同时制作PC端和Android端的软件。

那么,开干!


总流程

process

关于爬虫框架

在爬网页内容这部分,并没有用什么黑科技,只是普通的正则匹配爬虫。我用了自己的工具类,后来有些网页有些太复杂也引入了Jsoup负责解析html。这里默认大家都明白怎么解析html内容。

关于编码方式的坑

大多数人一想到编码方式,肯定是首选uft-8了。但是在小说网站里,我们需要首选gbk,因为很多小说的某些字符是没有包含在utf-8里的,会变成??常见的小说网站编码方式都是默认gbk。

因此在框架中需要保留一个方法设置编码方式,并且默认应为gbk。

如何实现搜索功能

这里我想到了两种方案。第一种是利用百度这些搜索引擎搜索小说,再对搜索引擎得到的结果进行解析;还有一种就是直接利用小说网站的搜索功能实现搜索。很明显,搜索引擎的优势是能够得到很多小说网站的搜索结果,但是这些网站是随机的,在没有足够多的解析前拿到结果也无能为力。第二种方式是使用小说网站内部的搜索功能,缺点是书籍不够全面,但是只要搜索到就必定能够解析,比起漫无目的的搜索能提高更多效率。而书籍不全的缺点可以通过解析更多网站来弥补。于是最终确定使用小说站内的搜索功能。

基本上每个小说网站都会提供搜索功能的。抓一下包,会发现,无非就是post请求了某个网址,请求内容就是书籍名字。于是可以通过网络请求拿到搜索结果,如下图的某趣阁。

search_result

搜索结果有很多附带属性,为了能够提供更好的使用体验,记得把小说名字,小说的目标url,最新章节名,作者,字数,最近更新时间都解析下来,以便后面使用。有的网站有图片链接,也可以解析下来,我个人觉得用处不是很大就没有解析。

以上,就实现了单个站点的搜索解析,按照同样的方式,我解析了十多个站点的搜索功能。在用户点击搜索时,使用并发的方式同时请求,把所有结果保存到集合里并展示给用户。并发的好处是只要网络状况良好,就能用搜索一个网站的时间获得十几个网站的结果,这在网站多了之后是必须的。至于如何使用并发,用最简单的线程池+CountdownLatch就好。

这里有个小技巧,很多小说站内搜索直接使用了百度站内,他们的解析方式是一样的,可以封装起来。只要是这样的网站,搜索代码一行就能解决。

如何解析小说

通过搜索功能我们拿到了小说目录的url,对这个url进行请求并解析出所有章节名和url即可得到这本小说的所有目录。这里单线程就行,多线程并不能提高多少效率。还是放一张某趣阁的截图吧。

catalog

拿到url以后,就可以得到章节内容的html了,同样解析出来,就拿到章节内容了。如下图,解析content所在的div即可。

content

关于解析小说的第二种方式

昨天看了一下owllook的实现方案,使用了匹配class或者id的方法,实现自动解析。说明白点就是传入目录所在的div的class或者id,通过某个算法自动解析章节名字和url,这样做的好处是解析网站特别简单。如上图的某趣阁是没问题的,因为这个网站还是比较规范的,div里就是目录或者小说内容。

但是一旦遇到比较流氓的,就不行了。例如下图,这个网站的目录div分成了三列,每一列是一个单独的div。如果按照自然顺序去解析,解析出来的内容自然就乱序了,而如果要靠第x章的顺序去重新排序显然不太现实。

catalog1

因此,我并没有采用自动解析的逻辑,而是每个网站全部需要自己实现解析。这样虽然麻烦了点,但是能够保证所有网站都能正常解析,并且还能根据不同的网站删除广告之类的内容。同时,可以将比较常用的解析方法封装起来使用,也不会太繁琐。例如使用Jsoup的textNodes的功能,能够直接提取所有文字到一个集合里。

如何实现章节并发解析,失败无限重试

这个工具类还真是构思了很久,也写了很多个版本。

粗略一想,直接用线程池,再配合CountdownLatch不就行了?这样确实可以实现并发解析,但是如果某一章小说网络请求出错怎么办?错误的章节直接就跳过了,导致小说不完整。所以需要一个机制来实现失败重试。

我最开始的方案比较粗略,直接把错误的章节添加到一个并发集合里,等待所有章节都解析完毕后再重新解析所有错误的章节。这样在网络情况稳定下,基本没什么问题,但万一重试的章节又失败了呢?所以我想要一个能够失败无限重试直到成功的工具。

于是实现了一个下载队列,这个队列一边把任务添加到线程池,一边重新入队失败的任务,这样就能无限重试了。使用了锁实现这个队列,并且实现了进度监听。wait真香O(∩_∩)O。具体可以参考github上的engine->BookFu*ker类。

关于并发

  • 网络请求的并发线程一定不要设置高了,不然会返回504错误。使用okhttp默认配置即可。
  • 网页解析的线程池可以设置任意数量,因为okhttp维护了一个队列,只要使用同一个client,单个ip最多并发数量为5。增加解析的线程数量可以提高解析速度,是因为能够让html下载下来后更快被解析。玄学调参:电脑300,手机150。
  • 小说服务器是有限制的,比如某趣阁测试量大约2w章节,就被封ip了,只有等到第二天才能重新测试,所以千万别用校花的贴身高手什么的去测试=_=数量和速度应该都有影响吧,大家自行把握。我测试时2m/s的下载速度,断断续续大概30分钟不到。
  • 网络请求的超时设置。由于已经实现了失败重试机制,所以可以适当减少超时,以免某一章节一直卡着浪费资源。如果没有失败重试,必须设置长一点。

小说内容怎么保存

最开始我直接将每一章小说的内容储存为一个String类型,然后返回统一处理。这样做如果只是针对txt格式是没问题的,因为拿到所有小说章节后只需要简单按顺序合并起来就ok。

但是类似EPUB格式的小说,每一章其实是一个html文本,需要在每一行放在</p/>标签里,标题放在</h1/>里,还要统一配置css等。

于是,我们解析的小说内容最好按照每一行保存在一个集合里,这样能方便拓展小说保存格式。

关于小说保存格式

epub格式保存需要将所有章节转换成html,并生成章节目录的html,和epub格式的配置文件,最后压缩打包,生成.epub后缀的文件即可。

虽然知道原理,但是真的不想自己写,于是直接抄袭别人的代码。

在我github代码里的EpubWriter忘记从哪儿抄的了,这个工具类有点坑,不能在安卓平台上使用,Android版本我依赖了epublib,亲测可以使用。

至于mobi格式,目前我还没有找到直接生成的方法,最佳方法应该是下载kindlegen(外网)软件,用epub转mobi,效果很好。但是这个软件在不同操作系统需要下载不同版本,所以没法集成到软件中。

Android开发心得

java代码写的飞起,各种接口各种抽象,复制到安卓工程里才发现,有些接口使用还是不太方便,只能一遍一遍地重构了,现在只是把最基本的功能做出来了,以后慢慢说吧。

庞大集合的传递

很多小说一个网页就几百k的大小,解析后的目录集合能有几千的数量,千万别把这个集合传进intent里跳转activity。如果你的手机性能不错可能不会有bug,其他人手机可能会在跳转时闪退,而且没有报错!!!因此,对于目录这样的超大集合,可以用单例保存。

解析真的太慢了

电脑上解析一个8k+章节的小说只要1s不到,但即便是骁龙835处理器手机也要3~4秒。因此最好将解析结果保存起来,防止不小心退出后需要重新解析。

实现追更

其实实现非常的简单,只需要在每次用户打开App的时候,将追更列表里的书籍并发解析一遍目录,对比数据库里的最新目录即可。

自动更新以及bugly

这种才开始写的app真的很需要bugly来查看异常状况,并且实现自动更新功能,便于修复bug后的推送。后续可能使用热修复方案,毕竟小说网站解析可能会频繁出错。