這一節主要內容是使用正則表達式提取網站的正文,主要面向於小說章節網站。其中涉及到一些其他知識點,比如異步讀取、異步流寫入等,代碼中都會有詳細的注解。現在流行的網絡文學都是每日一更或幾更,沒有一個統一的下載入口。以下我將實現一個簡單的章節小說下載器的功能,將章節小說以整本的形式下載保存,保守估計能下載網絡上70%以上小說。
先看看小說網站的網頁源碼,天蠶土豆的大主宰第一章。
http://www.biquge.com/4_4606/991334.html 筆趣網
http://www.fqxsw.com/html/11739/4636404.html 番茄小說網
正文正則
結果發現正文內容一般都是嵌套在div中,樣式表可能會略有不同,所以正則表達式可以這樣表示
(<div).*</div>
當然有div標簽的不一定是正文內容,還有可能是其中不相關的數據。那么按照一般小說的規律,我們指定一個匹配符。
<br\\s*>
只有當匹配符超過5個以上的,我們才認為這是正文內容。
下一頁正則
再來找下一頁的鏈接。下一頁的鏈接的格式一般存在兩種格式
或是
所以正則表達式可以這樣表示
<a.*href=(")(([^<]*[^"])[^>])(\s*)?>.*((→)|(下一頁))
異步讀取網頁流
讀取網頁數據使用HttpClient異步方法,在讀取過程中將主控制權返回到UI層,不會阻塞界面。具體原理請查看我上一篇文章
await httpClient.GetByteArrayAsync(url);
配置文件
為了匹配更多的網站信息,我把正則表達式存在一個ini文件中,在需要的時候可以繼續擴充。
核心代碼

private async Task downLoadNovel(byte[] bytes, string url) { title = string.Empty; nextPageUrl = string.Empty; content = string.Empty; novelInfo = string.Empty; try { byte[] response = bytes; if (bytes == null) { response = await httpClient.GetByteArrayAsync(url); } content = Encoding.Default.GetString(response, 0, response.Length - 1); //獲取網頁字符編碼描述信息 var charSetMatch = Regex.Match(content, "<meta([^<]*)charset=([^<]*)\"", RegexOptions.IgnoreCase | RegexOptions.Multiline); string webCharSet = charSetMatch.Groups[2].Value; if (chartSet == null || chartSet == "") chartSet = webCharSet; if (chartSet != null && chartSet != "" && Encoding.GetEncoding(chartSet) != Encoding.Default) content = Encoding.GetEncoding(chartSet).GetString(response, 0, response.Length - 1); } catch (Exception ex) { throw ex; } //小說主域名 if (webSiteDomain.Length == 0) { var websiteDomainMath = Regex.Match(url, "(http).*(/)", RegexOptions.IgnoreCase); webSiteDomain = websiteDomainMath.Groups[0].Value; } //標題信息 var titleInfoMath = Regex.Match(content, "(<title>)([^>]*)(</title>)", RegexOptions.IgnoreCase | RegexOptions.Multiline); title = titleInfoMath.Groups[2].Value; content = content.Replace("'", "\"").Replace("\r\n", ""); for (int i = 0; i < contextPatterns.Length; i++) { var cpattern = contextPatterns[i]; if (novelInfo.Length == 0) { //正文信息 var webInfoMath = Regex.Matches(content, cpattern, RegexOptions.IgnoreCase | RegexOptions.Multiline); for (int j = 0; j < webInfoMath.Count; j++) { foreach (Group g in webInfoMath[j].Groups) { var value = Regex.Split(g.Value, contextNewLine, RegexOptions.IgnoreCase); if (value.Length > 5) { novelInfo = g.Value; foreach (var pattern in filterPatterns) novelInfo = Regex.Replace(novelInfo, pattern, new MatchEvaluator(p => null)); novelInfo = Regex.Replace(novelInfo, contextNewLine, new MatchEvaluator(p => "\r\n")); break; } } } } else break; } bytes = null; for (int i = 0; i < nextPagePatterns.Length; i++) { if (nextPageUrl.Length == 0) { //下一頁信息 var webNextPageMath = Regex.Match(content, nextPagePatterns[i], RegexOptions.IgnoreCase | RegexOptions.Multiline); if (webNextPageMath.Groups.Count > 0) { foreach (Group g in webNextPageMath.Groups) { if (!g.Value.EndsWith("\"")) nextPageUrl = g.Value; if (nextPageUrl.StartsWith("/")) nextPageUrl = nextPageUrl.Substring(1); if (!nextPageUrl.StartsWith("http", true, null) && (Regex.IsMatch(nextPageUrl, "[a-z]") || Regex.IsMatch(nextPageUrl, "[0-9]")) && !url.EndsWith(nextPageUrl)) { nextPageUrl = webSiteDomain + nextPageUrl; } try { bytes = await httpClient.GetByteArrayAsync(nextPageUrl); break; } catch { continue; } } } } else break; } bool isAdd = false; cacheNovel.ForEach(p => { if (p == (title + novelInfo)) { isAdd = true; } }); if (!isAdd) { if (title.Length > 0) { writeNovelLog("正在下載章節:" + title); } writeNovelLog("章節長度:" + novelInfo.Length); cacheNovel.Add(title + novelInfo); if (nextPageUrl.Length > 0) { writeNovelLog("下一頁:" + nextPageUrl); await downLoadNovel(bytes, nextPageUrl); } else { downloadFinish(); } } else { writeNovelLog("存在重復的章節,章節名稱:" + title + " 地址:" + url); downloadFinish(); } }
最后效果