格式化HTML标签缩进
August 4th, 2010
Categories: 问答
读者“神の呼出”留言询问如何格式化HTML的标签缩进,并给出了他的思路和解法,是从纯粹的正则出发。例如,寻找配对的标签要用到后向引用,标签嵌套则使用递归。不过,这两个特性虽然很有用,却不宜滥用。本文试图从另一个角度出发,简化思路,降低对正则的依赖,以便提高速度。
问题描述
- 总目标:格式化HTML文本,按标签的层级输出。
- 标签是平衡的,例如
<div></div>。- 标签也有可能不是以配对的形式出现的,例如
<img ... />。- 标签可能嵌套出现的,例如:
1
2
3 <div>
<div></div>
</div>- 所有的文本可能是以单行形式给出,没有换行符、水平制表符等空白字符。
- 输出时,每出现一组新的标签,缩进一个层级。
- 最内层的标签应处于同级。
- 所有的文字应与其父标签同级。
- 独立元素实现同级缩进。
解决思路
我主要说一下思路,并给出Python版的实现。其中用到的正则,都是简单正则,可以方便地翻译为其它语言。
- 由于源文本是单行文本(默认情况),需要在合适的地方插入换行符。我的思路是,在不是位于行首的左尖括号处加入换行符。要使用多行模式,以便让
^能匹配字串内的行首。使用的正则式是(?<!^)\s*(?=<)。注意,它顺便去掉了尖括号左侧的可能的任意个空白字符。实际运行时,它不但处理单行,还处理多行。- 同理,在不是位于行尾的右尖括号的右侧插入换行符。正则式为
(?<!^)\s*(?=<)。同理。- 现在,以文本行为单位处理每一行文本。
- 设置层级变量level,初始值为0。
- 如果层级向右缩进,则level++;如果向左伸出,则level–。
- 如果该行包含
"/>",则这是一个独立的缩进单位,层级不变,直接输出level个层级符号,以及该行文本即可。- 否则,如果该行以
"</"开头,则表明是一个层级的结束,应该先level–,再输出该行内容。此顺序很重要。- 否则,如果该行以
"<"开头,则表明这是一个层级的开始,应该先level++,再输出该行内容。顺序同样重要。- 其余情况,就是普通文本,直接继承上个层级的缩进量,再输出该行文本即可。
程序至此为止。当然,如果想处理更加复杂的情况,可以酌情增减语句。例如,我所处理的文本,有的是一个标签太长,因此分行写的,例如:
1
2
3
4 <!DOCTYPE
html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">对于这样的情况,我只好给出纯粹的正则解法,虽然速度不快,但是不重复,不遗漏:
1
2
3
4
5 #combine < \n..> lines
x=re.search(r"(<[^<>]+)\s*\n\s*",content)
while x:
content=content.replace(x.group(0),x.group(1)+" ")
x=re.search(r"(<[^<>]+)\s*\n\s*",content)这种情况,我想不出正则以外的解法。
Python代码
实现只是小道。如果理解了上述思路,很容易转为其它语言的代码。JS, PHP都可以。请读者自已实现。有问题请留言。
该python代码的使用方式是:
./format_html.py source.html> dest.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 #!/usr/bin/python
# -*- coding: utf-8 -*-
#
#author: rex
#blog: http://iregex.org
#filename format_html.py
#created: 2010-08-04
import re,sys
indent="\t"
f=open(sys.argv[1])
content=f.read()
f.close()
content=content.strip()
#combine < \n..> lines
x=re.search(r"(<[^<>]+)\s*\n\s*",content)
while x:
content=content.replace(x.group(0),x.group(1)+" ")
x=re.search(r"(<[^<>]+)\s*\n\s*",content)
content=re.sub(r"(?m)(?<!^)\s*(?=<)","\n", content)
content=re.sub(r"(?<=>)\s*(?=\S)","\n", content);
lines=content.splitlines()
level=0
for l in lines:
if "/>" in l:
print "%s%s"%(indent*level,l)
elif l[:2]=='</' :
level -=1
print "%s%s"%(indent*level,l)
elif l[:1]=='<':
print "%s%s"%(indent*level,l)
level +=1
else:
print "%s%s"%(indent*level,l)
老大,我在GoogleReader里订阅了你的博客,只要点开就被墙,我在想是不是你的Title里的‘爱’字触到了敏感词?
GFW他大爷的!!!
[Reply]
rex Reply:
August 4th, 2010 at 4:12 pm
本站有两个Feed,一个是 http://feeds.feedburner.com/iregex 在国外,更新时间几乎为0.
一个是http://feed.iregex.org 在国内,从发布到显示,大约需要四小时;
如果你能翻墙,建议订阅前者,便于第一时间读到本博客;如果不能,则订阅后者,虽然慢,但是能访问。
[Reply]
Regexer Reply:
August 4th, 2010 at 5:37 pm
嗯,订阅的前者。今天打开GR就看到你这更新了,还好你的域名好记,直接输网址来学习了。
[Reply]
看来是不能用正则完全匹配的了。和我实现的方法相似啊!也是从前往后一个个读,要是真能两边同时向内处理就好了。
另外,对于多行的,我建议还是使用(PHP)str_replace将\n和\t替换成空的,这样就成一行了,毕竟字符串要比多行处理起来简单。
[Reply]
rex Reply:
August 4th, 2010 at 6:29 pm
能否给出源码,看一下您的具体实现?
[Reply]
神の呼出 Reply:
August 4th, 2010 at 11:13 pm
这是我的
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
* (PHP)
* Description: 此代码可以用来格式化html文本或包含有如<tags></tags>形式的代码
*
* (string) _code_format($args);
*
* $args = array(
* 'code' => '',
* 'indent' => 0,
* 'offset' => false,
* 'echo' => true,
* );
*
* Parameters:
* $code
* (string) 需要格式化的文本
* Default: None
* $indent
* (bool) 代码插入前是否已进行过缩进
* Default: false
* $offset
* (int) 整体缩进量。如:1表示每行前多加一个Tab符
* Default: 0
* $echo
* (bool) 是否显示格式化后的文本
* Default: true
*
* 无论$echo为true还是false,函数返回格式化后的文本
*
* Example:
* _code_format(array(
* 'code' => '<div>Hello<span>World!</span><img src="sample.jpg" /></div><div>',
* 'offset' => 2
* ));
*
*/
function _code_format($args = array()) {
$defaults = array('code' => '', 'indent' => false, 'offset' => 0, 'echo' => true);
$args = wp_parse_args($args, $defaults);
$args = (object) $args;
$code = str_replace(array("\n", "\t"), '', $args->code);
$indent = '';
for ($i = 0; $i < $args->offset; $i++) {
$indent .= "\t";
}
$pattern = "/<[\w]+[^>]*>[^<]*|<\/[\w]+>[^<]*/";
while (preg_match($pattern, $code, $matches)) {
if (substr($matches[0], -2, 1) != '/') {
if (substr($matches[0], 1, 1) != '/') {
$format_code .= "\n" . $indent . $matches[0];
$indent = $indent . "\t";
$tag_flag = true;
} else {
if ($tag_flag) {
$format_code .= $matches[0];
$indent = substr($indent, 0, -1);
} else {
$indent = substr($indent, 0, -1);
$format_code .= "\n" . $indent . $matches[0];
}
$tag_flag = false;
}
} else {
$format_code .= $matches[0];
}
$code = preg_replace("/" . str_replace('/', '\/', $matches[0]) . "/", '', $code, 1);
}
if ($args->indent) $format_code = preg_replace("/\t/", '', $format_code, $args->offset);
$format_code = preg_replace("/\n/", '', $format_code, 1) . "\n";
if ($args->echo) echo $format_code;
return $format_code;
}
测试下来只能说效率还行吧。
PS: 目前只能匹配以开头,以结尾的代码(即最外层标签外没有文字)。
[Reply]
神の呼出 Reply:
August 4th, 2010 at 11:15 pm
补充:其中的wp_parse_args()函数是WordPress中的函数
[Reply]
这样的事,似乎用 DOM 实现会更好一些。
[Reply]
rex Reply:
August 6th, 2010 at 12:56 pm
好点子。有没有人愿意实现出来?
[Reply]
jlake Reply:
August 12th, 2010 at 12:59 pm
有一个 jQuery 的 Plugin:
http://www.m12i.com/jquery_indent.html
日本人写的,用 DOM + 正则实现,效果马马虎虎。
这儿有个纯正则的实现:
http://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript
[Reply]
rex Reply:
August 13th, 2010 at 7:07 pm
学习了。感谢提供线索!
[Reply]