格式化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)
Tags: , ,

11 Responses to “格式化HTML标签缩进”

  1. Regexer
    August 4th, 2010 at 15:14
    1

    老大,我在GoogleReader里订阅了你的博客,只要点开就被墙,我在想是不是你的Title里的‘爱’字触到了敏感词?

    GFW他大爷的!!!

    [Reply]

    rex Reply:

    本站有两个Feed,一个是 http://feeds.feedburner.com/iregex 在国外,更新时间几乎为0.
    一个是http://feed.iregex.org 在国内,从发布到显示,大约需要四小时;

    如果你能翻墙,建议订阅前者,便于第一时间读到本博客;如果不能,则订阅后者,虽然慢,但是能访问。

    [Reply]

    Regexer Reply:

    嗯,订阅的前者。今天打开GR就看到你这更新了,还好你的域名好记,直接输网址来学习了。

    [Reply]

  2. 神の呼出
    August 4th, 2010 at 18:18
    2

    看来是不能用正则完全匹配的了。和我实现的方法相似啊!也是从前往后一个个读,要是真能两边同时向内处理就好了。
    另外,对于多行的,我建议还是使用(PHP)str_replace将\n和\t替换成空的,这样就成一行了,毕竟字符串要比多行处理起来简单。

    [Reply]

    rex Reply:

    能否给出源码,看一下您的具体实现?

    [Reply]

    神の呼出 Reply:

    这是我的

    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
    /**
     * (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:

    补充:其中的wp_parse_args()函数是WordPress中的函数

    [Reply]

  3. jlake
    August 6th, 2010 at 09:06
    3

    这样的事,似乎用 DOM 实现会更好一些。

    [Reply]

    rex Reply:

    好点子。有没有人愿意实现出来?

    [Reply]

    jlake Reply:

    有一个 jQuery 的 Plugin:
    http://www.m12i.com/jquery_indent.html
    日本人写的,用 DOM + 正则实现,效果马马虎虎。

    这儿有个纯正则的实现:
    http://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript

    [Reply]

    rex Reply:

    学习了。感谢提供线索!

    [Reply]

Leave a Comment

如何在本站贴代码?

可以使用任意语言名称代替“python”.