好玩的Canvas 学习:曲线图形的绘制(贝塞尔曲线的妙用)

直线图形和曲线图形是Canvas中的两大基本图形,《好玩的Canvas:直线(线段)、图形的绘制》已经介绍了直线图形的绘制,本文将讲述如何在Canvas中绘制曲线图形。

初中数学里面的曲线定义如下:

任何一根连续的线条都称为曲线,包括直线、折线、线段、圆弧等。

弧线是圆周的一部分,且每个点曲率相同,曲线包含弧线。本文讲述的曲线主要是常规的曲线和特殊的曲线——弧线,包含圆形绘制、弧线绘制、贝塞尔曲线绘制。

Canvas圆形的绘制

描边圆绘制

canvas arc
在Canvas中,使用arc()方法来绘制圆形,代码如下

cxt.beginPath();
cxt.arc(x,y,radius,startAngle,endAngle,anticlockwise);
cxt.closePath();

解释:

  • x和y是圆心坐标,也就是图中O(x0,y0);
  • radius是圆的半径;
  • startAngle和endAngle是图中起始角和结束角,单位为弧度;
  • anticlockwise指绘制的方向,是一个布尔值,true代表逆时针方向绘制,false代表顺时针方向绘制,默认是false,顺时针方向绘制;

注意:
需要先调用beginPath()方法来开始一个新路径,然后才可以开始绘制,绘制完成之后,还需要调用closePath()方法来关闭当前路径。

下面是一段代码示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>圆形的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");

        cxt.beginPath();
        cxt.arc(150,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.stroke();
    </script>
</html>

效果如下:
canvas画圆

上面这个示例画的圆就采用stroke()方法进行描边了,我们还可以给边设置属性,代码如下:

cxt.strokeStyle = "颜色值";

下面是一个代码示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>圆形的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");

        cxt.beginPath();
        cxt.arc(150,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.stroke();

        cxt.beginPath();
        cxt.arc(450,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.strokeStyle="green";
        cxt.stroke();

        //1/4圆 逆时针方向绘制
        cxt.beginPath();
        cxt.arc(150,350,100,0*Math.PI/180,45*Math.PI/180,false);
        cxt.closePath();
        cxt.strokeStyle="green";
        cxt.stroke();

        //1/4圆顺时针方向绘制变成 3/4圆
        cxt.beginPath();
        cxt.arc(450,350,100,0*Math.PI/180,45*Math.PI/180,true);
        cxt.closePath();
        cxt.strokeStyle="green";
        cxt.stroke();
    </script>
</html>

效果如下:
canvas画圆

填充圆绘制

在Canvas中,采用fill()方法来绘制填充圆。

cxt.beginPath();
cxt.arc(x,y,radius,startAngle,endAngle,anticlockwise);
cxt.closePath();
cxt.fillStyle = "颜色值";
cxt.fill();

下面是一个代码示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>圆形的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");

        //描边圆
        cxt.beginPath();
        cxt.arc(150,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.stroke();

        //绿色边描边圆
        cxt.beginPath();
        cxt.arc(250,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.strokeStyle="green";
        cxt.stroke();

        //红色填充圆
        cxt.beginPath();
        cxt.arc(350,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.fillStyle="red";
        cxt.fill();

        //绿色填充圆
        cxt.beginPath();
        cxt.arc(450,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.fillStyle="green";
        cxt.fill();

        //蓝色描边 绿色填充圆(先填充后描边,不然样式会被覆盖)
        cxt.beginPath();
        cxt.arc(550,150,100,0*Math.PI/180,360*Math.PI/180,false);
        cxt.closePath();
        cxt.fillStyle="green";
        cxt.fill();
        cxt.strokeStyle = "blue";
        cxt.stroke();

        //1/4圆 逆时针方向绘制 
        cxt.beginPath();
        cxt.arc(150,350,100,0*Math.PI/180,45*Math.PI/180,false);
        cxt.closePath();
        cxt.fillStyle="blue";
        cxt.fill();
    </script>
</html>

效果如下:
canvas绘制圆形

注意:
后面的操作呈现的样式会覆盖前面的操作呈现的样式。

弧线的绘制

arc()绘制弧线

前面我们采用arc()绘制了圆形和1/4圆,代码最后一行有closePath(),将这一行代码去掉就变成了圆弧,因为圆弧不是闭合路径,closePath()方法的作用是关闭路径、连接起点与终点。一个简单示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>弧线的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");

        //1/4圆 逆时针方向绘制 
        cxt.beginPath();
        cxt.arc(150,350,100,0*Math.PI/180,45*Math.PI/180,false);
        // cxt.closePath();
        cxt.stroke();
    </script>
</html>

效果如下:
canvas画弧线

stroke()fill()只能绘制当前路径,不能同时绘制几个路径,下面是一个示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>弧线的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");

        //情况1:
        // 先绘制直线
        cxt.moveTo(250, 50);
        cxt.lineTo(250, 350);
        // cxt.stroke();//画完直线不使用stroke()
        //再逆时针方向绘制 1/4圆 
        cxt.beginPath();
        cxt.arc(150, 350, 100, 0 * Math.PI / 180, 45 * Math.PI / 180, false);
        cxt.stroke();

        //情况2:
        // 先绘制绘制直线
        cxt.moveTo(300, 50);
        cxt.lineTo(300, 350);
        cxt.stroke(); //画完直线使用stroke()
        //再逆时针方向绘制 1/4圆 
        cxt.beginPath();
        cxt.arc(200, 350, 100, 0 * Math.PI / 180, 45 * Math.PI / 180, false);
        cxt.stroke();
    </script>
</html>

效果如下:
stroke

可以看出stroke()fill()只能绘制当前路径,不能同时绘制几个路径。

arcTo()方法绘制弧线

除了使用arc()方法可以绘制弧线外,我们还可以使用arcTo()方法绘制弧线。
arcTO()

cxt.arcTo(cx,cy,x2,y2,radius);

解释:

  • cx,cy是图中控制点坐标(cx,cy);
  • x2,y2是图中结束点坐标(x2,y2);
  • radius是图中圆O的半径;

开始点、控制点、结束点这三个点组成两条如图中直线,两条直线夹一个角,在这个角中做与两条直线相切的圆,所夹弧即为绘制的圆弧。此时会有一个问题,圆的半径不同,那么两个切点位置也不同,所以切点不一定就是开始点和控制点,也就是说切点在两条线段延长线上

细心的朋友还会发现上面语法中没有开始点的坐标,那么开始点由谁来设置呢?

答案很简单,一般由moveTo()lineTo()提供开始点,下面是一个示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>弧线的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");

        //情况1:切点正好与开始点和结束点重合
        cxt.moveTo(20,40);
        cxt.arcTo(40,40,40,60,20);
        cxt.stroke();

        //情况2:切点与开始点和结束点不重合
        cxt.moveTo(20,40);
        cxt.arcTo(90,40,90,60,20);
        cxt.stroke();

        //情况3:直线弧线结合做出情况2效果
        cxt.moveTo(120,40);
        cxt.lineTo(140,40);
        cxt.arcTo(160,40,160,60,20);
        cxt.stroke();
    </script>
</html>

效果如下:
arcTo()

从情况2和情况3对比可以看出如果切点和开始点不重合时,arcTo()方法还将添加一条当前切点到开始点的直线线段。

圆角按钮的绘制

canvas
上面是一个示例,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>圆角按钮的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        function create_button(cxt,x,y,w,h,r,fillStyle=""){
            cxt.beginPath();
            cxt.moveTo(x+r,y);
            cxt.lineTo(x+w-r,y);
            cxt.arcTo(x+w,y,x+w,y+r,r);

            cxt.lineTo(x+w,y+h-r);
            cxt.arcTo(x+w,y+h,x+w-r,y+h,r);

            cxt.lineTo(x+r,y+h);
            cxt.arcTo(x,y+h,x,y+h-r,r);

            cxt.lineTo(x,y+r);
            cxt.arcTo(x,y,x+r,y,r);
            if(fillStyle){
                cxt.fillStyle = fillStyle;
                cxt.fill();
            }else{
                cxt.stroke();
            }
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");
        create_button(cxt,5,5,100,50,10);
        create_button(cxt,120,5,100,50,10,"red");
    </script>
</html>

效果如下:
canvas画按钮

绘制贝塞尔曲线

前面已经介绍如何使用arc()arcTo()绘制圆形和圆弧,接下来介绍一下如何使用Canvas绘制曲线,说到曲线,做前端开发的都应该听说的贝塞尔曲线的大名,这里也只介绍一下贝塞尔曲线的用法。

贝塞尔曲线简介

贝塞尔曲线又叫贝兹曲线、贝济埃曲线,是计算机图形学中非常重要的曲线,前端开发中可应用于轨迹的绘制以及定义动画曲线。

贝塞尔曲线原理

贝塞尔曲线是由n个点来决定,其曲线轨迹公式为:
贝塞尔曲线公式

n代表贝塞尔曲线是几阶曲线。
详细原理可以参考下面给出的链接:

https://baike.baidu.com/item/%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF/1091769?fr=aladdin
https://segmentfault.com/a/1190000018502700#item-1

二阶贝塞尔曲线绘制

和前面一样,二阶贝塞尔曲线也需要开始点、控制点、结束点,但是不需要半径,具体如何绘制出来的可以参考上面发的链接,还可以看看photoshop的钢笔工具。

cxt.quadraticCurveTo(cx,cy,x2,y2);

解释:

  • cx,cy是控制点的坐标;
  • x2,y2是结束点坐标;
  • 起始点坐标由moveTo()lineTo()确定;

下面是一段代码示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>二阶贝塞尔曲线的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");
        cxt.moveTo(50,50);
        cxt.quadraticCurveTo(50,100,100,50);
        cxt.stroke();
    </script>
</html>

效果如下:

二阶贝塞尔曲线绘制

手工计算然后再采用贝塞尔曲线绘制我们想要的图形有点小难,我们可以用下面工具辅助设计:

演示:http://wx.karlew.com/canvas/bezier/

源码:https://github.com/karlew/bezier-tool-for-canvas

下面是一个用贝塞尔曲线绘制云朵的案例,首先打开演示地址,点击upload上传效果图图片,根据图片线条,手动匹配出相应的贝塞尔曲线。

卡通云朵

将上面这张云朵图片上传,然后手动调整贝塞尔曲线匹配轮廓,这个工具的一些使用技巧就不在此处阐述。

贝塞尔曲线辅助设计

完整代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>二阶贝塞尔曲线的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");
        // cxt.lineWidth = 10;//设置线条宽度
        cxt.beginPath();
        cxt.moveTo(79, 143);
        cxt.quadraticCurveTo(90, 18, 214, 84);

        cxt.moveTo(352, 91);
        cxt.quadraticCurveTo(284, 18, 214, 84);

        cxt.moveTo(352, 91);
        cxt.quadraticCurveTo(541, 115, 399, 204);

        cxt.moveTo(283, 239);
        cxt.quadraticCurveTo(359, 292, 399, 204);

        cxt.moveTo(283, 239);
        cxt.quadraticCurveTo(222, 285, 158, 243);

        cxt.moveTo(79, 143); //与前面形成闭合
        cxt.quadraticCurveTo(13, 243, 158, 243);

        cxt.stroke();
    </script>
</html>

效果如下:

贝塞尔曲线绘制的云朵

三阶贝塞尔曲线

cxt.bezierCurveTo(cx1,cy1,cx2,cy2,x,y);

三阶贝塞尔曲线有两个控制点,下面是一个简单demo:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>三阶贝塞尔曲线的绘制</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");
        cxt.beginPath();
        cxt.moveTo(30,30);
        cxt.bezierCurveTo(40,240,840,500,280,365);
        cxt.stroke();
    </script>
</html>

效果如下:
三阶贝塞尔曲线绘制的图形

下面还是使用前面的辅助设计工具,绘制一个爱心,爱心素材如下:
爱心
在工具中去获取坐标点:
贝塞尔曲线

代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>三阶贝塞尔曲线绘制爱心</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script>
        function get_dom(id) {
            return document.getElementById(id);
        }

        var canvas = get_dom('canvas');
        var cxt = canvas.getContext("2d");
        cxt.beginPath();
        cxt.moveTo(245, 383);
        cxt.bezierCurveTo(610, 144, 412, -7, 250, 84);

        cxt.moveTo(245, 383);
        cxt.bezierCurveTo(-3, 206, 11, 3, 250, 84);

        cxt.fillStyle = "red";
        cxt.fill();
    </script>
</html>

效果如下:

贝塞尔曲线绘制的爱心

总结

理论上可以利用Canvas的曲线绘制各种图形,也可以利用曲线绘制一些图表。

Canvas 快速绘制五角星

前面《好玩的Canvas:直线(线段)、图形的绘制》一文讲述了Canvas绘制直线和矩形,本文将讲述如何绘制五角星,首先在纸上计算推导五角星五个坐标点:
canvas绘制五角星推导
然后我们可以看到五个角坐标规律,下面开始绘制:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制五角星</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
    <script type="text/javascript">
        function get_dom(id){
            return document.getElementById(id);
        }

        //canavs绘制五角星
        //中心点坐标(x0,y0)
        //m,h五角星中心点到凸角和凹角的长度,使m大小接近于2*h
        function create_5(cxt,x0,y0,m,h){
            var a = 18;
            var b = 54;
            cxt.beginPath();
            for(i=0;i<5;i++){
                cxt.lineTo( m*Math.cos((a+i*72)*Math.PI/180)+x0, m*Math.sin((a+i*72)*Math.PI/180)+y0 );
                cxt.lineTo( h*Math.cos((b+i*72)*Math.PI/180)+x0, h*Math.sin((b+i*72)*Math.PI/180)+y0 );
            }
            cxt.closePath();
            cxt.stroke();
        }

        var canvas = get_dom("canvas");
        var cxt = canvas.getContext("2d");
        create_5(cxt,100,100,100,50);

    </script>
</html>

效果如下:
image

重学前端:你不知道的CSS Reset、Normalize.css

html5默认样式

所谓的HTML默认样式就是当我们写一段HTML代码,然后不写任何css样式,这段代码在浏览器界面上还是有样式存在。例如<html></html>h1~h6等都有默认样式,自己可以在浏览器上查看。

body默认样式

可以看到<body></body>默认加了margin:8px;,然后样式来自于user agent stylesheet,这是浏览器的默认样式表。

默认样式在我们开发过程中会带来一些问题,我们通常需要重写样式覆盖默认样式,通常称之为CSS Reset

CSS Reset

最暴力的CSS Reset方案:

*{
    margin:0px;
    padding:0px;
}

最早的一份CSS Reset来自Tantek 的undohtml.css,下面是代码:

/* undohtml.css */
/* (CC) 2004 Tantek Celik. Some Rights Reserved.             */
:link,:visited { text-decoration:none }
ul,ol { list-style:none }
h1,h2,h3,h4,h5,h6,pre,code { font-size:1em; }
ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,
fieldset,input{ margin:0; padding:0 }
a img,:link img,:visited img { border:none }
address { font-style:normal }

从URL中的日期可以看出时间是2004年,Tantek根据自身需要对于一些标签进行了简单的重置。

在Google搜索css reset后出现的第一个结果,网址如下:

https://meyerweb.com/eric/tools/css/reset/

代码如下:

/* http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed, 
figure, figcaption, footer, header, hgroup, 
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, 
footer, header, hgroup, menu, nav, section {
    display: block;
}
body {
    line-height: 1;
}
ol, ul {
    list-style: none;
}
blockquote, q {
    quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}

我们可以看到这份文件的日期是2011年1月26日,整个代码将一些默认样式暴力清除了。

雅虎的YUI团队也出了一份CSS Reset方案:

/*
YUI 3.18.1 (build f7e7bcb)
Copyright 2014 Yahoo! Inc. All rights reserved.
Licensed under the BSD License.
http://yuilibrary.com/license/
*/

/**
 * Percents could work for IE, but for backCompat purposes, we are using keywords.
 * x-small is for IE6/7 quirks mode.
 */
body {
    font:13px/1.231 arial,helvetica,clean,sans-serif;
    *font-size:small; /* for IE */
    *font:x-small; /* for IE in quirks mode */
}

/**
 * Nudge down to get to 13px equivalent for these form elements
 */
select,
input,
button,
textarea {
    font:99% arial,helvetica,clean,sans-serif;
}

/**
 * To help tables remember to inherit
 */
table {
    font-size:inherit;
    font:100%;
}

/**
 * Bump up IE to get to 13px equivalent for these fixed-width elements
 */
pre,
code,
kbd,
samp,
tt {
    font-family:monospace;
    *font-size:108%;
    line-height:100%;
}

/* YUI CSS Detection Stamp */
#yui3-css-stamp.cssfonts { display: none; }

下面应该是中国最早的一份CSS Reset

/* KISSY CSS Reset
理念:清除和重置是紧密不可分的
特色:1.适应中文 2.基于最新主流浏览器
维护:玉伯(lifesinger@gmail.com), 正淳(ragecarrier@gmail.com)
*/

/* 清除内外边距 */
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */
dl, dt, dd, ul, ol, li, /* list elements 列表元素 */
pre, /* text formatting elements 文本格式元素 */
fieldset, lengend, button, input, textarea, /* form elements 表单元素 */
th, td { /* table elements 表格元素 */
    margin: 0;
    padding: 0;
}

/* 设置默认字体 */
body,
button, input, select, textarea { /* for ie */
    /*font: 12px/1 Tahoma, Helvetica, Arial, "宋体", sans-serif;*/
    font: 12px/1 Tahoma, Helvetica, Arial, "\5b8b\4f53", sans-serif; 
    /* 用 ascii 字符表示,使得在任何编码下都无问题 */
}

h1 { font-size: 18px; /* 18px / 12px = 1.5 */ }
h2 { font-size: 16px; }
h3 { font-size: 14px; }
h4, h5, h6 { font-size: 100%; }

address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */
code, kbd, pre, samp, tt { font-family: "Courier New", Courier, monospace; } /* 统一等宽字体 */
small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */

/* 重置列表元素 */
ul, ol { list-style: none; }

/* 重置文本格式元素 */
a { text-decoration: none; }
a:hover { text-decoration: underline; }

abbr[title], acronym[title] { /* 注:1.ie6 不支持 abbr; 2.这里用了属性选择符,ie6 下无效果 */
 border-bottom: 1px dotted;
 cursor: help;
}

q:before, q:after { content: ''; }

/* 重置表单元素 */
legend { color: #000; } /* for ie6 */
fieldset, img { border: none; } /* img 搭车:让链接里的 img 无边框 */
/* 注:optgroup 无法扶正 */
button, input, select, textarea {
    font-size: 100%; /* 使得表单元素在 ie 下能继承字体大小 */
}

/* 重置表格元素 */
table {
 border-collapse: collapse;
 border-spacing: 0;
}

/* 重置 hr */
hr {
    border: none;
    height: 1px;
}
/* 让非ie浏览器默认也显示垂直滚动条,防止因滚动条引起的闪烁 */
html { overflow-y: scroll; }

这是玉伯和正淳这两位前辈完成的,向前辈致敬。

仔细观察上面不同时代不同作者的CSS Reset代码可以看出,很多都是采用一刀切的方案,多了很多冗余代码,而且例如h1~h6这种具有语义化的标签,去除默认样式后而又没有设置它们本身语义化该有的样式,导致越来越多人弄不清它们的语义。从另一个角度讲这也是div满天飞,缺乏语义化标签的一个重要原因。

Normalize.css

normalize

只要思想不滑坡,方法总比问题多,所以出现了Normalize.css这样的新方案。

Normalize.css简介

知乎上面的张小核桃这样评价Normalize.css:

CSS Reset 是革命党,CSS Reset 里最激进那一派提倡不管你小子有用没用,通通给我脱了那身衣服,凭什么你 body 出生就穿一圈 margin,凭什么你姓 h 的比别人吃得胖,凭什么你 ul 戴一胳膊珠子。于是 *{margin:0;}等等运动,把人家全拍扁了。看似是众生平等了,实则是浪费了资源又占不到便宜,有求于人家的时候还得贱贱地给加回去,实在需要人家的默认样式了怎么办?人家锅都扔炉子里烧了,自己看着办吧。

Normalize.css是改良派。他们提倡,各个元素都有其存在的道理,简单粗暴地一视同仁是不好的。body 那一圈确实挤压了页面的生存空间,那就改掉。士农工商,谁有谁的作用,给他们制定个规范,确保他们在任何浏览器里都干好自己的活儿。

Normalize.css是国外两位大牛花了几百个小时来研究不同浏览器的默认样式的差异才写出来的,是前辈心血结晶,向前辈致敬。

Normalize.css注释完整,每一段代码都说明了作用,官网中对它的描述如下:

  • Preserves useful defaults, unlike many CSS resets.
  • Normalizes styles for a wide range of elements.
  • Corrects bugs and common browser inconsistencies.
  • Improves usability with subtle modifications.
  • Explains what code does using detailed comments.

Normalize.css没有一刀切,而是注重通用的方案,重置掉该重置的样式,保留该保留的 user agent 样式,同时进行一些bug的修复。相比于传统的CSS Reset,Normalize.css是一种现代的、为HTML5准备的优质替代方案。Normalize.css现在已经被用于Twitter Bootstrap、HTML5 Boilerplate、GOV.UK、Rdio、CSS Tricks 以及许许多多其他框架、工具和网站上。

Github:https://github.com/necolas/normalize.css/

总结

无论是使用CSS Reset还是使用Normalize.css,在我看来,没有最好的只有最适合自己的。

好玩的Canvas:直线(线段)、图形的绘制

Canvas
在Canvas中有直线和曲线两种基本图形。本文将讲述Canvas直线图形的绘制,并详细讲述直线、矩形、多边形这三种常见直线图形的绘制。

Canvas坐标系介绍

W3C坐标系

数学坐标系(笛卡尔坐标系)常在数学中运用,而在前端开发中CSS3、Canvas、SVG等使用的都是W3C坐标系,这一点需要时刻谨记。

  • 数学坐标系:y轴正方向朝上;
  • W3C坐标系:y轴正方向朝下;

绘制直线(线段)

绘制一条线段

在Canvas中,moveTo()和lineTo()这两个方法配合使用来绘制线段。

canvas绘制直线

下面这个示例绘制了上面坐标系中的线段AB,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");
            cxt.moveTo(80,20);
            cxt.lineTo(20,60);
            cxt.stroke();
        }
    </script>
</html>

我们在纸上画一条线段一般都是先找个起始点,然后确定终点,最后连接两个端点,canvas也是这样。

首先使用getContext('2d')获取上下文环境对象context。然后使用moveTo()将画笔移到坐标A(80,20)moveTo()的含义是“将画笔移到该点(x1, y1)位置上,然后开始绘图”。再使用lineTo()将画笔从起点A画到终点坐标B(20,60)

如果是在纸上画,这样就已经结束了,但是在Canvas中,moveTo()lineTo()仅仅是确定线段的“起点坐标”和“终点坐标”这两个状态,但是实际上画笔还没开始“动”。因此我们还需要调用上下文对象的stroke()方法保存状态。

绘制多条线段

使用moveTo()lineTo()可以画一条线段,如果我们要同时画多条直线,也是使用这两个方法。lineTo()方法是可以重复使用的,第一次使用lineTo()后,画笔将自动移到终点坐标位置,第二次使用lineTo()后,Canvas会以“上一个终点坐标”作为第二次绘制的起点坐标,然后再开始画线段,以此类推。如果第二条直线需要用一个新的起点,使用moveTo()方法移动画笔即可。
Canvas绘制多条直线

下面是一个示例,绘制上面图片里的三个线段,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");
            cxt.moveTo(80,20);
            cxt.lineTo(20,60);
            cxt.lineTo(80,120);
            cxt.moveTo(20,80);
            cxt.lineTo(80,140)
            cxt.stroke();
        }
    </script>
</html>

所以如果使用Canvas绘制三角形、四边形等多边形就很简单了,绘制首尾相连的多边形即可,下面是一个绘制正方向的示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");
            cxt.moveTo(20,20);
            cxt.lineTo(20,60);
            cxt.lineTo(60,60);
            cxt.lineTo(60,20);
            cxt.lineTo(20,20);
            cxt.stroke();
        }
    </script>
</html>

效果如下:

canvas绘制正方形

Canvas中的矩形绘制

前面使用moveTo()lineTo()这两个方法的配合使用绘制了一个正方形。通过画4条线段来绘制正方形代码过于繁琐,因此在实际开发中一般不采用前面的方法绘制矩形/正方形。

在Canvas中,矩形分为描边矩形和填充矩形这两种,而且提供了单独的方法,直接调用即可。

描边矩形

strokeStyle属性和strokeRect()方法配合使用来绘制描边矩形。strokeStyle是context对象的一个属性,strokeRect()是content对象的一个方法。

strokeStyle属性值

cxt.strokeStyle = "属性值";

strokeStyle属性取值有三种,即颜色值、渐变色、图案。
颜色值支持十六进制颜色值、颜色关键字、rgb颜色值、rgba颜色值。颜色值默认为#000000。渐变色和图案暂时不说。

strokeRect()方法

cxt.strokeRect(x,y,width,height);

这段代码对应下图所示:

image

注意
strokeStyle属性必须在strokeRect()方法之前定义,否则strokeStyle属性无效。

下面用strokeStylestrokeRect重新绘制前面的正方形,并设置边为绿色,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");
            cxt.strokeStyle="green";
            cxt.strokeRect(20,20,40,40);
        }
    </script>
</html>

效果如下:

image

填充矩形

在Canvas中,用fillStyle属性和fillRect()方法配合使用来画填充矩形。

fillStyle属性值

fillStyle属性跟strokeStyle属性一样,取值也有颜色值、渐变色、图案。

cxt.fillStyle="属性值"

fillRect()方法

cxt.fillRect(x,y,width,height);

fillRect()方法和strokeRect()的各参数意思一样。

注意:fillStyle必须在fillRect()之前设置。
下面将前面的正方形用填充矩形重新绘制,填充色为绿色,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");
            cxt.fillStyle="green";
            cxt.fillRect(20,20,40,40);
        }
    </script>
</html>

效果如下:

image

当然填充矩形和描边矩形也可以一起使用,下面是一个代码示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");

            cxt.fillStyle="black";
            cxt.fillRect(20,20,40,40);

            cxt.fillStyle="green";
            cxt.fillRect(20,20,40,40);

            cxt.fillStyle="black";
            cxt.fillRect(50,50,40,40);
        }
    </script>
</html>

效果如下:

image

rect()方法直接绘制矩形

在Canvas,除了使用strokeRect()fillRect()可以绘制矩形外,使用rect()方法也可以绘制矩形。

cxt.rect(x,y,width,height);

rect()+stroke()

cxt.strokeStyle = "green";
cxt.rect(20,20,40,40);
cxt.stroke();

效果等同于:

cxt.strokeStyle="green";
cxt.strokeRect(20,20,40,40);

rect()+fill()

cxt.fillStyle = "green";
cxt.rect(20,20,40,40);
cxt.fill();

效果等同于:

cxt.fillStyle="green";
cxt.fillRect(20,20,40,40);

clearRect()方法清空矩形区域

在Canvas中,clearRect()方法用来清空矩形区域。

cxt.clearRect(x,y,width,height);

一个简单示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");

            cxt.fillStyle="black";
            cxt.fillRect(20,20,40,40);

            cxt.fillStyle="green";
            cxt.fillRect(20,20,40,40);

            cxt.fillStyle="black";
            cxt.fillRect(50,50,40,40);

            cxt.clearRect(25,25,15,15);
        }
    </script>
</html>

效果如下:
image

cxt.clearRect(0, 0, canvas.width, canvas.height);

上面这段代码用于清空整个Canvas画布。canvas.width获取Canvas画布宽度,canvas.height表示获取Canvas画布的高度(canvas是指上述代码示例中用document.getElementById(id)获取的canvas对象)。

Canvas绘制正多变形

前面绘制了正方形,理论上任意正多边形都可以绘制出来,
正多边形绘制推导

依次类推,可以推导正n边形各坐标点,下面是代码示例:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Canvas绘制直线</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800" style="border: 0.0625rem;"></canvas>
    </body>
    <script>
        function get_dom(id){
            return document.getElementById(id);
        }
        // 绘制多边形函数
        // cxt : context上下文对象,
        // x0,y0 : 正多边形中心坐标点,即(x0,y0)
        // n :正n边形
        // size: 边长
        function create_n(cxt,x0,y0,n,size){
            cxt.beginPath();
            var jd = 2*Math.PI/n;
            for (var i=0;i<n;i++) {
                var x = Math.cos(i*jd)*size+x0;
                var y = Math.sin(i*jd)*size+y0;
                console.log(x,y)
                cxt.lineTo(x,y);
            }
            cxt.closePath();
        }
        window.onload = function(){
            var canvas = get_dom("canvas");
            var cxt = canvas.getContext("2d");
            // 绘制中心点O(100,100),边长为50的正八边形
            create_n(cxt,100,100,8,50);
            cxt.stroke();
        }
    </script>
</html>

效果如下:
image

同理还可以用这些绘制五角星、六芒星等,绘制之前在纸上计算清楚。

总结

利用Canvas可以绘制各种常见的几何图形,可以深入学习。

我爬取了人人都是产品经理6574篇文章,发现产品竟然在看这些

作为互联网界的两个对立的物种,产品汪与程序猿似乎就像一对天生的死对头;但是在产品开发链条上紧密合作的双方,只有通力合作,才能更好地推动项目发展。那么产品经理平日里面都在看那些文章呢?我们程序猿该如何投其所好呢?我爬取了人人都是产品经理(http://www.woshipm.com)产品经理栏目下的所有文章,看看产品经理都喜欢看什么

产品经理与程序员

1.分析背景

1.1. 为什么选择「人人都是产品经理」

人人都是产品经理是以产品经理、运营为核心的学习、交流、分享平台,集媒体、培训、招聘、社群为一体,全方位服务产品人和运营人,成立8年举办在线讲座500+期,线下分享会300+场,产品经理大会、运营大会20+场,覆盖北上广深杭成都等15个城市,在行业有较高的影响力和知名度。平台聚集了众多BAT美团京东滴滴360小米网易等知名互联网公司产品总监和运营总监。选取这个社区更有代表性。

1.2. 分析内容

  • 分析人人都是产品经理产品经理栏目下的 6574 篇文章的基本情况,包括收藏数、评论数、点赞数等
  • 发掘最受欢迎的文章及作者
  • 分析文章标题长度与受欢迎程度之间的关系
  • 展现产品经理都在看什么

1.3. 分析工具

  • Python 3.6
  • Matplotlib
  • WordCloud
  • Jieba

2. 数据抓取

使用 Python编写的爬虫抓取了人人都是产品经理社区的产品经理栏目下的所有文章并保存为csv格式,文章抓取时期为 2012年6月至 2019 年 1月 21日,共计6574篇文章。抓取 了 10个字段信息:文章标题、作者、作者简介、发文时间、浏览量、收藏量、点赞量、评论量、正文、文章链接。

2.1. 目标网站分析

这是要爬取的网页界面,可以看到是直接加载出来的,没有AJAX,爬取起来毫无难度。

人人都是产品经理

仔细观察要爬取的网页,我们可以看到页面连接有规律可循,连接中page后面的参数就是页面数,所以我们编写爬虫时可以直接用for循环来构造所有页面连接代码如下:

import requests
from bs4 import BeautifulSoup
import csv

headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
               'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
               'Cache-Control': 'max-age=0',
               'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
               'Connection': 'keep-alive',
               'Host': 'www.woshipm.com',
               'Cookie' : 't=MHpOYzlnMmp6dkFJTEVmS3pDeldrSWRTazlBOXpkRjBzRXpZOU4yVkNZWWl5QVhMVXBjMU5WcnpwQ2NCQS90ZkVsZ3lTU2Z0T3puVVZFWFRFOXR1TnVrbUV2UFlsQWxuemY4NG1wWFRYMENVdDRPQ1psK0NFZGJDZ0lsN3BQZmo%3D; s=Njg4NDkxLCwxNTQyMTk0MTEzMDI5LCxodHRwczovL3N0YXRpYy53b3NoaXBtLmNvbS9XWF9VXzIwMTgwNV8yMDE4MDUyMjE2MTcxN180OTQ0LmpwZz9pbWFnZVZpZXcyLzIvdy84MCwsJUU1JUE0JUE3JUU4JTk5JUJF; Hm_lvt_b85cbcc76e92e3fd79be8f2fed0f504f=1547467553,1547544101,1547874937,1547952696; Hm_lpvt_b85cbcc76e92e3fd79be8f2fed0f504f=1547953708'
               }
with open('data.csv', 'w', encoding='utf-8',newline='') as csvfile:
    fieldnames = ['title', 'author', 'author_des', 'date', 'views', 'loves', 'zans', 'comment_num','art', 'url']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    writer.writeheader()
    for page_number in range(1, 549):
        page_url = "http://www.woshipm.com/category/pmd/page/{}".format(page_number)
        print('正在抓取第' + str(page_number) + '页>>>')
        response = requests.get(url=page_url, headers=headers)
        if response.status_code == 200:
            page_data = response.text

页面连链接构造完之后我们可以开始爬取文章详情页,提取所需要的信息,在这里用到的解析库是BeautifulSoup,整个爬虫非常简单,完整代码如下:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#author:PM 小凯哥<xuxinkai.cn>

import requests
from bs4 import BeautifulSoup
import csv

headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
               'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
               'Cache-Control': 'max-age=0',
               'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
               'Connection': 'keep-alive',
               'Host': 'www.woshipm.com',
               'Cookie' : 't=MHpOYzlnMmp6dkFJTEVmS3pDeldrSWRTazlBOXpkRjBzRXpZOU4yVkNZWWl5QVhMVXBjMU5WcnpwQ2NCQS90ZkVsZ3lTU2Z0T3puVVZFWFRFOXR1TnVrbUV2UFlsQWxuemY4NG1wWFRYMENVdDRPQ1psK0NFZGJDZ0lsN3BQZmo%3D; s=Njg4NDkxLCwxNTQyMTk0MTEzMDI5LCxodHRwczovL3N0YXRpYy53b3NoaXBtLmNvbS9XWF9VXzIwMTgwNV8yMDE4MDUyMjE2MTcxN180OTQ0LmpwZz9pbWFnZVZpZXcyLzIvdy84MCwsJUU1JUE0JUE3JUU4JTk5JUJF; Hm_lvt_b85cbcc76e92e3fd79be8f2fed0f504f=1547467553,1547544101,1547874937,1547952696; Hm_lpvt_b85cbcc76e92e3fd79be8f2fed0f504f=1547953708'
               }
with open('data.csv', 'w', encoding='utf-8',newline='') as csvfile:
    fieldnames = ['title', 'author', 'author_des', 'date', 'views', 'loves', 'zans', 'comment_num','art', 'url']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    writer.writeheader()
    for page_number in range(1, 549):
        page_url = "http://www.woshipm.com/category/pmd/page/{}".format(page_number)
        print('正在抓取第' + str(page_number) + '页>>>')
        response = requests.get(url=page_url, headers=headers)
        if response.status_code == 200:
            page_data = response.text
            if page_data:
                soup = BeautifulSoup(page_data, 'lxml')
                article_urls = soup.find_all("h2", class_="post-title")
                for item in article_urls:

                    url = item.find('a').get('href')
                    # 文章页面解析,获取文章标题、作者、作者简介、日期、浏览量、收藏量、点赞量、评论量、正文、文章链接
                    response = requests.get(url=url, headers=headers)
                    # time.sleep(3)
                    print('正在抓取:' + url)
                    # print(response.status_code)
                    if response.status_code == 200:
                        article = response.text
                        # print(article)
                        if article:
                            try:
                                soup = BeautifulSoup(article, 'lxml')
                                # 文章标题
                                title = soup.find(class_='article-title').get_text().strip()
                                # 作者
                                author = soup.find(class_='post-meta-items').find_previous_siblings()[1].find('a').get_text().strip()
                                # 作者简介
                                author_des = soup.find(class_='post-meta-items').find_previous_siblings()[0].get_text().strip()
                                # 日期
                                date = soup.find(class_='post-meta-items').find_all(class_='post-meta-item')[0].get_text().strip()
                                # 浏览量
                                views = soup.find(class_='post-meta-items').find_all(class_='post-meta-item')[1].get_text().strip()
                                # 收藏量
                                loves = soup.find(class_='post-meta-items').find_all(class_='post-meta-item')[2].get_text().strip()
                                # 点赞量
                                zans = soup.find(class_='post-meta-items').find_all(class_='post-meta-item')[3].get_text().strip()
                                # 评论量
                                comment = soup.find('ol', class_="comment-list").find_all('li')
                                comment_num = len(comment)
                                # 正文
                                art = soup.find(class_="grap").get_text().strip()

                                writer.writerow({'title':title, 'author':author, 'author_des':author_des, 'date':date, 'views':views, 'loves':int(loves), 'zans':int(zans), 'comment_num':int(comment_num), 'art':art, 'url':url})
                                print({'title':title, 'author':author, 'author_des':author_des, 'date':date, 'views':views, 'loves':loves, 'zans':zans, 'comment_num':comment_num})
                            except:
                                print('抓取失败')
    print("抓取完毕!")

在这里说一点,评论数的爬取,观察文章详情页你可以发现并没有评论数,我这里是直接计算出来的,可以看到评论是嵌套在ol里面,抓起所有的li,然后就可以计算出,代码如下:

 # 评论量
comment = soup.find('ol', class_="comment-list").find_all('li')
comment_num = len(comment)

这样,我们运行一下爬虫就能够顺利爬取594页的结果了,我这里一共抓取了6574条结果,大概也就玩了两把吃鸡就抓完了。

以上,就完成了数据的获取。有了数据我们就可以着手分析,不过这之前还需简单地进行一下数据的清洗、处理。

3. 数据清洗处理

首先,我们需要把csv文件转换为 DataFrame。

# 将csv数据转为dataframe
csv_file = "data.csv"
csv_data = pd.read_csv(csv_file, low_memory=False)  # 防止弹出警告
csv_df = pd.DataFrame(csv_data)

下面我们看一下数据的总体情况,可以看到数据的维度是6574行×10 列。需要将views列更改为数值格式、date列更改为日期格式。

date列更改为日期非常简单,代码如下:

# 修改date列时间,并转换为 datetime 格式
csv_df['date'] = pd.to_datetime(csv_df['date'])

views列处理思路是增加一列,名字就叫views_num吧,我们可以观察到views列有的数值是整数,有的则是1.7万这种,代码如下:
image

我们再输出一下看看各列数据类型:
image

可以看到数据类型已经变成我们想要的了,下面,我们看一下数据是否有重复,如果有,那么需要删除。
image

然后,我们再增加两列数据,一列是文章标题长度列,一列是年份列,便于后面进行分析。
image

以上,就完成了基本的数据清洗处理过程,针对这些数据可以开始进行分析了。

4. 描述性数据分析

通常,数据分析主要分为四类:「描述型分析」、「诊断型分析」「预测型分析」「规范型分析」。「描述型分析」是用来概括、表述事物整体状况以及事物间关联、类属关系的统计方法,是这四类中最为常见的数据分析类型。通过统计处理可以简洁地用几个统计值来表示一组数据地集中性(如平均值、中位数和众数等)和离散型(反映数据的波动性大小,如方差、标准差等)。

这里,我们主要进行描述性分析,数据主要为数值型数据(包括离散型变量和连续型变量)和文本数据。

4.1. 总体情况

先来看一下总体情况,使用了data.describe() 方法对数值型变量进行统计分析。
image

mean 表示平均值,std表示标准差,从上面可以简要得出以下几个结论:

  • 产品经理热爱学习,看到好的文章就收藏下来。75%的文章收藏量破百,50%的文章浏览量破百;
  • 产品话少,对别人的文章很少会评头论足。文章的评论数都寥寥无几。
  • 产品不愿意承认别人比自己优秀。绝大部分文章点赞数都是一二十个,所以程序猿们以后不要在产品面前吹嘘技术如何了得了,产品是不会承认你厉害的。

对于非数值型变量(author、date),使用 describe() 方法会产生另外一种汇总统计。
image

unique 表示唯一值数量,top表示出现次数最多的变量,freq表示该变量出现的次数,所以可以简单得出以下几个结论:

  • 一共有1531位作者为社区的产品经理栏目贡献了文章,其中贡献量最大的作者叫 Nairo ,贡献了315篇;
  • 在2015年1月29日栏目文章发布数最大,达到了16篇。栏目第一篇文章发布在2012年11月25日。

4.2. 不同时期文章发布的数量变化

image
从图中可以看到,网站文章发布数量在2012到2015年逐年递增,增幅很大,这可能与网站的知名度提高有关;2015年2季度之后比较平稳。后面的分析代码就不一一贴出,文末会留下代码下载链接。

4.3. 文章浏览量 TOP10

接下来,到了我们比较关心的问题:几万篇文章里,到底哪些文章写得比较好或者比较火
image

这里以阅读量作为衡量标准,排在第一的是《小白产品经理看产品:什么是互联网产品》,第一名的浏览量遥遥领先于第二名,接近百万,看来很多社区里面很多都是产品小白。而且看这几篇文章标题,貌似都是介绍什么是产品经理,产品经理干什么,看来社区里面初级产品挺多的。

4.4. 历年文章收藏量 TOP3

在了解文章的总体排名之后,我们来看看历年的文章排名是怎样的。这里,每年选取了收藏量最多的 3 篇文章。
image
image

从图中可以看出,2015年是的那篇文章收藏量是最多的,达到了2000,文章内容则是后台产品设计,看来这篇文章里面干货满满。

4.4.1. 最高产作者 TOP20

上面,我们从收藏量指标进行了分析,下面,我们关注一下发布文章的作者。前面提到发文最多的是Nairo,贡献了315篇,这里我们看看还有哪些比较高产的作者。
image

可以看到第一名遥遥领先,是个狼人,大家可以关注一下这些优质作者。

4.4.2. 平均文章收藏量最多作者 TOP 10

我们关注一个作者除了是因为文章高产以外,可能更看重的是其文章水准。这里我们选择「文章平均收藏量」(总收藏量/文章数)这个指标,来看看文章水准比较高的作者是哪些人。这里,为了避免出现「某作者只写了一篇高收藏率的文章」这种不能代表其真实水准的情况,我们将筛选范围定在至少发布过 5 篇文章的作者们。
image

对比这张图和前面的发文数量排行榜,我们可以发现这张图的作者均没有上榜,相比于数量,质量可能更重要吧。

4.5. 文章评论数最多 TOP10

说完了收藏量。下面,我们再来看看评论数量最多的文章是哪些。
image

我们可以看到大部分都与初级产品有关,而且我们可以看到评论多,收藏量也挺多的,我们进一步探寻两者之间关系。

image

我们可以发现绝大部分文章评论数和收藏量都很小。

4.6. 文章标题长度

下面,我们再来看看文章标题的长度和阅读量之间有没有什么关系。
image
我们可以看到文章标题长度在20左右时阅读量普遍较高。

4.7. 文本分析

最后,我们从这 5 万篇文章中的正文内容中看看产品经理都在看什么。
image
我们可以看到设计、工作、数据、功能、需求、项目等都是产品经理们关注的东西,产品们也很辛苦啊,程序猿以后不要吐槽自己多辛苦了。

  1. 小结
    • 本文简要分析了人人都是产品经理产品经理栏目下6574篇文章信息,大致了解了产品经理都在看什么;
    • 发掘了那些优秀的文章和作者,能够产品新人指明方向;
    • 告诉了程序猿与产品经理聊天时该说什么;
    • 本文尚未做深入的文本挖掘,而文本挖掘可能比数据挖掘涵盖的信息量更大,更有价值。进行这些分析需要机器学习和深度学习的知识。

参考资料:

  • 搜狗细胞词库;

本文源代码下载地址:
点击下载

本文首发于2019年1月份,现迁移到博客。

重学前端:HTML元素嵌套关系及HTML5内容模型

重学前端系列

HTML元素众多,各种标签相互包裹,或者称之为嵌套都存在一些规则,本文讲述一下常见的html元素嵌套关系。

HTML元素嵌套关系

  • 块级元素可以包含行内元素;
  • 块级元素不一定能包含块级元素:例如p标签里面不能包含注入div之类的块级元素;
  • 行内元素一般不能包含块级元素:例如行内元素span里面不能包含块级元素div,行内元素a可以包含块级元素div

为什么行内元素a可以包含块级元素div

在HTML4和XHTML中规定了行内元素不能包含块级元素,所以按照HTML4和XHTML的规范,a包含div是不合法的。
但是但时主流浏览器都支持a包含块级元素,因为这个使用场景很常见。

在HTML5中对这个做了合法化,HTML5中对元素分类重新做了规定,由HTML4的按默认样式分(行内元素、块级元素、行内块级元素)转变成按元素相似特征分。

每个类别的元素有相似的内容模型,并且它们的内容必须遵照规则。现在主要有7个类别,其实总共有15个类别。

HTML5内容模型

https://html.spec.whatwg.org/multipage/indices.html#element-content-categories
类型 描述
元数据型(Metadata) 设置展示、行为、关联文档,或其他内容的元数据的元素
区块型(Sectioning) 定义区块内容范围的元素
标题型(Heading) 定义区块内容标题的元素
文档流型(Flow) 大部分文档body内的元素
语句型(Phrasing) 文档里的文字,还有在段落内标记这些文字的元素。一系列的语句型内容构成段落。(注意大部分语句型内容只能包含也是语句型内容的元素,不能包含文档流型元素)
内嵌型(Embedded) 在文档引入另一个资源的元素,或者插入文档的另一种语言,比如在HTML文档里的MathML
交互型(Interactive) 专门用于用户交互的元素

这里主要介绍了7个类别,7个类别也没有完全覆盖所有元素的所有情况。例如,元素可能不属于任何一个类别(比如html元素),或者被称为“穿透的”,即它的类别继承自父元素。很多元素属于不止一个类别。在这种情况下,它们被称为有混合内容的模型。

a内容模型

a标签内容模型

https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element
https://html.spec.whatwg.org/multipage/dom.html#transparent

上图可以看到atransparent内容模型,transparent是透明的意思,在计算内容模型的时候,这个元素本身不参与计算,也就是计算合法性的时候,a相当于不存在。

下面一个是示例:

<div><a href="">div > a </a></div>
<a href=""><div> a %gt; div </div></a>
<p><a href=""><div> p > a > div </div></a></p>
  • 上面的div>a是合法的;
  • a>div也是合法的;
  • p>a>div是不合法的,计算内容模型时,a不参与计算,此时是p>div,这是不合法的。

p标签

https://html.spec.whatwg.org/multipage/grouping-content.html#grouping-content

从上图可以看出p可包含的元素没有div,当采用这种错误写法时,浏览器的容错机制就会启动,让这段标签变得混乱不堪,具体可以运行上面那段代码,然后检查。

重学前端:HTML元素分类之块级、行内、行内块级元素

重学前端系列
说到HTML分类,其实HTML分类标准有很多种。本文介绍按默认样式分类。

块级元素 block

顾名思义,所谓的块级元素就是占据一块区域,占据整行,有自己的宽高等尺寸。

块级元素的特点:

  • 块级元素会独占一行;
  • 高度,行高,外边距和内边距都可以单独设置;
  • 宽度默认是容器的100%;
  • 可以容纳内联元素和其他的块级元素。

块级元素列表:

 <address>//定义地址 
 <article> //文章内容
 <aside> //伴随内容
 <caption>//定义表格标题 
 <dd>    //定义列表中定义条目 
 <div>     //定义文档中的分区或节 
 <dl>    //定义列表 
 <dt>     //定义列表中的项目
 <figcaption> //图文信息组标题
 <fieldset> //定义一个框架集 
 <form> //创建 HTML 表单
 <figure> //图文信息组
 <h1>    //定义最大的标题
 <h2>    // 定义副标题
 <h3>     //定义标题
 <h4>     //定义标题
 <h5>     //定义标题
 <h6>     //定义最小的标题
 <hr>     //创建一条水平线
 <legend>    //元素为 fieldset 元素定义标题
 <li>     //标签定义列表项目
 <noframes>    //为那些不支持框架的浏览器显示文本,于 frameset 元素内部
 <noscript>    //定义在脚本未被执行时的替代内容
 <ol>     //定义有序列表
 <ul>    //定义无序列表
 <p>     //标签定义段落
 <pre>     //定义预格式化的文本
 <table>     //标签定义 HTML 表格
 <tbody>     //标签表格主体(正文)
 <td>    //表格中的标准单元格
 <tfoot>     //定义表格的页脚(脚注或表注)
 <th>    //定义表头单元格
 <thead>    //标签定义表格的表头
 <tr>     //定义表格中的行
 <output> //表单输出
 <audio> //音频播放
 <footer> //区段尾或页尾 <blockquote> //块引用
 <canvas> //绘制图形
 <section> //一个页面区段
 <header> //区段头或页头
 <hgroup> //标题组
 <video> //视频
MDN 参考链接:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Block-level_elements

行内元素 inline

inline有时也被称为内联元素,这种元素不一定有规则形状,也不会独占一行,会与其他元素和谐的挤在一起,没有自己的宽高,仅仅依靠自己的字体大小或者是图像大小来支撑结构,不能设置宽高对齐等属性。

行内元素的特点:

  • 和相邻的行内元素在一行上;
  • 高度和宽度无效,但是水平方向上的padding和margin可以设置,垂直方向上的无效;
  • 默认的宽度就是它本身的宽度;
  • 行内元素只能容纳纯文本或者是其他的行内元素(a标签除外)。

注意事项:

  • 只有文字才能组成段落,因此类似<p>,<h1>~<h6>,<dt>等里面不能放块级元素,因为它们都是文字块级标签,里面不能再存放其他的块级标签;
  • 链接里面不能再存放链接。

行内元素列表:

b, big, i, small, tt
abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var
a, bdo, br, img, map, object, q, script, span, sub, sup
button, input, label, select, textarea
MDN 参考链接:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Inline_elements

行内块级元素 inline-block

这种元素也被称为内联块级元素,和inline元素一样与其他元素堆在一行内,和谐共处,对外表现看起来像一个inline元素,对内表现为block,可以设置宽高等尺寸。也可以在一行中放置多个行内块级元素.

行内块级元素的特点:

  • 和相邻的行内元素(行内块)在一行上,但是中间会有空白的间隙;
  • 默认的宽度就是本身内容的宽度;
  • 高度,行高,内边距和外边距都可以设置。

注意事项:
在行内块级元素中有几个特殊的标签,<img/>,<input/>,<td/>,可以设置它们的宽度高度以及对齐属性.

元素类型转换display

  • display:block;定义元素为块级元素;
  • display : inline;定义元素为行内元素;
  • display:inline-block;定义元素为行内块级元素。

总结

不管块级元素还是行内元素,区别主要是三个方面:

  • 排列方式:块级元素会独占一行,而内联元素和内联块元素则会在一行内显示;
  • 宽高边距设置:块级元素和内联块级元素可以设置 width、height属性,而内联元素设置无效;
  • 默认宽度:块级元素的 width 默认为100%,而内联元素则是根据其自身的内容或子元素来决定其宽度。

重学前端:深入理解header、footer、main、address标签

页面结构

header标签

header标签通常用来放置整个页面或页面内的一个内容区块的标题,但也可以包含其他内容,例如数据表格、搜索表单或相关的LOGO图片。

一个web页面内可以拥有多个header标签,一个header标签中可以包裹多个h1~h6标签,也可以再包含一个或多个header标签或多个footer标签,也可以包括table、form、nav之类的标签。

footer标签

footer标签可以作为其父级内容区块的脚注。footer通常包括其相关区块的脚注信息,如作者、相关阅读链接以及版权信息等。
一个web页面中也不限制只可以用一个footer标签。同时,可以为article元素或section元素添加footer标签。

main标签

main标签表示网页中与网页标题或应用程序中本页面主要功能直接相关或进行扩展的内容。

  • 该区域应该为每一个网页中所特有的内容,不能包含整个网站的导航条、版权信息、网站LOGO、公共搜索表单等整个网站内部的共同内容。
  • 每个网页内部只能放置一个main标签。
  • 不能将main标签放置在任何article、aside、footer、header或nav标签内部。
  • 由于main标签不对页面内容进行分区或分块,所以不会对所要描述的网页大纲产生任何影响。

address标签

address标签用来在web页面中呈现诸如作者的网站链接、电子邮箱、真实地址、电话号码等。address应该不只是用来呈现电子邮箱或真实地址,还可以用来展示跟页面相关的联系人的所有联系信息。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>article_demo</title>
    </head>
    <body>
        <header>
            <header>
                <h1>PM 小凯哥博客</h1>
            </header>
            <nav>
                <ul>
                    <li>首页</li>
                    <li>代码</li>
                    <li>产品</li>
                </ul>
            </nav>
        </header>
        <main>
            <article>
                <header>
                    <h1>标题</h1>
                    <p>发布日期:<time pubdate="true">2020-03-09</time></p>
                </header>
                <p>此处是正文内容</p>
                <aside>
                    <!-- aside在article内部,说明aside的内容是与文章相关的,例如参考文献 -->
                    <h1>参考文献</h1>
                    <ul>
                        <li>参考小凯哥博客</li>
                    </ul>
                </aside>
                <footer>
                    <address>
                        <a href="https://xuxinkai.cn">PM 小凯哥</a>
                    </address>
                    <p><small>此处是脚注,例如:PM 小凯哥<xuxinkai.cn>版权所有</small></p>
                </footer>
            </article>
        </main>
        <aside>
            <nav>
                <h2>热门文章</h2>
                <ul>
                    <li>产品经理真牛逼</li>
                    <li>前端真厉害</li>
                    <li>程序员真辛苦</li>
                </ul>
            </nav>
        </aside>
        <aside>
            <h2>友情链接</h2>
            <address>
                <a href="">PM 小凯哥</a>
                <a href="">阮一峰</a>
            </address>
        </aside>
    </body>
</html>