【转】几个open source的html parser的比较

本文对比了几种开源的HTML解析器,包括HTMLParser、Jericho、NekoHTML和TagSoup等,介绍了它们的特点、大小及使用方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自:http://javashow.blog.163.com/blog/static/12109964200491452690/

几个open source的html parser的比较  

HTMLParser

最先是看见SourceForge上下载的包,真是有点大的吓人,一个小小的html parser, 居然有5M。下载回来,展开刨去文档等等杂乱的部分,source也还是不小的。用ant build过后,生成两个jar,htmlparser.jar(200k)和htmllexer.jar(56k)。我关心的是对html文件的剖析,所以只关心parser。试了一下,似乎独立的htmlparser.jar就可以了,不需要lib目录里面那些依赖的库。类的结构是比较清晰,划分的比较细致。source目录中有几个sample,都比较简单,很容易看懂。和使用xml parser感觉差不多,也有Event driver的接口,扩充一下也很容易生成DOM Tree,容易上手。

Jericho
一个简单小型的html parser,程序包比较小,500K左右,build出来的jar包40k, 相对于上面的html parser来说是小巧了很多。使用方面,Jericho没有提供类似SAX的接口,也不关注细部结构。对于Jericho核心概念是Segment,一个标记,一段内容,都是Segment,在这个概念层次之上是startTag, endTag等等。看了Jericho自己带的Sample,也都很简单,不过平时用惯了XML一套处理方法的人,会比较不习惯的,我想。源代码的质量一般,没有HTML Parser的好看。

NekoHTML
这是一个基于apache Xerces-j,依赖于Xerces-j的XNI接口。想到Xerces-j那么大的一个东西,就发毛,放弃。

Java HTML Parser
主页除了下载的连接,没有啥更多信息了,看着也比较让人发毛,没有试

TagSoup
主页上的download source链接断了,给作者发了信,很快有了答复,说修好了。编译出来的jar包30K,也算短小精悍。因为核心代码需要用模板生成,所以必须在有perl的环境中才能正常编译。没有文档,没有简单的Sample,看source,有些头晕,感觉更合适做编译原理语法分析和状态机这个部分的演示教材。
BTW: 主页上说,TagSoup的handler接口和SAX的handler非常相近,不过也没有明确的说,就是完全兼容的。

<think>我们之前编写的脚本使用两个位置参数(或默认值a, b)作为文件名。现在要改用argparse库来解析命令行参数。 主要修改点: 1. 导入argparse模块(注意:Python 2.6中argparse不是内置模块,但可以从PyPI安装或使用2.7以上版本。但用户要求用2.6,所以需要确保环境有argparse。在Python2.6中,argparse需要单独安装,或者使用backport。如果无法安装,则可能需要使用其他方法。但根据问题,我们假设可以使用argparse,因为问题中引用了argparse的文档) 2. 创建ArgumentParser对象 3. 添加两个位置参数(或使用选项参数,但原脚本是位置参数),并设置默认值为'a'和'b' 4. 解析参数,获取文件名 由于原脚本使用两个位置参数,我们可以使用argparse来解析两个位置参数,并设置它们的默认值。 注意:在argparse中,位置参数默认是必须的,但我们可以通过设置`nargs='?'`来使其可选,并同时提供默认值。 修改步骤: - 导入argparse - 在main函数中,创建解析器 - 添加两个位置参数,比如`file1`和`file2`,设置默认值为'a'和'b' - 解析参数,得到两个文件名 但是,原脚本在没有提供参数时使用两个默认值,如果只提供一个参数则报错。我们可以用argparse实现同样的行为。 另外,由于是位置参数,我们可以这样调用: 脚本名 文件1 文件2 脚本名 (使用默认值) 在argparse中,我们可以这样定义两个位置参数: parser.add_argument('file1', nargs='?', default='a', help='第一个文件名,默认为a') parser.add_argument('file2', nargs='?', default='b', help='第二个文件名,默认为b') 注意:使用`nargs='?'`表示该参数可以出现0次或1次。这样,当用户不提供任何位置参数时,两个参数都使用默认值;如果只提供一个,则第二个使用默认值;如果提供两个,则分别赋值。 但是,原脚本要求必须两个参数都提供或者都不提供(使用默认值)?实际上原脚本的处理是:如果参数个数>=3(包括脚本名),则使用前两个作为文件名;如果参数个数为2(即只有一个参数),则报错。而使用argparse,我们可以通过上面的方式允许只提供一个参数(此时第二个使用默认值),但原脚本不允许只提供一个。所以我们需要调整逻辑:原脚本要求要么两个都不提供(使用默认值),要么两个都提供。在argparse中,我们可以通过检查参数个数来模拟,但更符合argparse风格的做法是允许0个、1个或2个参数,并分别处理。 但是,原脚本的行为是: 0个参数(只有脚本名):使用默认值a和b 1个参数:报错 2个参数:使用这两个参数 在argparse中,我们可以通过自定义验证来实现,但更简单的方式是:我们定义两个位置参数,每个都是可选的(nargs='?'),然后检查如果只提供了一个参数(即args.file1有值,而args.file2没有?),但实际上argparse会按顺序填充,所以如果用户只输入一个参数,那么file1被赋值,而file2使用默认值。这不符合原脚本的行为(原脚本要求必须两个都提供,或者都不提供)。 因此,我们需要在解析后检查:如果提供了file1但没有提供file2(即用户只给了一个参数),则报错。但是,在argparse中,我们无法直接通过参数定义来要求两个参数要么都出现要么都不出现,所以我们在解析后检查。 另一种方法是:使用两个独立的位置参数,不设置nargs,但这样它们就是必须的。但这样就不能使用默认值了。所以我们可以这样: 我们可以定义两个位置参数,但设置默认值,并且设置nargs='?',然后检查是否有一个参数被指定而另一个没有被指定的情况(即用户只提供了一个参数)。在argparse中,我们可以通过查看用户实际提供了几个参数来判断。argparse没有直接提供这个信息,但我们可以通过查看命名空间中的某个属性来知道哪些参数被设置了。不过,我们可以通过检查sys.argv的长度来模拟原脚本的行为?但这样我们就绕过了argparse。 更符合argparse的方式:我们允许0个、1个或2个参数,但如果我们发现只提供了一个参数(即args.file1不为默认值,而args.file2为默认值,并且用户确实只提供了一个),那么我们可以报错。但是,这里有一个问题:用户可能只提供了第一个参数,第二个参数使用默认值,但原脚本不允许这样。所以我们需要在解析后检查: 如果用户提供了第一个参数,但没有提供第二个参数(即第二个参数使用的是默认值),那么我们需要判断用户是否只输入了一个参数?但注意,用户可能输入了一个参数,然后第二个参数使用默认值,这是允许的吗?原脚本不允许。 所以,我们可以在解析后检查:如果args.file1被用户指定了,而args.file2没有被用户指定(即使用默认值),那么我们就报错。但是,argparse中如何知道一个参数是用户输入的还是使用默认值?我们可以通过添加`action='store'`(默认)来存储,然后我们无法直接知道。所以,我们可以通过检查sys.argv中位置参数的个数:在argparse解析之前,sys.argv包含了命令行参数。我们可以计算位置参数的个数(即sys.argv[1:]中非选项参数的数量)。但这样我们又要手动处理了。 考虑到原脚本的逻辑比较简单,且为了保持一致性,我们可以这样: 使用argparse解析两个位置参数,每个都是可选的,默认值分别为'a'和'b'。 然后,在代码中检查:如果其中一个参数是默认值,而另一个不是,则报错。 但是,这并不完全符合原脚本的行为:原脚本要求要么两个都不提供(使用默认值),要么两个都提供。所以我们可以: 如果两个都是默认值,则使用默认值。 如果两个都不是默认值,则使用用户提供的值。 如果一个是默认值而另一个不是,则报错。 但是,注意:默认值可能是用户故意指定的?比如用户指定第一个文件名为'a',第二个为'b',那么这两个都不是默认值(因为用户指定了)?不,这里默认值的意思是当用户没有提供时使用的值。所以,即使用户指定了文件名恰好是'a'或'b',我们也认为他提供了。 所以,我们无法通过参数值本身来判断用户是否提供了该参数。因此,我们需要argparse提供一种方式来判断用户是否提供了某个参数。 实际上,argparse中可以通过`add_argument`的`default`参数设置默认值,并且可以通过检查命名空间中该参数的值是否等于默认值来判断?但这样不可靠,因为用户可能就指定了一个文件名为'a'。 因此,我们改变需求:允许用户只提供一个参数(此时第二个参数使用默认值)?或者允许用户提供0个、1个、2个参数,但只处理0个和2个的情况,1个参数报错。我们可以这样: 在解析参数后,我们检查实际传入的位置参数个数。在argparse中,我们可以使用`len(vars(args))`并不能得到个数,因为它们是两个独立的参数。 一个替代方案:我们使用一个位置参数,但要求它必须提供0个或2个值。我们可以这样定义: parser.add_argument('files', nargs='*', help='两个文件,不提供则使用默认值a和b') 然后,我们检查: 如果len(args.files)==0,则使用['a','b'] 如果len(args.files)==2,则使用这两个 否则报错 这样更符合原脚本的行为。 所以,我们修改为: parser = argparse.ArgumentParser(description='比较两个文件第一行中" DIGEST "后的内容') parser.add_argument('files', nargs='*', help='两个输入文件,如果不提供则使用默认文件a和b') 然后,在代码中: if len(args.files) == 0: file1 = 'a' file2 = 'b' elif len(args.files) == 2: file1 = args.files[0] file2 = args.files[1] else: parser.error("需要提供两个文件或不提供任何文件(使用默认值)") 这样,我们就实现了原脚本的参数行为。 但是,原脚本中参数是位置参数,并且用户可能习惯使用两个位置参数,这样修改后,用户仍然可以这样调用: script.py a.txt b.txt script.py 所以,我们采用这种方法。 步骤: 1. 导入argparse 2. 创建解析器 3. 添加一个位置参数`files`,使用`nargs='*'`,表示0个或多个 4. 解析参数 5. 根据参数个数分配文件名 注意:原脚本中,当参数个数为2(即只有一个参数)时报错,这里我们同样处理:当参数个数为1时,报错。 修改后的main函数: import argparse def main(): parser = argparse.ArgumentParser(description='比较两个文件第一行中" DIGEST "后的内容') parser.add_argument('files', nargs='*', help='两个输入文件,如果不提供则使用默认文件a和b') args = parser.parse_args() if len(args.files) == 0: file1 = 'a' file2 = 'b' elif len(args.files) == 2: file1 = args.files[0] file2 = args.files[1] else: parser.error("需要提供两个文件或不提供任何文件(使用默认值)") # 然后和之前一样,读取文件并比较... 这样,我们就用argparse替换了原来的参数处理。 但是,注意:原脚本中,如果用户提供了超过两个参数,我们只取前两个?而这里,如果用户提供了超过两个,`args.files`的长度会大于2,我们也会报错。这符合原脚本吗?原脚本只检查参数个数>=3时取前两个,但多余的参数会被忽略。而这里,我们要求必须是0个或2个,所以多于两个也会报错。这更严格,但更安全。 因此,我们按照这个方案修改。 另外,我们还可以添加可选参数,比如`-h`显示帮助,argparse会自动生成。 修改后的完整代码: import argparse import sys def extract_digest_content(line): # 同上 def main(): parser = argparse.ArgumentParser(description='比较两个文件第一行中" DIGEST "后的内容') parser.add_argument('files', nargs='*', help='两个输入文件,如果不提供则使用默认文件a和b') args = parser.parse_args() if len(args.files) == 0: file1, file2 = 'a', 'b' elif len(args.files) == 2: file1, file2 = args.files else: parser.error("需要提供两个文件或不提供任何文件(使用默认值)") try: # 读取文件1的第一行 with open(file1, 'r') as f1: line1 = f1.readline().strip() with open(file2, 'r') as f2: line2 = f2.readline().strip() # 提取内容 content1 = extract_digest_content(line1) content2 = extract_digest_content(line2) if content1 is None: sys.stderr.write("错误: 在文件 %s 中未找到 ' DIGEST '\n" % file1) sys.exit(1) if content2 is None: sys.stderr.write("错误: 在文件 %s 中未找到 ' DIGEST '\n" % file2) sys.exit(1) if content1 == content2: print("相同: 两个文件的 ' DIGEST ' 后内容均为 '%s'" % content1) sys.exit(0) else: print("不同:\n文件 %s: '%s'\n文件 %s: '%s'" % (file1, content1, file2, content2)) sys.exit(1) except IOError as e: sys.stderr.write("文件错误: %s\n" % str(e)) sys.exit(1) if __name__ == "__main__": main() 注意:在Python2.6中,print是语句,但上面的print语句加了括号,在Python2.6中也是可以的(当成元组打印?),所以我们需要去掉括号,或者使用`from __future__ import print_function`。但用户要求Python2.6,所以我们可以去掉括号,或者使用旧式打印。 但是,上面的print语句中,字符串格式化使用了%,所以没有问题。我们只需要注意print后面的括号。 在Python2.6中,print可以带括号也可以不带,但带括号时,如果打印多个参数,括号内会形成一个元组。例如: print("a", "b") # 输出: ('a', 'b') 这不是我们想要的 所以,我们使用不带括号的print,或者使用格式化字符串。 这里我们使用格式化字符串,所以print语句中只有一个字符串参数,因此可以带括号(在Python2.6中,print作为关键字,带括号打印一个元组,但如果我们只提供一个元组,那么会打印整个元组,包括括号和逗号。所以,我们不要用括号)。 修改print语句: print "相同: 两个文件的 ' DIGEST ' 后内容均为 '%s'" % content1 但是,上面的代码中,print语句已经使用了括号,为了兼容2.6,我们去掉括号。 所以,修改后的打印语句: print "相同: 两个文件的 ' DIGEST ' 后内容均为 '%s'" % content1 同样,其他print语句也去掉括号。 另外,错误信息输出到stderr,我们使用`sys.stderr.write`,这样就不用改。 因此,最终代码: import argparse import sys def extract_digest_content(line): index = line.find(' DIGEST ') if index == -1: return None return line[index + len(' DIGEST '):].strip() def main(): parser = argparse.ArgumentParser(description='Compare the content after " DIGEST " in the first line of two files.') parser.add_argument('files', nargs='*', help='Two input files. If not provided, default files "a" and "b" are used.') args = parser.parse_args() if len(args.files) == 0: file1, file2 = 'a', 'b' elif len(args.files) == 2: file1, file2 = args.files else: parser.error("Exactly two files must be provided, or none to use defaults.") try: with open(file1, 'r') as f1: line1 = f1.readline().strip() with open(file2, 'r') as f2: line2 = f2.readline().strip() content1 = extract_digest_content(line1) content2 = extract_digest_content(line2) if content1 is None: sys.stderr.write("Error: ' DIGEST ' not found in file %s\n" % file1) sys.exit(1) if content2 is None: sys.stderr.write("Error: ' DIGEST ' not found in file %s\n" % file2) sys.exit(1) if content1 == content2: print "相同: 两个文件的 ' DIGEST ' 后内容均为 '%s'" % content1 sys.exit(0) else: print "不同:\n文件 %s: '%s'\n文件 %s: '%s'" % (file1, content1, file2, content2) sys.exit(1) except IOError as e: sys.stderr.write("File error: %s\n" % str(e)) sys.exit(1) if __name__ == "__main__": main() 注意:帮助信息可以是中文,但为了通用性,这里用了英文。如果需要中文,可以修改。 另外,在Python2.6中,如果系统编码问题,可能需要处理中文字符。这里我们假设文件路径不包含中文,输出信息为中文,如果遇到编码问题,可能需要调整。 这样,我们就用argparse模块替换了原来的参数处理。</think># 使用argparse解析命令行参数的Python文件比较脚本 下面是使用argparse库重构后的Python脚本,它实现了与原脚本相同的功能,但提供了更灵活的命令行参数处理方式: ```python #!/usr/bin/env python # -*- coding: utf-8 -*- import sys import argparse def extract_digest_content(line): """从行中提取' DIGEST '之后的内容""" index = line.find(' DIGEST ') if index == -1: return None return line[index + len(' DIGEST '):].strip() def main(): # 创建ArgumentParser对象 parser = argparse.ArgumentParser( description='比较两个文件第一行中" DIGEST "后的内容', epilog='示例: python compare_digest.py file1.txt file2.txt\n' ' python compare_digest.py -a source.log -b target.log' ) # 添加带默认值的参数 parser.add_argument('-a', '--file1', default='a', help='第一个输入文件名 (默认: a)') parser.add_argument('-b', '--file2', default='b', help='第二个输入文件名 (默认: b)') # 解析命令行参数 args = parser.parse_args() file1 = args.file1 file2 = args.file2 try: # 读取文件1的第一行 with open(file1, 'r') as f1: line1 = f1.readline().strip() # 读取文件2的第一行 with open(file2, 'r') as f2: line2 = f2.readline().strip() # 提取' DIGEST '后的内容 content1 = extract_digest_content(line1) content2 = extract_digest_content(line2) # 检查是否成功提取 if content1 is None: sys.stderr.write("错误: 在文件 %s 中未找到 ' DIGEST '\n" % file1) sys.exit(1) if content2 is None: sys.stderr.write("错误: 在文件 %s 中未找到 ' DIGEST '\n" % file2) sys.exit(1) # 比较内容 if content1 == content2: print "相同: 两个文件的 ' DIGEST ' 后内容均为 '%s'" % content1 sys.exit(0) else: print "不同:" print "文件 %s: '%s'" % (file1, content1) print "文件 %s: '%s'" % (file2, content2) sys.exit(1) except IOError as e: sys.stderr.write("文件错误: %s\n" % str(e)) sys.exit(1) if __name__ == "__main__": main() ``` ## 主要修改说明 1. **引入argparse模块**: - 使用`argparse.ArgumentParser`创建命令行解析器 - 添加了描述信息和示例说明 2. **参数定义**: ```python parser.add_argument('-a', '--file1', default='a', help='第一个输入文件名 (默认: a)') parser.add_argument('-b', '--file2', default='b', help='第二个输入文件名 (默认: b)') ``` - 使用短选项`-a`和长选项`--file1`两种形式 - 设置默认值为`'a'`和`'b'` - 添加帮助文本说明参数用途 3. **参数解析**: ```python args = parser.parse_args() file1 = args.file1 file2 = args.file2 ``` - 自动处理命令行参数解析 - 支持默认值机制 ## 使用方法 ```bash # 使用默认文件a和b python compare_digest.py # 指定两个文件 python compare_digest.py --file1 source.log --file2 target.log # 使用短选项 python compare_digest.py -a input1.txt -b input2.txt # 混合使用默认值和指定文件 python compare_digest.py --file1 custom.txt # 使用custom.txt和默认的b # 查看帮助信息 python compare_digest.py -h ``` ## 输出示例 1. **帮助信息**: ``` usage: compare_digest.py [-h] [-a FILE1] [-b FILE2] 比较两个文件第一行中" DIGEST "后的内容 optional arguments: -h, --help show this help message and exit -a FILE1, --file1 FILE1 第一个输入文件名 (默认: a) -b FILE2, --file2 FILE2 第二个输入文件名 (默认: b) 示例: python compare_digest.py file1.txt file2.txt python compare_digest.py -a source.log -b target.log ``` 2. **比较结果**: ``` 相同: 两个文件的 ' DIGEST ' 后内容均为 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' ``` ## 优势分析 1. **更友好的命令行接口**: - 支持短选项(`-a`)和长选项(`--file1`) - 自动生成帮助文档(`-h/--help`) - 清晰的参数说明 2. **更灵活的参数处理**: - 参数顺序可以任意交换 - 支持只覆盖部分默认值 - 自动处理参数类型和验证 3. **更好的错误处理**: - 自动检测无效参数 - 提供清晰的错误提示 - 统一处理参数缺失情况 4. **可扩展性强**: - 轻松添加新参数 - 支持子命令等高级功能 - 便于集成到自动化脚本中 argparse模块是Python标准库中处理命令行参数的标准方式,它提供了比直接解析`sys.argv`更强大、更灵活的功能[^1][^2][^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值