今天來分析下 嗨-部落格 中的搜尋功能。搜尋功能在個人網站裡面要有這麼個東西,但又不是特别重要。是以我們需要有,可以不用太深入了解,畢竟我們不是專門做搜尋這塊的。
是以,我打算把搜尋分兩塊。一塊是,用Lucene.Net實作站内搜尋。一塊是利用第三方搜尋引擎來 實作站内搜尋。
前言
這次開發的部落客要功能或特點:
第一:可以相容各終端,特别是手機端。
第二:到時會用到大量html5,炫啊。
第三:導入部落格園的精華文章,并做分類。(不要封我)
第四:做個插件,任何網站上的技術文章都可以轉發收藏 到本部落格。
是以打算寫個系類:《一步步搭建自己的部落格》
- 一步步開發自己的部落格 .NET版(1、頁面布局、blog遷移、資料加載)
- 一步步開發自己的部落格 .NET版(2、評論功能)
- 一步步開發自己的部落格 .NET版(3、注冊登入功能)
- 一步步開發自己的部落格 .NET版(4、文章釋出功能)
- 一步步開發自己的部落格 .NET版(5、搜尋功能)
- 一步步開發自己的部落格 .NET版(6、手機端的相容)
示範位址:http://haojima.net/ 群内共享源碼:469075305
Lucene.Net簡介
Lucene.net是Lucene的.net移植版本,是一個開源的全文檢索引擎開發包,即它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎。開發人員可以基于Lucene.net實作全文檢索的功能。
Lucene.net是Apache軟體基金會贊助的開源項目,基于Apache License協定。
Lucene.net并不是一個爬行搜尋引擎,也不會自動地索引内容。我們得先将要索引的文檔中的文本抽取出來,然後再将其加到Lucene.net索引中。标準的步驟是先初始化一個Analyzer、打開一個IndexWriter、然後再将文檔一個接一個地加進去。一旦完成這些步驟,索引就可以在關閉前得到優化,同時所做的改變也會生效。這個過程可能比開發者習慣的方式更加手工化一些,但卻在資料的索引上給予你更多的靈活性,而且其效率也很高。(來源百度百科)
Lucene幫助類
其實 在之前 我也是接觸到過Lucene.net,那也是自己 做的個小玩意(部落格備份小工具3) 瞎折騰的。但是 這次打算遷移到這個系統中,不知怎麼的 報錯了。可能是這次用的是 .net 4.5。Lucene這東西太高深,我也沒打算深究。于是 在網上收索了一把,資料還挺多的。《lucene.net 3.0.3、結合盤古分詞進行搜尋的小例子(分頁功能)》 我随意看了下,這裡有個 幫助類 挺不錯的,也還符合 我這樣想要的效果。這裡來分析下這個幫助類。
1.首先建立索引。
IndexWriter writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED);
Document doc = new Document();
doc.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED));
writer.AddDocument(doc);
這裡的
directory_luce 是索引建立路徑
analyzer 分析器
value 是對應 存入索引額名字和值
2.從索引裡面搜尋
string[] fileds = { "title", "content" };//查詢字段
QueryParser parser = null;
parser = new MultiFieldQueryParser(version, fileds, analyzer);//多個字段查詢
Query query = parser.Parse(keyword);
int n = 1000;
IndexSearcher searcher = new IndexSearcher(directory_luce, true);//true-表示隻讀
TopDocs docs = searcher.Search(query, (Filter)null, n);
if (docs == null || docs.TotalHits == 0)
{
return null;
}
else
{
List<SearchResult> list = new List<SearchResult>();
int counter = 1;
foreach (ScoreDoc sd in docs.ScoreDocs)//周遊搜尋到的結果
{
try
{
Document doc = searcher.Doc(sd.Doc);
int id = int.Parse(doc.Get("id"));
string title = doc.Get("title");
string content = doc.Get("content");
string blogTag = doc.Get("blogTag");
string url = doc.Get("url");
int flag = int.Parse(doc.Get("flag"));
int clickQuantity = int.Parse(doc.Get("clickQuantity"));
string createdate = doc.Get("createdate");
PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>");
PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new PanGu.Segment());
highlighter.FragmentSize = 50;
content = highlighter.GetBestFragment(keyword, content);
string titlehighlight = highlighter.GetBestFragment(keyword, title);
if (titlehighlight != "") title = titlehighlight;
list.Add(new SearchResult(title, content, url, blogTag, id, clickQuantity, flag));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
counter++;
}
return list;
3.完整代碼
public class PanGuLuceneHelper
{
private PanGuLuceneHelper() { }
#region 單一執行個體
private static PanGuLuceneHelper _instance = null;
/// <summary>
/// 單一執行個體
/// </summary>
public static PanGuLuceneHelper instance
{
get
{
if (_instance == null) _instance = new PanGuLuceneHelper();
return _instance;
}
}
#endregion
#region 00一些屬性和參數
#region Lucene.Net的目錄-參數
private Lucene.Net.Store.Directory _directory_luce = null;
/// <summary>
/// Lucene.Net的目錄-參數
/// </summary>
public Lucene.Net.Store.Directory directory_luce
{
get
{
if (_directory_luce == null) _directory_luce = Lucene.Net.Store.FSDirectory.Open(directory);
return _directory_luce;
}
}
#endregion
#region 索引在硬碟上的目錄
private System.IO.DirectoryInfo _directory = null;
/// <summary>
/// 索引在硬碟上的目錄
/// </summary>
public System.IO.DirectoryInfo directory
{
get
{
if (_directory == null)
{
string dirPath = AppDomain.CurrentDomain.BaseDirectory + "SearchIndex";
if (System.IO.Directory.Exists(dirPath) == false) _directory = System.IO.Directory.CreateDirectory(dirPath);
else _directory = new System.IO.DirectoryInfo(dirPath);
}
return _directory;
}
}
#endregion
#region 分析器
private Analyzer _analyzer = null;
/// <summary>
/// 分析器
/// </summary>
public Analyzer analyzer
{
get
{
{
_analyzer = new Lucene.Net.Analysis.PanGu.PanGuAnalyzer();//
}
return _analyzer;
}
}
#endregion
#region 版本号枚舉類
private static Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30;
/// <summary>
/// 版本号枚舉類
/// </summary>
public Lucene.Net.Util.Version version
{
get
{
return _version;
}
}
#endregion
#endregion
#region 01建立索引
/// <summary>
/// 建立索引(先删 後更新)
/// </summary>
/// <param name="datalist"></param>
/// <returns></returns>
public bool CreateIndex(List<SearchResult> datalist)
{
IndexWriter writer = null;
try
{
writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED);//false表示追加(true表示删除之前的重新寫入)
}
catch
{
writer = new IndexWriter(directory_luce, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);//false表示追加(true表示删除之前的重新寫入)
}
foreach (SearchResult data in datalist)
{
writer.DeleteDocuments(new Term("id", data.id.ToString()));//新增前 删除 不然會有重複資料
CreateIndex(writer, data);
}
writer.Optimize();
writer.Dispose();
return true;
}
public bool CreateIndex(SearchResult data)
{
List<SearchResult> datalist = new List<SearchResult>();
datalist.Add(data);
return CreateIndex(datalist);
}
public bool CreateIndex(IndexWriter writer, SearchResult data)
{
try
{
if (data == null) return false;
Document doc = new Document();
Type type = data.GetType();//assembly.GetType("Reflect_test.PurchaseOrderHeadManageModel", true, true); //命名空間名稱 + 類名
//建立類的執行個體
//object obj = Activator.CreateInstance(type, true);
//擷取公共屬性
PropertyInfo[] Propertys = type.GetProperties();
for (int i = 0; i < Propertys.Length; i++)
{
//Propertys[i].SetValue(Propertys[i], i, null); //設定值
PropertyInfo pi = Propertys[i];
string name = pi.Name;
object objval = pi.GetValue(data, null);
string value = objval == null ? "" : objval.ToString(); //值
if (name == "id" || name == "flag")//id在寫入索引時必是不分詞,否則是模糊搜尋和删除,會出現混亂
{
doc.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED));//id不分詞
}
else
{
doc.Add(new Field(name, value, Field.Store.YES, Field.Index.ANALYZED));
}
}
writer.AddDocument(doc);
}
catch (System.IO.FileNotFoundException fnfe)
{
throw fnfe;
}
return true;
}
#endregion
#region 02在title和content字段中查詢資料
/// <summary>
/// 在title和content字段中查詢資料
/// </summary>
/// <param name="keyword"></param>
/// <returns></returns>
public List<SearchResult> Search(string keyword)
{
string[] fileds = { "title", "content" };//查詢字段
QueryParser parser = null;
parser = new MultiFieldQueryParser(version, fileds, analyzer);//多個字段查詢
Query query = parser.Parse(keyword);
int n = 1000;
IndexSearcher searcher = new IndexSearcher(directory_luce, true);//true-表示隻讀
TopDocs docs = searcher.Search(query, (Filter)null, n);
if (docs == null || docs.TotalHits == 0)
{
return null;
}
else
{
List<SearchResult> list = new List<SearchResult>();
int counter = 1;
foreach (ScoreDoc sd in docs.ScoreDocs)//周遊搜尋到的結果
{
try
{
Document doc = searcher.Doc(sd.Doc);
int id = int.Parse(doc.Get("id"));
string title = doc.Get("title");
string content = doc.Get("content");
string blogTag = doc.Get("blogTag");
string url = doc.Get("url");
int flag = int.Parse(doc.Get("flag"));
int clickQuantity = int.Parse(doc.Get("clickQuantity"));
string createdate = doc.Get("createdate");
PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>");
PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new PanGu.Segment());
highlighter.FragmentSize = 50;
content = highlighter.GetBestFragment(keyword, content);
string titlehighlight = highlighter.GetBestFragment(keyword, title);
if (titlehighlight != "") title = titlehighlight;
list.Add(new SearchResult(title, content, url, blogTag, id, clickQuantity, flag));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
counter++;
}
return list;
}
//st.Stop();
//Response.Write("查詢時間:" + st.ElapsedMilliseconds + " 毫秒<br/>");
}
#endregion
#region 03在不同的分類下再根據title和content字段中查詢資料(分頁)
/// <summary>
/// 在不同的類型下再根據title和content字段中查詢資料(分頁)
/// </summary>
/// <param name="_flag">分類,傳空值查詢全部</param>
/// <param name="keyword"></param>
/// <param name="PageIndex"></param>
/// <param name="PageSize"></param>
/// <param name="TotalCount"></param>
/// <returns></returns>
public List<SearchResult> Search(string _flag, string keyword, int PageIndex, int PageSize)
{
if (PageIndex < 1) PageIndex = 1;
Stopwatch st = Stopwatch.StartNew();
st.Start();
BooleanQuery bq = new BooleanQuery();
if (_flag != "")
{
QueryParser qpflag = new QueryParser(version, "flag", analyzer);
Query qflag = qpflag.Parse(_flag);
bq.Add(qflag, Occur.MUST);//與運算
}
if (keyword != "")
{
string[] fileds = { "blogTag", "title", "content" };//查詢字段
QueryParser parser = null;// new QueryParser(version, field, analyzer);//一個字段查詢
parser = new MultiFieldQueryParser(version, fileds, analyzer);//多個字段查詢
Query queryKeyword = parser.Parse(keyword);
bq.Add(queryKeyword, Occur.MUST);//與運算
}
TopScoreDocCollector collector = TopScoreDocCollector.Create(PageIndex * PageSize, false);
IndexSearcher searcher = new IndexSearcher(directory_luce, true);//true-表示隻讀
searcher.Search(bq, collector);
if (collector == null || collector.TotalHits == 0)
{
//TotalCount = 0;
return null;
}
else
{
int start = PageSize * (PageIndex - 1);
//結束數
int limit = PageSize;
ScoreDoc[] hits = collector.TopDocs(start, limit).ScoreDocs;
List<SearchResult> list = new List<SearchResult>();
int counter = 1;
//TotalCount = collector.TotalHits;
st.Stop();
//st.ElapsedMilliseconds;//毫秒
foreach (ScoreDoc sd in hits)//周遊搜尋到的結果
{
try
{
Document doc = searcher.Doc(sd.Doc);
int id = int.Parse(doc.Get("id"));
string title = doc.Get("title");
string content = doc.Get("content");
string blogTag = doc.Get("blogTag");
string url = doc.Get("url");
int flag = int.Parse(doc.Get("flag"));
int clickQuantity = int.Parse(doc.Get("clickQuantity"));
content = Highlight(keyword, content);
//string titlehighlight = Highlight(keyword, title);
//if (titlehighlight != "") title = titlehighlight;
list.Add(new SearchResult(title, content, url, blogTag, id, clickQuantity, flag));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
counter++;
}
return list;
}
}
#endregion
#region 把content按照keywords進行高亮
/// <summary>
/// 把content按照keywords進行高亮
/// </summary>
/// <param name="keywords"></param>
/// <param name="content"></param>
/// <returns></returns>
private static string Highlight(string keywords, string content)
{
SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<strong>", "</strong>");
Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new Segment());
highlighter.FragmentSize = 200;
return highlighter.GetBestFragment(keywords, content);
}
#endregion
#region 04删除索引
#region 删除索引資料(根據id)
/// <summary>
/// 删除索引資料(根據id)
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public bool Delete(string id)
{
bool IsSuccess = false;
Term term = new Term("id", id);
IndexWriter writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED);
writer.DeleteDocuments(term); // writer.DeleteDocuments(term)或者writer.DeleteDocuments(query);
writer.Commit();
IsSuccess = writer.HasDeletions();
writer.Dispose();
return IsSuccess;
}
#endregion
#region 删除全部索引資料
/// <summary>
/// 删除全部索引資料
/// </summary>
/// <returns></returns>
public bool DeleteAll()
{
bool IsSuccess = true;
try
{
IndexWriter writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED);
writer.DeleteAll();
writer.Commit();
IsSuccess = writer.HasDeletions();
writer.Dispose();
}
catch
{
IsSuccess = false;
}
return IsSuccess;
}
#endregion
#endregion
#region 分詞測試
/// <summary>
/// 分詞測試
/// </summary>
/// <param name="keyword"></param>
/// <returns></returns>
public string Token(string keyword)
{
string ret = "";
System.IO.StringReader reader = new System.IO.StringReader(keyword);
Lucene.Net.Analysis.TokenStream ts = analyzer.TokenStream(keyword, reader);
bool hasNext = ts.IncrementToken();
Lucene.Net.Analysis.Tokenattributes.ITermAttribute ita;
while (hasNext)
{
ita = ts.GetAttribute<Lucene.Net.Analysis.Tokenattributes.ITermAttribute>();
ret += ita.Term + "|";
hasNext = ts.IncrementToken();
}
ts.CloneAttributes();
reader.Close();
analyzer.Close();
return ret;
}
#endregion
}
View Code
public class SearchResult
{
public SearchResult() { }
public SearchResult(string title, string content, string url, string blogTag, int id, int clickQuantity, int flag)
{
this.blogTag = blogTag;
this.clickQuantity = clickQuantity;
this.content = content;
this.id = id;
this.url = url;
this.title = title;
this.flag = flag;
}
/// <summary>
/// 标題
/// </summary>
public string title { get; set; }
/// <summary>
/// 正文内容
/// </summary>
public string content { get; set; }
/// <summary>
/// url位址
/// </summary>
public string url { get; set; }
/// <summary>
/// tag标簽
/// </summary>
public string blogTag { get; set; }
/// <summary>
/// 唯一id
/// </summary>
public int id { get; set; }
/// <summary>
/// 點選量
/// </summary>
public int clickQuantity { get; set; }
/// <summary>
/// 标記(使用者)
/// </summary>
public int flag { get; set; }
}
必應站内搜尋
1.為什麼要用必應搜尋?
因為我們現在做的主要功能是部落格系統,搜尋隻是其中的一小塊環節。而 我對這搜尋并不了解,是以就用第三方搜尋,省事嘛。
2.為什麼不用别的三方收索呢?
百度?不用說了,咱們程式員都懂的。谷歌?我倒是想用,可生在天朝,也是沒得辦法。選來選去 還是選了必應。
3.怎麼來使用第三方的站内搜尋?
格式如下:http://cn.bing.com/search?q=關鍵字+site:網站位址
例如:http://cn.bing.com/search?q=部落格+site:blog.haojima.net
效果圖:
嘿嘿,如此之簡單。既然都已經看到效果了,那麼 我們可以幹些什麼呢?
我打算 直接把結果 顯示在我的 站内搜尋結果。為什麼 不直接跳轉到這個頁面顯示 搜尋結果?因為 這個頁面有廣告什麼的,不能按照我自己的方式顯示。我直接把結果放我的搜尋頁面 可以和 我上面用Lucene.net的搜尋結果一起顯示,這樣豈不是 顯得更專業。
,不知道的 還以為 是我自己怎麼弄出來的。那麼 我們怎麼解析 搜到的結果呢?我這裡推薦下 Jumony 之前我一直是用 HtmlAgilityPack ,現在為什麼不用了,因為有了更好的。HtmlAgilityPack 缺點是 要去xpath,然 如果頁面存在js動态改變文檔結構的話,我們直接F12 複制出來的 xpath是不準的。那麼有人 會說 HtmlAgilityPack 我已經用習慣了,不想 重新學習Jumony 。這裡我告訴你錯了,根本就需要重新學習,如果你會jquery 的話。常用功能文法基本一樣,還支援拉姆達表達式,爽的一逼。
我們來看看 怎麼使用Jumony 解析 解鎖結果吧。
var document = jumony.LoadDocument(url);
var list = document.Find("#b_results .b_algo").ToList().Select(t => t.ToString()).ToList();
兩行代碼搞定,還直接轉成了list集合。在頁面循環加載就ok了。
站内下的某個使用者内搜尋
我個人覺得 這是個蠻實用的功能,我們有時候 寫了部落格(很多時候我們寫部落格就是把自己怕會忘記的知識點 整理記錄),而後期找不到。那麼通過這個功能 可以很好的解決我們的問題。我們不想全站搜尋,隻搜尋自己的内容就可以了。
頁面還是用全站的搜尋頁面,我們直接在搜尋關鍵詞上做手腳就可以了。比如,我們想搜尋 zhaopei 使用者下的内容,那麼我們可以要搜尋的關鍵字前面加上 blog:zhaopei 那麼完整的搜尋關鍵字就成了 blog:zhaopei 關鍵字
那麼 我們要做的就是 在使用者頁面 搜尋 就在關鍵字 前面加上 blog:使用者名 我們在搜尋 頁面解析的時候 需要做的就是 分解關鍵字 blog:使用者名 關鍵字 先用空格 分割 然後如果中間有 空格的話 ,然後判斷 前面五個字元是不是 blog: 然後截取 到使用者名和 關鍵字。
我們下面具體看看 在Lucene.net 和 必應搜尋裡面是怎麼做的。
1.Lucene.net
#region 加載 Lucene.net 的搜尋結果
/// <summary>
/// 加載 Lucene.net 的搜尋結果
/// </summary>
/// <returns></returns>
public ActionResult ShowLuceneResult()
{
if (!Request.QueryString.AllKeys.Contains("key"))
return null;
string key = Request.QueryString["key"];
var zhankey = key.Split(' ');//分割關鍵字
var blogName = string.Empty;
if (zhankey.Length >= 2)
{
var str = zhankey[0].Trim();
if (str.Length > 6 && str.Substring(0, 5) == "blog:")
blogName = str.Substring(5);//取得使用者名
}
string userid = Request.QueryString.AllKeys.Contains("userid") ? Request.QueryString["userid"] : "";
//這裡判斷是否 使用者名不為空 然後取得使用者對應的 使用者ID (因為 我在做Lucene 是用使用者id 來标記的)
if (!string.IsNullOrEmpty(blogName))
{
key = key.Substring(key.IndexOf(' '));
var userinfo = CacheData.GetAllUserInfo().Where(t => t.UserName == blogName).FirstOrDefault();
if (null != userinfo)
userid = userinfo.Id.ToString();
}
string pIndex = Request.QueryString.AllKeys.Contains("p") ? Request.QueryString["p"] : "";
int PageIndex = 1;
int.TryParse(pIndex, out PageIndex);
int PageSize = 10;
var searchlist = PanGuLuceneHelper.instance.Search(userid, key, PageIndex, PageSize);
return PartialView(searchlist);
}
#endregion
2. 必應搜尋
#region 加載 bing 的搜尋結果
/// <summary>
/// 加載 bing 的搜尋結果
/// </summary>
/// <returns></returns>
public ActionResult ShowBingResult()
{
if (!Request.QueryString.AllKeys.Contains("key"))
return null;
string key = Request.QueryString["key"];//搜尋關鍵字
JumonyParser jumony = new JumonyParser();
//http://cn.bing.com/search?q=AJAX+site%3ablog.haojima.net&first=11&FORM=PERE
string pIndex = Request.QueryString.AllKeys.Contains("p") ? Request.QueryString["p"] : "";
int PageIndex = 1;
int.TryParse(pIndex, out PageIndex);
PageIndex--;
//如:blog:JeffreyZhao 部落格
var zhankey = key.Split(' ');//先用空格分割
var blogName = string.Empty;
if (zhankey.Length >= 2)
{
var str = zhankey[0].Trim();
if (str.Length > 6 && str.Substring(0, 5) == "blog:")
blogName = "/" + str.Substring(5);//這裡取得 使用者名
}
if (!string.IsNullOrEmpty(blogName))
key = key.Substring(key.IndexOf(' '));
//如:
var url = "http://cn.bing.com/search?q=" + key + "+site:" + siteUrl + blogName + "&first=" + PageIndex + "1&FORM=PERE";
var document = jumony.LoadDocument(url);
var list = document.Find("#b_results .b_algo").ToList().Select(t => t.ToString()).ToList();
var listli = document.Find("li.b_pag nav ul li");
if (PageIndex > 0 && listli.Count() == 0)
return null;
if (listli.Count() > 1)
{
var text = document.Find("li.b_pag nav ul li").Last().InnerText();
int npage = -1;
if (text == "下一頁")
{
if (listli.Count() > 1)
{
var num = listli.ToList()[listli.Count() - 2].InnerText();
int.TryParse(num, out npage);
}
}
else
int.TryParse(text, out npage);
if (npage <= PageIndex)
list = null;
}
return PartialView(list);
}
#endregion
看看 我們的搜尋結果的效果圖吧。
總結
首先 搜尋是必不可少的功能,但又不是主要功能。那麼我們可以直接用lucene.net 來做搜尋,然後用必應搜尋做備用。但是 用必應 有個弊端。就是 如果我們 的文章頁面 被使用者自己删除了,而 必應已經收錄了,那麼 我們在搜尋結果頁面 點選 可能就是404 或 500 了。當然 我們自己的 lucene.net 也會有這個 問題,我們可以在使用者删除 文章的時候 也删除 對應的那天搜尋索引就好了。
示範位址:http://blog.haojima.net/Search/Index?key=blog:zhaopei 部落格&p=1
如果您對本篇文章感興趣,那就麻煩您點個贊,您的鼓勵将是我的動力。 當然您還可以加入QQ群:
讨論。
如果您有更好的處理方式,希望不要吝啬賜教。
一步步開發自己的部落格 .NET版系列:http://www.cnblogs.com/zhaopei/tag/Hi-Blogs/
本文連結:http://www.cnblogs.com/zhaopei/p/4783986.html
- 學習本是一個不斷抄襲、模仿、練習、創新的過程。
- 雖然,園中已有本人無法超越的同主題博文,為什麼還是要寫。
- 對于自己,博文隻是總結。在總結的過程發現問題,解決問題。
- 對于他人,在此過程如果還能附帶幫助他人,那就再好不過了。
- 由于部落客能力有限,文中可能存在描述不正确,歡迎指正、補充!
- 感謝您的閱讀。如果文章對您有用,那麼請輕輕點個贊,以資鼓勵。
- 工控物聯Q群:995475200