空闲之余自己弄了一个小说网站,也没有什么目的性,就当是兴趣之余的一点专注和对那些小说爱好者的一点贡献(曾经有朋友让我为他在小说站点充值看小说的),挺不值的。看上去就一个小说站,无非就几张页面,小说详情、小说章节列表、小说内容页,核心业务就是小说章节内容收录。貌似没多大功能,可是这期间投入了我不少时间和精力,走了不少弯路,把一些坎坷和曲折记录下,以供有做小说站的朋友一点借鉴,这篇文章只是对小说收录的业务做简单说明,关于网站的建设和上线、运行、优化就不陈述了。
在小说收录的工具编写中,经历了三个阶段,第一阶段直接依赖盗版网站,这个阶段是比较幼嫩的,也是比较简单的,就是找几个盗版网站,从小说更新列表收录小说详情地址,再根据小说详情地址收录小说详情(书名、作者、简介、类型、封面、章节列表地址等),再根据小说章节列表地址收录小说章节信息(章节名称、更新时间、章节字数、章节内容地址),最后根据章节内容地址得到章节内容,功能实现用到的是正则表达式,这个很简单,相信没用过正则表达式的朋友用两天时间也能搞定。简单的流程图如下:
这样收录小说存在几个问题:
1、 要专门为盗版站编写正则(小说更新列表、小说详情、小说章节列表、小说内容),盗版站排版可能会经常变化,正则也要跟着变化。
2、 盗版站不稳定,可能投入或者其他的原因,明天或者后天就没了。
3、 指定的盗版站未必是更新速度最快的,直接依赖与具体的盗版站,必须等它更新了你才能更新到最新章节,速度不行。
4、 会存在断章节的情况。这个是比较致命的,很多读者因为你断章节,就开溜了。
为了规避这几个问题,开始了新的思考,首先我要解决致命的问题,不能断章节,所以要去正版网站收录章节,正版网站章节是什么样的顺序就应该是什么顺序,包括分卷之类的,经过一般思考之后到了第二阶段,到正版网站小说更新列表页面收录小说详情地址,根据详情地址收录小说详情,根据章节列表地址收录小说所有章节列表,根据小说章节内容地址收录小说非VIP章节内容(vip章节这里是没办法收录的),然后再根据书名去指定的盗版网站查询这本书的章节列表地址,根据章节列表地址收录章节列表,比对vip章节名称来得到盗版的vip章节内容地址,从来得到vip章节内容,简单的流程图如下:
相对于第一阶段来说是解决了断章节的问题,可这引入了更繁琐的逻辑,要依赖正版网站,还得依赖盗版网站,当然这里不会因为某个盗版网站挂掉了,小说章节收录就进行不下去了,一本小说可以对应多个盗版小说网站的章节列表,我们只要为没本小说查找更多的盗版小说地址就为更新提高了更稳定的保证,但是这个工作量就增加了,没增加一个盗版小说网站,你要为这个站点编写正则,而且还是第一阶段的问题,不能保证指定的盗版站点就是更新最快的站点。所以我想,如果要是VIP章节不依赖与具体的盗版网站,而依赖与中间的一个代理地址那多好,那就不用管网站什么结构,那个网站更新快的问题了,于是我想到了搜索引擎,不得不佩服这玩意确实是个好东西,你想到的或者没想到的它都能给你答案,不得不说搜索引擎是万能的,一番思考和尝试之后,总算实现了用搜索引擎来收录小说VIP章节,从正版网站收录小说章节列表后,过滤出需要收录的vip章节,然后根据小说名称加上一定的搜索关键字到搜索引擎搜索盗版站点,从而得到盗版站点的章节列表地址,根据需要收录的vip章节比对章节名称得到章节内容地址,从而得到vip章节内容,简单流程图如下:
想法是好的,可是怎么实现呢,我们并不知道搜索引擎会给我们那些盗版网站,我们也不知道这些盗版站的页面结果,怎么去得到章节内容呢?
只要用心,最终功夫还是不负有心人,我查看了很多小说站点的章节内容基本上都是放在<td></td>或者<div></div>之间,或者就是直接输出,不用任何html标签包裹,对于第一种情况我们可以用htmlparse把文本转换成doc对象,遍历每个标签的内容,对内容加以判断,对于第二种情况那还是得用正则,匹配里面的汉子,判断得到的每组汉子中间的html标签,如果中间的标签只是<br>或者<p>或者 那么肯定是章节内容了,下面来看看代码怎么实现的。
/// <summary> /// 默认章节字数要1000长度 /// </summary> private int m_chapter_content_length = 1000; /// <summary> /// 从google收录章节 /// </summary> /// <param name=”i_book_name”>小说名称</param> /// <param name=”i_chapter_list”>需要收录的vip章节列表</param> /// <returns></returns> public List<BookChapterInfo> CollectBookChapterList(string i_book_name,List<BookChapterInfo> i_chapter_list) { i_book_name = Regex.Replace(i_book_name, “[a-zA-Z0-9()()]”, “”, RegexOptions.Compiled | RegexOptions.IgnoreCase); if (i_chapter_list == null || i_chapter_list.Count <= 0) return null; Encoding t_encoding = Encoding.GetEncoding(“gb2312”); string t_key_word = string.Format(“小说{0}最新章节txt”, i_book_name); string t_baidu_url = string.Format(“http://www.baidu.com/s?wd={0}”, HttpUtility.UrlEncode(t_key_word, t_encoding)); string t_list_reg = “<h3\\s*?class=[\’\”]?t[\’\”]?><a[^<>]*?hrefs*=s*[\’\”]*([^\”\’]*)[\’\”]*[^<>]*?>(.*?)</a>\\s*?</h3>”; List<BookChapterInfo> t_need_collect_list = new List<BookChapterInfo>(); List<BookChapterInfo> t_collect_chapter_list = new List<BookChapterInfo>(); List<BookChapterInfo> t_vip_chapter_list = new List<BookChapterInfo>(); string t_book_url = string.Empty; try { string t_html = NetSiteCatchManager.ReadUrl(t_baidu_url, t_encoding); if (!string.IsNullOrEmpty(t_html)) { MatchCollection t_ma = Regex.Matches(t_html, t_list_reg, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (t_ma != null) { for(int index=0;index<t_ma.Count;index++) { t_book_url = t_ma[index].Groups[1].Value.ToString(); t_html = NetSiteCatchManager.ReadUrl(t_book_url, Encoding.Default); t_need_collect_list = GetNeedCollectChapter(i_chapter_list, t_vip_chapter_list); t_collect_chapter_list = GetBookChapterList(t_book_url, t_html, t_need_collect_list, i_book_name); if (t_collect_chapter_list != null && t_collect_chapter_list.Count > 0) t_vip_chapter_list.AddRange(t_collect_chapter_list); //就差10个章节退出 if (t_vip_chapter_list != null && t_vip_chapter_list.Count > 0 && i_chapter_list.Count-t_vip_chapter_list.Count<10) break; } } } } catch (Exception ex) { LogHelper.Error(“从google收录章节列表失败” + ex.ToString()); } return t_vip_chapter_list; } /// <summary> /// 获取还没有收录到的章节列表 /// </summary> /// <param name=”i_vip_list”></param> /// <param name=”i_have_collect_list”></param> /// <returns></returns> private List<BookChapterInfo> GetNeedCollectChapter(List<BookChapterInfo> i_vip_list, List<BookChapterInfo> i_have_collect_list) { if (i_have_collect_list == null || i_have_collect_list.Count <= 0) return i_vip_list; List<BookChapterInfo> t_list = new List<BookChapterInfo>(); foreach (BookChapterInfo t_chapter in i_vip_list) { List<BookChapterInfo> t_temp = i_have_collect_list.FindAll(delegate(BookChapterInfo t_have_chapter) { return t_chapter.ChapterName == t_have_chapter.ChapterName; }); if (t_temp == null || t_temp.Count <= 0) { t_list.Add(t_chapter); } } return t_list; } /// <summary> /// 获取章节列表 /// </summary> /// <param name=”i_html”></param> /// <param name=”i_chapter_list”></param> /// <returns></returns> private List<BookChapterInfo> GetBookChapterList(string i_url,string i_html, List<BookChapterInfo> i_chapter_list,string i_book_name) { if (!NetSiteCatchManager.IsPiraticSite(i_url)) return null; if (string.IsNullOrEmpty(i_html)) return null; string t_chapter_name_reg = “<a[^<>]*?hrefs*=s*[\’\”]*([^\”\’]*)[\’\”]*[^<>]*?>(.*?)</a>”; List<BookChapterInfo> t_chapter_list = new List<BookChapterInfo>(); BookChapterInfo t_chapter = null; bool t_is_stop = false; string t_chapter_url = string.Empty; try { MatchCollection t_ma = Regex.Matches(i_html, t_chapter_name_reg, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (t_ma != null) { foreach (BookChapterInfo t_ch in i_chapter_list) { foreach (Match t_mc in t_ma) { if (CompareChapterName(t_mc.Groups[2].Value.ToString().Trim(), t_ch.ChapterName) == true) { t_chapter_url = NetSiteCatchManager.GetFullUrl(i_url, t_mc.Groups[1].Value.ToString().Trim()); if (string.IsNullOrEmpty(t_chapter_url)) { t_is_stop = true; break; } t_chapter = GetBookChapter(t_chapter_url, t_ch, i_book_name); if (t_chapter == null) { t_is_stop = true; break; } if (t_chapter != null) t_chapter_list.Add(t_chapter); break; } } if (t_is_stop) break; } } return t_chapter_list; } catch (Exception ex) { LogHelper.Error(“从百度分离章节名称失败” + ex.ToString()); return null; } } /// <summary> /// 得到章节信息 /// </summary> /// <param name=”i_url”></param> /// <param name=”i_chapter_name”></param> /// <param name=”i_chapter_list”></param> /// <returns></returns> private BookChapterInfo GetBookChapter(string i_url, BookChapterInfo i_chapter, string i_book_name) { //最后一个章节不一定有1000字 if (i_chapter.ChapterName.IndexOf(“完”) > -1 || i_chapter.ChapterName.IndexOf(“终”) > -1 || i_chapter.ChapterName.IndexOf(“结”) > -1) { m_chapter_content_length = 300; } else { m_chapter_content_length = 1000; } BookChapterInfo t_chapter_info=null; string t_chapter_content = GetContent(i_url, i_chapter.ChapterName); t_chapter_content = NetSiteCatchManager.ReplaceContent(t_chapter_content); if (string.IsNullOrEmpty(t_chapter_content) || t_chapter_content.Length < m_chapter_content_length) { t_chapter_content = GetChapterContentByChapterName(i_book_name, i_chapter.ChapterName); t_chapter_content = NetSiteCatchManager.ReplaceContent(t_chapter_content); if (string.IsNullOrEmpty(t_chapter_content) || t_chapter_content.Length < m_chapter_content_length) return null; } t_chapter_content = string.Format(“document.write(‘{0}’);”, t_chapter_content); t_chapter_info = new BookChapterInfo(); t_chapter_info.ChapterName = i_chapter.ChapterName; t_chapter_info.ChapterContent = t_chapter_content; t_chapter_info.WordsCount = t_chapter_content.Length; t_chapter_info.Comfrom = i_url; t_chapter_info.IsVip = i_chapter.IsVip; t_chapter_info.UpdateTime = i_chapter.UpdateTime; t_chapter_info.VolumeName = i_chapter.VolumeName; t_chapter_info.BookId = i_chapter.BookId; t_chapter_info.SiteId = i_chapter.SiteId; return t_chapter_info; } /// <summary> /// 获取章节内容 /// </summary> /// <param name=”i_url”></param> /// <param name=”i_chapter_name”></param> /// <returns></returns> private string GetContent(string i_url, string i_chapter_name) { Encoding t_encoding = Encoding.Default; string t_chapter_content = string.Empty; string t_charset = string.Empty; try { string t_html = NetSiteCatchManager.ReadUrl(i_url, t_encoding); if (string.IsNullOrEmpty(t_html)) { //重复一次 t_html = NetSiteCatchManager.ReadUrl(i_url, t_encoding); } t_chapter_content = GetChapterContent(t_html); return t_chapter_content; } catch (Exception ex) { LogHelper.Error(“获取页面内容失败” + ex.ToString()); return string.Empty; } } /// <summary> /// 获取html章节内容 /// </summary> /// <param name=”i_html”></param> /// <param name=”i_chapter_name”></param> /// <returns></returns> private string GetChapterContent(string i_html) { HtmlDocument t_html_doc = HtmlDocument.Create(i_html); string t_content = string.Empty; string t_temp_content = string.Empty; foreach (HtmlElement t_ele in t_html_doc.GetElementsByTagName(“td”)) { t_temp_content = t_ele.InnerText; t_temp_content = Regex.Replace(t_temp_content, “<.*?>.*?</.*?>”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); t_temp_content = Regex.Replace(t_temp_content, “[a-zA-Z0-9]”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (t_temp_content.Length > m_chapter_content_length) { t_content = t_ele.HTML; } } if (!string.IsNullOrEmpty(t_content)) return t_content; foreach (HtmlElement t_ele in t_html_doc.GetElementsByTagName(“div”)) { t_temp_content = t_ele.InnerText; t_temp_content = Regex.Replace(t_temp_content, “<.*?>.*?</.*?>”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); t_temp_content = Regex.Replace(t_temp_content, “[a-zA-Z0-9,\\/;_()]”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (t_temp_content.Length > m_chapter_content_length) { t_content = t_ele.HTML; } } if (string.IsNullOrEmpty(t_content) || t_content.Length < m_chapter_content_length) t_content = GetContentByReg(i_html); if (t_content.Length < m_chapter_content_length) return string.Empty; return t_content; } /// <summary> /// 用正则表达式获取章节内容 /// </summary> /// <param name=”i_html”></param> /// <returns></returns> private string GetContentByReg(string i_html) { StringBuilder t_sb = new StringBuilder(); string t_reg = “([\u4E00-\u9FA5][^<>]*[\u4E00-\u9FA5])”; MatchCollection t_ma = Regex.Matches(i_html, t_reg, RegexOptions.IgnoreCase | RegexOptions.Compiled); string t_sub_html = string.Empty; int t_start_index = 0; int t_length = 0; if (t_ma != null) { int t_total_count=t_ma.Count; for (int index = 0; index < t_total_count-1; index++) { t_start_index = t_ma[index].Index + t_ma[index].Groups[1].Value.ToString().Length; t_length = t_ma[index + 1].Index – t_ma[index].Index – t_ma[index].Groups[1].Value.ToString().Length; t_sub_html = i_html.Substring(t_start_index, t_length); t_sub_html = Regex.Replace(t_sub_html, “ ”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); t_sub_html = Regex.Replace(t_sub_html, “<[/]*p[^<>]*>”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); t_sub_html = Regex.Replace(t_sub_html, “<[/]*br>”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); t_sub_html=Regex.Replace(t_sub_html, “[【】(),!?(),!?;;、……]”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (t_sub_html.Length < 10) { t_sb.Append(t_ma[index].Groups[1].Value.ToString()); t_sb.Append(“<p> ”); } } } return t_sb.ToString(); } /// <summary> /// 判断是否是相同的章节 /// </summary> /// <param name=”i_chapter_source”></param> /// <param name=”i_chapter_target”></param> /// <returns></returns> private bool CompareChapterName(string i_chapter_source, string i_chapter_target) { if (i_chapter_source.Equals(i_chapter_target)) return true; //去掉空格 i_chapter_source = Regex.Replace(i_chapter_source, “[\\s【】(),!?(),!?;\\.;、/……]”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); i_chapter_target = Regex.Replace(i_chapter_target, “[\\s【】(),!?(),!?;;\\.、/……]”, “”, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (i_chapter_source.IndexOf(i_chapter_target) > -1 || i_chapter_target.IndexOf(i_chapter_source) > -1) return true; return false; } /// <summary> /// 通过章节名称去搜索引擎收录 /// </summary> /// <param name=”i_book_name”></param> /// <param name=”i_chapter_name”></param> /// <returns></returns> private string GetChapterContentByChapterName(string i_book_name, string i_chapter_name) { string t_key_word=i_chapter_name; //章节名称长度小于5加上书名作为关键字 if (i_chapter_name.Length < 5) { t_key_word = string.Format(“{0} {1}”, i_book_name, i_chapter_name); } Encoding t_encoding = Encoding.GetEncoding(“gb2312”); string t_baidu_url = string.Format(“http://www.baidu.com/s?wd={0}”, HttpUtility.UrlEncode(t_key_word, t_encoding)); string t_list_reg = “<h3\\s*?class=[\’\”]?t[\’\”]?><a[^<>]*?hrefs*=s*[\’\”]*([^\”\’]*)[\’\”]*[^<>]*?>(.*?)</a>\\s*?</h3>”; string t_chapter_url = string.Empty; string t_chapter_content = string.Empty; try { string t_html = NetSiteCatchManager.ReadUrl(t_baidu_url, t_encoding); if (!string.IsNullOrEmpty(t_html)) { MatchCollection t_ma = Regex.Matches(t_html, t_list_reg, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (t_ma != null) { foreach (Match t_mc in t_ma) { t_chapter_url = t_mc.Groups[1].Value.ToString(); t_html = NetSiteCatchManager.ReadUrl(t_chapter_url, Encoding.Default); if (string.IsNullOrEmpty(t_html)) { //重复一次 t_html = NetSiteCatchManager.ReadUrl(t_chapter_url, Encoding.Default); if (NetSiteCatchManager.IsContainChapterName(i_book_name, i_chapter_name, t_html) == false) continue; t_chapter_content = GetChapterContent(t_html); if (!string.IsNullOrEmpty(t_chapter_content) && t_chapter_content.Length > m_chapter_content_length) break; } } } } return t_chapter_content; } catch (Exception ex) { LogHelper.Error(“根据章节名称收录章节失败” + ex.ToString()); return string.Empty; } }
现在第三阶段的代码已经正常运行两天,激动之余有点迫不及待的跟大家分享,有小说爱好者可以关注下小站(http://www.dazhongxiaoshuo.com),下班之余我大部分时间都是看小说,接下来会话点时间开发手机阅读功能,在被窝里看小说是我的追求。