背景:

  随着公司的项目不断的完善,功能越来越复杂,服务也越来越多(微服务),公司迫切需要对整个系统的每一个程序的运行情况进行监控,并且能够实现对自动记录不同服务间的程序调用的交互日志,以及通一个服务或者项目中某一次执行情况的跟踪监控

根据log4net的现有功能满足不了实际需求,所以需要以log4net为基础进行分装完善,现在分装出了一个基础的版本,如有不妥之处,多多指点
功能简介:
  该组件是在log4net的基础上,进行了一定的扩展封装实现的自动记录交互日志功能
  该组件的封装的目的是解决一下几个工作中的实际问题
  1、对记录的日志内容格式完善
  2、微服务项目中,程序自动记录不同服务间的调用关系,以及出参、入参、执行时间等
  3、同一项目中,不同方法及其层之间的调用关系等信息
  4、其最终目的就是,实现对系统的一个整体监控

主要封装扩展功能点:
1、通过对log4net进行扩展,能够自定义了一些日志格式颜色内容等
2、通过代理+特性的方式,实现程序自动记录不同服务间,以及同一程序间的相互调用的交互日志
3、采用队列的方式实现异步落地日志到磁盘文件

主要核心代码示例,具体的详细代码,我已经上传至githut开源项目中,如有需要可以下载了解

github源码地址:https://github.com/xuyuanhong0902/XYH.Log4Net.Extend.git

代理实现自动记录方法调用的详细日志

   /// <summary>
/// XYH代理实现类.
/// </summary>
public class XYHAopProxy : RealProxy
{
/// <summary>
/// 构造函数.
/// </summary>
/// <param name="target">目标类型.</param>
public XYHAopProxy(Type target)
: base(target)
{
} /// <summary>
/// 重写代理实现.
/// </summary>
/// <param name="msg">代理函数</param>
/// <returns>返回结果</returns>
public override IMessage Invoke(IMessage methodInvoke)
{
//// 方法开始执行时间
DateTime executeStartTime = System.DateTime.Now; //// 方法执行结束时间
DateTime executeEndTime = System.DateTime.Now; IMessage message = null;
IMethodCallMessage call = methodInvoke as IMethodCallMessage;
object[] customAttributeArray = call.MethodBase.GetCustomAttributes(false);
call.MethodBase.GetCustomAttributes(false); try
{
// 前处理.
List<IAopAction> proActionList = this.InitAopAction(customAttributeArray, AdviceType.Before); //// 方法执行开始记录日志
if (proActionList != null && proActionList.Count > 0 )
{
foreach (IAopAction item in proActionList)
{
IMessage preMessage = item.PreProcess(methodInvoke, base.GetUnwrappedServer());
if (preMessage != null)
{
message = preMessage;
}
} if (message != null)
{
return message;
}
} message = Proessed(methodInvoke); // 后处理.
proActionList = this.InitAopAction(customAttributeArray, AdviceType.Around); //// 方法执行结束时间
executeEndTime = System.DateTime.Now; //// 方法执行结束记录日志
if (proActionList != null && proActionList.Count > 0)
{
foreach (IAopAction item in proActionList)
{
item.PostProcess(methodInvoke, message, base.GetUnwrappedServer(), executeStartTime, executeEndTime);
}
}
}
catch (Exception ex)
{
//// 方法执行结束时间
executeEndTime = System.DateTime.Now; // 异常处理.吃掉异常,不影响主业务
List<IAopAction> proActionList = this.InitAopAction(customAttributeArray, AdviceType.Around);
if (proActionList != null && proActionList.Count > 0)
{
foreach (IAopAction item in proActionList)
{
item.ExceptionProcess(ex, methodInvoke, base.GetUnwrappedServer(), executeStartTime, executeEndTime);
}
}
} return message;
} /// <summary>
/// 处理方法执行.
/// </summary>
/// <param name="methodInvoke">代理目标方法</param>
/// <returns>代理结果</returns>
public virtual IMessage Proessed(IMessage methodInvoke)
{
IMessage message;
if (methodInvoke is IConstructionCallMessage)
{
message = this.ProcessConstruct(methodInvoke);
}
else
{
message = this.ProcessInvoke(methodInvoke);
}
return message;
} /// <summary>
/// 普通代理方法执行.
/// </summary>
/// <param name="methodInvoke">代理目标方法</param>
/// <returns>代理结果</returns>
public virtual IMessage ProcessInvoke(IMessage methodInvoke)
{
IMethodCallMessage callMsg = methodInvoke as IMethodCallMessage;
object[] args = callMsg.Args; //方法参数
object o = callMsg.MethodBase.Invoke(base.GetUnwrappedServer(), args); //调用 原型类的 方法 return new ReturnMessage(o, args, args.Length, callMsg.LogicalCallContext, callMsg); // 返回类型 Message
} /// <summary>
/// 构造函数代理方法执行.
/// </summary>
/// <param name="methodInvoke">代理目标方法</param>
/// <returns>代理结果</returns>
public virtual IMessage ProcessConstruct(IMessage methodInvoke)
{
IConstructionCallMessage constructCallMsg = methodInvoke as IConstructionCallMessage;
IConstructionReturnMessage constructionReturnMessage = this.InitializeServerObject((IConstructionCallMessage)methodInvoke);
RealProxy.SetStubData(this, constructionReturnMessage.ReturnValue); return constructionReturnMessage;
} /// <summary>
/// 代理包装业务处理.
/// </summary>
/// <param name="customAttributeArray">代理属性</param>
/// <param name="adviceType">处理类型</param>
/// <returns>结果.</returns>
public virtual List<IAopAction> InitAopAction(object[] customAttributeArray, AdviceType adviceType)
{
List<IAopAction> actionList = new List<IAopAction>();
if (customAttributeArray != null && customAttributeArray.Length > 0)
{
foreach (Attribute item in customAttributeArray)
{
XYHMethodAttribute methodAdviceAttribute = item as XYHMethodAttribute;
if (methodAdviceAttribute != null && (methodAdviceAttribute.AdviceType == adviceType))
{
if (methodAdviceAttribute.ProcessType == ProcessType.None)
{
continue;
} if (methodAdviceAttribute.ProcessType == ProcessType.Log)
{
actionList.Add(new LogAopActionImpl());
continue;
}
}
}
} return actionList;
}
}

  类注解

  /// <summary>
/// XYH代理属性[作用于类].
/// ************************************
/// [DecorateSymbol] Class ClassName
/// ************************************
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class XYHAopAttribute : ProxyAttribute
{
public XYHAopAttribute()
{
} public override MarshalByRefObject CreateInstance(Type serverType)
{
XYHAopProxy realProxy = new XYHAopProxy(serverType);
return realProxy.GetTransparentProxy() as MarshalByRefObject;
}
}

  队列实现异步日志落地到磁盘文件

namespace XYH.Log4Net.Extend
{
/// <summary>
/// 通过队列的方式实现异步记录日志
/// </summary>
public sealed class ExtendLogQueue
{
/// <summary>
/// 记录消息 队列
/// </summary>
private readonly ConcurrentQueue<LogMessage> extendLogQue; /// <summary>
/// 信号
/// </summary>
private readonly ManualResetEvent extendLogMre; /// <summary>
/// 日志
/// </summary>
private static ExtendLogQueue _flashLog = new ExtendLogQueue(); /// <summary>
/// 构造函数
/// </summary>
private ExtendLogQueue()
{
extendLogQue = new ConcurrentQueue<LogMessage>();
extendLogMre = new ManualResetEvent(false);
} /// <summary>
/// 单例实例
/// </summary>
/// <returns></returns>
public static ExtendLogQueue Instance()
{
return _flashLog;
} /// <summary>
/// 另一个线程记录日志,只在程序初始化时调用一次
/// </summary>
public void Register()
{
Thread t = new Thread(new ThreadStart(WriteLogDispatch));
t.IsBackground = false;
t.Start();
} /// <summary>
/// 从队列中写日志至磁盘
/// </summary>
private void WriteLogDispatch()
{
while (true)
{ //// 如果队列中还有待写日志,那么直接调用写日志
if (extendLogQue.Count > 0)
{
//// 根据队列写日志
WriteLog(); // 重新设置信号
extendLogMre.Reset();
} //// 如果没有,那么等待信号通知
extendLogMre.WaitOne();
}
} /// <summary>
/// 具体调用log4日志组件实现
/// </summary>
private void WriteLog()
{
LogMessage msg;
// 判断是否有内容需要如磁盘 从列队中获取内容,并删除列队中的内容
while (extendLogQue.Count > 0 && extendLogQue.TryDequeue(out msg))
{
new LogHandlerImpl(LogHandlerManager.GetILogger(msg.LogSerialNumber)).WriteLog(msg);
}
} /// <summary>
/// 日志入队列
/// </summary>
/// <param name="message">日志文本</param>
/// <param name="level">等级</param>
/// <param name="ex">Exception</param>
public void EnqueueMessage(LogMessage logMessage)
{
//// 日志入队列
extendLogQue.Enqueue(logMessage); // 通知线程往磁盘中写日志
extendLogMre.Set();
}
}
}

  自定义扩展log4net日志格式内容

namespace XYH.Log4Net.Extend
{
/// <summary>
/// 自定义布局(对log2net日志组件的布局自定义扩展).
/// </summary>
public class HandlerPatternLayout : PatternLayout
{
/// <summary>
/// 构造函数.
/// </summary>
public HandlerPatternLayout()
{
///// 机器名称
this.AddConverter("LogMachineCode", typeof(LogMachineCodePatternConvert)); //// 方法名称
this.AddConverter("MethodName", typeof(LogMethodNamePatternConvert)); //// 方法入参
this.AddConverter("MethodParam", typeof(LogMethodParamConvert)); //// 方法出参
this.AddConverter("MethodResult", typeof(LogMethodResultConvert)); //// 程序名称
this.AddConverter("LogProjectName", typeof(LogProjectNamePatternConvert)); //// IP 地 址
this.AddConverter("LogIpAddress", typeof(LogServiceIpPatternConvert)); //// 日志编号
this.AddConverter("LogUniqueCode", typeof(LogUniquePatternConvert)); //// 日志序列号
this.AddConverter("LogSerialNumber", typeof(LogSerialNumberPatternConvert)); //// 调用路径
this.AddConverter("InvokeName", typeof(LogInvokeNamePatternConvert)); //// 执行开始时间
this.AddConverter("ExecuteStartTime", typeof(ExecuteStartTimePatternConvert)); //// 执行结束时间
this.AddConverter("ExecuteEndTime", typeof(ExecuteEndTimePatternConvert)); //// 执行时间
this.AddConverter("ExecuteTime", typeof(ExecuteTimePatternConvert));
}
}
}

  

使用说明:
第一步:需要dll文件引用
需要引用两个dell文件:
jeson序列化:Newtonsoft.Json.dll
log4net组件:log4net.dll
log3net扩展组件:XYH.Log4Net.Extend.dll

第二步:log4配置文件配置
主要配置日志的存储地址,日志文件存储格式、内容等
下面,给一个参考配置文件,具体的配置可以根据实际需要自由配置,其配置方式很log4net本身的配置文件一样,在此不多说

<log4net>
<root>
<!-- 定义记录的日志级别[None、Fatal、ERROR、WARN、DEBUG、INFO、ALL]-->
<level value="ALL"/>
<!-- 记录到什么介质中-->
<appender-ref ref="LogInfoFileAppender"/>
<appender-ref ref="LogErrorFileAppender"/>
</root>
<!-- name属性指定其名称,type则是log4net.Appender命名空间的一个类的名称,意思是,指定使用哪种介质-->
<appender name="LogInfoFileAppender" type="log4net.Appender.RollingFileAppender">
<!-- 输出到什么目录-->
<param name="File" value="Log\\LogInfo\\"/>
<!-- 是否覆写到文件中-->
<param name="AppendToFile" value="true"/>
<!-- 单个日志文件最大的大小-->
<param name="MaxFileSize" value="10240"/>
<!-- 备份文件的个数-->
<param name="MaxSizeRollBackups" value="100"/>
<!-- 是否使用静态文件名-->
<param name="StaticLogFileName" value="false"/>
<!-- 日志文件名-->
<param name="DatePattern" value="yyyyMMdd".html""/>
<param name="RollingStyle" value="Date"/>
<!--布局-->
<layout type="XYH.Log4Net.Extend.HandlerPatternLayout">
<param name="ConversionPattern" value="<HR COLOR=blue>%n%n
日志编号:%property{LogUniqueCode} <BR >%n
日志序列:%property{LogSerialNumber} <BR>%n
机器名称:%property{LogMachineCode} <BR>%n
IP 地 址:%property{LogIpAddress} <BR>%n
开始时间:%property{ExecuteStartTime} <BR>%n
结束时间:%property{ExecuteEndTime} <BR>%n
执行时间:%property{ExecuteTime} <BR>%n
程序名称:%property{LogProjectName} <BR>%n
方法名称:%property{MethodName} <BR>%n
方法入参:%property{MethodParam} <BR>%n
方法出参:%property{MethodResult} <BR>%n
日志信息:%m <BR >%n
日志时间:%d <BR >%n
日志级别:%-5p <BR >%n
异常堆栈:%exception <BR >%n
<HR Size=1 >"/>
</layout>
</appender>
<!-- name属性指定其名称,type则是log4net.Appender命名空间的一个类的名称,意思是,指定使用哪种介质-->
<appender name="LogErrorFileAppender" type="log4net.Appender.RollingFileAppender">
<!-- 输出到什么目录-->
<param name="File" value="Log\\LogError\\"/>
<!-- 是否覆写到文件中-->
<param name="AppendToFile" value="true"/>
<!-- 备份文件的个数-->
<param name="MaxSizeRollBackups" value="100"/>
<!-- 单个日志文件最大的大小-->
<param name="MaxFileSize" value="10240"/>
<!-- 是否使用静态文件名-->
<param name="StaticLogFileName" value="false"/>
<!-- 日志文件名-->
<param name="DatePattern" value="yyyyMMdd".html""/>
<param name="RollingStyle" value="Date"/>
<!--布局-->
<layout type="XYH.Log4Net.Extend.HandlerPatternLayout">
<param name="ConversionPattern" value="<HR COLOR=red>%n
日志编号:%property{LogUniqueCode} <BR >%n
日志序列:%property{LogSerialNumber} <BR>%n
机器名称:%property{LogMachineCode} <BR>%n
IP 地 址: %property{LogIpAddress} <BR>%n
程序名称:%property{LogProjectName} <BR>%n
方法名称:%property{MethodName}<BR>%n
方法入参:%property{MethodParam} <BR>%n
方法出参:%property{MethodResult} <BR>%n
日志信息:%m <BR >%n
日志时间:%d <BR >%n
日志级别:%-5p <BR >%n
异常堆栈:%exception <BR >%n
<HR Size=1 >"/>
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="ERROR"/>
<levelMax value="FATAL"/>
</filter>
</appender>
</log4net>

  

第三步:在Global.asax文件中注册消息队列
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);

////注册日志队列
ExtendLogQueue.Instance().Register();
}

第四步:在Global.asax文件中生成处理日志序列号
/// <summary>
/// 每一个请求执行开始
/// </summary>
protected void Session_Start() {
//// 记录获取创建每一个请求的序列号
/// 如果调用放传递了序列号,那么就直接去调用放传递的序列号
/// 如果调用放未传递,那么则生成一个序列号
/// 这样,在一次请求的头部传递一个该请求的唯一序列号,并在以后的每一个请求都一直传递下去
/// 这样,就能够通过这个序列号把每一次请求之间的服务或者方法调用关系串联起来
String[] serialNumber = Request.Headers.GetValues("serialNumber");
if (serialNumber!=null && serialNumber.Length>0 && !string.IsNullOrEmpty(serialNumber[0]))
{
Session["LogSerialNumber"] = serialNumber[0];
}
else
{
Session["LogSerialNumber"] = Guid.NewGuid().ToString().Replace("-", "").ToUpper();
}
}

第五步:在需要自动记录日志的方法类上加上对应的注解

//// 在需要自动记录日志的类上加上 XYHAop注解
[XYHAop]
public class Class2: calssAdd
{
//// 需要记录自动记录交互日志的方法注解 ProcessType.Log

//// 同时该类还必须继承ContextBoundObject

[XYHMethod(ProcessType.Log)]
public int AddNum(int num1, int num2)
{
}
//// 需要记录自动记录交互日志的方法注解 ProcessType.None,其实不加注解也不会记录日志
[XYHMethod(ProcessType.None)]
public int SubNum(int num1, int num2)
{
}
}

第六步:完成上面五步已经能够实现自动记录交互日志了,

 但是在实际使用中我们也会手动记录一些日志,本插件也支持手动记录日志的同样扩展效果

目前支持以下6中手动记录日志的重载方法基于log4net的日志组件扩展分装,实现自动记录交互日志 XYH.Log4Net.Extend

 /// <summary>
/// 记录日志扩展入口
/// </summary>
public class XYHLogOperator
{
/// <summary>
/// 添加日志.
/// </summary>
/// <param name="message">日志信息对象</param>
public static void WriteLog(object message)
{
new MessageIntoQueue().WriteLog(message);
} /// <summary>
/// 添加日志.
/// </summary>
/// <param name="message">日志信息对象</param>
/// <param name="level">日志信息级别</param>
public static void WriteLog(object message, LogLevel level)
{
new MessageIntoQueue().WriteLog(message, level);
} /// <summary>
/// 添加日志.
/// </summary>
/// <param name="message">日志信息对象</param>
/// <param name="level">日志信息级别</param>
/// <param name="exception">异常信息对象</param>
public static void WriteLog(object message, Exception exception)
{
new MessageIntoQueue().WriteLog(message, exception);
} /// <summary>
/// 添加日志.
/// </summary>
/// <param name="message">日志信息对象</param>
/// <param name="methodName">方法名</param>
/// <param name="methodParam">方法入参</param>
/// <param name="methodResult">方法请求结果</param>
public static void WriteLog(object message, string methodName, object methodParam, object methodResult)
{
new MessageIntoQueue().WriteLog(message, methodName, methodParam, methodResult);
} /// <summary>
/// 添加日志.
/// </summary>
/// <param name="message">日志信息对象</param>
/// <param name="methodName">方法名</param>
/// <param name="methodParam">方法入参</param>
/// <param name="methodResult">方法请求结果</param>
/// <param name="level">日志记录级别</param>
public static void WriteLog(object message, string methodName, object methodParam, object methodResult, LogLevel level)
{
new MessageIntoQueue().WriteLog(message, methodName, methodParam, methodResult, level);
} /// <summary>
/// 添加日志
/// </summary>
/// <param name="extendLogInfor">具体的日志消息model</param>
public static void WriteLog(LogMessage extendLogInfor)
{
new MessageIntoQueue().WriteLog(extendLogInfor);
}
}
}

  

手动记录日志示例:

object message = "一个参数日志记录单元测试"; // TODO: 初始化为适当的值
XYHLogOperator.WriteLog(message);

如有问题,欢迎QQ随时交流
QQ:1315597862

github源码地址:https://github.com/xuyuanhong0902/XYH.Log4Net.Extend.git

基于log4net的日志组件扩展封装,实现自动记录交互日志 XYH.Log4Net.Extend(微服务监控)的更多相关文章

  1. 基于SkyWalking的分布式跟踪系统 - 微服务监控

    上一篇文章我们搭建了基于SkyWalking分布式跟踪环境,今天聊聊使用SkyWalking监控我们的微服务(DUBBO) 服务案例 假设你有个订单微服务,包含以下组件 MySQL数据库分表分库(2台 ...

  2. 微服务监控zipkin、skywalking以及日志ELK监控系列

    0.整体架构 整体架构目录:ASP.NET Core分布式项目实战-目录 一.目录 1.zipkin监控 2.skywalking监控 3.ELK日志监控 asp.net Core 交流群:78746 ...

  3. .NetCore中的日志(1)日志组件解析

    .NetCore中的日志(1)日志组件解析 0x00 问题的产生 日志记录功能在开发中很常用,可以记录程序运行的细节,也可以记录用户的行为.在之前开发时我一般都是用自己写的小工具来记录日志,输出目标包 ...

  4. C#组件系列———又一款日志组件:Elmah的学习和分享

    前言:好久没动笔了,都有点生疏,12月都要接近尾声,可是这月连一篇的产出都没有,不能坏了“规矩”,今天还是来写一篇.最近个把月确实很忙,不过每天早上还是会抽空来园子里逛逛.一如既往,园子里每年这个时候 ...

  5. C#组件系列——又一款日志组件:Elmah的学习和分享

    前言:好久没动笔了,都有点生疏,12月都要接近尾声,可是这月连一篇的产出都没有,不能坏了“规矩”,今天还是来写一篇.最近个把月确实很忙,不过每天早上还是会抽空来园子里逛逛.一如既往,园子里每年这个时候 ...

  6. 细说JDK日志组件

    1. 概述 JDK自带的日志组件在包java.util.logging下,如图: 2. 架构如上图所示,JDK日志组件核心元素包括:Logger,Handler,Filter和Formatter,他们 ...

  7. 细说java平台日志组件

    1. java.util.logging JDK自带日志组件,使用方式简单,不需要依赖第三方日志组件.支持将日志打印到控制台,文件,甚至可以将日志通过网络打印到指定主机.相对于第三方独立日志框架来说, ...

  8. 【Go】类似csv的数据日志组件设计

    原文链接:https://blog.thinkeridea.com/201907/go/csv_like_data_logs.html 我们业务每天需要记录大量的日志数据,且这些数据十分重要,它们是公 ...

  9. .NETCore微服务探寻(三) - 分布式日志

    前言 一直以来对于.NETCore微服务相关的技术栈都处于一个浅尝辄止的了解阶段,在现实工作中也对于微服务也一直没有使用的业务环境,所以一直也没有整合过一个完整的基于.NETCore技术栈的微服务项目 ...

随机推荐

  1. 转:用 Python 一键分析你的上网行为, 看是在认真工作还是摸鱼

    简介 想看看你最近一年都在干嘛?看看你平时上网是在摸鱼还是认真工作?想写年度汇报总结,但是苦于没有数据?现在,它来了. 这是一个能让你了解自己的浏览历史的Chrome浏览历史记录分析程序,当然了,他仅 ...

  2. 深入理解JVM,类加载器

    虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流(即字节码)”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这个动作的代码模块称 ...

  3. Python数据分析揭秘知乎大V的小秘密

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: 清风小筑 PS:如有需要Python学习资料的小伙伴可以加点击下方链 ...

  4. 易优CMS:小白学代码之notempty

    [基础用法] 名称:notempty 功能:判断某个变量是否为空,可以嵌套到任何标签里面使用,比如:channel.type等 语法: {eyou:notempty name='$eyou.field ...

  5. 浅谈Vue下的components模板

    浅谈Vue下的components模板在我们越来越深入Vue时,我们会发现我们对HTML代码的工程量会越来越少,今天我们来谈谈Vue下的 components模板的 初步使用方法与 应用 我们先来简单 ...

  6. 区块链社交APP协议分析预告

    2017年,比特币的火热,直接导致了代币市场的繁荣: 2018年,作为信用体系的未来解决方案,区块链引发了互联网原住民的淘金热. 作为风口上的引流神器,区块链技术与社交网络结合起来,产生了一系列区块链 ...

  7. iOS 禁用`URL Scheme`和`Universal Link`(通用链接)

    为什么要禁用URL Scheme和Universal Link(通用链接) 通常我们APP中都会嵌套一些web页面,有时我们的web页面会被DNS劫持从而跳转到其他APP中:或者是某些APP的Univ ...

  8. 为什么 Redis 为什么如此受欢迎

    现在大多数开发人员都会听说过 Redis.Redis 是目前市场上最好的开源内存 NoSQL 数据库之一.它为前端以及后端服务(如键值查找,队列,哈希等)提供了非常多的帮助. 一.什么是 Redis? ...

  9. Python散列类型和运算符

    集合定义 集合的交 并 差 常见的运算符的用法 字典的定义 字典的 get  items  keys  pop  popitem  update  方法 三种逻辑运算 集合 集合特性 唯一性:不存在两 ...

  10. 4-9 Panadas与sklearn结合实例

      1.显示百分比的柱状图 In [1]: import pandas as pd import numpy as np import matplotlib.pyplot as plt %matplo ...