前言
此专栏记录信息检索课程的学习。
部分代码框架来自温柔的助教小哥哥。
Talk is cheap.
一、对文本进行分词
使用了NLTK工具
def get_words(text):
text = text.lower() # 全部字符转为小写
words = nltk.word_tokenize(text) # 分词
return words
二、获取文本文件
给定文本文件目录,获取目录下所有符合要求的文件列表
def get_files(dir, file_type='.txt'):
file_list = []
for home, dirs, files in os.walk(dir):
for filename in files:
if file_type in filename:
file_list.append(os.path.join(home, filename))
return file_list
三、词法分析
通过正则表达式对查询进行词法分析
# 构造每种类型词的正则表达式,()代表分组,?P<NAME>为组命名
token_or = r'(?P<OR>\|\|)'
token_not = r'(?P<NOT>\!)'
token_word = r'(?P<WORD>[a-zA-Z]+)'
token_and = r'(?P<AND>&&)'
token_lp = r'(?P<LP>\()'
token_rp = r'(?P<RP>\))'
token_dp = r'(?P<DP>\")'
lexer = re.compile('|'.join([token_or, token_not, token_word,token_and, token_lp, token_rp, token_dp]))
# 编译正则表达式
# 用编译好的正则表达式进行词法分析
def get_tokens(query):
tokens = [] # tokens中的元素类型为(token, token类型)
for token in re.finditer(lexer, query):
tokens.append((token.group(), token.lastgroup))
return tokens
四、布尔检索类
加入了位置索引与双词索引
class BoolRetrieval:
"""
布尔检索类
dic = {'hello':{1:[1,3,5], 3:[5,6]}, 'world':{4:[2,4]}}
"""
def __init__(self, index_path=''):
if index_path == '':
self.index = defaultdict(dict)
# 已有构建好的索引文件
else:
data = np.load(index_path, allow_pickle=True)
self.files = data['files'][()]
self.index = data['index'][()]
self.query_tokens = []
def build_index(self, text_dir):
self.files = get_files(text_dir) # 获取所有文件名
for num in range(0, len(self.files)):
f = open(self.files[num])
text = f.read()
words = get_words(text) # 分词
print(words)
# 构建倒排索引
# for word in words:
# self.index[word].append(num)
for pos_num in range(0, len(words)):
word = words[pos_num]
if num not in (self.index[word]).keys():
self.index[word][num] = []
self.index[word][num].append(pos_num)
# print(self.files, self.index)
self.add_phrase('phrase.txt') #插入双词索引
np.savez('index_2.npz', files=self.files, index=self.index)
# 在构建索引之后, 加入短语的索引
def add_phrase(self, phrase_path):
f = open(phrase_path)
text = f.read()
phrases = text.split('\n')
for i in phrases:
self.query_tokens = get_tokens("\""+ i + "\"") # 获取查询的tokens, [(a,word), (b, word)]
for num in self.evaluate(0, len(self.query_tokens) - 1):
self.index[i][num] = [] # 短语的索引 位置记录为空
def search(self, query):
self.query_tokens = get_tokens(query) # 获取查询的tokens, [(a,word), (||, or), (b, word)]
print(self.query_tokens)
result = []
# 将查询得到的文件ID转换成文件名
for num in self.evaluate(0, len(self.query_tokens) - 1):
result.append(self.files[num])
return result
# 递归解析布尔表达式,p、q为子表达式左右边界的下标
def evaluate(self, p, q):
# 解析错误
if p > q:
return []
# 单个token,一定为查询词
elif p == q:
return self.index[self.query_tokens[p][0]].keys()
# 去掉外层括号
elif self.check_parentheses(p, q):
return self.evaluate(p + 1, q - 1)
#被双引号包围的短语
elif self.check_quotation (p, q):
return self.phrase_search(p + 1, q - 1)
else:
op = self.find_operator(p, q)
if op == -1:
return []
# files1为运算符左边得到的结果,files2为右边
if self.query_tokens[op][1] == 'NOT':
files1 = []
else:
files1 = self.evaluate(p, op - 1)
files2 = self.evaluate(op + 1, q)
return self.merge(files1, files2, self.query_tokens[op][1])
# 判断表达式是否为 (expr)
def check_parentheses(self, p, q):
"""
判断表达式是否为 (expr)
整个表达式的左右括号必须匹配才为合法的表达式
返回True或False
"""
if self.query_tokens[p][1] == 'LP' and self.query_tokens[q][1] == 'RP':
rp = 0 # 记录括号
for i in range(p+1, q):
if self.query_tokens[i][1] == 'LP':
rp += 1
elif self.query_tokens[i][1] == 'RP':
rp -= 1
if rp < 0:
return False
if rp == 0:
return True
return False
# 被双引号包围的短语,内部不能出现引号及运算符
def check_quotation(self, p, q):
if self.query_tokens[p][1] == 'DP' and self.query_tokens[q][1] == 'DP':
for i in range(p+1, q):
if self.query_tokens[i][1] in ["DP", "&&", "||", "!"]:
return False
return True
return False
#短语查询: 先找出files 包含短语里的每个词, 然后遍历files 及 每个词
#双词索引查找结果 与 位置索引查找结果 比较
def phrase_search(self, p, q):
# 双词索引查找
result = []
phrase = self.query_tokens[p][0]
for i in range(p+1 , q+1):
phrase += " "
phrase += self.query_tokens[i][0]
if phrase in self.index.keys():
result = list(self.index[phrase].keys())
return result #若短语存在索引中,直接返回
#位置索引查找
files = self.evaluate(p, p) #短语可能存在的文件
for i in range(p+1, q+1):
files = self.merge(files, self.evaluate(i,i), 'AND')
for i in files:
pos_list = self.index[self.query_tokens[p][0]][i] # p 词在文件 i 中的位置
for j in range(p+1, q+1):
pos_list = [pos+1 for pos in pos_list] # 如果是短语,后面的词的位置应该 +1
true_pos_list = self.index[self.query_tokens[j][0]][i] # 真实的位置
pos_list = self.merge(pos_list, true_pos_list, 'AND') # 取交集
if not pos_list == []:
result.append(i)
return result
# 寻找表达式的dominant的运算符(优先级最低)
def find_operator(self, p, q):
rp = 0 # 记录括号
index = -1 #记录位置,若没有运算符,返回-1
i = p
tem = 0 # 记录op
op_dic = {'!':1, '&&':2, '||':3} #比较优先级
while i <= q:
if self.query_tokens[i][1] == 'RP':
rp += 1
elif self.query_tokens[i][1] == 'LP':
rp -= 1
elif rp == 0 and self.query_tokens[i][1] in ['AND', "OR", 'NOT'] :
if tem <= op_dic[self.query_tokens[i][0]]:
tem = op_dic[self.query_tokens[i][0]]
index = i
i += 1
# print(index)
return index
def merge(self, files1, files2, op_type):
#根据运算符对进行相应的操作
'''
此处set方法十分便利,
但也可以使用遍历的优化方法。
'''
test = []
if op_type == 'AND':
test = list(set(files1) & set(files2))
elif op_type == "OR":
test = list(set(files1) | set(files2))
elif op_type == "NOT":
test = list(set(range(0, len(self.files))) - set(files2))
# print(test)
# result = []
# i = 0
# j = 0
# if op_type == 'AND':
# while i < len(files1) and j < len(files2):
# if files1[i] == files2[j]:
# result.append(files1[i])
# i += 1
# j += 1
# elif files1[i] < files2[j]:
# i += 1
# elif files1[i] > files2[j]:
# j += 1
# elif op_type == "OR":
# while i < len(files1) and j < len(files2):
# if files1[i] == files2[j]:
# result.append(files1[i])
# i += 1
# j += 1
# elif files1[i] < files2[j]:
# result.append(files1[i])
# i += 1
# elif files1[i] > files2[j]:
# result.append(files2[j])
# j += 1
# if i < len(files1):
# for t in range(i,len(files1)):
# result.append(files1[t])
# elif j < len(files2):
# for t in range(j, len(files2)):
# result.append(files2[t])
# elif op_type == "NOT":
# while i < len(self.files) and j < len(files2):
# if i == files2[j]:
# i += 1
# j += 1
# elif i < files2[j]:
# result.append(i)
# i += 1
# for t in range(i, len(self.files)):
# result.append(t)
return test
五、调用
首次调用,生成.npz文件。再次调用时,直接读取该文件中构建好的索引即可。
#首次调用,构建索引
br = BoolRetrieval()
br.build_index(r'要构建索引的文档路径')
#查询调用
while True:
query = input("请输入与查询(与||,或&&,非!):")
print(br.search(query))
总结
若有不解或疏漏之处,欢迎留言。