爬虫系统升级改造正式启动:

    在第一篇文章,博主主要介绍了本次改造的爬虫系统的业务背景与全局规划构思:

    未来Support云系统,不仅仅是爬虫系统,是集爬取数据、数据建模处理统计分析、支持全文检索资源库、其他业务部门和公司资讯系统重要数据来源、辅助决策等功能于一身的企业级Support系统。

    介于好多园友对博主的任务排期表感兴趣,便介绍一下博主当时针对这个系统做的工作任务排期概要(排期表就是更加详细细分外加估算工时的一份excel表格,就不贴出来了):

      1.总分四大阶段,逐步上线,最终达到预期规划

      2.第一阶段实现一个新的采集系统,自动实时化爬取数据、初步规则引擎实现数据规则化、统计邮件自动推送、开放数据检索,并上线替换原有爬虫系统

      3.第二阶段实现规则化引擎升级,扩展成长式规则引擎,并开放采集源提交、管理、规则配置、基础数据服务等基本系统操作

      4.第三阶段引入全文检索,针对规则化数据创建索引,提供数据全文搜索功能,开放工单申请,可定制数据报告

      5.第四阶段引入数据报表功能,开放统计分析结果,并向舆情监控与决策支持方向扩展

    当然,在博主未争取到更多资源的情况下,第一阶段的排期要求了一个月,后面各阶段只做了功能规划,并未做时间排期。

    这也算是一个小手段吧,毕竟第一阶段上线,boss们是可能提很多其他意见,或者遇到其他任务安排的,不能一开始就把时间节点写死,不然最终受伤的可能是程序员自己。

你比他好一点,他不会承认你,反而会嫉妒你,只有你比他好很多,他才会承认你,然后还会很崇拜你,所以要做,就一定要比别人做得好很多。

  代码框架搭建:

    虽然大家都对我的“SupportYun”命名颇有异议,但是我依然我行我素,哈哈~~~总感觉读起来很和谐

    先上一张截止今天,项目结构的整体图:

    博主一直很喜爱DDD的设计模式,也在很多项目中引用了一些经典DDD模式的框架,但是明显这次的任务是不适合做DDD的。

    引入了EF Code First做数据持久化,未引入相关的各种操作扩展,这次打算纯拉姆达表达式来写,毕竟吃多了荤的,偶尔也想尝几口素,调剂调剂口味~

    两个WinServices分别是爬虫服务与规则化引擎服务。全文检索相关由于近期不会涉及,故暂未引入,相信其他的类库大家看命名就明白是干什么的了。

一匹真正的好马,即使没有伯乐赏识,也能飞奔千里。

  爬虫服务剖析:

      1.先来看Support.Domain,sorrry,原谅我对DDD爱得深沉,总是喜欢用Domain这个命名。

      Basic和Account是一些常规表模型,就不一一介绍了。

      顺带给大家共享一份一直在用的全国省市县数据sql,下载地址(不要积分,放心下载):http://download.csdn.net/detail/cb511612371/9700143

      Migrations熟悉EF的都应该知道,是DB迁移文件夹,每次模型有所改变,直接命令行执行,生成迁移文件,update数据库就OK了。命令行如下:

        a)Enable-Migrations -ProjectName EFModel命名空间
          -- 开启数据迁移(开启后,该类库下会生成Migrations文件夹,无需多次开启)

        b)Add-Migration Name -ProjectName EFModel命名空间
          -- 添加数据迁移方案(指定一个名称,添加后会在Migrations文件夹下生成对应迁移方案代码)

        c)Update-Database -ProjectName EFModel命名空间
          -- 执行数据迁移方案(匹配数据库迁移方案,修改数据库)

      再来看爬虫服务的模型:

      

      博主设计了四张表来处理爬虫服务,分别存储采集源<-1:n->采集规则<-1:n->初始采集数据,规则分组(主要用于将执行间隔相同的规则分为一组,以便后期抓取任务量大时,拆分服务部署)

      

      2.再来看SupportYun.GrabService,顾名思义,这就是我们爬虫抓取服务的核心逻辑所在。

      

      由于时间紧急,博主当前只做了使用AngleSharp来抓取的服务,以后会逐步扩充基于正则表达式以及其他第三方组件的抓取服务。

      CrawlerEngineService 是爬虫服务的对外引擎,所有爬取任务都应该是启动它来执行爬取。

      其实,爬取别人网页服务的本质很简单,就是一个获取html页面,然后解析的过程。那么我们来看看针对博主的模型设计,具体又该是怎样一个流程:

      可以看到,博主目前是在爬虫引擎里面循环所有的规则分组,当以后规则扩张,抓取频率多样化后,可以分布式部署多套任务框架,指定各自的任务规则组来启动引擎,即可达到面向服务的任务分流效果。

      3.最后,我们需要创建一个Windows服务来做任务调度(博主当前使用的比较简单,引入其他任务调度框架来做也是可以的哈~),它就是:SupportYun.CrawlerWinServices

      

      windows服务里面的逻辑就比较简单啦,就是起到一个定时循环执行任务的效果,直接上核心代码:      

     public partial class Service1 : ServiceBase
{
private CrawlerEngineService crawlerService=new CrawlerEngineService(); public Service1()
{
InitializeComponent();
} protected override void OnStart(string[] args)
{
try
{
EventLog.WriteEntry("【Support云爬虫服务启动】");
CommonTools.WriteLog("【Support云爬虫服务启动】"); Timer timer = new Timer();
// 循环间隔时间(默认5分钟)
timer.Interval = StringHelper.StrToInt(ConfigurationManager.AppSettings["TimerInterval"].ToString(), ) * ;
// 允许Timer执行
timer.Enabled = true;
// 定义回调
timer.Elapsed += new ElapsedEventHandler(TimedTask);
// 定义多次循环
timer.AutoReset = true;
}
catch (Exception ex)
{
CommonTools.WriteLog("【服务运行 OnStart:Error" + ex + "】");
}
} private void TimedTask(object source, System.Timers.ElapsedEventArgs e)
{
System.Threading.ThreadPool.QueueUserWorkItem(delegate
{
crawlerService.Main();
});
} protected override void OnStop()
{
CommonTools.WriteLog(("【Support云爬虫服务停止】"));
EventLog.WriteEntry("【Support云爬虫服务停止】");
}
}

      第35行是启用了线程池,放进队列的是爬虫抓取引擎服务的启动方法。

      windows服务的具体部署,相信大家都会,园子里也有很多园友写过相关文章,就不详细解释了。

      4.那么我们再来梳理一下当前博主整个爬虫服务的整体流程:

不论对错,只要你敢思考,并付诸行动,你就可以被称为“软件工程师”,而不再是“码农”。

  爬取服务核心代码:

    上面说的都是博主针对整个系统爬虫服务的梳理与设计。最核心的当然还是我们最终实现的代码。

一切不以最终实践为目的的构思设计,都是耍流氓。

    我们首先从看看抓取服务引擎的启动方法:

         public void Main()
{
using (var context = new SupportYunDBContext())
{
var groups = context.RuleGroup.Where(t => !t.IsDelete).ToList();
foreach (var group in groups)
{
try
{
var rules =
context.CollectionRule.Where(r => !r.IsDelete && r.RuleGroup.Id == group.Id).ToList();
if (rules.Any())
{
foreach (var rule in rules)
{
if (CheckIsAllowGrab(rule))
{
// 目前只开放AngleSharp方式抓取
if (rule.CallScriptType == CallScriptType.AngleSharp)
{
angleSharpGrabService.OprGrab(rule.Id);
}
}
}
}
}
catch (Exception ex)
{
// TODO:记录日志
continue;
}
}
}
}

    上面说了,当前只考虑一个爬虫服务,故在这儿循环了所有规则组。

    第16行主要是校验规则是否允许抓取(根据记录的上次抓取时间和所在规则组的抓取频率做计算)。

    我们看到,引擎服务只起到一个调度具体抓取服务的作用。那么我们来看看具体的AngleSharpGrabService,基于AngleSharp的抓取服务:

    IsRepeatedGrab 这个方法应该是抽象类方法,博主就不换图了哈。

    它对外暴露的是一个OprGrab抓取方法:

         /// <summary>
/// 抓取操作
/// </summary>
/// <param name="ruleId">规则ID</param>
public void OprGrab(Guid ruleId)
{
using (var context = new SupportYunDBContext())
{
var ruleInfo = context.CollectionRule.Find(ruleId);
if (ruleInfo == null)
{
throw new Exception("抓取规则已不存在!");
} // 获取列表页
string activityListHtml = this.GetHtml(ruleInfo.WebListUrl, ruleInfo.GetCharset()); // 加载HTML
var parser = new HtmlParser();
var document = parser.Parse(activityListHtml); // 获取列表
var itemList = this.GetItemList(document, ruleInfo.ListUrlRule); // 读取详情页信息
foreach (var element in itemList)
{
List<UrlModel> urlList = GetUrlList(element.InnerHtml);
foreach (UrlModel urlModel in urlList)
{
try
{
var realUrl = "";
if (urlModel.Url.Contains("http"))
{
realUrl = urlModel.Url;
}
else
{
string url = urlModel.Url.Replace(ruleInfo.CollectionSource.SourceUrl.Trim(), "");
realUrl = ruleInfo.CollectionSource.SourceUrl.Trim() + url;
} if (!IsRepeatedGrab(realUrl, ruleInfo.Id))
{
string contentDetail = GetHtml(realUrl, ruleInfo.GetCharset());
var detailModel = DetailAnalyse(contentDetail, urlModel.Title, ruleInfo); if (!string.IsNullOrEmpty(detailModel.FullContent))
{
var ruleModel = context.CollectionRule.Find(ruleInfo.Id);
ruleModel.LastGrabTime = DateTime.Now;
var newData = new CollectionInitialData()
{
CollectionRule = ruleModel,
CollectionType = ruleModel.CollectionType,
Title = detailModel.Title,
FullContent = detailModel.FullContent,
Url = realUrl,
ProcessingProgress = ProcessingProgress.未处理
};
context.CollectionInitialData.Add(newData);
context.SaveChanges();
}
} }
catch
{
// TODO:记录日志
continue;
}
}
}
}
}

    第16行用到的GetHtml()方法,来自于它所继承的抓取基类BaseGrabService:

    具体代码如下:

     /// <summary>
/// 抓取服务抽象基类
/// </summary>
public abstract class BaseGrabService
{
/// <summary>
/// 线程休眠时间 毫秒
/// </summary>
private readonly static int threadSleepTime = ; /// <summary>
/// 加载指定页面
/// </summary>
/// <param name="url">加载地址</param>
/// <param name="charsetType">编码集</param>
/// <returns></returns>
public string GetHtml(string url, string charsetType)
{
string result = null;
HttpHelper httpHelper = new HttpHelper();
result = httpHelper.RequestResult(url, "GET", charsetType);
result = ConvertCharsetUTF8(result); // 简单的休眠,防止IP被封
// TODO:后期视情况做更进一步设计
Thread.Sleep(threadSleepTime);
return result;
} /// <summary>
/// 强制将html文本内容转码为UTF8格式
/// </summary>
/// <param name="strHtml"></param>
/// <returns></returns>
public string ConvertCharsetUTF8(string strHtml)
{
if (!strHtml.Contains("Content-Type") && !strHtml.Contains("gb2312"))
{
if (strHtml.Contains("<title>"))
{
strHtml = strHtml.Insert(strHtml.IndexOf("<title>", StringComparison.Ordinal), "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
}
}
else
{
strHtml = strHtml.Replace("gb2312", "utf-8").Replace("gbk", "utf-8");
}
return strHtml;
} /// <summary>
/// 根据规则,从html中返回匹配结果
/// </summary>
/// <param name="doc">html doc</param>
/// <param name="rule">规则</param>
/// <returns></returns>
public IEnumerable<IElement> GetItemList(IDocument doc,string rule)
{
var itemList = doc.All.Where(m => m.Id == rule.Trim());
if (!itemList.Any())
{
itemList = doc.All.Where(m => m.ClassName == rule.Trim());
}
return itemList;
} /// <summary>
/// 获取列表项中的url实体
/// </summary>
/// <returns></returns>
public List<UrlModel> GetUrlList(string strItems)
{
List<UrlModel> itemList = new List<UrlModel>();
Regex reg = new Regex(@"(?is)<a[^>]*?href=(['""]?)(?<url>[^'""\s>]+)\1[^>]*>(?<text>(?:(?!</?a\b).)*)</a>");
MatchCollection mc = reg.Matches(strItems);
foreach (Match m in mc)
{
UrlModel urlModel = new UrlModel();
urlModel.Url = m.Groups["url"].Value.Trim().Replace("amp;", "");
urlModel.Title = m.Groups["text"].Value.Trim();
itemList.Add(urlModel);
} return itemList;
}
} /// <summary>
/// URL对象
/// </summary>
public class UrlModel
{
/// <summary>
/// 连接地址
/// </summary>
public string Url { get; set; } /// <summary>
/// 连接Title
/// </summary>
public string Title { get; set; }
} /// <summary>
/// 详情内容对象
/// </summary>
public class DetailModel
{
/// <summary>
/// title
/// </summary>
public string Title { get; set; } /// <summary>
/// 内容
/// </summary>
public string FullContent { get; set; }
}

    注意AngleSharpGrabService的OprGrab方法第33行至42行,在做url的构建。因为我们抓取到的a标签的href属性很可能是相对地址,在这里我们需要做判断替换成绝对地址。

    具体逻辑大家可以参考上面的爬取流程图。

    OprGrab方法的第47行即从抓取的具体详情页html中获取详情数据(目前主要获取title和带html标签的内容,具体清理与分析由规则化引擎来完成)。

    具体实现代码并无太多营养,和抓取列表页几乎一致:构建document对象,通过规则匹配出含有title的html片段和含有内容的html片段,再对title进行html标签清洗。

    具体清洗一个html文本html标签的方法已经属于规则化引擎的范畴,容博主下一篇写规则化引擎服务的时候再来贴出并给大家作分析。

    这时候,我们部署在服务器上的windows服务就能按我们配好的规则进行初始数据抓取入库了。

    贴一张博主当前测试抓取的数据截图:

    博主终于算是完成了系统的第一步,接下来就是规则化引擎分析FullContent里面的数据了。

    博主争取本周写完规则化引擎相关的代码,下周再来分享给大家哈!

    可是答应了一个月时间要做好第一阶段的所有内容并上线呢,哎~~~敲代码去

硬的怕横的,横的怕不要命的,疯子都是不要命的,所以疯子力量大,程序员只有一种,疯狂的程序员。

    共勉!!!

原创文章,代码都是从自己项目里贴出来的。转载请注明出处哦,亲~~~

记一次企业级爬虫系统升级改造(二):基于AngleSharp实现的抓取服务的更多相关文章

  1. 记一次企业级爬虫系统升级改造(六):基于Redis实现免费的IP代理池

    前言: 首先表示抱歉,春节后一直较忙,未及时更新该系列文章. 近期,由于监控的站源越来越多,就偶有站源做了反爬机制,造成我们的SupportYun系统小爬虫服务时常被封IP,不能进行数据采集. 这时候 ...

  2. 记一次企业级爬虫系统升级改造(五):基于JieBaNet+Lucene.Net实现全文搜索

    实现效果: 上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图: 基本风格是模仿的百度搜索结果,绿色的分页略显小清新. 目前已采集并创建索引的文章约3W多篇,索引文件不算太大,查询速度 ...

  3. Python3从零开始爬取今日头条的新闻【二、首页热点新闻抓取】

    Python3从零开始爬取今日头条的新闻[一.开发环境搭建] Python3从零开始爬取今日头条的新闻[二.首页热点新闻抓取] Python3从零开始爬取今日头条的新闻[三.滚动到底自动加载] Pyt ...

  4. python爬虫成长之路(一):抓取证券之星的股票数据

    获取数据是数据分析中必不可少的一部分,而网络爬虫是是获取数据的一个重要渠道之一.鉴于此,我拾起了Python这把利器,开启了网络爬虫之路. 本篇使用的版本为python3.5,意在抓取证券之星上当天所 ...

  5. Python爬虫入门教程 30-100 高考派大学数据抓取 scrapy

    1. 高考派大学数据----写在前面 终于写到了scrapy爬虫框架了,这个框架可以说是python爬虫框架里面出镜率最高的一个了,我们接下来重点研究一下它的使用规则. 安装过程自己百度一下,就能找到 ...

  6. python 手机App数据抓取实战二抖音用户的抓取

    前言 什么?你问我国庆七天假期干了什么?说出来你可能不信,我爬取了cxk坤坤的抖音粉丝数据,我也不知道我为什么这么无聊. 本文主要记录如何使用appium自动化工具实现抖音App模拟滑动,然后分析数据 ...

  7. 基于Casperjs的网页抓取技术【抓取豆瓣信息网络爬虫实战示例】

    CasperJS is a navigation scripting & testing utility for the PhantomJS (WebKit) and SlimerJS (Ge ...

  8. Python爬虫入门教程 31-100 36氪(36kr)数据抓取 scrapy

    1. 36氪(36kr)数据----写在前面 今天抓取一个新闻媒体,36kr的文章内容,也是为后面的数据分析做相应的准备的,预计在12月底,爬虫大概写到50篇案例的时刻,将会迎来一个新的内容,系统的数 ...

  9. 【转】抓包工具Fiddler的使用教程(十二)下:Fiddler抓取HTTPS

    在教程十二(上),我们也了解了HTTPS协议,该教程就和大家分享Fiddler如何抓取HTTPS 抓包工具Fiddler的使用教程(十二):[转载]HTTPS协议 再次回忆一下关键内容: iddler ...

随机推荐

  1. Python中的类、对象、继承

    类 Python中,类的命名使用帕斯卡命名方式,即首字母大写. Python中定义类的方式如下: class 类名([父类名[,父类名[,...]]]): pass 省略父类名表示该类直接继承自obj ...

  2. jQuery.ajax 根据不同的Content-Type做出不同的响应

    使用H5+ASP.NET General Handler开发项目,使用ajax进行前后端的通讯.有一个场景需求是根据服务器返回的不同数据类型,前端进行不同的响应,这里记录下如何使用$.ajax实现该需 ...

  3. 在线课程笔记—.NET基础

    关于学习北京理工大学金旭亮老师在线课程的笔记. 介绍: 在线课程网址:http://mooc.study.163.com/university/BIT#/c 老师个人网站:http://jinxuli ...

  4. DataTable的orderby有关问题

    在网上找了一个在后台重新对DataTable排序的方法(之所以不在数据库是因为我生成的是报表,写了存储过程用的表变量,order by也要用变量,死活拼不起来,sql能力没过关,动态sql也试了) s ...

  5. 几道web前端练习题目

    在 HTML 语言中,以下哪个属性不是通用属性?A]<class>B]<title>C]<href>D]<style> 在线练习:http://hove ...

  6. C# - 计时器Timer

    System.Timers.Timer 服务器计时器,允许指定在应用程序中引发事件的重复时间间隔. using System.Timers: // 在应用程序中生成定期事件 public class ...

  7. FunDA(2)- Streaming Data Operation:流式数据操作

    在上一集的讨论里我们介绍并实现了强类型返回结果行.使用强类型主要的目的是当我们把后端数据库SQL批次操作搬到内存里转变成数据流式按行操作时能更方便.准确.高效地选定数据字段.在上集讨论示范里我们用集合 ...

  8. gRPC源码分析2-Server的建立

    gRPC中,Server.Client共享的Class不是很多,所以我们可以单独的分别讲解Server和Client的源码. 通过第一篇,我们知道对于gRPC来说,建立Server是非常简单的,还记得 ...

  9. 从网上找的 visual studio 的各个版本下载地址,vs2010/vs2012/vs2013带注册码

    从网上找的 visual studio 的各个版本下载地址,很全,从 6.0 一直 到 vs2013,要的拿去吧... Microsoft Visual Studio 6.0 下载:英文版360云盘下 ...

  10. 数据可视化案例 | 如何打造数据中心APP产品

    意识到数据探索带来的无尽信息,越来越多的企业开始建立自有的数据分析平台,打造数据化产品,实现数据可视化. 在零售商超行业,沃尔玛"啤酒与尿布"的故事已不再是传奇.无论是大数据还是小 ...