当前位置:文档之家› 互联网搜索引擎设计与实现(上)

互联网搜索引擎设计与实现(上)

互联网搜索引擎设计与实现(上)
互联网搜索引擎设计与实现(上)

互联网搜索引擎设计与实现(上)下载引擎+ 文档预处理

目录

1.项目要求 (3)

2. 系统环境 (3)

3. 项目总体设计 (3)

4. 详细设计 (4)

4.1 下载引擎 (4)

4.2 去标签和符号 (6)

4.3 去停用词或找词干 (7)

5. 运行结果 (9)

5.1 下载文档模块 (9)

5.2 文档预处理模块 (10)

5.3 删除停用词模块 (12)

5.4 自动化执行整个模块 (13)

6. 日志 (14)

7. 收获与感想 (14)

附录Ⅰ:下载引擎 (15)

附录Ⅰ:中文文档预处理 (17)

附录Ⅰ:英文文档预处理 (20)

附录Ⅰ:停用词表转字典 (23)

附录Ⅰ:删除停用词 (24)

附录Ⅰ:自动化shell脚本 (27)

1.项目要求

(一)建立并实现文本搜索功能

?利用/调用开源搜索引擎Lucene(https://www.doczj.com/doc/c55627172.html,/)或者

Lemur(https://www.doczj.com/doc/c55627172.html,/)实现文本搜索引擎。查阅相关资料,安装软件。

?对经过预处理后的500个英文和中文文档/网页建立搜索并实现搜索功能。

?通过上述软件对文档建立索引(Indexing),然后通过前台界面或者已提供

的界面,输入关键字,展示搜索结果。

?前台可通过网页形式、应用程序形式、或者利用已有的界面工具显示。

?必须实现英文搜索功能。中文搜索功能作为可选项。

(二)比较文档之间的相似度

?通过余弦距离(Cosine Distance)计算任意两个文档之间的相似度,列出文

档原文,并给出相似度值。

(三)对下载的文档,利用K-Means聚类算法进行聚类。

?将下载的500个中文/英文文档聚为20个类,并显示聚类之后所形成的三个

最大的类,及每个类中代表性的文档(即,离类中心最近的五个文档)。

?距离计算公式,可采用余弦距离,也可用欧式距离。

2. 系统环境

操作系统:Windows 10专业版

编程语言:Java1.8,HTML5, CSS3, Javascript

编程软件:Eclipse Photon Release (4.8.0)、HBuilder 8.8.0.201706142254

开源搜索引擎:Lucene5.5.5

JAR包:

IK分词器:ik2012lucene4.jar

Lucene中文分词器:lucene-analyzers-smartcn-5.5.5.jar

Lucene 标准分词器:lucene-analyzers-common-5.5.5.jar

Lucene核心包:lucene-core-5.5.5.jar

Lucene搜索结果高亮标注:lucene-highlighter-5.5.5.jar、lucene-memory-5.5.5.jar

Lucene短语查询:lucene-queryparser-5.5.5.jar

索引查看器:luke-5.5.0-luke-release

3. 项目总体设计

该项目是对项目一的补充,由于项目一爬取的内容为网页,为了能让系统检索的结果更好的在前端展示,本项目采用B/S架构,前端为WEB端,能够直接打开检索出来的源HTML 文档。

在搜索引擎上,为了更好的保证该IR系统的稳定性,编程语言采用Java1.8。Java在开

源搜索引擎Lucene上技术已经非常成熟,较Python而言具有先天优势。虽然Apache公司也提供的Lucene的python版本,但各个版本之间的兼容性比较差,且在检索效率和矩阵运算的效率上显然不如Java。

项目前端为浏览器,用户通过浏览器与后端交互。关键字(词)查询时,用户在前端输入关键字,后端返回结果集,并显示在前端。文本相似度查询时,用户上传两个网页预处理之后的txt文档,后端接收文档内容,返回两篇文档的相似度。做聚类处理时,用户通过前端选择需要聚类的数据集合(英文/中文),返回聚类的结果并呈现给用户。

为了更好的满足以上内容,本项目采用MVC模式来设计本项目。

图1. 项目结构

最后,编写linux的shell脚本调用各个模块,将各个模块集成一个完整的自动化系统。

4. 详细设计

4.1 下载引擎

文档的下载通过网络爬虫实现,该过程调用urllib库。

urllib通过urllib.request.Request()函数模拟浏览器的请求体向服务器发出请求,通过urllib.request.urlopen(url)函数获取url链接的数据。

其次,现在大部分的url链接都采用了https安全协议,如果直接使用urllib.request.Request(url)打开https链接,是没法跳过安全认证直接获取数据的,因此,需要设置打开链接时跳过https认证,如下:

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0'}

req = urllib.request.Request(url, headers=headers)

context = ssl._create_unverified_context()

respond = urllib.request.urlopen(req, context=context, timeout=0.5)

批量爬取html文件时,首先需要从一个url爬取数据,然后从爬取的数据中用正则表达式匹配url,并将返回的url列表添加到一个队列中去。

起始的url对爬取的速率和效率都至关重要,为了方便起见,我们直接把一个超链接比

较多的url作为起始url,如各大门户网站的主页,新闻网站主页等,这里以新浪新闻(https://https://www.doczj.com/doc/c55627172.html,/)和环球时报(https://www.doczj.com/doc/c55627172.html,/)作为我们的起始url。

在浏览器中打开这两个链接,进入到开发者模式,查看其代码结构,如图:

图2. 新浪新闻首页结构(中文)

图3. 环球时报首页结构(英文)

如图所示,该url中存在大量以.html或.shtml格式结尾的url。该网址所有的url中,有些url属于POST请求,有些属于GET请求,POST请求需要接受特定的数据格式才能获取数据,因此需要过滤到属于POST请求的url,打开一个html文档属于GET请求,直接通过url就能获取数据,因此,我们只需要爬取这.html和.shtml两种格式结尾的url皆可。

当要爬取网页时,队列的队列头出队,用出队的url去爬取数据,匹配数据中的url,返回符合正则表达式规则的url列表,并将url列表中是以.html或.shtml格式的文件依次添加到队列尾。然后队列头出队,爬取下一个网页,依次循环,下载630个文档后结束。

打开url时,并不能保证所有爬取下来的url链接都能打开,为了减少不必要的等待,为每个url都设置一个响应响应时间是非常有必要的,为每个url都设置了0.5s的响应时间,当超过这个时限依旧未响应时,则丢弃这个url直接爬取下一个url。

批量下载数据的过程可能存在很多无法预料的问题,为了防止下载过程进行到一半时由于发生异常而中断,我们需要做好异常处理。

4.2 去标签和符号

去标签和符号调用re模块实现。

读取下载好的html文档,然后通过https://www.doczj.com/doc/c55627172.html,pile()和re.sub()函数将文档中符合正则表达式规则的字符串进行替换。

需要替换的内容包括换行符、回车符、html标签、html属性、样式、url等除文本之外的所有内容,将这些内容替换成空格再进行后续的处理。代码实现如下:re_cdata = https://www.doczj.com/doc/c55627172.html,pile('//]*//\]\]>', re.I) # 匹配CDATA

re_script = https://www.doczj.com/doc/c55627172.html,pile('<\s*script[^>]*>[^<]*<\s*/\s*script\s*>', re.I) # Script

re_style = https://www.doczj.com/doc/c55627172.html,pile('<\s*style[^>]*>[^<]*<\s*/\s*style\s*>', re.I) # style

re_br = https://www.doczj.com/doc/c55627172.html,pile('') # 处理换行

re_h = https://www.doczj.com/doc/c55627172.html,pile(']*>') # HTML标签

re_comment = https://www.doczj.com/doc/c55627172.html,pile('') # HTML注释

re_delone = https://www.doczj.com/doc/c55627172.html,pile(r'(.*?)') # 第一行

s = re_cdata.sub('', htmlstr) # 去掉CDA TA

s = re_script.sub('', s) # 去掉SCRIPT

s = re_style.sub('', s) # 去掉style

s = re_br.sub('', s) # 将br转换为换行

s = re_h.sub('', s) # 去掉HTML 标签

s = re_comment.sub('', s) # 去掉HTML注释

s = re_delone.sub('', s) # 去掉第一行

# 去掉多余的空行

blank_line = https://www.doczj.com/doc/c55627172.html,pile('\n+')

s = blank_line.sub('\n', s)

其次,还需要去掉文档中的符号,由于符号比较多,该步只需去掉键盘能够键入的符号,特殊的符号可作为停用词在下一个步骤中去除。

可将所有符号都映射成空格,然后通过映射表直接替换成空格,如下:

# 定义中英文符号

fuhao = r'qwertyuioplkjhgfdsazxcvbnm~`!1@2#3$4%5^6&7*8(9)0_-+=|}]{[;:"\'?/>.<,!¥……()——、】【”“:;’‘?》《。,'

rep = ' '

# 所有英文字符转化为小写

data = data.lower()

tup = str.maketrans(fuhao, rep) # 建立字符映射表

data = data.translate(tup) # 将所有符号转化为空格

然后是去除多余的空格,首先将字符串通过空格来切割,生成一个分割后的列表,该列表中存在大量长度为0的字符串元素,而只需将这些长度为0的元素去掉,再将长度大于0的元素重新组合即可,如下:

data = data.split(' ')

while(1):

try:

data.remove('')

except:

将处理好的文本保存为txt。

4.3 去停用词或找词干

在网上查找到一份中文和英文的停用词表如下:

图4. 中英文停用词表

停用词表包含了一些常用词和特殊字符,预处理中未去除的特殊字符通过停用词表删除。删除停用词之前,需要对中文文本分词。可通过中文分词库jieba库对文本进行分词,如下:def fenci(data):

ci_list = jieba.cut(data)

# print('/'.join(ci_list))

ci_list = [str(i) for i in ci_list] # 将迭代器转化为列表

return ci_list

删除停用词一般采用的是遍历停用词表,但是中文停用词有1800多个,英文停用词有800多个,直接遍历的方式太耗时。因此先将停用词表转化为字典,如下:

def list_to_json(data):

dic = {}

i = 1

for one in data:

i += 1

return dic

将文档通过空格切割,将其转化为列表。

通过字典的方式删除停用词能很大程度上提高效率。然后遍历列表中的每个词,判断其是否在停用词表中,如果在则将其删除。如下:

def del_chinese_stop_word(data, path):

chinese_stop_word = r'.\stop_word\chinese.txt'

chinese_stop_word = stop_word_to_json.stop_word_to_json(chinese_stop_word)

# print(chinese_stop_word)

# print(data)

pre_len = len(data)

i = 0

while(True):

if data[i] in chinese_stop_word:

# print('删除停用词%s' % data[i])

data.pop(i)

else:

i += 1

if i == len(data):

break

del_len = len(data)

print('文件'+path+'删除停用词%d 个' % (pre_len - del_len))

return data

删除停用词后,还需要对英文词找词干,通过调用nltk库来实现,如下:

from nltk.stem.porter import PorterStemmer

def porter_stemming(data):

porter_stemmer = PorterStemmer()

result = ''

for w in data:

# print("Actual: %s Stem: %s" % (w, porter_stemmer.stem(w)))

result = result + porter_stemmer.stem(w) + ' '

return result

完成最终处理后将词列表转化重新拼接为字符串再写入到txt文件中,如下:

data = ' '.join(data_list)

def write_file(path, data):

with open(path, 'w', encoding='utf-8') as file:

file.write(data)

file.close()

print(path+'写入成功')

5. 运行结果

5.1 下载文档模块

执行html_download.py文件,进行文档下载:

图5. 下载引擎模块执行过程下载结果和文档内容如下:

图6. 下载的html文档

图7. 下载的html文档内容

5.2 文档预处理模块

执行chinese_html_text_preprocess.py和english_html_text_preprocess.py文件:

图8. 中文文档预处理过程

图9. 英文文档预处理过程

文档保存的结果如下:

图10. 中文文档预处理结果

图11. 英文文档预处理结果

5.3 删除停用词模块

执行del_stop_word.py文件:

图12. 停用词删除过程得到最终的字符单元如下:

图13. 中文文档最终的处理结果

图14. 英文文档最终的处理结果

5.4 自动化执行整个模块

使用shell脚本执行优点在于不用打开编译器,并且不要手动去配置运行的环境,直接在终端执行shell脚本即可。终端执行app.sh脚本,执行整个项目,结果如下:

图15. 项目自动化脚本执行过程

6. 日志

执行时app.sh文件时,看运行结果并不是那么方便,因此将运行的结果直接写入到日志文件中,如下:

图16. 项目日志

7. 收获与感想

通过本项目,对网络爬虫有了更加深入的理解,并且掌握了网络爬虫在搜索引擎中应用方法。在文本的处理上,掌握了如何通过正则表达式来获取需要的内容,去除不需要的内容,掌握了中文分词库和英文自然语言库的初步使用。

同时,在项目中,也遇到了许多问题,如:如何批量爬取网址,如何获取html文档中的文本,去除html的标签、注释、符号等问题,后来通过对python更加深入的学习解决了这些问题。

通过本项目,又学会了一门新语言,并且感受到了python在网路爬虫和数据处理上的强大之处。

数据预处理结束后,仍存在一些问题,如中文预处理和删除停用词之后,仍然存在一些特殊字符,如‖▲▼▽等一些键盘无法键入的图形符号,虽然这些符号并不影响字符单元的检索,但可能会对后续的构造倒排表的构建。此外中文分词的结果效果似乎不太好,有些分词似乎并不能构成一个词。英文找词干后,依旧存在“‘”符号。

在优化方案中,对中文文档预处理时,可以通过Unicode编码只匹配汉字字符,并采用其他分词库。英文文档预处理时也把“‘”符号也去掉。

附录Ⅰ:下载引擎

# html_download.py

import urllib.request

import ssl

import re

import os

import collections

i = 1

def writeFileBytes(path, byteData, url):

with open(path, 'wb') as file:

file.write(byteData)

file.close()

print(url+'的内容'+'已写入文件:', path)

with open('./data/chinese/path.txt', 'a', encoding='utf-8') as file:

file.write(url+'的内容'+'已写入文件:'+path+'\n')

file.close()

print()

def writeFileStr(path, byteData):

# 文件默认打开的编码是gbk,改变目标文件的编码,可以很好解决字符格式不兼容问题

with open(path, 'w', encoding='utf-8') as file:

file.write(byteData.decode('utf-8'))

file.close()

def HtmlCrawler(url, path):

try:

global i

HtmlByte = getHtml(url)

path = os.path.join(path, str(i)+'.html')

writeFileBytes(path, HtmlByte, url)

i = i + 1

# pat_url = r'(href="(.*?).(html|shtml)")'

pat_url = r'((http|https|ftp)://(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-`-]*)?)'

re_url = https://www.doczj.com/doc/c55627172.html,pile(pat_url)

url_list = re_url.findall(HtmlByte.decode('utf-8'))

# 去重复网页

url_list = list(set(url_list))

return url_list

except:

print(url + ' 文件格式错误')

return None

def getHtml(url):

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0'}

req = urllib.request.Request(url, headers=headers)

context = ssl._create_unverified_context()

try:

respond = urllib.request.urlopen(req, context=context, timeout=0.5)

HtmlByte = respond.read()

# 返回二进制文件

return HtmlByte

except:

print(url + ' 请求超时')

return None

def CPU(url, path):

global i

# 创建队列

deque = collections.deque()

# 将初始网址添加到队列

deque.append(url)

# 循环读取队列中的链接

while(len(deque) != 0):

if i == 630:

break

try:

targetUrl = deque.popleft()

url_list = HtmlCrawler(targetUrl, path)

# print(url_list)

for item in url_list:

tempUrl = item[0]

if str(tempUrl).endswith('.html') or str(tempUrl).endswith('.shtml'):

deque.append(tempUrl)

except Exception as e:

print(e)

if __name__ == '__main__':

i = 0

# 爬取环球时报及相关链接的内容

# url = r'https://www.doczj.com/doc/c55627172.html,/'

# path = r'./data/english'

# CPU(url, path)

# 抓取新浪新闻及其相关链接的内容

i = 1

url = r'https://https://www.doczj.com/doc/c55627172.html,/'

path = r'./data/chinese'

CPU(url, path)

附录Ⅰ:中文文档预处理

# chinese_html_text_preprocess.py

import re

##过滤HTML中的标签

# 将HTML中标签等信息去掉

# @param htmlstr HTML字符串.

def filter_tags(htmlstr):

# 先过滤CDA TA

re_cdata = https://www.doczj.com/doc/c55627172.html,pile('//]*//\]\]>', re.I) # 匹配CDATA

re_script = https://www.doczj.com/doc/c55627172.html,pile('<\s*script[^>]*>[^<]*<\s*/\s*script\s*>', re.I) # Script re_style = https://www.doczj.com/doc/c55627172.html,pile('<\s*style[^>]*>[^<]*<\s*/\s*style\s*>', re.I) # style

re_br = https://www.doczj.com/doc/c55627172.html,pile('') # 处理换行

re_h = https://www.doczj.com/doc/c55627172.html,pile(']*>') # HTML标签

re_comment = https://www.doczj.com/doc/c55627172.html,pile('') # HTML注释

re_delone = https://www.doczj.com/doc/c55627172.html,pile(r'(.*?)') # 第一行

s = re_cdata.sub('', htmlstr) # 去掉CDA TA

s = re_script.sub('', s) # 去掉SCRIPT

s = re_style.sub('', s) # 去掉style

s = re_br.sub('', s) # 将br转换为换行

s = re_h.sub('', s) # 去掉HTML 标签

s = re_comment.sub('', s) # 去掉HTML注释

s = re_delone.sub('', s) # 去掉第一行

# 去掉多余的空行

blank_line = https://www.doczj.com/doc/c55627172.html,pile('\n+')

s = blank_line.sub('\n', s)

s = replaceCharEntity(s) # 替换实体

return s

# 替换常用HTML字符实体.

# 使用正常的字符替换HTML中特殊的字符实体.

# 你可以添加新的实体字符到CHAR_ENTITIES中,处理更多HTML字符实体. # @param htmlstr HTML字符串.

def replaceCharEntity(htmlstr):

CHAR_ENTITIES = {'nbsp': ' ', '160': ' ',

'lt': '<', '60': '<',

'gt': '>', '62': '>',

'amp': '&', '38': '&',

'quot': '"', '34': '"', }

re_charEntity = https://www.doczj.com/doc/c55627172.html,pile(r'&#?(?P\w+);')

sz = re_charEntity.search(htmlstr)

while sz:

entity = sz.group() # entity全称,如>

key = sz.group('name') # 去除&;后entity,如>为gt

try:

htmlstr = re_charEntity.sub(CHAR_ENTITIES[key], htmlstr, 1)

sz = re_charEntity.search(htmlstr)

except KeyError:

# 以空串代替

htmlstr = re_charEntity.sub('', htmlstr, 1)

sz = re_charEntity.search(htmlstr)

return htmlstr

def repalce(s, re_exp, repl_string):

return re_exp.sub(repl_string, s)

def open_file(path):

with open(path, 'r', encoding='utf-8') as f:

data = f.read()

f.close()

return data

def get_text(path):

data = open_file(path)

data = filter_tags(data)

# print(data)

# print('------------------------------------------------------')

# data = replaceCharEntity(data)

data = data.replace('\n', '')

data = data.replace('\t', '')

# 定义中英文符号

fuhao = r'qwertyuioplkjhgfdsazxcvbnm~`!1@2#3$4%5^6&7*8(9)0_-+=|}]{[;:"\'?/>.<,!¥……()——、】【”“:;’‘?》《。,'

rep = ' '

# 所有字符转化为小写

data = data.lower()

tup = str.maketrans(fuhao, rep) # 建立字符映射表

data = data.translate(tup) # 将所有符号转化为空格

data = data.split(' ')

while(1):

try:

data.remove('')

except:

break

return data

def list_to_str(data):

text = ' '.join(data)

return text

def str_write_to_file(path, data):

with open(path, 'w', encoding='utf-8') as file:

file.write(data)

file.close()

print('write to file', path)

def CPU():

i = 1

while(1):

try:

path = r'./data/chinese/'+str(i)+'.html'

data = get_text(path)

print(data)

data = list_to_str(data)

path = r'./preprocess/chinese/'+str(i)+'.txt'

str_write_to_file(path, data)

except:

print('文件错误')

i += 1

if i == 630:

break

if __name__ == '__main__':

path = r'./data/Chinese/1.html'

# get_text(path)

CPU()

附录Ⅰ:英文文档预处理

# english_html_text_preprocess.py

英文文档的预处理:

import re

##过滤HTML中的标签

# 将HTML中标签等信息去掉

# @param htmlstr HTML字符串.

def filter_tags(htmlstr):

# 先过滤CDA TA

re_cdata = https://www.doczj.com/doc/c55627172.html,pile('//]*//\]\]>', re.I) # 匹配CDATA

re_script = https://www.doczj.com/doc/c55627172.html,pile('<\s*script[^>]*>[^<]*<\s*/\s*script\s*>', re.I) # Script re_style = https://www.doczj.com/doc/c55627172.html,pile('<\s*style[^>]*>[^<]*<\s*/\s*style\s*>', re.I) # style

re_br = https://www.doczj.com/doc/c55627172.html,pile('') # 处理换行

re_h = https://www.doczj.com/doc/c55627172.html,pile(']*>') # HTML标签

re_comment = https://www.doczj.com/doc/c55627172.html,pile('') # HTML注释

re_delone = https://www.doczj.com/doc/c55627172.html,pile(r'(.*?)') # 第一行

s = re_cdata.sub('', htmlstr) # 去掉CDA TA

s = re_script.sub('', s) # 去掉SCRIPT

s = re_style.sub('', s) # 去掉style

s = re_br.sub('', s) # 将br转换为换行

s = re_h.sub('', s) # 去掉HTML 标签

s = re_comment.sub('', s) # 去掉HTML注释

s = re_delone.sub('', s) # 去掉第一行

相关主题
文本预览
相关文档 最新文档