我正在尝试在文本文件中设置范围,以将搜索结果与特定章节关联

2 投票
5 回答
599 浏览
提问于 2025-04-16 21:12

我知道有很多更可行的方法来解决这个问题,比如使用mysql、oracle等数据库。不过,我手上有一个mysql数据库文件(KJV圣经),可以通过PHP代码进行搜索。但我想用Python打开Bible.txt文件,搜索特定的字符串,并返回找到的行和行号。此外,对我来说一个挑战是,我还想返回找到的行所在的书名(从一个平面文件中)。我一直在学习Python,想让自己更熟悉这门语言。不幸的是,我的知识和技能还不够,无法有效地解决问题。于是我想到了一个办法:我可以使用range方法来设置章节的开始和结束(代表行号),然后为每本书/章节硬编码一个名字(比如,range(38, 4805)表示这一范围内的所有行都是《创世纪》)。这个方法似乎有效;我只试过几本书。但代码写得很冗长(有很多elif语句)。有没有人知道更高效的方法?下面是我写的一段代码,试了几本书,而KJV.txt文件可以从古腾堡计划获取。

 import os
 import sys
 import re

 word_search = raw_input(r'Enter a word to search: ')
 book = open("KJV.txt", "r")
 regex = re.compile(word_search)
 bibook = ''

 for i, line in enumerate(book.readlines()):
     result = regex.search(line)
     ln = i
     if result:
         if ln in range(36, 4809):
            bibook = 'Genesis'
         elif ln in range(4812, 8859):
            bibook = 'Exodus'
         elif ln in range(8867, 11741):
            bibook =  'Leviticus'
         elif ln in range(11749, 15713):
            bibook = 'Numbers'

         template = "\nLine: {0}\nString: {1}\nBook: {2}\n"
         output = template.format(ln, result.group(), bibook)
         print output

5 个回答

1

避免使用elifs的一个简单方法是用循环。用start <= ln < stop来检查一个数字是否在范围内,这样比用range生成一个列表再逐个比较要高效得多。

import os
import sys
import re


word_search = raw_input(r'Enter a word to search: ')
book = open("KJV.txt", "r")
regex = re.compile(word_search)
bibook = ''

bookranges = [
    ((36, 4809),  'Genesis'),
    ((4812, 8859), 'Exodus'),
    ((8867, 11741), 'Leviticus'),
    ((11749, 15713), 'Numbers')
]


for ln, line in enumerate(book.readlines()):
    result = regex.search(line)
    if result:
        for (start, stop), bibook in bookranges:
            if start <= ln <= stop:
                # found the book, so end the loop and use it later
                break
        else:
            # didnt find any range that matches.
            bibook = 'Somewhere between books'

     template = "\nLine: {0}\nString: {1}\nBook: {2}\n"
     output = template.format(ln, result.group(), bibook)
     print output
1

你可以试试这样做。注意,书本是一个接一个出现的,所以你只需要记录你当前在看的那本书是什么。此外,你用来检查行号是否在一个范围里的方法其实很耗费资源,因为对于文本文件中的每一行,你都要构建每个范围,然后再逐行检查行号是否在里面。

books = [("Introduction",36),("Genesis",4809),("Exodus",8859),
         ("Leviticus",11741),("Numbers",15713)]

import os
import sys
import re

word_search = raw_input(r'Enter a word to search: ')
book = open("KJV.txt", "r")
bookIndex = 0
bookEnd = books[bookIndex][1]

for lineNum, line in enumerate(book):
    if lineNum > bookEnd:
        bookIndex += 1
        bookEnd = books[bookIndex][1]
    if word_search in line:
        template = "\nLine: {0}\nString: {1}\nBook: {2}\n"
        output = template.format(lineNum, line, books[bookIndex][0])
        print output

有一个评论提到,你可以尝试一种更数据驱动的方法,而不是死记硬背书的位置。每本书的开头是否有一行或多行是容易识别的格式?如果是这样,你可以试着检查这些格式,并记录你当前在看的书是哪一本。

2

这是一个很好的开始。不过,我有一些建议。

首先,你使用的 readlines 有点不够高效。readlines 会把文件的所有行都读到一个新的列表里,这样会占用很多内存。但其实你不需要这样做;如果你只是想逐行读取文件,可以直接用 for line in file,在你的例子中可以这样写:

for i, line in enumerate(book):

另外,如果你确实想把文件内容存到内存中,可能是为了多次查找,可以把 readlines 的结果保存到一个变量里:

booklines = book.readlines()
for i, line in enumerate(booklines):

你也可以用 read 把文本作为一个完整的字符串存储,不过在这种情况下用处不大,因为你还得把它分开:

booktxt = book.read()
booklines = book.splitlines() #
for i, line in enumerate(booklines)

其次,我建议不要用 i 作为索引变量,然后再单独保存到 ln,不如一开始就用一个有意义的变量名。ln 还不错,line_number 更清楚但有点啰嗦,lineno 是个不错的折中方案。这里我们就用 ln,因为大家都知道它的意思。

for ln, line in enumerate(book):

第三,正如 utdemir 在评论中提到的,你其实不需要用正则表达式。可能如果你想让用户能输入更复杂的搜索条件,这样做有意义,但正则表达式比较复杂,作为默认的用户界面不太合适。我建议用 in 来做简单的子字符串匹配,比如:

    if word_search in line: 

剩下的 if 语句没问题,在某些情况下这样做是最好的。不过,很多情况下,如果需要用到 case 语句,其实用字典会更好。当然,这里你有范围,所以我们得聪明一点。

我们先来定义一个起始页的字典。显然,这个字典应该放在循环之前,这样我们就不会每次循环都重新定义它。

first_lines = {36: 'Genesis', 4812: 'Exodus', 8867: 'Leviticus', 11749: 'Numbers'}

现在我们需要把 ln 映射到这个字典的某个值上。但很可能 ln 并不等于上面任何一个数字,所以我们不能直接把它放进字典。我们 可以 用一个 for 循环来遍历字典的键(for key in first_lines),把前一个键存到 prev_key,测试 ln > key,如果成立,就返回 prev_key。但其实在 Python 中有更好的方法。我们可以用 filter 函数或者列表推导式来过滤掉大于 ln 的值,然后找到 max

first_line = max(filter(lambda l: l < ln, first_lines))

这里 first_lines 就像一个无序的键列表;一般来说,你可以像遍历列表一样遍历字典的键,但要注意,键的顺序可能不一样。lambda 是一种定义短函数的方法:这个函数接受 x 作为参数,并返回 x < ln 的结果。我们得这样做,因为 filter 的第一个参数需要是一个函数。它会返回一个列表,包含所有在 first_lines 中返回 True 的值。

因为这可能有点难以理解,尤其是涉及到 lambda 时,我们用列表推导式可能会更好。列表推导式对大多数人来说都很易读和直观。

first_line = max([l for l in first_lines if l < ln])

在这种情况下,我们甚至可以省略括号,因为我们是直接把它传给一个函数。Python 会把它解释为一种叫做“生成器表达式”的东西,类似于列表推导式,但它是动态计算值,而不是一开始就把它们存储在列表里。

first_line = max(l for l in first_lines if l < ln)

现在要获取书名,你只需用 first_line 作为键:

bibook = first_lines[first_line]

最终结果:

import os
import sys
import re

word_search = raw_input(r'Enter a word to search: ')
book = open("KJV.txt", "r")
first_lines = {36: 'Genesis', 4812: 'Exodus', 8867: 'Leviticus', 11749: 'Numbers'}

for ln, line in enumerate(book):
    if word_search in line:
        first_line = max(l for l in first_lines if l < ln)
        bibook = first_lines[first_line]

        template = "\nLine: {0}\nString: {1}\nBook: {2}\n"
        output = template.format(ln, word_search, bibook)
        print output

撰写回答