文章目录
- 1.案例简介
- 2.设计思路
- 3.设计结构
- 4.关键技术
- 5.数据结构
- 6.数据集合
- 7.设计过程
- 7.1 信息采集模块
- 7.2 索引模块
- 7.3 网页排名和搜索
- 8.示例效果
1.案例简介
本例使用Python建立一个指定网站专用的Web搜索引擎,它能爬取所有指定的网页信息,然后准确的进行中文分词,创建网站分词索引,从而实现对网站信息的快速检索展示。
本例以清华官网新闻网站(http://news.tsinghua.edu.cn)为练习目标,抓取所有新闻页面,然后对其进行分词索引,构建分词索引数据库,方便用户对清华新闻资讯的快速检索。
2.设计思路
第1步,爬取整个网站,得到所有网页链接。
爬取的方式包括:广度优先检搜索(BFS)和深度优先搜索(DFS)。本例采用广度优先搜索策略,实现完整的站内新闻检索。
第2步,获取网页源代码,解析出新闻内容、标题等信息。
第3步,对获取的内容进行分词,建立分词索引库。
索引一般包括:正排索引(正向索引)和倒排索引(反向索引)两种类型。
- 正排索引(正向索引,forward index):正排数据表是以文档的ID为关键字,表中记录文档(即网页)中每个字或词的位置信息,查找时扫描表中每个文档中字或词的信息,直到找出所有包含查询关键字的文档。这种组织方法在建立索引的时候结构比较简单,建立比较方便,且易于维护。因为索引是根据文档建立的,若是有新的文档加入,直接为该文档建立一个新的索引块,添加在原来索引文件的后面。若是有文档删除,则直接找到该文档号对应的索引信息,将其直接删除。但是在查询的时候需要对所有的文档进行扫描,以确保没有遗漏,这样就使得搜索的时间大大延长,检索效率低下。
正排数据表的工作原理非常简单,但是由于其搜索效率太低,除非在特定情况下,否则实用价值不大。
- 倒排索引(反向索引,inverted index):倒排数据表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。
由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字所对应的所有文档,所以效率高于正排数据表。
在全站搜索中,搜索的响应速度是一个最关键的性能,而索引的建立由于在后台进行,尽管效率相对低一些,但不会影响整个搜索引擎的效率。
简单总结,正排索引是从文档到关键字的映射(已知文档求关键字),倒排索引是从关键字到文档的映射(己知关键字求文档)。
在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合。实际上,在搜索引擎索引库中关键词已经转换为关键词ID。
例如,“文档1”经过分词提取了20个关键词,每个关键词都会记录它在文档中的出现次数和出现位置,得到正向索引的结构如下:
-
“文档1”的ID --> 关键词1:出现次数,出现位置列表; 关键词2:出现次数,出现位置列表; 关键词3:出现次数,出现位置列表;…
-
“文档2”的ID --> 关键词1:出现次数,出现位置列表; 关键词2:出现次数,出现位置列表; 关键词3:出现次数,出现位置列表;…
例如,当用户搜索关键词“研究生”时,假设只存在正向索引(forward index),那么就需要扫描索引库中的所有文档,找出所有包含关键词“研究生”的文档,再根据打分模型进行打分,排出名次后呈现给用户。因为互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。所以搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应一系列的文件,这些文件中都出现这个关键词,得到倒排索引的结构如下: -
“关键词1”的ID --> 文档1 ID; 文档2 ID; 文档3 ID;…
-
“关键词2”的ID --> 文档1 ID; 文档2 ID; 文档3 ID;…
第4步,搜索时,根据搜索关键词在词条索引中查询,按顺序返回相关的搜索结果,也可以按网页评价排名顺序返回相关的搜索结果。
当用户输入一串搜索字符串时,程序会先进行分词,然后依照每个词的索引找到相应网页。假如在搜索框中输入“清华大学”,搜索引擎首先会对字符串进行分词处理“清华/大学”,然后按照一定的规则对词做布尔运算.
例如,每个词之间做“与”运算,在索引中搜索“同时”包含这些词的页面。
3.设计结构
本例主要由以下4个模块组成。
- 信息采集模块:主要是利用网络爬虫实现对网站信息的抓取。
- 索引模块:负责对爬取的新闻网页的标题、内容进行分词并建立倒排词表。
- 网页排名模块:TF/IDF是一种统计方法,用于评估一个关键词对于一个文件集的重要程度。
- 用户搜索界面模块:负责用户关键字的输入,以及搜索结果信息的返回。
4.关键技术
中文分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。中文分词是网页分析索引的纂础。分词的准确性对搜索引擎来说十分重要,如果分词速度太慢,即使 再准确,对于搜索引擎来说也是不可用的,因为搜索引擎需要处理很多网页,如果分析消耗的时间过长,会严重影响搜索引擎内容更新的速度。因此,搜索引擎对于分词的准确率 和速率都提出了很高的要求。
jieba是一个支持中文分词、高准确率、高效率的Python中文分词组件,它支持繁体分词和自定义词典,并支持3种分词模式。
- 精确模式:试图将句子最精确地切开,适合文木分析。
- 全模式:把句子中所有可以成词的词语都扫描出来,速度非常快,但是不能解决歧义的问题。
- 搜索引擎模式:在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。
使用本例之前,需要安装jieba模块:
pip install jieba
组件提供了jieba.cut()方法用于分词,cut()方法接受两个输入参数:第1个参数为需要分词的字符串;第2个参数用来控制分词模式。
jieba.cut()返回的结构是一个可迭代的生成器(generator),可以使用for循环来获得分词后得到的每一个词语,也可以用list(jieba.cut(...))
转化为list列表。
jieba.cut_for_search()方法仅有一个参数,为分词的字符串,该方法适用于搜索引擎构造倒排索引的分词,粒度比较细。
deque (double-ended queue的缩写)双向队列类,似于list列表,位于Python标准库collections中。它提供了两端都可以操作的序列,这意味着在序列的前后都可以执行添加 或删除操作。
deque支持从任意一端添加元素。append()用于从右端添加一个元素,appendleft())用于从左端添加一个元素。
deque也支持从任意一端抛出元素。pop()用于从右端抛出一个元素,popleft())用于从左端抛出一个元素。
5.数据结构
在数据库中建立两个table,其中一个是doc表,存储每个网页ID和URL链接。
另一个是word表,即倒排表,存储词语和其对应的网页ID的list。
6.数据集合
如果一个词在某个网页中出现多次,那么list中这个网页的序号也出现多次。list最后转换成一个字符串存进数据库。
例如,词“清华”出现在网页ID为12、15、18号的网页里,12号页而1次,15号页面3次,18号页面2次,它的list应为[12,15,15,15,18,18],转换成字符串“12 15 15 15 18 18”存储在word表的一条记录中,如下图所示。
7.设计过程
7.1 信息采集模块
获取初始的URL。初始的URL地址可以由用户指定的某个或某几个初始爬取网页。
根据初始的URL爬取页面,并获得新的URL。在获得初始的URL地址之后,首先需要爬取对应URL地址中的网页,在爬取了对应的URL地址中的网页后,将网页存储到原始数据库中,并目在爬取网页的同时发现新的URL地址,将已爬取的URL地址存放到一个已爬URL列表中,用于去重复判断爬取的进程。
将新的URL放到URL队列中。注意,在第2步中获取了下一个新的URL地址。之后会将新的URL地址放到URL队列中。
从URL队列中读取新的URL,并依据新的URL爬取网页,同时从新网页中获取新URL,并重复上述的爬取过程。
当满足爬虫系统设置的停止条件时停止爬取。在编写爬虫的时候一般会设置相应的停止条件,如果没有设置停止条件,爬虫会一直爬取下去,直到无法获取新的URL地址为止,若设置了停止条件,爬虫则会在停止条件满足时停止爬取。
使用unvisited队列存储待爬取URL链接的集合,并使用广度优先搜索,使用visited集合存储已访问过的URL链接。
7.2 索引模块
建立倒排词表。解析新闻网页内容,这个过程需要根据网站的网页结构的具体情况来处理。
提取出的网页内容存在于title, article中,对它们进行中文分词,并对每个分出的词语建立倒排词表。
# test1.py 信息采集和索引模块
import sys
from collections import deque
import urllib
from urllib import request
import re
from bs4 import BeautifulSoup
import lxml
import sqlite3
import jieba
safelock=input('整个检索可能需要很长时间,确定要重构清华网站的新闻词库?(y/n)')
if safelock!='y':
sys.exit('退出')
url='http://news.tsinghua.edu.cn/publish/thunews/index.html' #'http://news.tsinghua.edu.cn/'#入口
unvisited=deque()#待爬取链接的列表,使用广度优先搜索
visited=set() #已访问的链接集合
unvisited.append(url)
conn=sqlite3.connect('tsinghua_news.db') # 创建或打开数据库
c=conn.cursor() # 获取游标指针
# 创建数据表
# c.execute('drop table doc')
c.execute('create table IF NOT EXISTS doc (id int primary key,link text)')
# c.execute('drop table word')
c.execute('create table IF NOT EXISTS word (term varchar(25) primary key,list text)')
conn.commit() # 提交操作
conn.close() # 关闭连接
print('***************开始,请耐心等待***************')
cnt=0
while unvisited:
url=unvisited.popleft() # 从左侧读取第一个url列表项
visited.add(url) # 同时存入到已读取列表中
cnt+=1
print('开始抓取第',cnt,'个链接:',url)
# 爬取网页内容,时长30秒,避免假死问题
try:
response=request.urlopen(url,timeout=3) # 设置请求失效为3秒,可根据情况调整
content=response.read().decode('utf-8') # 读取网页内容
except: # 如果读取失败,则继续下一条
continue
# 检索下一个可爬取的链接,因为搜索范围是网站内,所以对链接有格式要求,这个格式要求根据具体情况而定
# 解析网页内容,可能有几种情况,这个也是根据这个网站网页的具体情况而定
soup=BeautifulSoup(content,'lxml')
# all_a=soup.find_all('a',{'class':"content"}) # 本页面所有内容链接
# all_a=soup.select(".content li a,figure a") # 本页面所有列表项链接
all_a=soup.select("a[href]") # 本页面所有包含href属性的链接
for a in all_a:
x=a.attrs['href'] # 获取网址
if re.match(r'http.+',x): # 排除是http开头,而不是http://news.tsinghua.edu.cn的网址,限定检索链接的范围,仅检索清华新闻网页
if not re.match(r'http\:\/\/news\.tsinghua\.edu\.cn\/.+',x):
continue
if re.match(r'\/publish\/.+',x): #"/publish/thunews/9654/20191234.html"
x='http://news.tsinghua.edu.cn'+x
elif re.match(r'publish/.+',x) : #"publish/thunews/9654/20191234.html"
x='http://news.tsinghua.edu.cn/'+x
elif re.match(r'\.\.\/publish/.+',x): #"../publish/thunews/9654/20191234.html"
x='http://news.tsinghua.edu.cn'+x[2:]
elif re.match(r'\.\.\/\.\.\/publish/.+',x): #"../../publish/thunews/9654/2019123420314.html"
x='http://news.tsinghua.edu.cn'+x[5:]
if not re.match(r'.+\.html?$',x): # 过滤掉非.html、.htm网页文件,要根据具体网站而定,对于动态生成页面,可能为其他扩展名,主要目的是过滤掉.pdf、.jpg、.png等非网页文件
continue
if (x not in visited) and (x not in unvisited): # 存入未访问列表
unvisited.append(x)
# 分词
title=soup.title # 网页标题
article=soup.find('article',class_='article') # 新闻文章内容
if title==None and article==None:
print('无内容的页面。')
continue
elif article==None:
print('只有标题。')
title=title.text
title=''.join(title.split())
article=''
else:
title=title.text
title=''.join(title.split())
article=article.get_text("",strip=True)
article=''.join(article.split())
print('网页标题:',title)
# 提取出的网页内容存在title,article两个变量里,对它们进行中文分词
seggen=jieba.cut_for_search(title)
seglist=list(seggen)
seggen=jieba.cut_for_search(article)
seglist+=list(seggen)
# 数据存储
conn=sqlite3.connect("tsinghua_news.db")
c=conn.cursor()
print(cnt)
print(url)
c.execute('insert into doc values(?,?)',(cnt,url))
conn.commit()
#对每个分出的词语建立词表
for word in seglist:
# 检验看看这个词语是否已存在于数据库
c.execute('select list from word where term=?',(word,))
result=c.fetchall()
# 如果不存在
if len(result)==0:
docliststr=str(cnt)
c.execute('insert into word values(?,?)',(word,docliststr))
# 如果已存在
else:
docliststr=result[0][0] # 得到字符串
docliststr+=' '+str(cnt)
c.execute('update word set list=? where term=?',(docliststr,word))
conn.commit()
conn.close()
print('***************词库重构完毕***************')
7.3 网页排名和搜索
网页排名采用TF/IDF统计。TF/IDF是一种用于信息检索与数据挖掘的常用加权技术。TF/IDF是一种用于评估一词对于一个文件集或一个语料库中的一份文件的重要程度。TF的意思是词频(Term Frequency), IDF的意思是逆文本频率指数(Inverse Document Frequency)。TF表示词条t在文档d中出现的频率。IDF的主要思想是:如果包含词条t的文档越少,则词条t的IDF越大,说明词条t具有很好的类别区分能力。
词条t的IDF计算公式如下:ids = log(N/df),其中,N是文档总数,df是包含词条t的文档数量。
在本例中tf={文档号:出现次数},存储的是某个词在文档中出现的次数。例如,"清华"的tf={12:1,15:3,18:2},即词“清华”出现在网页ID为12、15、18号的网页里,12号页面1次,15号页面3次,18号页面2次。
score= {文档号:文档得分}用于存储搜索到文档的排名得分。
# test2.py 打分和搜索模块
import re
import urllib
from urllib import request
from collections import deque
from bs4 import BeautifulSoup
import lxml
import sqlite3
import jieba
import math
conn=sqlite3.connect("tsinghua_news.db")
c=conn.cursor()
c.execute('select count(*) from doc')
N=1+c.fetchall()[0][0] # 文档总数
target=input('请输入搜索词:')
seggen=jieba.cut_for_search(target)
score={} # 文档号:文档得分
for word in seggen:
print('得到查询词:',word)
# 计算score
tf={} # 文档号:文档数
c.execute('select list from word where term=?',(word,))
result=c.fetchall()
if len(result)>0:
doclist=result[0][0] # 字符串“12 35 35 35 88 88”
doclist=doclist.split(' ') # ['12','35','35','35','88','88']
doclist=[int(x) for x in doclist] # 把字符串转换为元素为int的list [12,35,35,35,88,88]
df=len(set(doclist)) # 当前word对应的df数(即几篇文档中出现)
idf=math.log(N/df)
print('idf:',idf)
for num in doclist:
if num in tf:
tf[num]=tf[num]+1
else:
tf[num]=1
# tf统计结束,现在开始计算score
for num in tf:
if num in score:
# 如果该num文档已经有分数了,则累加
score[num]=score[num]+tf[num]*idf
else:
score[num]=tf[num]*idf
sortedlist=sorted(score.items(),key=lambda d:d[1],reverse=True)
# print('得分列表',sortedlist)
cnt=0
for num,docscore in sortedlist:
cnt=cnt+1
c.execute('select link from doc where id=?',(num,))
url=c.fetchall()[0][0]
print(url,'得分:',docscore)
try:
response=request.urlopen(url)
content=response.read().decode('utf-8')
except:
print('oops...读取网页出错')
continue
soup=BeautifulSoup(content,'lxml')
title=soup.title
if title==None:
print('No title.')
else:
title=title.text
print(title)
if cnt>20:
break
if cnt==0:
print('无搜索结果')
8.示例效果