Posted in: Biology Science, Python

7天用 Flask 开发完一个生物信息学数据库的体会

这不是教程,而是我的随笔✏️

以前,做普通计算时用 Perl,开发数据库网页后台却用 PHP,然后前端还要用 JavaScript,简直要把人折腾死。后来普通计算从 Perl 转到 Python,发现 Python 有个工具叫做 Flask 可以代替 PHP 作为网页后台,那就方便了,能省用一种语言。

第一个 Flask 开发的工具是 2019 年刚开始学 Python 时开发的一个 Pipeline,不涉及数据库查询,只是接收输入、后台计算,得到结果,页面只有一个,比较简单。

这次开发的是真正的数据库,多个页面,需要与 MySQL 对接,而且也有 Pipeline 的内容。一共花了 7 天时间。生物信息学数据库与传统IT不一样的在于它多为纯查询类,不允许用户进行数据的增加、删除、修改,看起来应该很简单,但由于经常涉及到后台计算,可能还会碰到多线程分配问题。下面列条目说一下体会:

HTML 里面所有的 URL 一定不要硬编码

错误:

<a href="/about/contact/" title="Contact Us">Contact Us</a>

正确:

<a href="{{url_for('about.show', page='contact')}}" title="Contact Us">Contact Us</a>

原因:App 投入正式使用时可能不在根目录下,而是在某个子路径下(例如 https://your.domain.com/your/app/)。这时“/about/contact/”这样的 URL 就悲剧了。

JS 里面 $SCRIPT_ROOTurl_for 不要连用

我发现url_for 威力还是很强,能自动编码你的 URL,可以适应任何环境,所以不需要再加 $SCRIPT_ROOT ,加了就重复了。实际上 $SCRIPT_ROOT 是让你单独制作纯 JS 文件使用的。

错误:

$.get($SCRIPT_ROOT + "{{url_for('sidebar.stat')}}", function(data){});

正确

$.get("{{url_for('sidebar.stat')}}", function(data){}); // 或者只用 $SCRIPT_ROOT,总之不要两个一起

使用 Python 的 Multiprocess 需要小心

第一:有些链接第三方工具的库不一定能在 Multiprocess 里面运行,例如 rpy2(Python 运行 R)就有点问题,这种情况下不要死磕,不如直接调用 system 命令,虽然死板但成功率很高。

第二:想要读取里面的 subprocess 修改的变量时,必须用Manager()。为了这破玩意儿我足足折腾了一整天。以下就是个经典例子:有一个大程序large_job,为了节省时间用多个线程并列运行它(run),运行过程中想在网页上显示它的状态怎么办?答案就是用Manager()去代理一个变量,subprocess可以在中途修改它,而且其他的函数还能读取它(例如progress())

import multiprocessing as mp
manager = mp.Manager()
current_task = manager.dict()

def large_job(arg1, arg2):
    # do_the_large_job
    current_task[arg1] = 'Step 1'
    # do_the_large_job
    current_task[arg1] = 'Step 2'
    # do_the_large_job
    current_task[arg1] = 'Finished'

@annotation.route('/run/', methods=['POST'])
def run():
    pool = mp.Pool(processes=10)
    pool.apply_async(large_job, args=(current_task, arg2))

def progress():
    while True:
        # Get current_task here
        if current_task[arg1] == True:
            break

@annotation.route('/monitor/')
def monitor():
    return Response(progress(), mimetype= 'text/event-stream')

第三:subprocess 里面如果发生错误,错误的原因不会显示在主输出里面,需要使用 error_callback 参数来显示它(见下文)。

production 环境中的 WSGI 用户 和 development 环境中的 flask app 用户不一样

development 环境中的 flask app 用户就是登录的自己,所以可以继承全部的环境变量值(主要就是 PATH),但production 环境中的 WSGI 用户是系统(例如 apache 服务器)的root用户切换过来的,无法读取自己的PATH,所以必须在 wgsi 文件里面加入 os.environ["PATH"]这对于生物信息数据库需要频繁调用第三方软件进行后台计算来说很重要。现代版的软件(例如R)都很智能,PATH在哪里,使用的库就跟着去哪里,不用单独设置其他的库目录。所以适当追新也很重要。

另外用户的 locale 也存在问题。 apache 服务器启动的环境没有 UTF-8,所以需要在 conf 文件里面指定locale

WSGIDaemonProcess me user=me group=me threads=10 lang=en_US.UTF-8 locale=en_US.UTF-8

巧用 send_from_directory 自由选择数据目录

刚开始我把数据目录直接塞入了 app 的 static 下面,就是为了方便下载,后来发现可以用 send_from_directory 避开,可以选择任何一个目录作为数据目录,让 app 自身保持纯净。不过需要注意以下两点:

  1. send_from_directory 发送的都是二进制数据,注意如果用 ajax 接收,需要转换(见下文)。
  2. 需要加上 as_attachment=True 这个参数,否则如果下载的是 gzip (.gz) 文件,在 chrome 浏览器上会出现奇怪的现象:文件会自动被 chrome 解压缩,名字里面却还带着 .gz,因为 chrome 浏览器把它当成了“资源”而非“文件下载”。

SQLAlchemy 叠加多个查询条件的方法

query = ViewSpecies.query
if XXXX:
    query = query.filter(something)
if YYYY:
    query = query.filter(something)
if ZZZZ:
    query = query.filter(something)
search_result = query.all()

前面的全是构建查询条件,并没有运行,最后一句才运行。

数据库上线一个晚上之后,卡死没反应了

原因:MySQL 认为 session 已过期,Python 却认为 session 没有过期,两者打架了。解决方法只有一个:每次请求之后自动把 session 删除。以下是在蓝图里面操作的例子:

@your_blue_print.teardown_request
def shutdown_session(exception=None):
    db.session.remove()

Jinjia 可以把来自 Python 的复杂的数据结构传递给 JavaScript

传递的中介是 JSON,终于可以从不同语言的痛苦转换过程里解脱了。

var array_in_js = {{list_in_python|tojson}} // 注意加上 |tojjson 这个filter,此时Jinjia会自动把来自 Python 的 list_in_python 转换成 JavaScript 的 array_in_js。

文件输入的 accept 参数不能挡住 drag and drop

以下限制用 drag and drop 就可以轻易破解:

<input type="file" accept="text/plain">

因此必须重新根据文件名过滤。

表单输入的值传到 python 后全是字符串

<input type="number" step="1" min="0" max="50" required>

千万不要以为这里有个type="number"就是数字了,传到 python 后还是字符串。python 和 perl 不一样,对字符串进行加减乘除直接报错,如果错误正好在 subprocess 里面,那就看不出来了。在这个坑里面倒腾了好久。

subprocess 可以使用 error_callback 参数来显示错误信息

如上所述,默认错误信息不显示,很费劲。因此需要这样设置:

import multiprocessing as mp
from time import sleep

def wrongFunc():
    i = 3
    j = i/0
    k = j
    print(k)

def callback_error(result):
    print('error', result.__cause__, flush=True)

pool = mp.Pool(processes=1)
pool.apply_async(wrongFunc, error_callback=callback_error)
pool.close()
pool.join()

此时运行就会显示:

error 
"""
Traceback (most recent call last):
  File "/XXXXXX/lib/python3.9/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/XXXXXX/test.py", line 6, in wrongFunc
    j = i/0
ZeroDivisionError: division by zero
"""

AJAX 默认接收到的 response 都是字符串

仅仅使用const myBlob = new Blob([data]) 这样去转换是不行的!必须在 AJAX 里面重新设置 response 为 blob。

$.ajax({
    url: "/path/to/url",
    type: 'post',
    xhr: function(){
        var xhr = new XMLHttpRequest();
        xhr.responseType= 'blob'
        return xhr;
    },
    success: function(data){
        const myBlob = new Blob([data]);
        saveAs(myBlob, "my_file.zip");
    },
});

Comments (8) on "7天用 Flask 开发完一个生物信息学数据库的体会"

    1. Safari 16.0 Mac OS X  10.15.7

      我工作中开放的都是小网站,所以部署起来还算容易。用 Python 只是为了少学一门语言,以免相互混淆

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注