实习、毕设、毕业、入职、找房子……在被这么多东西搞的焦头烂额后,终于有时间再来写一点关于毕设项目的东西了。

项目地址:CAWProject

项目的内容还是一个轻小说阅读器,但与之前做过的东西不同的地方在于~做了所有事情:抓取数据、API、客户端,做完的那一刻感觉一本满足(最近发现wenku8又诈尸了,只要输入以前的帐号密码,就能够登录,继续浏览,或许下次可以重新抓取一次试试,wenku8最大的优点就在于它丰富的插图了)。

工程主要分成三大部分:数据、服务端和客户端。

1.数据

(1)数据抓取

全部的数据都从sf小说的移动站点抓取:SF轻小说。得益于移动站点规范、简洁的设计,数据的抓取、格式化和建模都十分方便。数据相关的代码在Paersr文件下。

因为不熟悉爬虫,采用了最low的方式,全部下载到本地后再解析为格式化的JSON文本文件。

用shell写一个循环,然后把所有小说的简介页面,目录页面,章节页面全部下载下来了。

1
2
3
4
for k in $(seq begin end)
do
    wget ....
done

因为数量比较大,大约有4万多本小说,经过一次数据处理后,剩下26392本,目录页面与简介页面个数相同,章节页面更多,原始数据中页面个数接近60万,处理后剩下465834个。

按照简介 -> 目录 -> 章节 -> 封面与插图的顺序,处理了所有的数据。全部的HTML文件使用Ono解析,用XPath定位到数据,提取,存入JSONModel对象中,再保存到文件中。小说的简介数据全部存入MySQL,使用Objective-C连接MySQL,拼接成Insert语句后,瞬间插完。对于封面和插图数据,全部从处理后的JSON文本文件中全部提取到一个文件中,然后使用wget下载。

1
wget -i urls.txt

虽然单线程下载速度较慢,但对于大量的数据来说,稳定比速度更重要,特别是我的macbook pro只有4GB的内存,所有数据都存在移动硬盘上,随时说爆就爆了。

处理后,全部的数据加起来大约13GB左右,接近整个SF轻小说移动站点的大小了。挂机一晚上就下载完了所有的HTML文件,在四核的iMac上,大约花上2分钟左右,就完成了数据格式化,最后从JSON文件里抽出所有图片链接,又挂机了一个晚上,全部下载完成。由于数据太大,GitHub上的项目中只上传了数据库文件,包含user与book两个表。

当一个文件夹下有数十万个文件时,用finder打开时,每次都会卡住,在终端里也不要尝试用tab键补全文件名或用ls列出文件,因为肯定会卡住,最好也关闭掉对文件数较多的文件夹的spotlight索引,不然mds_stores的写入量可以吓死人。

(2)chapterModel

cawmodel

项目里使用了很多的model,但没有一个比chapterModel更麻烦了,大多数的model只时单纯地存储数据,或者包含一些简单的处理逻辑,而chapterModel还包含了图文混排的逻辑(虽然内容只有.h文件),它对应的60多万个章节数据处理起来也是最麻烦的。

chapterModel.h文件的内容如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@interface imageModel : JSONModel
@property (nonatomic,strong) NSString *imageName;
@property (nonatomic,assign) CGFloat width;
@property (nonatomic,assign) CGFloat height;
@end

@protocol imageModel @end
@interface chapterModel : JSONModel
@property (nonatomic,assign) NSInteger cid;
@property (nonatomic,strong) NSString *title;
@property (nonatomic,strong) NSString *previous;
@property (nonatomic,strong) NSString *index;
@property (nonatomic,strong) NSString *next;
@property (nonatomic,strong) NSArray *content;
@property (nonatomic,strong) NSArray *images;
@end

cid为章节ID,title为章节标题;

previous和next对应上一章节和下一章节的ID,当前章节为起始或末位章节时,则内容分别为header与footer;

index对应章节所属目录的ID;

content为包含文本内容的数组;

images为包含图片模型的数据;

SF小说的阅读界面结构如下:

contentFrame

看到这样的结构,应该马上可以联想到数组,通过遍历数组,生成包含文本和插图的NSAttributedString,对于插图,可以预先存储其大小,让客户端优先完成排版,当图片下载完成时,再刷新插图。图片的占位符使用不可见字符\u{FFFC}标识采用chapter-cid-imName的格式。

遍历content数组时,如果NSString对象含有chapter-cid前缀,则在images数组中查找匹配的对象,获取图片大小。但是由于许多图片不规范,需要缓存排版后的图片坐标,暂时还未启用这个加速分页的方法。

(3)小结

不知道哪一种方案更好,基本是这个过程中遇到的最大的问题,如果没有数据的话,App只是一个空壳,所以一开始的时间都投入到模型设计中,考虑App中每个界面可能用到哪些数据,并如何尽可能地利用已有的数据(毕竟我不生产数据,我只是数据的搬运工)。然而从网页上直接抓取的数据,有时候并不是十分规范,例如有些页面,把整本小说的内容,全部塞入简介中……不小心碰到时,就跪了。

2.服务端

(1)Swift、Python and AES

一开始为了方便,就选择使用Perfect框架来开发API,使用Swift编写,但基本上所有的操作都是处理请求、查数据库、返回数据还有加解密,Base64转换,因为还没有其他可以配套使用的工具,没办法尝试做缓存和负载均衡,部署到服务器上也很复杂,同时在我的macbook pro上运行iPhone模拟器和Perfect服务端时,内存十分紧张,所以后期使用Flask-RESTful来重写了接口,部署到服务器上也更加简单。服务端没有太多可以讲的内容,熟悉了接口编写的套路后,基本就在写业务逻辑了,从Swift迁移到Python时,唯一遇到的问题就是加解密的填充方式了。

AES是一种分组加密算法,对于原始数据分组中长度不足的部分,需要进行填充。由于CryptoSwift中默认使用了PKCS7的填充方式,加密等级与密钥长度相关,iOS自带的CommonCrypto也支持多种填充方式,然而Python自带的AES加解密工具中并没有预先进行填充的操作,需要自行实现,网上的一种实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import binascii
import StringIO
class PKCS7Encoder(object):
def __init__(self, k=16):
        self.k = k

    def decode(self, text):
        nl = len(text)
        val = int(binascii.hexlify(text[-1]), 16)
        if val > self.k:
            raise ValueError('Input is not padded or padding is corrupt')

        l = nl - val
        return text[:l]


    def encode(self, text):
        l = len(text)
        output = StringIO.StringIO()
        val = self.k - (l % self.k)
        for _ in xrange(val):
            output.write('%02x' % val)
        return text + binascii.unhexlify(output.getvalue())

分别在加密前填充,解密前去除填充即可。

(2)小结

不论是采用Perfect还是Flask,能够提供稳定的服务才是关键,不过如果是在生产环境下的话,这两个貌似都没办法做到十分靠谱的程度,只是开发起来简单、迅速而够用,特别是这种毕设类型的……但在没有现成生产环境参考和熟悉的技术采纳时,Flask还是最佳的选择。

3.客户端

在连续写了两个Swift应用后,回头过来写Objective-C时,反而会有种不习惯的感觉,总的来说,Swift写起来比Objective-C更加爽快,缺点是混编时自动补全经常抽风,需要处理指针时十分麻烦,还有许多常用的第三方库都没有Swift版本,客户端的一些界面如下。

(1)界面展示

书架

Simulator Screen Shot 2016年7月10日 下午7.16.28

个人中心

Simulator Screen Shot 2016年7月10日 下午7.16.32

发现:目前存在内存泄漏,也许是因为做缩放效果导致的。

Simulator Screen Shot 2016年7月10日 下午7.19.07 Simulator Screen Shot 2016年7月10日 下午7.19.09

文库首页

Simulator Screen Shot 2016年7月10日 下午7.25.56

搜索

Simulator Screen Shot 2016年7月10日 下午7.26.10Simulator Screen Shot 2016年7月10日 下午7.26.11 Simulator Screen Shot 2016年7月10日 下午7.26.14

小说简介

Simulator Screen Shot 2016年7月10日 下午7.27.52

目录

Simulator Screen Shot 2016年7月10日 下午7.28.10

最后是阅读器~又是一大波图,基本上一个阅读器该有的部分,全部都有了,只是bug较多。

图文混排的情况:图片做了圆角处理,尽可能占据屏幕。

Simulator Screen Shot 2016年7月10日 下午7.29.37

菜单栏+字体、主题调整:延续了之前的18种主题,在图层上分别绘制文字和背景色,我最喜欢的还是羊皮纸。

Simulator Screen Shot 2016年7月10日 下午7.30.35Simulator Screen Shot 2016年7月10日 下午7.29.49 Simulator Screen Shot 2016年7月10日 下午7.29.53

目录+书签+文本检索

Simulator Screen Shot 2016年7月10日 下午7.29.59 Simulator Screen Shot 2016年7月10日 下午7.30.03 Simulator Screen Shot 2016年7月10日 下午7.30.30

(2)阅读器

整个客户端工程中,阅读器模块的代码量是最大的,除了需要完成排版相关的操作外,还整合了多个界面,如同上面的截图所展示的,这一小节记录一些有意思的东西。

1)使用pop展示和隐藏view。

pop引擎不止可以用来制作动画,也可以用来展示和隐藏view,比起使用UIView动画,pop在逻辑上更加清晰,阅读器中多个不同的View分别对应着不同的controller来处理交互,但需要集中展示在阅读器中,使用pop让这些视图看起来如同存放在同一个场景下。

2)使用extension来分隔函数。

Swift中没有pragma标记,在controller比较庞大的时候,查找函数变得更加困难,extension可以用来分隔不同的功能,比如将不同的代理方法实现存放在不同的extension中,或者拓展selector,让target-action模式更加方便使用。

3)图文混排的简化。

图文混排的逻辑依旧是:

将文本与图片占位符存入到同一个NSMutableAttributedString中;

对于图片占位符,设置CTRunDelegate,主要就是提供图片尺寸大小,在合适范围内绘制对应的CTRun;

使用CoreText生成CTFrame,遍历CTFrame,获取图片在当前分页中的坐标;

在获得的坐标范围内绘制图片。

对于插图的坐标,基本就是一张图片占居一个CTRun,而这个CTRun又占据了所在的整个CTLine,所以只要获取到CTLine的Y轴坐标,再根据CTRunDelegate中设定的图片高度,就可以直接在该范围内绘制图片了,只有类似聊天消息中表情与文字混排的情况时,才需要获取到精确的CTRun所占据的Rect。

(3)小结

Swift是未来,虽然已经过了两年,但在很多方面,Swift没有Objective-C强大和稳定;

交互也存在于UI中,对于需要动起来的界面,设计的时候,需要想象力;

UICollectionView有着强大的自定义接口,例如阅读器、发现界面、书架和文库首页都是用它实现的,在一些场景下使用它来代替UITableView也不是不可能的;

设计比实现更加考验编程能力。