Month: 四月 2018

[译] 用 Python 编写一个模板引擎

一直对模板引擎的实现很好奇,正好看到了这篇文章,翻译一下,供大家学习、参考。

我们编写一个最简单的模板引擎,并且探索一下它的底层实现。如果你想直接看代码的话,GitHub 是你的好朋友

语言设计

这里设计的模板语言非常基础。使用两种标签,变量和块。

<!-- 变量使用 `{{` 和 `}}` 作为标识-->
<div>{{my_var}}</div>

<!-- 块使用 `{%` 和 `%}` 作为标识-->
{% each items %}
    <div>{{it}}</div>
{% end %}

大多数的块需要使用关闭标签,关闭标签使用{% end %}表示。

这个模板引擎能够处理基本的循环和条件语句,而且也支持在块中使用 callable。在我看来,能够在模板中调用任意的 Python 函数非常方便。

循环

使用循环可以遍历集合或者 iterable。

{% each people %}
    <div>{{it.name}}</div>
{% end %}

{% each [1, 2, 3] %}
    <div>{{it}}</div>
{% end %}

{% each records %}
    <div>{{..name}}</div>
{% end %}

在上面的例子里面,people 是一个集合,it 指向了当前迭代的元素。使用点分隔的路径会被解析成字典属性。使用 .. 可以访问外部上下文中的对象。

条件语句

条件语句不需要多解释。这个语言支持 if 和 else 结构,而且支持 ==, <=, >=, !=, is, <, > 这几个操作符。

{% if num > 5 %}
    <div>more than 5</div>
{% else %}
    <div>less than or equal to 5</div>
{% end %}

调用块

Callable 可以通过模板上下文传递,并且使用普通位置参数或者具名参数调用。调用块不需要使用 end 关闭。

<!-- 使用普通参数... -->
<div class='date'>{% call prettify date_created %}</div>
<!-- ...使用具名参数 -->
<div>{% call log 'here' verbosity='debug' %}</div>

原理

在探索引擎是如何编译和渲染模板之前,我们需要了解下在内存中如何表示一个编译好的模板。

编译器使用抽象语法树(Abstract Syntax Tree, AST)来表示计算机程序。AST 是对源代码进行词法分析(lexical analysis)的结果。AST 相对源代码来说有很多好处,比如说它不包含任何无关紧要的文本元素,比如说分隔符这种。而且,树中的节点可以使用属性来添加更多的功能,而不需要改动代码。

我们会解析并分析模板来构造这样一棵树,并用它来表示编译后的模板。渲染的时候,遍历这棵树,传给它对应的上下文,然后输出 HTML。

模板切词(tokenize)

解析的第一步是把内容分隔成不同的片段。每个片段可以是任意的 HTML 或者是一个标签。这里使用正则表达式和 split() 函数分隔文本。

VAR_TOKEN_START = '{{'
VAR_TOKEN_END = '}}'
BLOCK_TOKEN_START = '{%'
BLOCK_TOKEN_END = '%}'
TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % (
    VAR_TOKEN_START,
    VAR_TOKEN_END,
    BLOCK_TOKEN_START,
    BLOCK_TOKEN_END
))

让我们来看一下 TOK_REGEX。可以看到这个正则的意思是 TOK_REGEX 要么是一个变量标签,要么是一个块标签,这是为了让变量标签和块标签都能够分隔文本。表达式的最外层是一个括号,用来捕获匹配到的文本。其中的 ? 表示非贪婪的匹配。我们想让我们的正则表达式是惰性的,并且在第一次匹配到的时候停下来。

下面这个例子实际展示了一下上面的正则:

>>> TOK_REGEX.split('{% each vars %}<i>{{it}}</i>{% endeach %}')
['{% each vars %}', '<i>', '{{it}}', '</i>', '{% endeach %}']

把每个片段封装成 Fragment 对象。这个对象包含了片段的类型,并且可以作为编译函数的参数。片段有以下四种类型:

VAR_FRAGMENT = 0
OPEN_BLOCK_FRAGMENT = 1
CLOSE_BLOCK_FRAGMENT = 2
TEXT_FRAGMENT = 3

构建 AST

一旦我们做好了分词,下一步就可以遍历每个片段并构建语法树了。我们使用 Node 类来作为树的节点的基类,然后创建对每一种节点类型创建子类。每个子类都必须提供 process_fragmentrender 方法。process_fragment 用来进一步解析片段的内容并且把需要的属性存到 Node 对象上。render 方法负责使用提供的上下文转换对应的节点内容到 HTML。

子类也可以实现 enter_scopeexit_scope 钩子方法,这两个方法不是必须的。在编译器编译期间,会调用这两个钩子函数,他们应该负责进一步的初始化和清理工作。当一个 Node 创建了一个新的作用域(scope)的时候,会调用 enter_scope,当退出作用域时,会调用 exit_scope。关于作用域,下面会讲到。

Node 基类如下:

class _Node(object):
    def __init__(self, fragment=None):
        self.children = []
        self.creates_scope = False
        self.process_fragment(fragment)

    def process_fragment(self, fragment):
        pass

    def enter_scope(self):
        pass

    def render(self, context):
        pass

    def exit_scope(self):
        pass

    def render_children(self, context, children=None):
        if children is None:
            children = self.children
        def render_child(child):
            child_html = child.render(context)
            return '' if not child_html else str(child_html)
        return ''.join(map(render_child, children))

下面是变量节点的定义:

class _Variable(_Node):
    def process_fragment(self, fragment):
        self.name = fragment

    def render(self, context):
        return resolve_in_context(self.name, context)

为了确定 Node 的类型(并且进一步初始化正确的类),需要查看片段的类型和文本。文本和变量片段直接翻译成文本节点和变量节点。块片段需要一些额外的处理 —— 他们的类型是使用块命令来确定的。比如说:

{% each items %}

是一个 each 类型的块节点,因为块命令是 each。

一个节点也可以创建作用域。在编译时,我们记录当前的作用域,并且把新的节点作为作为当前作用域的子节点。一旦遇到一个正确的关闭标签,关闭当前作用域,并且从作用域栈中把当前作用域 pop 出来,使用栈顶作为新的作用域。

def compile(self):
    root = _Root()
    scope_stack = [root]
    for fragment in self.each_fragment():
        if not scope_stack:
            raise TemplateError('nesting issues')
        parent_scope = scope_stack[-1]
        if fragment.type == CLOSE_BLOCK_FRAGMENT:
            parent_scope.exit_scope()
            scope_stack.pop()
            continue
        new_node = self.create_node(fragment)
        if new_node:
            parent_scope.children.append(new_node)
            if new_node.creates_scope:
                scope_stack.append(new_node)
                new_node.enter_scope()
    return root

渲染

管线的最后一步就是把 AST 渲染成 HTML 了。这一步访问 AST 中的所有节点并且使用传递给模板的 context 参数调用 render 方法。在渲染过程中,render 不断地解析上下文变量的值。可以使用使用 ast.literal_eval 函数,它可以安全的执行包含了 Python 代码的字符串。

def eval_expression(expr):
    try:
        return 'literal', ast.literal_eval(expr)
    except ValueError, SyntaxError:
        return 'name', expr

如果我们使用上下文变量,而不是字面量的话,需要在上下文中搜索来找到它的值。在这里需要处理包含点的变量名以及使用两个点访问外部上下文的变量。下面是 resolve 函数,也是整个难题的最后一部分了~

def resolve(name, context):
    if name.startswith('..'):
        context = context.get('..', {})
        name = name[2:]
    try:
        for tok in name.split('.'):
            context = context[tok]
        return context
    except KeyError:
        raise TemplateContextError(name)

结论

我希望这个小小的学术联系能够让你对模板引擎是怎样工作的有一点初步的感觉。这个生产级别的代码还差得很远,但是也可以作为你开发更好的工具的基础。

你可以在 GitHub 上找到完整的代码,你也可以进一步在 Hacker News 上讨论。

感谢 Nassos Hadjipapas, Alex Loizou, Panagiotis Papageorgiou and Gearoid O’Rourke 审阅本文。

mysql 基础知识(6) – Join

看到网上有篇文章用韦恩图来讲解了一下 SQL 的 join 操作,但是感觉举的例子似乎不太实际,遂自己写了一篇,图是从那篇文章里面盗的(逃

假设我们有下面两张表,上边的是表 user,下边的是 package,表示每个用户对应的包裹

id name
1 Luke
2 Leia
3 Anakin
4 Padem
id content user_id
1 droid 3
2 lightsaber 2
3 blaster 1
4 R2D2 5

创建这两个表的语句分别是:

create table user (id integer, name string);
create table package (id integer, content string, user_id integer);
insert into user (id, name) values (1, 'Luke');
insert into user (id, name) values (2, 'Leia');
insert into user (id, name) values (3, 'Anakin');
insert into user (id, name) values (4, 'Padme');
insert into package (id, content, user_id) values (1, 'droid', 3);
insert into package (id, content, user_id) values (2, 'lightsaber', 2);
insert into package (id, content, user_id) values (3, 'blaster', 1);
insert into package (id, content, user_id) values (4, 'R2D2', 5);

Veen diagram(韦恩图)是一种表示集合的图形语言。SQL 的 join 本质上也是从集合论里面来的,可以从集合论的角度来学习和记忆 Join 的语法。

Inner Join

如果我们要选出每个有包裹的人,以及对应的包裹,可以使用 inner join。内连接(inner join)计算的是两个表的交集,也就是 A ∩ B

select
user.id, user.name, package.id, package.content 
from
user inner join package
on user.id == package.user_id;

结果一共有3列,每个表中的第四列都因为在对方表中没有而没有出现在结果里。

id          name        id          content
----------  ----------  ----------  ----------
1           Luke        3           blaster
2           Leia        2           lightsaber
3           Anakin      1           droid

Full Outer Join

如果我们想要选出所有的任何包裹的对应关系,哪怕是对应得人或者包裹不存在的话,可以使用 full outer join。全连接计算的是两个表的并集,也就是 A ∪ B

select
user.id, user.name, package.id, package.content
from
user full outer join package
on user.id == package.user_id;

结果一共有6列,注意其中缺字段的地方被补上了 null。另外 SQLite 不支持 full outer join。感觉这个 Join 似乎用的不是太多,因为实际情况中,往往 package.user_id 是 user.id 的外键,所以不会出现 user_id 不存在的情况。

// 结果省略

Left Outer Join

如果我们要取出每个人的包裹情况,没有包裹的也写上 null,那么这用情况下应该使用 left outer join。

select
user.id, user.name, package.id, package.content
from
user left outer join package
on user.id == package.user_id;

id          name        id          content
----------  ----------  ----------  ----------
1           Luke        3           blaster
2           Leia        2           lightsaber
3           Anakin      1           droid
4           Padme       NULL        NULL

Cross Join

要获得A表和B表左右可能的交叉组合的话,可以使用 cross join,也就是笛卡尔乘积。

select
user.id, user.name, package.id, package.content
from
user cross join package;

结果如下

id          name        id          content
----------  ----------  ----------  ----------
1           Luke        1           droid
1           Luke        2           lightsaber
1           Luke        3           blaster
1           Luke        4           R2D2
2           Leia        1           droid
2           Leia        2           lightsaber
2           Leia        3           blaster
2           Leia        4           R2D2
3           Anakin      1           droid
3           Anakin      2           lightsaber
3           Anakin      3           blaster
3           Anakin      4           R2D2
4           Padme       1           droid
4           Padme       2           lightsaber
4           Padme       3           blaster
4           Padme       4           R2D2

mysql 基础知识(5) – 聚合语句(group by)

Group by 用来按照某一列或者某几列的值聚合数据。group by x 按照 x 相同的值聚合,group by x, y 按照 x 和 y 都相同的值聚合。而查询的列要么是聚合的列,要么应该通过聚合函数来选取一列。而且所有的 null 会被聚合成一行

比如说下面的数据表中

-- How many countries are in each continent?
select
  continent
  , count(*)
from 
  countries
group by 
  continent

执行查询可以得到每个洲的国家的数量。

过滤

在 SQL 中,Where 子句是在 group 子句之前运行的,所以我们无法通过 where 来过滤 group 之后的结果,而应该使用 having 子句来过滤。

select
 continent
  , max(area)
from 
  countries
group by 
  1
having
  max(area) >= 1e7

隐式聚合

当你没有使用 group by,而使用了 max、min、count 等聚合函数的时候已经在聚合了

-- What is the largest and average country size in Europe?
select
  max(area) as largest_country
  , avg(area) as avg_country_area
from 
  countries
where 
  continent = 'Europe'

MySQL 的特殊处理

如果在查询中有没有聚合的列,那么 MySQL 就会随机选取一个列,比如下面就会随机选取一个州。

select 
  country
  , state
  , count(*)
from
  countries
group by 
  country

ref

这篇文章主要参考这里:https://www.periscopedata.com/blog/everything-about-group-by

完全理解 SQL 的内在逻辑

太多的程序员认为SQL像是洪水猛兽一样。它是少有的几种声明式的语言,和其他的命令似的面向对象的甚至函数使得语言大相径庭。
我每天都会写SQL而且在我的开源项目中大量的使用SQL,因此我非常地想要把SQL的美展现给你们这些还在挣扎着使用它的渣渣们。下面的教程适合

  1. 使用过SQL但是从来没有完全理解他的人
  2. 很了解SQL,但是从来没有思考过他的语法的人
  3. 想要把SQL交给其他人的人

这个教程将会这关注SELECT语句,其他的DML将会在另一篇文章中介绍

SQL是声明式的

首先要记住,声明式。唯一的一种范式就是你可以只是声明你想要的结果就得到了他。而不是告诉你的电脑怎样去把这个结果计算出来,不错吧?

Select first_name, last_name FROM employees WHERE salary > 100000

很简单,你不需要关心employee的记录是存在哪里的,你只想要知道那些薪水还不错的人。

如此简单,那么问题在哪里呢?问题在于我们大部分时候是在按照命令式的编程思维在思考,比如“机器,干这个,然后干那个,但是在这之前检查一下,如果是这样或者那样就不行”。这其中包括了存储临时结果在变量里,循环,迭代,调用函数等等。

忘掉那些东西,思考如何声明东西,而不是告诉机器怎样去计算。

SQL语法的顺序有些问题

常见的混乱的来源可能是SQL语法并不是按他们的执行顺序来排序的,词法(Lexical)排序是

  1. SELECT [DISTINCT]
  2. FROM
  3. WHERE
  4. GROUP BY
  5. HAVING
  6. UNION
  7. ORDER BY

简洁起见,并没有列出所有语句,而从逻辑上来说,真正的逻辑执行顺序是这样的:

  1. FROM。FROM后面的表标识了这条语句要查询的数据源。和一些子句如,(1-J1)笛卡尔积,(1-J2)ON过滤,(1-J3)添加外部列,所要应用的对象。FROM过程之后会生成一个虚拟表VT1。

    1. (1-J1)笛卡尔积 这个步骤会计算两个相关联表的笛卡尔积(CROSS JOIN) ,生成虚拟表VT1-J1。
    2. (1-J2)ON过滤 这个步骤基于虚拟表VT1-J1这一个虚拟表进行过滤,过滤出所有满足ON 谓词条件的列,生成虚拟表VT1-J2。
    3. (1-J3)添加外部行 如果使用了外连接,保留表中的不符合ON条件的列也会被加入到VT1-J2中,作为外部行,生成虚拟表VT1-J3。
  2. WHERE 对VT1过程中生成的临时表进行过滤,满足where子句的列被插入到VT2表中。

  3. GROUP BY 这个子句会把VT2中生成的表按照GROUP BY中的列进行分组。生成VT3表。

  4. HAVING 这个子句对VT3表中的不同的组进行过滤,满足HAVING条件的子句被加入到VT4表中。

  5. SELECT 这个子句对SELECT子句中的元素进行处理,生成VT5表。

    1. (5-1)计算表达式 计算SELECT 子句中的表达式,生成VT5-1
    2. (5-2)DISTINCT 寻找VT5-1中的重复列,并删掉,生成VT5-2
    3. (5-3)TOP 从ORDER BY子句定义的结果中,筛选出符合条件的列。生成VT5-3表
  6. ORDER BY 从VT5-3中的表中,根据 ORDER BY 子句的条件对结果进行排序,生成VC6表。

当然强大的 SQL 执行引擎在实际执行过程用会有各种优化,不一定严格按照这个顺序来。但是在写和看 SQL 的时候可以按照这个逻辑思考。

例子

可以思考一下下面这个语句的执行过程

SELECT C.customerid, COUNT(O.orderid) AS numorders
FROM dbo.Customers AS C
  LEFT OUTER JOIN dbo.Orders AS O
    ON C.customerid = O.customerid
WHERE C.city = 'Madrid'
GROUP BY C.customerid
HAVING COUNT(O.orderid) < 3
ORDER BY numorders

SQL是关于表的(而不是列)

因为词法排序和逻辑排序上的不同,很多的初学者认为列的值是SQL中的一等公民,实际上,不是。最重要的是表的引用。

比如说

FROM a,b

这个语句实际上是 a cross join b,也就是笛卡尔乘积。比如说,a 中有3列3行数据,b中有5列5行数据。上面的一句产生的结果是一个3+5=8列,3×5=15行的数据。

不过,尽量显式 join 的表,而不要使用逗号。

SQL 中衍生的表可以看做表的变量。

-- A derived table
FROM (SELECT * FROM author) a -- 后边这个变量是可选的
-- Get authors' first and last names, and their age in days
SELECT first_name, last_name, age
FROM (
  SELECT first_name, last_name, current_date - date_of_birth age
  FROM author
)
-- If the age is greater than 10000 days
WHERE age > 10000

在 MySQL 8.0 中还可以使用 with 语句

WITH a AS (
  SELECT first_name, last_name, current_date - date_of_birth age
  FROM author
)
SELECT *
FROM a
WHERE age > 10000

SQL 中的 Select 语句在关系代数中被称作投影(projection)。一旦你生成了表的引用,然后过滤,转换,接着你就可以把它投影成另一种形式。在 select 语句中,你终于可以按列操作生成的表了。也就是说其他的语句都是按表,或者说按照行操作的,只有到了 select 语句中你才可以操作列。

执行完了 select 语句之后,你就可以执行其他的集合排序等等操作了。

  • distinct 删除重复的行
  • union 把两个查询组合起来,并且删除重复的行
  • union all 把两个查询组合起来,并且不删除重复
  • except 做差集并且删除重复的行
  • intersect 求交集

ORDER BY 排序

Ref:

  1. https://web.archive.org/web/20150424213133/http://tech.pro:80/tutorial/1555/10-easy-steps-to-a-complete-understanding-of-sql
  2. http://www.cnblogs.com/myprogram/archive/2013/01/24/2874666.html