第四章 HTML 处理

目录

4.1. 接触

我经常在comp.lang.python上看到关于象“怎么才能从我的 HTML 文档中列出所有的[头|图像|链接]呢?怎么才能[分析|解释|munge]我的HTML文档的文本,但是不要标记呢?怎么才能一次给我所有的HTML标记[增加|删除|加引号]属性呢?”本章将回答所有这些问题。

以下是一个完整的,可工作的Python程序,它分为两部分。第一部分,BaseHTMLProcessor.py,是一个通用工具,它可以帮助你处理 HTML 文件,通过扫描标记和文本块。第二部分,dialect.py,是一个例子,演示了如何使用 BaseHTMLProcessor.py 来转化 HTML 文档文本,但是去掉了标记。阅读文档字符串(doc string)和注释来了解将要发生事情的概况。大部分内容看上去象巫术,因为任一个这些类的方法是如何调用的不是很清楚。不要紧,所有内容都会按进度被展示出来。

例 4.1. BaseHTMLProcessor.py

如果你还没有运行过,可以下载本例或本书用到的其它的例子

from sgmllib import SGMLParser
import htmlentitydefs

class BaseHTMLProcessor(SGMLParser):
    def reset(self):                       
        # 扩展 (可以通过调用 SGMLParser.__init__)
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs):
        # 对每一个开始标记进行调用
        # attrs 是(attr, value)元组的一个列表
        # 例如,对于 <pre class="screen">标记,tag="pre",attrs=[("class", "screen")]
        # 理想地,我们愿意重构初始的标记和属性,但是我们可能由于在源文档中没对属性
        # 值加引号而终止,或我们可能修改了在属性值前后的引号类型(单引号到双引号)。
        # 注意不正确地嵌入非HTML代码(象客户端的Javascript)可能被父类分析得不正确,
        # 从而引发运行时脚本错误。
        # 所有非HTML代码必须被包括在HTML注释标记之中(<!-- code -->),来保证非HTML
        # 代码可以不被修改地通过这个分析器(在 handle_comment中处理)。
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):         
        # 对每一个结束标记进行调用,例如,对于 </pre>, 标记将是 "pre"。
        # 重构原始结束标记。
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):         
        # 对每一个字符引用进行调用,例如,对于"&#160;", ref 将是"160"# 重构原始字符引用。
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):       
        # 对每一个实体引用进行调用,例如,对于"&copy;", ref 将是"copy"。
        # 重构原始实体引用。
        self.pieces.append("&%(ref)s;" % locals())
        # 标准的 HTML 实体以一个分号结束;其它的实体则不是这样
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):           
        # 对每一块无格式文本进行调用,也就是在任何标记之外的,不包含任何字符或
        # 实体引用的文本
        # 保存原始的文本逐字复制。
        self.pieces.append(text)

    def handle_comment(self, text):        
        # 对每一个HTML注释进行调用,例如,<!-- insert Javascript code here -->
        # 重构原始注释。
        # 当源文档中将客户端代码(如Javascript)包括在注释中时这一点非常重要,
        # 这样这些代码才可以不受影响地通过处理器;参见在unknown_starttag中的
        # 注释来了解更多细节。
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):             
        # 对每一个处理指令进行调用,例如 <?instruction>
        # 重构原始处理指令。
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        # 对DOCTYPE进行调用, 如果存在的话,例如
        # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        #     "http://www.w3.org/TR/html4/loose.dtd">
        # 重构原始DOCTYPE
        self.pieces.append("<!%(text)s>" % locals())

    def output(self):              
        """返回处理后的HTML作为单个字符串"""
        return "".join(self.pieces)from sgmllib import SGMLParser

例 4.2. dialect.py

import re
from BaseHTMLProcessor import BaseHTMLProcessor

class Dialectizer(BaseHTMLProcessor):
    subs = ()

    def reset(self):
        # 扩展 (从父类中的 __init__ 进行调用)
        # 重置所有数据属性
        self.verbatim = 0
        BaseHTMLProcessor.reset(self)

    def start_pre(self, attrs):            
        # 对HTML源代码中的每个<pre>标记进行调用
        # 增加verbatim模式计数,然后象正常方式处理标记
        self.verbatim += 1                 
        self.unknown_starttag("pre", attrs)

    def end_pre(self):                     
        # 对HTML源代码中的每个</pre>标记进行调用
        # 减少verbatim模式计数
        self.unknown_endtag("pre")         
        self.verbatim -= 1                 

    def handle_data(self, text):                                        
        # 覆盖
        # 对HTML源代码中的每个文本块进行调用
        # 如果处于verbatim模式,保存未修改过的文本;
        # 否则用一系列的替换来处理文本
        self.pieces.append(self.verbatim and text or self.process(text))

    def process(self, text):
        # 从 handle_data 中进行调用
        # 通过执行一系列的正则表达式替换处理文本块(真正的替换定义在子类中)
        for fromPattern, toPattern in self.subs:
            text = re.sub(fromPattern, toPattern, text)
        return text

class ChefDialectizer(Dialectizer):
    """转换HTML为瑞典厨师用语(Swedish Chef-speak)
    
    基于著名的chef.x, 版权所有 (c) 1992, 1993 John Hagerman
    """
    subs = ((r'a([nu])', r'u\1'),
            (r'A([nu])', r'U\1'),
            (r'a\B', r'e'),
            (r'A\B', r'E'),
            (r'en\b', r'ee'),
            (r'\Bew', r'oo'),
            (r'\Be\b', r'e-a'),
            (r'\be', r'i'),
            (r'\bE', r'I'),
            (r'\Bf', r'ff'),
            (r'\Bir', r'ur'),
            (r'(\w*?)i(\w*?)$', r'\1ee\2'),
            (r'\bow', r'oo'),
            (r'\bo', r'oo'),
            (r'\bO', r'Oo'),
            (r'the', r'zee'),
            (r'The', r'Zee'),
            (r'th\b', r't'),
            (r'\Btion', r'shun'),
            (r'\Bu', r'oo'),
            (r'\BU', r'Oo'),
            (r'v', r'f'),
            (r'V', r'F'),
            (r'w', r'w'),
            (r'W', r'W'),
            (r'([a-z])[.]', r'\1.  Bork Bork Bork!'))

class FuddDialectizer(Dialectizer):
    """转换HTML为埃尔默唠叨者用语(Elmer Fudd-speak)"""
    subs = ((r'[rl]', r'w'),
            (r'qu', r'qw'),
            (r'th\b', r'f'),
            (r'th', r'd'),
            (r'n[.]', r'n, uh-hah-hah-hah.'))

class OldeDialectizer(Dialectizer):
    """转换HTML为模仿中世纪英语(mock Middle English)"""
    subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'),
            (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'),
            (r'ick\b', r'yk'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'),
            (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'),
            (r'([aeiou])re\b', r'\1r'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'),
            (r'tion\b', r'cioun'),
            (r'ion\b', r'ioun'),
            (r'aid', r'ayde'),
            (r'ai', r'ey'),
            (r'ay\b', r'y'),
            (r'ay', r'ey'),
            (r'ant', r'aunt'),
            (r'ea', r'ee'),
            (r'oa', r'oo'),
            (r'ue', r'e'),
            (r'oe', r'o'),
            (r'ou', r'ow'),
            (r'ow', r'ou'),
            (r'\bhe', r'hi'),
            (r've\b', r'veth'),
            (r'se\b', r'e'),
            (r"'s\b", r'es'),
            (r'ic\b', r'ick'),
            (r'ics\b', r'icc'),
            (r'ical\b', r'ick'),
            (r'tle\b', r'til'),
            (r'll\b', r'l'),
            (r'ould\b', r'olde'),
            (r'own\b', r'oune'),
            (r'un\b', r'onne'),
            (r'rry\b', r'rye'),
            (r'est\b', r'este'),
            (r'pt\b', r'pte'),
            (r'th\b', r'the'),
            (r'ch\b', r'che'),
            (r'ss\b', r'sse'),
            (r'([wybdp])\b', r'\1e'),
            (r'([rnt])\b', r'\1\1e'),
            (r'from', r'fro'),
            (r'when', r'whan'))

def translate(url, dialectName="chef"):
    """取得URL并且使用dialect进行转换
    
    dialect in ("chef", "fudd", "olde")"""
    import urllib                      
    sock = urllib.urlopen(url)         
    htmlSource = sock.read()           
    sock.close()                       
    parserName = "%sDialectizer" % dialectName.capitalize()
    parserClass = globals()[parserName]                    
    parser = parserClass()                                 
    parser.feed(htmlSource)
    parser.close()         
    return parser.output() 

def test(url):
    """测试对于URL的所有言转换器"""
    for dialect in ("chef", "fudd", "olde"):
        outfile = "%s.html" % dialect
        fsock = open(outfile, "wb")
        fsock.write(translate(url, dialect))
        fsock.close()
        import webbrowser
        webbrowser.open_new(outfile)

if __name__ == "__main__":
    test("http://diveintopython.org/odbchelper_list.html")

例 4.3. dialect.py 的输出

运行这个脚本会将列表 101转换成模仿瑞典厨师用语(mock Swedish Chef-speak) (来自The Muppets), 模仿埃尔默唠叨者用语(mock Elmer Fudd-speak) (来自 Bugs Bunny 卡通画), 和模仿中世纪英语(mock Middle English) (零散地来源于乔叟的《坎特伯雷故事集》). 如果你查看输出页面的HTML源代码,你会发现所有的HTML标记和属性没有改动,但是在标记之间的文本被转换成模仿语言了。如果你观查得更仔细些,你会发现,实际上,仅有标题和段落被转换了;代码列表和屏幕例子没有改动。