使用Python在.txt文件中搜索单词或短语(并显示上下文)

3 投票
2 回答
11266 浏览
提问于 2025-04-15 23:45

基本上,问题就是这样。我对Python还比较陌生,喜欢通过看和做来学习。

我想创建一个脚本,可以在一个文本文件中搜索特定的单词或短语(比如从新闻文章中复制粘贴的文本)。理想情况下,这些单词和短语的列表应该存放在一个单独的文件里。

在得到搜索结果时,能够看到结果的上下文会很棒。比如,可以打印出每个找到的搜索词前后各50个字符的内容。如果还能显示出搜索词所在的行数,那就更好了。

如果能给我一些关于如何编写这个脚本的建议,或者提供一些代码示例,我会非常感激。

2 个回答

3

可以从这样的代码开始。这个代码并不是你需求的完美解决方案,但它是一个不错的起点。

import sys

words = "foo bar baz frob"

word_set = set(words.split())
for line_number, line in enumerate(open(sys.argv[1])):
    if words_set.intersection(line.split()):
        print "%d:%s" % (line_number, line.strip())

下面是一些解释:

  • 我们要找的单词最开始是存储在一个字符串里的(在第3行)。我把这个单词列表按空格分开,然后创建一个集合,这样可以更方便地检查当前行中的单词是否在这个单词列表里。因为在集合中检查一个元素是否存在是非常快的(O(1)),而在列表中则比较慢(O(n))。

  • 在主要的循环中,我打开输入文件(这个文件名是通过命令行参数传入的),并使用 enumerate 这个内置方法来获取行号和实际的行内容。sys.argv 是一个存储命令行参数的数组;sys.argv[0] 始终是Python脚本的名称。

  • 在循环内部,我获取当前行,把它分割成单个单词,然后再次创建一个单词集合。接着,我可以快速找到当前行的单词集合和我想找的单词集合的交集。如果这个交集有值(也就是说,它不是空的),我就打印出行号和这一行的内容。

还有一些问题没有解决(留给你自己去做):

  • 现在单词列表是硬编码在代码里的,但你可以很容易地打开一个额外的文件(比如通过 sys.argv[2] 传入文件名),逐行读取单词并存储到一个集合中。注意,你可以通过 addupdate 方法来扩展集合(而不是用 appendextend,这两个是用在列表上的)。

  • 显然,如果你要找的是短语而不是单词,上面的办法就不适用了(正如某个评论中提到的)。我假设你是想学习,而不需要一个完美的解决方案,所以我只想说,如果你有短语在集合中,你可以通过 any(phrase in line for phrase in set_of_phrases) 来检查当前行中是否有任何短语。这可以替代集合的交集(当然在这种情况下不要把行分割成单词)。

  • 如果你想打印出匹配的上下文,可以使用两个额外的变量(比如 prev_linenext_line)来存储前一行和下一行。在循环中,你实际上会读取 next_line 而不是 line,在循环结束时,你需要把 line 复制到 prev_line,并把 next_line 复制到 line

  • 还有一种更“Pythonic”的方法来跟踪前一行和下一行,就是创建一个Python生成器函数,它会为每个给定的可迭代对象(比如文件)返回一个包含前一项、当前项和下一项的元组。这种方法比较高级,因为你对Python还比较陌生,所以我建议你先不去碰这个。不过,如果你感兴趣,做这个任务的生成器函数可能看起来像这样:

    def context_generator(iterable):
        prev, current, next = None, None, None
        for element in iterable:
            prev, current, next = current, next, element
            if current is not None:
                yield prev, current, next
        if next is not None:
            yield current, next, None
    
7

尽管在Python社区中,很多人对正则表达式(Regular Expressions)有些反感,但其实在合适的情况下,它们是非常有用的工具。比如说,正则表达式可以帮助我们识别单词和短语,这得益于它里面的 \b “单词边界”元素。相比之下,基于字符串处理的方法就麻烦多了,比如 .split() 方法是用空格来分隔的,这样就会把标点符号留在相邻的单词上,真让人烦。

如果你觉得正则表达式还不错,我推荐你可以用类似这样的方式:

import re
import sys

def main():
  if len(sys.argv) != 3:
    print("Usage: %s fileofstufftofind filetofinditin" % sys.argv[0])
    sys.exit(1)

  with open(sys.argv[1]) as f:
    patterns = [r'\b%s\b' % re.escape(s.strip()) for s in f]
  there = re.compile('|'.join(patterns))

  with open(sys.argv[2]) as f:
    for i, s in enumerate(f):
      if there.search(s):
        print("Line %s: %r" % (i, s))

main()

第一个参数是一个文本文件的路径,这个文件里每行写一个你想找的单词或短语;第二个参数是另一个文本文件的路径,在这个文件里寻找这些单词。你还可以很简单地让搜索不区分大小写(也许可以通过命令行选项来选择),等等。

对于不熟悉正则表达式的读者,简单解释一下……:

patterns 中的 \b 确保了不会出现意外的匹配。如果你在找“cat”或“dog”,那么“catalog”或“underdog”就不会被误匹配;同时,在句子“猫在微笑着跑开了”中,你也不会因为分割而错过“cat”这个词,因为它旁边有个逗号;-)

| 代表“或”,比如说,如果你的文本文件内容是(两行)

cat
dog

那么这会形成模式 '\bcat\b|\bdog\b',这样就能找到“cat”或“dog”(作为独立的单词,忽略标点,但不会在更长的单词中找到)。

最后,re.escape 用来转义标点符号,这样它们就会被当作普通字符来匹配,而不是在正则表达式中有特殊含义。

撰写回答