
暂无个人介绍
下着雨 彩雾中的水精灵 我想起了过去的你 清新美丽 是否值得回忆 是否让它过去 凝望天空 俯视大地 雨在下 不停地下 你是否也在哭泣 雨在下 轻轻地下 你在为谁伤心 稚嫩的小手 触碰这份静谧 冰凉点滴在手心 滴答滴答的交响曲 落下的雨 太冰凉 太凄冷 击打着死的心 一丝阳光驱散乌云 露出彩虹的秘密 风停 雨静 这份画意 我宁愿放在心底 我想做的 还是傻傻的自己的 我想要的 只是一点点的孩子气 我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=jepq8bm1wuyl
初月儿明处少,又被浮云遮蔽了。香消烛灭心静悄,夜迢迢,难睡着,窗儿外雨打芭蕉。 我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
接着上面的内容,我们本章节通过设置数据库表结构,将json数据通过一种数据结构存储在数据库中。 首先我们需要设计数据库的表。 image.png 数据库字段说明 Id 主键 HolidayDate 节假日日期 HolidayName 节假日名称 HolidayDescripton 说明 HolidayConfig 放假配置 [放假日期|工作日期] =[A|B] A,B存在多个时用逗号分隔,见上图,通过这样的数据结构,然后编码实现。 后台代码贴部分,数据库交互使用EntityFramework public static void SetWnlConfig() { wnlEntities db = new wnlEntities(); var list = db.HolidayArrange.ToList(); var dateFestival = list.Select(u => u.HolidayDate + "||" + u.HolidayName).ToList(); var dateFestivalContent = list.Select(u => new FestivalContent { name = u.HolidayName, value = u.HolidayDescription }).ToList(); List<string[]> splists = list.Select(u => u.HolidayConfig.Split('|').ToArray()).ToList(); List<string> dateRest = new List<string>(); List<string> dateWork = new List<string>(); foreach (var split in splists) { dateRest.AddRange(split[0].Split(',')); dateWork.AddRange(split[1].Split(',')); } var data = new WnlConfig { dateFestival = dateFestival, dateFestivalContent = dateFestivalContent, dateRest = dateRest, dateWork = dateWork, }; //默认缓存一个小时 CacheHelper.SetCache("wnl", JsonConvert.SerializeObject(data), new TimeSpan(0, 1, 0, 0)); } 通过调用上述方法能够得到json数据,回传给前端,前端绑定后,即可得到相应的结果。 配置修改 既然保存在数据库中,那对应的数据应该能够被动态更改,下图给出配置界面 image.png 点击新增弹出配置窗体 配置窗体.png 配置代码 /// <summary> /// 新增 /// </summary> /// <param name="formjson"></param> /// <returns></returns> public ActionResult AddConfig(string formjson) { DataMsg datamsg = null; var model = JsonConvert.DeserializeObject<HolidayArrange>(formjson); if (!ValidateModel(model)) { datamsg = new DataMsg() { code = CodeStatus.Error, msg = "配置验证失败,请核对后重新提交!" }; return Json(datamsg, JsonRequestBehavior.AllowGet); } wnlEntities db = new wnlEntities(); db.HolidayArrange.Add(model); int iret = db.SaveChanges(); if (iret > 0) { datamsg = new DataMsg() { code = CodeStatus.Ok, msg = "新增成功" }; SetWnlConfig(); } else { datamsg = new DataMsg() { code = CodeStatus.Error, msg = "新增失败" }; } return Json(datamsg, JsonRequestBehavior.AllowGet); } 剩下的编辑和删除配置同理
项目需求: 某门户项目需要在右上角添置一个日期(yyyy-MM-dd)的显示,同时点击此时间可以弹出一个窗体,窗体内容为万年历。 万年历需求: 1、日志显示24节气,且24节气内容可配置 2、对法定节假日可进行配置管理 项目开发周期: 一周 项目技术选择: 1、基于网上现存的万年历组件进行扩展 2、.Net MVC 开发一个Web项目进行内容配置 准备 首先我需要在网上找到一个相对完善的万年历模板,经过多番寻找,总算是找到了一个见如下链接,组件默认实现了很多的功能。 image.png 下载下来之后我需要对其内容进行修改,当我们打开文件结构时,我们发现所有的页面元素都已经被压缩过了,这就使得代码很难阅读,不方便我们后期的更改。这个时候一个神奇就出现了,LZ在之前的文章中也提到过,Jsbeautifier JS代码美化库,通过使用此工具我们可以将前台的代码进行格式化输出,同理对目录下的其他文件也可使用此方法,最后结果如图2所示 图1.png 图2.png 此时,代码阅读就很方便了。然后我们开始根据需求进行更改代码 1、首先我们不需要如下模块,将此模块内容更改为节日内容,如果点击日期是某个节气,显示具体内容,否则显示“无”。 image.png 2、下如所示“2015年假日安排”以及下拉框内容要将静态数据改为动态获取的方式,我们找到代码段见下图,将此处内容之后更改为AJAX动态获取。 image.png image.png 思路 如图我们可以看到window.OB.RILi开头的内容对应的就是下拉框的显示数据以及各种前台绑定之。 dataRest:休息日 dataWork:工作日 dateFestival:假日 dateFestivalContent:节假日说明 dateAllFestival:所有的假期 jieqi24:新增加的节气 源程序内容,静态赋值 window.OB = window.OB || {}, window.OB.RiLi = window.OB.RiLi || {}, window.OB.RiLi.dateRest = ["0101", "0102", "0103", "0218", "0219", "0220", "0221", "0222", "0223", "0224", "0404", "0405", "0406", "0501", "0502", "0503", "0620", "0621", "0622", "0903", "0904", "0905", "0926", "0927", "1001", "1002", "1003", "1004", "1005", "1006", "1007"], window.OB.RiLi.dateWork = ["0104", "0215", "0228", "0906", "1010"], window.OB.RiLi.dateFestival = ["20150101||元旦", "20150219||春节", "20150405||清明节", "20150501||劳动节", "20150620||端午节", "20150903||抗战纪念日", "20150927||中秋节", "20151001||国庆节"], window.OB.RiLi.dateAllFestival = ["正月初一|v,春节", "正月十五|v,元宵节", "二月初二|v,龙头节", "五月初五|v,端午节", "七月初七|v,七夕节", "七月十五|v,中元节", "八月十五|v,中秋节", "九月初九|v,重阳节", "十月初一|i,寒衣节", "十月十五|i,下元节", "腊月初八|i,腊八节", "腊月廿三|i,祭灶节", "0202|i,世界湿地日,1996", "0214|v,西洋情人节", "0308|i,国际妇女节,1975", "0315|i,国际消费者权益日,1983", "0422|i,世界地球日,1990", "0501|v,国际劳动节,1889", "0512|i,国际护士节,1912", "0518|i,国际博物馆日,1977", "0605|i,世界环境日,1972", "0623|i,国际奥林匹克日,1948", "0624|i,世界骨质疏松日,1997", "1117|i,世界学生日,1942", "1201|i,世界艾滋病日,1988", "0101|v,元旦", "0312|i,植树节,1979", "0504|i,五四青年节,1939", "0601|v,儿童节,1950", "0701|v,建党节,1941", "0801|v,建军节,1933", "0903|v,抗战胜利纪念日", "0910|v,教师节,1985", "1001|v,国庆节,1949", "1224|v,平安夜", "1225|v,圣诞节", "w:0520|v,母亲节,1913", "w:0630|v,父亲节", "w:1144|v,感恩节(美国)", "w:1021|v,感恩节(加拿大)"]; 我们通过更改原来的方式,通过ajax获取数据,数据放在一个json文件中,文件内容见下图 ajax调用.png json文件内容 { "24jieqi": [ { "name": "立春", "value": "斗指东北。太阳黄经为315度。是二十四个节气的头一个节气。其含义是开始进入春天,“阳和起蛰,品物皆春”,过了立春,万物复苏生机勃勃,一年四季从此开始了" }, { "name": "雨水", "value": "斗指壬。太阳黄经为330°。这时春风遍吹,冰雪融化,空气湿润,雨水增多,所以叫雨水。人们常说:“立春天渐暖,雨水送肥忙”。" } ], "dateRest": [ "0101", "0102", "0103", "0218", "0219", "0220", "0221", "0222", "0223", "0224", "0404", "0405", "0406", "0501", "0502", "0503", "0620", "0621", "0622", "0903", "0904", "0905", "0926", "0927", "1001", "1002", "1003", "1004", "1005", "1006", "1007" ], "dateWork": [ "0104", "0215", "0228", "0906", "1010" ], "dateFestival": [ "20150101||元旦", "20150219||春节", "20150405||清明节", "20150501||劳动节", "20150620||端午节", "20150927||中秋节", "20151001||国庆节" ], "dateFestivalContent": [ { "name": "国庆节", "value": "10月1日至7日放假调休,共7天。10月10日(星期六)上班。" }, { "name": "中秋节", "value": "9月27日放假。" }, { "name": "端午节", "value": "6月20日放假,6月22日(星期一)补休。" }, { "name": "劳动节", "value": "5月1日放假,与周末连休。" }, { "name": "清明节", "value": "4月5日放假,4月6日(星期一)补休。" }, { "name": "春节", "value": "2月18日至24日放假调休,共7天。2月15日(星期日)、2月28日(星期六)上班。" }, { "name": "元旦", "value": "1月1日至3日放假调休,共3天。1月4日(星期日)上班。" } ], "dateAllFestival": [ "正月初一|v,春节", "正月十五|v,元宵节", "二月初二|v,龙头节", "五月初五|v,端午节", "七月初七|v,七夕节", "七月十五|v,中元节", "八月十五|v,中秋节", "九月初九|v,重阳节", "十月初一|i,寒衣节", "十月十五|i,下元节", "腊月初八|i,腊八节", "腊月廿三|i,祭灶节", "0202|i,世界湿地日,1996", "0214|v,西洋情人节", "0308|i,国际妇女节,1975", "0315|i,国际消费者权益日,1983", "0422|i,世界地球日,1990", "0501|v,国际劳动节,1889", "0512|i,国际护士节,1912", "0518|i,国际博物馆日,1977", "0605|i,世界环境日,1972", "0623|i,国际奥林匹克日,1948", "0624|i,世界骨质疏松日,1997", "1117|i,世界学生日,1942", "1201|i,世界艾滋病日,1988", "0101|v,元旦", "0312|i,植树节,1979", "0504|i,五四青年节,1939", "0601|v,儿童节,1950", "0701|v,建党节,1941", "0801|v,建军节,1933", "0903|v,抗战胜利纪念日", "0910|v,教师节,1985", "1001|v,国庆节,1949", "1224|v,平安夜", "1225|v,圣诞节", "w:0520|v,母亲节,1913", "w:0630|v,父亲节", "w:1144|v,感恩节(美国)", "w:1021|v,感恩节(加拿大)" ] } 通过这样的方式将原来写死在页面上的数据,变为数据可通过json文件配置。新增了一个点击事件——鼠标点击,如果为节气,显示节气内容,我们添加如下的一段内容即可 点击事件.png 到现在位置,功能已全部实现,只不过内容代码都是html+css+js基本结构,没有将数据可配置化在数据库中。 结果演示.png 代码下载地址:后续放开
目录一、使用ExcelReport导出Excel二、ExcelReport源码解析三、扩展元素格式化器 ExcelReport的作者长久未对文档进行更新,后续对文章进行更新,并对源码进行解析。
SQLServer 连接字符串 <add name="DapperSql" connectionString="Data Source=.; Initial Catalog=HangFire_DB;User Id=sa;Password=123456;"/> MySql 连接字符串 Data Source=127.0.0.1;Database=DBName;User Id=root;Password=root;
场景模拟 每天8点爬取今日发布的新闻和通知公告,将爬取后的信息保存到Excel文件中,将程序发布成windows服务,开机即可自动启动。 技术使用 1.每天8点定时执行任务,使用Quartz.Net 2.爬取数据采用HtmlAgility 3.Excel操作采用NPOI 4.将应用程序发布为Windows服务,使用Topshelf 5.日志记录Log4Net 思路 因为最后的输出形式为Windows服务,所以使用Topshelf进行打包 TopShelf使用链接 http://www.cnblogs.com/jys509/p/4614975.html TopShelf概述 Topshelf是创建Windows服务的另一种方法,老外的一篇文章Create a .NET Windows Service in 5 steps with Topshelf通过5个步骤详细的介绍使用使用Topshelf创建Windows 服务。Topshelf是一个开源的跨平台的宿主服务框架,支持Windows和Mono,只需要几行代码就可以构建一个很方便使用的服务宿主。 不了解TopShelf的童鞋可以先百度或者根据LZ提供的链接看看TopShelf是什么以及如何使用。 在了解TopShelf为何物后,我们首先建立一个控制台项目,将我们所需要的组件一一安装。 Install-package Quartz.Net Install-package Log4Net Install-package HtmlAgility Install-package TopShelf 第一步:配置Log4Net日志 新建Log4net.config配置文件 <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/> </configSections> <log4net> <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender"> <!--日志路径--> <param name= "File" value= "D:\App_Log\servicelog\"/> <!--是否是向文件中追加日志--> <param name= "AppendToFile" value= "true"/> <!--log保留天数--> <param name= "MaxSizeRollBackups" value= "10"/> <!--日志文件名是否是固定不变的--> <param name= "StaticLogFileName" value= "false"/> <!--日志文件名格式为:2008-08-31.log--> <param name= "DatePattern" value= "yyyy-MM-dd&quot;.read.log&quot;"/> <!--日志根据日期滚动--> <param name= "RollingStyle" value= "Date"/> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n %loggername" /> </layout> </appender> <!-- 控制台前台显示日志 --> <appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender"> <mapping> <level value="ERROR" /> <foreColor value="Red, HighIntensity" /> </mapping> <mapping> <level value="Info" /> <foreColor value="Green" /> </mapping> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%n%date{HH:mm:ss,fff} [%-5level] %m" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <param name="LevelMin" value="Info" /> <param name="LevelMax" value="Fatal" /> </filter> </appender> <root> <!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) --> <level value="all" /> <appender-ref ref="ColoredConsoleAppender"/> <appender-ref ref="RollingLogFileAppender"/> </root> </log4net> </configuration> image.png 在Main方法种开始执行时,对Log4Net进行日志配置加载。 第二部:爬取信息 使用HtmlAgility,如果对此第三方库不是很理解的,可以参考LZ的爬虫系列文章,里面有对其讲解。 代码不做过多讲解,主要就是注意获取当天数据 public class JKNews { /// <summary> /// 得到今日的新闻 /// </summary> /// <returns></returns> public static List<Notice> GetTodayNews() { string[] url = { "http://www.jit.edu.cn/myNews_list_out.aspx?infotype=1", "http://www.jit.edu.cn/myNews_list_out.aspx?infotype=2" }; var web = new HtmlWeb(); web.OverrideEncoding = Encoding.GetEncoding("gb2312"); List<Notice> noticeItems = new List<Notice>(); for (int i = 0; i < url.Length; i++) { var docWeb = web.Load(url[i]); var listItems = docWeb.DocumentNode.SelectNodes("//*[@id=\"table_list\"]/li").ToList(); foreach (var item in listItems) { string href = item.SelectSingleNode("./a").Attributes["href"].Value; string title = item.InnerText; string remark = item.SelectSingleNode("./span[@class=\'puber\']").InnerText; var splitArr = remark.Split(' '); string dep = splitArr[0].TrimStart('['); string time = splitArr[1].TrimEnd(']'); noticeItems.Add(new Notice() { Href = href, Title = title, Dep = dep, Time = time }); } } return noticeItems.Where(u => Convert.ToDateTime(u.Time).Date.Equals(DateTime.Now)).ToList(); } } 比较俩日期相同方法一: System.Data.Entity.DbFunctions.DiffDays(cs.StartTime.Value,DateTime.Now) == 0只获取当天方法二: XX.StartTime.Value.Date.Equals(DateTime.Now.Date)//只获取当天 使用方法一.png 第三步:使用QuartZ建立一个定时任务 1.建立一个 ToExcelJob 继承 IJob接口,实现方法 定时方法.png 定时方法Exceute种分俩部分执行 第一部分:获取爬取后的数据 第二部分:数据保存到Excel文件 public class ToExcelJob : IJob { private static string excelPath = ConfigurationManager.AppSettings["ExcelPath"]; static readonly ILog Log = LogManager.GetLogger(typeof(ToExcelJob)); public void Execute(IJobExecutionContext context) { try { if (!Directory.Exists(excelPath)) { Directory.CreateDirectory(excelPath); } var items = JKNews.GetTodayNews(); var excel = new Excel(); excel.CreateSheet("Sheet1"); int rowIndex = 0; excel.WriteTitle(new string[] { "链接", "标题", "部门", "日期" }, 0, 0); rowIndex++; foreach (var item in items) { excel.CreateRow(0, rowIndex); excel.WriteProperty<Notice>(item, 0, rowIndex, 0); rowIndex++; } excel.SetColumnWidth(0, 0, new[] { 20, 30, 10, 10 }); string savePath = Path.Combine(excelPath, string.Format("{0}.xlsx", DateTime.Now.ToString("yyyy.MM.dd"))); excel.WriteFile(savePath); } catch (Exception ex) { Log.Error(ex.Message, ex); } } } 第四步:定制TopShelf服务类,对开始和结束进行代码描述 构造函数中使用Quartz.Net 开始进行任务的创建 首先创建一个调度器 然后创建一个任务 然后创建一个触发器,这一步中我们设置了cron为每晚8点,具体使用的大家可以看cron语法 然后将任务与触发器添加到调度器中并执行 在构造函数中对当前任务(Job)进行初始化配置,然后开放俩个方法Start和Stop让便上层调用 public class ToExcelRunner { static readonly ILog _log = LogManager.GetLogger(typeof(ToExcelRunner)); private readonly IScheduler scheduler; public ToExcelRunner() { // 创建一个调度器 scheduler = StdSchedulerFactory.GetDefaultScheduler(); //2、创建一个任务 IJobDetail job = JobBuilder.Create<ToExcelJob>().WithIdentity("job1", "group1").Build(); //3、创建一个触发器 ITrigger trigger = TriggerBuilder.Create() .WithIdentity("trigger1", "group1") .WithCronSchedule("0 0 20 ? * *") //每天晚上8点执行 .Build(); //4 将任务与触发器添加到调度器中并执行 scheduler.ScheduleJob(job, trigger); } public void Start() { try { _log.Info("服务开启"); scheduler.Start(); } catch (Exception e) { Console.WriteLine(e); throw; } } public void Stop() { _log.Info("服务结束"); scheduler.Shutdown(false); } } 第五步:TopShelf配置 s.ConstructUsing(name => new ToExcelRunner()); 服务使用ToExcelRunner 对服务的Started注册tc.Start()方法 对服务的Stopped注册tc.Stop()方法 使用日志记录,出错时候记录。 class Program { static readonly ILog _log = LogManager.GetLogger(typeof(Program)); static void Main(string[] args) { FileInfo fi = new FileInfo(AppDomain.CurrentDomain.BaseDirectory + "\\log4net.config"); XmlConfigurator.ConfigureAndWatch(fi); try { HostFactory.Run(x => { x.Service<ToExcelRunner>(s => { s.ConstructUsing(name => new ToExcelRunner()); s.WhenStarted(tc => tc.Start()); s.WhenStopped(tc => tc.Stop()); }); x.RunAsLocalSystem(); x.SetDescription("每天晚上8点讲当日新闻保存为Excel"); x.SetDisplayName("新闻保存服务"); x.SetServiceName("新闻保存服务"); }); } catch (Exception ex) { _log.Error(ex); } } } 第六步:发布为windows服务 配置运行 没错,整个程序已经开发完了,接下来,只需要简单配置一下,即可以当服务来使用了。安装很方便: 安装:JKNoticeget.exe install 启动:JKNoticeget.exe start 卸载:JKNoticeget.exe uninstall 管理员身份进入,对应路径,注册服务 image.png image.png image.png 代码链接 https://github.com/happlyfox/FoxCrawler/tree/master/%E5%AD%A6%E4%B9%A0%E7%A4%BA%E4%BE%8B/JKNoticeget
错误信息:程序包无效。 详细信息:“Cannot load extension with file or directory name . Filenames starting with "" are reserved for use by the system.”。 1、找到Chrome安装程序路径,找到对应的插件 image.png 2、把crx后缀名改为rar,解压缩得到文件夹(有错误提示不用理会),选择全部替换即可 image.png 3、打开该文件夹,把里面的"_metadata"文件夹改名为"metadata"(去掉下杠) image.png 4、进入扩展程序中心,启用开发者模式,加载正在开发的程序包,选择刚才的文件夹就行了,搞定! 进入扩展插件目录后(chrome://extensions)加载已解压的扩展程序
正常情况下,Chrome插件扩展程序的默认安装目录如下: 1.windows xp中chrome插件默认安装目录位置: C:\Documents and Settings\用户名\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions 2.windows7中chrome插件默认安装目录位置: C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions 3.MAC中chrome插件默认安装目录位置:~/Library/Application Support/Google/Chrome/Default/Extensions 4.Ubuntu中chrome插件默认安装目录位置: ~/.config/google-chrome/Default/Extensions 如果在这些不同操作系统中的chrome插件默认安装位置,没有找到插件。那么请通过下面的方式查看,如下图所示: 1.地址栏输入chrome:version 回车 2.用资源管理器打开"个人资料路径"栏的路径,该路径下的Extensions文件夹即默认的扩展安装路径 chrome插件安装目录位置
常用日期帮助使用 C# DateTime日期格式化 C# DateTime与时间戳转换 不同运行环境日期统一 1、CultureInfo的InvariantCulture的作用 (1)、CultureInfo使整个.NET Framework更加人性化,因为这可以使同一个数据适应不同地区和文化,这样当然满足处于不同地区和文化的用户。但前提是数据给“人”看,如果这些数据用于计算机之间的传输,即给“机器”看,这样的多文化处理反而不妥,造成同一个数据的不同展现形式,尤其是读写两方的文化地区不同时,数据可能根本无法被正常读取或者产生潜在bug,因此这里,正是InvariantCulture的用武之地。 (2)、当进行数字,日期时间,字符串匹配时,都会进行CultureInfo的操作,也就是不同的CultureInfo下,这些操作的结果可能会不一样。这里要介绍一下非常容易被忽视InvariantCulture。 2、使用场景 你编写一个程序,要向数据中心服务器传递一些时间数据,你会怎么写?直接DateTime.ToString()?那你就大错特错了,下面用代码,举个非常形象的例子。在一个控制台里,模拟数据中心,然后放出多个线程,模拟客户端程序传递数据。 static readonly string[] CultureSources = { "en-us", "zh-cn", "ar-iq", "de-de" }; static readonly Random Ran = new Random(Environment.TickCount); static void Main() { Console.WriteLine("数据中心开始接受客户端数据:"); for (int i = 0; i < CultureSources.Length; i++) ThreadPool.QueueUserWorkItem(Client, i); Console.ReadKey(true); Console.WriteLine(""); Console.WriteLine("数据中心:…………"); } static void Client(object obj) { int id = (int)obj; Thread.Sleep(Ran.Next(1000)); CultureInfo cul = CultureInfo.GetCultureInfo(CultureSources[id]); Thread.CurrentThread.CurrentCulture = cul; Console.WriteLine("某客户端操作系统语言设置{0}\n传送数据:{1}\n", cul.DisplayName, new DateTime(1990, 10, 27).ToShortDateString()); } 同样的DateTime.ToShortDateString(),在英语-美国,中文-中国,阿拉伯语-伊拉克和德语-德国的不同环境下,1990年10月27日竟然有如此不同的输出结果,这些数据让数据中心服务器情何以堪啊!!! 造成这个情况的原因是:在进行日期时间输出时,.NET会考虑当前线程的CultureInfo,即Thread.CurrentThread.CurrentCulture(或者CultureInfo.CurrentCulture),并根据CultureInfo,进行相应地区文化的数据处理。注意不要和UICulture混淆。 解决方案就是使用这个特殊的InvariantCulture.解决代码如下: Console.WriteLine("某客户端操作系统语言设置{0}\n传送数据:{1}\n", cul.DisplayName, new DateTime(1990, 10, 27).ToString(CultureInfo.InvariantCulture.DateTimeFormat.ShortDatePattern, CultureInfo.InvariantCulture)); image ok,这样不管客户端运行在什么语言环境下,输出的时间格式都是统一的,方面数据中心服务器对数据做后续处理。这样所有的输出结果都保持一致了。
Postman 使用手册系列教程: Postman软件安装Postman使用手册1——导入导出和发送请求查看响应Postman使用手册2——管理收藏Postman使用手册3——环境变量Postman使用手册4——API test
案例 same.png 语句 查找相同的数据 SELECT * FROM Test.dbo.test a WHERE EXISTS ( SELECT * FROM Test.dbo.test WHERE a.name=name GROUP BY name ,dep HAVING COUNT(*) > 1 ); 去重方法 DELETE FROM Test.dbo.Test WHERE ID NOT IN ( SELECT MIN(ID) FROM Test.dbo.Test GROUP BY Name ,Dep ); OR DELETE FROM Test.dbo.Test WHERE ID NOT IN ( SELECT MAX(ID) FROM Test.dbo.Test GROUP BY Name ,Dep )
引用的程序集: NewtonSoft 第一种:使用对象的字段属性设置JsonProperty来实现(不推荐,因为需要手动的修改每个字段的属性) public class UserInfo { [JsonProperty("id")] public int Id{ set; get; } [JsonProperty("userName")] public string UserName{ set; get; } } 第二种:使用newtonsoft.json来设置格式化的方式(推荐使用) var user = new UserInfo { UserName = "john", Id = 19 }; var serializerSettings = new JsonSerializerSettings { // 设置为驼峰命名 ContractResolver = new CamelCasePropertyNamesContractResolver() }; var userStr = JsonConvert.SerializeObject(user, serializerSettings); var data = JsonConvert.DeserializeObject<UserInfo>(userStr); Console.WriteLine(data.UserName + " " + data.Id); 转换后的JSON 序列化.png 驼峰命名,反列化也一样能够映射到实体上面 反序列化.png
请求发送者与接收者解耦——命令模式(一)请求发送者与接收者解耦——命令模式(二)请求发送者与接收者解耦——命令模式(三)请求发送者与接收者解耦——命令模式(四)请求发送者与接收者解耦——命令模式(五)请求发送者与接收者解耦——命令模式(六)
数据库环境: 1、SQLServer 2008R2 2、SQLServer 代理打开 一、新建一个数据库 创建数据库 Incremental_DB image.png 二、创建俩张测试表 数据库脚本链接 --创建用户表 CREATE TABLE [dbo].[Person]( [Id] [INT] IDENTITY(1,1) NOT NULL, [Name] [NVARCHAR](120) NULL, [Age] [INT] NULL, CONSTRAINT [PK_Demo] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] --创建部门表 CREATE TABLE [dbo].[Department]( [Id] [INT] IDENTITY(1,1) NOT NULL, [Name] [NVARCHAR](50) NULL, CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] 三、实现数据变更捕获 一、对目标库显式启用CDC USE Incremental_DB GO EXECUTE sys.sp_cdc_enable_db; GO 查看是否启用CDC SELECT is_cdc_enabled,CASE WHE is_cdc_enabled=0 THEN 'CDC功能禁用'ELSE 'CDC功能启用'END [描述] FROM sys.databases WHERE [name]='Incremental_DB' 创建成功后,会在测试库自动添加CDC用户和架构 image.png 二、对目标库数据表显式启用CDC USE Incremental_DB EXECUTE sys.sp_cdc_enable_table @source_schema = N'dbo'--架构名称 , @source_name = N'Department'--表名称 , @role_name = 'cdc_Admin'--会自动生成自定义 'cdc_Admin' 角色 如果不想控制访问角色,则@role_name必须显式设置为null , @capture_instance=NULL EXECUTE sys.sp_cdc_enable_table @source_schema = N'dbo'--架构名称 , @source_name = N'Person'--表名称 , @role_name = 'cdc_Admin'--会自动生成自定义 'cdc_Admin' 角色 如果不想控制访问角色,则@role_name必须显式设置为null , @capture_instance=NULL 语句执行成功后在系统表中生成俩张变化表 新增表.png 数据库可编程性中增加俩个函数 新增函数.png 同时SQLServer 代理中新增俩个作业 capture(捕获作业) clean(清理作业) 新增作业.png 如果出现如下错误,请参考上一篇文章CDC注意事项第三点 image.png 验证数据表是否启用CDC USE Incremental_DB --查看是否已启用: SELECT name , is_tracked_by_cdc , CASE WHEN is_tracked_by_cdc = 0 THEN 'CDC功能禁用' ELSE 'CDC功能启用' END 描述 FROM sys.tables WHERE OBJECT_ID IN(OBJECT_ID('Person'),OBJECT_ID('Department')) 四、禁用数据库或数据表CDC功能 禁用数据表CDC --禁用人员表CDC功能 EXECUTE sys.sp_cdc_disable_table @source_schema = N'dbo', @source_name = N'Person', @capture_instance = 'dbo_Person' 禁用后系统表中原来的dbo_Person_CT表被删除,函数删除等 禁用后系统表截图.png 禁用数据库CDC EXEC sys.sp_cdc_disable_db
CDC介绍 cdc.png 为了满足数据迁移和数据抽取的业务需要,使得有机会在数据库层面上直接实现增量抽取功能,ORACLE综合性能和场景需要,在数据库引擎层面直接集成了CDC功能,由于提供了类似API的功能接口,变更数据捕获和更改跟踪均不要求在源中进行任何架构更改或使用触发器,所以比第三方工具具有一定的优势。利用CDC捕获变更有以下特点: ① 性能影响小。使用异步进程捕获,通过进程读取事务日志,对系统造成的影响很小,不对业务系统造成太大的压力,影响现有业务。 ② 监控范围大。对该表的所有DML和DDL操作都会被记录,有助于跟踪表的变化,实现表操作的追根溯源。 ③ 操作简单 。CDC是在数据库引擎中添加的功能,封装在数据库中,类似于API接口调用,不需要复杂的业务处理逻辑就可以实现DML和DDL的操作监控。 ④ 有一定时延性。由于捕获进程从事务日志中提取更改数据,因此,向源表提交更改的时间与更改出现在其关联更改表中的时间之间存在内在的延迟。 虽然这种延迟通常很小,但务必记住,在捕获进程处理相关日志项之前无法使用更改数据。 CDC注意事项 1. SQL Server的版本必须是2008或以上; 2. 不能同时使用内存优化表(SQL Server2014或以上版本才有的功能)。否则会出现以下错误: image.png @@SERVERNAME、serverproperty('servername')两者(本地服务器名和服务器实例的属性必须一致)必须一致。下面脚本可将两者调整成一致。如果执行后两者仍不一致,需要重启SQL Server服务。 if serverproperty('servername') <> @@servername begin declare @server sysname set @server = @@servername exec sp_dropserver@server =@server set @server = cast(serverproperty('servername') as sysname) exec sp_addserver@server = @server , @local = 'LOCAL' PRINT 'ok' end select @@SERVERNAME,serverproperty('servername') 必须开启SQL Sever代理服务。CDC功能必须通过作业来实现。 开启CDC功能的表,无法使用 TRUNCATE TABLE 。可以先禁用,执行完truncate再启用cdc。 6.如果表结构发生变化,则捕获实例表中:新增列无法捕获到、删除列保持NULL、修改列类型会发生强制转换。为保险起见,应禁用捕获实例,然后再启用。 7.在查询CDC相关表时,建议加上With(NOLOCK),否则易产生阻塞或死锁。 一个表最多只能有两个捕获实例。
核心代码分析 最关键的在于获取捕获表信息(系统表中间_CT结尾的数据)。 根据网上资料查取,找到了获取当前捕获表时间区间范围内数据的方式。 见[SQL Server 多表数据增量获取和发布 2.3(https://www.jianshu.com/p/6a400eca6e79) --10.按照时间范围查询CDC结果 DECLARE @from_lsn BINARY(10),@end_lsn BINARY(10) DECLARE @start_time DATETIME = '2018-08-01' DECLARE @end_time DATETIME ='2018-08-30' SELECT @from_lsn=sys.fn_cdc_map_time_to_lsn('smallest greater than or equal',@start_time) SELECT @end_lsn=sys.fn_cdc_map_time_to_lsn(' largest less than or equal',@end_time) SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_Department(@from_lsn,@end_lsn,'all') 数据既然能够通过sql语句获取到,那么逻辑判断就会变得简单,通过分析我们可以发现select * from XXX ,XXX就是上文中讲到的CDC生成的表值函数,表值函数前面相等,可变化的就是架构名_表名称(dbo_Person) image.png 所以我们完全可以通过拼接sql语句得到我们需要的内容,可以默认返回给我们的数据是不友好的,我们还需要自己在做一步设置,将某些字段变成我们好理解的内容 如对下文内容不理解,可翻阅LZ之前的文章 sys.fn_cdc_map_lsn_to_time(__$start_lsn) AS UpdateTime [__$operation] AS Operation 通过查看CDC生成的捕获表我们发现,其实他是在原来的数据表结构上新增了几个字段给我们,其他的表也相同。 image.png image.png 那我们在代码中对实体的设计就可以基于继承相同父类的方式,定义一个父类,拥有共同属性 public partial class ExtBase { /// <summary> /// 更新时间 /// </summary> public DateTime UpdateTime { get; set; } /// <summary> /// 操作方式 1 = 删除,2 = 插入,3 = 更新(旧值),4 = 更新(新值) /// </summary> public int Operation { get; set; } } 其他表都是在自己原来字段的基础上继承当前父类 public class Department : ExtBase { public int Id { get; set; } public string Name { get; set; } } public class Person : ExtBase { public int Id { get; set; } public string Name { get; set; } public int? Age { get; set; } } 实体类结构完毕后我们开始考虑获取数据的业务逻辑,根据业务我们可以假设获取数据的方法几乎相同,不同的地方就是返回的数据实体集合不同,那我们通过何种方法来完成逻辑的有效封装,这是需要考虑的问题。 经过思考,我构想出了一种方法 1、定义一个抽象基类,在其中定义公共业务逻辑(GetDate)方法,然后定义一个抽象方法,抽象方法需要被子类继承,而子类需要做的就是覆写父类的GetData方法,唯一需要修改的就是传递的实体——可以采用泛型变量的形式去实现 2、等所有的子类构建完成以后,创建一个简单工厂,传递需要的参数,然后根据参数中的唯一标识符,实例化对应的操作类去执行公共方法。 首先是基类抽象类 /// <summary> /// Cdc 数据捕获服务类 /// </summary> /// <typeparam name="T"></typeparam> public abstract class CTBaseService { /// <summary> /// 获取CDC捕获表的数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="schema_table"></param> /// <param name="startDateTime"></param> /// <returns></returns> private List<T> GetRangeList<T>(string schema_table, DateTime startDateTime) where T : class, new() { //获取当前需要更新的日期集合列表 var conn = new SqlConnection(StaticConst.Conn); string query = @"SELECT * ,sys.fn_cdc_map_lsn_to_time(__$start_lsn) AS UpdateTime,[__$operation] AS Operation FROM [cdc].[{2}_CT] WHERE[__$operation] IN(1, 2, 4) AND sys.fn_cdc_map_lsn_to_time(__$start_lsn) > '{0}' AND sys.fn_cdc_map_lsn_to_time(__$start_lsn) <= '{1}';"; string nowDate = DateTime.Now.ToString(); query = string.Format(query, startDateTime.ToString(), nowDate, schema_table); var queryList = conn.Query<T>(query).ToList(); return queryList; } /// <summary> /// 抽象方法,由父类实现 /// </summary> /// <param name="id"></param> /// <param name="schema_table"></param> /// <param name="startDateTime"></param> public abstract void Work(int id, string schema_table, DateTime startDateTime); /// <summary> /// 得到CDC捕获数据并插入队列 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="id"></param> /// <param name="schema_table"></param> /// <param name="startDateTime"></param> protected void GetRangeListAndInsertQueue<T>(int id, string schema_table, DateTime startDateTime) where T : ExtBase, new() { //获取当前需要更新的日期集合列表 List<T> queryList = GetRangeList<T>(schema_table, startDateTime); if (queryList.Count > 0) { //对集合进行操作 【集合序列化,注意集合反序列化】 string jsonItem = JsonConvert.SerializeObject(queryList); QueueWorker.Instance.EnqueueItem(jsonItem); int iret = UpdateServiceLog(id, queryList); string nowDate = DateTime.Now.ToString(); if (iret > 0) { Log4NetHelper.Info(string.Format("表名:{0}\r\n 队列数据:{1} \r\n 插入时间:{2}", schema_table, jsonItem, nowDate)); } } } } 子类实现 我们可以发现子类实现非常好理解,正如上文所说,基类提供了一个抽象接口供子类实现,而子类也真的只需要修改一个实体就可以。如果大家不懂,可以多看几遍来裂解其中的设计方式。 public class DepartmentCT : CTBaseService { public override void Work(int id, string schema_table, DateTime startDateTime) { base.GetRangeListAndInsertQueue<Department>(id, schema_table, startDateTime); } } public class PersonCT : CTBaseService { public override void Work(int id, string schema_table, DateTime startDateTime) { base.GetRangeListAndInsertQueue<Person>(id, schema_table, startDateTime); } } 最后我们建立一个工厂类,工厂类主要负责接受参数并创建对应的CT帮助类,代码结构如下。根据表名作为唯一标识符字段,创建***CT服务类,然后因为他们继承并覆写了父类抽象方法Work,所以调用.Work方法即可实现获取数据并插入队列的功能。 public class CTCExecuteFactory { /// <summary> /// 执行服务 /// </summary> /// <param name="id"></param> /// <param name="schema_table"></param> /// <param name="updateTime"></param> public static void ExecuteService(int id, string schemaName, string tableName, DateTime updateTime) { CTBaseService service = null; switch (tableName.ToLower()) { case ServiceTables.person: service = new PersonCT(); break; case ServiceTables.department: service = new DepartmentCT(); break; default: break; } if (service != null) service.Work(id, string.Format("{0}_{1}", schemaName, tableName), updateTime); } } 其他模块的代码我觉得属于正常理解范围内的东西,不予说明,有兴趣的可自行下载代码查看具体功能。下载链接
程序结构 新建一个窗体应用程序,新增三个类库,实现各个层次责任分离 BLL 业务逻辑层 Common 公共帮助类层 Models 模型层 SqlMonitoring SQL监视器程序 image.png 程序设计思路 大概的设计思路是这样的: 1、因为是多表数据增量获取,首先通过配置CDC已经完成多表的捕获配置。 通过CDC实现了数据的捕获,我们需要开一个服务,循环读取捕获表的内容(通过时间戳字段),因为使用时间戳,所以需要对每次的时间进行保存,方便下一次获取数据区间做判断条件。 因此设计一张数据表时间戳记录表,数据结构如下 时间记录表数据结构 主键,架构名,表名称,上次更新时间 image.png image.png 2、根据时间戳循环读取多表数据,将数据放入队列中。 3、在开启一个服务,循环读取队列里面的数据,为了保证数据有效性,将队列中的数据传送给服务接口的时候还要再本地同时进行备份的保存,只有当数据服务返回正确值得时候,更改日志表的状态位。 日志记录表数据结构 主键,时间节点,序列化数据,状态位 image.png image.png 重点 如果需要更新状态位,那么需要一个标识位来判断,那么这个标识位是什么。大家可以猜一猜,答案就是UpdateTime字段。 数据库CDC数据捕获的时间精确到毫秒三位,可以保证一般系统数据唯一性,队列中存在当前字段,将当前字段作为标识来更新日志记录表的状态位。
一、验证DML SELECT COUNT(1) AS '原总行数' FROM dbo.Person /* 原总行数 0 */ --1. Insert 插入5条数据 INSERT INTO Department( Name ) VALUES ('部门0000000009') GO 5 --2. Update UPDATE Department SET Name = substring(Name,0,10)+'_Update' --3. Delete DELETE FROM Department WHERE id>4 --查看捕获到的数据变更信息 SELECT * FROM cdc.dbo_Department_CT 列名 数据类型 说明 __$start_lsn binary(10) 更改提交的LSN。在同一事务中提交的更改将共享同一个提交 LSN 值。 __$seqval binary(10) 一个事务内可能有多个更改发生,这个值用于对它们进行排序。 __$operation Int 更改操作的类型:1 = 删除2 = 插入3 = 更新(捕获的列值是执行更新操作前的值)。4 = 更新(捕获的列值是执行更新操作后的值)。 __$update_mask varbinary(128) 位掩码,源表中被CDC跟踪的每一列对应一个位。如果__operation = 3 或 4,则只有那些对应已更改列的位设置为 1。 image.png 二、验证DDL ALTER TABLE dbo.Department ADD remark NVARCHAR(20) NOT NULL DEFAULT(0) image.png 三、相关脚本 --1. 返回所有表的变更捕获配置信息 EXECUTE sys.sp_cdc_help_change_data_capture; --2. 查看对某个实例(即表)的哪些列做了捕获监控: EXEC sys.sp_cdc_get_captured_columns @capture_instance='dbo_Department' --3. 查找配置信息: SELECT * FROM msdb.dbo.cdc_jobs image.png --4. 查看配置 EXEC sp_cdc_help_jobs image.png --5. -------------------- 修改配置 ---------------------- --显示原有配置: EXEC sp_cdc_help_jobs GO --更改数据保留时间为24*60分钟 (默认4320) EXECUTE sys.sp_cdc_change_job @job_type = N'cleanup', @retention=1440 GO --停用作业 EXEC sys.sp_cdc_stop_job N'cleanup' GO --启用作业 EXEC sys.sp_cdc_start_job N'cleanup' GO --再次查看 EXEC sp_cdc_help_jobs GO image.png --7. 最近进行的会话的平均滞后时间 SELECT latency FROM sys.dm_cdc_log_scan_sessions WHERE session_id = 0 --8. 最近会话的平均吞吐量 ( 每个会话期间每秒处理的平均命令数 ) SELECT command_count/duration AS [Throughput] FROM sys.dm_cdc_log_scan_sessions WHERE session_id = 0 --9. 使用 sys.fn_cdc_map_lsn_to_time 函数.( Sys.fn_cdc_map_time_to_lsn 略 ) SELECT [__$operation] , CASE [__$operation] WHEN 1 THEN '删除' WHEN 2 THEN '插入' WHEN 3 THEN '更新(捕获的列值是执行更新操作前的值)' WHEN 4 THEN '更新(捕获的列值是执行更新操作后的值)' END [类型], sys.fn_cdc_map_lsn_to_time([__$start_lsn]) [更改时间] , * FROM cdc.dbo_Department_CT image.png --10.按照时间范围查询CDC结果 DECLARE @from_lsn BINARY(10),@end_lsn BINARY(10) DECLARE @start_time DATETIME = '2018-08-01' DECLARE @end_time DATETIME ='2018-08-30' SELECT @from_lsn=sys.fn_cdc_map_time_to_lsn('smallest greater than or equal',@start_time) SELECT @end_lsn=sys.fn_cdc_map_time_to_lsn(' largest less than or equal',@end_time) SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_Department(@from_lsn,@end_lsn,'all') image.png
数据库环境: 1、SQLServer 2008R2 2、SQLServer 代理打开 一、新建一个数据库 创建数据库 Incremental_DB image.png 二、创建俩张测试表 数据库脚本链接 --创建用户表 CREATE TABLE [dbo].[Person]( [Id] [INT] IDENTITY(1,1) NOT NULL, [Name] [NVARCHAR](120) NULL, [Age] [INT] NULL, CONSTRAINT [PK_Demo] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] --创建部门表 CREATE TABLE [dbo].[Department]( [Id] [INT] IDENTITY(1,1) NOT NULL, [Name] [NVARCHAR](50) NULL, CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] 三、实现数据变更捕获 一、对目标库显式启用CDC USE Incremental_DB GO EXECUTE sys.sp_cdc_enable_db; GO 查看是否启用CDC SELECT is_cdc_enabled,CASE WHE is_cdc_enabled=0 THEN 'CDC功能禁用'ELSE 'CDC功能启用'END [描述] FROM sys.databases WHERE [name]='Incremental_DB' 创建成功后,会在测试库自动添加CDC用户和架构 image.png 二、对目标库数据表显式启用CDC USE Incremental_DB EXECUTE sys.sp_cdc_enable_table @source_schema = N'dbo'--架构名称 , @source_name = N'Department'--表名称 , @role_name = 'cdc_Admin'--会自动生成自定义 'cdc_Admin' 角色 如果不想控制访问角色,则@role_name必须显式设置为null , @capture_instance=NULL EXECUTE sys.sp_cdc_enable_table @source_schema = N'dbo'--架构名称 , @source_name = N'Person'--表名称 , @role_name = 'cdc_Admin'--会自动生成自定义 'cdc_Admin' 角色 如果不想控制访问角色,则@role_name必须显式设置为null , @capture_instance=NULL 语句执行成功后在系统表中生成俩张变化表 新增表.png 数据库可编程性中增加俩个函数 新增函数.png 同时SQLServer 代理中新增俩个作业 capture(捕获作业) clean(清理作业) 新增作业.png 如果出现如下错误,请参考上一篇文章CDC注意事项第三点 image.png 验证数据表是否启用CDC USE Incremental_DB --查看是否已启用: SELECT name , is_tracked_by_cdc , CASE WHEN is_tracked_by_cdc = 0 THEN 'CDC功能禁用' ELSE 'CDC功能启用' END 描述 FROM sys.tables WHERE OBJECT_ID IN(OBJECT_ID('Person'),OBJECT_ID('Department')) 四、禁用数据库或数据表CDC功能 禁用数据表CDC --禁用人员表CDC功能 EXECUTE sys.sp_cdc_disable_table @source_schema = N'dbo', @source_name = N'Person', @capture_instance = 'dbo_Person' 禁用后系统表中原来的dbo_Person_CT表被删除,函数删除等 禁用后系统表截图.png 禁用数据库CDC EXEC sys.sp_cdc_disable_db
CDC介绍 cdc.png 为了满足数据迁移和数据抽取的业务需要,使得有机会在数据库层面上直接实现增量抽取功能,ORACLE综合性能和场景需要,在数据库引擎层面直接集成了CDC功能,由于提供了类似API的功能接口,变更数据捕获和更改跟踪均不要求在源中进行任何架构更改或使用触发器,所以比第三方工具具有一定的优势。利用CDC捕获变更有以下特点: ① 性能影响小。使用异步进程捕获,通过进程读取事务日志,对系统造成的影响很小,不对业务系统造成太大的压力,影响现有业务。 ② 监控范围大。对该表的所有DML和DDL操作都会被记录,有助于跟踪表的变化,实现表操作的追根溯源。 ③ 操作简单 。CDC是在数据库引擎中添加的功能,封装在数据库中,类似于API接口调用,不需要复杂的业务处理逻辑就可以实现DML和DDL的操作监控。 ④ 有一定时延性。由于捕获进程从事务日志中提取更改数据,因此,向源表提交更改的时间与更改出现在其关联更改表中的时间之间存在内在的延迟。 虽然这种延迟通常很小,但务必记住,在捕获进程处理相关日志项之前无法使用更改数据。 CDC注意事项 1. SQL Server的版本必须是2008或以上; 2. 不能同时使用内存优化表(SQL Server2014或以上版本才有的功能)。否则会出现以下错误: image.png @@SERVERNAME、serverproperty('servername')两者(本地服务器名和服务器实例的属性必须一致)必须一致。下面脚本可将两者调整成一致。如果执行后两者仍不一致,需要重启SQL Server服务。 if serverproperty('servername') <> @@servername begin declare @server sysname set @server = @@servername exec sp_dropserver@server =@server set @server = cast(serverproperty('servername') as sysname) exec sp_addserver@server = @server , @local = 'LOCAL' PRINT 'ok' end select @@SERVERNAME,serverproperty('servername') 必须开启SQL Sever代理服务。CDC功能必须通过作业来实现。 开启CDC功能的表,无法使用 TRUNCATE TABLE 。可以先禁用,执行完truncate再启用cdc。 6.如果表结构发生变化,则捕获实例表中:新增列无法捕获到、删除列保持NULL、修改列类型会发生强制转换。为保险起见,应禁用捕获实例,然后再启用。 7.在查询CDC相关表时,建议加上With(NOLOCK),否则易产生阻塞或死锁。 一个表最多只能有两个捕获实例。
功能需求: 子公司统一门户系统已完成开发,安全运行一年。接到通知,总部也开发了一套统一门户,要求各子公司使用总部开发的平台,子公司领导讨论决定使用总公司开发的平台,但是也不放弃自己开发的平台,而使用一个折中的方案——使用子公司统一平台发布信息后需要将数据同步到总公司开发的平台数据库中(时间间隔需要越短越好) 开发问题: 1、无法提供统一门户系统源码,只能提供门户表的数据字典 2、领导要求基于门户表内容进行数据变化记录功能——当数据变化(新增,编辑,删除),同步信息到总公司平台,同时本地记录变化信息(永久保存或者保存几个月) 3、需要保证同步的准确性和数据变化的实效性 开发思路: 在开发前,首先考虑的了几个问题: 1、无法获取统一门户平台源代码,哪些技术方法就不能使用 2、如何实时记录数据库表的变化信息,写一个循环服务还是有工具软件可以实现 3、用什么技术进行开发,选取什么应用作为开发模式 一、首先基于第一点,无法得到源代码,如果在有源代码的前提下,可以采取的方式为——对需要修改的表,进行代码层更改,新建一张表历史信息表,在对应增加删除修改的方法前后的前后进行日志记录。因为没有源码,服务->数据库的这条路径就走不通,只能想办法走数据库->服务这条路。 二、实时记录数据库表变化信息,通过百度找到了俩种可能实现的方式。 1、SqlDependency SqlDependency是封装在.net framework种的一个帮助类库,可以实现对sqlserver 数据库的单表监听 好处: 1、使用简洁 2、网上资源较多 坏处: 1、版本好像只支持sqlserver 2005,太局限 2、实现逻辑太复杂,且只是对单表监控,如果要对多表,还需自己定制化修改 3、根据demo,调试运行发现无法实现监听功能(本机用的是sqlserver 2008 r2版本,估计正好和第1点对应) 2、变更数据捕获(Change Data Capture 即CDC)功能 CDC功能主要捕获SQLServer指定表的增删改操作,由于任何操作都会写日志(哪怕truncate),所以CDC的捕获来源于日志文件。日志文件会把更改应用到数据文件中,同时也会标记符合要求的数据标记为需要添加跟踪的项。然后通过一些配套函数,最后写入到数据仓库中。大概流程: cdc流程.png 好处: 1、在SQLServer2008(含)以后的企业版、开发版和评估版中可用。支持的数据库版本更多 2、网上对CDC的讲解和使用文档很多,方便学习 3、sqlserver自身的服务,安全可靠,且支持多表(只需一条配置语句),简单方便,效率高 4、方便运维 三、项目基于.Net开发,先编写一个测试demo,使用控制台程序。 大体的思路是: 建立三个类库+一个服务 模型层,公共帮助层,业务逻辑层+UI(控制台) 步骤1:基于cdc功能配置数据库需要的表,完成数据捕获功能配置。 步骤2:编写一个服务:循环秒后读取CDC新生成的数据捕获表,根据时间戳获取需要每张表的数据,将获取的数据放入一个队列中,同时将数据序列化放入日志表,设置标志位:未读状态 步骤3:开启一个方法——循环秒读取队列中的内容,如果队列中存在值,启动事务,同时修改数据库日志表当前信息的状态位和发送信息到总公司统一门户平台
对象间的联动——观察者模式(一)对象间的联动——观察者模式(二)对象间的联动——观察者模式(三)对象间的联动——观察者模式(四)对象间的联动——观察者模式(五)对象间的联动——观察者模式(五)
转载 确保对象的唯一性——单例模式 (一)确保对象的唯一性——单例模式 (二)确保对象的唯一性——单例模式 (三)确保对象的唯一性——单例模式 (四)确保对象的唯一性——单例模式 (五)
转载 操作复杂对象结构——访问者模式(一)(https://blog.csdn.net/LoveLion/article/details/7433523) 操作复杂对象结构——访问者模式(二)(https://blog.csdn.net/LoveLion/article/details/7433567) 操作复杂对象结构——访问者模式(三)(https://blog.csdn.net/LoveLion/article/details/7433576) 操作复杂对象结构——访问者模式(四)(https://blog.csdn.net/LoveLion/article/details/7433591)
转载 请求的链式处理——职责链模式(一) 请求的链式处理——职责链模式(二) 请求的链式处理——职责链模式(三) 请求的链式处理——职责链模式(四)
转载 https://blog.csdn.net/lovelion/article/details/8228042 某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下: (1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统; (2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。 该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。 试使用代理模式设计并实现该收费商务信息查询系统。 实例分析及类图 通过分析,可以采用一种间接访问的方式来实现该商务信息查询系统的设计,在客户端对象和信息查询对象之间增加一个代理对象,让代理对象来实现身份验证和日志记录等功能,而无须直接对原有的商务信息查询对象进行修改,如图1-1所示: 1-1.png 在图中,客户端对象通过代理对象间接访问具有商务信息查询功能的真实对象,在代理对象中除了调用真实对象的商务信息查询功能外,还增加了身份验证和日志记录等功能。使用代理模式设计该商务信息查询系统,结构图如图1-2所示。 1-2.png 在图1-2中,业务类AccessValidator用于验证用户身份,业务类Logger用于记录用户查询日志,Searcher充当抽象主题角色,RealSearcher充当真实主题角色,ProxySearcher充当代理主题角色 实例代码 (1) AccessValidator:身份验证类,业务类,它提供方法Validate()来实现身份验证。 //AccessValidator.cs using System; namespace ProxySample { class AccessValidator { //模拟实现登录验证 public bool Validate(string userId) { Console.WriteLine("在数据库中验证用户'" + userId + "'是否是合法用户?"); if (userId.Equals("杨过")) { Console.WriteLine("'{0}'登录成功!",userId); return true; } else { Console.WriteLine("'{0}'登录失败!", userId); return false; } } } (2) Logger:日志记录类,业务类,它提供方法Log()来保存日志。 //Logger.cs using System; namespace ProxySample { class Logger { //模拟实现日志记录 public void Log(string userId) { Console.WriteLine("更新数据库,用户'{0}'查询次数加1!",userId); } } } (3) Searcher:抽象查询类,充当抽象主题角色,它声明了DoSearch()方法。 namespace ProxySample { interface Searcher { string DoSearch(string userId, string keyword); } } (4) RealSearcher:具体查询类,充当真实主题角色,它实现查询功能,提供方法DoSearch()来查询信息。 //RealSearcher.cs using System; namespace ProxySample { class RealSearcher : Searcher { //模拟查询商务信息 public string DoSearch(string userId, string keyword) { Console.WriteLine("用户'{0}'使用关键词'{1}'查询商务信息!",userId,keyword); return "返回具体内容"; } } } (5) ProxySearcher:代理查询类,充当代理主题角色,它是查询代理,维持了对RealSearcher对象、AccessValidator对象和Logger对象的引用。 namespace ProxySample { class ProxySearcher : Searcher { private RealSearcher searcher = new RealSearcher(); //维持一个对真实主题的引用 private AccessValidator validator; private Logger logger; public string DoSearch(string userId, string keyword) { //如果身份验证成功,则执行查询 if (this.Validate(userId)) { string result = searcher.DoSearch(userId, keyword); //调用真实主题对象的查询方法 this.Log(userId); //记录查询日志 return result; //返回查询结果 } else { return null; } } //创建访问验证对象并调用其Validate()方法实现身份验证 public bool Validate(string userId) { validator = new AccessValidator(); return validator.Validate(userId); } //创建日志记录对象并调用其Log()方法实现日志记录 public void Log(string userId) { logger = new Logger(); logger.Log(userId); } } } (6) 配置文件App.config,在配置文件中存储了代理主题类类名。 <?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="proxy" value="ProxySample.ProxySearcher"/> </appSettings> </configuration> (7) Program:客户端测试类 //Program.cs using System; using System.Configuration; using System.Reflection; namespace ProxySample { class Program { static void Main(string[] args) { //读取配置文件 string proxy = ConfigurationManager.AppSettings["proxy"]; //反射生成对象,针对抽象编程,客户端无须分辨真实主题类和代理类 Searcher searcher; searcher = (Searcher)Assembly.Load("ProxySample").CreateInstance(proxy); String result = searcher.DoSearch("杨过", "玉女心经"); Console.Read(); } } } 结果及分析 在数据库中验证用户'杨过'是否是合法用户? '杨过'登录成功! 用户'杨过'使用关键词'玉女心经'查询商务信息! 更新数据库,用户'杨过'查询次数加1! 本实例是保护代理和智能引用代理的应用实例,在代理类ProxySearcher中实现对真实主题类的权限控制和引用计数,如果需要在访问真实主题时增加新的访问控制机制和新功能,只需增加一个新的代理类,再修改配置文件,在客户端代码中使用新增代理类即可,源代码无须修改,符合开闭原则。
Facade模式的几个要点 从客户程序的角度来看,Facade模式不仅简化了整个组件系统的接口,同时对于组件内部与外部客户程序来说,从某种程度上也达到了一种“解耦”的效果——内部子系统的任何变化不会影响到Facade接口的变化。 Facade设计模式更注重从架构的层次去看整个系统,而不是单个类的层次。Facade很多时候更是一种架构设计模式。 模式结构 UML.png Facade ° 知道哪些子系统类负责处理请求 ° 将客户的请求代理给相应的子系统对象 Subsystem Classes ° 实现子系统的功能 ° 处理由Facade对象指派的任务来协调子系统下各子类的调用方式 在外观模式中,外观类Facade的方法OptionWrapper实现的就是以不同的次序调用下面类SubSystem1、SubSystem2的方法Operation,通过不同的Operation组合实现装饰功能。 模式图例 假设我们需要开发一个坦克模拟系统用于模拟坦克车在各种作战环境中的行为,其中坦克系统由引擎、控制器、车轮、车身等各子系统构成。 结构.png 设计图.png 代码 public class BodyWork { public void BAction() { Console.WriteLine("车身"); } } public class Controller { public void CAction() { Console.WriteLine("车身"); } } public class Enigine { public void EAction() { Console.WriteLine("引擎"); } } public class Wheel { public void WAction() { Console.WriteLine("车轮"); } } public class Tank { private BodyWork _bodyWork; private Enigine _enigine; private Controller _controller; private Wheel _wheel; public Tank() { _bodyWork = new BodyWork(); _enigine = new Enigine(); _controller = new Controller(); _wheel = new Wheel(); } public void MethodA() { _bodyWork.BAction(); _enigine.EAction(); _controller.CAction(); _wheel.WAction(); } }
出错提示:windows功能无法安装以下功能:.NET Framework 3.5(包括.NET 2.0和3.0) 安装方式1 1.png 安装方式2 运行网上下载的.net framework 3.5安装包 解决方案 在出现无法安装这种情况的时候,LZ当初尝试了各种方法,百度了很多内容,但排在前面的一些方法,不是盗版别人的文章就是上文不接下文,还有一些就是没有说清楚,给出的解决方法看不太懂,在这里我把内容整理一下,用简单的方式让人类可以听懂。 我经过过俩种系统无法安装的,我觉得大概的方式也就这俩个了 win7或者win10系统 第一种 使用安装系统的镜像文件,我们首先装载镜像文件,记录下镜像文件的盘符 1.png 接下来管理员身份开启cmd, dism.exe /online /enable-feature /featurename:netfx3 /Source:L:\sources\sxs 只需要修改盘符位置,路径不需要修改 第二种 1、如果找不到了安装系统的镜像文件,下载一个NetFx3.cab文件, 下载地址 https://pan.baidu.com/s/1geAjsaf 2、将下载的文件放到系统盘C:\Windows文件夹里面 3、进入cmd命令行模式,管理员身份运行 dism /online /Enable-Feature /FeatureName:NetFx3 /Source:"%windir%" /LimitAccess 等待命令执行成功,即可 windowserver 2012版本 server版本的系统相较于上文操作上其实没有太大的差别,不过有一个坑需要踩一下,我们在通过上述俩中方式的实验中可能会报错,这个时候我们查看报错信息,如果报错信息种存在NetFX3ServerFeatures这个英文单词,就是告诉我们在执行上述操作前要先执行一步这个操作 dism /online /Enable-Feature /FeatureName:NetFX3ServerFeatures /Source:L:\sources\sxs /LimitAccess 同样也是修改盘符位置,或使用下载的NetFx3.cab文件,本人还是推荐镜像装载后命令行的形式。 执行完当前命令后重新执行#win7或者win10系统的操作方式,即成功
结构 UML.png 模式的组成 环境类(Context): 定义客户感兴趣的接口。维护一个ConcreteState子类的实例,这个实例定义当前状态。 抽象状态类(State): 定义一个接口以封装与Context的一个特定状态相关的行为。 具体状态类(ConcreteState): 每一子类实现一个与Context的一个状态相关的行为。 本文状态模式的例子使用我们生活中的交通信号灯的例子。交通信号灯一共具有三种状态,红灯,绿灯,黄灯,在这三种状态之间相互切换。如果让我们做一个demo,得到不同的状态时的信号灯颜色,最简单的写法应该是如下 if (light=='红灯') { Console.WriteLine("红灯"); }else if (light=='绿灯') { Console.WriteLine("绿灯"); } else { Console.WriteLine("黄灯"); } 这样的写法通俗易懂,但是同时也存在着很大的问题。所有的 业务逻辑都在上端被定义,如果某一天业务逻辑修改了或者新增了呢,我们就要添加新的if条件,因为逻辑的高度集成,不可避免的bug就有可能触发,这是很不友好的,所以这样的方式实不可取的。 因为这一章节介绍的是状态模式,所以我们采用此方式来进行设计,其他的方式LZ也试过,也可以,不过如果是不同的状态之间带着关联,且不同状态拥有不同行为的,推荐状态模式,废话不多话,开始Lu代码 上面我们说到状态模式的组成分为三个: 环境类(Context) 抽象状态类(State) 具体状态类(ConcreteState) 就算有每一个的解释,我们还是不能很好的理解意思,接下来我用一个通俗的方式来说明。 还是用信号灯的例子来作为参考 抽象状态类,就是信号灯抽象类,里面包含颜色的显示接口 具体状态类,就是三个颜色的信号灯,继承信号灯抽象类,重载颜色显示接口 环境类,就是信号灯,负责切换不同信号灯状态 大同小异,如果我们的对象是电梯,我们是不是也可以 用同样的道理推断出来。接下来让我们对三个组成进行编码 信号灯抽象类 public enum LightColor { Green = 0, Yellow = 1, Red = 2 } /// <summary> /// 灯基类 /// </summary> public abstract class LightBase { public LightColor Color { get; set; } /// <summary> /// 展示灯状态 /// </summary> public abstract void Show(); /// <summary> /// 切换灯颜色 /// </summary> public abstract void Turn(); /// <summary> /// 切换上下文 /// </summary> public abstract void TurnContext(LightContext context); } 具体状态类 三种颜色的信号灯实现 public class GreenLight:LightBase { public override void Show() { Console.WriteLine("绿灯"); } public override void Turn() { this.Color = LightColor.Yellow; } public override void TurnContext(LightContext context) { context.LigthBase = new YellowLight(); } } public class RedLight : LightBase { public override void Show() { Console.WriteLine("红灯"); } public override void Turn() { this.Color = LightColor.Green; } public override void TurnContext(LightContext context) { context.LigthBase = new GreenLight(); } } public class YellowLight : LightBase { public override void Show() { Console.WriteLine("黄灯"); } public override void Turn() { this.Color = LightColor.Red; } public override void TurnContext(LightContext context) { context.LigthBase = new RedLight(); } } 环境类 信号灯控制 /// <summary> /// 灯控制上下文 /// </summary> public class LightContext { public LightBase LigthBase; public LightContext(LightBase lightBase) { this.LigthBase = lightBase; } /// <summary> /// 展示当前灯颜色 /// </summary> public void Show() { this.LigthBase.Show(); } /// <summary> /// 切换到一下个灯 /// </summary> public void Turn() { this.LigthBase.TurnContext(this); } } 上面的代码,其实是对原来的一个简单的修改,将逻辑从三层if中分离出来,不同颜色的类完成自己的功能,实现了业务逻辑的分离。 输出 public class StateShow { public static void Show() { LightBase greeBase = new GreenLight(); LightContext context = new LightContext(greeBase); context.Show(); context.Turn(); context.Show(); context.Turn(); context.Show(); context.Turn(); Console.Read(); } } show.png 重要点解析 抽象基类中重要的一个接口 public abstract void TurnContext(LightContext context); 在具体抽象类中我们可以看到对应的实现,以YellowLight(黄灯状态举例) public override void TurnContext(LightContext context) { context.LigthBase = new RedLight(); } 通过实现接口,我们将传递的上下文进行更改。这么一来大概的意思就出来了,例如我们现在是绿灯状态,类就是GreenLight,对应的行为是Show(),如果我们想要切换到下一个状态,我们只需要将环境上下文进行切换,例如我们切换到红灯状态,那我们就可以操作红灯的行为Show()。 接下去就是环境类的说明 /// <summary> /// 灯控制上下文 /// </summary> public class LightContext { public LightBase LigthBase; public LightContext(LightBase lightBase) { this.LigthBase = lightBase; } /// <summary> /// 展示当前灯颜色 /// </summary> public void Show() { this.LigthBase.Show(); } /// <summary> /// 切换到一下个灯 /// </summary> public void Turn() { this.LigthBase.TurnContext(this); } } 环境类的实现也很简单,我们定义了一个内部的成员变量(LightBase),因为LightBase是所有状态灯的基类,所以我们可以在LightContext上下文内部定义操作LigthBase的方法,也就是Show。 Turn方法的实现可能有些人一下子看不懂,这个实现还是有点意思的。当前LightBase执行TurnContext(参数),参数是他自身。 举一个红灯变绿灯的例子也就能看懂了 LightContext context=new LightContext(new RedLight()); 默认是红灯,我们调用 context.Turn(); 执行的其实是RedLigth 的TurnContext方法 public override void TurnContext(LightContext context) { context.LigthBase = new GreenLight(); } 解析.png
原型模式的结构 原型模式包含以下3个角色: •Prototype(抽象原型类) •ConcretePrototype(具体原型类) •Client(客户类) 浅克隆与深克隆 浅克隆(Shallow Clone):当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制 深克隆(Deep Clone):除了对象本身被复制外,对象所包含的所有成员变量也将被复制 原型核心代码 /// <summary> /// 班级 /// </summary> [Serializable] public class Class { public int Num { get; set; } public string Remark { get; set; } } [Serializable] public class StudentPrototype { public int Id { get; set; } public string Name { get; set; } public Class Class { get; set; } private StudentPrototype() { } private static readonly StudentPrototype _studentPrototype = null; static StudentPrototype() { _studentPrototype = new StudentPrototype() { Id = 0, Name = "复制", Class = new Class() { Num = 100, Remark = "100" } }; } public static StudentPrototype CreateInstanceClone() { StudentPrototype studentPrototype = (StudentPrototype)_studentPrototype.MemberwiseClone(); //这样就是深clone,如果没有这段,Class实例就是浅克隆 studentPrototype.Class = new Class() { Num = 1, Remark = "软谋高级班" }; return studentPrototype; } public static StudentPrototype CreateInstanceSerialize() { //序列化方式创建克隆,需要保证被克隆的对象具有 serialize 特性标签 return SerializeHelper.DeepClone<StudentPrototype>(_studentPrototype); } } 原型模式的理解 原型模式在我的理解中时基于单例模式的一个扩展,在保证实例对象唯一的情况下,能快递new出不同的新实例 对外提供一个接口 CreateCloneInstance创建克隆对象 浅层次克隆 Console.WriteLine("*****浅层次克隆Start**********"); var box1 = StudentPrototype.CreateInstanceClone(); Console.WriteLine($"Id={box1.Id} name={box1.Name}"); var box2 = StudentPrototype.CreateInstanceClone(); box2.Id = 10; box2.Name = "测试"; Console.WriteLine($"Id={box2.Id} name={box2.Name}"); Console.WriteLine("*****浅层次克隆End**********"); 深层次克隆1 失败+成功方法 { //对值类型的克隆修改值,不会影响克隆出的新对象的内容。但是引用类型的克隆有需要注意的点,Class是引用类型的值,具有Num和Remark俩个属性。如果使用下面的写法 box1.Class.Num=***,实际上源对象Class地址和克隆对象地址相同,修改一个相当于全部修改了,并没有实现我们想要的克隆效果 Console.WriteLine("*****深层次克隆 Start**********"); var box1 = StudentPrototype.CreateInstanceClone(); box1.Class.Num = 10; box1.Class.Remark = "备注"; Console.WriteLine($"Id={box1.Id} name={box1.Name} ClassNum={box1.Class.Num } Remark={box1.Class.Remark }"); var box2 = StudentPrototype.CreateInstanceClone(); box2.Id = 10; box2.Name = "测试"; Console.WriteLine($"Id={box2.Id} name={box2.Name} ClassNum={box2.Class.Num } Remark={box2.Class.Remark }"); Console.WriteLine("*****深层次克隆 End**********"); } 深层次克隆2 序列化 { //使用序列化的方式进行克隆原理,因为深层次克隆的主要问题是引用类型克隆时还是相同地址 //我们先将初始对象序列化,在将得到的值进行反序列化,这样便能保证生成的对象存在不同的地址引用 Console.WriteLine("*****深层次克隆2 序列化Start**********"); var box1 = StudentPrototype.CreateInstanceSerialize(); box1.Class.Num = 10; box1.Class.Remark = "备注"; Console.WriteLine($"Id={box1.Id} name={box1.Name} ClassNum={box1.Class.Num } Remark={box1.Class.Remark }"); var box2 = StudentPrototype.CreateInstanceSerialize(); box2.Id = 10; box2.Name = "测试"; Console.WriteLine($"Id={box2.Id} name={box2.Name} ClassNum={box2.Class.Num } Remark={box2.Class.Remark }"); Console.WriteLine("*****深层次克隆2 序列化End**********"); }
一.装饰者模式的定义: 装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。 结构: 装饰器UML.png (1)抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。 (2)具体构件(Concrete Component)角色:定义一个将要接收附加责任的类。 (3)装饰(Decorator)角色:持有一个构件(Component)对象的实例,并实现一个与抽象构件接口一致的接口。 (4)具体装饰(Concrete Decorator)角色:负责给构件对象添加上附加的责任。 抽象构件 /// <summary> /// 手机抽象类,即抽象者模式中的抽象组件类 /// </summary> public abstract class Phone { /// <summary> /// 打印方法 /// </summary> public abstract void Print(); } 具体构件 /// <summary> /// 苹果手机,即装饰着模式中的具体组件类 /// </summary> public class ApplePhone:Phone { /// <summary> /// 重写基类的方法 /// </summary> public override void Print() { Console.WriteLine("我有一部苹果手机"); } } 装饰角色 /// <summary> /// 装饰抽象类,让装饰完全取代抽象组件,所以必须继承Phone /// </summary> public abstract class Decorator:Phone { private Phone p ; //该装饰对象装饰到的Phone组件实体对象 public Decorator(Phone p) { this.p = p; } public override void Print() { if (this.p != null) { p.Print(); } } } 具体装饰 /// <summary> /// 贴膜,具体装饰者 /// </summary> public class Sticker:Decorator { public Sticker(Phone p) : base(p) { } public override void Print() { base.Print(); //添加行为 AddSticker(); } /// <summary> /// 新的行为方法 /// </summary> public void AddSticker() { Console.WriteLine("现在苹果手机有贴膜了"); } } /// <summary> /// 手机挂件,即具体装饰者 /// </summary> public class Accessories:Decorator { public Accessories(Phone p) : base(p) { } public override void Print() { base.Print(); // 添加新的行为 AddAccessories(); } /// <summary> /// 新的行为方法 /// </summary> public void AddAccessories() { Console.WriteLine("现在苹果手机有漂亮的挂件了"); } } 使用 /// <summary> /// 设计模式-装饰者模式 /// </summary> class Program { static void Main(string[] args) { Phone ap = new ApplePhone(); //新买了个苹果手机 Decorator aps = new Sticker(ap); //准备贴膜组件 aps.Print(); Decorator apa = new Accessories(ap); //过了几天新增了挂件组件 apa.Print(); Sticker s = new Sticker(ap); //准备贴膜组件 Accessories a = new Accessories(s);//同时准备挂件 a.Print(); } } 优点: Decorator模式与继承关系的目的都是要扩展对象的功能,但是Decorator可以提供比继承更多的灵活性。 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。 缺点: 这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。 装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。 装饰模式是针对抽象组件(Component)类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。当然也可以改变Component接口,增加新的公开的行为,实现“半透明”的装饰者模式。在实际项目中要做出最佳选择。
通过上文的例子,我们对Selenium的用法有了一个简单的印象。接下去我们还是基于这个组件进行模拟百度贴吧签到的功能。 逻辑分析 1、登陆 2、得到关注的贴吧 3、签到 登陆首页 首页.png 点击登陆按钮 点击登陆png 输入用户名和密码 输入用户名密码.png 检测异常登录,手机认证 手机验证.png 登陆代码说明 public static void Login(string userName, string pwd) { var driver = new ChromeDriver(); driver.Manage().Window.Maximize();//浏览器最大化 driver.Navigate().GoToUrl("https://www.baidu.com"); oldwin = driver.CurrentWindowHandle;//首页签句柄 driver.FindElement(By.XPath("//*[@id=\"u1\"]/a[7]")).Click();//点击登陆 /*隐式等待设置的内容在driver的整个生命周期都有效,所以实际使用过程当中有弊端。*/ driver.Manage().Timeouts().ImplicitWait = new TimeSpan(0, 0, 0, 5); driver.FindElement(By.Id("TANGRAM__PSP_10__footerULoginBtn")).Click();//点击用户名登陆 driver.FindElement(By.Name("userName")).SendKeys(userName);//用户名 driver.FindElement(By.Name("password")).SendKeys(pwd);//密码 driver.FindElement(By.Id("TANGRAM__PSP_10__submit")).Click(); //点击登陆 Thread.Sleep(1000); try { //判断是否存在手机验证码 driver.FindElement(By.Id("TANGRAM__36__button_send_mobile")).Click();//发送手机验证码 string vcode = Console.ReadLine(); driver.FindElement(By.Id("TANGRAM__36__input_vcode")).SendKeys(vcode);//输入6为数字验证码 driver.FindElement(By.Id("TANGRAM__36__button_submit")).Click();//确认 } catch (Exception e) { } } 贴吧签到 代码说明 传入浏览器Driver,通过xpath得到所有关注的贴吧,循环打开贴吧的页面,模拟点击签到按钮 xpath说明.png 签到.png //贴吧一键签到 public static void OnTimeSign(ChromeDriver driver) { driver.FindElement(By.XPath("//*[@id=\"u_sp\"]/a[5]")).Click(); driver.SwitchTo().Window(driver.WindowHandles[1]);//操作权限为第二个页签 var tiebaList = driver.FindElements(By.XPath("//*[@id=\"likeforumwraper\"]/a")); foreach (var tieba in tiebaList) { tieba.Click(); driver.SwitchTo().Window(driver.WindowHandles[2]);//操作权限为第三个页签 driver.FindElement(By.XPath("//*[@id=\"signstar_wrapper\"]/a")).Click(); driver.Close(); driver.SwitchTo().Window(driver.WindowHandles[1]);//操作权限为第二个页签 } driver.Navigate().Refresh(); driver.Close(); driver.SwitchTo().Window(oldwin); } 就是这么简单。是不是相比原来的方式要简单的多,你是如何浏览器操作的,代码就怎么写。 代码直通车 Github FoxCrawler项目下的SeleniumClawer解决方案
代码直通车 Github FoxCrawler项目下的SeleniumClawer解决方案 工具介绍 Selenium:是一个自动化测试工具,封装了很多WebDriver用于跟浏览器内核通讯,我用开发语言来调用它实现PhantomJS的自动化操作。它的下载页面里有很多东西,我们只需要Selenium Client,它支持了很多语言(C#、JAVA、Ruby、Python、NodeJS),按自己所学语言下载即可。 下载地址:http://docs.seleniumhq.org/download/ Nuget 使用 image.png Selenium的好处 Selenuim的好处是显而易见的,当我们爬取网站信息时候,难免会碰到异步加载,数据延时绑定,数据接口定位难,加密信息解码难等问题。其实最终数据都会完整的显示在界面上,既然数据能够显示出来,使用Selenium操控WebDriver进行模拟浏览器行为(点击,切换,移动)等等事件,等待数据显示,然后使用选择器(Id,Class,XPath等)进行爬取,这是一种符合人习惯的编程方式。当然我也不是说其他的方式不好,只是在同等时间的情况下,这种方式效率更高,耗时更快,可靠性也更高。 下面使用Selenium进行一个简单的百度贴吧一键签到功能编码 项目创建,环境配置 打开Vs,新建控制台项目,使用Nuget获取最新Selenium的C#库,然后根据自己机型安装的浏览器选择WebDirver(有点类似运行时打开的模拟浏览器,不过时单独的一个.exe文件,首先你电脑要安装这个浏览器),我以自己的电脑Chrome浏览器为例子,所以我Nuget下载一个 chrome.webdriver.png 下载完成后在项目根目录的packages文件夹中找到对应内容 根据系统类型,系统是32还是64自行选择 路径.png 复制.exe文件到项目Bin文件夹下即可,环境配置完成 先来一下简单例子 在完成一键签到功能之前,我们先来完成一个简单的例子,这样能让大家对这种方式有一个基本的了解 我的例子选取的是某学校的通知公告数据爬取,进行一般爬虫和Selenium爬取的区别 爬取地址http://www.jit.edu.cn/myNews_list_out.aspx?infotype=1 截图.png 普通方式爬取 我们首先要分析如何获取数据,当我们点击下一页的时候,我们发现页面整体刷新,且地址栏没有发生变化,通过分析Respons信息我们发现IIS字样,这样可以推定使用的技术是.net webform 自带的gridview服务端控件,这种方式自带了加密验证,破解的方式网上有,就是要获取每次页面生成的加密码,然后带上其他参数向后台重新发起请求。 缺点: 如果使用这种方式,当我们碰到不同的问题,需要根据不同的问题寻找解决方案,测试可行然后再进行编码,要花多的时间在一个一个没有接触过的问题身上。 Selenium 模拟爬取 这种方式就相对简单,也很好理解。编码的逻辑就是如下 1、打开网页 2、找到下一页按钮 3、模拟点击 4、数据获取 这样的方式就和我们使用浏览器操作习惯一置,逻辑也更加清楚。 接下去我就基于这一种方法,对代码进行说明 打开网页 var docHtml = new HtmlDocument(); var driver = new ChromeDriver(); driver.Navigate().GoToUrl("http://www.jit.edu.cn/myNews_list_out.aspx?infotype=1"); 业务逻辑 代码简单明了,爬取当页数据,然后找到下一页按扭,如果存在点击,如何不存在,退出循环 bool nextpage = true; do { ReadOnlyCollection<IWebElement> newsNodes = driver.FindElements(By.XPath("//*[@id=\"table_list\"]/li/a")); //获取li内容 GetNewList(newsNodes);//获得新闻内容 docHtml.LoadHtml(driver.PageSource); //找到下一页按钮 HtmlNode node = docHtml.GetElementbyId("nextpage"); IWebElement element = null; if (node != null) { element = driver.FindElementById("nextpage"); } else { nextpage = false; } //如果存在下一页按钮,模拟点击 if (nextpage) { element.Click(); } } while (nextpage); 获取新闻内容 private static List<NewInfo> GetNewList(ReadOnlyCollection<IWebElement> newsNodes) { List<NewInfo> newInfoList = new List<NewInfo>(); foreach (var news in newsNodes) { newInfoList.Add(new NewInfo() { Url = news.GetAttribute("href"), Title = news.Text }); Console.WriteLine($"{news.Text} {news.GetAttribute("href")}"); } return newInfoList; } 好处: 1、代码简单明了 2、逻辑清晰 3、后期维护方便
一 概述 定义:适配器模式将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper)。 属于结构型模式 主要分为三类:类适配器模式、对象的适配器模式、接口的适配器模式。 本文定义: 需要被适配的类、接口、对象(我们有的),简称 src(source) 最终需要的输出(我们想要的),简称 dst (destination,即Target) 适配器称之为 Adapter 。 一句话描述适配器模式的感觉: src->Adapter->dst,即src以某种形式(三种形式分别对应三种适配器模式)给到Adapter里,最终转化成了dst。 使用场景: 1 系统需要使用现有的类,而这些类的接口不符合系统的需要。 2 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。 3 需要一个统一的输出接口,而输入端的类型不可预知。 二 类适配器模式 一句话描述:Adapter类,通过继承 src类,实现 dst 类接口,完成src->dst的适配。 我们通过一个最常用的例子来说明 生活中最常见的就是充电器,220V的交流电被转换成5V的直流电,充电器在其中就担当了适配器的功能 既然我们是进行适配,那就说明是基于某一个功能点的实现,所以我们先定义一个输出220v交流电的功能,这个功能如下 /// <summary> /// 220V电压 /// </summary> public class Voltage220 { public int output220v() { int src = 220; Console.WriteLine("我是" + src + "伏电压"); return src; } } 定义一个接口,接口的方法签名是转换(220V-5V) /// <summary> /// 客户需要的5V电压接口 /// </summary> public interface Voltage5 { /// <summary> /// 输出5V /// </summary> /// <returns></returns> int output5v(); } 接下去我们开始定义适配器类 !!!注意点 1、继承后来的类 2、实现适配器接口 /// <summary> /// 类适配器 /// </summary> public class VoltageAdapter : Voltage220, Voltage5 { public int output5v() { int src = new Voltage220().output220v(); Console.WriteLine("适配器工作开始适配电压"); int dst = src / 44; Console.WriteLine("适配完成后输出电压:" + dst); return dst; } } 此时我们需要一个设备,使用适配器查看功能是否正确 定义一个手机类,实现转换功能Mobile.charging(适配器 box1) 实例化手机,调用充电方法,形参是适配器的实例 /// <summary> /// 手机类 /// </summary> public class Mobile { /// <summary> /// 充电方法 /// </summary> /// <param name="voltage5"></param> public void charging(Voltage5 voltage5) { if (voltage5.output5v() == 5) { Console.WriteLine("电压刚刚好5V,开始充电"); } else if (voltage5.output5v() > 5) { Console.WriteLine("电压超过5V,都闪开 我要变成note7了"); } } } 功能实现 /// <summary> /// 类适配器 /// </summary> public static void ClassStart() { Console.WriteLine("===============类适配器=============="); Mobile mobile = new Mobile(); mobile.charging(new VoltageAdapter()); } 三 对象适配器 基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承src类,而是持有src类的实例,以解决兼容性的问题。 即:持有 src类,实现 dst 类接口,完成src->dst的适配。 (根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。) /// <summary> /// 对象适配器 /// </summary> public class VoltageAdapter2 : Voltage5 { private Voltage220 voltage220; public VoltageAdapter2(Voltage220 voltage220) { this.voltage220 = voltage220; } public int output5v() { int dst = 0; if (null != voltage220) { int src = voltage220.output220v(); Console.WriteLine("对象适配器工作,开始适配电压"); dst = src / 44; Console.WriteLine("适配完成后输出电压:" + dst); } return dst; } } 功能实现 /// <summary> /// 对象适配器 /// </summary> public static void ObjectStart() { Console.WriteLine("===============对象适配器=============="); VoltageAdapter2 voltageAdapter2=new VoltageAdapter2(new Voltage220()); Mobile mobile = new Mobile(); mobile.charging(voltageAdapter2); } 小结: 对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。 根据合成复用原则,组合大于继承, 所以它解决了类适配器必须继承src的局限性问题,也不再强求dst必须是接口。 同样的它使用成本更低,更灵活
1、简单工厂 2、工厂方法 3、抽象工厂 简单工厂和工厂方法这俩个设计模式不做详细介绍,请各位看官自行百度,有大量的解释。再次我简单引用一下其他博客主对这三种模式的理解。 引言 简单工厂、工厂方法、抽象工厂都属于设计模式中的创建型模式。其主要功能都是帮助我们把对象的实例化部分抽象取了出来,优化了系统的架构,并且增强了系统的扩展性。此篇博客是笔者对学完这三种模式之后一个小小的总结 简单工厂 简单工厂模式的工厂类一般是使用静态方法,通过接收的参数不同来返回不同的对象实例。不修改代码的话,是无法扩展的 优点:客户端可以免除直接创建产品对象的责任,而仅仅是“消费”产品。简单工厂模式通过这种做法实现了对责任的分割 缺点:由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了 工厂方法 工厂方法是针对每一种产品提供一个工厂类。通过不同的工厂实例来创建不同的产品实例。在同一等级结构中,支持增加任意产品 优点:允许系统在不修改具体工厂角色的情况下引进新产品 缺点:由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量 抽象工厂 抽象工厂是应对产品族概念的。应对产品族概念而生,增加新的产品线很容易,但是无法增加新的产品。比如,每个汽车公司可能要同时生产轿车、货车、客车,那么每一个工厂都要有创建轿车、货车和客车的方法 优点:向客户端提供一个接口,使得客户端在不必指定产品具体类型的情况下,创建多个产品族中的产品对象 缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性 以上是对三种方法的介绍和优缺点描述,接下来我们使用一个实例来说明抽象工厂的创建 设计说明 案例如下,手机厂商拥有屏幕和主板这俩条生产线,不同的手机厂商生产规格不同的产品(小米公司和苹果公司) 我们进行分析,俩家手机厂商,俩条生产线生产俩种设备 1 定义如下,首先我们定义一个抽象的工厂,这个工厂的方法包含 1、创建屏幕 2、创建主板 这个抽象工厂的作用主要是让继承他的子类 实现这俩个抽象方法 /// <summary> /// 抽象工厂类:提供创建不同品牌的手机屏幕和手机主板 /// </summary> public abstract class AbstractFactory { //工厂生产屏幕 public abstract Screen CreateScreen(); //工厂生产主板 public abstract MotherBoard CreateMotherBoard(); } 2 定义手机屏幕和手机主板的的抽象方法,因为不同的厂商对产品规格不同,所以定义好抽象的基类,再交由各不同的厂商去实现功能,实现如相 /// <summary> /// 屏幕抽象类:提供每一品牌的屏幕的继承 /// </summary> public abstract class Screen { public abstract void print(); } /// <summary> /// 主板抽象类:提供每一品牌的主板的继承 /// </summary> public abstract class MotherBoard { public abstract void print(); } 不同厂商对屏幕和主板的实现类如下: /// <summary> /// 苹果手机屏幕 /// </summary> public class AppleScreen:Screen { public override void print() { Console.WriteLine("苹果手机屏幕!"); } } /// <summary> /// 苹果手机主板 /// </summary> public class AppleMotherBoard:MotherBoard { public override void print() { Console.WriteLine("苹果手机主板!"); } } /// <summary> /// 小米手机屏幕 /// </summary> public class XiaoMiScreen:Screen { public override void print() { Console.WriteLine("小米手机屏幕!"); } } /// <summary> /// 小米手机主板类 /// </summary> public class XiaoMiMotherBoard :MotherBoard { public override void print() { Console.WriteLine("小米手机主板!"); } 3 经过上面俩个步骤我们得到了不同商品的实现,现在我们还缺少一个东西,就是不同的商家对自己商品的获取实现 此时我们应该建立不同商家的工厂类,有俩个商家,我们就建立俩个工厂类,工厂中分别实现创建自己产品 实现的工厂要继承AbstractFactory /// 小米手机工厂类 /// </summary> public class XiaoMiFactory : AbstractFactory { /// <summary> /// 生产小米手机屏幕 /// </summary> /// <returns></returns> public override Screen CreateScreen() { return new XiaoMiScreen(); } /// <summary> /// 生产小米手机主板 /// </summary> /// <returns></returns> public override MotherBoard CreateMotherBoard() { return new XiaoMiMotherBoard(); } } /// <summary> /// 苹果手机工厂 /// </summary> public class AppleFactory : AbstractFactory { /// <summary> /// 生产苹果手机屏幕 /// </summary> /// <returns></returns> public override Screen CreateScreen() { return new AppleScreen(); } /// <summary> /// 生产苹果手机主板 /// </summary> /// <returns></returns> public override MotherBoard CreateMotherBoard() { return new AppleMotherBoard(); } } 4 内容创建完成,接下去我们就来进行结果的输出 //小米工厂生产小米手机的屏幕和主板 AbstractFactory xiaomiFactory = new XiaoMiFactory(); Screen xiaomiScreen = xiaomiFactory.CreateScreen(); xiaomiScreen.print(); MotherBoard xiaomiMotherBoard = xiaomiFactory.CreateMotherBoard(); xiaomiMotherBoard.print(); //苹果工厂生产苹果手机屏幕和主板 AbstractFactory appleFactory = new AppleFactory(); Screen appleScreen = appleFactory.CreateScreen(); appleScreen.print(); MotherBoard appleMotherBoard = appleFactory.CreateMotherBoard(); appleMotherBoard.print(); 理解: 不同的商家拥有自己的工厂创建自己的产品 1、首先实例化商家工厂 2、创建不同的商品 3、调用商品功能
模式定义 桥接模式即将抽象部分与它的实现部分分离开来,使他们都可以独立变化。 桥接模式将继承关系转化成关联关系,它降低了类与类之间的耦合度,减少了系统中类的数量,也减少了代码量。 将抽象部分与他的实现部分分离这句话不是很好理解,其实这并不是将抽象类与他的派生类分离,而是抽象类和它的派生类用来实现自己的对象。这样还是不能理解的话。我们就先来认清什么是抽象化,什么是实现化,什么是脱耦。 抽象化:其概念是将复杂物体的一个或几个特性抽出去而只注意其他特性的行动或过程。在面向对象就是将对象共同的性质抽取出去而形成类的过程。 实现化:针对抽象化给出的具体实现。它和抽象化是一个互逆的过程,实现化是对抽象化事物的进一步具体化。 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。 对于那句话:将抽象部分与他的实现部分分离套用《大话设计模式》里面的就是实现系统可能有多个角度分类,每一种角度都可能变化,那么把这种多角度分类给分离出来让他们独立变化,减少他们之间耦合。 桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。 模式结构 桥接模式主要包含如下几个角色: Abstraction:抽象类。 RefinedAbstraction:扩充抽象类。 Implementor:实现类接口。 ConcreteImplementor:具体实现类 。 UML图如下 桥接模式UML.png 案例说明 我们通过一个简单的案例来说明桥接模式的用法 一切的对象都是基于现实生活的抽象,那我们以不同颜色不同形状的物理举例。假设我们现在拥有红,绿,蓝三种颜色,拥有圆形,正方形和菱形三种物体,我们希望得到不同物体的不同颜色的打印。 方法1: 定义实体类: 颜色三种,物品三种。生成3*3个实体类=9 这种方法的缺点在于,如果我在添加一个颜色,或者添加一个物品,那个对应的实体类个数就要增加,这样的方法一定是不适用的,太过笨。 方法2: 采用颜色和物品组合的方法实现功能 我们打印出物品的颜色,那我们首先要知道是什么物品,物品是什么颜色。 基于这俩个问题我们其实可以得到一个方法 物品.打印(颜色) 基于桥接模式角色定义我们可以一一匹配 物品是抽象类的实现 颜色是接口 1、我们首先需要一个物品的基类,让所有的物品实现这个基类,调用基类抽象方法打印物品。 2、定义颜色接口,定义颜色类实现接口方法,接口定义的方法就是返回当前的颜色 3、物品调用抽象应该有一个形参(IColor),方便在打印物品的时候添加颜色说明 案例编码 形状 /// <summary> /// 形状抽象基类 /// </summary> public abstract class Shape { public abstract void Draw(IColor color); } /// <summary> /// 圆形类 /// </summary> public class Cycle : Shape { public override void Draw(IColor color) { Console.WriteLine(color.GetColor() + "圆形"); } } /// <summary> /// 正方形类 /// </summary> public class Square : Shape { public override void Draw(IColor color) { Console.WriteLine(color.GetColor() + "正方形"); } } 颜色 /// <summary> /// 颜色接口 /// </summary> public interface IColor { string GetColor(); } public class WhiteColor : IColor { public string GetColor() { return "白色"; } } public class BlankColor : IColor { public string GetColor() { return "黑色"; } } 调用 public class ShapeColor { public static void PrintShapeColor() { Shape shape = new Cycle();//实例化形状类 IColor color = new WhiteColor();//实例化颜色类 shape.Draw(color);//形状调用颜色 //实现耦合,使用组合模式而不是继承模式 } }
废话不多说,我们直接来分析源码,首先我们查看目录结构 目录结构.png 目录结构功能 Extend 通用扩展方法 Parameter 公共实体类 Parser 解析器 Validate 验证工具集 目录结构展开.png 展开目录结构,我们能够更加请详细的分析出每个目录所完成的功能模块。 这里主要讲解工具集中最重要的一个模块Validate 要设计,我们就一定要知道自己想怎么做。 如果我对外提供接口调用,怎么样的方式是最方便,让人容易理解的,我就是朝着这个方向做的。 我希望的结果是 实例化验证对象,参数是验证文件的路径 调用验证方法,可以区分工作表验证,可以选择添加或不添加逻辑验证 验证成功或失败都返回一个对象,如果验证失败,返回的对象中要包含出错的信息(尽可能细化) 基于上述的设计理念 我定义了三个对象 RowValidate 行验证 WorkSheetValidate 工作表验证 WorkBookValidate 工作簿验证 RowValidate 行验证 RowValidate对象执行的调用方是WorkSheetValidate Validate<T>执行返回值为 得到当前行的的出错信息集合 /// <summary> /// 行验证 /// </summary> public class RowValidate { public static string GetCellStation(int rowIndex, int columnIndex) { int i = columnIndex % 26; string cellRef = Convert.ToChar(65 + i).ToString() + (rowIndex + 1); return cellRef; } public static List<ErrCell> Validate<T>(int rowIndex, List<string> colNames, List<int> colIndexs, List<string> rowCellValues) { List<ErrCell> errCells = new List<ErrCell>(); T singleT = Activator.CreateInstance<T>(); foreach (PropertyInfo pi in singleT.GetType().GetProperties()) { var propertyAttribute = (Attribute.GetCustomAttribute(pi, typeof(ExcelColumnAttribute))); if (propertyAttribute == null) { continue; } var proName = ((ExcelColumnAttribute)propertyAttribute).ColumnName; for (int colIndex = 0; colIndex < colNames.Count; colIndex++) { try { if (proName.Equals(colNames[colIndex], StringComparison.OrdinalIgnoreCase)) { string fieldName = pi.PropertyType.GetUnderlyingType().Name; string cellValue = rowCellValues[colIndex]; if (!String.IsNullOrWhiteSpace(cellValue)) { //如果是日期类型,特殊判断 if (fieldName.Equals("DateTime")) { string data = ""; try { data = cellValue.ToDateTimeValue(); } catch (Exception) { data = DateTime.Parse(cellValue).ToString(); } } cellValue.CastTo(pi.PropertyType); } } } catch (Exception ex) { errCells.Add(new ErrCell() { RowIndex = rowIndex, ColumnIndex = colIndexs[colIndex], Name = GetCellStation(rowIndex, colIndexs[colIndex]), ErrMsg = ex.Message }); } } } return errCells; } } WorkBookValidate 工作簿验证 WorkBookValidate是根的验证对象。我们首先看构造函数,参数为filePath,在构造函数中,我们做的操作是:实例化N个WorkSheetValidate对象。 定义索引器,这样可以通过外部调用WorkSheetValidate的验证方法 /// <summary> /// 工作簿验证 /// </summary> public class WorkBookValidate { public string FilePath { get; set; } private List<WorkSheetValidate> _workSheetList = new List<WorkSheetValidate>(); public List<WorkSheetValidate> WorkSheetList { get { return _workSheetList; } set { _workSheetList = value; } } public WorkSheetValidate this[string sheetName] { get { foreach (WorkSheetValidate sheetParameterContainer in _workSheetList) { if (sheetParameterContainer.SheetName.Equals(sheetName)) { return sheetParameterContainer; } } throw new Exception("工作表不存在"); } } public WorkSheetValidate this[int sheetIndex] { get { foreach (WorkSheetValidate sheetParameterContainer in _workSheetList) { if (sheetParameterContainer.SheetIndex.Equals(sheetIndex)) { return sheetParameterContainer; } } throw new Exception("工作表不存在"); } } /// <summary> /// /// </summary> /// <param name="filePath">路径</param> public WorkBookValidate(string filePath) { FilePath = filePath; var excel = new ExcelQueryFactory(filePath); List<string> worksheetNames = excel.GetWorksheetNames().ToList(); int sheetIndex = 0; foreach (var sheetName in worksheetNames) { WorkSheetList.Add(new WorkSheetValidate(filePath, sheetName, sheetIndex++)); } } } WorkSheetValidate 工作表验证 这是这三个验证模块中最复杂的一个,代码就不贴全部的图了,主要讲解一下重要的地方。 首先也是构造函数,这个构造函数主要是给WorkBookVaidate调用 public WorkSheetValidate(string filePath, string sheetName, int sheetIndex) { FilePath = filePath; SheetName = sheetName; SheetIndex = sheetIndex; TootarIndex = 0; } 验证方法说明 这是一个泛型方法,方法逻辑很简单 首先验证数据有效性 ValidateParameter 如果返回的错误集合为空,验证逻辑有效性ValidateMatching 最后返回验证集合 public Verification StartValidate<T>(List<CellMatching<T>> rowValidates = null) { List<ErrCell> errCells = this.ValidateParameter<T>(TootarIndex); if (!errCells.Any()) { TootarIndex += 1; errCells.AddRange(this.ValidateMatching<T>(rowValidates, TootarIndex)); } Verification validate = new Verification(); if (errCells.Any()) { validate = new Verification() { IfPass = false, ErrCells = errCells }; } else { validate = new Verification() { IfPass = true, ErrCells = errCells }; } return validate; } 验证数据有效性 这个模块相对复杂,看不懂的小伙伴可以多看几遍理解消化吸收下。 首先调用LinqToExcel的WorksheetNoHeader方法获得除了标题的集合数据 然后得到当前标题行和Excel列的映射关系 调用GetErrCellByParameter方法进行验证 GetErrCellByParameter说明 得到所有列名称集合,得到所有列名称索引 遍历行数据,调用RowValidate的静态方法RowValidate.Validate<T> 传递的参数是,行索引,列名称集合,列索引集合,行数据集合 private List<ErrCell> GetErrCellByParameter<T>(List<RowNoHeader> rows, int startRowIndex) { List<string> colNames = _propertyCollection.Values.Select(u => u.ColName).ToList(); List<int> colIndexs = _propertyCollection.Values.Select(u => u.ColIndex).ToList(); List<ErrCell> errCells = new List<ErrCell>(); for (int rowIndex = startRowIndex; rowIndex < rows.Count; rowIndex++) { List<string> rowValues = rows[rowIndex].Where((u, index) => colIndexs.Any(p => p == index)).Select(u => u.ToString()).ToList(); errCells.AddRange(RowValidate.Validate<T>(rowIndex, colNames, colIndexs, rowValues)); } return errCells; } private List<ErrCell> ValidateParameter<T>(int startRowIndex) { //第一步 得到集合 var excel = new ExcelQueryFactory(FilePath); var rows = (from c in excel.WorksheetNoHeader(SheetIndex) select c).ToList(); //第二步 获得标题行和Excel列的映射关系 方法体省略 //第二步 调用验证方法 return GetErrCellByParameter<T>(rows, startRowIndex); }
接着上文的内容继续讲,上文中我提到了对Excel操作帮助类库LinqToExcel类库的优缺点和使用方法。我也讲到了自己在使用中碰到的问题,我也开发了一个简单的类库解决,下面就讲解一下这个帮助类。 Github链接 LinqToExcel.Extend LinqToExcel没有包含验证,在无法保证客户提供的Excel数据规范的前提下容易发生错误。所以对其进行扩展,扩展的内容主要就是在实体转换前对数据进行验证。 经过网络的简单学习,我总结验证包含俩点 1.默认验证参数有效性 2.自定是逻辑有效性 数据有效性 数据有效性主要指的是Excel单元格字段类型是否和定义的实体类属性字段一致,如果不一致需要提示。例如定义了一个字段Datetime,但是传进来的内容是2018年9月33日。这种不符合要求的数据就是有问题的值。 逻辑有效性 逻辑有效性这个就更加好理解了,Excel的单元格只允许出现 是 否 这俩值,可是用户却填写了不是这样的值,后来在验证的时候就要过滤并给出错误提示 我的扩展主要就是基于这俩个内容,大概的代码贴图如下。 如果要验证Excel的数据是否满足条件我们New一个WorkBookValidate对象,参数是Excel路径。 然后我们执行方法,如果我们想要验证第一个工作表是否符合User实体类的要求,代码就是这样workbook[0].StartValidate<User>(); //自定义工作簿验证 WorkBookValidate workbook = new WorkBookValidate("Default.xlsx"); //验证结束返回Verification对象,对象包含 俩个属性,一个为是否验证成功,一个为验证出错的集合信息 workbook[0].StartValidate<User>(); 需要验证的实体,这里的内容和LinqToExcel定义不变,还是使用相同的特性标签。 public class User { [ExcelColumn("Id")] public string ID { get; set; } [ExcelColumn("名称")] public string Name { get; set; } [ExcelColumn("年龄")] public int Age { get; set; } [ExcelColumn("出生日期")] public DateTime BirthDay { get; set; } public override string ToString() { return string.Format("{0}\t{1}\t{2}", ID, Name, Age); } } 这里主要是对各种事例的说明,主要分三个方法讲解 对出错对象的输出,是下面例子的通用方法 private static void Validate(Verification verifity) { Console.WriteLine(string.Format("是否通过{0}", verifity.IfPass)); //出错列 foreach (var item in verifity.ErrCells) { Console.WriteLine(item); } } BasicValidate 首先第一个方法BasicValidate,很标准的一个Action,相信大家能够看懂,实例化对象,执行方法,打印错误字段 /// <summary> /// 基础验证 通过实体特性 /// </summary> public static void BasicValidate() { WorkBookValidate workbook = new WorkBookValidate("Default.xlsx"); var errlist = workbook[0].StartValidate<User>(); Validate(errlist); } BasicValidate2 第二个方法BasicValidate2,和第一个方法类似,只是多了一个实体映射的方法,这个方法和LinqToExcel的方法使用类似,如果实体类未定义映射的特性标签,那么就通过如下方式自定义设置 /// <summary> /// 基础验证 自定义对应关系 /// </summary> public static void BasicValidate2() { WorkBookValidate workbook = new WorkBookValidate("Other.xlsx"); workbook[0].AddMapping("ID", "主键"); var errlist = workbook[0].StartValidate<User>(); Validate(errlist); } ValidateWithCondition 第三个方法ValidateWithCondition主要是增加了逻辑有效性验证,调用上述俩个方法,就相当于执行的是数据有效性验证,如果要增加逻辑有效性验证,只需要多添加一个参数就可以,参数如下所示 首先是验证的字段 然后是验证条件 最后是出错的提示信息 /// <summary> /// 带条件验证 /// </summary> public static void ValidateWithCondition() { WorkBookValidate workbook = new WorkBookValidate("Default.xlsx"); List<CellMatching<User>> rowValidate = new List<CellMatching<User>>(); rowValidate.Add(new CellMatching<User>() { paramater = u => u.Age, matchCondition = u => u.Age > 10, errMsg = "请选择年龄大于10的人员" }); var errlist = workbook[0].StartValidate<User>(rowValidate); Validate(errlist); } 总结 以上主要是对API使用的一些说明,大家可以自行下载代码。希望大家一起提出好的意见,一起优化,一起进步,一起学习。
接下去我们进行索引建立,本项目索引建立我们使用Lucene.Net。在使用前我们介绍以下Lucene是什么! Lucene概述 Lucene是一款高性能的、可扩展的信息检索(IR)工具库。信息检索是指文档搜索、文档内信息搜索或者文档相关的元数据搜索等操作。 索引过程: ①获取内容 ②建立文档 获取原始内容后,就需要对这些内容进行索引,必须首先将这些内容转换成部件(通常称为文档),以供搜索引擎使用。文档主要包括几个带值的域,比如标题、正文、摘要、作者和链接。 ③文档分析 搜索引擎不能直接对文本进行索引:确切地说,必须将文本分割成一系列被称为语汇单元的独立的原子元素。每一个语汇单元大致与语言中的“单词”对应起来。 ④文档索引 在索引步骤中,文档被加入到索引列表。 Lucene 的参考链接,想多了解的小伙伴可以点击借助 Lucene.Net 构建站内搜索引擎使用Lucene.Net实现全文检索Lucene.Net+盘古分词器(详细介绍) 在阅读上述内容和文章链接后,相信大家对Lucene是什么有了一定的了解。那么我们再来说说分词,分词我们简单理解是这样的 “今天是个好日子”通过分词中间件,我们能够得到一个集合,集合内容为["今天","是","一个","好日子"]这样的内容,相当于把内容分解成了我们日常理解的词汇。中文分词现在有很多种 庖丁解牛,盘古分词,结巴分词,IK分词等等,大家可以通过百度对分词组件进行了解,这里也不做多的说明。 本项目选用的分词组件是 盘古分词,采用Lucene.Net建立索引 索引建立是基于当前已经存在的20张表 image.png 我们是对这些数据建立索引,那我们如何高效的建立索引,当然也是多线程啦! 思路说明 遍历20张表 第一步、得到每张表的个数集合 Dictionary[表名,分页总数] 我们的分页以1000作为基数,即一次取1000条数据 _commodityService.GetTableCount(i) 得到当前表格的个数 (i是商品表索引号) PageHelper.GetPageNum(A,B) A是集合,B是个数,得到的集合是对当前表的数据每次取1000个,需要分多少页 public static int GetPageNum(int allCount, int pageSize) { int PageNum = 0;//任务分页个数 if (allCount % pageSize == 0) { PageNum = allCount / pageSize; } else { PageNum = allCount / pageSize + 1; } return PageNum; } //分页以1000作为基数 //Dictionary[表名,分页总数] Dictionary<int, int> tableCountDictionary = new Dictionary<int, int>(); //StaticConst.CategorySheetCount=20 for (int i = 0; i < StaticConst.CategorySheetCount; i++) { int pageNum = PageHelper.GetPageNum(_commodityService.GetTableCount(i), StaticConst.PageGetCount); ; tableCountDictionary.Add(i, pageNum); } 第二步、得到[表索引,页码]集合 对第一步的Dic字典循环,我们又得到一个新的集合列表,列表的内容是【表索引,分页索引】的集合 集合的例子是:[{0,1},{0,2},{0,3}] 解释,第一张表第一页,第一张表第二页,第一张表第三页这样的集合 public class TableIndexModel { /// <summary> /// 表索引 /// </summary> public int TableIndex { get; set; } /// <summary> /// 分页索引 /// </summary> public int PageIndex { get; set; } } //得到[表索引,页码]集合 List<TableIndexModel> timList = new List<TableIndexModel>(); foreach (var tcd in tableCountDictionary) { for (int i = 1; i <= tcd.Value; i++) { timList.Add(new TableIndexModel() { TableIndex = tcd.Key, PageIndex = i }); } } 第三步、定义每一个线程需要完成的内容 根据第二步骤,我们得到了一个[表索引,页码]的集合,接下去我们开始分配每个线程要完成的任务量 如果我们集合个数是3000,我们对其3000进行分页,最好是将页数定义的多一点,这样每个集合处理的任务量少,耗时少。 执行完上述代码后我们可以得到一个List<[表索引,页码]>的集合,这个集合就是我们最终得到的集合。这个集合的好处如果大家看得懂代码,能够体会到好处,那就是任务分配均匀,每个集合要处理的任务数都是相同的,这样多线程处理的时候就不会有快慢之分。能够快速切换任务和节约执行时间。 /*平均分配任务的业务逻辑为: 每个线程需要处理多少任务=timList.Count 总个数 /分页数 */ int workPageNum = PageHelper.GetPageNum(timList.Count, threadCount); //得到[平均分配后的任务列表] List<List<TableIndexModel>> taskDataList = new List<List<TableIndexModel>>(); for (int i = 1; i <= workPageNum; i++) { var list = timList.Skip((i - 1) * threadCount).Take(threadCount).ToList(); taskDataList.Add(list); } 第四步、多线程处理 如下代码因为是部分贴图,所以可能理解起来较为困难,如果有问题的观众可以直接下载github上面的源码,自己调试看看效果,有问题也可以email给我。 代码的大概意思是,开启20个线程,20个线程处理第三步得到的集合。 得到一个随机编码,这是索引存储的Lucene文件夹名称,判断编码是否存在,如果不存在加入编码list集合 对当前集合建立索引 将当前任务加入List<Task>集合,判断任务集合是否超出20上限,如果超出,等待集合中任务完成。这一步是用来手动限定20个线程数量的。 在所有任务结束后,对当前的编码集合进行索引合并。在索引建立时,如果存在错误,即认定索引建立失败,结束所有的任务 List<Task> taskList = new List<Task>(); var threadNums = Enumerable.Range(1, StaticConst.CategorySheetCount).ToList(); Random random = new Random(); int index = 0, alltakCount = taskDataList.Count; foreach (var taskData in taskDataList) { index++; var threadCode = CommodityDAL.GetTName(random.Next(1, threadNums.Count)); Task task = taskFactory.StartNew(() => { try { LuceneBulid luceneBuild = new LuceneBulid(); if (!PathSuffixList.Any(u => u == threadCode)) { PathSuffixList.Add(threadCode); } //建立索引 luceneBuild.BuildIndex(taskData, threadCode, true); } catch (Exception ex) { Console.WriteLine($"BuildIndexError\t{ex.Message}"); CTS.Cancel(); } }, CTS.Token); taskList.Add(task); if (taskList.Count > 20) { taskList = taskList.Where(t => !t.IsCompleted && !t.IsCanceled && !t.IsFaulted).ToList(); Task.WaitAny(taskList.ToArray()); } } taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), MergeAllLuceneIndex)); private void MergeAllLuceneIndex(Task[] obj) { try { ILuceneBulid builder = new LuceneBulid(); builder.MergeAllLuceneIndex(PathSuffixList.ToArray()); OnTaskComplate(new EventArgs());//任务完成触发事件 } catch (Exception ex) { StringValueEventArgs e = new StringValueEventArgs() { Value = ex.Message }; OnTaskError(e);//每完成一个任务触发事件 Console.WriteLine($"MergeAllLuceneIndex\t{ex.Message}{ex}"); } }
接下去进入代码的模块,首先我们分析一下如何实现代码 我们拥有一张类别表,类别表中记录了 类别名称,编码,地址,该类别所拥有的页数等信息 类别表.png 抽象描述: 取出所有的类别 循环类别集合 循环类别页数 得到当前类别当前页集合数据 插入数据库 对上述内容我们又几个地方需要注意,我们依次说明 第一点 数量量大 类别大致有4000多个,每个类别页数各不相同(一页60个商品是固定的),这么多的商品数据,如果我们还是采用同步方法依次执行的话,效率势必大打折扣,爬取所需花费的时间也很长。这个时候我们就要考虑多线程执行。 作者的思路是这样的: 不管数据量有多少,固定线程数量20个,即我只开20个线程处理任务。每个线程处理一个类别的工作任务。因为有的类别页数有100个,有的类别页数只有10个,这一块如何继续平均分配,不做考虑,各位可以开动大家的脑筋。 第二点 数据库如何存储 如此多的数据,我们应该如何存储。我们爬取的是相同的商品数据,只是内容不同。所以很自然的我们想到了分表。我们既然分表了,那么势必涉及到以后的查询,查询我们以后使用的是Lucene,自己建立一个简答的搜索引擎。在此基础之上,我们在表设计的时候就没有太大的约束。以下是我的商品表的设计图 商品数据表.png Id 主键自增 SUID 商品唯一码 CategoryId 商品类别 Titile 商品名称 Description 商品描述 Price 价格 Url 地址 ShopName 店铺名称 商品编码来源于每一个li标签的id【如下图】,我们可以看到这是一个数字编码。我假设这个编码是一个自增的数字,那么我就可以使用百分取余的方法确定这个商品应该放在哪个表中。这是什么意思呢。 我们假设自己有20张表,每个表的数据结构都如上述描述的那样,那我们要解决的问题就是数据应该如何存储的问题。 自增数字的取余意思就是。如果当前编号是30001 30001 % 20=1 存放在商品表1中 Commodity_01 40871 % 20=11 存放在商品表11中 Commodity_11 这样做的好处是什么呢,因为商品如果是自增累加的。通过此方法可以平均分配每张表的数据,不会让某张表数据多,而某张表数据少 image.png 第三点 数据插入如何操作 我们可以从上文中了解到,在得到当前也数据后,我们要将其集合(60个)插入数据库,选用何种方式可以保证事务基础上又减少数据库链接是要考虑的问题。 以下是我的思路,贴代码讲解 代码为剪贴版,要看全部代码可以去我的github上面下载最新的源码 //取值 List<POCO_Commodity> cateList = CommodityAnalysis.GetData(category.Url, category.Id, i); //处理 List<CommodityGroupInput> groupList = cateList.GroupBy(u => Convert.ToInt64(u.SUId) % StaticConst.CategorySheetCount).Select(u => new CommodityGroupInput { Id = u.Key, Units = u.OrderBy(p => p.SUId).ToList() }).ToList(); //入库 _commodityService.InsertGroupBulk(groupList); 第一步取值,参数为 地址,类别码,页码 第二步是对List集合的分组,分组条件为唯一码%20,得到的内容为 [表索引号,对应的集合] 第三步,入库,代码贴图如下 入库代码.png 入库数据库操作我通过dapper实现,不知道dapper是什么的,可以自行百度。代码的解释如下,使用dapper数据库链接,开启事务,对形参集合进行SQL语句拼接。
GitHub: LinqToExcel 以前项目中对Excel进行信息读取,我都是使用的NPOI的封装类,给定一个fileurl,然后返回给我一个datatable。接下去自己去解析数据。如果使用这种方式,那开发者就还要有点小痛苦,因为我们还要在此基础上自己做一些处理,才可以得到我们想要的数据,例如:行列匹配,定义一个实体,一行一行取值,一列一列赋值,这样的操作没有意义,而且机械。突然有一天我在博客园上看到了一个Excel操作库,LinqtoExcel,然后我看了一下操作方式和内容,突然觉得阳光普照大地,眼前一片光明。 下面我简单介绍以下LinqtoExcel的优点和缺点 优点 兼容 以往我们通过NPOI操作数据库的时候,.xlsx和.xls是需要区分处理的。而Linqtoexcel则没有这个问题,作者已经封装好了。一个方法,操作任一后缀,很舒服。 API操作方便 下面的代码相当的简单,通过这样的一行代码我们就能够将excel表中的内容变成实体集合 简单解释一下代码的意思 1 首先创建一个excel文件,定义俩列,公司名称和地址,程序读取集合数据。 2 定义一个实体类,俩个字段,Excel Colunm特性标签表明Excel中列和属性英文名称的匹配 3 实例化ExcelQueryFactory (Excel查询工厂),参数是文件路径 .Worksheet<T> T中写我们定义的实体 4 结束 是不是特别的简单,特别的好用! excel内容.png public class Company { [ExcelColumn("公司名称")] public string Name { get; set; } [ExcelColumn("地址")] public string Address{ get; set; } } var excel = new ExcelQueryFactory("excel文件路径"); var indianaCompanies = (from c in excel.Worksheet<Company>() select c; 如果我们自定义了Sheet表的名称怎么办呢,程序能够识别到吗?答案是不能。但是有方法哈,O(∩_∩)O 只要在上面的内容修改一点点,重载方法给定一个参数,就是工作表名就可以了。 像上面什么都没有给定的,是因为Excel工作簿默认第一个工作表是"Sheet1",所以如果我们什么参数都不加,就相当与是"Sheet1"。我们只要改动了工作表名称,就一定要赋参数 var excel = new ExcelQueryFactory("excel文件路径"); var oldCompanies = from c in excel.Worksheet<Company>("工作表名") select c; 上面是我们自己定义的实体类,完成了列名称和实体属性的转换,如果我们要自己做这个事情呢,我们又改如何做,如下所示就可以了,api提供了俩种方式,一个简单方法,一个泛型方法。 var excel = new ExcelQueryFactory("excel文件路径"); excel.AddMapping<Company>(x => x.Name, "公司名称"); excel.AddMapping("Address", "地址"); var indianaCompanies = from c in excel.Worksheet<Company>() select c; LinqToExcel还有很多很有趣的方法,大家可以去官网自己看,自己实践,作者这里不多做叙述。 缺点 转换实体没有错误提示 问题是我在工作过程种使用这个类库觉得很变扭的一个地方,如果有大神有比较好的解决方案,希望给我留言,互相学习。对这个类库的缺点我自己封装了一个帮助类库LinqToExcel.Extend,我会在后期的文章种给予说明。 问题描述 问题代码如下,不能说这一样有问题,而是在某种情况下会触发exception,是什么情况呢。见下图 我们可以看到开业日期这一列有一个数据日期格式出错,这个时候如果调用方法就会报错,因为类型转换不成功,实质上来说这没问题,可是有这么一个场景。 一般这种需要上传Excel的都是导入操作,客户很多时候填写数据,因为粗心或者疏忽,很容易填错,所以我们一般会对excel文件先进行一下解析,如果有问题的字段,会告诉使用者:“XX”行“XX”列字段有问题 问题如下XXX 类似这样的提示。 我本来以为这个类库会大致给一个提示信息,我可以不用再封装,结果是没有。好啵,那我就只能自己封装一个了。 from c in excel.Worksheet<Company>() public class Company { [ExcelColumn("公司名称")] public string Name { get; set; } [ExcelColumn("开业日期")] public Datetime StartDate{ get; set; } } excel内容.png 使用范围有限 可能是因为小弟使用水平有限,我发现这个类库只适用于规规矩矩的行列数据,对特殊的一些数量没有办法识别,这里的特殊不是说多么变态的需求。我还是举例子,下面的图片是我们实际过程种可能碰到的需求,即表格的数据列不一定在第一行,没有一个方法让我选定从哪一行开始选取数据集。 大家不要说有的,官网里面有的,我们通过指定开始并和结束作为判断条件。我是觉得很不舒服,我并不能确认我的结束行在哪里,然后写个F80或者E999吗,代码不美观。 var excel = new ExcelQueryFactory("excelFileName"); var indianaCompanies = from c in excel.WorksheetRange<Company>("B3", "G10") select c; excel例子.png
问题2 局部加载 经过问题1“所见非所得”,我们大概对一些套路有了了解,接下取的局部加载也是一个简单的小套路。 这个方式可以这么解释 你所看到的内容其实是一步一步加载出来的,而不是一下子都给你看到的。这是什么意思呢,其实有点问题1种jsonp种每次加载5个数据的意思。而我们现在要将的是商品的局部加载。还是一样的,我们给出几张图来进行说明。 局部加载1.png 局部加载2.png 如上图所示,当我们第一次打开页面的时候,我们可以看到当前商品li标签个数为30,可是当我们将下拉框移动到页面底部,这个时候我们再看查看li标签个数,此时变成了60。这就又是一个小把戏了。 如果我们按照一般的方法爬取数据,我们就会丢掉一般的数据,是不是特别的坑呢。 既然我们已经发现了这个问题,我们如果解决,这又是一个值得大家考虑的问题,大家可以先进行一下尝试,然后在看接下去的内容,也算是对自己的一个锻炼。 分析 首先我们看第一张图,还是通过f12打开开发者共工具,工具栏我们选择查看xhr内容,这个时候我们就能够发现有符合我们条件的数据出现。我们看到请求返回的内容“分析1.1.png”我们发现就是当前页面商品去掉了一些价格,类别等属性的“结构”。所以内容我们得到了,接着我们分析什么样子请求可以得到对应的内容 如图“分析3.png” “分析4.png”比较,他们只有一个后缀不同,我们不妨猜测,是不是加了后缀的为后30个商品,不加的为前30个商品。经过实现证实了我们的猜想。 如果我要得到A类别的第B页的商品我应该如何拼接符合条件的地址 我们首先分析地址,地址如下 https://list.suning.com/emall/showProductList.do?ci=179001&pg=03&cp=2&il=0&iy=0&adNumber=0&n=1&prune=0&sesab=ABBAAA&id=IDENTIFYING&cc=025 分析当前地址 “分析2.png” “分析2.1.png” 我们可以知道pg=03为当前页码 ci=179001为当前产品编码,所以如果我们要得到符合上述条件的地址,只需要请求如下俩个地址 https://list.suning.com/emall/showProductList.do?ci={页码}&pg={产品编码}&cp=2&il=0&iy=0&adNumber=0&n=1&prune=0&sesab=ABBAAA&id=IDENTIFYING&cc=025 https://list.suning.com/emall/showProductList.do?ci={页码}&pg={产品编码}&cp=2&il=0&iy=0&adNumber=0&n=1&prune=0&sesab=ABBAAA&id=IDENTIFYING&cc=025&paging=1&sub=0 当然在实际编码过程种还会存在其他问题,我在此稍稍提一下,第一个页面获取商品的xpath和第二个页面获取商品的xpah是不一样的 分析1.png 分析1.1.png 分析2.png 分析2.1.png 分析3.png 分析4.png
本章节是最重要,也是最复杂的章节,因为这里面涉及到的点比较多。直至我编码完成后,我还有几个问题没有解决,希望各位网友有好的思路可以提供给我,具体的问题在之后的描述中我会说明。 思路解析 如下图,我们可以得到当前商品的xpath路径 //*[@id="filter-results"]/ul/li 在根据第二张图我们又可以得到单独商品的价格,标题,链接地址等等信息 看上去好像非常的简单,其逻辑和商品类别爬取类似。首先得到所有的商品,然后循环,对实体类别,最后返回一个list列表就行。可是实际却并不是如此。我们接下去就对内容进行分析。 商品解析图1.png 商品解析图2.png 问题1 所见非所得 我们首先来看俩张对比图,第一张图是我们请求当前页面会返回给我们的Html Document数据,我们之前做的爬取都是基于Document的爬取。在图1中我们去找寻第一个商品的价格,我们发现<em>标签中内容为空,这是为什么呢? 之后我们再来看第二张图,第二张图是F12通过开发者工具定位在第一个商品的价格,这时候我们发现,价格是存在的。 此时,我们就要开始提示了,是什么样子情况才会导致这种问题的产生。各位看客也可以想一想。 经过分析后,我们得出了结论 京东做了反爬处理,即不会把一些重要信息直接作为response返回,而且在网页加载结束之后,通过js在将对应的值赋值上去。 抽象来讲,用一个成语,画龙点睛。 先画好结构,最后赋值。这种在一定程度上能否对小白进行一些反爬虫,可是如果有点水平的人,我们通过分析f12的web请求,我们就可以发现,无非就是用了一个jsonp的方式,做了值的获取 所见非所得1.png 所见非所得2.png 上面我们说到苏宁用了jsonp的方法做了价格的获取,那么我们怎么查看呢,见图所示 分析我们得到,每次得到5个值,页面上一行也是5个商品。 所以他们的做法是,当每次鼠标向下移动,下一行商品出现的时候,执行一个jsonp方法,将对应的内容进行绑定 jsonp获取1.png jsonp获取2.png 那么我们又迎来了新的问题,这个jsonp的方法是通过什么样的规律生成接口获得数据的呢 我们取俩个案例进行分析,我们发现方法头和尾部都是标准jsonp的用法,那么只有中间的参数会有影响。 我们可以看到,参数由逗号分隔,参数形式总共有俩种 参数1+下划线 000000010088601142_ 参数1+下划线+2+参数2 000000000617721823__2_0070137013 那我们开始对这俩种参数形式进行分析 参数1和参数2分别是什么?我们不妨大胆猜测一下,有没有可能是商品的唯一key 还有一点,如果是商品的唯一key,我们如何获取? https://ds.suning.cn/ds/generalForTile/000000010044087492_,000000000688241235_,000000010044087493_,000000000144695267_,000000010073350865_,000000000610267239_,000000000132230908_,000000000192930418_,000000010526739331__2_0070207958,000000000624606189_-025-2-0000000000-1--ds0000000009487.jsonp?callback=ds0000000009487 https://ds.suning.cn/ds/generalForTile/000000000617721823__2_0070137013,000000000132236927_,000000000638993733_,000000010519041260__2_0070203680,000000010084257326_-025-2-0000000000-1--ds000000000588.jsonp?callback=ds000000000588 通过分析,我得到了结论,请大家看如下图红框圈出来的内容,我们发现也是俩中形式,一种是参数1+参数2 ,一种是参数1+0000000 example: 0000000000-10044087492 0070207958-10526739331 分析1.png 分析2.png 分析3.png 分析4.png 我们可以通过打黄色标识的俩张图发现规律,页面上的内容和jsonp方法体组成的内容是相反的,所以我们需要在获取内容后,将顺序颠倒,然后按照规定的字符串长度进行拼接即可
通过上述章节内容,我们得到了类别的数据,现在我们需要对每个类别进行商品的爬取。点击移动电源,进行商品总页数抓取,这个模块相对简单,正好适合用来练手。 我们可以从“列表页.png”的图片中看到,当前移动电源的页数为右上角所显示 1/100,即100页. xpath的获取如第三张图所示,结果为 //*[@id="second-filter"]/div[2]/div/span 类别.png 列表页.png xpath.png 分析出了如果获取页数,我们现在要考虑的问题是,如果更新所有的类别。 其实思路非常简单,从数据库中取出对应的等级为3的类别(最底层类别),对这些类别进行循环,参数就是当前行的url,然后执行网页爬取代码,得到页数,更新数据。 数据库类别数据.png 根据Sql语句,得到等级为3的类别一共有4197个。这个时候就存在问题了,如果同步执行(循环一个一个执行),那么我的效率就很低,为了验证自己的写法。我以50个类别为例做了一个小demo测试性能。 //获取符合条件的列表 var urlList = _categoryService.GetListByLevel(3).Select(u => u.Url).ToList(); CategoryPageAnalysis.GetData(string url) 方法为获取类别个数方法 同步 循环执行,耗时18233毫秒 var dics = new Dictionary<string, int>(); foreach (var url in urlList) { dics.Add(url, CategoryPageAnalysis.GetData(url)); } 异步方法 6163毫秒 3倍的效率差 异步方法体的说明如下: 首先因为存在4197个类别,需要对这些类别进行分类。 4197/2000 约等于20. 即开20个线程,每个线程执行200条数据 int pageNum = 200; int pageCount = urlList.Count % pageNum == 0 ? urlList.Count / pageNum : urlList.Count / pageNum + 1; var pageListCollection = new List<List<string>>(); for (int i = 0; i < pageCount; i++) { var pageList = urlList.Skip(i * pageNum).Take(pageNum).ToList(); pageListCollection.Add(pageList); } Console.WriteLine(pageCount); //异步 6163毫秒 3倍的效率差 int pageIndex = 1; List<Task> taskList = new List<Task>(); foreach (var pageList in pageListCollection) { try { Task task = Task.Factory.StartNew(() => { var dics = new Dictionary<string, int>(); foreach (var url in pageList) { dics.Add(url, CategoryPageAnalysis.GetData(url)); } lock (lock_obj) { _categoryService.BatchUpdatePage(dics); } }); taskList.Add(task); } catch (Exception ex) { Console.WriteLine($"button3_Click 异步{ex.Message}"); } } 存在的问题: 这种方法是为了单独解决这个问题而使用的,很笨拙,因为如果只有200个类别,多线程的意义就没有办法体现出来,这一点在之后的编码中我进行了修改。
苏宁类别面 https://list.suning.com/ 解析图.png 通过图可知,总共有N个类别,每个类别都是一个DIV区块,然后再继续分解DIV区块分析内容。我们要得到的是类别表,据图所示我们可以分析得出类别表的结构应当树形的。所以涉及的表应该是包含子节点和父节点的。初步设计图如下 Id Pid Code Name Url 主键 父节点 编码 名称 地址 解析图2.png 我们可以得到解析图2对应的 Xpath为://*[@id="20089"]/div[2]/div[2] 。可是因为是通过ID作为唯一Key来向下找,所以我们需要先得到所有的Key值。这个方法被我放弃而选用了另外一种方式。 /html/body/div[5]/div[2].首先找到如果所示xpath对应的内容 /html/body/div[5]/div[2].png 那么如果我们想要得到下属的内容只需要增加一个后缀 /html/body/div[5]/div[2]/div 此时我们得到了所有模块的内容,那么我们接下去分析 一级.png 二级+三级.png 还是以“手机配件”为例。一级类别,二级类别、三级类别如果所示。我们又如何得到内容,然后将其变成单元行的形式插入数据库中呢? 解决方案如下 根据网页内容可知,一级类别包含着二级类别,二级类别包含着三级类别。所以我们可以采用如下方式。 首先获取所有一级类别,即解析图2.png所示内容。 一级类别 A方法 循环当前内容 1、解析内容 增加当前A级类别实体 2、循环包含的二级内容,处理 3、合并实体 二级类别 B方法 循环当前内容 1、解析内容 增加当前B级类别实体 2、循环包含的三级级内容,处理 3、返回实体给A方法 三级类别 C方法 循环当前内容 1、解析内容 增加当前C级类别实体 2、返回实体给B方法 ABC.png 代码讲解 ABC(Combine)方法 遍历InitA方法获取的内容,增加A实体后将ANode作为参数传递给InitB方法。依次类推,最后得到符合要求的实体。 private static List<POCO_Category> CombineA_B_C() { List<POCO_Category> AList = new List<POCO_Category>(); int idIndex = 1; foreach (HtmlNode xNode in InitA()) { POCO_Category aModel = new POCO_Category() { Id = ToLevelCode(idIndex), PId = "000", Levels = 1, Code = ToLevelCode(idIndex), Name = xNode.SelectSingleNode("./h2").InnerText }; AList.Add(aModel); var blist = InitB(aModel, xNode); AList.AddRange(blist); idIndex = idIndex + blist.Count + 1; } return AList; } private static List<HtmlNode> InitA() { var url = "https://list.suning.com/#20089"; var web = new HtmlWeb(); var docWeb = web.Load(url); //var cssNodes = docWeb.DocumentNode.CssSelect(".search-main.introduce.clearfix > div").ToList();//147毫秒 List<HtmlNode> xpathNodes = docWeb.DocumentNode.SelectNodes("/html/body/div[5]/div[2]/div").ToList(); return xpathNodes; } private static List<POCO_Category> InitB(POCO_Category parentModel, HtmlNode node) { int idIndex = Convert.ToInt32(parentModel.Id) + 1; List<POCO_Category> bList = new List<POCO_Category>(); var xNodes = node.SelectNodes("./div").ToList(); foreach (var xNode in xNodes) { var cateModel = xNode.SelectSingleNode("./div[1]/a"); POCO_Category bModel = new POCO_Category() { Id = ToLevelCode(idIndex), PId = parentModel.Id, Code = $"{parentModel.Code}_{ToLevelCode(idIndex)}", Name = cateModel.InnerText, Url = $"https:{cateModel.GetAttributeValue("href")}", Levels = 2 }; bList.Add(bModel); var clist = InitC(bModel, xNode.SelectSingleNode("./div[2]")); bList.AddRange(clist); idIndex = idIndex + clist.Count + 1; } return bList; } private static List<POCO_Category> InitC(POCO_Category parentModel, HtmlNode node) { int idIndex = Convert.ToInt32(parentModel.Id) + 1; List<POCO_Category> cList = new List<POCO_Category>(); HtmlNodeCollection xNodes = node.SelectNodes("./a"); if (xNodes != null && xNodes.Count > 0) { foreach (var xNode in xNodes) { POCO_Category cModel = new POCO_Category() { Id = ToLevelCode(idIndex), PId = parentModel.Id, Code = $"{parentModel.Code}_{ToLevelCode(idIndex)}", Name = xNode.InnerText, Url = $"https:{xNode.GetAttributeValue("href")}", Levels = 3 }; cList.Add(cModel); idIndex += 1; } } return cList; } private static string ToLevelCode(int index) { return index.ToString("000"); }
代码下载链接 苏宁百万级商品爬虫 目录 思路讲解 类别爬取 思路讲解 类别页数爬取 商品爬取 3.1 思路讲解 商品爬取1 3.2 思路讲解 商品爬取2 3.3 代码讲解 商品爬取 索引讲解 4.1 代码讲解 索引建立 4.2 代码讲解 索引查询 声明 本系列文章+代码案例时对爬虫的内容学习概括,希望更多的人知道如何使用c#进行简单爬虫项目的开发,并不存在恶意工具部分电商网站的观念。分享的的代码中对网页爬取都做了休眠等待(200-500)毫秒的限制,希望大家不要恶意使用。 学习回顾 首先简单概述一下自己的学习计划,在爬虫这个模块的学习过程中。可以了解到很多的知识,例如 Xpath语法(网页解析),css(网页解析),正则表达式(文本处理或网页解析) .net 第三方爬虫类库 html agility pack +第三方爬虫框架(用的相对较少) 学习的时候还是趋向于写一些底层的东西 异步多线程,主要用在苏宁百万数据爬取时。多线程爬取,多线程存储。 Lucene索引和分词 简单使用,并未深入。主要时对爬取的百万数据建立索引库,做一个简单的查询。 运行环境+技术选型 ide 使用 vs 2017 数据库 sqlserver 2008r2 或mysql 语言 c# 一、开发预估周期和安排 1、开发周期 因为工作时无聊想到的东西,所以在不耽误工作的情况下,编码周期为1个礼拜。 2、程序模块抽象描述 数据库相关 实体 数据库访问层 业务逻辑层 网页爬取 分析器(包含取数据功能) 服务层(取分析器数据,调用业务逻辑层方法,将数据入库) 索引 分词帮助(盘古分词器) 索引帮助 界面描述 采用winform程序的形式,分首页+4个子页面 首页是对主要功能的概述,添加4个按钮,每个按钮触发新的页面,按钮分别为: 数据初始化(进行数据初始化功能) 商品类别(对商品类别的爬取和更新) 商品内容(对商品内容的爬取和更新) 建立索引(使用Lucene+分词器建立索引) 查询产品 三、开发中可能遇到的问题 因为之前都是对单页面的爬取,或者是对某些分页数据爬取,都只是一个小demo。所以在设计程序结构的过程中一定会存在问题。当我已经完成项目后,重新回顾自己的代码也觉得好多地方存在可以修改的地方。 对很多技术的生疏,异步多线程在工作中不长使用,没有踩过坑,所以一定会跌的很惨。xpath,正则这些语法的遗忘 界面设计可能会很丑,不美观 四、功能设计图 在新建解决方案后,我首先建立一个demo项目,这个项目只是用来做效果图,用来让自己对所做的程序有个大概的布局。 首页设计图: 首页是对功能的详细抽象描述,所以定义三个模块,每个模块再放置自己的内容。 “初始化数据”只是一个按钮,点击弹出提示框,点击确认清理所有产品数据 首页.png 商品类别设计图.png 商品内容设计图.png 建立索引设计图.png 查询产品设计图.png 五、程序准备: 对商品进行爬取,首先要知道有多少类别,不同类别数据性展示是否为不同形式。如果为不同形式,那就要区分爬取,如果相同,那就更加方便。 产品有那么多属性,取哪些字段,百万的数据量应该如何存储,同样的百万数量应该如果查询,这都是在前期应该考虑好的问题。不然等开始编码再修改就很麻烦
1.第一步 mmc comexp.msc /32 打开DCOM配置 2. 第二步 1).通过webconfig中增加模拟,加入管理员权限, <identity impersonate="true" userName="系统管理员" password="系统管理员密码"/> 2).这样就能够启动Application进程,操作EXCEL了,能够新建EXCEL,导出EXCEL,但是还是不能打开服务器端的EXCEL文件 在组件服务,DOCM设置 Microsoft Excel Application的属性, 因为是在64位系统上面操作,组件服务中DOCOM中默认是没有的,因为Microsoft Excel Application是32的DCOM配置,所以通过如下方式解决(参考第三步) 1).开始--〉运行--〉cmd 2)命令提示符下面,输入mmc -32,打开32的控制台 3).文件菜单中,添加删除管理单元--〉组件服务 4).在"DCOM配置"中找到"Microsoft Excel 应用程序",在它上面点击右键,然后点击"属性",弹出"Microsoft Excel 应用程序属性"对话框 5).点击"标识"标签,选择"交互式用户" 6).点击"安全"标签,在"启动和激活权限"上点击"自定义",然后点击对应的"编辑"按钮,在弹出的"安全性"对话框中填加一个"NETWORK SERVICE"用户(注意要选择本计算机名),并给它赋予"本地启动"和"本地激活"权限 7).依然是"安全"标签,在"访问权限"上点击"自定义",然后点击"编辑",在弹出的"安全性"对话框中也填加一个"NETWORK SERVICE"用户,然后赋予"本地访问"权限. 4.重新启动IIS,测试通过
链接地址: https://msdn.microsoft.com/zh-cn/library/ms252091.aspx
WinForm客户端软件开发时,使用rdlc做报表,并且使用ReportViewer呈现报表时,开发者的机器运行正常。但是部署到第三方机器上运行时报错。大致有以下几种错误: (1)未能加载文件或程序集“Microsoft.ReportViewer.Common, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91”或它的某一个依赖项。系统找不到指定的文件。文件名:“Microsoft.ReportViewer.Common, Version=11.0.0.0, Culture=neutral,PublicKeyToken=89845dcd8080cc91” (2)RDLC的部署(无法找到Microsoft.ReportViewer.ProcessingObjectModel.dll文件) (3)找不到 Microsoft.SqlServer.Types.dll或者其他的依赖项 错误的提示意思是第三方机器上缺少相关dll。到程序开发计算机下找到对应的烤到客户端的程序启动目录下即可(项目烤到Bin\Debug目录下)。由于以上的dll都是在开发者机器的Windows\assembly目录中, image.png 该目录是微软操作系统预定义的特殊目录,无法直接操作里面的文件,只能通过命令来处理。 启动cmd.exe程序,在窗口中输入以下命令行,每次输入一个命令,回车后复制成功。 copy C:\Windows\assembly\gac_msil\Microsoft.ReportViewer.Common\11.0.0.0__89845dcd8080cc91\Microsoft.ReportViewer.Common.dll D:\ReportViewer copy C:\Windows\assembly\gac_msil\Microsoft.ReportViewer.WinForms\11.0.0.0__89845dcd8080cc91\Microsoft.ReportViewer.WinForms.DLL D:\ReportViewer copy C:\Windows\assembly\gac_msil\Microsoft.ReportViewer.ProcessingObjectModel\11.0.0.0__89845dcd8080cc91\Microsoft.ReportViewer.ProcessingObjectModel.DLL D:\ReportViewer copy C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.Types\11.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.Types.dll D:\ReportViewer 【其他的dll复制方式与此类似】 image.png 复制到D盘ReportViewer目录后,将其复制到项目的bin\Debug目录,再次运行程序,一切正常。