Dijkstra算法的最小堆优化

关于Dijkstra算法的具体步骤可以参考关于最短路径的几个算法
Dijkstra算法里面每次找出离源点最近之点,使用了两个嵌套的循环语句。导致时间复杂度为O(N^2)。
本例通过最小堆,优化这两个嵌套的循环语句。令算法整体复杂度降为O(NlogN)。

实现代码

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include<stdio.h>
// book记录那些顶点已经放入生成树中
int dis[8];
// h用来保存堆,pos用来存储每个顶点再堆中的位置,size为堆的大小
int h[7], pos[7], size;
int n=6, m=9,k;
int inf = 99999999;
int i, j;
int u[10], v[10], w[10];
int first[7], next[10];
void swap(int x, int y)
{
int t;
t = h[x];
h[x] = h[y];
h[y] = t;
t = pos[h[x]];
pos[h[x]] = pos[h[y]];
pos[h[y]] = t;
}
//传入一个需要向下调整的顶点编号
void siftdown(int i)
{
int t, flag = 0;
while (i * 2 <= size&&flag == 0)
{
// 首先判断左儿子
if (dis[h[i]] > dis[h[i * 2]])
t = i * 2;
else
t = i;
// 判断右儿子
if (i * 2 + 1 <= size)
{
// 如果右儿子的值更小
if (dis[h[t]]>dis[h[i * 2 + 1]])
t = i * 2 + 1;
}
// 若无须交换,也就是子树满足最小堆
if (t != i)
{
swap(t, i);
i = t;
}
else
flag = 1;
}
}
void siftup(int i)
{
int flag = 0;
if (i == 1) return;
while (i != 1 && flag == 0)
{
// 判断父节点
if (dis[h[i]]<dis[h[i / 2]])
swap(i, i / 2);
else
flag = 1;
i = i / 2;
}
}
int pop()
{
int t;
t = h[1];
h[1] = h[size]; //将堆的最后一个点赋值到堆顶
pos[h[1]] = 1;
size--;
siftdown(1);
//pop顶点编号出来
return t;
}
int main()
{
int count = 0;
for (i = 1; i <= m; i++)
scanf("%d %d %d", &u[i], &v[i], &w[i]);
// 初始化dis
dis[1] = 0;
for (i = 2; i <= n; i++) dis[i] = inf;
// 邻接表
for (i = 1; i <= n; i++) first[i] = -1;
for (i = 1; i <= m; i++)
{
next[i] = first[u[i]];
first[u[i]] = i;
}
count++;
k = first[1];
// 遍历邻接表。找出离顶点1相邻点的权值,放入dis中
while (k != -1)
{
dis[v[k]] = w[k];
k = next[k];
}
// 初始化堆
size = n;
for (i = 1; i <= size; i++){ h[i] = i; pos[i] = i; }
//生成最小堆
for (i = size / 2; i >= 1; i--){ siftdown(i); }
pop();
while (count < n)
{
j = pop();
count++;
// 遍历以顶点j开头的所有边
// k=为第n条边
k = first[j];
while (k != -1)
{
if (dis[v[k]]>dis[u[k]] + w[k])
{
dis[v[k]] = dis[u[k]] + w[k];
siftup(pos[v[k]]);
}
k = next[k];
}
}
for (i = 1; i <= n; i++)
printf("%d ", dis[i]);
return 0;
}

输入
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4
结果
0 1 8 4 13 17

分析

h是堆,值为顶点号;pos的值对应顶点在堆中的位置。
h与pos配合,形成最小堆。注意,是根据各个顶点在dis中对应的离源点的距离生成最小堆。




第0次循环




第一次循环




将1号顶点出堆,size前移,根据1的出边(1-N,这里指的是1-2,1-3)更新dis。1号顶点变为确定值。并更新堆。
第二次循环




将2号顶点出堆,size前移,根据2的出边更新dis。2号顶点变为确定值。并更新堆
第三次循环




将四号顶点出堆,size前移,根据4的出边更新dis。4号顶点变为确定值。并更新堆
第四次循环




将三号顶点出堆,size前移,根据3的出边更新dis。3号顶点变为确定值。并更新堆
如此类推

关于最短路径的几个算法

Flody-Warshall算法




求任意两点之间的最短路径。称为“多源最短路径”
## 分析
如果要让任意两点(a到b)之间的路径变短,只能引入第三个点(k),有时候甚至不止一个k
若只允许经过1号顶点。只需判断e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示从i号顶点到j号顶点之间的路程。
只允许经过1号顶点
1
2
3
4
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][1]+e[1][j])
e[i][j]=e[i][1]+e[1][j];





只允许经过1号和2号顶点
1
2
3
4
5
6
7
8
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][1]+e[1][j])
e[i][j]=e[i][1]+e[1][j];
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][2]+e[2][j])
e[i][j]=e[i][2]+e[2][j];





只允许经过1号2号3号顶点



最终


实现代码

1
2
3
4
5
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][K]+e[K][j])
e[i][j]=e[i][K]+e[K][j];

复杂度

时间复杂度:O(N^3)
空间复杂度:O(N^2)

特点

适用于稠密图和顶点关系密切的图
不能解决负权回路

Dijkstra算法



求1到23456最短路径。“单源最短路径”问题

分析

我们建立一个数组dis。用于存放1号顶点到其余各个顶点的距离。



先找出一个离1号顶点最近的顶点。
找出的是2号顶点。
讨论2-3这条路径能否让1到3的路径变短,也就是比较dis[3]和dis[2]+e[2][3]的大小。
可知dis[3]>dis[2]+e[2][3]。我们称为“松弛”
同理2-4,将dis[4]松弛为4
都松弛完毕之后,将2号顶点在book数组中标记起来



接下来在剩下的3456顶点中,选出离1号顶点最短的顶点。
当前离1号顶点最近的是4号顶点。因此dis[4]从“估计值”变为“确定值”
接下来对4号顶点的所有相邻点进行松弛(4-5,4-6,4-3)
都松弛完毕之后,将4号顶点在book数组中标记起来



接下来在剩下的356顶点中,选出离1号顶点最短的顶点。
当前离1号顶点最近的是3号顶点。因此dis[3]从“估计值”变为“确定值”
接下来对3号顶点的所有相邻点进行松弛(3-5)
都松弛完毕之后,将3号顶点在book数组中标记起来



接下来在剩下的56顶点中,选出离1号顶点最短的顶点。
当前离1号顶点最近的是5号顶点。因此dis[5]从“估计值”变为“确定值”
接下来对5号顶点的所有相邻点进行松弛(5-6)
都松弛完毕之后,将5号顶点在book数组中标记起来



最后对6号顶点进行松弛



Dijkstra算法的主要思想:通过”边“来松弛1号顶点到其他顶点的距离。

实现代码

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
// 初始化dis数组
for(i=1;i<=n;i++)
dis[i]=e[1][i];
// 初始化book数组
for(i=1;i<=n;i++)
book[i]=0;
book[1]=1;
for(i=1;i<=n;i++)
{
min=inf;//inf假设为无限
// 找出离1号顶点最近的顶点
for(j=1;j<=n;j++)
{
if(book[j]==0&&dis[j]<min)
{
min=dis[j];
u=j;
}
}
book[u]=1;
for(v=1;v<=n;v++)
{
if(e[u][v]<inf)
{
if(dis[v]>dis[u]+e[u][v])
dis[v]=dis[u]+e[u][v];
}
}
}

复杂度

时间复杂度:O(N^2)
空间复杂度:O(M)

优化

  1. 可用堆降低至O(logN)
  2. 可用邻接表降至O(M+N)logN

邻接表

对于边数M少于N^2的系数图,可以使用相邻接表代替邻接矩阵。使得事件复杂度降至O(M+N)logN
对于M=N^2,O(M+N)logN是要大于O(N^2)的

存入



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// n为顶点数,m为边数
int n,m,i;
// u为开始点,v为结束点,w为权值
int u[6],v[6],w[6];
int first[5],next[5];
// 初始化first
for (i = 1; i <= n; i++)
{
first[i]=-1
}
// 输入
// 1 4 9
// 2 4 6
// 1 2 5
// 4 3 8
// 1 3 7
for(i=1;i<=m;i++)
{
scanf("%d %d %d",&u[i],&v[i],&w[i]);
next[i]=first[u[i]];
first[u[i]]=i;
}


first是存放顶点对应的边数的数组,如果一个顶点有多条边,可以使用next遍历。
如:

1
2
3
4
5
// 1 4 9
// 2 4 6
// 1 2 5
// 4 3 8
// 1 3 7

顶点1对应有3条边;2,4有1条,3无。
遍历



1
2
3
4
5
6
7
8
9
for (int i = 0; i < n; ++i)
{
    k=first[i];
    while(k!=-1)
    {
        // do somethings
        k=next[k];
    }
}

Bellman-Ford算法

解决负权边问题



核心代码

v,u,w数组分别一一对应给出的边
n为顶点数,m为边数

1
2
3
4
for (int k = 1; k <= n-1; ++k)
    for (int i = 1; i <= m; ++i) //枚举每一条边
        if(dis[v[i]]>dis[u[i]]+w[i]) //对每一条边进行松弛
            dis[v[i]]=dis[u[i]]+w[i]

分析



第一轮在对所有的边进行松弛之后,得到的是从1号顶点“只能经过一条边”到达其余各顶点的最短路径长度。
第二轮在对所有的边进行松弛之后,得到的是从1号顶点“最多经过两条边”到达其余各顶点的最短路径长度。
只需进行n-1轮。因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边
最短路径肯定是一个不包含回路的简单路径。
回路分为正权回路与负权回路
正权回路:回路权值为正。去掉这个回路会得到更短的路径。
负权回路:回路权值为负。每走一次会得到更短的路径。
Bellman-Ford算法还可以检测一个图是否存在负权回路
如果在进行n-1轮松弛之后,依然存在:

1
2
if(dis[v[i]]>dis[u[i]]+w[i])
dis[v[i]]=dis[u[i]]+w[i]

复杂度

时间复杂度:O(NM),比Dijkstra还要高
空间复杂度:O(M)

优化

  1. Bellman-Ford算法经常会在未到达n-1轮松弛前就已经计算出最短路径。如果在新一轮的松弛中数组dis没有发生变化。就可以跳出循环。
  2. Bellman-Ford的队列优化每次选取队首顶点u,对顶点u的所有出边进行松弛操作。如,有一条u-v,如果通过u-v这条边使得源点到顶点v的最短路径变短。(dis[u]+e[u][v]<dis[v]),且顶点v不在当前的队列中,就将顶点v放入队尾。我们还需要对队列中的顶点去重对顶点u的所有出边松弛完毕之后,将顶点v出队。
    接下来不断从队列中取出新的队首顶点再进行如上操作直至队列为空。
    实例分析




取1-2,dis[1]+e[1][2]<dis[2]。入队

取1-5



1号顶点处理完毕。出队
取2-3,2-5。处理2-5的时候,由于5号顶点已经在队列中,所以不能再次入队。




如此类推。最终处理结果




实现代码
本例使用邻接表存储
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
// n为顶点数,m为边数
int n,m,i;
// u为开始点,v为结束点,w为权值
int u[6],v[6],w[6];
int first[5],next[5];
// book数组用于记录那些顶点已经在队列中
int dis[6],book[6]={0};
int que[101]={0},head=1,tail=1;
for (i = 1; i <=n; i++)
{
    dis[i]=inf;
}
dis[1]=0;
for (i = 1; i <= n; i++) book[i]=0;
// 初始化first
for (i = 1; i <= n; i++)
{
    first[i]=-1
}
// 输入
// 1 4 9
// 2 4 6
// 1 2 5
// 4 3 8
// 1 3 7
for(i=1;i<=m;i++)
{
    scanf("%d %d %d",&u[i],&v[i],&w[i]);
    next[i]=first[u[i]];
    first[u[i]]=i;
}
que[tail]=1;tail++;        //入队
book[1]=1;                //标记
while(head<tail)
{
    k=first[que[head]];
    while(k!=-1)        //扫描当前顶点所有的边
    {
        if(dis[v[k]]>dis[u[k]]+w[k])
        {
            dis[v[k]]=dis[u[k]]+w[k];
            // 去重
            if(book[v[k]]==0)
            {
                que[tail]=v[k];
                tail++;
                book[v[k]]=1;
            }
        }
        k=next[k];
    }
    // 出队
    book[que[head]]=0;
head++;
}

队列优化的Bellman-Ford算法:其实上就是只处理那些在前一遍松弛中改变了最短路径估计值的顶点(因为只有这样才可能引起它们的邻接点最短路径估计值发生改变。)
使用队列优化的Bellman-Ford算法在形式上和广度优先搜索非常类似,不同的是在广度优先搜索的时候一个顶点出队后通常就不会再重新进入队列。
当一个顶点的最短路径估值变小后,需要重新进行松弛。
如果某个点进入队列的次数超过n次,则这个图有负环。
复杂度
最坏的情况下为O(NM)

JavaScript——Ajax与Comet

XMLHttpRequest对象

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
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
//在早期的IE中,XHR对象是通过MSXML库中一个ActiveX对象实现的。
//对于IE早期的版本IE7-,可能会遇到三种不同版本的XHR对象。
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;
for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}

响应数据
responseText:作为响应主体被返回的文本。
responseXML:如果响应的内容类型是就”text/xml”或”application/xml”,这个属性中将保存包含着响应数据的XML DOM文档
status:响应的HTTP状态。
statusText:HTTP状态的说明。

1
2
3
4
5
6
7
8
9
var xhr = createXHR();
xhr.open("get", "example.txt", false);
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.statusText);
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}

readyState 属性
0:未初始化。尚未调用open()方法
1:启动。已经调用oepn()方法,但尚未调用send()方法。
2:发送。已经调用send()方法,但尚未接收到响应。
3:接收。已经接收到部分响应数据。
4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。

1
2
var xhr = createXHR();
//这里不使用this的原因。因为onreadystatechange

是哪吃力程序的作用域问题。如果使用this对象,在有的浏览器中会导致函数执行错误

1
2
3
4
5
6
7
8
9
10
11
xhr.onreadystatechange = function (event) {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example.txt", true);
xhr.send(null);

HTTP头部信息

默认情况下,在发送XHR请求的同时,还会发送下列头部信息。
Accept:浏览器能够处理的内容类型。
Accept-Charset:浏览器能够显小的字符集。
Accept-Encoding:浏览器能够处理的压缩编码。
Accept-Language:浏览器当前设置的语言。
Connection:浏览器与服务器之间连接的类型。
Cookie:当前页面设置的任何Cookie
Host:发出请求的页面所在的域
Referer:发出请求的页面的URI。注意,HTTP规范将这个头部字段拼写错了,而为保证与规范一致,也只能将错就错了(这个英文单词的正确拼法应该是referrer)
User-Agent:浏览器的用户代理字符串。
setRequestHeader()
参数:头部信息的名称和值

1
xhr.setRequestHeader("MyHeader", "MyValue");

getRequestHeader()
参数:头部信息的名称

1
var header=xhr.getRequestHeader("MyHeader");

getAllRequestHeader()
包含所有头部信息的长字符串

GET请求

必须使用encodeURIComponent()编码

1
2
3
4
5
6
7
8
9
function addURLParam(url, name, value) {
url += (url.indexOf(";?") == -1 ? "?" : "&")
url += encodeURIComponent(name) + "=" + encodeURIComponent(value)
return url
}
var url = "example. php"
url = addURLParam(url, "name", "vincent")
url = addURLParam(url, "bock", "java")
xhr.open("get", url, false);

POST请求

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
function submitData() {
var xhr = createXHR();
xhr.onreadystatechange = function (event) {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post", "postexample.php", true);
//表单请求
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var form = document.getElementById("user-info");
//发送序列化数据
xhr.send(serialize(form));
}
function serialize(form) {
var parts = new Array();
var field = null;
for (var i = 0, len = form.elements.length; i < len; i++) {
field = form.elements[i];
switch (field.type) {
case "select-one":
case "select-multiple":
for (var j = 0, optLen = field.options.length; j < optLen; j++) {
var option = field.options[j];
if (option.selected) {
var optValue = "";
if (option.hasAttribute) {
optValue = (option.hasAttribute("value") ? option.value : option.text);
} else {
optValue = (option.attributes["value"].specified ? option.value : option.text);
}
parts.push(encodeURIComponent(field.name) + "=" + encodeURIComponent(optValue));
}
}
break;
case undefined: //fieldset
case "file": //file input
case "submit": //submit button
case "reset": //reset button
case "button": //custom button
break;
case "radio": //radio button
case "checkbox": //checkbox
if (!field.checked) {
break;
}
/* falls through */
default:
parts.push(encodeURIComponent(field.name) + "=" + encodeURIComponent(field.value));
}
}
return parts.join("&");
}

XMLHttpRequest2级

FormData

序列化表单以及创建于表单格式相同的数据(用于通过XHR传输)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function submitData() {
var xhr = createXHR();
xhr.onreadystatechange = function (event) {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post", "postexample.php", true);
var form = document.getElementById("user-info");
xhr.send(new FormData(form));
}

超时设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var xhr = createXHR();
xhr.onreadystatechange = function (event) {
try {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
} catch (ex) {
}
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000;
xhr.ontimeout = function () {
alert("Request did not return in a second.");
};
xhr.send(null);

overrideMimeType()

用于重写XHR相应的MIME类型

1
xhr.overrideMImeType("text/xml");

进度事件

loadstart:在接收到响应数据的第一个字节时触发
progress:在接收响应期间持续不断地触发。
error:在请求发生错误时触发。
abort:在因为调用abort()方法而终止连接时触发。
load:在接收到完整的响应数据时触发。
loadend:在通信完成或者触发error、abort或load事件后触发

load事件

用以代替readystatechange事件
响应接受完毕后将会触发load事件,因此没有必要去检查readyState
只要浏览器接收到服务器的响应,不管状态如何,都会触发load事件

1
2
3
4
5
6
7
8
xhr.onload = function (event) {
if ((xhr.status >= 200 && xhr.status < 300) ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};

progress事件

在浏览器接受新数据期间周期性触发
onprogress事件 处理程序会接受一个event对象,其target属性是XHR对象,有额外三个属性:
lengthComputable:表示进度信息是否可用
position:已经接受的字节
totalSize:Content-Length响应头部确定的预期字节数

1
2
3
4
5
6
xhr.onprogress = function(event){
var divStatus = document.getElementById("status");
if (event.lengthComputable){
divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize + " bytes";
}
};

跨源资源共享

默认情况下,XHR对象只能访问与包含它的页面位于同一域中的资源。
CORS(跨源资源共享)背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
GET或POST发送的请求,它没有自定义的头部,主题内容是text/plain。
在发送该请求时,需要给他附加一个额外的Origin头部
Origin:http//www.baidu.com
如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中会发同样的源信息
Access-Control-Allow-Origin:http//www.baidu.com

IE对CORS的实现

IE8中引入XDR(XdomainRequest)类型。
与XHR类似,但能实现安全可靠的跨域通信。
XDR与XHR不同之处

  • cookie不会随请求发送,也不会随响应返回。
  • 只能设置请求头部信息中的Content-Type字段。
  • 不能访问响应头部信息。
  • 只支持GET和POST请求。
    这些变化使CSRF(Cross-Site Request Forgery,跨站点请求伪造)和XSS(Cross-SiteScnpting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)。来决定是否设置Allow-origin头部作为请求的一部分,origin头部的值表示请求的来源域,以便远程资源明确地识别XDR请求。
    所有XDR请求都是异步执行的,不能用它来创建同步请求。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var xdr = new XDomainRequest();
    xdr.onload = function () {
    alert(xdr.responseText);
    };
    //如果失败,会触发error事件,遗憾的是没有其他信息可用
    xdr.onerror = function () {
    alert("Error!");
    };
    xdr.open("get", "http://www.somewhere-else.com/xdr.php");
    xdr.send(null);
    //终止请求
    xdr.abort();

与XHR一样,XDR对象也支持timeout属性以及ontimeout事件处理程序。
为支持POST请求,XDR对象提供了contentType属性,用于表示发送数据的格式

其他浏览器对CORS的实现

在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可
与IE不同,通过跨域XHR对象可以访问status和statusText属性。
限制

  • 不能使用setRequestHeader()设置自定义头部
  • 不能发送和接收cokie
  • 调用getAllRespnseHeaders()方法总会返回空字符串
    因此,对于本地资源,最好使用相对URL

    Preflighted Requests

    CORS通过一种叫做Prefltghted Requests的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求。这种请求使用OPTIONS方法,发送下列头部
  • origin:与简单的请求相同。
  • Access-Control-Request-Method:请求自身使用的方法。
  • Access-ControlRequest-Headers:(可选)自定义的头部信息,多个头部以逗号分隔
    POST发送的请求
    Origin:http:\www.nczonline.net
    Access-Control-RequestMethod:POST
    Access-Control-Request-Headers:NCZ
    服务器通过在响应中发送如下头部
  • Access- Control-Allow-Origin:与简单的请求相同。
  • Access- Control-AllowMethods:允许的方法·多个方法以逗号分隔。
  • Access- Control-Allow-Headers:允许的头部,多个头部以逗号分隔
  • Access- Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表小)。
    例如:
    1
    2
    3
    4
    Access- Control-Allow-Origin:http://www.nczonline.net
    Access- Control-AllowMethods :POST,GET
    Access- Control-Allow-Headers:NCZ
    Access- Control-Max-Age:1728000g

带凭据的请求

默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。
如果服务器接受带凭据的请求,会用下面的HTTP头部来响应。
Access-Control-Allow-Credentialg:true
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是,responseText中将是空字符串,status的值为0,而且会调用onerror事件处理程序)。

跨浏览器的CORS

检测XHR是否支持CORS的最简单方式,就是检查是否存在withCredentials属性。再结合检测XDornainRequest对象是否存在·就可以兼顾所有浏览器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined") {
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("get", "http://www.somewhere-else.com/xdr.php");
if (request) {
request.onload = function () {
//do something with request.responseText
};
request.send();
}

XMLHttpRequest与XDomainRequest共同属性方法

  • abort():用于停止正在进行的清求。
  • onerror():用于替代onreadystatechange检测错误
  • onload():用于替代onreadystatechange检测成功
  • responseText():用于取得响应内容。
  • send():比用于发送请求
    以上成员都包含在createcoCORSRequest()函数返回的对象中

其他跨域技术

图像Ping

上述第一种跨域请求技术是使用标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。

1
2
3
4
5
var img = new Image();
img.onload = img.onerror = function () {
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";

图像Ping最常用于跟踪用户点击页面或动态广告光次数。图像Ping有两个主要的缺点

  • 只能发送GET请求
  • 无法访问服务器的响应文本
    因此,图像Ping只能用于浏览器与服务器间的单向通信。

    JSONP

    JSONP跟JSON差不多,只不过是被包含在函数调用中的JSON
    callback({“name”:”vincent”})
    JSONP是通过动态<script>元素来使用的,使用时可以为src属性指定一个跨域URL这里的<script>元素,与以<img>兀素类似,都有能力不受限制地从其他域加载资源。
    因为JSONP是有效的JavaScript代码,所以在请求完成后,即在JSONP响应加载到页面中以后,就会立即执行。
    1
    2
    3
    4
    5
    6
    function handleResponse(response) {
    alert("You're at IP address " + response.ip + ", which is in " + response.city + ", " + response.region_name);
    }
    var script = document.createElement("script");
    script.src = "http://freegeoip.net/json/?callback=handleResponse";
    document.body.insertBefore(script, document.body.firstChild);

优点:

  • 在于能与直接访问相应文本,支持浏览器与服务器之间双向通信

    Comet

    Ajax是种从页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。
    有两种实现Comet的方式:长轮询和流。

    短轮询



    长轮询

    长轮询是传统轮询(也称为短轮询)的一个翻版,页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。


    HTTP流

    流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个HTTP连接。具体来说,就是浏览器向服务器发送一个请求·而服务器保持连接打开,然后周期性地向浏览器发送数据。
    在Firefox,Opera,Safari和Chrome中,通过侦听readystatechange事件及检测readystate的值是否为3,就可以利用XHR对象实现HTTP流。在上述这些浏览器中,随着不断从服务器接收数据,readystate的值会周期性地变为3。当readystate值变为3时,respongerext属性中就会保存接收到的所有数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function createStreamingClient(url, progress, finished) {
    var xhr = new XMLHttpRequest(),
    received = 0;
    xhr.open("get", url, true);
    xhr.onreadystatechange = function () {
    var result;
    if (xhr.readyState == 3) {
    //只取得最新数据冰糖调整计数器
    result = xhr.responseText.substring(received);
    received += result.length;
    //调用回调函数
    progress(result);
    } else if (xhr.readyState == 4) {
    finished(xhr.responseText);
    }
    };
    xhr.send(null);
    return xhr;
    }
    var client = createStreamingClient("streaming.php", function (data) {
    alert("Received: " + data);
    }, function (data) {
    alert("Done!");
    });

服务器发送事件

SSE(Server-Sent Events,服务器发送事件)是围绕只读Comet交互推出的API或者模式。SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须婧text/event-stream,而且是浏览器中的JavaScript API能解析格式输出。SSE支持短轮询、长轮询和HTTP流,而且能在断开连接时自动确定何时重新连接。

SSE API

1
2
3
var source = new EventSource("event.php")
//手动关闭连接
source.close()

EventSource有一个readyState属性
0:正在链接到服务器
1:打开了连接
2:关闭了连接
EventSource三个事件

  • open:在建立连接时触发。
  • message:在从服务器接收到新事件时触发。
  • error:在无法建立连接时触发

事件

所谓的服务器事件会通过一个持久的HTTP响应发送·这个响应的MIME类型为text/event-stream,响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data

1
2
3
4
data:foo
data:bar
data:foo
data:bar

对以上响应而言,事件流中的第一个message事件返回的event.data值为’foo’,第二个message事件返回的event.data值为”bar”,第三个message事件返回的event.data值为”foo\nbar·(注意中间的换行符)。对于多个连续的以data:开头的数据行,将作为多段数据解析,每个值之间以一个换行符分隔。只有在包含data:的数据行后面有空行时,才会触发message事件,因此在服务器上生成事件流时不能忘了多添加这一行。

通过id:前缀可以给特定的事件指定一个关联的ID,这个ID行位于data:行前面或后面皆可:
data:foo
id:1

设置了ID后,Eventsource对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个包含名为Last-Event-ID的特殊HTTP头部的请求,以便服务器知道下一次该触发哪个事件。在多次连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。

Web Sockets

在JavaScript中创建了Web Socket之后,会有一个HTTP请求发到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。
由于WebSockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http,而是ws。加密的连接也不是https,而是wss。

Web Sockets API

必须给WebSocket构造函数 传入绝对URL。同源策略对Web Socket不适用。

1
var socket = new WebSocket("ws://www.examplr.com/server.php")

实例化WebSocket对象后,浏览器就会马上尝试创建链接
readyState属性表示当前状态

  • Websocket.OPENING(0):正在建立连接。
  • WebSocket.OPEN(1):已经建立连接
  • WebSoCket.CLOSING(2)正在关闭连接。
  • WebSocket.CLOSE(3)已经关团连接。
    关闭连接
1
socket.close()

发送和接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
var socket = new WebSocket("ws://www.examplr.com/server.php")
var socket = new WebSocket("ws://www.examplr.com/server.php")
socket.send("hello world!")
//由于只能发送纯文本数据,对于复杂的数据必须进行序列化
var message = {
time: new Date(),
text: 'hello world'
}
socket.send(JSON.stringify(message))
//当服务器向客户端发来消息时,WebSocket对象就会触发message事件。
socket.onmessage = function (event) {
var data = event.data
}

其他事件

Web Socket事件

  • open:在成功建立连接时触发。
  • error:在发生错误时触发,连接不能持续。
  • close:在连接关团时触发
    WebSocket不支持DOM2级事件侦听器。

Mona Lisa




Erupean jazz trio是我个人挺喜欢的一支小众爵士乐队。网上对他的介绍也少得可怜,在维基百科上仅仅是“European Jazz Trio, a jazz group co-founded by Marc van Roon。其实严格来说,不算是一支爵士风格的乐队,因为这个乐队曲目融合了古典流行爵士等风格。比如这张《Mona Lisa》,里面的最后一曲《The Londonderry Air》,跟弦乐的mix在一起,更像是仲夏晴朗午后一杯加了奶的咖啡。

一个简单的Web服务器

Hello, Web

现在,我们开始编写一个简单的Web服务器,基本思路如下:

  1. 等到别人连接到我们的服务器并发送HTTP请求
  2. 分析请求
  3. 搞清楚要回复什么
  4. 从数据库中获取数据
  5. 生成HTML文件
  6. 发送回去

Python有一个叫BaseHTTPServer的内置模块可以完成第1,2,6步的工作。在下面的例子中,我们只需要完成3~5步就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
Page = '''\
<html>
<body>
<p>Hello, web!</p>
</body>
</html>
'''
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(self.Page)))
self.end_headers()
self.wfile.write(self.Page)

BaseHTTPRequestHandler类接收HTTP请求并决定使用什么方法处理请求。如果请求方法是GET,将会调用do_GET方法。我们的类RequestHandler重载了BaseHTTPRequestHandler。字符串变量Page 里面存放着我们回复客户端的HTML文档;方法send_response返回状态码200;方法send_header添加HTML头;方法end_headers插入一空行,分割头部跟页面。

1
2
3
4
if __name__ == '__main__':
    serverAddress = ('', 8080)
    server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
    server.serve_forever()

serverAddress 是个包含服务器地址跟端口的tuple,服务器地址为空字符串代表“本机”。然后我们创造BaseHTTPServer.HTTPServer的实例,然后运行。
运行效果



更进一步

我们一般很少用到完全静态的HTML文档。一般来说,都是用模板生成HTML文档。
Page就是我们的模板`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Page = '''\
<html>
<body>
<table>
<tr> <td>Header</td> <td>Value</td> </tr>
<tr> <td>Date and time</td> <td>{date_time}</td> </tr>
<tr> <td>Client host</td> <td>{client_host}</td> </tr>
<tr> <td>Client port</td> <td>{client_port}s</td> </tr>
<tr> <td>Command</td> <td>{command}</td> </tr>
<tr> <td>Path</td> <td>{path}</td> </tr>
</table>
</body>
</html>
'''

我们不妨上面的do_GET方法分开,分成生成跟发送两部分。

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
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
page = self.create_page()
self.send_page(page)
def create_page(self):
pass
def send_page(self, page):
pass
#发送部分,我们直接我们刚刚写的代码复制过来
def send_page(self, page):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page)
#生成部分
def create_page(self):
values = {
'date_time' : self.date_time_string(),
'client_host' : self.client_address[0],
'client_port' : self.client_address[1],
'command' : self.command,
'path' : self.path
}
page = self.Page.format(**values)
return page
## 主题部分依然不变
if __name__ == '__main__':
serverAddress = ('', 8080)
server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
server.serve_forever()

运行效果

注意到,就算我们目录下没有“test.html”这个文件,也不会遇到404错误。



提供静态页面服务

下一步,我们要编写一个真正能够工作的服务器——它会从我们的运行目录里读取任何静态的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
def do_GET(self):
try:
# 搜索当前目录下有无此HTML文档
full_path = os.getcwd() + self.path
# 如果不存在
if not os.path.exists(full_path):
raise ServerException("'{0}' not found".format(self.path))
# 如果存在
elif os.path.isfile(full_path):
self.handle_file(full_path)
#无法处理
else:
raise ServerException("Unknown object '{0}'".format(self.path))
except Exception as msg:
self.handle_error(msg)
#handle_file 方法,读取HTML文件并使用send_content法返回内容
def handle_file(self, full_path):
try:
# 以二进制模式打开
with open(full_path, 'rb') as reader:
content = reader.read()
self.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
self.handle_error(msg)

我们必须还要对错误情况进行处理

显示错误的页面

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
Error_Page = """\
<html>
<body>
<h1>Error accessing {path}</h1>
<p>{msg}</p>
</body>
</html>
"""
def handle_file(self, full_path):
try:
with open(full_path, 'rb') as reader:
content = reader.read()
self.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
self.handle_error(msg)
#错误处理
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content, 404)
# 发送
def send_content(self, content, status=200):
self.send_response(status)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)

运行效果



监听目录

下一步,我们编写一个可以显示文件列表的服务器(当访问的是文件夹的时候)。或者更深一步,访问文件夹的时候首先看目录下有没有index.html文件,若没有显示文件夹文件列表。
我们重写了do_GET方法

分类和处理请求

1
2
3
4
5
6
7
8
9
10
11
12
def do_GET(self):
try:
#计算出物理路径.
self.full_path = os.getcwd() + self.path
# 找出用来处理请求的case.
for case in self.Cases:
if case.test(self):
case.act(self)
break
# 错误处理
except Exception as msg:
self.handle_error(msg)

首先我们计算请求的物理路径。接下来的代码跟之前有很大不同,通过遍历Cases列表。每一个case都是一个只有两个方法(test,判断是否能处理请求;act,处理请求)的object。我们将self作为参数传进去test和act方法作进一步的处理。这里的self指的是RequestHandler实例(别忘了,我们是在编写RequestHandler的方法do_GET)
下面是case类

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
class case_no_file(object):
'''文件或目录不存在'''
def test(self, handler):
return not os.path.exists(handler.full_path)
def act(self, handler):
raise ServerException("'{0}' not found".format(handler.path))
#-------------------------------------------------------------------------------
class case_existing_file(object):
'''文件存在'''
def test(self, handler):
return os.path.isfile(handler.full_path)
def act(self, handler):
handler.handle_file(handler.full_path)
#-------------------------------------------------------------------------------
class case_directory_index_file(object):
'''处理目录下的index.html文件'''
def index_path(self, handler):
return os.path.join(handler.full_path, 'index.html')
def test(self, handler):
return os.path.isdir(handler.full_path) and \
os.path.isfile(self.index_path(handler))
def act(self, handler):
handler.handle_file(self.index_path(handler))
#-------------------------------------------------------------------------------
class case_directory_no_index_file(object):
'''如果没有index.html,列出目录文件列表'''
def index_path(self, handler):
return os.path.join(handler.full_path, 'index.html')
def test(self, handler):
return os.path.isdir(handler.full_path) and \
not os.path.isfile(self.index_path(handler))
def act(self, handler):
handler.list_dir(handler.full_path)
#-------------------------------------------------------------------------------
class case_always_fail(object):
'''最底层的case类,处理其他case不能处理的情况'''
def test(self, handler):
return True
def act(self, handler):
raise ServerException("Unknown object '{0}'".format(handler.path))

前面提到,在do_GET方法里面,将RequestHandler的实例作为handler参数传进去。在act方法里面调用RequestHandler(在act函数里面,handler就是RequestHandler的实例)的方法(如list_dir,handle_file等)进行处理。
然后,我们把RequestHandler类完善一下

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
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
Cases = [case_no_file(),
case_existing_file(),
case_directory_index_file(),
case_directory_no_index_file(),
case_always_fail()]
# 错误页面
Error_Page = """\
<html>
<body>
<h1>Error accessing {path}</h1>
<p>{msg}</p>
</body>
</html>
"""
# 目录文件列表
Listing_Page = '''\
<html>
<body>
<ul>
{0}
</ul>
</body>
</html>
'''
#分类和处理请求
def do_GET(self):
try:
#计算出物理路径.
self.full_path = os.getcwd() + self.path
# 找出用来处理请求的case.
for case in self.Cases:
if case.test(self):
case.act(self)
break
# 错误处理
except Exception as msg:
self.handle_error(msg)
#读取HTML文件并使用send_content方法返回内容
def handle_file(self, full_path):
try:
with open(full_path, 'rb') as reader:
content = reader.read()
self.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
self.handle_error(msg)
# 列出当前目录下的文件并使用send_content方法返回
def list_dir(self, full_path):
try:
entries = os.listdir(full_path)
bullets = ['<li>{0}</li>'.format(e) for e in entries if not e.startswith('.')]
page = self.Listing_Page.format('\n'.join(bullets))
self.send_content(page)
except OSError as msg:
msg = "'{0}' cannot be listed: {1}".format(self.path, msg)
self.handle_error(msg)
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content, 404)
# 发送
def send_content(self, content, status=200):
self.send_response(status)
self.send_header("Content-type", "text/html")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)

运行结果



CGI协议

当然,大多数人都不想每次添加新的功能都修改服务器的源代码。一般都服务器都支持一套叫做“通用网关接口”的东西,允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器。

CGI。在物理上是一段程序,运行在服务器上,提供同客户端HTML页面的接口。这样说大概还不好理解。那么我们看一个实际例子:现在的个人主页上大部分都有一个留言本。留言本的工作是这样的:先由用户在客户端输入一些信息,如评论之类的东西。接着用户按一下“发布或提交”(到目前为止工作都在客户端),浏览器把这些信息传送到服务器的CGI目录下特定的CGI程序中,于是CGI程序在服务器上按照预定的方法进行处理。在本例中就是把用户提交的信息存入指定的文件中。然后CGI程序给客户端发送一个信息,表示请求的任务已经结束。此时用户在浏览器里将看到“留言结束”的字样。整个过程结束。

假设我们想要服务器在HTML文档里显示本地时间,将其保存为simple.py

1
2
3
4
5
6
7
from datetime import datetime
print '''\
<html>
<body>
<p>Generated {0}</p>
</body>
</html>'''.format(datetime.now())

我们添加一个case handler
test方法检查目录下的py文件,如果有就可以用RequestHandler的run_cgi方法执行

1
2
3
4
5
6
class case_cgi_file(object):
def test(self, handler):
return os.path.isfile(handler.full_path) and \
handler.full_path.endswith('.py')
def act(self, handler):
handler.run_cgi(handler.full_path)

run_cgi核心思想很简单:

  1. 在子程序中运行程序
  2. 捕获标准输出
  3. 把数据发送回客户端

一个完整的CGI协议肯定比这复杂。比如,它要处理URL里面的参数。。。

1
2
3
4
5
6
7
def run_cgi(self, full_path):
    cmd = "python " + full_path
    child_stdin, child_stdout = os.popen2(cmd)
    child_stdin.close()
    data = child_stdout.read()
    child_stdout.close()
    self.send_content(data)

最后,我们把case handler类重写。所有case handler都继承base_case这个case handler基类

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
class base_case(object):
def handle_file(self, handler, full_path):
try:
with open(full_path, 'rb') as reader:
content = reader.read()
handler.send_content(content)
except IOError as msg:
msg = "'{0}' cannot be read: {1}".format(full_path, msg)
handler.handle_error(msg)
def index_path(self, handler):
return os.path.join(handler.full_path, 'index.html')
def test(self, handler):
assert False, 'Not implemented.'
def act(self, handler):
assert False, 'Not implemented.'
case handler
class case_no_file(base_case):
def test(self, handler):
return not os.path.exists(handler.full_path)
def act(self, handler):
raise ServerException("'{0}' not found".format(handler.path))
#-------------------------------------------------------------------------------
class case_cgi_file(base_case):
def run_cgi(self, handler):
cmd = "python " + handler.full_path
child_stdin, child_stdout = os.popen2(cmd)
child_stdin.close()
data = child_stdout.read()
child_stdout.close()
handler.send_content(data)
def test(self, handler):
return os.path.isfile(handler.full_path) and \
handler.full_path.endswith('.py')
def act(self, handler):
self.run_cgi(handler)
#-------------------------------------------------------------------------------
class case_existing_file(base_case):
def test(self, handler):
return os.path.isfile(handler.full_path)
def act(self, handler):
self.handle_file(handler, handler.full_path)
#-------------------------------------------------------------------------------
class case_directory_index_file(base_case):
def test(self, handler):
return os.path.isdir(handler.full_path) and \
os.path.isfile(self.index_path(handler))
def act(self, handler):
self.handle_file(handler, self.index_path(handler))
#-------------------------------------------------------------------------------
class case_directory_no_index_file(base_case):
Listing_Page = '''\
<html>
<body>
<ul>
{0}
</ul>
</body>
</html>
'''
def list_dir(self, handler, full_path):
try:
entries = os.listdir(full_path)
bullets = ['<li>{0}</li>'.format(e) for e in entries if not e.startswith('.')]
page = self.Listing_Page.format('\n'.join(bullets))
handler.send_content(page)
except OSError as msg:
msg = "'{0}' cannot be listed: {1}".format(self.path, msg)
handler.handle_error(msg)
def test(self, handler):
return os.path.isdir(handler.full_path) and \
not os.path.isfile(self.index_path(handler))
def act(self, handler):
self.list_dir(handler, handler.full_path)
#-------------------------------------------------------------------------------
class case_always_fail(base_case):
def test(self, handler):
return True
def act(self, handler):
raise ServerException("Unknown object '{0}'".format(handler.path))

编写一个简单的模板引擎

前言

我们知道,互联网上的HTML页面极少是完全静态的。它们都或多或少掺入一些动态数据(比如,用户名),通常一个网页会含有大量的动态数据:展示的商品,或好友的动态等等。

怎么用一种简单的方法将动态数据插入到完全静态的页面中?要解决这个问题,我们必须设计出一种前端人员熟悉的类似于HTML标记语言的方式,将动态数据插入到静态的HTML页面中。
举个例子

1
2
3
4
5
6
7
<p>Welcome, Vincent!</p>
<p>Products:</p>
<ul>
<li>Apple: $1.00</li>
<li>Fig: $1.50</li>
<li>Pomegranate: $3.25</li>
</ul>

我们知道,用户名是要动态的,因为页面根据不同的用户呈现出不同的效果;水果的列表也要是动态的,因为水果的价格不能一成不变,它需要从数据库中获取数据,动态改变价格或种类。

为了让HTML文档在我们代码中以字符串形式显示,我们必须将需要动态处理的数据单独标记出来,以便程序进行处理。

1
2
3
4
5
6
7
8
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"

可以看到,整个页面被放在名为PAGE_HTML的字符串中;而可以用循环显示的数据(如我们的水果清单)可以单独放在用名为PRODUCT_HTML的字符串。

1
2
3
4
5
6
7
def make_page(username, products):
product_html = ""
for prodname, price in products.iteritems():
product_html += PRODUCT_HTML.format(
prodname=prodname, price=price)
html = PAGE_HTML.format(name=username, product=product_html)
return html

在上面的代码中,我们向make_page函数传入一个字符串与一个字典,分别表示用户名与水果。通过字符串的format函数可以将HTML中的标记的动态数据替换成我们想要的数据。可以看出,程序确实可以运行。但是随之而来的一个问题是,这段代码使用Python写的,不懂Python的人就不能使用了。而且,这只是个简单的HTML就已经要写如此复杂的代码。设想一下,当要处理的HTML文档是现在的100倍的时候,工作量是该有多大。

模板(Templates)

最好的方法是,前端人员可以直接在HTML文档里面编辑,甚至使用一些简单的语句。

1
2
3
4
5
6
7
8
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>

我们知道,Python中的字符串是通过双引号或单引号标记起来的。当解释器遇到第一个双引号的时候,它就知道“哦,这里是一个字符串”,一直直到它遇到第二个双引号的时候,表示字符串的结束。

在我们的模板中也一样,当模板引擎遇到“{{”的时候,它就知道“哦,这里会输出一个表达式”,一直直到遇到“}}”才结束。

1
<p>Welcome, {{user_name}}!</p>

我们从头开始看,“<p>Welcome, ”是静态的HTML内容,一直到遇到“{{”的时候,切换到动态模式,在输出的时候使用变量user_name代替。这让我们想到在Python里面,有类似的格式化函数

1
<p>Welcome, {user_name}!</p>".format(user_name="vincent")

当然,模板还可以使用条件判断语句跟循环语句。

这些文件被称为“模板”,是因为他们使用相同的结构却能生成许多内容不同的页面。为了在我们的程序中使用HTML模板,我们需要编写一个模板引擎(template engine):接受静态的模板,通过把动态数据与其组合起来,生成一个HTML文档。我们把这称为模板的解析(interpret)——把模板动态部分换成真实的数据。

语法

模板引擎的语法不尽相同。接下来要编写的模板引擎的语法跟Django的(一个著名的Web框架)差不多。

表达式

表达式在我们的模板引擎中用两对大括号括起来。它可是是一个变量,亦或是变量调用的方法。

1
<p>Welcome, {{user_name}}!</p>

在Python中,我们可以用如下方法访问变量的属性或方法。

1
2
3
dict["key"]
obj.attr
obj.method()

在我们编写的模板引擎中,我们统一使用”.”操作符。来获取属性,如果获取的是可执行的方法将会自动执行。

1
2
3
4
dict.key
obj.attr
obj.method
<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>

我们也能用一个叫做“过滤器(filter)”的函数,过滤我们想要的数据。过滤器通过“|”分割

1
<p>Short name: {{story.subject|slugify|lower}}</p>

条件判断语句

条件判断语句,这是必须的。

1
2
3
{% if user.is_logged_in %}
<p>Welcome, {{ user.name }}!</p>
{% endif %}

循环语句

循环能大大减少了代码量

1
2
3
4
5
6
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>

注释

注释功能还是挺重要的

1
{# This is the best template ever! #}

开始着手

模板引擎通常会做两件事:

分析模板(parsing the template)
渲染模板(rendering the template):

  • 在模板中找出动态数据
  • 处理逻辑语句(模板中的条件判断或循环语句)
  • 执行点操作符和过滤器

问题的关键是我们如何把分析模板跟渲染模板连接在一起,也就是我们要通过“分析模板”这个步骤得出些什么东西——这些东西能够被渲染到HTML文件中。方法主要有两种:解析(interpretation)和编译(compilation)。

在解释型模板引擎中:“分析模板”阶段的产物是一些代表模板的数据,然后每次“渲染模板”阶段都要运重复这个过程。著名Web框架Django里面的模板引擎就是其中的代表。
在编译型模板引擎中:“分析模板”阶段的产物是一些可直接执行的代码,“渲染模板”阶段执行这些代码并得到静态的HTML文件,无须再次编译。Jinja2与Mako就是其中的代表。

在速度方面,编译型模板引擎在第一次运行模板的时候速度会比解释型模板引擎要慢,而当第二次第三次…执行的时候,编译型模板引擎会比解释型模板引擎快。这是一个典型的“空间换时间”的例子。

我们的模板是编译型模板引擎,我们把模板编译成Python代码。每次运行这些Python代码都会渲染得到静态的HTML文件。
我们的模板编译器应用到了代码生成(code generation)的技术,代码生成可以产生很多灵活而又功能强大的工具,包括但不限于程序语言编译器(programming language compilers)。代码生成可能比较复杂,但很实用。

编译成Python代码

我们看回前面的代码。在分析模板阶段,模板引擎将会把它们转变成Python函数。

1
2
3
4
5
6
7
8
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>

模板引擎会把模板转变成Python代码,虽然转换的结果看起来有点奇怪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([ '<p>Welcome, ',
to_str(c_user_name),
'!</p>\n<p>Products:</p>\n<ul>\n'
])
for c_product in c_product_list:
extend_result([
'\n <li>',
to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'</li>\n'
])
append_result('\n</ul>\n')
return ''.join(result)

每一个模板都会被转换成一个名为render_function的函数,第一个参数接收一个字典(第二个参数我们后面说)。

我们看看函数的开始
我们把字典解包出来(因为这样会使得二次访问的时候速度更快),赋给带前缀“c_”的变量(加上前缀避免了命名冲突)

1
2
3
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']

我们注意到append,extend,str方法赋给了局部变量result_append,result_extend和str。

1
2
3
append_result = result.append
extend_result = result.extend
to_str = str

为什么要这样做?我们看得更深一些。

Python里面对象(Object)的方法(method)(如result.append(“hello”))虽然看起来好像是一步执行,其实执行分为两步。第一步,获取对象的方法result.append;第二步,传递参数”hello”。所以我们可以将第一步的结果保存起来。这样做我们可以节省一步,这些细小的优化节省了一点点时间。

1
2
3
4
#以下语句执行的结果一样
result.append("hello")
append_result = result.append
append_result("hello")

to_str也是一个细小的优化。Python里面,寻找局部变量比寻找全局变量或内置的对象和函数都要快。str是一个Python内置的对象,虽然无论在那里都可以使用,但是Python每次还是会寻找str。将其赋给一个局部变量,将会节省一点点时间。

1
2
3
4
#string1与string2内容一样
string1=str("hello world")
to_str=str
string2=to_str("hello world")

接下来,我们使用result_append,result_extend把字符串添加到列表中。

1
2
{{ ... }}:被转换成表达式,的点操作符使用函数do_dots (render_function的第二个参数)执行。
{% if ... %} 或{% for ... %}:被转换成Python的条件判断和循环语句if与for。{% endif%}或{% end或%}代表语句的结束。

最后函数返回的是字符串。将多个部分的字符串快速转换拼接为长字符串的方法是,创建一个列表然后把他们join起来。

1
return ''.join(result)

编写引擎

CodeBuilder类

在我们编写Template类之前。我们先看看CodeBuilder。

模板引擎主要的工作就是分析模板并产生大量的Python代码。为了方便生成大量的Python代码,我们编写了CodeBuilder类,它生成代码,管理Python的缩进。为什么要管理缩进?因为我们要生成的Python代码是字符串的形式,然后使用exec函数执行。而Python是基于缩进来定义语法的,所以我们必须有一套办法来管理Python的缩进。

接来下我们开始编写,我们先看构造函数。构造函数生成一个名为code列表,用来保存最终生产的Python代码,还有一个名为indent_level 整形变量,用来管理Python的缩进。

1
2
3
def __init__(self, indent=0):
self.code = []
self.indent_level = indent

我们重载了__str__方法,__str__返回一个字符串,将列表code里面的代码连接成字符串。

1
2
def __str__(self):
return "".join(str(c) for c in self.code)

add_line函数生成一行新的代码,它会根据当前的缩进自动进行代码的缩进和换行。

1
2
def add_line(self, line):
   self.code.extend([" " * self.indent_level, line, "\n"])

根据Python的PEP8规范,规定一个缩进等于4个空格

1
INDENT_STEP = 4

indent和dedent方法,分别增加与减少缩进

1
2
3
4
def indent(self):
self.indent_level += self.INDENT_STEP
def dedent(self):
self.indent_level -= self.INDENT_STEP

add_section函数,生成一个新的CodeBuilder类。将当前列表code的代码连接成字符串(自动调用str方法),放入新的CodeBuilder类中,返回新的CodeBuilder类。

1
2
3
4
def add_section(self):
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section

get_globals函数,执行code的代码(在我们的模板引擎中,也就是定义一个函数。),返回一个名为global_namespace的字典,里面包含有刚定义的函数。

1
2
3
4
5
6
def get_globals(self):
assert self.indent_level == 0
python_source = str(self)
global_namespace = {}
exec (python_source, global_namespace)
return global_namespace

如下面的代码中,global_namespace[‘SEVENTEEN’]就是数字17,global_namespace[‘three’]就是刚定义的函数three。

1
2
3
4
5
6
python_source = """
\ SEVENTEEN = 17
def three():return 3
"""
global_namespace = {}
exec(python_source, global_namespace)

到此为止,我们的CodeBuilder类就已经完成了。CodeBuilder其实跟模板引擎没多大关系,我们仅仅是通过它来生成模板渲染的函数render_function。当然利用CodeBuilder定义的函数,因为不同的命名空间,所以完全避免的名字相同带来的冲突问题。

Template类

Template类仅有少数的接口,构造函数接收类型为字符串的模板。它的render方法通过接收一个字典,进行模板的渲染。

我通过向Template的构造函数传入模板,产生一个实例,我们就完成了编译。之后我们可以多次调用render方法渲染,从而得到不同的结果。

构造函数还接收一个字典(一般来说,是一些过滤器),把它放在template类中,当我们调用render方法的时候会用到。

1
2
3
4
5
6
7
8
9
10
template= Template(
''' <h1>Hello {{name|upper}}!</h1>
{% for topic in topics %}
<p>You are interested in {{topic}}.</p>
{% endfor %} ''', {'upper': str.upper},
)
text = template.render({
'name': "Ned",
'topics': ['Python', 'Geometry', 'Juggling'],
})

编译

Duang!接来下我们开始编写,我们先看构造函数。编译的主要工作都在构造函数里面,构造函数时整个Template类的重中之重。我们一点一点来。

构造函数接收一个字符串以及多个变量(这些变量可以是函数,也可以是列表,字符串等在模板里面要用到的东西),将多个变量放入内部定义的context字典中。

1
2
3
4
def __init__(self, text, *contexts):
self.context = {}
for context in contexts:
self.context.update(context)

我们还需要一个集合来存放定义的变量。集合all_vars存放所有模板里面出现的变量,集合loop_vars存放模板循环里面(如for循环)里面出现的变量。你现在可能感到困惑,等会儿你就知道这两个小东西对我们有啥帮助了。

1
2
self.all_vars = set()
self.loop_vars = set()

我们遇到了之前我们写的CodeBuilder类。我们通过add_line方法添加一行Python语句,定义一个名为render_function的函数,之前我们讲过render_function函数的第一个参数接收一个字典参数,第二个参数接收一个点操作符执行函数do_dots。

注意到CodeBuilder类十分简单,它甚至不知自己在做什么,它只会生成一行行新的代码。

我们使用了add_section方法生成一个新的CodeBuilder实例,并把之前编写的代码放入其中。这可以方便我们以后插入代码。

1
2
3
4
5
6
7
8
code = CodeBuilder()
code.add_line("def render_function(context, do_dots):")
code.indent()
vars_code = code.add_section()
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")

接下来我们定义一个flush_output函数,帮我们把buffered里面的Python语句通过CodeBuilder的add_line方法把添加到CodeBuilder实例的code列表中。

1
2
3
4
5
6
7
buffered = []
   def flush_output():
   if len(buffered) == 1:
   code.add_line("append_result(%s)" % buffered[0])
elif len(buffered) > 1:
code.add_line("extend_result([%s])" % ", ".join(buffered))
del buffered[:]

回到我们的模板,当我们处理模板的条件判断语句或循环语句的时候,我们想要确认这些语句是否正确。我们就需要一个栈。例如,当我们遇到一个{% if .. %}标签的时候。我们把“if”push进栈;当我们遇到{% endif %}的时候,我们pop栈,如果没有“if”在栈顶的话,将会报错。

1
    ops_stack = []

现在,我们正式开始分析模板。通过正则表达式,我们把模板分成各个部分,然后放入列表tokens中。

re.split是一个用正则表达式把一个长字符串分割成几个短字符串的函数;r表示raw_string;?s说明“.”匹配任何东西,包括换行符;{{.?}},{%.?%},{#.*?#}分别匹配表达式,条件判断与循环语句,注释。

1
    tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)

如果,有如下模板:

1
<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>

我们把它分割成:

1
2
3
4
5
6
7
8
9
10
11
[
'<p>Topics for ',
'{{name}}',
': ',
'{% for t in topics %}',
'',
'{{t}}',
', ',
'{% endfor %}',
'</p>'
]

一件将模板分割成上面这样的列表,我们就可以遍历tokens列表并做下一步处理了。

每个token都被检测,看符合四种情况的哪一种。我们只需要检查前两个字符就可以。第一种情况是注释,我们直接忽略。

1
2
3
    for token in tokens:
   if token.startswith('{#'):
   continue

对于{{…}},我们砍掉两对大括号,忽略前后的空格,把里面的语句提取出来。然后把它传递给_expr_code方法。
_expr_code方法会把我们模板的表达式转化成Python的表达式。我们会在后面详细说_expr_code这个方法。

1
2
3
   elif token.startswith('{{'):
   expr = self._expr_code(token[2:-2].strip())
   buffered.append("to_str(%s)" % expr)

第三种情况就是{% … %},首先我们运行flush_output函数通过CodeBuilder的add_line方法把buffered里面的Python语句添加到CodeBuilder实例的code列表中。

1
2
3
   elif token.startswith('{%'):
   flush_output()
   words = token[2:-2].strip().split()

现在我们对于if,for或者end这三种情况分别作出不同的处理。

对于if的情况。if标签通常只有一个表达式,所以对于长度不符的情况,我们使用_syntax_error方法抛出一个错误。我们把“if”压进栈 ops_stack,以便我们检查endif标签。然后通过_expr_code方法,把if标签里面的表达式编译成Python可识别的代码。

1
2
3
4
5
6
if words[0] == 'if':
if len(words) != 2:
self._syntax_error("Don't understand if", token)
ops_stack.append('if')
code.add_line("if %s:" % self._expr_code(words[1]))
code.indent()

第二种情况就是for,这里出现了一个新的方法_variable。还记得我们之前说过的两个用来放变量的集合吗?方法_variable的作用除了检查变量是否有非法字符外,还会将变量添加到集合中。为了避免命名冲突,我们还把变量的名字加上了“c_”的前缀。注意,for..in的in后面可能跟的是一个变量,亦或是一个可迭代的表达式(如 for i in range(10))。所以我们要使用_expr_code方法。

1
2
3
4
5
6
7
8
9
10
11
12
elif words[0] == 'for':
if len(words) != 4 or words[2] != 'in':
self._syntax_error("Don't understand for", token)
ops_stack.append('for')
self._variable(words[1], self.loop_vars)
code.add_line(
"for c_%s in %s:" % (
words[1],
self._expr_code(words[3])
)
)
code.indent()

最后一种情况就是end,通过字符串的切片提取,与ops_stack栈顶的元素作比较,判断语句是否正确。最后注意到有一个反缩进。

1
2
3
4
5
6
7
8
9
10
elif words[0].startswith('end'):
if len(words) != 1:
self._syntax_error("Don't understand end", token)
end_what = words[0][3:]
if not ops_stack:
self._syntax_error("Too many ends", token)
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
code.dedent()

对于不可识别的,我们通过_syntax_error方法抛出错误

1
2
else:
self._syntax_error("Don't understand tag", words[0])

这样我们就完成了模板里面的三种不同的语法{{…}} , {#…#} 和 {%…%} 的处理了。最后剩下普通的字符串。我们把它添加到buffered里面以便输出。repr函数与str函数类似,但是它是将对象转换成Python内部的字符串,而str是将对象转换成用户可读的友好的字符串。注意到我们的条件判断语句,主要就检测空的字符串,因为我们必须防止append_result(“”)这样无用的操作。

1
2
3
   else:
   if token:
   buffered.append(repr(token))

最后,我们还需要检查一下ops_stack是否为空。当我们的语句都是合法的,有始有终的时候,ops_stack的值应为空的。如果不为空,我们已经在某处丢掉了end标签了。检查完之后,我们将调用flush_output函数,通过CodeBuilder的add_line方法把buffered里面的Python语句添加到CodeBuilder实例的code列表中

1
2
3
    if ops_stack:
   self._syntax_error("Unmatched action tag", ops_stack[-1])
   flush_output()

我们来看看一个模板

1
2
3
4
5
6
7
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>

在模板中user_name与product是两个变量,因为他们在两对大括号之间。集合all_vars里面也有他们的名字,因为方法_expr_code将它们添加到集合allvars里面。但只有user

name需要从模板中提取出来,因为product是在循环里面定义的变量。
在模板里面的所有变量,都会保存在名为allvars的集合里面;而所有在模板的语句里面定义的变量,都会保存在名为loop

vars的集合里面。所以我们需要把在all_vars集合 而不在loopvars集合里面的变量找出来。把context里面的变量解包出来,放入加上“c”前缀的同名变量中。

1
2
    for var_name in self.all_vars - self.loop_vars:
   vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))

最后。我们把CodeBuilder的属性code代码都连接起来。使用get_globals方法,执行code的代码(在我们的模板引擎中,也就是定义一个函数。(def render_function(..):)),返回一个名为global_namespace的字典,里面包含有刚定义的函数。

现在self._render_function就是一个Python函数了,我们将会在模板的渲染阶段用到这个函数。

1
2
3
    code.add_line("return ''.join(result)")
   code.dedent()
   self._render_function = code.get_globals()['render_function']

编译表达式

上面我们只介绍了模板中的变量与语句,还有一个很重要的方法_expr_code。_expr_code将模板中的表达式编译成Python中的表达式。接下来我们来编写上面一直提到的_expr_code方法。
在我们编写的模板中,表达式可以是单独一个变量。

1
{{user_name}}

也可是包含属性方法和过滤器的复杂形式。

1
{{user.name.localized|upper|escape}}

_expr_code方法必须能处理所有的情况。
第一考虑的是我们的表达式中是否存在“|”,如果存在,我们把它分隔开来,放在列表pipes 中。把“|”分离之后,pipes[0]即为变量与”.“操作符,我们对其继续用_expr_code方法。pipes的其他项为过滤器函数的名字,我们把它逐个放入all_vars集合中。然后生成一条“函数链”

如{{user.name.localized|upper|escape}},运行之后就得到code=c_escape(c_upper(user.name.localized))

1
2
3
4
5
6
7
def _expr_code(self, expr):
if "|" in expr:
pipes = expr.split("|")
code = self._expr_code(pipes[0])
for func in pipes[1:]:
self._variable(func, self.all_vars)
code = "c_%s(%s)" % (func, code)

对于表达式中存在的“.”。首先我们要理解“.”操作符是如何操作的,在模板中x.y在Python中有两种意思:x[‘y’]或者x.y(表示哪种意思取决于x[‘y’]或者x.y哪种是可行的)。如果结果是可执行的,自动执行。这种不确定性表明了,我们只能在运行的时候尝试这些可能性,而不是在编译的时候。所以我们把x.y.z编译成一个调用的函数do_dots(x, ‘y’, ‘z’)

do_dots 函数会在编译完成的Python代码运行的时候传递进去。我们后面会详细讲述如何编写这个函数。

1
2
3
4
5
elif "." in expr:
dots = expr.split(".")
code = self._expr_code(dots[0])
args = ", ".join(repr(d) for d in dots[1:])
code = "do_dots(%s, %s)" % (code, args)

_expr_code方法的最后一部分,没有“|”,没有“.”的表达式。注意的是,传进all_vars集合的只是名字而已。

1
2
3
4
else:
self._variable(expr, self.all_vars)
code = "c_%s" % expr
return code

辅助方法

抛出一个异常

1
2
def _syntax_error(self, msg, thing):
raise TempliteSyntaxError("%s: %r" % (msg, thing))

检查变量是否有非法字符,将变量添加到集合中。

1
2
3
4
def _variable(self, name, vars_set):
if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
self._syntax_error("Not a valid name", name)
vars_set.add(name)

渲染

当我们把模板编译成Python函数之后。渲染函数要做的是,处理动态的数据,然后调用生成的Python函数。注意到,这里的self.context是一个包含需要用来渲染模板的动态数据和过滤器函数的字典。我们在Template类的构造函数里面已经update过,一般来说我们在Template类的构造函数里update的是过滤器的函数;在方法render里面update的是用来渲染模板的动态数据。因为创建了一个Template实例出来就说明编译完成。调用render,通过传入不同的context实现不同的渲染。

1
2
3
4
5
def render(self, context=None):
render_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, self._do_dots)

接下来是最后一个方法_do_dots。在编译阶段,模板表达式如x.y.z被编译成do_dots(x, ‘y’, ‘z’) 。首先把dot当做attribute,如果失败,当做key;如果可以被调用,调用它。

1
2
3
4
5
6
7
8
9
def _do_dots(self, value, *dots):
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value

注意到每次调用self._render_function方法的时候,我们都传进去一个函数用来执行点表达式。但是很多时候我们传进去的函数都一样,我们还可以把这部分代码变成编译模板的一部分。当然,这是后面要讨论的东西了。

后记

到此为止,我们的简单模板引擎就已经完工了。如果有兴趣的话你还可以为这个模板添加如下功能。

  • 模板继承与包含
  • 自定义标签
  • 自动转义
  • 带参数的过滤器
  • 更加复杂的条件语句,如else和elif
  • 多个循环嵌套
  • 空格控制

Stardust




去年冬天的最后一天65岁的Natalie Cole因病离世。

每逢冬天听起她跟她父亲Nat King Cole“合唱”的《When I Fall In Love》,心里总会泛起一股温暖。1965年,在Natalie Cole 9岁的时候,他父亲Nat King Cole因肺癌去世。31年后的1996年,Natalie Cole跟她的父亲的“合唱”第一次出现在这张唱片中。当然,这是通过音频技术将她父亲1956年的一张唱片的声音提取出来与她形成“合唱”的效果。我不知道相隔31年,跟自己的父亲合唱是一种什么样的感受。但我每次听这首歌的时候,总有一种时空交错的感觉,仿佛虽然时间会流逝,但总有一些东西会永远不变,是他们的歌声?还是他们歌声中的爱?


When I fall in love it will be forever or I I never fall in love,
In a restless world like this is
Love is ended before it begun
And too many moonlight kisses
Seem to cool in the warmth of the sun
When I give my heart,it will be completely or I never give my heart
And the moment I can feel that you feel that way too
is when I fall in love with you
when I fall in love
And the moment I can feel that you feel that way too
is when I fall in love with you

Nothing's Gonna Change




一般人叫我推荐爵士的时候,我会推荐Simone

Kopmajer的这张专辑。Simone Kopmajer这张专辑里收录的都是经典的欧美流行歌曲。经过重新编曲后,这些耳熟能详的欧美流行歌曲居然真的有点“爵士味”起来。全张专辑配器简单精致,只用钢琴,贝斯,鼓,和少许的萨克斯,让人平静且舒服。《Nothing’s gonna change my love for you》这既是专辑名字,也是一首歌的名字,是经典爱情片《廊桥遗梦》主题曲。这首歌演唱版本众多(可能大家听过最多的版本是方大同翻唱的版本了),原唱是黑人爵士乐手George Benson。Simone Kopmajer演唱的这首歌,全曲仅有一架钢琴作伴奏,没有花俏的和弦,简单平和但触动人心。

来自奥地利的Simone Kopmajer, 于1981年出生与音乐世家,双亲都是音乐老师,父亲还是音乐学校的校长,因此Simone八岁就开始接受钢琴训练,十二岁就开始在父亲领导的大乐团中担任歌手。

这张专辑的发行公司,是泰国一家小爵士唱片公司Hitman Jazz。

关于WP Slimstat Analytics插件拖慢网站解析

前几天网站速度很慢,开瀑布图发现时间都花在等在php解析html上。后来分析错误,发现问题出在WP Slimstat Analytics上面。
WP Slimstat Analytics是一款功能强大的流量分析软件,可是对网站解析的速度的拖慢也很“强大”


使用WP Slimstat Analytics之前



使用WP Slimstat Analytics之后




居然达到惊人的2秒多。

IIS显示404找不到该文件错误解决方法

今天测试的时候发现服务器上一个后缀名为.woff2的文件出现404 Not Found。确认路径,文件名,后缀名无误后还是依然404。这有点吊诡。后来查资料发现,IIS只能识别MIME 中注册过的文件类型,所以如果需要识别自己定义的某些类型请自行添加到MIME中。泪。

|