javascript实战pdf_NearAdmin实战!(项目初始化篇与PDF预处理)

从上一个篇文章的基本介绍后,这一篇开始我面将手把手教你进入Near-Admin的实战篇,在进入实战篇之前,需要你有以下的基本能力:

1、vue、vuex、vue-router的相关知识

2、javascript、typescript的相关知识

3、ant-design-vue的相关知识

4、python、flask的相关知识

本次实战项目要做的是一个图书管理系统,包括一个移动端的C端电子书书架和内容的展示和后台管理系统,这个系统的接口数据都是基于本地mock和websql  Flask开发的(不要问我为什么用Flask?原本想做纯前端的,但是后面翻车了,考虑到这个公众号的读者群体可能写node不太合适,Go里面好用的pdf处理库貌似没有),在项目开始前,我们需要对实战项目提供的功能做一个简单的思维导图。

343510d747e6941d76043b0a2904df62.png

一、项目初始化

目前near-admin项目的初始化比较简单,只需要直接从git上面拉取对应代码即可,接下来运行npm install或cnpm install安装项目依赖:

npm install

接下来我们先介绍下项目主要的目录结构

├── build # 打包的相关内容│   ├── config.js # 打包配置文件│   └── utils.js # 打包工具类├── public│   ├── basic.html # html模板│   └── static│       ├── images│       ├── js│       │   ├── apiConfigDev.js # 测试环境api配置,不同打包环境会选择不同的配置文件│       │   ├── apiConfigProd.js # 生产环境api配置│       │   ├── apiConfigUat.js # uat环境api配置 │       │   └── near.polyfill.js # 自定义polyfill├── script│   ├── singleoperate.js # 单独打包脚本│   ├── tpl│   │   └── lang│   │       └── lang.tpl # 翻译ts模板│   └── translate.js # 翻译脚本├── src│   ├── assets # 非框架资源│   │   ├── css│   │   ├── font│   │   │   ├── iconfont # 阿里字体,默认antd的icon│   │   │   └── webfont # webfont字体│   │   ├── images│   │   │   ├── icon # 对于想自己打包成雪碧图的可以放到这里│   │   ├── js # 自定义js│   │   ├── scss # 自定义scss│   │   │   └── custom│   │   └── ts # 自定义scss│   │       ├── api # 项目api,请根据多页和api的功能进行合理的分级│   │       │   ├── auth│   │       │   │   ├── apiMethod.ts│   │       │   │   └── apiUrl.ts│   │       ├── custom│   │       │   ├── base.ts # 每个页面组件都会minxins的一个组件│   │       │   ├── config.ts # 全局配置文件,可以通过comConfig获取│   │       │   ├── dict.ts # 全局数据字典│   │       │   ├── directives.ts # 自定义指令│   │       │   ├── hotkeyconfig.ts # 快捷键│   │       │   ├── mbase.ts # NManage组件mixins的一个组件│   │       │   └── mroot.ts # App组件mixins的一个组件│   │       └── locale # 不同的语言版本│   │           ├── en_US.ts│   │           ├── ja_JP.ts│   │           ├── ko_KR.ts│   │           ├── locale_BASE.ts # 翻译的基准文件│   │           ├── locale_MAP.ts # 可选语种│   │           ├── zh_CN.ts│   │           └── zh_TW.ts│   ├── core # 框架的核心文件│   │   ├── assets│   │   │   ├── scss│   │   │   │   ├── auth│   │   │   │   ├── manage│   │   │   └── ts│   │   │       ├── base.ts│   │   │       ├── countryflag.ts│   │   │       ├── eventbus.ts│   │   │       ├── formtype.ts│   │   │       ├── lang.ts│   │   │       ├── mbase.ts│   │   │       ├── plugin.ts│   │   │       ├── rewrite│   │   │       │   ├── index.ts│   │   │       │   └── keep-alive.ts│   │   │       ├── root.ts│   │   │       ├── type.ts│   │   │       └── utils.ts│   │   ├── component│   │   │   ├── NAside.vue│   │   │   ├── NBreadCrumb.vue│   │   │   ├── NHeader.vue│   │   │   ├── NKeepAlive.vue│   │   │   ├── NLangPicker.vue│   │   │   ├── NManage.vue│   │   │   ├── NNoFound.vue│   │   │   ├── NNoRight.vue│   │   │   ├── NTag.vue│   │   │   ├── NTagContentMenu.vue│   │   │   └── NWebView.vue│   │   └── store│   │       └── core.ts│   ├── global.d.ts # 类型定义│   ├── lang.d.ts # 翻译脚本自动生产的语言类型定义│   ├── mock # 本地mock│   │   ├── data│   │   │   └── common.ts│   │   └── index.ts│   ├── pages # 多页内容,名字需与build/config中的moduleNames一样│   │   ├── auth│   │       ├── App.vue # 实例│   │       ├── auth.ts # 入口文件│   │       ├── component│   │       │   ├── common│   │       │   │   └── CopyRight.vue│   │       │   ├── login│   │       │   │   ├── LoginBottom.vue│   │       │   │   ├── LoginByAccount.vue│   │       │   │   ├── LoginByPhone.vue│   │       │   │   └── LoginForm.vue│   │       │   └── register│   │       ├── page.config.json # 页面配置(用于引入js和css)│   │       ├── router # 路由│   │       │   └── auth.ts│   │       └── view # view层的组件│   │           └── Login.vue│   └── store # 全局的vuex状态│       ├── index.ts│       └── modules├── tests # 测试用例│   ├── e2e│   └── unit└── vue.config.js # vue-cli配置

那么接下来就详细列下near-admin在使用过程中需要遵循的一些开发规范:

1、不要动core里面的内容

core中文件是框架层面的代码,因此不要改动,主要是以后版本更新方面,只需要替换简单的整体替换core即可。

2、创建单个单页应用的目录结构

├── auth       ├── App.vue # 实例       ├── auth.ts # 入口文件       ├── component       ├── page.config.json # 页面配置(用于引入js和css)       ├── router # 路由       │   └── auth.ts       └── view # view层的组件

这是一个单页应用的目录结构,包含了实例、入口文件、组件文件夹、页面配置json、路由以及view层的组件内容,需要注意的是入口文件的命名需要跟单页应用的命名一致,即auth.ts。

这块大家不用担心,后面会做专门的脚本帮大家方便的创建一个单页应用。

3、api需要维护在api的文件夹中,并按照实际进行合理的拆分,考虑到地址和方法的修改,这里建议分成apiUrl和apiMethod两个文件

具体可以参考框架自带的登录页和管理页的api写法。

4、对于要合成雪碧图的icon,请丢在assets/images/icon目录中,框架会自动帮你打包生成css和图片

5、view中的组件都需要mixin CoreBase和Base两个组件,一些页面通用的方法可以放到其中维护,在没有模板的情况下大家可以在webstorm中配置对应的模板,这样创建新的页面的时候就不会担心漏掉了

基本上现阶段需要注意的就是这部分的内容,事不宜迟,让我们马上进入实(fan)战(che)环(xian)节(chang)

二、C端电子书页面

首先我们先来实现电子书部分,这里其实我在实际开发过程中是翻了一次小车,因为完全需求给过来的一本电子书呢,有足足180M,所以在前端使用pdfjs做解析是比较不靠谱的行为,只有pdfjs比较小的情况下比较合适,或者想办法把pdf进行切分,然后进行懒加载。由于我比较懒,所以这种方式不太合适,但是这里还是会给大家介绍下怎么样简单的通过pdfjs实现电子书功能。

1、创建ebook单页应用

首先,我们按照前面约定的目录结构在pages下面创建ebook目录,在view中创建一个Ebook.vue的sfc文件(sfc也要遵循约定中的要求,mixin CoreBase和Base)。创建后的目录结构如下所示:

80b1a3a3bc5374732f555dbddc872df4.png

目录结构

路由方面我们配置根目录为Ebook.vue这个sfc

// Ebook.vueimport Vue from 'vue'import VueRouter, { RouteConfig, RouterOptions } from 'vue-router'import dict from '@custom/dict'import utils from '@corets/utils'const Ebook = () => import('../view/Ebook.vue')Vue.use(VueRouter)const routesConfig: RouteConfig[] = [    {        path: '/',        name: 'Ebook',        component: Ebook    }]const routerOpt: RouterOptions = {    mode: 'history',    base: `/${dict.commonObj.ebookPath}`,    routes: routesConfig}const routeObj = new VueRouter(routerOpt)routeObj.beforeEach((to, from, next) => {    const title = to.meta.title    if (to.meta.title) {        utils.setPageTitle(title)    } else {        utils.setPageTitle('')    }    next()})export default routeObj

然后我们使用npm run serve在本地运行项目,访问http://localhost:port/ebook,就可以看到我们路由已经进入并渲染了Ebook.vue。

接下来,因为我们要先尝试pdfjs的方案,所以我们从pdfjs的官网把文件下载下来,并放到/public/static/js/pdf中,可能会有朋友问我为什么不用npm?第一,npm会被打包到vendor,而pdfjs这个库本身体积不小(含worker整体需要渠道1.6M),第二,我这不是要给你们展示下怎么使用page.config.json嘛。

在ebook下面的page.config.json的external中的js,引入pdfjs和pdfjs.worker,注意二者引入的先后顺序,先引入pdf后引入worker。

{  "title": "Near-Admin",  // 页面的title  "externals": {    // 按照功能区分引入js    // polyfill    "polyfill": {      "type": "js",      "url": [        "/static/js/near.polyfill.js"      ]    },    // pdf    "pdf": {      "type": "js",      "url": [        "/static/js/pdf/pdf.js",        "/static/js/pdf/pdf.worker.js"      ]    },    // api config    "apiconfig": {      "type": "js",      "url": []    }  }}

需要注意的是,这page.config.json是在打包的时候固定的一些内容,因此不会涉及到热更新,因此修改完需要重新运行npm run serve来保证修改的内容生效。

运行成功后,我们可以在网页中看到,文件已经被成功打包到html中。

db7d70c24b6501a15318f9a1b6833093.png

引入js

接下来就开始我们业务方面代码的编写,首先介绍戏pdfjs,pdfjs是一个非常便捷的前端pdf reader,他可以让用户在前端展示pdf文件的内容。官方库的地址是:


http://mozilla.github.io/pdf.js

以首先我们把我们要展示的书籍放到public/static中,这样我们可以本地访问这个文件。这里主要用的几个api分别为

1、getDocument  // 用户获取pdf的文档对象2、getPage // 用于获取pdf文档对象下的每一页的对象3、render // 用于渲染canvas4、getTextContent // 用于获取pdf中d的文本d对象

这块我们就不赘述了,有兴趣的朋友可以自行看下开发文档。接下来我们说下为什么这种方案不行,第一,整个pdf的大小180M,放在现在4G的速度下面,http请求也要长达几十秒到几分钟不等,再算上pdfjs处理文档的时间,会给用户非常不好的体验;第二,即使对pdf进行切割,并使用按需加载的模式,在翻页过程中再通过swiper的一些钩子去触发page的文本信息获取,整体会给用户造成卡顿的感觉;第三,由于pdfjs是一个纯前端的reader,那么就意味着,每次刷新用户都需要重新等待内容处理,这种也是白白浪费用户硬件资源的行为。所以考虑了之后这块还是需要交给后端去处理并将内容存起来,并在后端提供给用户手动排版的功能,因为pdf读出来的文本分段,都是按照实际排版内容来的。

那么是否我们就不能用这种方法了呢?也不是,这块完全要看场景,比如说,一些纯文本的书,页数不那么大的就完全可行,而像这个实战案例中用的图书就不行,因为他属于彩印书,图片表多,书的体积比较大。

总之最后的效果大概是这样子的。

974f4ccba38b6a74da8686ea19ec337d.gif

电子书效果

可以看到,由于用了按需处理的模式,所以在切换页面的时候,会触发page信息的获取,再加上整本书有180M,140多页,因此整个swiper页非常的huge,影响了整体性能。

那么接下来我就走第二种方法,使用后端处理文本返回给前端的模式,由于之前后台经常收到朋友们问我为啥不继续使用flask的问题?,那么本次我就使用python写这次实战项目,首先我们先看下用python处理pdf转txt的效果如何。

这里选用的库是pdfminer,首先我们先测试下转换的效果如何:

pip3 install pdfminerpdf2txt.py -o book.txt book.pdf

看到输出的内容基本上是ok的。那接下来我们写一个处理pdf的工具类,对内容进行预排版。新建一个flask的工程。

mkdir flask-book# 创建虚拟环境python3 -m venv venv# 激活虚拟环境. venv/bin/activate# 安装Flaskpip3 install Flask# 以上均为mac环境下的,windows环境自行查看官方文档

项目根目录创建__init__.py文件,初始化一个最小web应用

# __init__.pyfrom flask import Flaskapp = Flask(__name__)@app.route('/')def hello_world():    return 'Hello, World!'if __name__ == '__main__':    app.run(debug=True)
# 运行python __init__.py

运行后可以在http://localhost:5000看到Hello,World服务正常运行,接下来我们创建一个工具类用于初步处理pdf的文本(注意,由于pdf处理是一个比较耗时的操作,所以这里的工具类需要考虑把pdf处理作为一个异步任务,这块我们放到下一篇文章再处理)

创建一个upload目录用于存放后台管理系统上传的pdf内容,我们把180M的pdf放到这个文件夹中。接下来我们创建一个用户pdf处理的类,在utils里面创建一个一个pdf.py,第一步,我们先完成pdf文档的读取以及每个页面的读取。

# pdf.pyfrom io import StringIOfrom pdfminer.converter import TextConverterfrom pdfminer.layout import LAParamsfrom pdfminer.pdfdocument import PDFDocumentfrom pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreterfrom pdfminer.pdfpage import PDFPagefrom pdfminer.pdfparser import PDFParserfrom pdfminer.converter import PDFPageAggregatorfrom pdfminer.layout import LAParams, LTTextBox, LTTextLine, LTFigure, LTImageclass PdfReader:    def getDocumentByPath (self, filePath):        fp = open(filePath, 'rb')        parser = PDFParser(fp)        document = PDFDocument(parser)        return document    def getEnumDocumentPage (self, pdfDoc):        rsrcmgr = PDFResourceManager()        laparams = LAParams()        outputString = StringIO()        device = PDFPageAggregator(rsrcmgr, laparams=laparams)        interpreter = PDFPageInterpreter(rsrcmgr, device)        for i, page in enumerate(PDFPage.create_pages(pdfDoc)):            interpreter.process_page(page)            layouts = device.get_result()            print('当前页:{i}'.format(i=i+1))            print(layouts)
# __init__.pyfrom flask import Flaskfrom utils.pdf import PdfReaderimport osapp = Flask(__name__)pdfReader = PdfReader()pdfDocPath = '{curDir}/upload/{pdfName}'pdfDocPath = pdfDocPath.format(curDir=os.path.curdir, pdfName='book.pdf')pdfDocObj = pdfReader.getDocumentByPath(pdfDocPath)pdfReader.getEnumDocumentPage(pdfDocObj)if __name__ == '__main__':    app.run(debug=True)

运行后我们可以看到控制台输出了每个页面的对象由各种类型的图层组成,具体参考官方的结构图。

59325a9f0026fd902513629ae147029d.png

控制台信息

ccf9f32b97d81effbaa5982839925cb1.png

LTPage对象结构

接下来我们需要对LTPage中的内容进行处理,提取出我们要的图片和文本,并进行简单的机器排版。增加一个dealWithPages的方法并引入一些工具函数对pdf页面进行处理。其核心原理就是将通过递归的方式把整颗LTPage树进行遍历,提取出文本和图片组成每一页的内容,保存成为一个单独的txt(下一篇将会将他存到数据库中,这里仅测试效果)。需要提到的是,这里保存出来的图片可能存在问题,就是导出来的图片被进行了反相操作,变得比较诡异,主要是因为有些pdf文档采用的是印刷常用的CMYK模式,所以会存在这个问题,因此我们需要对图片进行一个处理后在保存(PS:不知道为啥这本书反相的图片非常恐怖....不要问我是怎么知道的)。

# pdf.js# ...def dealWithPages (self, layouts):    curPageCtx = []    for pageLayoutObj in layouts:        if isinstance(pageLayoutObj, LTTextBox) or isinstance(pageLayoutObj, LTTextLine):            curPageCtx.append(pageLayoutObj.get_text())        elif isinstance(pageLayoutObj, LTImage):            savedFile = self.savePdfImage(pageLayoutObj)            if savedFile:                curPageCtx.append('')        elif isinstance(pageLayoutObj, LTFigure):            curPageCtx.append(self.dealWithPages(pageLayoutObj))    return '\n'.join(curPageCtx)def savePdfImage (self, imgObj):    result = None    if imgObj.stream:        imgStream = imgObj.stream.get_rawdata()        fileExt = self.getPdfImageType(imgStream[0:4])        if fileExt:            fileName = ''.join([self.md5WithTime(), fileExt])            isCMYK = (LITERAL_DEVICE_CMYK in imgObj.colorspace)            if self.saveFile(imgStream, os.path.join(pdfImgPath, fileName), flags='wb', cmyk=isCMYK):                result = fileName    return resultdef getPdfImageType (self, imgStream):    fileType = None    imgHex = b2a_hex(imgStream).decode()    if imgHex.startswith('ffd8'):        fileType = '.jpeg'    elif imgHex == '89504e47':        fileType = ',png'    elif imgHex == '47494638':        fileType = '.gif'    elif imgHex.startswith('424d'):        fileType = '.bmp'    return fileTypedef saveFile (self, imgData, imgPath, flags, cmyk):    result = False    try:        fileObj = open(imgPath, flags)        if cmyk:            ifp = BytesIO(imgData)            i = Image.open(ifp)            i = ImageChops.invert(i)            i = i.convert('RGB')            i.save(fileObj, 'JPEG')        else:            fileObj.write(imgData)        fileObj.close()        result = True    except Exception as e:        print(e)        pass    return resultdef md5WithTime (self):    m = hashlib.md5()    imgPath = str(random.random()) + str(round(time.time() * 1000))    imgPath = imgPath.encode(encoding='utf-8')    m.update(imgPath)    return m.hexdigest()def savePageTxt (self, txtPath, txtData, flags):    result = False    try:        fileObj = open(txtPath, flags)        fileObj.write(txtData)        fileObj.close()        result = True    except Exception as e:        print(e)        pass    return result

c7c66b0719dcca656e20abf8d1a03c8c.png

最终输出的文本内容

可以看到,输出的文本内容本身排版各方面还是有点问题,因此我们需要按照一些普适的规则对文本数据进行清洗,这里主要按照以下几个规则:

1、首先去掉字与字之间的间隔(去掉空格)

2、去掉行与行之间的换行(把两个换行替换成一个换行)

经过这两步的操作,我们发现txt整体的布局好看了很多,少了很多多余的字符,但是我们可以看到,很多完整的段落被拆分开,这主要是因为pdf文件本身里面就对段落进行了换行拆分,这对我们到时实现电子书切换字体大小等操作是非常不便的,虽然说我们到时后台会最人工排版的功能,但是这类型的修改比较繁琐,因此,机器排版上必须要有一个算法能够满足段落换行的合并。

def dealWithTxt (self, bookPath):        fileList = glob.glob(bookPath + '/*.txt')        for i in fileList:            temStrList = []            temStr = ''            lastLineLength = 0            f = open(i, 'r', encoding='utf-8')            lines = f.readlines()            for j in range(0, len(lines)):                item = lines[j]                if j == 0:                    temStr = item                else:                    if abs(len(item) - lastLineLength) < 6:                        temStr += item                    else:                        temStrList.append(temStr.replace('\n', ''))                        temStr = item                lastLineLength = len(item)                if j == len(lines) - 1:                    temStrList.append(temStr.replace('\n', ''))            f.close()            fileObj = open(i, 'w')            fileObj.write('\n'.join(temStrList))            fileObj.close()

我们再整体的看下导出文本的效果和图片的效果,一些比较接近的已经进行了合并。

8d00ec55589da4e3ea56d2f7972a800a.png

合并效果

这样子,我们前期的工作基本完成,下一篇,我们的内容是:

1、完善整个flask的架构,把导出的文本存进数据库

2、增加一个后台管理系统的上传pdf页面以及增加用户手动排版的功能

3、初步实现图书查询接口提供给到前端电子书

实战项目已经放到github上面,因为电子书太大所以就没传,大家可以自己下载一本pdf进行测试。

前端工程是:https://github.com/yiptsangkin/near-book-admin
后台工程是:https://github.com/yiptsangkin/near-book-admin-flask

这篇文章可能有些东西讲的不太清楚,因为最近时间比较有限,大家可以尝试上手跟着做一下,有疑问的话可以在加微信群或者在后台留言。

c1047b99d1de4be74dc103753e48485a.png

参考文献

[1]如何处理pdf中的文本图片 http://denis.papathanasiou.org/archive/2010.08.04.post.pdf

[2]CMYK模式的pdf如何处理 https://pydoc.net/pdfminer/20140328/pdfminer.image/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值