原文出自:http://www.codeproject.com/KB/applications/SearchDotnet.aspx
作者:Stevan Rodrigues
翻译:张远山
站点搜索引擎搜索整个页面(动态页面也可以)中的关键字或关键词并且计算关键字或关键词在页面中出现的次数,然后按最高匹配优先把结果显示出来。
l 下载最新版本的示例工程 (VB.NET) - 67 Kb
l 下载读写XML的示例工程 (VB.NET) - 88 Kb
l 下载最新版本的示例工程(Visual Studio .NET 非必须的) - 52 Kb
引言
站点搜索引擎搜索整个页面(动态页面也可以)中的关键字或关键词并且计算关键字或关键词在页面中出现的次数,然后按最高匹配优先把结果显示出来。你可以轻易地把文件扩展名放到web.congig文件标记的地方,搜索模块将搜索所有符合这些文件扩展名的文件。你不想搜索的文件或文件夹也可以放在web.config标记的地方,这些文件和文件夹将不会被搜索。现在你还可以选择编码格式。
这篇已更新的文章包含全球化和优化代码的提示。
注:此搜索引擎适最合于小站点。你也可以使用正则表达式和改变这些代码来搜索内部网。对于大型网站你需要周期性的写入XML然后从XML中读取。我已经在文章的后面部分提供了使用说明。我也加入了一个读写XML的示例工程。
背景
搜索引擎帮助用户找到他感兴趣的页面。当我在做ASP.NET项目的时候,我一定把搜索模块加到网站中去。我曾经有一个ASP的而非.NET的,这个搜索引擎就是因此而产生的。我的第一个版本仅仅是一个简单的Web窗体,还没有充分使用.NET 语言面向对象的特性。在空闲时间,我使我的代码最大限度地使用面向对象语言。在这篇文章中,我在实际经验和不同作者良好实践建议基础上进一步加强我的设计。
来自北京的 宋涛先生带着怎样把模块转成中文的疑问联系了我。在他的帮助下我增强代码可以支持其他语言。一些用户在把SiteSearch.asp放到网站根目录的时候也遇到了一些问题,我已经更改了代码修正了错误。
源代码概述
网站搜索引擎结构如下所示:
类
定义类和创建类示例的能力是面向对象语言的最重要的特性之一。在紧接着的章节中,我们将讲述搜索模块中用到的类。
类名 | 描述 |
| Web窗体使用的类,用户可以通过某些词搜索网站 |
| 用来清理HTML内容的类 |
| 从HTML文件获取内容的类 |
| 用来存储页面中数据的类 |
| 用来在dataset中创建和存储结果的类 |
| 用来读取网站配置的类 |
| 用来为每个用户存储搜索信息的类 |
SiteSearch.aspx
Web窗体是微软的.NET首创的令人激动的特性之一。SiteSearch.aspx是一个Web窗体也是搜索模块的起始页面。
一个Web窗体页面由页面(ASPX 文件)和后台代码(.aspx.cs 文件或.aspx.vb文件)组成。我们的Web窗体由SiteSearch..aspx和SiteSearch.aspx.vb。下面我将处理这两个页面同时涉及一些Web窗体的主要元素。
ASP.NET是一个事件驱动的设计环境,在下来的章节我们将会讲述一些事件的处理和方法
Page_Load
服务器控件在Page对象被加载,view state信息在此时也是可用的。Page_Load事件检测sSite是否为nothing并且把Session(“Site”)变量给它赋值。
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
If IsNothing(sSite) Then
sSite = Session("Site")
End If
End Sub
srchbtn_Click
当搜索按钮被点击后搜索按钮的事件将被激活。这里我们编写代码来改变控件设置或在页面中显示文本。我们检测搜索是否包含文本然后调用SearchSite方法。DisplayContent()被调用来给web页面的不同控件赋值。
'*********************************************************
'
' srchbtn_Click 事件
'
' 在此事件中增加代码.
'
'**********************************************************
Private Sub srchbtn_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles srchbtn.Click
Dim strSearchWords As String
'如果用户没有输入要搜索的词
'则不执行文件搜索程序
pnlSearchResults.Visible = False
strSearchWords = Trim(Request.Params("search"))
If Not strSearchWords.Equals("") Then
Searchs.Site.ApplicationPath = String.Format("http://{0}{1}",
Request.ServerVariables("HTTP_HOST"), Request.ApplicationPath)
sSite = SearchSite(strSearchWords)
Session("Site") = sSite
dgrdPages.CurrentPageIndex = 0
DisplayContent()
End If
End Sub
DisplayContent
DisplayContent()被调用来给web页面的不同控件赋值。通过调用BindDataGrid方法来设置DataGrid的内容。使用ViewState(“SortExpression”)来设置排序方法的描述。
'*********************************************************************
'
' DisplayContent 方法
'
' 数据被绑定到各个地方
'
'*********************************************************************
Private Sub DisplayContent()
If Not IsNothing(sSite.PageDataset) Then
pnlSearchResults.Visible = True
lblSearchWords.Text = sSite.SearchWords
If ViewState("SortExpression") Is Nothing Then
ViewState("SortExpression") = "MatchCount Desc"
End If
BindDataGrid(ViewState("SortExpression"))
lblTotalFiles.Text = sSite.TotalFilesSearched
lblFilesFound.Text = sSite.TotalFilesFound
End If
End Sub
搜索
搜索的主要调用在这个方法中进行。UserSearch类保存所有搜索信息和搜索结果,之后我们将讲述这个类。UserSearch类对象也就是srchSite被创建,它的属性如SearchWords和SearchCriteria被赋值,并且srchSite.Search方法被调用。
'************************************************************
'
' SearchSite 方法
'
' sSite.PageDataset 用来给dataset设置值。.
'
'************************************************************
Private Function SearchSite(ByVal strSearch_
As String) As Searchs.UserSearch
Dim srchSite As Searchs.UserSearch
srchSite = New Searchs.UserSearch()
'Read in all the search words into one variable
srchSite.SearchWords = strSearch
If Phrase.Checked Then
srchSite.SearchCriteria = Searchs.SearchCriteria.Phrase
ElseIf AllWords.Checked Then
srchSite.SearchCriteria = Searchs.SearchCriteria.AllWords
ElseIf AnyWords.Checked Then
srchSite.SearchCriteria = Searchs.SearchCriteria.AnyWords
End If
srchSite.Search(Server.MapPath("./"))
Return srchSite
End Function
DataGrid
DataGrid控件是代表一个多列的、完全模板化的表格,在众多的数据绑定控件中它的功能更加强大,而且DataGrid控件是ASP.NET为数据报表所特制的,因此我使用它来显示搜索结果。因为本文的重点是讲内部网络的搜索引擎,因此我只给出使用DataGrid特性的简单介绍。
数据绑定
数据绑定是从数据源中获取数据并且动态联结到可视化组件的属性的过程。因为DataGrid同时处理(只是在内存中有)更多的条目,你应该明确地把数据集合联结到DataGrid——那就是,数据源。
DataGrid的内容通过设置它的DataSource属性来实现。整个搜索结果被保存在sSite.PageDataset.Tables(“Pages”)中,因此DataGrid的内容被设置到dvwPages,即sSitePageDataset(“Pages”)。DefaultView.BindDataGrid方法在每次页面加载的时候被调用。
'************************************************************
'
' BindDataGrid 方法
'
' sSite.PageDataset 用来给dataset设置值
'
'************************************************************
Private Sub BindDataGrid(ByVal strSortField As String)
Dim dvwPages As DataView
dvwPages = sSite.PageDataset.Tables("Pages").DefaultView
dvwPages.Sort = strSortField
dgrdPages.DataSource = dvwPages
dgrdPages.DataBind()
End Sub
控件可以根据数据域的结构动态创建列。“自动创建”是DataGrid的默认设置,但是你可以通过使用一个叫AutoGenerateColumns的Boolean属性来手动改变这个设置。当你需要控件只显示你在Columns集合中增加的列时,把这个属性设置成False即可;当你需要控件显示数据源要求的那么多列时,把这个属性设置成True(默认)即可。“自动创建”不能使用自定义表头文本,也不支持文本格式化。因此,这里我把它设置成False。你可以使用<asp:datagrid>服务器控件里的<columns>标签来绑定特定的列。
<columns>
<asp:TemplateColumn>
<ItemTemplate>
<%# DisplayTitle(Container.DataItem( "Title" ), _
Container.DataItem( "Path" )) %>
<br>
<%# Container.DataItem( "Description" ) %>
<br>
<span class="Path">
<%# String.Format("{0} - {1}kb", DisplayPath( _
Container.DataItem( "Path" )) , _
Container.DataItem( "Size" ))%>
</span>
<br>
<br>
</ItemTemplate>
</asp:TemplateColumn>
</columns>
DisplayTitle方法和DisplayPath方法用来把自定义信息显示在DataGrid的列中。
'****************************************
'
' DisplayTitle 方法
'
' 显示搜索页的标题
'
'****************************************
Protected Function DisplayTitle(ByVal Title _
As String, ByVal Path As String) As String
Return String.Format("<A href="{1}">{0}</A>", Title, Path)
End Function
'****************************************
'
' DisplayPath 方法
'
' 返回文件路径
'
'****************************************
Protected Function DisplayPath(ByVal Path As String) As String
Return String.Format("{0}{1}/{2}", _
Request.ServerVariables("HTTP_HOST"), _
Request.ApplicationPath, Path)
End Function
分页
与DataList控件不同的是,DataGrid控件支持数据分页显示,即,把数据源的行分别显示在多个页面中。数据源的大小很容易超过实际页面的区域,因此为了维持服务端的可扩展性和使页面对用户有更高的可访问性,你可以每次只显示某些行。为了实现DataGrid控件可分页功能,你可以通过设置控件的Allowpaging属性来告诉它。
分页栏是DataGrid控件提供的一个非常有趣和值得称赞的特性,它可以让用户轻易的从一个页面跳到另外一个页面。分页栏显示在DataGrid控件的下面,它包含了已有页面的链接。当你点击这里的任何一个链接时,控件自动激活PageIndexChanged事件,并且更新相应的页面索引。当页面索引改变的时候dgrdPages_PageIndexChanged将被调用。
'*****************************************************************
'
' dgrdPages_PageIndexChanged 事件
'
' CurrentPageIndex 被赋值为页面索引的值
' 然后datagrid 通过函数 BindDataGrid 被赋值
'
'*****************************************************************
Protected Sub dgrdPages_PageIndexChanged(ByVal s As Object, _
ByVal e As DataGridPageChangedEventArgs) _
Handles dgrdPages.PageIndexChanged
dgrdPages.CurrentPageIndex = e.NewPageIndex
DisplayContent()
End Sub
你可以通过PagerStyle的Mode属性控制分页栏,Mode属性的值从PagerMode中得到。这里我们选择逐条的数字按钮,每个按钮指向一个特定的页面。
<PagerStyle CssClass="GridPager" Mode="NumericPages"></PagerStyle>
排序
DataGrid控件实际上不支持行排序,但是只要数据源具有排序的功能,它就可以提供很好的排序支持。数据源根据用户在DataGrid控件的用户界面中选择的排序表达式返回排好序的记录。通过设置AllowSorting属性为True内置的排序机制就会被触发。
dgrdPages_SortCommand 被调用使DataGrid排序。SortCommand事件处理器通过DataGridSortCommandEventArgs类的SortExpression属性知道排序表达式。在我们的代码中,排序信息是一直存在的,因为它被保存在页面的ViewState集合中。
注:在我的页面中,我已经使表头无效,但是如果表头显示了,你可以通过它来使DataGrid排序。
'*****************************************************************
'
' dgrdAdditionalItems_SortCommand 事件
'
' ViewState( "SortExpression" ) 被赋予
' 排序表达式的值
' 然后datagrid 通过调用函数 BindDataGrid.被赋值
'
'*****************************************************************
Protected Sub dgrdPages_SortCommand(ByVal s As Object, _
ByVal e As DataGridSortCommandEventArgs) _
Handles dgrdPages.SortCommand
ViewState("SortExpression") = e.SortExpression
DisplayContent()
End Sub
Page.vb
Page对象的作用是存储跟网站每个页面相关的数据。
Page类定义了以下属性
Path | 存储文件路径 |
Title | 存储HTML里Title标签里的文本 |
Keywords | 存储HTML里meta keywords标签里的文本 |
Description | 存储HTML里meta description标签里的文本 |
Contents | 存储HTML页面里面的文本 |
Matchcount | 存储在HTML里找到的匹配数 |
'***************************************************
'
' Size 属性
'
' 设置获取文件的大小
'
'***********************************************
Public Property Size() As Decimal
Get
Return m_size
End Get
Set(ByVal Value As Decimal)
m_size = Value
End Set
End Property
'***************************************************
'
' Path 属性
'
' 设置获取文件的路径
'
'***********************************************
Public Property Path() As String
Get
Return m_path
End Get
Set(ByVal Value As String)
m_path = Value
End Set
End Property
'***********************************************
'
' Title 属性
'
'设置获取文件的标题
'
'***********************************************
Public Property Title() As String
Get
Return m_title
End Get
Set(ByVal Value As String)
m_title = Value
End Set
End Property
'***********************************************
'
' Keywords 属性
'
' 设置获取文件中的 Keywords(在Meta 标签中)
'
'***********************************************
Public Property Keywords() As String
Get
Return m_keywords
End Get
Set(ByVal Value As String)
m_keywords = Value
End Set
End Property
'***********************************************
'
' Description 属性
'
'设置获取文件中的description(在meta tags 中)
'
'***********************************************
Public Property Description() As String
Get
Return m_description
End Get
Set(ByVal Value As String)
m_description = Value
End Set
End Property
'***********************************************
'
' Contents 属性
'
'设置获取文件的内容
'
'***********************************************
Public Property Contents() As String
Get
Return m_contents
End Get
Set(ByVal Value As String)
m_contents = Value
End Set
End Property
'***********************************************
'
' MatchCount 属性
'
'设置获取文件的匹配数
'
'***********************************************
Public Property MatchCount() As Integer
Get
Return m_matchcount
End Get
Set(ByVal Value As Integer)
m_matchcount = Value
End Set
End Property
Page类有两个私有函数和两个公有函数,定义如下:
CheckFileInfo方法
这是个公有方法,它检测title、description和content是否存在,如果title的文本为空,则给它赋予默认值”No Title”;如果description的文本为空则要么把content的值给它要么给它赋”There is no description available for this page”。
'*************************************************
'
' CheckFileInfo 方法
'
'子程序,检测文件是否包含
' title 和 decription
'
'*************************************************
Public Sub CheckFileInfo()
'如果页面不包含title则
' 赋予Title变量适当的显示信息
If IsNothing(m_title) Or m_title.Trim().Equals("") Then
m_title = "No Title"
End If
'如果页面不包含title(description)则
'赋予description变量适当的显示信息
If IsNothing(m_description) Or _
m_description.Trim().Equals("") Then
If IsNothing(m_contents) Or _
m_contents.Trim().Equals("") Then
m_description = _
"There is no description available for this page"
Else
If m_contents.Length > 200 Then
m_description = m_contents.Substring(0, 200)
Else
m_description = m_contents
End If
End If
End If
End Sub
Search方法
Search方法是一个公有方法,它根据搜索条件决定调用SearchPhrase方法还是调用SearchWords。SearchPhrase方法搜索词组而SearchWords搜索所有或任何单词。这两个方法都调用SearchPattern方法,这个方法使用正则表达式来搜索文件。
'*******************************************
'
' Search 方法
'
' 搜索文件子程序
'
'*******************************************
Public Sub Search(ByVal strSearchWords As String, _
ByVal SrchCriteria As SearchCriteria)
'如果用户选择搜索词组
If SrchCriteria = SearchCriteria.Phrase Then
SearchPhrase(strSearchWords)
'如果是搜索所有或任何单词
Else
SearchWords(strSearchWords, SrchCriteria)
End If
End Sub
'******************************************************
'
' SearchPhrase 方法
'
' 搜索文件子程序
'
'******************************************************
Private Sub SearchPhrase(ByVal strSearchWords As String)
Dim mtches As MatchCollection
mtches = SearchPattern(strSearchWords)
'检查词组是否被找到
If mtches.Count > 0 Then
'获取词组被找到的次数
m_matchcount = mtches.Count
End If
End Sub
'**************************************************
'
' SearchWords 方法
'
' 搜索文件子程序
'
'**************************************************
Private Sub SearchWords(ByVal strSearchWords As String, _
ByVal SrchCriteria As SearchCriteria)
Dim intSearchLoopCounter As Integer
Dim sarySearchWord As String()
'包含被搜索的所有单词的数组
Dim mtches As MatchCollection
'分开每个被搜索的单词并放到一个数组中
sarySearchWord = Split(Trim(strSearchWords), " ")
'循环搜索每个要被搜索的单词
For intSearchLoopCounter = 0 To UBound(sarySearchWord)
'设置搜索的方式
mtches = SearchPattern(sarySearchWord(_
intSearchLoopCounter))
If SrchCriteria = SearchCriteria.AnyWords Then
m_matchcount = m_matchcount + mtches.Count
ElseIf SrchCriteria = SearchCriteria.AllWords Then
'检查是否有单词不搜索到
If mtches.Count > 0 Then
'获取搜索单词匹配的次数
If m_matchcount = 0 Or (m_matchcount > 0 _
And m_matchcount > mtches.Count) Then
m_matchcount = mtches.Count
End If
Else
'如果被搜索的单词没有找到,则设置
'搜索被找到变量为false,因为
'其中的一个单词没有被找到
m_matchcount = 0
Exit Sub
End If
End If
Next
End Sub
转义符/b是一个特殊的情况。在正则表达式中,除了在字符“[”和“]”之间表示退格符,/b表示一个单词的边界(在/w和/W之间)。在替换模式中,/b总是表示退格。
当我们不是使用UTF-8编码的时候必须把单词边界去掉。
'****************************************************
'
' SearchPattern 方法
'
' 搜索文件子程序
'
'****************************************************
Private Function SearchPattern( _
ByVal strSearchWord As String) As MatchCollection
Dim regexp As Regex
Dim strPattern
'Set the pattern to search for
regexp = New Regex("", RegexOptions.IgnoreCase)
'Search the file for the phrase
If Searchs.Site.Encoding.Equals("utf-8") Then
strPattern = "/b{0}/b"
Else
strPattern = "{0}"
End If
Return regexp.Matches(m_contents, String.Format(strPattern, _
strSearchWord), RegexOptions.IgnoreCase)
End Function
UserSearch.vb
它包含以下属性:
SearchCriteria | 保存、获取用户搜索选项 |
SearchWords | 保存、获取用户搜索的单词 |
TotalFilesSearched | 获取搜索到的总文件数 |
TotalFilesFound | 获取找到的总文件数 |
'**********************************************************
'
' SearchCriteria 属性
'
' 设置、获取网站搜索条件 '
'**********************************************************
Public Property SearchCriteria() As Searchs.SearchCriteria
Get
Return m_searchCriteria
End Get
Set(ByVal Value As Searchs.SearchCriteria)
m_searchCriteria = Value
End Set
End Property
'**********************************************************
'
' SearchWords 属性
'
'设置、获取网站搜索单词
'
'**********************************************************
Public Property SearchWords() As String
Get
Return m_searchWords
End Get
Set(ByVal Value As String)
m_searchWords = Value
End Set
End Property
'**********************************************************
'
' TotalFilesSearched 属性
'
'获取搜索到的总文件数
'
'**********************************************************
Public ReadOnly Property TotalFilesSearched() As Integer
Get
Return m_totalFilesSearched
End Get
End Property
'**********************************************************
'
' TotalFilesFound 属性
'
' 获取网站文件总数
'
'**********************************************************
Public ReadOnly Property TotalFilesFound() As Integer
Get
Return m_totalFilesFound
End Get
End Property
'**********************************************************
'
' PageDataset 共享属性
'
' 获取整个网站的数据
'
'**********************************************************
Public ReadOnly Property PageDataset() As DataSet
Get
Return m_dstPages
End Get
End Property
Search方法
搜索的实际处理从这里开始。存储搜索结果的DataSet在此创建,ProcessDirectory方法被调用。
'********************************************
'
' Search 方法
'
' 搜索整个网站
'
'********************************************
Public Function Search(ByVal targetDirectory As String) As DataSet
'如果网站是英文的则调用 Server.HtmlEncode 方法
If Searchs.Site.EnglishLanguage = True Then
'在同一个字符串中用HTML编码代替所有的HTML标签
' (停止用户输入HTML标签)
m_searchWords = m_page.Server.HtmlEncode(m_searchWords)
'如果网站不是英文的,则改变脚本标签
Else
'用“<”和“>”的HTML标签替换标签“<”和“>”
m_searchWords = Replace(m_searchWords, "<", "<", 1, -1, 1)
m_searchWords = Replace(m_searchWords, ">", ">", 1, -1, 1)
End If
If m_dstPages Is Nothing Then
m_dstPages = Searchs.PagesDataset.Create()
End If
ProcessDirectory(targetDirectory)
Return m_dstPages
End Function
ProcessDirectory方法
ProcessDirectory方法遍历所有文件并调用ProcessFile方法。然后,变量搜有子目录并调用自己。
'*********************************************
'
' ProcessDirectory 方法
'
' 搜索目录中的文件
'
'********************************************
Private Sub ProcessDirectory(ByVal targetDirectory As String)
Dim fileEntries As String()
Dim subdirectoryEntries As String()
Dim filePath As String
Dim subdirectory As String
fileEntries = Directory.GetFiles(targetDirectory)
' 处理目录中找到的文件列表
For Each filePath In fileEntries
m_totalFilesSearched += 1
ProcessFile(filePath)
Next filePath
subdirectoryEntries = Directory.GetDirectories(targetDirectory)
' 递规进入目录的子目录
For Each subdirectory In subdirectoryEntries
'检测确认要被搜索的文件夹不是被禁用的文件夹
'如果是则不搜索
If Not InStr(1, Searchs.Site.BarredFolders, _
Path.GetFileName(subdirectory), vbTextCompare) > 0 Then
'调用搜索子程序搜索网站
ProcessDirectory(subdirectory)
End If
Next subdirectory
End Sub 'ProcessDirectory
ProcessFile方法
ProcessFile调用GetInfo,GetInfo函数返回包含特定文件所有信息的Searchs.Page对象。然后,它检测matchcount是否大于0并调用CheckFileInfo函数清理保存在Page对象中的信息,然后把文件保存在PagesDataset中。
'*******************************************************
'
' ProcessFile 函数
'
' 处理已找到的文件真实过程在此开始
'
'*******************************************************
Private Sub ProcessFile(ByVal FPath As String)
Dim srchFile As Searchs.Page
srchFile = GetInfo(FPath)
If Not IsNothing(srchFile) Then
srchFile.Search(m_searchWords, m_searchCriteria)
If srchFile.MatchCount > 0 Then
m_totalFilesFound += 1
'Response.Write(srchFile.Contents)
srchFile.CheckFileInfo()
Searchs.PagesDataset.StoreFile(m_dstPages, srchFile)
End If
End If
End Sub 'ProcessFile
GetInfo方法
GetInfo方法的主要任务是获取文件的数据。它调用共享(静态)函数Searchs.FileContent.GetFileInfo大多数的工作在此完成。
'*****************************************************************
'
' GetInfo 方法
'
' 文件信息在此方法被获取
'
'*****************************************************************
Private Function GetInfo(ByVal FPath As String) As Searchs.Page
Dim fileInform As New FileInfo(FPath)
Dim sr As StreamReader
Dim srchFile As New Searchs.Page()
Dim strBldFile As New StringBuilder()
Dim strFileURL As String '保存网站中文件路径
'检测文件扩展名,确认文件
'是要被搜索的后缀的文件
If InStr(1, Searchs.Site.FilesTypesToSearch, _
fileInform.Extension, vbTextCompare) > 0 Then
'm_page.Trace.Warn("File ext.", fileInform.Extension)
'确认要被搜索的文件不是被禁止的文件
'如果是,则不搜索
If Not InStr(1, Searchs.Site.BarredFiles, _
Path.GetFileName(FPath), vbTextCompare) > 0 Then
'm_page.Trace.Warn("File", FPath)
If Not File.Exists(FPath) Then
'm_page.Trace.Warn("Error", _
'String.Format("{0} does not exist.", FPath))
'在此增加抛出异常
'
'
Return Nothing
End If
Searchs.FileContent.GetFileInfo(FPath, srchFile)
Return srchFile
End If
End If
Return Nothing
End Function
FileContent.vb
这里页面中的大量数据被检索出来。如果文件是静态文件,则文件内容通过使用GetStaticFileContent方法被读取出来;如果文件是动态的,则文件内容通过使用GetDynamicFileContent从服务器检索出来。title的信息从title标签中检索出来,meta标签中的descritpion和keywords通过调用GetMetaContent方法检索出来。通过调用Searchs.CleanHtml.Clean方法文件内容从HTML页面中被抽取出来。
'**********************************************
'
' GetFileInfo 方法
'
' 文件信息在此方法中获取
'
'**********************************************
Public Shared Sub GetFileInfo(ByVal FPath As String, _
ByVal srchFile As Searchs.Page)
Dim fileInform As New FileInfo(FPath)
Dim strBldFile As New StringBuilder()
Dim fileSize As Decimal = fileInform.Length / 1024
srchFile.Size = fileSize
GetFilePath(FPath, srchFile)
If InStr(1, Searchs.Site.DynamicFilesTypesToSearch, _
fileInform.Extension, vbTextCompare) > 0 Then
m_page.Trace.Warn("Path", String.Format("{0}/{1}", "", _
srchFile.Path))
GetDynamicFileContent(srchFile)
Else
GetStaticFileContent(FPath, srchFile)
End If
If Not srchFile.Contents.Equals("") Then
srchFile.Contents = sr.ReadToEnd()
'读取文件中title之间的内容
srchFile.Title = GetMetaContent(srchFile.Contents,_
"<title>", "</title>")
'm_page.Trace.Warn("Page Title", strPageTitle)
'读取文件中meta标签的description
srchFile.Description = GetMetaContent(srchFile.Contents,_
"<meta name=""description"" content=""", ",""">")
'm_page.Trace.Warn("Page Desc", strPageDescription)
'读取文件中meta标签的keywords
srchFile.Keywords = GetMetaContent(srchFile.Contents,_
"<meta name=""keywords"" content=""", ",""">")
'm_page.Trace.Warn("Page Keywords", strPageKeywords)
srchFile.Contents = _
Searchs.CleanHtml.Clean(srchFile.Contents)
srchFile.Contents = _
strBldFile.AppendFormat("{0} {1} {2} {3}", _
srchFile.Contents, srchFile.Description, _
srchFile.Keywords, srchFile.Title).ToString.Trim()
'm_page.Trace.Warn("File Info", strBldFile.ToString)
End If
End Sub
'******************************************************
'
' GetStaticFileContent 方法
'
' 文件内容在此方法中被获取
'
'*******************************************************
Private Shared Sub GetStaticFileContent(_
ByVal FPath As String, ByVal srchFile As Searchs.Page)
Dim sr As StreamReader
If Searchs.Site.Encoding.Equals("utf-8") Then
sr = File.OpenText(FPath)
Else
sr = New StreamReader(FPath, _
Encoding.GetEncoding(Searchs.Site.Encoding))
End If
Try
srchFile.Contents = sr.ReadToEnd()
sr.Close()
Catch ex As Exception
m_page.Trace.Warn("Error", ex.Message)
srchFile.Contents = ex.Message
End Try
End Sub
GetDynamicFileContent
GetDynamicFileContent根据编码调用两个分支函数GetDynamicFileContentOther或GetDynamicFileContentUTF总的一个。
'*********************************************************************
'
' GetDynamicFileContent Method
'
' 在此方法中文件内容被获取
'
'*********************************************************************
Private Shared Sub GetDynamicFileContent(ByVal srchFile As Searchs.Page)
Dim wcMicrosoft As System.Net.WebClient
If Searchs.Site.Encoding.Equals("utf-8") Then
GetDynamicFileContentUTF(srchFile)
Else
GetDynamicFileContentOther(srchFile)
End If
End Sub
System.Net.WebClient提供公共方法根据指定的URI资源发送或接收数据。我们使用DownloadData来从资源那里下载数据并返回字节数组。
应用程序使用encoding把原始的编码方式(Unicode)映射为其他编码方式来实现公共语言运行时;使用decoding把非原始编码方式(非Unicode)映射为原始编码方式。System.Text命名空间提供了类允许你对字符进行编码和解码。
'****************************************************************
'
' GetDynamicFileContentOther 方法
'
' 按照提供的编码方式,文件内容在此方法中被获取
'
'
'****************************************************************
Private Shared Sub GetDynamicFileContentOther( _
ByVal srchFile As Searchs.Page)
Dim wcMicrosoft As System.Net.WebClient
Dim fileEncoding As System.Text.Encoding
Try
fileEncoding = System.Text.Encoding.GetEncoding(_
Searchs.Site.Encoding)
srchFile.Contents = fileEncoding.GetString( _
wcMicrosoft.DownloadData(String.Format("{0}/{1}", _
Searchs.Site.ApplicationPath, srchFile.Path)))
Catch ex As System.Net.WebException
m_page.Trace.Warn("Error", ex.Message)
srchFile.Contents = ex.Message
Catch ex As System.Exception
m_page.Trace.Warn("Error", ex.Message)
srchFile.Contents = ex.Message
End Try
End Sub
UTF8Encoding类使用通用字符集转换格式——8位格式(UTF-8)对Unicode字符进行编码。这种编码支持所有Unicode字符值和代理字符。
'*********************************************************************
'
' GetDynamicFileContentUTF 方法
'
' 根据utf-8编码方式文件内容被获取
'
'*********************************************************************
Private Shared Sub GetDynamicFileContentUTF( _
ByVal srchFile As Searchs.Page)
Dim wcMicrosoft As System.Net.WebClient
Dim objUTF8Encoding As UTF8Encoding
Try
wcMicrosoft = New System.Net.WebClient()
objUTF8Encoding = New UTF8Encoding()
srchFile.Contents = objUTF8Encoding.GetString( _
wcMicrosoft.DownloadData(String.Format("{0}/{1}", _
Searchs.Site.ApplicationPath, srchFile.Path)))
Catch ex As System.Net.WebException
m_page.Trace.Warn("Error", ex.Message)
srchFile.Contents = ex.Message
Catch ex As System.Exception
m_page.Trace.Warn("Error", ex.Message)
srchFile.Contents = ex.Message
End Try
End Sub
GetFilePath方法
GetFilePath方法把本地文件夹路径转换成网站的URL。
'*****************************************
'
' GetFilePath 方法
'
' 在此方法中文件路径被转换成用于显示的超链接
'
'
'*****************************************
Private Shared Sub GetFilePath(ByVal strFileURL As String,_
ByVal srchFile As Searchs.Page)
'把服务器文件路径转换成URL文件路径
strFileURL = Replace(strFileURL, m_page.Server.MapPath("./"), "")
'把文件URL中,NT使用的反斜杠替换成
'互联网使用的斜杠
strFileURL = Replace(strFileURL, "/", "/")
'把文件名和路径编码成URL的编码方法
strFileURL = m_page.Server.UrlEncode(strFileURL)
'为了防止编码后出现反斜杠(斜杠)
strFileURL = Replace(strFileURL.Trim(), _
"% 2f", "/", vbTextCompare)
srchFile.Path = strFileURL
m_page.Trace.Warn("Url", srchFile.Path)
End Sub
GetMetaContent方法
GetMetaContent方法使用正则表达式剔除标签并获取需要的信息。
'************************************************
'
' GetMetaContent 方法
'
' Meta的内容在此函数中被剥离
'
'************************************************
Private Shared Function GetMetaContent(ByVal strFile As String, _
ByVal strMetaStart As String, ByVal strMetaEnd As String) As String
'列出在title标签之间的文本
Dim regexp As Regex
Dim strMeta As String
Dim strPattern As String
Dim strInPattern As String
'如果在meta标签中 description 或 keywords 没有找到 ,则
'使用http-equiv= 代替 name=
If InStr(1, LCase(strFile), strMetaStart, 1) = 0 _
And InStr(strMetaStart, "name=") Then
'把 name= 替换成 http-equiv=
strMetaStart = Replace(strMetaStart, "name=", "http-equiv=")
End If
'建立模式
strInPattern = "((.|/n)*?)"
strPattern = String.Format("{0}{1}{2}", _
strMetaStart, strInPattern, strMetaEnd)
regexp = New Regex(strPattern, RegexOptions.IgnoreCase)
'匹配模式
strMeta = regexp.Match(strFile).ToString
'建立模式
strInPattern = "(.*?)"
strPattern = String.Format("{0}{1}{2}", _
strMetaStart, strInPattern, strMetaEnd)
'获取模式内容
strMeta = regexp.Replace(strMeta, strPattern,_
"$1", RegexOptions.IgnoreCase)
Return strMeta
End Function
PagesDataset.vb
这个类用来创建DataSet,它有两个方法组成Create和StoreFile。Create方法创建存储搜索结果的DataSet,StoreFile负责把记录添加到DataSet的DataTable中。
'*******************************************************
'
' Create 方法 – 共享(静态)方法
'
' 为页面创建dataset并返回结果
'
'********************************************************
Public Shared Function Create() As DataSet
'对象被定义
Dim pgDataSet As New DataSet()
Dim keys(1) As DataColumn
'表被创建并加入Table集合
pgDataSet.Tables.Add(New DataTable("Pages"))
'表的结构被定义
pgDataSet.Tables("Pages").Columns.Add("PageId", _
System.Type.GetType("System.Int32"))
pgDataSet.Tables("Pages").Columns.Add("Title",_
System.Type.GetType("System.String"))
pgDataSet.Tables("Pages").Columns.Add("Description", _
System.Type.GetType("System.String"))
pgDataSet.Tables("Pages").Columns.Add("Path", _
System.Type.GetType("System.String"))
pgDataSet.Tables("Pages").Columns.Add("MatchCount", _
System.Type.GetType("System.Int32"))
pgDataSet.Tables("Pages").Columns.Add("Size", _
System.Type.GetType("System.Decimal"))
'PageId 被定义为标识
pgDataSet.Tables("Pages").Columns("PageID").AutoIncrement = True
pgDataSet.Tables("Pages").Columns("PageID").AutoIncrementSeed = 1
'PageId 被定义为主键
keys(0) = pgDataSet.Tables("Pages").Columns("PageId")
pgDataSet.Tables("Pages").PrimaryKey = keys
Return pgDataSet
End Function
'********************************************************
'
' StoreFile 方法 – 共享(静态)方法
'
'为页面创建dataset并返回结果
'
'********************************************************
Public Shared Sub StoreFile(ByVal dstPgs As DataSet,_
ByVal srchPg As Searchs.Page)
'对象被定义
Dim pageRow As DataRow
'新行被创建
pageRow = dstPgs.Tables("Pages").NewRow()
'数据被设置
pageRow("Title") = srchPg.Title
pageRow("Description") = srchPg.Description
pageRow("Path") = srchPg.Path
pageRow("MatchCount") = srchPg.MatchCount
pageRow("Size") = srchPg.Size
'行被加入dataset
dstPgs.Tables("Pages").Rows.Add(pageRow)
End Sub
CleanHtml.vb
CleanHtml类包含单独一个共享(静态)函数,这个函数使用正则表达式清空HTML内容。
'*****************************************************
'
' CleanFileContent 方法
'
' 清空html文件内容的子程序
'
'*****************************************************
Public Shared Function Clean(ByVal Contents As String) As String
Dim regexp As Regex
Dim strPattern As String
strPattern = ""
regexp = New Regex(strPattern, RegexOptions.IgnoreCase)
Contents = regexp.Replace(Contents, _
"<(select|option|script|style|title)(.*?)" & _
">((.|/n)*?)</(SELECT|OPTION|SCRIPT|STYLE|TITLE)>",_
" ", RegexOptions.IgnoreCase)
Contents = regexp.Replace(Contents, "&(nbsp|quot|copy);", "")
'Contents = regexp.Replace(Contents, "<[^>]*>", "")
Contents = regexp.Replace(Contents, "<([/s/S])+?>",_
" ", RegexOptions.IgnoreCase).Replace(" ", " ")
'Contents = regexp.Replace(Contents, "<[^<>]+>",_
" ", RegexOptions.IgnoreCase)
'Contents = regexp.Replace("(<(/w+)[^>]*?>(.*?)<//1>", "$1")
Contents = regexp.Replace(Contents, "/W", " ")
'Trace.Warn("File Contents", Contents)
Return Contents
End Function
Site.vb
Site类由保存整个网站配置信息的共享(静态)属性组成。这些属性使用ConfigurationSettings.AppSettings从web.config文件获取值。
以下是Site类的属性:
FilesTypesToSearch | 返回要搜索的文件类型 |
DynamicFilesTypesToSearch | 返回要搜索的动态文件 |
BarredFolders | 返回禁止的文件夹 |
EnglishLanguage | 返回一个Boolean值,表示语言是否为英语 |
Encoding | 返回网站编码 |
BarredFiles | 返回禁止的文件 |
ApplicationPath | 设置或返回应用程序路径 |
'*************************************************
'
' FilesTypesToSearch 只读属性
'
' 获取网站中要搜索的文件类型
'
'*************************************************
Public Shared ReadOnly Property FilesTypesToSearch() As String
Get
Return ConfigurationSettings.AppSettings(
"FilesTypesToSearch")
End Get
End Property
'*************************************************
'
' DynamicFilesTypesToSearch 只读属性
'
' 获取网站中要搜索的动态文件
'
'*************************************************
Public Shared ReadOnly Property DynamicFilesTypesToSearch() As String
Get
Return ConfigurationSettings.AppSettings(_
"DynamicFilesTypesToSearch")
End Get
End Property
'*************************************************
'
' BarredFolders 只读属性
'
' 获取网站中禁止的文件夹
'
'*************************************************
Public Shared ReadOnly Property BarredFolders() As String
Get
Return ConfigurationSettings.AppSettings("BarredFolders")
End Get
End Property
'*************************************************
'
' BarredFiles 只读属性
'
' 获取网站中禁止的文件
'
'*************************************************
Public Shared ReadOnly Property BarredFiles() As String
Get
Return ConfigurationSettings.AppSettings("BarredFiles")
End Get
End Property
'*************************************************
'
' EnglishLanguage 属性
'
' 获取语言是否为英语
'
'*************************************************
Public Shared ReadOnly Property EnglishLanguage() As String
Get
Return ConfigurationSettings.AppSettings("EnglishLanguage")
End Get
End Property
'*********************************************************************
'
' Encoding 属性
'
' 获取网站的编码
'
'*********************************************************************
Public Shared ReadOnly Property Encoding() As String
Get
Return ConfigurationSettings.AppSettings("Encoding")
End Get
End Property
'**********************************************************
'
' ApplicationPath 属性
'
' 设置或获取应用程序路径
'
'**********************************************************
Public Property ApplicationPath() As String
Get
Return m_ApplicationPath
End Get
Set(ByVal Value As String)
m_ApplicationPath = Value
End Set
End Property
Web.Config
ASP.NET配置系统建立一个可扩展的基本机构,允许你在你的ASP.NET应用程序部署的时候定义配置设置,因此你可以在任何时候增加或修改配置设置,这使对Web应用程序和服务器的操作影响降到最小。多个以Web.Config命名的配置文件可以出现在一个ASP.NET Web应用程序服务器的多个文件夹里面。每个Web.Config文件应用配置设置到它所在的文件夹和它所在的子文件夹。正如前面所述,网站的配置可以设置在web.config文件中。
<appSettings>
<!—在下面行中放置你要搜索的文件类型的名称,以逗号分隔 -->
<add key="FilesTypesToSearch" value=".htm,.html,.asp,.shtml,.aspx" />
<!--在下面行中放置你要搜索的动态文件类型的名称,以逗号分隔-->
<add key="DynamicFilesTypesToSearch" value=".asp,.shtml,.aspx" />
<!--在下面行中放置你不要搜索的文件夹名称,以逗号分隔-->
<add key="BarredFolders"
value="aspnet_client,_private,_vti_cnf,_vti_log,_vti_pvt,
_vti_script,_vti_txt,cgi_bin,_bin,bin,_notes,images,scripts"
/>
<!--在下面行中放置你不要搜索的文件名称(包括文件扩展名),以逗号分隔-->
<add key="BarredFiles"
value="localstart.asp,iisstart.asp,AssemblyInfo.vb,
Global.asax,Global.asax.vb,SiteSearch.aspx"
/>
<!—如果你的网站语言不是英语的,把这个boolean值设置为False-->
<add key="EnglishLanguage" value="True" />
<!—把这个值设置成网站编码-->
<add key="Encoding" value="utf-8" />
</appSettings>
如何整合
应用程序已经通过根目录中的web窗体页面SiteSearch.aspx测试,所以我的建议是你使用同样的方式测试。然后,你可以尝试把它移到任何(网站)子目录。我的所有类已经放到components文件夹,你可以把它放到你的任何文件夹。
注意:
1.对于那些没有Visual Studio.Net的用户
1.从超链接“下载最新版本的示例工程(Visual Studio .NET 非必须的)”下载
2.把SearchDotnet.dll放在根目录的bin文件夹里
3.把SiteSearch.aspx和web.config放在根目录
2.使用XML版本
1.从超链接“下载读写XML的示例工程 (VB.NET) ”
2.项目包含以下文件:
a.AdminiSearch.aspx用来把xml写入文件。
b.SiteSearch.aspx用来搜索文件。
c.我的所有类已经放入components文件夹。
错误
当应用程序被放到根目录的时候,你可能会得到以下错误。“The remote server returned an error: (401) Unauthorized.”或者“The remote server returned an error: (500) Internal Server Error.”
造成这个错误的原因是:
1.如果服务器返回“(401) Unauthorized”,应用程序由于访问权限不能读取文件。
2.如果服务器返回“(500) Internal Server Error”,正在尝试读取的页面返回一个错误,应用程序正在尝试读取的页面因为出现一个错误或者需要参数而返回错误。
按照以下步骤修改错误
1.在web.config中确认BarredFolders列表包含“aspnet_client,_private,_vti_cnf, _vti_log,_vti_pvt,_vti_script,_vti_txt, cgi_bin,_bin,bin,_notes,images,scripts”
2.确认BarredFiles列表是否全面,并且包含“localstart.asp,iisstart.asp”
全球化
搜索引擎模块可以轻易被全球化。以此为目的,我们来看看怎样把它转换成汉语语言。
Web.config
XML声明必须出现在文档任何内容(包括空格),在“<”开始之前的第一行。
在文档中XML声明由以下组成“<?xml version="1.0" encoding="Your Encoding" ?>”,默认情况下,visual studio使用utf-8编码,这需要改变成你要使用的编码。这里我们把它改成gb2312。因此XML声明需要被修改成以下所示:
英语
<?xml version="1.0" encoding="utf-8" ?>
汉语
<?xml version="1.0" encoding="gb2312" ?>
requestEncoding和responseEncoding指定请求和响应的可能编码。Machine.config在.NETFramework安装的时候创建,包含在Machine.config文件的<globalization>标签中默认编码是UTF-8。如果编码没有在Manchine.config或Web.config文件指定,编码默认按照电脑的区域选项的本地设置来指定。我们需要更改requestEncoding和responseEncoding来反映编码的改变。
英语
<globalization requestEncoding="utf-8" responseEncoding="utf-8" />
汉语
<globalization requestEncoding="gb2312" responseEncoding="gb2312" />
为了避免当编码改变的时候生成代码,我们需要在appsettings增加encoding设置。
<!—把网站的编码设置在这里-->
<add key="Encoding" value="gb2312" />
并把EnglishLanguage键设为False。
<!—如果你的网站语言不是英语的,把这个boolean值设置为False-->
<add key="EnglishLanguage" value="False" />
SiteSearch.aspx
最后同样重要的是,codepage属性必须增加在页面的指令中。
英语
<%@ Page Language="vb" Trace="False" AutoEventWireup="false" Codebehind="SiteSearch.aspx.vb" Inherits="SearchDotnet.SiteSearch" debug="false" %>
汉语
<%@ Page Language="vb" Trace="False" AutoEventWireup="false" Codebehind="SiteSearch.aspx.vb" Inherits="SearchDotnet.SiteSearch" debug="false" codePage="936" %>
强化代码
此应用程序是为小型站点编写的,对于更大的站点,代码可以进一步加强。事实上你可能需要周期性地写入到数据库(比如XML文件),然后从那里读取。我将为此提供一些提示。(我已经包含进一个“读写XML示例工程”)
(1)在我的代码中,我使用正则表达式来搜索并过滤数据,不然你将要通过以下方法把全部数据(非过滤的数据)写入XML文件。
Private Shared Sub WriteXmlToFile(ByVal thisDataSet As DataSet)
If thisDataSet Is Nothing Then
Return
End If
thisDataSet.WriteXml(XMLFile)
End Sub
(2)然后你将需要读取xml文件并保存到dataset ,Searchs.Site.PageDataset.Tables("Pages").中
Private Shared Function ReadXmlFromFile() As DataSet
' 创建 DataSet.
Dim newDataSet As New DataSet("New DataSet")
' 读取后台 XML 文档
' 创建读取文件的文件流.
Dim fsReadXml As New System.IO.FileStream(XMLFile,
System.IO.FileMode.Open)
' 创建一个 XmlTextReader 来读取文件.
Dim myXmlReader As New System.Xml.XmlTextReader(fsReadXml)
' 把XML文档读入DataSet.
newDataSet.ReadXml(myXmlReader)
' 关闭 XmlTextReader
myXmlReader.Close()
Return newDataSet
End Function
(3)每次搜索,你将要使用PageDataset.Tables的Select方法来筛选搜索结果,筛选dataset正是如此。FillDataset方法包含创建添加搜索结果(DataRow数组)到数据库的逻辑。
Private Sub FiterPagesDatset()
Dim strExpr As String
Dim foundRows As DataRow()
Dim Field() As String = {
"Title", "Description", "Keywords", "Contents"}
strExpr = SomeFunction '你的建立查询的功能.
foundRows = Searchs.Site.PageDataset.Tables(
"Pages").Select(strExpr)
FillDataset(foundRows)
End Sub
(4)筛选的结果被保存在另外一个dataset中,并用他来显示结果。
有趣的地方
当我在做这个项目的时候,我的问题是怎样显示结果。DataGrid是我的选择,因为我们可以开发出很多其他列表控件所没有的特性。一旦我的问题得到解决,下一步就是怎样传递内容到DataGrid,DataSet是唯一可选的。在保存信息到DataSet之前,当我进一步处理时,我不得不把页面的大量信息移来移去。我决定使用Site对象来保存信息。
作者之一建议下面的最优方法:
a.类应该要小到大约只有几个方法和属性。
b.方法应该短小到大约只有几行。
经过仔细的分析代码和保持最优方法在心里,我重新设计代码成现在的样子。
历史
l 修改代码可以读取动态页面。
l 增强代码支持全球化。
l 我已经加入一个读写XML的示例工程。
许可
本文没有附加明确的许可,但是在文章正文或者下载文件中可能包含使用条款,如果有疑问请通过下面讨论区和作者联系。
可能使用的许可作者列表可以在这里找到。
关于作者
| Stevan 是微软认证.NET架构解决方案开发专家(早期MCSD.Net获得者 – 世界范围内前2500之一), 微软认证.NET应用程序开发专家 – MCAD.Net (特许会员 – 世界范围内前5000名的开发工程师之一).
|
其他有名的应用程序和工具文章:
A hierarchical task manager with native XML support for custom reporting.
A memory leak detector for Visual C++ packaged in an easy to use library!
KeePass is a free, open-source, light-weight and easy-to-use password safe.
This tool automatically converts Visual C++ 7.0 projects back to Visual C++ 6.0 projects.
A drag & drop ImageList and ToolBar generator . Simplifies creation of large image lists and supports effects .