精华内容
下载资源
问答
  • 2019-07-12 09:14:25

    在上一篇文章中,我总结了一下HTML,这一篇我把CSS简单做一下归纳,使网页变得富有美感。

    一.CSS样式的基本知识

    1.关于注释:

    /*注释内容*/

    2.最常见的css样式格式——嵌入式

       例如对span里的内容的字改为蓝色:

    <style type=text/css>
      span{
                color:blue;
          }   
    </style>

    3.介绍两种选择器:类选择器和ID选择器

     (1)类选择器

              .类选择器名字{css样式代码;}

              举例:在body部分这样写:

    <span class="happy">你好</span>
    

    在style部分这样写:

    .happy{color:pink;}/*你好的颜色变成了粉色*/

        (2)ID选择器

            #ID选择器{css样式代码;}
            与类选择器一样,只不过class变成ID。

    注意点:类选择器可以使用很多次,很多个地方class等于一样都没问题,但是ID选择器只能用一次。

    4.子选择器和包含(后代)选择器

       直接举例:

     <html>     
            <style>
                .hello>li{
                    border:1px solid blue;
                }
            </style>
        </head>
        <body>
            <ul class="hello">
                <li>hello world!
                    <ul>
                        <li>HELLO WORLD!</li>
                        <li>HELLO WORLD!</li>
                    </ul>
                </li>
            </ul>
        </body>   
    </html>

        显示效果:

    上面是子选择器,接下来我把style部分改成一下内容,其他不变:

    .hello li{
                    border:1px solid blue;
                }

    显示效果:

    总结:>作用于元素的第一代后代,空格作用于元素的所有后代。

    5.通用选择器:*{里面填格式}

       它作用于html中的所有标签元素。

    6.伪类选择器:标签:hover{里面填格式}

       它是指鼠标滑过这段字的时候会出现的效果。

    二.CSS格式化排版

    我把下面分为字体排版和段落排版,简单综合一下。

    1.字体排版

    (1)字体:font-family

    (2)字号:font-size(用像素表示)

    (3)颜色:color

    (4)粗体:font-weight:bold

    (5)斜体:font-style:italic

    (6)下划线:text-decoration:underline

    (7)删除线:text-decoration:line-through

    2.段落排版

    (1)首行缩进两个字符:text-indent:2em

    (2)行高:line-height

    (3)字间隔:letter-spacing(如果是英文,则表示字母与字母之间的间隔)

                           word-spacing(单词时间的间隔)

    (4)居中:text-align:center;

             右对齐:text-align:right;

             左对齐:text-align:left;

    三.盒子模型的介绍

    1.在介绍盒子模型之前,先了解一下html中的标签分类。

    (1)常见的块状元素:<div>、<p>、<h1>...<h6>、<ol>、<ul>、、<dl>、<table>、<address>、<blockquote>、<form>

    (2)常见的内联元素:<a>、<span>、<br>、<i>、<em>、<strong>、<label>、<q>、、<var>、<cite>、<code>

    (3)常见的内联块状元素:<img>、<input>

    一般情况下,块状元素都具备盒子模型的特征。

    2.盒子模型的边框

    (1)粗细:border-width(大多用像素表示)

    (2)边框样式:border-style(dashed虚线、dotted点线、solid实线)

    (3)颜色:border-color

    (4)可以只设置一边:border-right、border-left、border-bottom、border-top

    3.盒子模型——填充

      元素内容与边框之间的距离就是填充用padding表示

    4.盒子模型——边界

       一个盒子模型和另一个盒子模型之间的距离就是边界用margin表示

    四.实例介绍

    在第三大点中介绍了盒子模型,接下来这个例子也会用到盒子模型,以及上面提到的一些样式的运用,在我上一篇博客介绍到的例子基础之上进行下一步的美化,其基本功能没有改变。但是我们可以看到,通过盒子模型我们把这个表单居中了,通过对字体、颜色、背景的改变使网页更加的富有美感。

    代码如下:

    <html>
        <head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style type="text/css">
                div {
    
                    font-weight:bold;    
                    font-family: Microsoft Yahei;
                    width: 400px;
                    padding-left: 50px;
                    margin-left: 450px;
                }
    
                h1 {
                    text-align: center;
                }
    
                #sub {
                    background-color: #689;
                    width: 250px;
                    height: 30px;
                    color:white;
                    font-weight: bold;
                }
                body {
                    background-image: url("http://pic.qiantucdn.com/58pic/20/13/88/25r58PICqQa_1024.jpg");
                }
            </style>
        </head>
        <body>
            <div id="sign">
                <h1>Sigh Up</h1></br>
                <h2>Your basic info</h2>
                <form>
                    <strong>Name:</strong>
                    <input type="text" name="myname" id="name"/></br>
                    </br>
    
                    <strong>Passward:</strong>
                    <input type="password" name="mypassward" id="passward"/></br>
                    </br>
    
                    <strong>Age:</strong></br>
                    <input type="radio" name="age" value="1" checked="checked"/>Under 13</br>
                    <input type="radio" name="age" value="2" />13 or older</br>
                    </br>
    
                    <strong>Your profile:</strong></br>
                    <textarea cols="50" rows="4" name="profile" id="profile"></textarea></br>
                    </br>
    
                    <strong>Job Role:</strong></br>
                    <select>
                        <option value="Front-End Developer" selected="selected" name="job">Front-End Developer</option>
                        <option value="Back-End Developer" name="job">Back-End Developer</option>
                    </select></br>
                    </br>
    
                    <strong>Interests:</strong></br>
                    <input type="checkbox" name="development" value="1" checked="checked">Development</br>
                    <input type="checkbox" name="design" value="2" >Design</br>
                    <input type="checkbox" name="business" value="3" >Business</br>
                    </br>
    
                    <input type="submit" name="Sign Up" value="Sign Up" id="sub" onclick="abc()">
                    </input>
                </form>
            </div>
        </body>   
    </html>
    
    

    代码运行后的效果图:

      

     

     





     

        

    更多相关内容
  • 全文共计十八个章节并附带三笔试练习篇,美中不足的是,第一章和第章是以截图形式展现的。由于本人一开始是在有道云笔记上写的初稿,当时想方便省事(有道云排版个人感觉确实比较美观)就直接以截图的形式完成了...

    前言:

    就在前几天,C语言入门到进阶部分的专栏——《维生素C语言》终于完成了。全文共计十八个章节并附带三张笔试练习篇,美中不足的是,第一章和第二章是以截图形式展现的。由于本人一开始是在有道云笔记上写的初稿,当时想方便省事(有道云排版个人感觉确实比较美观)就直接以截图的形式完成了第一章和第二章。本人考虑到因为是截图,不能复制文中出现的代码,不方便读者进行复制粘贴,所以我打算重新写一下第一章和第一章的内容,并且重新进行了排版。

    本章从如何写主函数开始讲起,对C语言基础知识点进行蜻蜓点水地介绍,想看详细讲解教学请订阅专栏!专栏连接:《维生素C语言》

    图标说明:

    ❓ 表示问题

    📚 表示知识点

    ❕ 表示注意

    🔍 表示比较

    🔨 表示操作/解决方案

    💬 代码块

    💭 思考/思考题

    🚩 运行代码

    ✅ 正确

    ❌ 错误

    ❎ 可以但是不建议

    🅰 .c文件B

    🅱 .c文件B

    📺 系统环境/编译器/环境

    🔺 表示总结

    🚫 表示禁止

    📜 表示建议

    📌 注意事项

    🔑 表示解析、答案

    💡 解决方案

    💀 死循环

    🐞 BUG

    💊 DEBUG

    🚀 提交

    📦 封装

    ⚡ 代码优化

    👆👇👈👉 方位

    ## 注:本图标适用于本人所有C语言系列博客!


    一、如何写代码

    0x00 首先写主函数

    ❓ 何为main函数?

    main函数,又称主函数,是程序执行的起点。

    1. 主函数是程序的入口

    2. 主函数有且只有一个

    📚 一个工程中可以有多个“.c文件”,但是多个“.c文件”中只能有一个“main 函数”

    0x01 然后写代码

    💬  "你好,世界!"

    #include <stdio.h> //头文件
    int main()
    {
        printf("hello world!\n");
        return 0;
    }

    0x02 最后运行程序

    🔨 编译 + 链接 + 运行代码

    快捷键: Ctrl + F5VS2013

    [调试] -> [选择' 开始执行(不调试)']

    ❓ “是不是有什么一闪而过了?”

    因为程序执行得太快,所以没有看到结果。

    为了看到结果,我们需要设置一下 VS2013 的属性 ( VS2019不需要设置项目属性 )

    [解决方案资源管理器] -> [右键项目名称] -> [属性] -> [链接器] -> [系统]
    -> [子系统] -> [选择' 控制台(/SUBSYSRTEM:CONSOLE) ']

    🚩 运行结果如下:

    二、数据类型

    0x00 基本类型

    📚 这里先做些简单的介绍:

    💬 基本数据类型的使用方法:

    int main()
    {
        char ch = 'a';
        int age = 20;
        float weight = 50.8;
        double d = 0.0;
        
        return 0;
    }

    0x01 格式说明

    📚 格式说明由“%”和格式字符组成:

    💬 演示:

    int main()
    {
        printf("%d\n", 100);
        printf("%c\n", 'a');
    
        float foo = 5.0;
        printf("%f\n", foo);
    
        double pi = 3.14;
        printf("%.2lf\n", pi); // .xf(x为小数点保留几位数)
    
        char str1[] = "hello";
        printf("%s\n", str1);
    
        int a = 10;
        printf("%p\n", &a);
    
        return 0;
    }

    🚩 运行结果: 100 a 5.000000 3.14 hello 0000000000061FE04

    0x03 数据类型的大小

    📺 32位系统下:

    📚 关键字 sizeof :获取数据在内存中所占用的存储空间,以字节为单位计数

    💬 使用方法演示:

    int main()
    {
        char a = 'A';
        printf("%d\n", sizeof(a))
        printf("%d\n", sizeof a) //求变量时可省略括号 ✅
        
        printf("%d\n", sizeof(char));
        printf("%d\n", sizeof char); //error!但是求字符类型时不可省略 ❌
    
        return 0;
    }

    🚩 运行结果:1

    💬 使用 sizeof 求元素个数:

    int main()
    {
        int arr[] = {1,2,3,4,5,6,7,8,9,10};
        printf("%d\n", sizeof(arr)); //计算的是数组的总大小,单位是字节
        printf("%d\n", sizeof(arr[0])); //计算的是数组首字符大小
        int sz = sizeof(arr) / sizeof(arr[0]);
        printf("%d\n", sz);
        
        return 0;
    }

    🚩 运行结果: 40  4  10

    📚 计算机中的单位: bit - 比特位

    计算机中识别二进制:1&0         8进制:0-7        10进制:0-9

     三、变量与常量

    0x00 创建变量

     💬 代码演示:

    #include <stdio.h>
    
    int main()
    {
        int age = 20;
        double weight = 62.5;
    
        age = age + 1;
        weight = weight - 10;
        printf("%d\n", age);
        printf("%lf\n", weight);
    
        return 0;
    }

    0x01 全局变量和局部变量

    💬 局部变量和全局变量的名字相同时,局部变量优先,对局部影响:

    int var = 100;
    int main()
    {
        int var = 10;
        printf("%d\n", var);  
        
        return 0;
    }

    🚩 运行结果: 10

    0x02 scanf 函数

    💬 使用方法演示:

    int main()
    {
        int a = 0;
        scanf("%d", &a);
        printf("%d", a);
        
        return 0;
    }

    0x03 变量的使用

    💬 使用方法演示:写一个代码求两个变量的和

    int main()
    {
        int a = 0;
        int b = 0;
        int sum = 0;
        
        scanf("%d %d", &a, &b);
        sum = a + b;
        printf("sum = %d\n", sum);
    
        return 0;
    }

    ❗ 如果编译器发出了警告,说 scanf 不安全,在代码前面添加

    #define _CRT_SECURE_NO_WANRINGS 1

    即可,当然还有其他方法可以解决,这里就不细说了。

    🚩 运行结果如下:(假设输入了 1 2)

    sum = 3

    0x04 作用域和生命周期

    📚 作用域(scope)

    程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用 的 而限定这个名字的可用性的代码范围就是这个名字的作用域。

    全局变量的作用域是整个工程。

    局部变量的作用域是变量所在的局部范围。

    📚 生命周期:

    变量的生命周期:变量的创建和销毁之间的时间段

    局部变量的生命周期:进入作用域生命周期开始,出作用域生命周期结束。

    全局变量的生命周期:整个程序的生命周期。

    💬 局部变量的生命周期:

    int main()
    {
        {
            int a = 10;
            printf("%d\n", a);
        }
        printf("%d\n", a); //error 生命周期结束了
    }

    💬 全局变量的生命周期

    //声明一下变量
    extern int g_val;
    int main()
    {
        printf("%d", g_val);
    }

    0x05 常量

    📚 定义:C语言中常量和变量的定义的形式有所差异

    C语言的常量分为以下几种:

    ① 字面常量:直接写

    const 修饰的常变量:用 const 定义

    #define 定义的标识符常量:宏定义

    ④ 枚举常量:可以一一列举的常量

    💬 字面常量:

    int main()
    {
        3.14;
        10;
        'a';
        "abcdef";
    }

    💬 const 修饰的常量:

    int main()
    {
        const int num = 10;
        num = 20; //error, 不能修改
        printf("num = %d\n", num);
    }

    🚩 运行结果: 10

    💬 #define 定义的标识符常量:

    #define MAX 10000  // 不写=,不加分号!
    
    int main()
    {
        int n = MAX;
        printf("%d\n", n);
    }

    🚩 运行结果:10000

    💬 枚举常量:

    //性别
    enum Sex {
        //这种枚举类型的变量的未来可能取值
        //枚举常量
        MALE = 3, //赋初值
        FEMALE,
        SECRET
    };
    
    int main()
    {
        enum Sex s = MALE;
        printf("%d\n", MALE);
        // MALE = 3 error 不可修改
        printf("%d\n", FEMALE);
        printf("%d\n", SECRET);
    
        return 0;
    }

    📌 注意事项:

    const 只是一个有常属性的变量,本质上仍然为变量!arr[], 数组的大小必须是常量!

    int main()
    {   
        const int n = 10; //n是变量,但是又有常属性,所以我们说n是常变量
        int arr[n] = {0}; // 仍然不可以,因为n是变量,只是有常属性而已
        
        return 0;
    }

    💬 #define 定义的可以,宏定义用得最多的地方是在数组中用于指定数组的长度:

    #define N 10
    int main()
    {
        int arr[N] = {0};
        
        return 0;
    }

    📌 注意事项:

    枚举常量虽然是不能改变的,但是通过枚举常量创造出来的变量是可以改变的!

    enum Color 
    {
        // 枚举常量
        RED,
        YEELOW,
        BULE
    };
    
    int main()
    {
        enum Color c = BULE; //我们创建一个变量c,并将BULE赋给它
        c = YEELOW; //这时将YEELOW赋给它,完全没有问题 ✅
        BULE = 6; //error!枚举常量是不能改变的  ❌
        return 0;
    }

    四、字符串&转义字符&注释

    0x00 字符串

    📚 定义:

    "hello world."

    这种由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal)

    📚 关于斜杠零:

    字符串的结束标志是一个 \0 转义字符。

    在计算字符串长度的时候 \0 是结束标志,不算字符串的内容!

    💬 下面代码,打印的结果是什么?为什么?

    int main()
    {
        char arr1[] = "abc"; //数组
        // "abc" -- 'a' 'b' 'c' '\0' 
        // 这里面,'\0' 字符串的结束标志
        char arr2[] = {'a', 'b', 'c'};
        char arr3[] = {'a', 'b', 'c', '\0'};
    
        printf("%s\n", arr1);
        printf("%s\n", arr2);
        printf("%s\n", arr3);
    
        return 0;
    }

    🚩 打印结果如下:

    0x01 计算字符串长度

    📚 strlen 函数:返回字符串的长度

    📜 使用时需引入头文件 <string.h>

    💬 代码演示:

    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
        char arr1[] = "abc";
        char arr2[] = {'a', 'b', 'c'};
        printf("%d\n", strlen(arr1));
        printf("%d\n", strlen(arr2));
    
        return 0;
    }

     🚩 运行结果如下:  3   随机值

     

    0x02 ASCII 码值

    📚 ASCII (American Standard Code for Information Interchange)

    “是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符”

    0x03 转义字符

    📚 转意字符顾名思义就是转变字符的意思。

    💬 让斜杠就是斜杠:

    int main()
    {
        printf("c:\\test\\32\\test.c"); //再加一个斜杠,规避转义字符检测
        
        return 0;
    }

     🚩 运行结果:  c:\test\32\test.c

    💬 三字母词、输出单引号、ddd、xdd:

    int main()
    {
        printf("(are you ok?\?)\n"); // ??) -- ] - 三字母词
        printf("%c\n", '\''); //输出一个单引号
        printf("%s\n", "\""); //输出一个双引号 
    
        printf("%c\n", '\130');//8进制的130是10进制的多少呢?
        //X - ASCII码值是88
        printf("%c\n", '\101'); // A- 65 - 8进制是:101
        printf("%c\n", '\x30'); //48 - '0'
        
        return 0;
    }

    🚩 运行结果:  (are you ok??) ' " X A 0

    ❓ 下列代码打印的结果是什么?

    int main()
    {
        printf("%d\n", strlen("c:\test\328\test.c"));
        
        return 0;
    }   

    💡 答案:14

    0x04 注释

    📚 代码中有不需要的代码可以直接删除,也可以注释掉

    代码中有些代码比较难懂,可以加一下注释文字

    有两种风格:

    ① C语言风格的注释   /* xxxxxx */

    缺陷:不能嵌套注释

    ② C++风格的注释    //xxxxxxxxx

    优点:可以注释一行也可以注释多行

    快捷键:Ctrl + /?

    💬 演示:

    /*  C语言风格注释
    int Add(int x, int y) {
        int z = 0;
        z = x + y;
        return z;
    }
    */
    
    int Add(int x, int y) {
        return x + y;
    }
    
    int main()
    {
        // C++注释风格
        // int a = 10;
        printf("%d\n", Add(1,2)); //调用Add函数
    
        return 0;
    }

    五、分支和循环

     0x00 if 语句

    📚 定义:if 语句是指编程语言(包括c语言、C#、VB、java、汇编语言等)中用来判定所给定的条件是否满足,根据判定的结果(真或假)决定执行给出的两种操作之一。

    💬 选择语句代码演示:

    int main()
    {
        int a = 10;
        int b = 20;
        
        if(a>b) {
            printf("a比b大\n") ;
        } else {
            printf("b比a大\n");
        }
    
        return 0;
    }

    🚩 运行结果:  b比a大

    ❓  C语言中如何实现循环呢?

    1. while 语句

    2. for 语句

    3. do ... while 语句

    这里我们着重先了解下 while 语句。

    📚 while 循环的基本概念:while是计算机的一种基本循环模式。当满足条件时进入循环,进入循环后,当条件不满足时,跳出循环。

    💬 演示代码:敲三万行代码

    #include <stdio.h>
    
    int main()
    {
        int line = 0;
        while(line < 30000) {
            printf("敲代码: %d\n", line);
            line++;
        }
        if(line == 30000) {
            printf("恭喜你敲满30000行代码!\n");
        }
    
        return 0;
    }

    六、函数

    ❓ 什么是函数?

    数学中的函数 f(x) = 2×x+5,C语言中的函数也是一样的。

    📚 函数的特点就是:简化代码,代码复用。

    💬 代码演示:

    int Add (int x, int y) {
        int z = 0;
        z = x + y;
        return z;
    }
    
    int main()
    {
        int num1 = 0;
        int num2 = 0;
        scanf("%d%d", &num1, &num2);
    
        // int sum = num1 + num2;
        // 函数的方式解决
        int sum = Add(num1, num2);
        printf("%d\n", sum);
    
        return 0;
    }

    七、数组

    📚 要存储1-10的数字,怎么存储?

    “C语言给了数组的定义:一组相同类型元素的集合”

    定义一个整型数组,最多放十个元素:

    💬 代码演示:

    int main()
    {
        int a = 0;
        int b = 0;
        int c = 0;
        int d = 0;
        //...
    
        // 10个整型1-10存起来
        int arr[10] = {1,2,3,4,5,6,7,8,9,10};
        char ch[5] = {'a', 'b', 'c'}; // 不完全初始化,剩余的默认为0
        
        // 用下标就可以访问元素
        printf("%d\n", arr[0]);
        
        // 打印整个数组
        int i = 0;
        while(i<10) {
            printf("%d ", arr[i]);
            i++;
        }
        
        return 0;
    }

    八、操作符简单介绍

    0x00 算数操作符

    ❓ 什么是算数操作符?

    📚 算术运算符即算术运算符号。是完成基本的算术运算 (arithmetic operators) 符号,就是用来处理四则运算的符号。

    💬 代码演示:

    int main()
    {
        int add = 1 + 1;
        int sub = 2 - 1;
        int mul = 2 * 3;
        float div = 9 / 2.0; //只要有浮点数出现,执行的就是浮点数除法
        int mod = 123 % 10;
    
        return 0;
    }

    0x01 移位操作符

    ❓ 什么是移位操作符?

    📚 移位运算符在程序设计中,是位操作运算符的一种。移位运算符可以在二进制的基础上对数字进行平移。

    💬 代码演示:

    int main()
    {   
        // 移位操作符 移动的是它的二进制位
        // << 左移
        // >> 右动
        int a = 1;
        // 整形1占4个字节-32bit位
        // 0000000000000000000000000000001
        int b = a<<1;
       // 0000000000000000000000000000010  左边丢弃,右边补0
        printf("%d\n", b);
        printf("%d\n", a);
    
        return 0;
    }

    🚩  运行结果: 2 1

    0x02 位操作符

    ❓ 什么是位操作符?

    📚 位操作是程序设计中对位模式按位或二进制数的一元和二元操作

    💬 代码演示:

    #include <stdio.h>
    
    int main()
    {
        //位操作 依然是二进制位
    
        // & 按位与   都为真,则返回真,只要出现假,就返回假
        int a = 3; //真
        int b = 5; //真
        int c = a&b; //按位与
        // 011
        // 101
        // 001
        // R→ 1 //真
        printf("%d\n", c);
        
        int d = a|b;// 按位或  有真则为真,无则假,只要出现真,就返回真
        // 011
        // 101
        // 111
        // R→ 7
        printf("%d\n", d);
    
        int e = a^b;// 按位异或  相同为假,相异为真   11\ 00-> 0   10 -> 1
        //异或的计算规律
        //对应的二进制位相同,则为0
        //对应的二进制位相异,则为1
        // 011
        // 101
        // 110
        // R→
        printf("%d\n", e);
        
        return 0;
    }

    0x03 赋值操作符

    ❓ 什么是赋值操作符?

    📚 赋值运算符是双目运算符,用在赋值表达式中。赋值运算符将值存储在运算符左边操作数指定的变量中。

    💬 代码演示:

    #include <stdio.h>
    
    int main()
    {
        int a = 10;
        a = 20; // “=” 赋值  “==” 判断相等
    
        a = a+10; //1
        a += 10; // 2
        
        a = a-20;
        a -= 20;
    
        a = a & 2;
        a &= 2;
        //这些操作叫做 复合赋值符
        
        return 0;
    }

    0x04 单目操作符

    ❓ 什么是单目操作符?

    📚 单目操作符,也就是只接受一个操作数的操作符。

    💬 逻辑反操作

    int main()
    {
        //0表示假,非0就是真
        int a = 5;
        printf("%d\n", !a);
    
        if(a) {
            //如果a为真,做事
        }
        if(!a) {
            //如果a为假,做事
        }
    
        while(a); //a != 0,做事
        while(!a); //a=0,做事
    
        return 0;
    }
    

    💬 按位取反 ~

    int main()
    {
        int a = 0;
        printf("%d\n", ~a);
        // ~ 按(二进制)位取法 
        //把所有二进制位的数字,1变成0,0变成1
    
        //整数a
        // 0
        // 00000000000000000000000000000000
        // 11111111111111111111111111111111  ~a
        // 数据在内存中存储的是补码
        // 一个整数的二进制表示有三种: 原码 反码 补码
        // 负数的计算  -1:
        // 10000000000000000000000000000001 (原码)
        // 11111111111111111111111111111110 (反码)
        // 11111111111111111111111111111111 (补码) -> 在内存中存的
    
        // 正的整数: 原码、反码、补码相同
    
        return 0;
    }

    💬 前置++

    int main()
    {
        int a = 10;
        int b = ++a; //前置++, 先++后使用
        
        printf("%d\n", b); //11
        printf("%d\n", a); //11
    
        return 0;
    }

    💬 后置++

    int main()
    {
        int a = 10;
        int b = a++; //后置++,先使用再++
    
        printf("%d\n", b);
        printf("%d\n", a);
    
        return 0;
    }

    💬 强制类型转换:

    int main()
    {
        int a = (int)3.14; //强制类型转换
        printf("%d\n", a);
    
        return 0;
    }

    0x05 逻辑操作符

    📚 逻辑运算符或逻辑联结词把语句连接成更复杂的复杂语句

    逻辑与:两个都为真即为真(and)

    逻辑或:一个为真即为真(or)

    💬 逻辑与 &&

    int main()
    {
        int a = 3;
        int b = 5;
        int c = a && b;
        printf("%d\n", c); //1
    
        return 0;
    }

    💬 逻辑或 ||

    int main()
    {
        int a = 3;
        int b = 0;
        int c = a||b;
        printf("%d\n", c); //1
        
        return 0;
    }
    

    0x06 条件操作符

    📚 条件操作符就是三目操作符

    exp1成立,exp2计算,整个表达式的结果是:exp2的结果。

    exp1不成立,exp2计算,整个表达式的结果是:exp3的结果。

    💬 条件操作符的用法:

    int main()
    {
        //exp1成立,exp2计算,整个表达式的结果是:exp2的结果
        //exp1不成立,exp3计算,整个表达式的结果是exp3的结果
        int a = 0;
        int b = 3;
        int max = 0;
    
        /* 等价于
        if(a>b) {
            max = a;
        } else {
            max = b;
        }
        */
    
        max = a>b ? a : b;
    
        return 0;
    }

    0x07 逗号表达式

    📚 逗号表达式:逗号隔开的一串表达式。

    逗号表达式是从左向右依次计算的。

    整个表达式的结果是最后一个表达式的结果。

    💬 逗号表达式的用法演示:

    int main()
    {
        (2, 3+5, 6);
        int a = 0;
        int b = 3;
        int c = 5;
                // a=5   c=5-4  b=1+2
        int d = (a=b+2, c=a-4, b=c+2);
        printf("%d\n", d); // 3
    
        return 0;
    }

    0x08 下标引用操作符

    📚  [] 就是下标引用操作符。

    💬 演示:

    int main()
    {
        int arr[10] = {1,2,3,4,5,6,7,8,9,10};
        printf("%d\n", arr[5]); // 6
    
        return 0;
    }

    0x09 函数引用操作符

    📚 调用函数时,函数名后面的()就是函数调用操作符

    💬 演示:

    int main()
    {   
        printf("hehe\n");
        printf("%d", 100);
    
        return 0;
    }

    九、关键字

    0x00 常见关键字

    📚 C语言提供的关键字:

    1. C语言提供的,不能自己创建关键字

    2. 关键字不能做变量名

     这里我们来简单地介绍几个:

    💬 auto - 自动的

    int main()
    {
        {
            int a = 10; //自动创建,自动销毁的 - 自动变量
            // auto省略掉了
            auto int a = 10;
            // auto新的C语言中也有其它用法 - 暂时不考虑
        }
    
        return 0;
    }

    💬 register - 寄存关键字

    int main()
    {
        // 大量/频繁使用的数据,想放在寄存器中,提升效率
        register int num = 100; //建议num的值存放在寄存器中
        //是不是最终放到寄存器中是编译器说的算
    
        // 其实现在编辑器已经很聪明了,
        // 就算你不主动加register,它也会帮你存到寄存器中。
        return 0;
    }

    💬 typedef - 类型重命名

    // 这种罗里吧嗦的变量我们可以把它重命名一下
    typedef unsigned int u_int;
    int main()
    {
        unsigned int num = 100;
        u_int num2 = 100; //和上面是等价的
    
        return 0;
    }

    📚 static - 静态的

    1. static修饰局部变量

    2. static修饰全局变量

    3. static修饰函数

    💬 static修饰局部变量

    函数中局部变量:

    声明周期延长:该变量不随函数结束而结束

    初始化:只在第一次调用该函数时进行初始化

    记忆性:后序调用时,该变量使用前一次函数调用完成之后保存的值

    存储位置:不会存储在栈上,放在数据段

    void test() {
        static int a = 1; //不销毁,生命周期变长了
        a++;
        printf("%d ", a);
    }
    
    int main()
    {
        int i = 0;
        while(i<10) {
            test();
            i++;
        }
    
        return 0;
    }

    🚩 运行结果: 2 3 4 5 6 7 8 9 10 11

    💬 static修饰全局变量

    改变该变量的链接属性,让该变量具有文件作用域,即只能在当前文件中使用。

    ① Global.c:

    static int g_val = 2021;
    // static 修饰全局变量,
    // 使得这个全局变量只能在自己所在的源文件(.c)内部可以使用
    // 其他的源文件不能使用!
    
    // 全局变量,在其他原文件内部可以被使用,是因为全局变量具有外部链接属性
    // 但是被static修饰之后,就变成了内部链接属性,
    // 其他的源文件就不能链接到这个静态的全局变量了!

    ② test.c:

    extern int g_val;
    
    int main()
    {
    	printf("%d\n", g_val);
    
    	return 0;
    }

    🚩 运行结果: test.obj : error LNK2001: 无法解析的外部符号 _g_val

    💬 static修饰函数

    改变该函数的链接属性,让该函数具有文件作用域,即只能在当前文件中使用。

    ① Add.c:

    //static修饰函数,
    static int Add(int x, int y) {
    	return x + y;
    }

    💬 test1.c:

    extern int Add(int, int); //声明外部函数
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int sum = Add(a, b);
    	printf("sum = %d\n", sum);
    
    	return 0;
    }

    🚩 运行结果:

    test.obj : error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用

    0x01 define

    📚 define 是一个预处理指令:

    1. define 定义标识符常量

    2. define 定义宏

    💬 define 定义标识符常量:

    #define MAX 1000
    int main()
    {
        printf("%d\n", MAX);
        return 0;
    }

    💬 define 定义宏:

    #define ADD(X, Y) X+Y
    #define ADD1(X, Y) ((X)+(Y))
    int main()
    {
        printf("%d\n", 4*ADD(2, 3));
        //             4*2+3 = 11
        printf("%d\n", 4*ADD1(2, 3));
        //             4*(2+3) = 20
        
        return 0;
    }

    十、指针初探

    0x00 指针介绍

    ❓ 什么是指针?

    指针就是地址,地址就是指针。

    指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

    💬 变量都有地址,取出变量的地址如下:

    int main()
    {
        int a = 10; // a在内存中要分配空间 - 4个字节
        &a; // 取出num的地址
        printf("%p\n", &a); //%p - 以地址的形式打印
        
        return 0;
    }

    🚩 打印出来的是a的地址

    💬 定义指针变量,存储地址

    * 说明 pa 是指针变量

    int 说明 pa 执行的对象是 int 类型的

    int main()
    {
        int a = 10;
        int* pa = &a; // pa是用来存放地址的,在C语言中叫pa的是指针变量
        
        char ch = 'w';
        char* pc = &ch;
        
        return 0;
    }

    0x01 解引用操作

    如果把定义指针理解为包装成快递,那么“解引用操作”就可以理解为是拆包裹

    拆出来的值就是那个变量的值,甚至还可以通过“解引用”来修改它的值。

    💬 解引用操作用法:

    int main()
    {
        int a = 10;
        int* pa = &a;
    
        *pa = 20; // * 解引用操作   *pa就是通过pa里边的地址,找到a
        printf("%d\n", a); //20
        
        return 0;
    }

    0x02 指针的大小

    📚 指针的大小

    “指针的大小是相同的”

    ❓ 为什么相同?

    “因为指针是用来存放地址的,指针需要多大空间,取决于地址的存储需要多大空间”

    📺 指针是一种复合数据类型,指针变量内容是一个地址,因此一个指针可以表示该系统的整个地址集合,故按照32位编译代码,指针占4个字节,按照64位编译代码,指针占8个字节(注意:不是64位系统一定占8个字节,关键是要按照64位方式编译)。

    32位 - 32bit - 4byte

    64位 - 64bit - 8byte

    💬 sizeof 计算指针的大小:

    int main()
    {
        printf("%d\n", sizeof(char*));
        printf("%d\n", sizeof(short*));
        printf("%d\n", sizeof(int*));
        printf("%d\n", sizeof(long*));
        printf("%d\n", sizeof(long long*));
        printf("%d\n", sizeof(float*));
        printf("%d\n", sizeof(double*));
    
        return 0;
    }

    32位下都是 4         64位下都是 8

    十一、结构体

    0x00 什么是结构体

    📚 结构体可以让C语言创建出新的类型

    “结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂的类型”

    💬 使用结构体描述学生 / 描述书:

    // 创建一个学生的类型
    struct Stu {
        char name[20]; // 姓名
        int age; // 年龄
        double score; // 分数
    };  // ← 不要忘了加分号
    
    // 创建一个书的类型
    struct Book {
        char name[20];
        float price;
        char id[30];
    };

    💬 结构体的初始化:

    点操作符和箭头操作符

    . 结构体变量.成员

    -> 结构体指针->成员

    struct Stu {
        char name[20]; // 姓名
        int age; // 年龄
        double score; // 分数
    };
    
    struct Book {
        char name[20];
        float price;
        char id[30];
    };
    
    int main()
    {
        struct Stu s = {"张三", 20, 85.5}; // 结构体的创建和初始化
        // 点操作符
        printf("1: %s %d %lf\n", s.name, s.age, s.score);
    
        struct Stu* ps = &s;
        printf("2: %s %d %lf\n", (*ps).name, (*ps).age, (*ps).score); // ❎ 可以但不推荐
        // 箭头操作符
        printf("3: %s %d %lf\n", ps->name, ps->age, ps->score); // ✅ 推荐
    
        return 0;
    }


    参考文献 / 资料

    Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

    俞甲子 / 石凡 / 潘爱民. 《程序员的自我修养》[M]. 电子工业出版社, 2009-4.

    百度百科[EB/OL]. []. https://baike.baidu.com/.

    比特科技. C语言基础[EB/OL]. 2021[2021.8.31]. .

    📌 本文作者: 王亦优

    📃 更新记录: 2021.9.25

    ❌ 勘误记录: int main写法不统一(道法自然)、sizeof关键字(ztm3726)、逻辑与和逻辑或的符号反了(伍六七_)、一处图标可能产生误解(嵙嵙程序猿)

    📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

    本章完。

    展开全文
  • 数据集的第部分是10000图片被用来测试,与训练数据的格式一样。我们将使用测试数据衡量我们的神经网络学习得怎么样。当然第部分的图片是另外250个人的手写样本,这有可以让我们证实网络可以识别不在训练集中的...

    一个简单的手写数字分类网络

    接上一篇文章,我们定义了神经网络,现在我们开始手写体的识别。我们可以将识别手写数字这个问题划分为两个子问题,一,我们需要将一幅包含了许多数字的图像分解为一系列独立的图像,每一幅图像包含了一个数字。比如,我们需要把下图分解:
    这里写图片描述
    将该图分解为6幅独立的图像:
    这里写图片描述

    我们人可以很轻松的将其分开,但是计算机可不那么认为。一旦图片被分割后,程序需要将每个数字单独识别。因此,举个例子,我们想要我们的程序可以识别上图中的第一个数字为5。
    这里写图片描述

    这里我们致力于解决第二个问题,就是对单一数字的分类。因为对于第一个分割问题,这里有许多方法可以解决了。因此,与其关⼼分割问题,我们不如把精⼒集中在设计⼀个神经⽹络来解决更有趣、更困难的问题,即⼿写数字的识别。

    为了识别单个数字,我们将用到三层神经网络:
    这里写图片描述

    网络的输入层包含了对输入像素值编码的神经元。正如后面讨论的,我们的训练数据有许多28X28的手写数字的像素图组成,也就是说我们的输入层包含了28X28=784个神经元,一个神经元对应一个像素位的值。输入的像素值为灰度值,0.0表示白色,1.0表示黑色,0到1之间的值表示不同程度的灰色。

    网络的第二层为隐藏层,我们记隐藏层的神经元数量为n,我们将对n的取值进行实验。这个图例中展示了一个小的隐藏层,它的隐藏层只有15个。

    网络的输出层包含了10个神经元。如果第一个神经元被激活,也就是输出值无限接近1,那么我们可以认为这个网络识别出来的数字是0.如果是第二个神经元激活,那么识别出来的数字为1。更准确的说,我们将对输出神经元从0到9编号,然后找出激活值最高的神经元。如果编号为6的神经元激活值最高,那么我们认为输入的数字为6。

    这里使用是个输出神经元很现实是为了对应输出的十种可能性。一种看起来更自然的方法是使用4个输出神经元,每一个神经元看作一个二进制数,结果取决于改神经元输出更靠近0还是更靠近1。4个输出神经元对于10个数字来说已经足够了,毕竟 24=16 是大于10的。那为什么我们选择使用是个输出的神经元而不是看起来更方便简洁的4个神经元呢?经验告诉我们:通过对两种设计的实验对比,是个输出的神经元比4个输出的神经元更加准确的识别数字。但这是为什么呢?有没有什么启发性的方法可以告诉提前告诉我们使用10个输出神经元比使用4个效果要更好。

    为了理解为什么要这样做,了解神经网络的工作的基本原理是很有帮助的。首先考虑我们使用10个输出神经的情况。让我们把目光放在第一个输出神经上,该神经决定了是否该数字是0。它是通过权衡隐藏层所输出的信息做出判断的。那么隐藏层的神经元做了些什么呢?假设隐藏层的第一个神经元是为了检测图像中是否存在如下图形:
    这里写图片描述

    它是通过对输入图像中与该图形重合的像素赋予很大的权值,而其他部分赋予很小的权值来判断的。以同样的方式,我们假设第二个第三个第四个神经元分别是为了探测图像中是否存在以下图形:
    这里写图片描述

    可能你已经看出来上面四个图形可以组成数字0:
    这里写图片描述

    因此,如果隐藏层的这四个神经元都被激活,那么我们可以认为这个图像上的数字是0。当然这不是我们用来推断数字为0的唯一组合,我们可以通过诸多图形组合得到0(比如将上图进行扭曲变形,但它还是0)。但至少在这个例子中,我们可以推断出输入数字是0。

    如果神经网络是这样工作的,那么我们似乎可以给出一个对为什么使用10个输出神经而不是4个这件事一个合理的解释。如果是4个输出神经元,那么,我们的第一个输出神经元将用于判断该数字对应二进制的最高有效位是什么,然而,我们很难将图形和数字的最高有效位对应起来。

    上面说了这么多都只是一个启发性的方法,没人说三层神经网络必须按照我上面所说的方式去工作,即每个隐藏层的神经元去探测一个简单的图像组成部分。也许一些聪明的学习算法会找到一些权重分配让我们使用4个输出神经元。但是作为一种启发,我所描述的方法可以工作的很好,这可以让你节省许多时间在设计神经网络的结构上。

    使用梯度下降算法进行学习

    现在我们已经设计出了一个神经网络,那么我们如何使用它去学习识别数字呢。首先,我们需要一个数据集去学习,我们使用的是MNIST数据集,它包含了数以万计的手写数字的扫描图像,以及他们的正确分类。下图是取值MNIST的几个图像:
    这里写图片描述

    正如你所见,这些数字和上一篇文章的数字是一样,当然我们需要我们的网络可以识别不在数据集中的图像。

    MNIST数据集包含两个部分,第一个部分包含了60000张图片被用作训练数据。这些通过扫面250个人的手写数字获得,其中有些是美国人口普查局的员工,有些是高中生。这些图像是28X28的灰度图。数据集的第二部分是10000张图片被用来测试,与训练数据的格式一样。我们将使用测试数据衡量我们的神经网络学习得怎么样。当然第二部分的图片是另外250个人的手写样本,这有可以让我们证实网络可以识别不在训练集中的人的手写数字。

    我们用 x 表示一个训练输入。显然x是一个28X28=784的向量,向量中每一个元素表示图像中的一个灰度值。我们用 y=y(x) 表示对应的期望输出值,其中 y 是一个10维的向量。比如,有一个特定的显示为6的图像输入为x,那么它期望的输出值应该为 y(x)=(0,0,0,0,0,0,1,0,0,0)T ,其中 T 表示矩阵的转置。

    我们想有这样一个算法,它可以让我们找到权重和偏差,这样网络的输出y(x)可以拟合所有的输入 x 。为了量化我们如何实现这个目标,我们定义一个代价函数:
    这里写图片描述

    其中w记为网络中的所有权重的集合, b 为所有的偏差,n是所有训练输入的数量, a 是当输入向量为x时网络的输出向量,并且对所有的输入 x 进行计算并求和。的确,输出a依赖于输入 xwb ,但是为了简介,我并没用使用 axi 这样的符号。符号 ||v|| 表示的是向量 v 的长度(模)。我们把C称为二次代价函数,有时也被称为均方误差或者MSE。观察二次代价函数,我们可以发现 C(w,b) 是非负的。此外,对于所有的 x ,当y(x)接近于 a 时,C(w,b)的值是很小的,也就是 C(w,b)0 ,因此我们的训练算法需要能够找到合适的权重和偏差,使得 C(w,b)0 相反,如果 C(w,b) 的值很大,就说明有大量的 y(x) a 相差很大,这是不好的。因此我们的训练算法目标是要能够找到最小化代价函数C(w,b)的权重和偏差。换句话说,我们需要找到一组权重和偏差,使得代价最小,下面我们将使用梯度下降算法来达到目标。

    为什么要介绍二次代价函数?我们不应该关注于图像的正确分类的数量上面吗?为什么不尝试直接最大化正确分类的数量而是要使用最小化一个二次代价函数来简介评测呢?这是因为正确分类图像的数量对于权重和偏差来说不是一个平滑的函数,那么在大多数情况下,对权重和偏差的微小改变不会造成目标函数即正确分类数量的值的改变,这让我们很难找到通过改变权重和偏差提高性能的方法。如果我们使用一个像二次代价函数这样的平滑函数,那将很容易找到通过改变权重和偏差来提高性能的方法。这就是我们为什么专注于最小化二次代价,因为只有这样,我们才能测试分类的准确性。

    那么可能你又会好奇为什么要选择式(6)那样的二次函数来作为平滑函数呢?这时临时想出来的么?可能如果我们选择另外一个不一样的代价函数,我们将会得到不同的代价最小时的权重和偏差。这个疑问是有必要的,稍后我将对上面的成本函数再次进行讨论,并作一些修改。然而式(6)中的代价函数对于我们理解神经网络的基础很有帮助,所以我们将一直使用它。

    重申一次,我们的目标是训练一个神经网络,找到权重和偏差使得二次代价函数 C(w,x) 最小化。这个目标没毛病,但是,现在有许多让我们分散精力的东西——对权重 w 和偏差b的解释,难以弄清的 σ 函数,网络架构的选择,MNIST等等。其实我们可以通过忽略上述这些中的绝大部分,仅仅考虑最小化这一点来理解这些东西。现在,我们打算忘掉代价函数的具体形式,神经网络的组成等等。现在我们只专注于最小化一个给定的多元函数。我们将学习一种用于最小化问题的梯度下降算法,然后再回到我们想要最小化神经网络的特定函数上去。

    OK,现在假设我们尝试最小化某些函数, C(v) ,它可能是任意的多元实数函数, v=v1,v2,... 。注意到我将使用 v 代替前面的w b ,因为我们刚说过我们不再特定研究神经网络代价函数的最小化问题上,而是任意的函数。我们先假设C是只有两个变量的函数,我们称为 v1 v2
    这里写图片描述

    我们想要找到使 C 达到全局最小的位置。现在对于上述的图像中,我们可以很容易的找到全局最小值。从某种意义上说,我可能展示了一个过于简单的函数。通常函数C是一个拥有许多变量的复杂的函数,并且将不可能通过肉眼一下找到它的全局最小值。

    解决这个问题的一种方法是使用微积分的方法。我们将计算函数 C 的导数来寻找它的极值点。运气好的话函数可能只有一个或者几个变量,但是一旦变量过多那将很麻烦。尤其是神经网络中,往往拥有数亿计的权重和偏差,微积分的方法将不再有效。

    现在微积分是不能解决最小值的问题了。幸运的是,有一种漂亮的推导法暗示有一种算法可以很好的解决问题。首先我们把函数想象成山谷,向上面那幅图画一样。然后我们假设有一个球从山谷上沿斜坡滚下,常识告诉我们这颗球会滚落到山谷底部。也许我们可以使用这样一个想法去寻找函数的最小值。开始我们把球随机放置在一个位置,然后模拟球从该点滚落到谷底这一过程。我们可以通过计算C的导数(和一些二阶导数)来简单模拟——这些导数将告诉我们这个山谷的一切局部形状,然后我们就知道这个球该怎么滚落了。

    说了这么多,你可能会以为接下来我将介绍牛顿定理,摩擦力和重力对球体的影响。事实上,我们只是做了一个假设,并不是真的要用这个球的运动来寻找最小值。提到球只是用来激发我们的想象力,而不是束缚我们的思维。因此与其陷进物理学⾥凌乱的细节,不如我们就这样问⾃⼰:如果我们扮演⼀天的上帝,能够 构造⾃⼰的物理定律,能够⽀配球体可以如何滚动,那么我们将会采取什么样的运动学定律来 让球体能够总是滚落到⾕底呢?

    为了使这个问题更明确,让我们讨论当我们将球在v1方向上移动 Δv1 ,在 v2 方向上移动 Δv2 。微积分告诉我们 C 的改变量为:
    这里写图片描述

    我们需要找到一种方法,找到一个Δv2 Δv2 ,使得 ΔC 为负值,即球总是在往低处移动。因此,我们需要定义 Δv v 的变化向量,Δv=(Δv1,Δv2)T。我们还需要定义 C 的梯度为偏导数的向量:(Cv1,Cv2)T。我们用 C 表示梯度向量:
    这里写图片描述

    后面我们将会用 CΔv 来重写 ΔC ,在此之前,我想先解释一下令人困惑的梯度这个概念。当我们第一眼看到 C 的时候,可能会尝试想去理解这个符号的意义。那它究竟是什么意思?事实上,你可以把它简单的当作是上述梯度向量的一个记号。也有很多其它的数学上不同视⻆对于 的专业解释(⽐如,作为⼀个微分操作),但我们不需要这些观点。

    有了这些定义,(7)式中的 ΔC 可以被重新写作:
    这里写图片描述

    这个等式帮助我们理解为什么 C 被称为梯度向量: C C v 的变化关联到C的变化,就像我们期望的用梯度来表示。但是,这个等式真的让我们激动的是因为,它可以让我们知道如何选择 Δv 来使得 ΔC 为负值。特别的,假设我们选择:
    这里写图片描述
    其中 η 是一个很小的,正的参数(被称为学习速率)。然后等式(9)就变为 ΔCηΔCΔC=ηC2 。因为 C0 ,这使得 ΔC0 ,也就是说 C 将总是减小。(当然要在方程9的近似约束下)。这个属性正是我们期望的!因此,我们使用方程(10)来定义球在梯度下降算法中的下降”定律”。也即是我们使用方程(10)来计算Δv的值,然后根据下面的值来移动球的位置 v
    这里写图片描述

    然后我们再一次使用这个更新规则,就可以计算它下一次下降的位置。如果我们一直这样做,C将一直减小知道我们希望它到达全局最小值。

    总结一下,我们使用的梯度下降算法的工作就是重复计算梯度 C ,然后沿着相反方向移动,滚下山谷。我们可以可视化这一过程:
    这里写图片描述

    注意到梯度下降规则并不满足真实的物理规则。在真实世界中,球有动量,动量可能允许它偏移斜坡,甚至向上滚。只有在摩擦力的影响下它才可能滚到山谷。相比之下,我们选择 Δv 规则就像在说”只能往下滚!”,这是一个好的方法去寻找最小值。

    为了使梯度下降正确地工作,我们需要选择一个足够小的学习速率 η ,使得等式(9)可以很好的近似。如果不这样,我们可能将会以 ΔC>0 结束,这显然是不好的。同样,我们又不希望 η 太小,这样会使的 Δv 变化太小,梯度下降算法就会下降得特别慢。在实际实现中, η 通常是变化的,这使得等式(9)能够保持很好的近似度,同时算法又不会太慢。稍后我们将看到它如何工作。

    我已经解释过当 C 只有两个变量时的梯度下降算法了。但是,事实上,即使C有许多变量,也是这么一回事。假设 C 是一个有m个变量的函数 v1,v2...,vm ,然后当 Δv=(Δv1,...,Δvm)T C C的变化量 ΔC 为:
    这里写图片描述

    其中梯度 C 为向量:
    这里写图片描述

    和前面的两个变量时的情况一样,我们可以选择:
    这里写图片描述

    我们保证我们的近似表达式(12)的值也将是负值。这将是我们可以在 C 为多个变量时,通过重复下面这一个更新规则,来找到我们的全局最小值:
    这里写图片描述

    你可以认为这个更新规则就是我们的梯度下降算法。它提供了重复改变位置v来使得函数 C 取最小值的方法。这个规则并不总是有效的,它有时候也会出错并使得我们无法通过梯度下降来寻找C的最小值,后面我们将讨论这个问题。但是,实际使用中,梯度下降往往工作的很好,并且发现它是在神经网络中寻找代价函数最小值的好方法,对网络的学习很有用。

    的确,在这种情况下,梯度下降是寻找最小值的最优策略。假设我们尝试移动 Δv ,最大程度的减小 C .这等价于最小化ΔCCΔv。我们限制步长为一个很小的固定值: Δv=ϵ ϵ>0 。换句话说,我们想在步长固定时,找到使得 C 下降得最快的方向。可以证明,使得CΔv最小化的 Δv Δv=ηC ,其中 η=ϵ/C ,它是由步长限制 ΔC=ϵ 所决定的。因此,梯度下降可以看作一种使得 C 下降最快的方向上做微小移动的方法。

    人们已经研究了梯度下降的许多变化形式,包括一些更接近真实的球的物理运动的形式。这种模拟球的形式有许多优点,但是也有一个重大的缺点:它最终必须计算C的二偏导,这将花费巨大的代价。为了理解为什么这么做代价很高,假设我们需要计算所有的二阶偏导 2C/vjvk 。如果我们有百万个变量 vj ,那么我们需要计算数万亿级别(百万的平方)的二阶偏导。那将会花费巨大。说了这么多,有一些技巧可以避免这些问题,寻找梯度下降算法的代替发也是一个很活跃的研究领域。但这本书我们将使用梯度下降法(和变种)来作为我们神经网络学习的主要方法。

    我们如何将梯度下降算法应用到一个神经网络?其思想是,使用梯度下降算法找到权重 wk 和偏差 bl ,使得代价公式(6)的值最小。为了了解这是如何实现的,让我们重申一下梯度下降算法的更新规则,用权重和偏差代替变量 vj 。换句话说,我们的”位置“现在由 wk bl 组成,梯度向量对应的元素变为 C/wk C/bl 。以向量中元素的形式写出梯度下降的更新规则,我们有:
    这里写图片描述

    通过迭代使用这个更新规则,我们可以“滚下山谷”,找到期望的最小的代价函数。换句话说,这个规则可以用来学习神经网络。

    在应用梯度下降时有许多挑战,我们将在后面的深入探讨这个问题。现在,我仅仅关注一个问题,在提出问题之前,我们先回顾一下公式(6)中的二次代价函数。这个代价函数有着 C=1nxCx 的形式,也就是它是遍及每个训练样本代价 Cx=y(x)a22 的均值。在实际中,为了计算梯度 C ,我们需要单独计算每一个输入样本 x 的梯度Cx,然后再求平均 C=1nxCx 。不幸的是,当训练样本过大时,这会使得训练花费的时间很长。

    有一种叫做随机梯度下降的方法可以用来加速学习。这个思想就是通过随机选取小的训练样本来计算 Cx 来近似估计 C 。通过平均这一个小的样本就可以快速的估计出梯度 C ,这有助于帮助我们加速梯度下降,进而更快的学习。更精确地说,随机梯度下降是通过随机选取一个小数量地训练样本 m 作为输入。 我们将这些随机的输入样本记为X1,X2,...,Xm,并把它们称为一个小批量数据。假设提供的样本容量 m 是足够使得Cxj与计算所有得样本 Cx 的值约等,即
    这里写图片描述

    其中第二个求和公式是对所有的训练样本而言。通过上式我们得到:
    这里写图片描述

    也就是我们可以通过计算样本大小为 m< 全部样本的小批量数据的梯度,来估计整体的梯度。为了明确地将其和神经网络联系起来,假设 wk bl 分别为神经网络的权值和偏差。然后随机梯度下降通过随机选取一个训练样本作为输入,然后通过下式训练:
    这里写图片描述

    其中两个求和符号是在随机选取的小批量的训练样本上进行的。然后我们使用另一组随机的训练样本去训练,以此类推,直到我们用完了所有的训练输入,这样被称为一个训练迭代期。然后我们会开始一个新的训练迭代期。

    另外,对于改变代价函数⼤⼩的参数,和⽤于计算权重和偏置的⼩批量数据的更新规则,会有不同的约定。在等式(6)中,我们通过因子 1n 来改变整个代价函数的大小,人们有时候忽略 1n ,对单个训练样本的代价求和而不是平均。这对我们提前不知道训练样本总数的时候特别有效,比如学习数据是实时产生的。同样,在式子(20)(21)中的 1m 也可以舍去。但是需要注意的是,这样会导致学习速率的改变,对不同工作进行对比时,需要注意是求平均还是求和。

    实现我们的数字分类网络

    现在我们准备开始用代码通过随机梯度下降和MNIST训练集实现我们的识别网络。我们将使用python(2.7)来实现,仅仅74行代码!首先我们需要获得MNIST数据集,如果你使用git,你可以通过克隆代码库来获取:
    git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
    如果你不使用git也可以点击这里来获取数据和源码。

    顺便提一下,之前我说MNIST数据集被分成了60000个训练图像和10000个测试图像,那是官方的描述。事实,我们将对这些数据做一些不同的划分。我们将对60000个MNIST训练集分成两部分,其中50000作为训练集,另外10000作为检验集。在这里我们先不会使用检验数据,但是在后面我们将会发现它对于神经网络中一些如学习速率这样的超参数的设置很有用,这些超参数不是学习算法所决定的。景观验证数据不是原始MNIST的规范,但是许多人都这样使用MNIST数据集,并且在神经网络中使用验证数据是很普遍的。从现在开始我们提到的MNIST训练数据指的是50000张训练图像而不是原始的60000张图像。

    除了MNIST数据集以外,我们还需要python中的Numpy包,用于快速线性代数运算。

    在列出一个完整的代码清单之前,让我先解释一下神经网络代码的核心特性。核心是一个Network类,用来表示神经网络,下面是初始化网络Network类的代码:

    class Network(object):
    
        def __init__(self, sizes):
            self.num_layers = len(sizes)
            self.sizes = sizes
            self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
            self.weights = [np.random.randn(y, x) 
                            for x, y in zip(sizes[:-1], sizes[1:])]

    在这个代码中,list对象sizes包含了各层中神经元的数量。比如,你想创建一个第一层有2个神经元,第二层3个神经元,第三次1个神经元的网络,你可以这样创建类对象:

    net = Network([2, 3, 1])

    网络对象中的偏差和权重是初始化为随机值,使用Numpy包中的np.random.randn函数来产生均值为0,标准差为1的⾼斯分布。这个随机初始化也就是我们使用随机梯度下降算法的一个起点。后面我们将讨论其他初始化权重和偏差的方法。注意,Network初始化代码中是假定第一层为输入层的,并且对这些神经元不设置任何偏差,因为偏差仅仅被用于后面层输出的计算。

    另外偏差和权重以Numpy矩阵列表的形式存储。因此,net.weights[1]是一个存储着链接第二层和第三层神经元权重矩阵。由于net.weights[1]写起来很冗长,我们就用 w 表示这个矩阵。也就是wjk表示的是第二次的第k个神经元和第三层的第j个神经元之间的权重。那么第三层神经元的激活向量为:
    这里写图片描述

    我们需要一块一块地来解释这个方程。 a 是第二层神经元的激活向量(输出向量)。为了获得a,我们用 a 乘以权重矩阵w,然后再加上偏差向量 b 。然后对向量wa+b中的每个元素使用 σ 函数。

    有了这些知识,很容易写出从Network计算输出的代码实例,我们从定义sigmoid函数开始:

    def sigmoid(z):
        return 1.0/(1.0+np.exp(-z))

    注意到当参数z是一个向量或者Numpy数组时,Numpy自动的对向量中每一个元素应用sigmoid函数。

    让后我们添加feedforward方法在Network类中,它对于网络给定输入a,返回对应的输出,这个方法是对每一层应用方程(22):

        def feedforward(self, a):
            """Return the output of the network if "a" is input."""
            for b, w in zip(self.biases, self.weights):
                a = sigmoid(np.dot(w, a)+b)
            return a

    的确,我们想要Network对象做的主要事情是学习。我们用SGD函数来实现随机梯度下降算法。下面是代码,有些地方比较神秘,我会在代码后面逐个分析。

    def SGD(self, training_data, epochs, mini_batch_size, eta,
                test_data=None):
            """Train the neural network using mini-batch stochastic
            gradient descent.  The "training_data" is a list of tuples
            "(x, y)" representing the training inputs and the desired
            outputs.  The other non-optional parameters are
            self-explanatory.  If "test_data" is provided then the
            network will be evaluated against the test data after each
            epoch, and partial progress printed out.  This is useful for
            tracking progress, but slows things down substantially."""
            if test_data: n_test = len(test_data)
            n = len(training_data)
            for j in xrange(epochs):
                random.shuffle(training_data)
                mini_batches = [
                    training_data[k:k+mini_batch_size]
                    for k in xrange(0, n, mini_batch_size)]
                for mini_batch in mini_batches:
                    self.update_mini_batch(mini_batch, eta)
                if test_data:
                    print "Epoch {0}: {1} / {2}".format(
                        j, self.evaluate(test_data), n_test)
                else:
                    print "Epoch {0} complete".format(j)

    training_data 是⼀个 (x, y) 元组的列表,表⽰训练输⼊和其对应的期望输出。变量 epochs 和mini_batch_size 正如你预料的 —— 迭代期数量,和采样时的⼩批量数据的⼤⼩。eta 是学习速率,η。如果给出了可选参数 test_data,那么程序会在每个训练器后评估⽹络,并打印出部分进展。这对于追踪进度很有⽤,但相当拖慢执⾏速度。

    update_mini_batch代码如下。在每一次迭代期,先随机排列训练样本,然后将它分成适当大小的小批量数据。这是一个简单的从训练样本随机采样的数据。然后对每一个小批量数据应用一次梯度下降。它仅仅使⽤ mini_batch 中的训练数据,根据单次梯度下降的迭代更新⽹络的权重和偏置:

      def update_mini_batch(self, mini_batch, eta):
            """Update the network's weights and biases by applying
            gradient descent using backpropagation to a single mini batch.
            The "mini_batch" is a list of tuples "(x, y)", and "eta"
            is the learning rate."""
            nabla_b = [np.zeros(b.shape) for b in self.biases]
            nabla_w = [np.zeros(w.shape) for w in self.weights]
            for x, y in mini_batch:
                delta_nabla_b, delta_nabla_w = self.backprop(x, y)
                nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
                nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
            self.weights = [w-(eta/len(mini_batch))*nw 
                            for w, nw in zip(self.weights, nabla_w)]
            self.biases = [b-(eta/len(mini_batch))*nb 
                           for b, nb in zip(self.biases, nabla_b)]

    大部分的工作有下面这行代码完成:

     delta_nabla_b, delta_nabla_w = self.backprop(x, y)

    这行调用了一个称为反向传播的算法,可以快速的计算代价函数的梯度。因此update_mini_batch 的⼯作仅仅是对 mini_batch 中的每⼀个训练样本计算梯度,然后适当地更新 self.weights 和 self.biases。

    我现在不会列出 self.backprop 的代码。我们将在后面学习反向传播是怎样⼯作的,包括self.backprop 的代码。现在,就假设它按照我们要求的⼯作,返回与训练样本 x <script type="math/tex" id="MathJax-Element-183">x</script>相关代价的适当梯度。

    测试:
    这里写图片描述

    首先创建三个py文件,第一个用来读取数据,第二个用来学习,第三个就是调用上面两个文件中的函数。
    源码下载地址:
    手写数字分类网络源码

    可以看到参数选取为:
    这里写图片描述
    即迭代30次,小批量数据大小为3,学习速率为3.0时的准确率如图:
    这里写图片描述 这里写图片描述

    准确率达到了94.67%。

    我们还可以通过自己生成一张手写数字的图片,测试一下结果,下面的图片是我通过画板自己画的:
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述

    我们可以通过改变神经网络隐藏层的神经元数量,或者学习速率,或者小批量数据大小来改变识别准确率。这些参数的选取,调试都是神经网络构造过程中重要的步骤。后面将会继续学习,下一篇文章将会讲到上面没有解释的backprop()函数,即反向传播算法。


    参考原文:http://neuralnetworksanddeeplearning.com/chap1.html#a_simple_network_to_classify_handwritten_digits
    以上是作者对原文的翻译和理解,有不对的地方请指正。


    注:转载请注明原文出处:
    作者:CUG_UESTC
    出处:http://blog.csdn.net/qq_31192383/article/details/77198870

    展开全文
  • Python黑帽第篇文章将分享Python网络攻防基础知识,看看Python能做什么,以及正则表达式、网络爬虫和套接通信入门基础。本文参考了i春秋ADO老师的课程内容,这里真心推荐大家去学习ichunqiu的课程,同时也结合...

    Python黑帽第二篇文章将分享Python网络攻防基础知识,看看Python能做什么,以及正则表达式、网络爬虫和套接字通信入门基础。本文参考了i春秋ADO老师的课程内容,这里真心推荐大家去学习ichunqiu的课程,同时也结合作者的经验进行讲解。希望这篇基础文章对您有所帮助,更希望大家提高安全意识,也欢迎大家讨论。

    娜璋AI安全之家于2020年8月18日开通,将专注于Python和安全技术,主要分享Web渗透、系统安全、CVE复现、威胁情报分析、人工智能、大数据分析、恶意代码检测等文章。真心想把自己近十年的所学所做所感分享出来,与大家一起进步。


    声明:本人坚决反对利用教学方法进行恶意攻击的行为,一切错误的行为必将受到严惩,绿色网络需要我们共同维护,更推荐大家了解技术背后的原理,更好地进行安全防护。虽然作者是一名安全小白,但会保证每一篇文章都会很用心地撰写,希望这些基础性文章对你有所帮助,在安全路上一起前行。



    一.为什么使用Python做网络攻防

    首先,你需要了解网络攻防的七个基础步骤。

    • 侦查: 漏洞挖掘
    • 武器制作: 攻击、载荷
    • 分发: 垃圾邮件等
    • 利用: 漏洞利用
    • 安装: 恶意代码、网页
    • 远程控制: 僵尸网络
    • 行动: 窃密、破坏、跳板

    下图是ATT&CK框架,包括12个步骤。

    在这里插入图片描述

    其次,为什选择Python作为开发工具呢?
    真正厉害的安全工程师都会自己去制作所需要的工具(包括修改开源代码),而Python语言就是这样一个利器。Python开发的平台包括Seebug、TangScan、BugScan等。在广度上,Python可以进行蜜罐部署、沙盒、Wifi中间人、Scrapy网络爬虫、漏洞编写、常用小工具等;在深度上,Python可以实现SQLMAP这样一款强大的SQL注入工具,实现mitmproxy中间人攻击神器等。由于Python具有简单、易学习、免费开源、高级语言、可移植、可扩展、丰富的第三方库函数特点,Python几行代码就能实现Java需要大量代码的功能,并且Python是跨平台的,Linux和Windows都能使用,它能快速实现并验证我们的网络攻防想法,所以选择它作为我们的开发工具。

    那么,我们又可以用Python做什么呢?

    • 目录扫描:Web+多线程(requests+threading+Queue)、后台敏感文件(svn|upload)、敏感目录(phpmyadmin)。
    • 信息搜集:Web+数据库、中间件(Tomcat | Jboss)、C段Web信息、搜集特点程序。例如:搜索某个论坛上的所有邮箱,再进行攻击。
    • 信息匹配&SQL注入:Web+正则、抓取信息(用户名|邮箱)、SQL注入。
    • 反弹shell:通过添加代码获取Shell及网络信息。

    最后,建议读者做好以下准备。

    1. 选择一个自己喜欢顺手的编辑器
    2. 至少看一本关于Python的书籍
    3. 会使用Python自带的一些功能,学习阅读开源代码
    4. 阅读官方文档,尤其是常用的库
    5. 多练习,多实战

    下面举个简单Python示例,通过import导入扩展包base64,它是将字符串base64加解码的模块, 通过print dir(base64)、help(base64)可以查看相关功能。

    # -*- coding: utf-8 -*-
    import base64
    
    print dir(base64)
    print base64.__file__
    print base64.b64encode('eastmount')
    

    输出结果如下图所示,包括查看源代码文件位置和“eastmount”转码。

    在这里插入图片描述

    接下来我们开始学习Python正则表达式、Python Web编程和Python网络编程。



    二.Python正则表达式

    (一) 正则表达式基础

    在使用正则表达式之前,我们需要基本了解Python基础知识、HTTP协议,熟悉使用BurpSuite、SQLMAP工具。Python正则表达式被广泛应用在爬虫开发、多线程、网络编程中,而hacker应用也会涉及到正则表示式相关知识,比如扫描、爆破、POC等。

    正则表达式(RegEx)使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。 例如,如果想获取里面的ip地址,就需要使用正则表达式实现。Python通过re模块提供正则表达式的支持,其基本步骤如下:

    • 先将正则表达式的字符串形式编译我Pattern实例(compile)
    • 使用Pattern实例处理文本并获得匹配结果(match find findall)
    • 使用实例获得信息,进行其他的操作( 匹配结果)

    举一个简单例子:

    import re
    
    pattern = re.compile('east')
    match = pattern.match('eastmount!')
    print(match.group())
    
    word = re.findall('east', 'east mount')
    print(word)
    

    输出结果为:

    • east
    • [‘east’]

    点(.)表示匹配任意换行符“\n”以外的字符。

    import re
    
    word = "http://www.eastmount.com Python_9.29"
    key = re.findall('t.', word)
    print key
    

    输出结果为:[‘tt’, ‘tm’, ‘t.’, ‘th’],依次匹配t加任意字符的两个字符。

    斜杠(\)表示匹配转义字符 如果需要匹配点的话,必须要\转义字符。

    import re
    
    word = "http://www.eastmount.com Python_9.29"
    key = re.findall('\.', word)
    print key
    

    输出结果为:[’.’, ‘.’, ‘.’]。

    […] 中括号是对应位置可以是字符集中任意字符。
    字符集中的字符可以逐个列出,也可以给出范围,如[abc]或[a-c],第一个字符如果是^表示取反,如 [ ^ abc]表示不是abc的其他字符。例如:a[bcd]e 能匹配到 abe、ace、ade。

    匹配数字和非数字案例。

    # -*- coding: utf-8 -*-
    import re
    
    #匹配数字
    word = "http://www.eastmount.com Python_9.29"
    key = re.findall('\d\.\d\d', word)
    print key
    
    #匹配非数字
    key = re.findall('\D', word)
    print key
    

    输出结果如下图所示:

    在这里插入图片描述

    正则表达式较为难理解,更推荐读者真正使用的时候学会去百度相关的规则,会使用即可。同时,更多正则表达式的使用方法建议读者下来之后自行学习,常见表如下图所示。

    在这里插入图片描述

    在这里插入图片描述


    (二) 常用正则表达式规则

    下面讲解比较常见的正则表达式规则,这些规则可能会对我们的网络攻防有一定帮助。

    1.获取数字

    # -*- coding: utf-8 -*-
    import re
    
    string="A1.45,b5,6.45,8.82"
    regex = re.compile(r"\d+\.?\d*")
    print(regex.findall(string))
    

    输出结果为:
    [‘1.45’, ‘5’, ‘6.45’, ‘8.82’]


    2.抓取标签间的内容

    # coding=utf-8  
    import re  
    import urllib
    
    html = u'<title>欢迎走进Python攻防系列专栏</title>' 
    title = re.findall(r'<title>(.*?)</title>', html)
    for i in title:
        print(i)
    

    输出结果为:

    在这里插入图片描述


    3.抓取超链接标签间的内容

    # coding=utf-8  
    import re  
    import urllib.request
    
    url = "http://www.baidu.com/"  
    content = urllib.request.urlopen(url).read()
    #print(content)
    
    #获取完整超链接
    res = r"<a.*?href=.*?<\/a>"
    urls = re.findall(res, content.decode('utf-8'))
    for u in urls:
        print(u)
    
    #获取超链接<a>和</a>之间内容
    res = r'<a .*?>(.*?)</a>'  
    texts = re.findall(res, content.decode('utf-8'), re.S|re.M)  
    for t in texts:
        print(t)
    

    输出结果部分内容如下所示,中文编码是常见的问题,我们需要注意下,比如utf-8编码。

    在这里插入图片描述


    4.抓取超链接标签的url

    # coding=utf-8  
    import re
    
    content = '''
    <a href="http://news.baidu.com" name="tj_trnews" class="mnav">新闻</a>
    <a href="http://www.hao123.com" name="tj_trhao123" class="mnav">hao123</a>
    <a href="http://map.baidu.com" name="tj_trmap" class="mnav">地图</a>
    <a href="http://v.baidu.com" name="tj_trvideo" class="mnav">视频</a>
    '''
    
    res = r"(?<=href=\").+?(?=\")|(?<=href=\').+?(?=\')"
    urls = re.findall(res, content, re.I|re.S|re.M)
    for url in urls:
        print(url)
    

    获取的超链接输出结果如下图所示:

    在这里插入图片描述


    5.抓取图片超链接标签的url和图片名称
    在HTML中,我们可以看到各式各样的图片,其图片标签的基本格式为“< img src=图片地址 />”,只有通过抓取了这些图片的原地址,才能下载对应的图片至本地。那么究竟怎么获取图片标签中的原图地址呢?下面这段代码就是获取图片链接地址的方法。

    content = '''<img alt="Python" src="http://www.yangxiuzhang.com/eastmount.jpg" />'''
    urls = re.findall('src="(.*?)"', content, re.I|re.S|re.M)
    print(urls)
    # ['http://www.yangxiuzhang.com/eastmount.jpg']
    

    其中图片对应的原图地址为“http://www.xxx.com/eastmount.jpg”,它对应一张图片,该图片是存储在“www.xxx.com”网站服务器端的,最后一个“/”后面的字段为图片名称,即为“eastmount.jpg”。那么如何获取url中最后一个参数呢?

    content = '''<img alt="Python" src="http://www..csdn.net/eastmount.jpg" />'''
    urls = 'http://www..csdn.net/eastmount.jpg'
    name = urls.split('/')[-1]  
    print(name)
    # eastmount.jpg
    

    更多正则表达式的用法,读者结合实际情况进行复现。



    三.Python Web编程

    这里的Web编程并不是利用Python开发Web程序,而是用Python与Web交互,获取Web信息。主要内容包括:

    • urllib、urllib2、requests
    • 爬虫介绍
    • 利用Python开发一个简单的爬虫

    (一) urllib\urllib2

    urllib是Python用于获取URL(Uniform Resource Locators,统一资源定址器)的库函数,可以用来抓取远程数据并保存,甚至可以设置消息头(header)、代理、超时认证等。urllib模块提供的上层接口让我们像读取本地文件一样读取www或ftp上的数据。它比C++、C#等其他编程语言使用起来更方便。其常用的方法如下:

    urlopen(url, data=None, proxies=None)
    该方法用于创建一个远程URL的类文件对象,然后像本地文件一样操作这个类文件对象来获取远程数据。参数url表示远程数据的路径,一般是网址;参数data表示以post方式提交到url的数据;参数proxies用于设置代理。urlopen返回一个类文件对象。

    # -*- coding:utf-8 -*-
    import urllib.request
    
    url = "http://www.baidu.com"
    content = urllib.request.urlopen(url)
    print(content.info())        #头信息
    print(content.geturl())      #请求url
    print(content.getcode())     #http状态码
    

    该段调用调用urllib.urlopen(url)函数打开百度链接,并输出消息头、url、http状态码等信息,如下图所示。

    在这里插入图片描述

    urlretrieve(url, filename=None, reporthook=None, data=None)
    urlretrieve方法是将远程数据下载到本地,参数filename指定了保存到本地的路径,如果省略该参数,urllib会自动生成一个临时文件来保存数据;参数reporthook是一个回调函数,当连接上服务器,相应的数据块传输完毕时会触发该回调,通常使用该回调函数来显示当前的下载进度;参数data指传递到服务器的数据。

    # -*- coding:utf-8 -*-
    import urllib.request
    
    url = 'https://www.baidu.com/img/bd_logo.png'
    path = 'test.png'
    urllib.request.urlretrieve(url, path)
    

    它将百度Logo图片下载至本地。

    在这里插入图片描述

    注意:Python3和Python2代码有少许区别,Python2直接调用urllib.urlopen()。


    (二) requests

    requests模块是用Python语言编写的、基于urllib的第三方库,采用Apache2 Licensed开源协议的http库。它比urllib更加方便,既可以节约大量的工作,又完全满足http测试需求。requests是一个很实用的Python http客户端库,编写爬虫和测试服务器响应数据时经常会用到。推荐大家从 requests官方网站 进行学习,这里只做简单介绍。

    假设读者已经使用“pip install requests”安装了requests模块,下面讲解该模块的基本用法。

    1.发送网络请求

    r = requests.get("http://www.eastmountyxz.com")
    r = requests.post("http://www.eastmountyxz.com")
    r = requests.put("http://www.eastmountyxz.com")
    r = requests.delete("http://www.eastmountyxz.com")
    r = requests.head("http://www.eastmountyxz.com")
    r = requests.options("http://www.eastmountyxz.com")
    

    2.为URL传递参数

    import requests
    payload = {'key1':'value1', 'key2':'value2'}
    r = requests.get('http://httpbin.org/get', params=payload)
    print(r.url)
    

    输出结果如下图所示,将参数进行了拼接。

    在这里插入图片描述

    3.响应内容

    import requests
    
    r = requests.get('http://www.eastmountyxz.com')
    print(r.text)
    print(r.encoding)
    

    4.二进制响应内容

    r = requests.get('http://www.eastmountyxz.com')
    print(r.content)
    

    5.定制请求头

    url = 'http://www.ichunqiu.com'
    headers = {'content-type':'application/json'}
    r = requests.get(url, headers=headers)
    

    注意:headers中可以加入cookies

    6.复杂的POST请求

    payload = {'key1':'value1', 'key2':'value2'}
    r = requests.post('http://httpbin.org/post', data=payload)
    

    7.响应状态码和响应头

    r = requests.get('http://www.ichunqiu.com')
    r.status_code
    r.headers
    

    8.Cookies

    r.cookies
    r.cookies['example_cookie_name']
    

    9.超时

    requests.get('http://www.ichunqiu.com', timeout=0.001)
    

    10.错误和异常
    遇到网络问题(如:DNS查询失败,拒绝链接等)时,requests会抛出一个ConnectionError异常;遇到罕见的无效HTTP响应式时,requests则会抛出一个HTTPError异常;若请求超时,会抛出一个Timeout异常。


    (三) 网络爬虫案例

    网络爬虫又称为网页蜘蛛,网络机器人,网页追逐者,是按照一定规则自动抓取万维网信息的程序或脚本。最大好处是批量且自动化获得和处理信息,对于宏观或微观的情况都可以多一个侧面去了解。在安全领域,爬虫能做目录扫描、搜索测试页面、样本文档、管理员登录页面等。很多公司(如绿盟)的Web漏洞扫描也通过Python来自动识别漏洞。

    下面两个案例虽然简单,却能解决很多人的问题,希望读者可以独立完成。

    1.设置消息头请求(流量分析相关)
    假设我们需要抓取360百科的乔布斯信息,如下图所示。

    在这里插入图片描述

    传统的爬虫代码会被网站拦截,从而无法获取相关信息。

    # -*- coding: utf-8 -*-
    import requests
    
    url = "https://baike.so.com/doc/24386561-25208408.html"
    content = requests.get(url, headers=headers)
    print(content.text)
    

    右键审查元素(按F12),在Network中获取Headers值。headers中有很多内容,主要常用的就是user-agent 和 host,它们是以键对的形式展现出来,如果user-agent 以字典键对形式作为headers的内容,就可以反爬成功。

    在这里插入图片描述

    代码如下:

    # -*- coding: utf-8 -*-
    import requests
    
    #添加请求头
    url = "https://baike.so.com/doc/24386561-25208408.html"
    headers = {
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36'
    }
    content = requests.get(url, headers=headers)
    content.encoding='utf-8'
    print(content.text)
    

    输出结果如下图所示:

    在这里插入图片描述

    有部分网站会返回Json格式的数据,我们可以通过json模块进行处理。核心代码如下:

    data = json.loads(r.text)
    print(data['result'])
    name_len = len(data['result'])
    for i range(name_len):
    	print(data['result'][i]['courseName'])
    

    2.提交数据请求(盲注相关)
    部分网站如果涉及到翻页,需要获取所有页码的信息,最传统的方法是定义一个函数,然后设计一个循环,一次遍历不同页面的内容实现。核心代码如下:

    url_start = ""
    url_end = ""
    
    def lesson(url):
        ....
    
    for i in range(1,9)
    	url = url_start+ str(i) + url_end
    	lesson(url)
    

    但如果URL始终保持不变,就需要我们深入地分析,或通过Selenium模拟浏览器抓取,这里提供一个技巧性比较强的方法。

    假设我们想爬取某网站的公开信息,但通过翻页发现这个页面的url地址是不变的,我们大致就可以判断出,中间表格的数据是通过js动态加载的,我们可以通过分析抓包,找到真实的请求地址。目标网址如下:

    在这里插入图片描述

    通过审查元素可以发现有个pagesnum变量,它标记为我们的页码,所以这里需要通过requests提交变量数据,就能实现翻页。

    在这里插入图片描述

    核心代码如下:

    # -*- coding: utf-8 -*-
    import requests
    import time
    import datetime
    
    url = "http://www.hshfy.sh.cn/shfy/gweb/ktgg_search_content.jsp?"
    
    page_num = 1
    date_time = datetime.date.fromtimestamp(time.time())
    print(date_time)
    
    data = {
        "pktrqks": date_time,
        "ktrqjs": date_time,
        "pagesnum": page_num
    }
    print(data)
    
    content = requests.get(url, data, timeout=3)
    content.encoding='gbk'
    print(content.text)
    


    四.Python套接字通信

    (一) 什么是C/S架构呢?

    Python网络通讯主要是C/S架构的,采用套接字实现。C/S架构是客户端(Client)和服务端(Server)架构,Server唯一的目的就是等待Client的请求,Client连上Server发送必要的数据,然后等待Server端完成请求的反馈。

    C/S网络编程:Server端进行设置,首先创建一个通信端点,让Server端能够监听请求,之后就进入等待和处理Client请求的无限循环中。Client编程相对Server端编程简单,只要创建一个通信端点,建立到服务器的链接,就可以提出请求了。


    (二) 什么是套接字?

    套接字是一种具有之前所说的“通信端点”概念的计算网络数据结构,网络化的应用程序在开始任何通信都必须创建套接字。相当于电话插口,没它无法通信,这个比喻非常形象。Python支持:AF_UNIX、AF_NETLINK、AF_INET,其中AF_INET是基于网络的套接字。

    套接字起源于20世纪70年代加州伯克利分校版本的Unix,即BSD Unix,又称为“伯克利套接字”或“BSD套接字”。最初套接字被设计用在同一台主机上多个应用程序之间的通讯,这被称为进程间通讯或IPC。

    套接字分两种:基于文件型和基于网络

    • 第一个套接字家族为AF_UNIX,表示地址家族:UNIX。包括Python在内的大多数流行平台上都使用术语“地址家族”及其缩写AF。由于两个进程都运行在同一台机器上,而且这些套接字是基于文件的,所以它们的底层结构是由文件系统来支持的。可以理解为同一台电脑上,文件系统确实是不同的进程都能进行访问的。
    • 第二个套接字家族为AF_INET,表示地址家族:Internet。还有一种地址家族AF_INET6被用于网际协议IPv6寻址。Python 2.5中加入了一种Linux套接字的支持:AF_NETLINK(无连接)套接字家族,让用户代码与内核代码之间的IPC可以使用标准BSD套接字接口,这种方法更为精巧和安全。

    如果把套接字比作电话的查看——即通信的最底层结构,那主机与端口就相当于区号和电话号码的一对组合。一个因特网地址由网络通信必须的主机与端口组成。而且另一端一定要有人接听才行,否则会提示“对不起,您拨打的电话是空号,请查询后再拨”。同样你也可能会遇到如“不能连接该服务器、服务器无法响应”等。合法的端口范围是0~65535,其中小于1024端口号为系统保留端口。


    (三) 面向连接与无连接

    1.面向连接 TCP
    通信之前一定要建立一条连接,这种通信方式也被成为“虚电路”或“流套接字”。面向连接的通信方式提供了顺序的、可靠地、不会重复的数据传输,而且也不会被加上数据边界。这意味着,每发送一份信息,可能会被拆分成多份,每份都会不多不少地正确到达目的地,然后重新按顺序拼装起来,传给正等待的应用程序。

    实现这种连接的主要协议就是传输控制协议TCP。要创建TCP套接字就得创建时指定套接字类型为SOCK_STREAM。TCP套接字这个类型表示它作为流套接字的特点。由于这些套接字使用网际协议IP来查找网络中的主机,所以这样形成的整个系统,一般会由这两个协议(TCP和IP)组合描述,即TCP/IP。

    2.无连接 UDP
    无需建立连接就可以通讯。但此时,数据到达的顺序、可靠性及不重复性就无法保障了。数据报会保留数据边界,这就表示数据是整个发送的,不会像面向连接的协议先拆分成小块。它就相当于邮政服务一样,邮件和包裹不一定按照发送顺序达到,有的甚至可能根本到达不到。而且网络中的报文可能会重复发送。那么这么多缺点,为什么还要使用它呢?由于面向连接套接字要提供一些保证,需要维护虚电路连接,这都是严重的额外负担。数据报没有这些负担,所有它会更”便宜“,通常能提供更好的性能,更适合某些场合,如现场直播要求的实时数据讲究快等。

    实现这种连接的主要协议是用户数据报协议UDP。要创建UDP套接字就得创建时指定套接字类型为SOCK_DGRAM。这个名字源于datagram(数据报),这些套接字使用网际协议来查找网络主机,整个系统叫UDP/IP。


    (四) socket()模块函数

    使用socket模块的socket()函数来创建套接字。语法如下,其中socket_family不是AF_VNIX就是AF_INET,socket_type可以是SOCK_STREAM或者SOCK_DGRAM,protocol一般不填,默认值是0。

    • socket(socket_family, socket_type, protocol=0)

    创建一个TCP/IP套接字的语法如下:

    • tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    同样创建一个UDP/IP套接字的语法如下:

    • udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    由于socket模块中有太多属性,所以使用"from socket import *"语句,把socket模块里面的所有属性都带到命名空间中,大幅缩短代码。调用如下:

    • tcpSock = socket(AF_INET, SOCK_STREAM)

    下面是最常用的套接字对象方法:

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    提示:在运行网络应用程序时,如果能够使用在不同的电脑上运行服务器和客户端最好不过,它能让你更好理解通信过程,而更多的是localhost或127.0.0.1。


    (五) TCP通信实例

    1.服务器 tcpSerSock.py
    核心操作如下:
    在这里插入图片描述

    # -*- coding: utf-8 -*- 
    from socket import *
    from time import ctime
    
    HOST = 'localhost'          #主机名
    PORT =  21567               #端口号
    BUFSIZE = 1024              #缓冲区大小1K
    ADDR = (HOST,PORT)
    
    tcpSerSock = socket(AF_INET, SOCK_STREAM)
    tcpSerSock.bind(ADDR)       #绑定地址到套接字
    tcpSerSock.listen(5)        #监听 最多同时5个连接进来
    
    while True:                 #无限循环等待连接到来
        try:
            print('Waiting for connection ....')
            tcpCliSock, addr = tcpSerSock.accept()  #被动接受客户端连接
            print('Connected client from : ', addr)
    
            while True:
                data = tcpCliSock.recv(BUFSIZE)     #接受数据
                if not data:
                    break
                else:
                    print('Client: ',data)
                info = ('[%s] %s' %(ctime(),data))
                info = bytes(info, encoding = "utf8")
                tcpCliSock.send(info) #时间戳
    
        except Exception as e:
            print('Error: ',e)
    tcpSerSock.close()          #关闭服务器
    tcpCliSock.close()
    

    2.客户端 tcpCliSock.py
    核心操作如下:
    在这里插入图片描述

    # -*- coding: utf-8 -*- 
    from socket import *
    
    HOST = 'localhost'          #主机名
    PORT =  21567               #端口号 与服务器一致
    BUFSIZE = 1024              #缓冲区大小1K
    ADDR = (HOST,PORT)
    
    tcpCliSock = socket(AF_INET, SOCK_STREAM)
    tcpCliSock.connect(ADDR)    #连接服务器
    
    while True:                 #无限循环等待连接到来
        try:
            data = input('>')
            data = bytes(data, encoding = "utf8")
            print(data,type(data))
            if not data:
                break
            tcpCliSock.send(data)            #发送数据
            data = tcpCliSock.recv(BUFSIZE)  #接受数据
            if not data:
                break
            print('Server: ', data)
        except Exception as e:
            print('Error',e)
            
    tcpCliSock.close()          #关闭客户端
    

    由于服务器被动地无限循环等待连接,所以需要先运行服务器,再开客户端。又因为我的Python总会无法响应,所以采用cmd运行服务器Server程序,Python IDLE运行客户端进行通信。运行结果如下图所示:

    在这里插入图片描述

    另一种方法同时打开Python3.6和Python3.7进行通信,如下图所示。

    在这里插入图片描述

    建议创建线程来处理客户端请求,SocketServer模块是一个基于socket模块的高级别的套接字通信模块,支持新的线程或进程中处理客户端请求。同时建议在退出和调用服务器close()函数时使用try-except语句。

    那么,如何反弹shell程序呢?
    使用 from subprocess import Popen, PIPE 导入库,调用系统命令实现。核心代码如下,后续Windows漏洞复现深入讲解后,你就更好理解这部分代码了。

    在这里插入图片描述



    五.总结

    希望这篇文章对你有所帮助,这是Python黑帽第二篇博客,后续作者也将继续深入学习,制作一些常用的小工具供大家交流。最后,真诚地感谢您关注“娜璋之家”公众号,也希望我的文章能陪伴你成长,希望在技术路上不断前行。文章如果对你有帮助、有感悟,就是对我最好的回报,且看且珍惜!再次感谢您的关注,也请帮忙宣传下“娜璋之家”,哈哈~初来乍到,还请多多指教。

    前文:

    三尺讲台,三寸笔,三千桃李。
    十年树木,十年风,十万栋梁。
    祝天下所有老师节日快乐,也是自己第五个教师节。从我们家女神2011年去山村支教,到我2014年走上支教的讲台,再到2016年真正成为大学老师,感恩这一路走来,也祝女神和我节日快乐,感谢所有人的祝福和帮助。

    无论未来是否继续当老师,秀璋都会一直牢记当老师的那份美好,记住分享知识的魅力,记住你们脸上洋溢着灿烂的笑容,我也会线上分享更好的文章,真心希望帮助更多人,把这些年所学所做分享出来。不忘初心,感恩前行。真是奇幻,师范出身的她没成为老师,程序猿却去教书了,哈哈!

    在这里插入图片描述

    (By:娜璋AI安全之家 Eastmount 2020-09-11 夜于武汉)


    参考文献:

    展开全文
  • 上面的第个小部分其实是一个文件体的结构,最后会以–分割符–结尾,表示请求体结束。 通过上面分析,可以知道要发送一个multipart/form-data的请求,其实任何支持post请求的工具或语言都可以支持,只是自己要...
  • 制作一张简单的网页(HTML+CSS+JS)【1】

    万次阅读 多人点赞 2017-02-06 12:18:20
    .认识标签 接下来我把一些常用标签整理出来,列在下面。 1. 段落 2. 文章标题,其中的x分为1~6,写的时候讲x改为1~6,并且从1开始字体逐级变小 3. 该部分变为斜体 4. 该部分变为粗体 5. 只是为了能够...
  • 、如何解决导航栏级菜单栏被图片覆盖的问题 1.设置css属性:z-index(可用于将一个元素放在另一个元素之后) (1)auto:默认,堆叠顺序和父元素相等 (2)number:设置元素的堆叠顺序 (3)inherit...
  • 历久而新,我的新书《第行代码》已出版!

    万次阅读 多人点赞 2016-11-23 09:49:08
    《第行代码》中的内容我相信很多人都会非常感兴趣,因为里面确实加入了很多很新的知识。略粗概括一下的话,新增内容就包括有Android Studio、Gradle、Material Design、运行时权限、多窗口模式、RecyclerView、...
  • Python识别图片中的文字

    万次阅读 多人点赞 2020-05-17 12:49:59
    但是当我们想用到里面的文字时,还是要一个一个打出来。那么我们能不能直接识别图片中的文字呢?答案是肯定的。 、Tesseract 文字识别是ORC的一部分内容,ORC的意思是光学字符识别,通俗讲就是文字识别。...
  • 一文搞懂图像值化算法

    万次阅读 多人点赞 2021-03-25 12:56:14
    图像值化( Image Binarization)就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果的过程。值图像每个像素只有两种取值:要么纯黑,要么纯白。
  • 图解进制(篇1)

    千次阅读 2021-08-12 22:03:38
    有符号数转进制之后,其原来对应的值位真值,带符号的进制转为其他进制之后的值称为***形式值***。 图解 注:红色的数字1是十进制-3转进制之后的符号位 原码 符号位+真值的绝对值,即是带符号的进制数 ...
  • 基于触发器的三位进制同步减法计数器无效态000/110 1 课程设计的目的与作用 掌握用multisim 的电路仿真程序 熟悉同步计数器工作原理和逻辑功能 熟悉计数器电路的分析和设计方法 掌握161 芯片集联成任意进制同步...
  • 10万208道Java经典面试题总结(附答案)

    万次阅读 多人点赞 2021-08-01 16:05:55
    关注公众号【哪吒编程】,回复 面试题 ,获取《10万208道Java经典面试题总结(附答案)》pdf,背题更方便,一文在手,面试我有 前言 最近有很多粉丝问我,有什么方法能够快速提升自己,通过阿里、腾讯、字节跳动、...
  • 前文分享了Wireshark抓包原理知识,并结合NetworkMiner工具抓取了图像资源和用户名密码,本文将讲解Python网络攻防相关基础知识,包括正则表达式、Web编程和套接通信。本文参考了爱春秋ADO老师的课程内容,这里也...
  • 数据表操作 数据库管理系统中, 可以有很多库, 每个数据库中可以包括多数据表 查看表: show tables; 创建表: create table 表名(字段名1 类型,字段名2 类型)engine=innodb default charset=utf8; 创建表: 如果表不...
  • 图像处理一之-摄像头值化处理-(什么是值化)

    万次阅读 多人点赞 2019-02-27 22:53:20
    图像值化binary image 什么是值化:值化是图像分割的一种最简单的方法。值化可以把灰度图像转换成值图像。把大于某个临界灰度值(阈值)的像素灰度设为灰度极大值(255),把小于这个值的像素灰度设为灰度极...
  • if img1[rows,cols]>=127:#值化处理,把一整图片的像素处理成只有0和1 img1[rows,cols]=1 else: img1[rows,cols]=0#这里选择的临界点是127,正好是0-255的中间值 imgs[i,rows*28+cols]=img1[rows,cols]#把...
  • 情况:(XP系统)相对来说,XP系统的调节更加简单。 我们只需要按照以下步骤操作,就可以调整字体显示的清晰度。 首先,用右键点击桌面,然后选择“属性”一栏。 其次,在跳出的窗口中,在最上方找到“外观”一栏...
  • 数据库字段数据(昵称)排序,规则: 数字>英文字母>汉字首字母 兼容繁体排序  在日常运用中我们经常会遇到要对数据进行排序,特别是对昵称的排序。最近被要求对昵称排序,刚拿到手的时候开始还觉得挺简单的,...
  • 转眼间今天又是五一劳动节了,作为程序员,...回想自己刚进入这一行的时候,自己当时一开始也是很菜鸟,连进制怎么计算,怎么转换等等这些都不了解,更谈不上其他进制的计算和转换了,但是随着自己技术的积累,再...
  • 基于opencv的数字识别

    万次阅读 多人点赞 2018-09-12 15:44:29
    最近学习了opencv,然后想通过其对图片上的...加载需要识别的图片,然后将其转化为值图 寻找数字的外轮廓,切记不可以找全部轮廓,否则一个数字将会有多个轮廓,识别起来就很麻烦了 对轮廓进行排序,因为使用...
  • 一招教你利用画图工具修改图片上的文字腾讯视频/爱奇艺/优酷/外卖 充值4折起相信很多小伙伴在日常办公中都会用到...小编接下来会将图片中的“激”改为“谢”。2. 接着,点击上方菜单栏中的颜色选取器工具(如下图...
  • Python简单又好玩的项目推荐!【持续更新】

    万次阅读 多人点赞 2020-06-28 08:59:40
    十行代码实现井棋游戏 十五行代码图片转字符画 三十行代码爬取任意百度图片 Hello!大家好哇!我是努力赚钱买生发水的灰小猿! 最近在做Python项目开发的时候愣是发现了好多好玩的小项目,并且都是代码量...
  • 游戏的流程是这样的:在界面上生成5个数1~5并显示在随机的位置上,点击第一个数字,其他数字会显示成白块数字消失,玩家可以通过记住数字的显示的位置点击按顺序消除白块,直到白块消除完,游戏成功。 效果图如下...
  • waitkey方法可以绑定按键保证画面的收放,通过q键退出摄像 k = cv2.waitKey(1) if k == '27': break #或者得到800个样本后退出摄像,这里可以根据实际情况修改数据量,实际测试后800的效果是比较理想的 elif count...
  • 5万、97 张图总结操作系统核心知识点

    万次阅读 多人点赞 2020-07-14 09:19:25
    跳转(jump):从指令中抽取一个,把这个复制到程序计数器(PC) 中,覆盖原来的值 进程和线程 关于进程和线程,你需要理解下面这脑图中的重点 进程 操作系统中最核心的概念就是 进程,进程是对正在运行中的程序...
  • 数字电子技术之逻辑函数的化简及表示

    千次阅读 多人点赞 2020-05-24 23:21:40
    逻辑功能中简单概括得出的逻辑函数,往往不是最表达式,根据这样的非最式来实现电路,系统会过于复杂,成本过高,同时,电路运行的安全性和可靠性也无法得到保障。 为了降低系统成本,提高工作可靠性,应在不...
  • 文章目录前言一、SQL简述1.SQL的概述2.SQL的优点3.SQL的分类、数据库的三大范式三、数据库的数据类型1.整数类型2.浮点数类型和定点数类型九、MySQL数据表简单查询1.简单查询概述2.查询所有字段(方法不唯一只是...
  • 8000干货:那些很厉害的人是怎么构建知识体系的

    万次阅读 多人点赞 2019-09-29 11:18:27
    本文约8000,正常阅读需要15~20分钟。读完本文可以获得如下收益: 分辨知识和知识体系的差别 理解如何用八大问发现知识的连接点; 掌握致用类知识体系的构建方法; 能够应用甜蜜区模型找到特定领域来构建知识体系...
  • % 测试数据 执行以上代码运行结果如下: 下面读取几训练和测试集的图片,显示原始图片帮助我们清楚该数据集的实际情况,按照两行显示:第一行为训练图片,第行为测试图片,该部分代码如下: figure; % 显示训练...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 315,795
精华内容 126,318
关键字:

张的二简字