利用Tornado搭建文檔預覽系統

  在平時的工做或學習中,咱們常常會接觸不一樣格式的文檔類型,好比txt,log,Offices文檔,編程代碼腳本,圖片,視頻等。本文將會介紹筆者的一個樸素想法,即把不一樣格式的文檔都放在同一個平臺中進行預覽,這樣既方便查看常見文檔,又能提高工做和學習效率。
  本項目的工程結構以下:
項目結構
本項目如今已支持8種文檔格式的格式,分別爲:javascript

  • text/html: 如html文件等;
  • text/plain: 如txt/log文件等;
  • text/csv: csv文件;
  • application/json: json文件;
  • application/pdf: pdf文件;
  • text/x-python: Python腳本文件;
  • image/*: 各類圖片文件,好比jpg, png等;
  • markdown文件

準備工做

  首先,咱們須要下載前端的PDF預覽JS框架PDF.js,它是一個網頁端的PDF文件解析和預覽框架,下載網址爲:http://mozilla.github.io/pdf.js/
  接着,本項目還用到了showdown.js,該JS框架用於渲染Markdown文檔。
  用Python作後端,tornado爲web框架,筆者使用的版本爲5.1.1css

項目代碼

  咱們下載PDF.js項目代碼,並在/pdfjs/web目錄下新建files文件夾,用於存放上傳的文件。爲了可以用PDF.js實現PDF文件預覽,須要切換至pdfjs文件夾,運行搭建文件服務器命令:html

python -m http.server 8081

或者:前端

python -m SimpleHTTPServer 8081

  接着介紹HTML文件,index.html是首頁代碼,主要實現文件上傳功能,代碼以下:java

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上傳</title>
</head>
<body>

    <div align="center">
        <br><br>
        <h1>文件上傳</h1>
    <form action='file' enctype="multipart/form-data" method='post'>
        <div class="am-form-group am-form-file">
            <input id="doc-form-file" type="file" name="file" multiple>
        </div>
        <div id="file-list"></div>
        <p>
            <button type="submit" class="am-btn am-btn-default">提交</button>
        </p>
    </form>
    </div>

</body>
</html>

頁面以下(有點兒過於簡單,還好本項目是注重文檔預覽功能):
文件上傳頁面
  markdown.html主要用於展現Markdown文件中的內容,代碼以下:python

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Markdown文件展現</title>
    <script src="https://cdn.bootcss.com/showdown/1.9.0/showdown.min.js"></script>
    <script>
        function convert(){
            var converter = new showdown.Converter();
            var text = "{{ md_content }}";
            var html = converter.makeHtml(text.replace(/newline/g, "\n"));
            document.getElementById("result").innerHTML = html;
        }
    </script>
</head>

<body onload="convert()">
<div id="result" ></div>

</body>
</html>

注意,咱們在head部分引用了showdown.js的CDN地址,這樣就不用下載該項目文件了。
  最後是後端部分,採用Python的Tornado模塊實現。tornado_file_receiver.py主要用於文檔的上傳和保存,並展現文檔內容,完整代碼以下:git

# -*- coding: utf-8 -*-
import os
import logging
import traceback
import tornado.ioloop
import tornado.web
from tornado import options

from parse_file import *


# 文檔上傳與解析
class UploadFileHandler(tornado.web.RequestHandler):
    # get函數
    def get(self):
        self.render('upload.html')

    def post(self):
        # 文件的存放路徑
        upload_path = os.path.join(os.path.dirname(__file__), 'pdfjs/web/files')
        # 提取表單中‘name’爲‘file’的文件元數據
        # 暫時只支持單文檔的上傳
        file_meta = self.request.files['file'][0]
        filename = file_meta['filename']
        # 保存文件
        with open(os.path.join(upload_path, filename), 'wb') as up:
            up.write(file_meta['body'])

        text = file_meta["body"]

        # 解析文件的內容
        mtype = file_meta["content_type"]
        logging.info('POST "%s" "%s" %d bytes', filename, mtype, len(text))
        if mtype in ["text/x-python", "text/x-python-script"]:
            self.write(parse_python(str(text, encoding="utf-8")))
        elif mtype in ["text/plain", "text/csv"]:
            self.write(parse_text_plain(str(text, encoding="utf-8")))
        elif mtype == "text/html":
            self.write(str(text, encoding="utf-8"))
        elif mtype.startswith("image"):
            self.write(parse_image(mtype, text))
        elif mtype == "application/json":
            self.write(parse_application_json(str(text, encoding="utf-8")))
        elif mtype == "application/pdf":
            self.redirect("http://127.0.0.1:8081/web/viewer.html?file=files/%s" % filename)
        elif mtype == "application/octet-stream" and filename.endswith(".md"):
            self.render("markdown.html", md_content=r"%s" % str(text, encoding="utf-8").replace("\n", "newline"))
        else:   # 其他文件格式
            try:
                self.write(str(text, encoding="utf-8").replace("\n", "<br>"))
            except Exception:
                logging.error(traceback.format_exc())
                self.write('<font color=red>系統不支持的文件解析格式!</font>')


def make_app():
    return tornado.web.Application([(r"/file", UploadFileHandler)],
                                    template_path=os.path.join(os.path.dirname(__file__), "templates"))  # 模板路徑


if __name__ == "__main__":
    # Tornado configures logging.
    options.parse_command_line()
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

  parse_file.py用於解析各類格式的文檔,並返回HTML展現的格式,完整代碼以下:github

# -*- coding: utf-8 -*-
# author: Jclian91
# place: Pudong Shanghai
# time: 2020/6/5 1:05 下午
# filename: parse_file.py
# 用於解析各類文件類型的數據
import json
import base64
import logging
import traceback
from json import JSONDecodeError


# 解析text/plain或者text/csv文件格式
def parse_text_plain(text):
    return "<html><head></head><body>%s</body></html>" % text.replace("\n", "<br>")


# 解析application/json文件格式
def parse_application_json(text):
    try:
        data_dict = json.loads(text)
        return json.dumps(data_dict, ensure_ascii=False, indent=2).replace("\n", "<br>").replace(" ", "&nbsp;")
    except JSONDecodeError:
        try:
            data_list = [json.loads(_) for _ in text.split("\n") if _]
            return json.dumps(data_list, ensure_ascii=False, indent=2).replace("\n", "<br>").replace(" ", "&nbsp;")
        except JSONDecodeError:
            logging.error(traceback.format_exc())
            return "JSON文件格式解析錯誤"
        except Exception as err:
            logging.error(traceback.format_exc())
            return "未知錯誤: %s" % err


# 解析image/*文件格式
def parse_image(mtype, text):
    return '<html><head></head><body><img src="data:%s;base64,%s"></body></html>' % \
           (mtype, str(base64.b64encode(text), "utf-8"))


# 解析Python文件
def parse_python(text):
    # indent和換行
    text = text.replace("\n", "<br>").replace(" ", "&nbsp;").replace("\t", "&nbsp;" * 4)

    # 關鍵字配色
    color_list = ["gray", "red", "green", "blue", "orange", "purple", "pink", "brown", "wheat", "seagreen", "orchid", "olive"]
    key_words = ["self", "from", "import", "def", ":", "return", "open", "class", "try", "except", '"', "print"]
    for word, color in zip(key_words, color_list):
        text = text.replace(word, '<font color=%s>%s</font>' % (color, word))

    colors = ["peru"] * 7
    punctuations = list("[](){}#")
    for punctuation, color in zip(punctuations, colors):
        text = text.replace(punctuation, '<font color=%s>%s</font>' % (color, punctuation))

    html = "<html><head></head><body>%s</body></html>" % text

    return html

實現方式

  下面將進一步介紹各類格式實現預覽的機制。web

text/html: 如html文件等

  html文件的MIMETYPE爲text/html,因爲本項目採用HTML展現,所以對於text/html的文檔,直接返回其內容就能夠了。
  從Tornado的代碼中咱們能夠看出,filename變量爲文檔名稱,text爲文檔內容,bytes字符串。在前端展現的時候,咱們返回其文檔內容:編程

self.write(str(text, encoding="utf-8"))

其中,str(text, encoding="utf-8")是將bytes字符串轉化爲UTF-8編碼的字符串。

text/plain: txt/log文件等

  txt/log等文件的MIMETYPE爲text/plain,其與HTML文檔的不一樣之處在於,若是須要前端展現,須要在返回的字符中添加HTML代碼,以下(parse_file.py中的代碼):

# 解析text/plain或者text/csv文件格式
def parse_text_plain(text):
    return "<html><head></head><body>%s</body></html>" % text.replace("\n", "<br>")

text/csv: csv文件

  csv格式文件的MIMETYPE爲text/csv,其預覽的方式與txt/log等格式的文檔一致。
  但csv是逗號分隔文件,數據格式是表格形式,所以在前端展現上應該有更好的效果。關於這一格式的文檔,其前端預覽的更好方式能夠參考文章: 利用tornado實現表格文件預覽

application/json: json文件

  關於json文件的預覽,筆者更關注的是json文件的讀取。這裏處理兩種狀況,一種是整個json文件就是json字符串,另外一種狀況是json文件的每一行都是json字符串。在前端展現的時候,採用json.dumps中的indent參數實現縮進,並轉化爲html中的空格,實現方式以下(parse_file.py中的代碼):

# 解析application/json文件格式
def parse_application_json(text):
    try:
        data_dict = json.loads(text)
        return json.dumps(data_dict, ensure_ascii=False, indent=2).replace("\n", "<br>").replace(" ", "&nbsp;")
    except JSONDecodeError:
        try:
            data_list = [json.loads(_) for _ in text.split("\n") if _]
            return json.dumps(data_list, ensure_ascii=False, indent=2).replace("\n", "<br>").replace(" ", "&nbsp;")
        except JSONDecodeError:
            logging.error(traceback.format_exc())
            return "JSON文件格式解析錯誤"
        except Exception as err:
            logging.error(traceback.format_exc())
            return "未知錯誤: %s" % err

  筆者相信必定有json文件更好的前端展現方式,這裏沒有采用專門處理json的JS框架,這之後做爲後續的改進措施。

application/pdf: pdf文件

  PDF文檔的展現略顯複雜,本項目藉助了PDF.js的幫助,咱們須要它來搭建PDF預覽服務,這點在上面的項目代碼部分的開頭已經講了。
  搭建好PDF預覽服務後,因爲上傳的文件都會進入pdfjs/web/files目錄下,所以PDF文檔預覽的網址爲:http://127.0.0.1:8081/web/viewer.html?file=files/pdf_name ,其中pdf_name爲上傳的PDF文檔名稱。
  有了這個PDF預覽服務後,咱們展現PDF文檔的代碼就很簡單了(tornado_file_receiver.py中的代碼):

elif mtype == "application/pdf":
            self.redirect("http://127.0.0.1:8081/web/viewer.html?file=files/%s" % filename)

text/x-python: Python腳本文件

  Python腳本的處理方式並不複雜,無非是在把Python文檔轉化爲HTML文件格式的時候,加入縮進、換行處理,以及對特定的Python關鍵字進行配色,所以代碼以下(parse_file.py中的代碼):

# 解析Python文件
def parse_python(text):
    # indent和換行
    text = text.replace("\n", "<br>").replace(" ", "&nbsp;").replace("\t", "&nbsp;" * 4)

    # 關鍵字配色
    color_list = ["gray", "red", "green", "blue", "orange", "purple", "pink", "brown", "wheat", "seagreen", "orchid", "olive"]
    key_words = ["self", "from", "import", "def", ":", "return", "open", "class", "try", "except", '"', "print"]
    for word, color in zip(key_words, color_list):
        text = text.replace(word, '<font color=%s>%s</font>' % (color, word))

    colors = ["peru"] * 7
    punctuations = list("[](){}#")
    for punctuation, color in zip(punctuations, colors):
        text = text.replace(punctuation, '<font color=%s>%s</font>' % (color, punctuation))

    html = "<html><head></head><body>%s</body></html>" % text

    return html

  根據筆者的瞭解,其實有更好的Python腳本內容的預覽方式,能夠藉助handout模塊實現,這點筆者將會在後續加上。

image/*: 各類圖片文件,好比jpg, png等

  圖片文件在HTML上的展現有不少中,筆者採用的方式爲:

<img src="data:image/png;base64,ABKAMNDKSJFHVCJSNVOIEJHVUEHVUV==">

就是對圖片讀取後的字符串進行base64編碼便可,所以實現代碼以下(parse_file.py中的代碼):

import base64
# 解析image/*文件格式
def parse_image(mtype, text):
    return '<html><head></head><body><img src="data:%s;base64,%s"></body></html>' % \
           (mtype, str(base64.b64encode(text), "utf-8"))

markdown文件

  markdown文件的預覽稍顯複雜,藉助showdown.js和不斷的嘗試探索,因爲markdown在讀取後的換行符\n在轉化爲JavaScript字符串時並不須要轉義,這是實現預覽的難點。筆者的作法是把Python讀取的markdown中的換行符\n轉化爲newline,並在JS渲染的時候才把newline替換成\n,這就解決了不須要轉移的難題。具體的實現能夠參考markdown.html,如今Python後端代碼中把Python讀取的markdown中的換行符\n轉化爲newline,代碼以下:

elif mtype == "application/octet-stream" and filename.endswith(".md"):
            self.render("markdown.html", md_content=r"%s" % str(text, encoding="utf-8").replace("\n", "newline"))

接着在markdown.html中的JS部分把Python讀取的markdown中的換行符\n轉化爲newline,代碼以下:

<script>
        function convert(){
            var converter = new showdown.Converter();
            var text = "{{ md_content }}";
            var html = converter.makeHtml(text.replace(/newline/g, "\n"));
            document.getElementById("result").innerHTML = html;
        }
    </script>

效果demo

  下面將給出上述8中文檔格式在本系統中的預覽效果。

text/html: 如html文件等

  上傳文件爲reponse.html,預覽效果以下:

html文件預覽示例

text/plain: txt/log文件等

  上傳文件爲info.log,預覽效果以下:

info.log文件預覽

text/csv: csv文件

  上傳文件爲iris.csv,預覽效果以下:

iris.csv文件預覽

application/json: json文件

  上傳文件爲test1.json,預覽效果以下:

test1.json文件預覽

application/pdf: pdf文件

  上傳文件爲,預覽效果以下:
PDF文檔預覽

text/x-python: Python腳本文件

  上傳文件爲test.py,預覽效果以下:
Python腳本預覽

image/*: 各類圖片文件,好比jpg, png等

  上傳圖片爲ffe3d40029eae71ccf8587e5dc21d58d.jpg,預覽效果以下:
jpg圖片預覽

markdown文件

  上傳文件爲Scrapy爬取動態網頁.md,預覽效果以下:

markdown文件預覽

文件上傳記錄

  爲了證實上面的預覽確實是筆者已經實現的,而不是從哪搬來的圖片,特放上程序運行記錄以及files目錄下的文件,以下:

文件上傳記錄
上傳文件

總結

  本項目已經開源至Github,網址爲https://github.com/percent4/document_reviewer
  本項目如今支持的文檔格式還比較少,後續能夠增長更好文檔格式的支持,另外,如今的文檔格式的預覽有些能夠作得更好,後續也會進行優化~
  感謝你們閱讀,但願讀者多多批評指正~

參考文檔

  1. PDF.js官方網址:http://mozilla.github.io/pdf.js/
  2. showdown.js官方網址:https://github.com/showdownjs/showdown
  3. 讓你的站點也支持Markdown——showdown.js使用教程: https://www.jianshu.com/p/747d6f8dddb0
相關文章
相關標籤/搜索