我正在尝试在文本文件中设置范围,以将搜索结果与特定章节关联
我知道有很多更可行的方法来解决这个问题,比如使用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 个回答
避免使用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
你可以试试这样做。注意,书本是一个接一个出现的,所以你只需要记录你当前在看的那本书是什么。此外,你用来检查行号是否在一个范围
里的方法其实很耗费资源,因为对于文本文件中的每一行,你都要构建每个范围,然后再逐行检查行号是否在里面。
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
有一个评论提到,你可以尝试一种更数据驱动的方法,而不是死记硬背书的位置。每本书的开头是否有一行或多行是容易识别的格式?如果是这样,你可以试着检查这些格式,并记录你当前在看的书是哪一本。
这是一个很好的开始。不过,我有一些建议。
首先,你使用的 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