Leetao's Blog

Talk is cheap, show me the code

0%

前言

相比写过 Python 的小伙伴对 “is” 和 “==” 这两个运算符肯定不会陌生,但是你真的能清楚两者的区别吗? 别着急,听我慢慢道来.

is 和 ==

“is” 和 “==” 都是用来比较两个对象的,区别在于 “is” 是用来判断,两个对象是否为相同,即它们实际的内存地址是否相等,也就是说 id() 值是否一样, 而 “==” 就很简单了,它只是比较两个对象的值是不是相同.

是不是很简单,让我们结合之前的知识点进行简单的测试.

例子

1.

1
2
3
4
>>> a = "leetao"
>>> b = "leetao"
>>> a == b #
>>>> a is b #

第一组输出分别是什么呢?让我们揭晓答案:

这个例子如果错了,需要看一下我之前写的关于字符串池化的那篇文章了,这里字符串 “leetao” 被池化了,所以 a,b 两者实际指向同一块内存.

2.

1
2
3
4
5
6
>>> a = b = ["l","e","e","t","a","o"]
>>> c = ["l","e","e","t","a","o"]
>>> a is b #
>>> a == b #
>>> a is c #
>>> a == c #

第二组的正确答案:

这个例子也不能理解,之前关于字符串池化的时候,也提过类似的例子,a,b 在同一行赋值, python 解释器进行了优化,将两者指向同一个对象,所以导致 a 和 b 值相等, id 也相等, 至于 a 和 c 很好理解,它们值相等,但是很明显,id值不一样.

3

1
2
3
4
5
6
7
8
9
>>> a = 256
>>> b = 256
>>> a is b #
>>> a == b #

>>> c = 257
>>> d = 257
>>> c is d #
>>> c == d #

第三组就更有意思了,答案:

这个例子,牵扯到另外一个知识点,关于 Integer 对象的一个小的知识点,让我们看一下官方原文:

The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you actually just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behaviour of Python in this case is undefined. :-)

翻译过来的意思大致就是: 当前实现为-5到256之间的所有整数保留一个整数对象数组,当您在该范围内创建一个int时,实际上只返回对现有对象的引用。 因此应该可以更改值1.我怀疑在这种情况下Python的行为是未定义的。:-)

有了这个知识作为前提,就不难理解,上面的例子的答案了.

参考链接

Integer Objects

index.html
example.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

## index.html

```index.html
<!DOCTYPE html>
<html>
<head>
<title> test login by Leetao</title>
</head>
<body>
<p id="test_login"></p>
<button onclick="load_msg()">未点击</button>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script>
var load_msg = function () {
$.get('/hello?api_key=test_login',function(data){
$('#test_login')[0].innerText = data
})
}
</script>
</body>
</html>

example.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from flask import Flask, request, jsonify, render_template
from flask_login import LoginManager, current_user, login_required
login_manager = LoginManager()

app = Flask(__name__)
login_manager.init_app(app)

class User:

def __init__(self,user_name):
self.id = 'test_id'
self.user_name = user_name

@property
def is_active(self):
return True

@property
def is_authenticated(self):
return True

@property
def is_anonymous(self):
return False

def get_id(self):
try:
return text_type(self.id)
except AttributeError:
raise NotImplementedError('No `id` attribute - override `get_id`')

user = User("leetao")

@login_manager.request_loader
def load_from_request(request):
api_key = request.args.get('api_key')
if api_key == 'test_login':
return user
return None

@app.route('/hello')
@login_required
def hello_world():
print(current_user.user_name)
return jsonify('Hello, World!')

@app.route("/")
def index():
return render_template("index.html")

结果

为了方便理解,我截了两张图,一张是 api_key 正确的情况下,一张是 错误的情况下

api_key 正确

api_key 错误

前言

字典(Dict),在大家写 Python 程序中,肯定会经常用到它,但是它在官方的文档有一段话,不知道大家有没有注意到,官方原文是这样的

A dictionary’s keys are almost arbitrary values. Values that are not hashable, that is, values containing lists, dictionaries or other mutable types (that are compared by value rather than by object identity) may not be used as keys. Numeric types used for keys obey the normal rules for numeric comparison: if two numbers compare equal (such as 1 and 1.0) then they can be used interchangeably to index the same dictionary entry. (Note however, that since computers store floating-point numbers as approximations it is usually unwise to use them as dictionary keys.)

如果你看到这里已经明白了我想说的内容的话,那么恭喜你可以节约几分钟的时间了,不需要阅读本篇文章了,没看明白的小伙伴,可以接着看下面的例子。

例子

1
2
3
4
>>> a = {}
>>> a[1.0]="元旦快乐"
>>> a[1] = "新年快乐"
# a[1.0] = ?

问题来了,这时候我们试着输出 a[1.0] 会发生什么呢?

原因

是不是觉得很神奇?这是为什么呢?划重点的时候到了,注意上面的官方文档中的一句话。

Numeric types used for keys obey the normal rules for numeric comparison: if two numbers compare equal (such as 1 and 1.0) then they can be used interchangeably to index the same dictionary entry.

其意思很简单,用于键数字类型遵循正常数值比较规则:如果两个数比较相等(如1和1.0),那么他们可以互换使用,以索引相同的字典条目。

等等,这是不是就意味着 1 == 1.0 ?,真的是这样吗?别着急,看下去。

1
2
3
4
>>> 1 == 1.0
True
>>> hash(1) == hash(1.0)
True

所以这就不难理解,为什么 a[1.0] 的值,被后面的 a[1] 覆盖了。最后官方也建议,不要用他们作为字典的键值。

参考链接

Mapping Types – dict

Why can a floating point dictionary key overwrite an integer key with the same value?

前言

在 Python 中经常通过内存池化技术来提高其性能,那么问题来了,在什么情况下会池化呢? 让我们通过几个例子进行一下理解一下.

预备知识

在查看例子之前,首先要提 python 中的一个函数 id(),让我们看一下函数说明:

1
2
3
4
5
id(obj, /)
Return the identity of an object.

This is guaranteed to be unique among simultaneously existing objects.
(CPython uses the object \'s memory address.)

通过上述说明,可以得知 id() 将会返回对像的唯一标识,在 CPython 中将会返回内存地址,也就是说如果两个对象 的 id 值一样,可以说着两个对象是相同的了.

例子

example 00

1
2
3
4
a = ""
b = ""
print(id(a),id(b))
print(a is b)

输出结果:

1
2
3
4
5
6
>>> a = ""
>>> b = ""
>>> print(id(a),id(b))
2114853370544 2114853370544
>>> print(a is b)
True

example 01

1
2
3
4
a = "a"
b = "a"
print(id(a),id(b))
print(a is b)

输出结果:

1
2
3
4
5
6
>>> a = "a"
>>> b = "a"
>>> print(id(a),id(b))
2114883022608 2114883022608
>>> print(a is b)
True

example 02

1
2
3
4
a = "magic_string"
b = "magic" + "_" + "string"
print(id(a),id(b))
print(a is b)

输出结果:

1
2
3
4
5
6
>>> a = "magic_string"
>>> b = "magic" + "_" + "string"
>>> print(id(a),id(b))
2114887161136 2114887161136
>>> print(a is b)
True

example 03

1
2
3
4
a = "magic!"
b = "mgaic!"
print(id(a),id(b))
print(a is b)

输出结果:

1
2
3
4
5
6
>>> a = "magic!"
>>> b = "mgaic!"
>>> print(id(a),id(b))
2114885855416 2114889455408
>>> print(a is b)
False

example 04

1
2
3
a,b = "magic!","magic!"
print(id(a),id(b))
print(a is b)

输出结果:

1
2
3
4
5
>>> a,b = "magic!","magic!"
>>> print(id(a),id(b))
2114885691912 2114885691912
>>> print(a is b)
True

example 05

1
2
3
4
a = "!"
b = "!"
print(id(a),id(b))
print(a is b)

输出结果:

1
2
3
4
5
6
7
>>> a = "!"
>>> b = "!"
>>> print(id(a),id(b))
140564571922024 140564571922024
>>> print(a is b)
True

example 06

1
2
print(a*20 is 'aaaaaaaaaaaaaaaaaaaa')
print(a*21 is 'aaaaaaaaaaaaaaaaaaaaa')

输出结果:

1
2
3
4
>>> print(a*20 is 'aaaaaaaaaaaaaaaaaaaa')
False
>>> print(a*21 is 'aaaaaaaaaaaaaaaaaaaaa')
False

总结

通过上述 7 个例子,我们不难对 python 的字符串池化有个大概的认识,我们这里做个简单的总结:

  1. 通过 example 00,01,05,我们可以得出对于长度为 0 或者 1 的字符串会被池化
  2. 通过 example 02,03,我们可以得出字符串中只包含字母数字以及下划线的字符串会被池化
  3. 通过 example 04, 我们可以得出当在同一行对不同变量,赋值如果相同的话,它们将会指向同一个对象,注意这里面的 “magic!” 并不符合池化的要求,这只是一种编译器的优化
  4. example 06 所出现的现象在 python 中有一个专业的术语,讲常量折叠(constant folding),顾名思义,在编译优化时,讲能够计算出的结果的变量直接替换为常量.但是这没有限制吗?显然不是的,在我们的例子中已经发现,当长度超过20的时候,折叠就会失效了,试想一下,如果没有限制的话,初始化的字符串过长,将会严重导致性能的下降以及内存的消耗

参考链接

  1. The internals of Python string interning

  2. exploring python code objects

  3. Python string interning

  4. Python String objects implementation

前言

十天前知乎上有人提问 python:openpyxl模块怎么给表格添加分页符?实现分页打印功能?,看到问题之后,我很快的给他了一个如何添加垂直分页符或水平分页符的示例,你以为问题就结束了?我是这么以为的,但是事实证明,我太天真了,就在我给出示例的几分钟后,他在我的回答下评论了,说是同时添加垂直分页符和水平分页符失败了.
我当时的第一反应:

心里想着,肯定是他的写法有问题,毫不犹豫的回复到,” 没有试过同时添加两种分页符的操作,默认是水平分页符,如果你先添加了垂直分页符的话,应该后面需要重新声明:openpyxl.worksheet.pagebreak.PageBreak.tagname = “rowBreaks”,听着自己飞快击打键盘的声音,自己不经有点飘飘然.就在沉浸在自己的YY当中,又过去了几分钟,他用正确的代码错误的结果狠狠的摔在了我的脸上:

1
2
3
4
5
6
7
col_break = openpyxl.worksheet.pagebreak.Break(5) #创建分页符,参数5:在第5/6中间分页
sheet1.page_breaks.tagname = 'rowBreaks' #分页符属性设置为行分页符
sheet1.page_breaks.append(col_break) #把分页符对象添加到sheet对象里

row_break = openpyxl.worksheet.pagebreak.Break(3) #创建分页符
sheet1.page_breaks.tagname = 'colBreaks' #分页符属性设置为列分页符
sheet1.page_breaks.append(row_break) #把分页符对象添加到sheet对象里

“结果是在第3和5列添加了两个垂直分页符,是哪里有问题?大师”,他问道. 我看了一眼代码好像没有错,我的心有点慌了,双手开始微微颤抖起来,一遍又一遍地仔细地巡视着代码,视图找出错误反驳他,但是并没有,抱着最后的希望,我把他的代码复制到自己的文件中,然后敲下回车符, excel 文件静静的生成在目录下,这可能是我最后的希望了.拿鼠标的手不自觉的颤抖起来,那么小的屏幕,那么大的文件,鼠标怎么半天都没办法移动上去,我深吸一口气,控制住自己手,终于把鼠标一上去了,双击excel,闭上眼睛,再睁开,我知道,我错了.但是就这么放弃了吗?绝不!我要把这个问题打到!

亮剑

“是时候展示真正的技术了”

俗话说”解铃还须系铃人”,我们还得自己看一下问题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# example.py

from openpyxl import Workbook
from openpyxl.compat import range
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.pagebreak import Break, PageBreak

wb = Workbook()
ws = wb.active

for row in range(1, 20):
for col in range(1,30):
_ = ws.cell(column=col, row=row, value="{0}".format(get_column_letter(col)))

col_break = Break(5) #创建分页符,参数5:在第5/6中间分页
ws.page_breaks.tagname = 'rowBreaks' #分页符属性设置为行分页符
ws.page_breaks.append(col_break) #把分页符对象添加到sheet对象里

row_break = Break(3) #创建分页符
ws.page_breaks.tagname = 'colBreaks' #分页符属性设置为列分页符
ws.page_breaks.append(row_break) #把分页符对象添加到sheet对象里

wb.save(filename = dest_filename)

从代码上应该是后面的 page_breaks 把前面的覆盖了, 那让我们看看 page_breaks 究竟是什么东西.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Worksheet(_WorkbookChild):
# 省略部分代码
def _setup(self):
self.page_breaks = PageBreak() # 再看 PageBreak

class PageBreak(Serialisable):
tagname = "rowBreaks"
# 省略部分代码
def append(self, brk=None):
"""
Add a page break
"""
vals = list(self.brk)
if not isinstance(brk, Break):
brk = Break(id=self.count+1)
vals.append(brk)
self.brk = vals

从 example 中我们不难发现,我们是通过修改 page_breaks 的 tag_name 去决定插入的分页符是垂直分页符还是水平分页符的.但是 page_breaks 现在只有一个 PageBreak 这就难怪后声明的会把前面的覆盖了,那么如果我们把 page_breaks 变成 PageBreak 的列表呢?

First Blood – page_breaks

说改咱就改啊,首先尝试修改 WorkSheet 类

1
2
3
4
class Worksheet(_WorkbookChild):
# 省略部分代码
def _setup(self):
self.page_breaks = [PageBreak()]

然后再修改一下 example.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from openpyxl import Workbook
from openpyxl.compat import range
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.pagebreak import Break, PageBreak

wb = Workbook()
dest_filename = 'empty_book.xlsx'

ws = wb.active
for row in range(1, 20):
for col in range(1,30):
_ = ws.cell(column=col, row=row, value="{0}".format(get_column_letter(col)))

rowPageBreak = PageBreak()
rowPageBreak.tagname = 'rowBreaks'

colPageBreak = PageBreak()
colPageBreak.tagname = 'colBreaks'

ws.page_breaks = [rowPageBreak, colPageBreak]

ws.page_breaks[0].append(Break(id=5))
ws.page_breaks[1].append(Break(id=3))
wb.save(filename = dest_filename)

敲下回车,心里那个美滋滋,还没高兴几秒钟,就出问题了,果然做人还是得低调一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Traceback (most recent call last):
File "test.py", line 24, in <module>
wb.save(filename = dest_filename)
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\workbook\workbook.py", line 391, in save
save_workbook(self, filename)
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\writer\excel.py", line 284, in save_workbook
writer.save(filename)
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\writer\excel.py", line 266, in save
self.write_data()
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\writer\excel.py", line 83, in write_data
self._write_worksheets()
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\writer\excel.py", line 203, in _write_worksheets
xml = ws._write()
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\worksheet\worksheet.py", line 893, in _write
return write_worksheet(self)
File "F:\workspace\python\test_openpyxl\test_openpyxl\lib\site-packages\openpyxl\writer\worksheet.py", line 151, in write_worksheet
xf.write(ws.page_breaks.to_tree())
AttributeError: 'list' object has no attribute 'to_tree'

看了一眼错误信息,发现了从中作祟的家伙再 worksheet.py 的 151 行, 让我们悄悄地看一眼,打枪的不要.

1
2
3
4
# worksheet.py
# 省略部分代码
if ws.page_breaks:
xf.write(ws.page_breaks.to_tree())

原来是我们修改了 page_breaks 之后, page_breaks 有时候不再是孤家寡人了,我们需要考虑它有另外的 PageBreak 的情况了.

Double Kill – Worksheet

1
2
3
4
5
6
7
8
# worksheet.py
# 省略部分代码
if ws.page_breaks:
if isinstance(ws.page_breaks,list):
for page_break_item in ws.page_breaks:
xf.write(page_break_item.to_tree())
else:
xf.write(ws.page_breaks.to_tree())

回车,毫无问题,人生啊就是这么寂寞如雪~~~

后记

已经在 openpyxl 提了相应的issue,目前再写测试用例,过段时间就提交 PR 了

写在前面

该项目在 Ubuntu16.04 平台下开发,基于 Flask 版本 1.0.2, 项目基本完成,现已开源,源码地址 : peach-blog, 网站实际运行效果: Leetao’s Blog

环境的搭建

本项目中的项目代码基于 Python 3.6 | 3.5 开发完成,建议使用以上两个版本

1.1 使用虚拟环境

关于为什么使用虚拟环境,可以参考很久之前我写的一篇文章 为什么使用virtualenv, 尽管我们接下来使用 virtualenv ,但是终究也是虚拟环境,其核心思想还是不变的: 可以避免各个项目环境的包的混乱和版本冲突,保持项目 python 环境干净,便于后期别人维护以及项目迁移.

由于我们使用的是 python3.+,其自带了 venv, 可以创建虚拟环境, 对于 Ubuntu 则需要通过下述命令手动安装

1
sudo apt install python3-venv

准备工作做好之后,我们需要创建一个文件夹 peach-blog

1
mkdir peach-blog

windows 下的文件夹的创建这里就不多说了.

下一步是使用 venv 在 peach-blog 文件中创建虚拟环境,这个命令和 virtualenv 类似,同样需要一个必须的参数,就是虚拟环境的名字,创建虚拟环境成功中,当前文件夹会出现一个子文件夹,名字就是上述命令中指定的参数, 与虚拟环境相关的文件都保存在这个文件夹中,个人习惯喜欢把虚拟环境命名为 env

1
python3 -m venv env

如果是 windows 下,默认安装的是 python3.5+ 的话,创建命令是:

1
python -m venv env # 确保 python 路径被添加到环境变量中

现在在 peach-blog 文件夹下有一个名为 env 的子文件夹, 它里面保存了一个全新的 python 环境 (与 windows 下不同的是, windows 下 env 中还有 Python 的二进制文件), 需要使用这个虚拟环境的话,再次之前我们首先需要激活它,可以通过下述命令激活:

1
source env/bin/activate

在 windows 下激活命令如下:

1
env\Script\activate

虚拟环境激活后,其命令行会出现虚拟环境的名称,提示你已经激活环境成功

1
(env) $

当我们不想使用虚拟环境的时候,我们可以通过 deactivate 这个命令退出当前的虚拟环境.

1.2 使用 pip 安装项目开发必须的包

大多数 Python 包都使用 pip 工具安装,在我们创建虚拟环境后, 其环境中已经包含 pip. 激活虚拟环境后,pip 就可以正常的使用了.

由于项目最终使用的依赖包比较多,但是在项目初期,我们并不需将其全部安装,我们可以先安装下述 Python 包:

1
2
3
4
5
Flask
Flask-sqlalchemy
Flask-migrate
Flask-bootstrap
Flask-wtf

执行下述命令在虚拟环境中安装上述包:

1
(env) $ pip install flask flask-sqlalchemy flask-migrate flask-bootstrap flask-wtf

如果没有报错的话,基本上说明已经正确的安装这些包了.在下次的教程中就可以尝试搭建其项目的最初的框架了.

前言

前几天在某乎上有人问问题,问道 list.append(ele) 和 list = list + [ele] 一样吗? 这个问题的答案自然是不一样的,这里就不过多描述了.重要的是,借由这个问题,我产生了一个新的问题: list1 += list2 和 list1= list1 + list2 这两者有区别吗 先说结论,自然是有区别的.

测试

花一分钟看个两个例子,如果你能正确的知道输出结果,那么这篇文章最重要的部分你已经掌握了,可以没有必要通读全文了.

Example00

1
2
3
4
5
6
list1 = [1,2,3]
list2 = list1
list1 += [4,5,6]

print(list1)
print(list2)

Example01

1
2
3
4
5
6
list1 = [1,2,3] 
list2 = list1
list1 = list1 + [4,5,6]

print(list1)
print(list2)

输出

让我们看一下这两个例子的输出

Example00

1
2
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]

Example01

1
2
[1, 2, 3, 4, 5, 6]
[1, 2, 3]

原因

你可以将 “+=” 看作 extend, 它在原有的 list 基础上进行元素的拓充, 而 list = list + [ele] 则是,重新创建了一个 list 然后将原来的 list 和新添加的元素添加进去.为了帮助理解,我画了两张动图:


前言

flask-admin 算是一个很不错的 flask 后台管理了,用它来做博客系统的管理后端再合适不过了,节约时间成本,避免重复造轮子,但是作为一个程序员,写文章怎么可以没有 markdown 呢? 现在让我们尝试一下让 flask-admin 支持 markdown 吧.

实践

Flask-PageDown

这个库是对 Pagedown 的封装,将其集成到了Flask-WTF表单中了。具体内容可以去它的 github 仓库看一下: Flask-PageDown

优缺点

优点:

  1. 配置简单
  2. 对 flask-admin 的改动较小

缺点:

  1. 无法实现对代码的高亮
  2. 生成的编辑器是上下布局的,不太美观

使用方法

安装和配置

通过 pip 进行安装:

1
pip install flask-pagedown

然后在项目中初始化:

1
2
3
from flask_pagedown import PageDown
app = Flask(__name__)
pagedown = PageDown(app)

当然也可以通过 init_app(app) 的方式进行初始化

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask
from flask_pagedown import PageDown

pagedown = PageDown()

def create_app(config_name):
app = Flask(__name__, static_folder='static', static_url_path='')
app.config.from_object(config[config_name])
# 省略部分代码
pagedown.init_app(app)
return app

这个编辑器还依赖两个 js 文件,所以还需要在我们的对应的模板中添加进去相应的方法:

1
2
3
4
5
6
7
8
<html>
<head>
{{ pagedown.html_head() }}
</head>
<body>
...
</body>
</html>

默认情况下这两个 js 是通过 CDN 加载的,也就意味着你的服务器需要可以访问网络,如果只想通过本地就可以访问的话,很简单把这两个 js 文件下载下来,然后将 pagedown.html_head() 替换成两个文件的本地路径就可以了. 两个文件的下载地址: Markdown.Converter,
Markdown.Sanitizer

新增自定义的 ModelView

flask-admin 支持自定义的 ModelView, 这是我们可以实现这个功能的基础.

1
2
3
4
5
6
7
8
9
10
class PeachPostView(ModelView):

form_overrides = {
'content': PageDownField
}

def __init__(self, model, session, **kwargs):
super(PeachPostView, self).__init__(model, session, **kwargs)

# 省略部分代码

最后在我们的 flask-admin 给我们需要 markdown 编辑器的 model 添加上自定义的 ModelView 就可以了.

1
2
# 省略部分代码
admin.add_view(PeachPostView(Post, db.session, endpoint='AdminPost'))

完整代码可以查看 use:使用 flask-pagedown 作为 markdown 编辑器

最终的效果图:

使用开源的 markdown 编辑器

没错,这个方法就比较自由了,找自己喜欢的 markdown 编辑器然后集成进去. 这里我最终选择了 [editor.md]
(https://github.com/pandao/editor.md.git) 这个开源编辑器,这个好像很久没维护了,但是颜值挺高的了,就决定是它了.

优缺点

优点:

  1. 优点很明星,颜值高,满足我对 markdown 的所有需求了,支持代码高亮.

缺点:

  1. 对 flask-admin 的改动较多,需要自己写的部分比较多.

使用方法

下载 editor.md 插件

插件下载很简单,从官网上或者 github 上下载, 下载完成后,我这里将 editor.md 下载的文件保存到 static 的 plugin 的目录下了.

覆盖 flask-admin 原有文件

这里由于我们需要使用插件,所以我们需要对 flask-admin 原有的 create.html 和 edit.html 文件进行重写.

  1. 在 templates 目录下新建 admin 目录
  2. 在 admin 目录下创建 model 目录, 并新增 peach-post-create.html, peach-post-edit.html 文件,将 flask-admin 对应的 create.html 和 edit.html 内容复制进去.
  3. 在 admin 目录下新增 peach-base.html 和 peach-lib.html 文件,将 flask-admin 对应的 base.html 和 lib.html 文件复制进去

tip: 上述说所的 flask-admin 的内容,均在你的开发环境中找到 flask-admin 的库所在目录,然后需要复制的文件在: flask_admin\templates\bootstrap3\admin 下

然后配置,使其访问我们指定的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 自定义的 modelview
class PeachPostView(ModelView):
# 指定访问文件
create_template = 'admin/model/peach-post-create.html'
edit_template = 'admin/model/peach-post-edit.html'

def __init__(self, model, session, **kwargs):
super(PeachPostView, self).__init__(model, session, **kwargs)

# 指定 base_template 文件
admin = Admin(name=name, template_mode=template_mode,index_view=PeachAdminIndexView(), base_template='admin/peach-base.html')
# 省略部分代码
admin.add_view(PeachPostView(Post, db.session, endpoint='AdminPost'))

修改文件, 支持 markdown

动手修改文件的之前,我们需要先了解一下 flask-amdmin 原有的文件是什么样子的,然后才能正确的修改,这里以 create.html 为例子,截取关键代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# create.html
{% import 'admin/lib.html' as lib with context %}
# ...
{% block edit_form %}
{{ lib.render_form(form, return_url, extra(), form_opts) }} # 使用了 lib 中的函数,看一下这个函数
{% endblock %}

# lib.html

{% macro render_form(form, cancel_url, extra=None, form_opts=None, action=None, is_modal=False) -%}
{% call form_tag(action=action) %}
{{ render_form_fields(form, form_opts=form_opts) }} # 重点是表单渲染,再看一下 render_form_fields 函数
{{ render_form_buttons(cancel_url, extra, is_modal) }}
{% endcall %}
{% endmacro %}

{% macro render_form_fields(form, form_opts=None) %}
# 省略部分代码
{{ render_field(form, f, kwargs) }} # 看一下 render_field 这个函数
{% endmacro %}

{% macro render_field(form, field, kwargs={}, caller=None) %}
{% set direct_error = h.is_field_error(field.errors) %}
<div class="form-group{{ ' has-error' if direct_error else '' }}">
<label for="{{ field.id }}" class="col-md-2 control-label">{{ field.label.text }}
{% if h.is_required_form_field(field) %}
<strong style="color: red">&#42;</strong>
{%- else -%}
&nbsp;
{%- endif %}
</label>
<div class="{{ kwargs.get('column_class', 'col-md-10') }}">
{% set _dummy = kwargs.setdefault('class', 'form-control') %}
{{ field(**kwargs)|safe }}
{% if field.description %}
<p class="help-block">{{ field.description|safe }}</p>
{% endif %}
{% if direct_error %}
<ul class="help-block input-errors">
{% for e in field.errors if e is string %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if caller %}
{{ caller(form, field, direct_error, kwargs) }}
{% endif %}
</div>
{% endmacro %}

通过上述 flask-admin 源码的浏览,我们不难发现,最终的渲染最核心的函数就是 lib.html 中的 render_field 函数, 要达到我们的目的,我们就需要对这个函数进行一点的修改了.由于我们只需要对数据库模型的某个字段支持 markdown 就可以了,所以需要我们可以通过 field.id == ‘字段名’ 的方法去判断,从而让他支持 markdown.

为了更好的理解接下来的代码,我们对 editor.md 的使用做一个简单的说明:

1
2
3
4
<link rel="stylesheet" href="editormd.min.css" />
<div id="editormd">
<textarea style="display:none;">### Hello Editor.md !</textarea>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script src="jquery.min.js"></script>
<script src="editormd.min.js"></script>
<script type="text/javascript">
$(function() {
var editor = editormd("editormd", {
path : "../lib/" // Autoload modules mode, codemirror, marked... dependents libs path
});

/*
// or
var editor = editormd({
id : "editormd",
path : "../lib/"
});
*/
});
</script>

说明完了怎么使用 editor.md,我们来说一下我们最终的思路, 通过字段名去判断,然后符合添加的情况下,新建一个 div, id 为 editor,然后在 peach-post-create.html 按照 editor.md 的使用方法渲染.说完了思路,接下来就看代码.

修改 peach-lib.html 和 peach-post-create.html 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# peach-lib.html
{% macro render_field(form, field, kwargs={}, caller=None) %}
{% set direct_error = h.is_field_error(field.errors) %}
<div class="form-group{{ ' has-error' if direct_error else '' }}">
<label for="{{ field.id }}" class="col-md-2 control-label">{{ field.label.text }}
{% if h.is_required_form_field(field) %}
<strong style="color: red">&#42;</strong>
{%- else -%}
&nbsp;
{%- endif %}
</label>
<div class="{{ kwargs.get('column_class', 'col-md-10') }}">
{% set _dummy = kwargs.setdefault('class', 'form-control') %}
{% if field.id == 'content' %} # 符合条件新建一个 div
<div id='editormd'>
{{ field(**kwargs)|safe }}
</div>
{% else %}
{{ field(**kwargs)|safe }}
{% endif %}
{% if field.description %}
<p class="help-block">{{ field.description|safe }}</p>
{% endif %}
{% if direct_error %}
<ul class="help-block input-errors">
{% for e in field.errors if e is string %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if caller %}
{{ caller(form, field, direct_error, kwargs) }}
{% endif %}
</div>
{% endmacro %}

# peach-post-create.html
{% extends 'admin/master.html' %}
{% import 'admin/peach-lib.html' as lib with context %}
{% from 'admin/peach-lib.html' import extra with context %} {# backward compatible #}

# 省略部分代码

{{ super() }}
{{ lib.form_css() }}
<link href="{{ url_for('static', filename='plugins/editor.md/css/editormd.min.css')}}" rel='stylesheet'>
{% endblock %}

# 省略部分代码

{% block tail %}
{{ super() }}
{{ lib.form_js() }}
<script src="{{ url_for('static', filename='plugins/editor.md/editormd.js')}}"></script>
<script type="text/javascript">
$(function() {
var editor = editormd("editormd", {
path : "/plugins/editor.md/lib/",
height : 640
});
});
</script>
{% endblock %}

修改 modelview

1
2
3
4
5
6
7
class PeachPostView(ModelView):
# 省略部分代码
create_template = 'admin/model/peach-post-create.html'

admin = Admin(name=name, template_mode=template_mode,index_view=PeachAdminIndexView(), base_template='admin/peach-base.html')
# 省略部分代码
admin.add_view(PeachPostView(Post, db.session, endpoint='AdminPost'))

完整代码可以查看 add:使用 editor.md 编辑器

最后看一下效果图:

最后

到此大功告成,欢迎大家关注我的公共号-Leetao, 偶尔分享 flask 之外的知识.

前言

写专栏写了一段时间了,但是一直都没有带大家做一个完整的小项目,俗话说的好,“光说不练假把式,光练不说真把式,连说带练全把式”,自然我要当仁不让做“全把式”了,然后自己就偷偷摸摸的花了几天时间撸了一个博客,功能暂时没有全部完成,完成后会写一个教程教大家从零开始撸一个自己的博客,博客地址:http://132.232.71.126:8080/, 测试账号(没有修改、创建、删除权限): test/test,大家可以先睹为快, 好了这都不是重点,重点是我在写的过程中遇到了一个问题,我的好几个页面都有一个标签云,这个标签云中的标签自然会随着博客内容的增多而变化,因此肯定需要根据博客内容,自动生成,那么问题来了,我改怎么写呢?

二话不说,先写为敬

博客里面有三个地方用到了标签云:主页面,分类页面,博客详情页面,于是有了下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 主页面
@main.route("/")
def index():
tags = Tag.query.all()
# ...省略部分代码
return render_html("index.html", tags=tags,)

# 分类页面
@main.route("tag/<int:id>")
def tags(id):
tags = Tag.query.all()
# ...省略部分代码
return render_html("tags.html", tags=tags,)

# 博客详情页面
@main.route("post/<int:id>")
def post(id):
tags = Tag.query.all()
# ...省略部分代码
return render_html("post.html", tags=tags,post=post)

好像问题解决了?所有页面都能显示出来标签云了? 但是这三个页面都回传 tags 会不会太难看了一些,完全不优雅了嘛,有没有好的办法呢? 答案自然是有的,接下来就到我们的重点了。

context_processor 闪亮登场

跟我们之前的说到的钩子函数一样,它也有一个兄弟–app_context_processor,区别很简单,后者是针对蓝图的。让我们看一下它的官方定义:

Registers a template context processor function.

翻译过来很简单: 注册模板上下文处理器功能。 这个真能解决我们的问题吗?别着急让我们试一下,把之前的代码改造一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@main.app_context_processor
def peach_blog_menu():
tags = Tag.query.all()
return dict(tags=tags)

@main.route("/")
def index():
# ...省略部分代码
return render_html("index.html")

# 分类页面
@main.route("tag/<int:id>")
def tags(id):
# ...省略部分代码
return render_html("tags.html")

# 博客详情页面
@main.route("post/<int:id>")
def post(id):
tags = Tag.query.all()
# ...省略部分代码
return render_html("post.html")

是不是发现什么消失了? tags 好像从之前的几个函数中消失了,没有回传到前台,能访问到吗?自然是可以的。原因自然是 context_processor 了,它可以将我们的定义变量在所有模板中可见

如何使用呢?

  1. 如上述代码那样, context_processor 作为一个装饰器修饰一个函数
  2. 函数的返回结果必须是 dict, 然后其 key 将会作为变量在所有模板中可见

当你的很多视图函数中需要回传一个相同的变量的时候,这个时候就可以考虑使用 context_processor 了

前言

现在知乎允许上传视频,奈何不能下载视频,好气哦,无奈之下研究一下了,然后撸了代码,方便下载视频保存。

接下来以 猫为什么一点也不怕蛇? 回答为例,分享一下整个下载过程。

调试一下

打开 F12, 找到光标,如下图:

然后将光标移动到视频上。如下图:

咦这是什么?视野中出现了一条神秘的链接: https://www.zhihu.com/video/xxxxx,让我们将这条链接复制到浏览器上,然后打开:

似乎这就是我们要找的视频,不要着急,让我们看一看,网页的请求,然后你会发现一个很有意思的请求(重点来了):

让我们自己看一下数据吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"playlist": {
"ld": {
"width": 360,
"format": "mp4",
"play_url": "https://vdn.vzuu.com/LD/05fc411e-d8e0-11e8-bb8b-0242ac112a0b.mp4?auth_key=1541477643-0-0-987c2c504d14ab1165ce2ed47759d927&expiration=1541477643&disable_local_cache=1",
"duration": 17,
"size": 1123111,
"bitrate": 509,
"height": 640
},
"hd": {
"width": 720,
"format": "mp4",
"play_url": "https://vdn.vzuu.com/HD/05fc411e-d8e0-11e8-bb8b-0242ac112a0b.mp4?auth_key=1541477643-0-0-8b8024a22a62f097ca31b8b06b7233a1&expiration=1541477643&disable_local_cache=1",
"duration": 17,
"size": 4354364,
"bitrate": 1974,
"height": 1280
},
"sd": {
"width": 480,
"format": "mp4",
"play_url": "https://vdn.vzuu.com/SD/05fc411e-d8e0-11e8-bb8b-0242ac112a0b.mp4?auth_key=1541477643-0-0-5948c2562d817218c9a9fc41abad1df8&expiration=1541477643&disable_local_cache=1",
"duration": 17,
"size": 1920976,
"bitrate": 871,
"height": 848
}
},
"title": "",
"duration": 17,
"cover_info": {
"width": 720,
"thumbnail": "https://pic2.zhimg.com/80/v2-97b9435a0c32d01c7c931bd00120327d_b.jpg",
"height": 1280
},
"type": "video",
"id": "1039146361396174848",
"misc_info": {}
}

没错了,我们要下载的视频就在这里面,其中 ld 代表普清,sd 代表标清, hd 代表高清,把相应链接再次在浏览器打开,然后右键保存就可以下载视频了。

代码

知道整个流程是什么样子,接下来撸代码的过程就简单了,这里就不过再做过多解释了,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# -*- encoding: utf-8 -*-

import re
import requests
import uuid
import datetime


class DownloadVideo:

__slots__ = [
'url', 'video_name', 'url_format', 'download_url', 'video_number',
'video_api', 'clarity_list', 'clarity'
]

def __init__(self, url, clarity='ld', video_name=None):
self.url = url
self.video_name = video_name
self.url_format = "https://www.zhihu.com/question/\d+/answer/\d+"
self.clarity = clarity
self.clarity_list = ['ld', 'sd', 'hd']
self.video_api = 'https://lens.zhihu.com/api/videos'

def check_url_format(self):
pattern = re.compile(self.url_format)
matches = re.match(pattern, self.url)
if matches is None:
raise ValueError(
"链接格式应符合:https://www.zhihu.com/question/{number}/answer/{number}"
)
return True

def get_video_number(self):
try:
headers = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36'
}
response = requests.get(self.url, headers=headers)
response.encoding = 'utf-8'
html = response.text
video_ids = re.findall(r'data-lens-id="(\d+)"', html)
if video_ids:
video_id_list = list(set([video_id for video_id in video_ids]))
self.video_number = video_id_list[0]
return self
raise ValueError("获取视频编号异常:{}".format(self.url))
except Exception as e:
raise Exception(e)

def get_video_url_by_number(self):
url = "{}/{}".format(self.video_api, self.video_number)

headers = {}
headers['Referer'] = 'https://v.vzuu.com/video/{}'.format(
self.video_number)
headers['Origin'] = 'https://v.vzuu.com'
headers[
'User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36'
headers['Content-Type'] = 'application/json'

try:
response = requests.get(url, headers=headers)
response_dict = response.json()
if self.clarity in response_dict['playlist']:
self.download_url = response_dict['playlist'][
self.clarity]['play_url']
else:
for clarity in self.clarity_list:
if clarity in response_dict['playlist']:
self.download_url = response_dict['playlist'][
self.clarity]['play_url']
break
return self
except Exception as e:
raise Exception(e)

def get_video_by_video_url(self):
response = requests.get(self.download_url)
datetime_str = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
if self.video_name is not None:
video_name = "{}-{}.mp4".format(self.video_name, datetime_str)
else:
video_name = "{}-{}.mp4".format(str(uuid.uuid1()), datetime_str)
path = "{}".format(video_name)
with open(path, 'wb') as f:
f.write(response.content)

def download_video(self):

if self.clarity not in self.clarity_list:
raise ValueError("清晰度参数异常,仅支持:ld(普清),sd(标清),hd(高清)")

if self.check_url_format():
return self.get_video_number().get_video_url_by_number().get_video_by_video_url()


if __name__ == '__main__':
a = DownloadVideo('https://www.zhihu.com/question/53031925/answer/524158069')
print(a.download_video())

结语

代码还有优化空间,这里面我只是下载了回答中的第一个视频,理论上应该存在一个回答下可以有多个视频的。如果还有什么疑问或者建议,可以多多交流。