1.1  算法目的

现在网络架构一般是Client-Server架构,所以网络流量一般是分 C-S 和 S-C 两个方向。tcpdump等抓包工具获取的pcap包,两个流向的数据没有被区分。流量方向的区分有什么好处?这种拆分至少有两个好处,一是在抓包基础上定制数据包,可以支持单独修改一个流向的IP,MAC等字段。二是实际测试被测设备的时候,可以将两个流向的流量通过不同的端口发送出来。Tcpprep支持了这种拆分(早先版本这部分功能混合在tcpreplay中,后来独立拆分成为tcpprep工具)

Tcpprep3.4.4 支持了以下流量拆分的参数

-a, --auto=str       Auto-split mode   自动模式

-c, --cidr=str        CIDR-split mode   子网匹配模式

-r, --regex=str      Regex-split mode  正则匹配模式

-p, --port           Port-split mode  端口匹配模式

-e, --mac=str        Source MAC split mode  MAC匹配模式

-reverse            Matches to be client instead of server

其中,auto 模式支持5种子模式

Auto = bridge|router|client|server|first

另外,下面两个参数也是流量拆分相关的,当 auto=router 时,用户可选下面的参数配合,

-m, --minmask=num          Minimum network mask length in auto mode

-M, --maxmask=num          Maximum network mask length in auto mode

1.2  算法思想

考虑一个问题,如何判断一个packets 是 C->S 还是 S->C ?

一种思路是用户指定,比如用户指定某个IP或MAC是C->S,或者更进一步,IP匹配某个正则表达式就是C->S,这种情况下,实际上是用户‘手动’判断,不需要特别的算法。上面各种模式中,除了 auto 模式,其余模式都属于这类‘手动’方法,前提是用户必须对这些pcap包非常熟悉。

那么,如何实现‘自动’识别C->S还是S->C?

答案是,利用网络包的协议特征。因为一般的网络流量都基于TCP或UDP,以TCP为例,SYN,ACK等标志位就可以支持流量方向的判断了。Tcpprep3.4.4 使用到的协议特征

包括如下:

Tcpprep3.4.4 使用到的协议特征

C->S 客户端特征:

Sending a TCP Syn packet to another host

Making a DNS request

Recieving an ICMP port unreachable

S->C 服务端特征:

Sending a TCP Syn/Ack packet to another host

Sending a DNS Reply

Sending an ICMP port unreachable

所以,自动流量拆分算法的基本思路是:解析pcap的每一个packet,判断其特征位的值,对比客户端特征和服务端特征,得到一个比较结果,通过比较结果来判断流量方向。

考虑上述思路,是逐个packet单独计算,但是,一个pcap的许多packet,其IP,MAC,PORT等都是相同的,换句话说,是属于一个方向。因此,特征匹配运算的基本单位,不应该是一个packet,而应该是以IP为单位。

Tcpprep在实现的时候根据IP大小建立了一颗红黑树,相同IP的packet不再单独生成节点,而是将特征匹配的运算结果累计在相同IP的节点上,最后某个IP属于哪个方向是由红黑树的对应节点的累计结果得到的。该算法需要大量查找和插入操作,用红黑树比较合适。

上面两段落的描述即是 auto=bridge 的算法思路,也是自动拆分流量的基本算法。其他4种自动拆分方向算法都建立在bridge之上,它们之间的关系是,都先使用 bridge 模式运算得到红黑树,对于树上还无法归入 C->S 或 S->C 的节点,再使用对应模式的策略。在auto=server ,就是剩下的节点全部视为 S->C,在 auto=client ,就是剩下的节点全部视为

C->S,在 auto=first ,就是剩下的节点全部视为与第一个packet 方向一致。

auto=router 的策略比较复杂,使用到了 CIDR 这种数据结构,CIDR其实是一个链表,每个链表节点存放一个 cidr 地址,它的思路是,如果这个无法判断的节点的IP刚好落在一个其余IP都是S->C的cidr里,那么它就是 S->C,相反,如果改节点的IP刚好落在某个cidr,而树上其他在该cidr 里的节点都是 C->S,那么该节点也是 C->S。

1.3  算法流程

Tcpprep 主流程

Process(pcap) 流程

1.1  算法实现

1.1.1  数据结构

/*tcpprep 控制结构*/

struct tcpprep_opt_s {

pcap_t *pcap;  /*pcap包控制句柄*/

int verbose;

char *tcpdump_args;

tcpr_cache_t *cachedata; /*缓存数据子控制结构,具体见缓存算法相关描述*/

tcpr_cidr_t *cidrdata;/*cidir 链表控制结构*/

char *maclist; /*mac 地址列表,适用于mode = MAC*/

tcpr_xX_t xX; /*exclude ip 列表*/

tcpr_bpf_t bpf;

tcpr_services_t services;

char *comment; /* cache file comment */

int nocomment; /* don't include the cli in the comment */

int mode;      /* mode */

int automode;  /* our auto mode */

int min_mask; /*这两个适用于 auto=router */

int max_mask;

double ratio; /*server 和 client 的比率*/

regex_t preg; /*适用于 mode = grex */

int nonip;

};

typedef struct tcpprep_opt_s tcpprep_opt_t;

/*红黑树节点控制结构*/

typedef struct tcpr_tree_s {

RB_ENTRY(tcpr_tree_s) node; /*在 redblack.h 中定义*/

int family;

union {

unsigned long ip;           /* ip/network address in network byte order */

struct tcpr_in6_addr ip6;

} u;

u_char mac[ETHER_ADDR_LEN]; /* mac address of system */

int masklen;                /* CIDR network mask length */

/*下面这两个变量就是用来累计客户端和服务端特征的变量*/

int server_cnt;             /* count # of times this entry was flagged server */

int client_cnt;             /* flagged client */

/*运算结果是什么方向存放在下面这个type里*/

int type;                   /* 1 = server, 0 = client, -1 = undefined */

} tcpr_tree_t;

/* *根节点*/

typedef struct tcpr_data_tree_s {

tcpr_tree_t *rbh_root;

} tcpr_data_tree_t;

1.1.2  主要函数实现

/**

* 使用 libpcap library 解析 packets

* 根据流量拆分算法运算结果生成cache file,去掉了部分无关代码

*/

static COUNTER

process_raw_packets(pcap_t * pcap)

{

ipv4_hdr_t *ip_hdr = NULL; /*ipv4 头结构,定义在 libpcap library*/

ipv6_hdr_t *ip6_hdr = NULL; /*ipv6 头结构*/

eth_hdr_t *eth_hdr = NULL; /*以太帧头结构*/

struct pcap_pkthdr pkthdr; /*pcap头控制结构,定义在 libpcap library*/

const u_char *pktdata = NULL;

COUNTER packetnum = 0;

int l2len, cache_result = 0;

u_char ipbuff[MAXPACKET], *buffptr;

tcpr_dir_t direction; /*流量方向*/

/*下面是主循环*/

while ((pktdata = pcap_next(pcap, &pkthdr)) != NULL) {

packetnum++;

/*下面检查exclude list,如果匹配,缓存写入 DON’T_SEND,continue处理下一个*/

/* look for include or exclude LIST match */

if (options.xX.list != NULL) {

if (options.xX.mode < xXExclude) {

if (!check_list(options.xX.list, packetnum)) {

add_cache(&(options.cachedata), DONT_SEND, 0);

continue;

}

}

else if (check_list(options.xX.list, packetnum)) {

add_cache(&(options.cachedata), DONT_SEND, 0);

continue;

}

}

/*获取ip头,如果获取不到,除非用户设定了MAC模式,在MAC模式下,还可以通过mac值判定方向,否则直接将 type=NONIP写入缓存,continue在下一个packet*/

eth_hdr = (eth_hdr_t *)pktdata;

if (options.mode != MAC_MODE) {

buffptr = ipbuff;

/* 获取IPv4 */

if ((ip_hdr = (ipv4_hdr_t *)get_ipv4(pktdata, pkthdr.caplen,

pcap_datalink(pcap), &buffptr))) {

dbg(2, "Packet is IPv4");

}

/* 获取IPv6 */

else if ((ip6_hdr = (ipv6_hdr_t *)get_ipv6(pktdata, pkthdr.caplen,

pcap_datalink(pcap), &buffptr))) {

dbg(2, "Packet is IPv6");

}

/* we're something else... */

else { /*都获取不到,写对应packet缓存为 nonip*/

if (options.mode != AUTO_MODE) {

dbg(3, "Adding to cache using options for Non-IP packets");

add_cache(&options.cachedata, SEND, options.nonip);

}

/* go to next packet */

continue;

}

/*下面判定 exclude ip 列表,如果匹配,则缓存写入 DON’T_SEND,continue处理下一个packet*/

l2len = get_l2len(pktdata, pkthdr.caplen, pcap_datalink(pcap));

/* look for include or exclude CIDR match */

if (options.xX.cidr != NULL) {

if (ip_hdr) {

if (!process_xX_by_cidr_ipv4(options.xX.mode, options.xX.cidr, ip_hdr)) {

add_cache(&options.cachedata, DONT_SEND, 0);

continue;

}

} else if (ip6_hdr) {

if (!process_xX_by_cidr_ipv6(options.xX.mode, options.xX.cidr, ip6_hdr)) {

add_cache(&options.cachedata, DONT_SEND, 0);

continue;

}

}

}

}

/*下面分别处理各种拆分模式*/

switch (options.mode) {

case REGEX_MODE: /*正则表达式模式*/

if (ip_hdr) {/*拿源IP跟用户设定的regex匹配得到结果,写入缓存*/

direction = check_ipv4_regex(ip_hdr->ip_src.s_addr);

} else if (ip6_hdr) {

direction = check_ipv6_regex(&ip6_hdr->ip_src);

}

cache_result = add_cache(&options.cachedata, SEND, direction);

break;

case CIDR_MODE: /*cidr列表模式*/

if (ip_hdr) {/*拿源IP跟用户设定的cidr列表匹配得到结果,写入缓存*/

direction = check_ip_cidr(options.cidrdata, ip_hdr->ip_src.s_addr) ? TCPR_DIR_C2S : TCPR_DIR_S2C;

} else if (ip6_hdr) {

direction = check_ip6_cidr(options.cidrdata, &ip6_hdr->ip_src) ? TCPR_DIR_C2S : TCPR_DIR_S2C;

}

cache_result = add_cache(&options.cachedata, SEND, direction);

break;

case MAC_MODE: /*MAC模式*/

direction = macinstring(options.maclist, (u_char *)eth_hdr->ether_shost);

cache_result = add_cache(&options.cachedata, SEND, direction);

break;

case AUTO_MODE: /*auto模式

比如 auto=bridge,会分成两次运行,第一次检测 auto_mode,创建红黑树,第二次检测 bridge_mode,对树做运算并将结果写入缓存,router等模式也是一样的处理*/

/* first run through in auto mode: create tree */

if (options.automode != FIRST_MODE) {

if (ip_hdr) {

add_tree_ipv4(ip_hdr->ip_src.s_addr, pktdata);

} else if (ip6_hdr) {

add_tree_ipv6(&ip6_hdr->ip_src, pktdata);

}

} else {

if (ip_hdr) {

add_tree_first_ipv4(pktdata);

} else if (ip6_hdr) {

add_tree_first_ipv6(pktdata);

}

}

break;

case ROUTER_MODE:

/* 具体到router,第二次运行,根据树的结果生成cache

*/

if (ip_hdr) {

cache_result = add_cache(&options.cachedata, SEND,

check_ip_tree(options.nonip, ip_hdr->ip_src.s_addr));

} else {

cache_result = add_cache(&options.cachedata, SEND,

check_ip6_tree(options.nonip, &ip6_hdr->ip_src));

}

break;

case BRIDGE_MODE:

/* 具体到bridge,第二次运行,根据树的结果生成cache

*/

if (ip_hdr) {

cache_result = add_cache(&options.cachedata, SEND,

check_ip_tree(DIR_UNKNOWN, ip_hdr->ip_src.s_addr));

} else {

cache_result = add_cache(&options.cachedata, SEND,

check_ip6_tree(DIR_UNKNOWN, &ip6_hdr->ip_src));

}

break;

case SERVER_MODE:

/* 具体到server,第二次运行,根据树的结果生成cache

*/

if (ip_hdr) {

cache_result = add_cache(&options.cachedata, SEND,

check_ip_tree(DIR_SERVER, ip_hdr->ip_src.s_addr));

} else {

cache_result = add_cache(&options.cachedata, SEND,

check_ip6_tree(DIR_SERVER, &ip6_hdr->ip_src));

}

break;

case CLIENT_MODE:

/* 具体到client,第二次运行,根据树的结果生成cache

*/

if (ip_hdr) {

cache_result = add_cache(&options.cachedata, SEND,

check_ip_tree(DIR_CLIENT, ip_hdr->ip_src.s_addr));

} else {

cache_result = add_cache(&options.cachedata, SEND,

check_ip6_tree(DIR_CLIENT, &ip6_hdr->ip_src));

}

break;

case PORT_MODE:

/*port模式,根据目的端口得到方向

*/

cache_result = add_cache(&options.cachedata, SEND,

check_dst_port(ip_hdr, ip6_hdr, (pkthdr.caplen - l2len)));

break;

case FIRST_MODE:

/* 具体到first,第二次运行,根据树的结果生成cache

*/

if (ip_hdr) {

cache_result = add_cache(&options.cachedata, SEND,

check_ip_tree(DIR_UNKNOWN, ip_hdr->ip_src.s_addr));

} else {

cache_result = add_cache(&options.cachedata, SEND,

check_ip6_tree(DIR_UNKNOWN, &ip6_hdr->ip_src));

}

break;

default:

errx(-1, "Whops!  What mode are we in anyways? %d", options.mode);

}

return packetnum;

}

/*

*从上面代码的实现可以看到,auto模式的(包括bridge,router,client,server,first)都需要两次处理,第一次根据pcap生成一颗树,第二次根据这棵树生成缓存。非auto模式的只需要执行一次,逐个解析pcap的每个packet,得到结果后马上写入缓存

*/

Auto-bridge算法最终归于对红黑树的操作,包括addtree,processtree,calcutree,checkiptree等操作,auto-router涉及tree2cidr,checkipcidr操作

/*

*下面函数实现了将一个packet解析后变成红黑树一个node的方法

*/

tcpr_tree_t *

packet2tree(const u_char * data)

{

tcpr_tree_t *node = NULL;

eth_hdr_t *eth_hdr = NULL;

ipv4_hdr_t ip_hdr;

ipv6_hdr_t ip6_hdr;

tcp_hdr_t tcp_hdr;

udp_hdr_t udp_hdr;

icmpv4_hdr_t icmp_hdr;

dnsv4_hdr_t dnsv4_hdr;

u_int16_t ether_type;

u_char proto = 0;

int hl = 0;

node = new_tree();

eth_hdr = (eth_hdr_t *) (data);/*将data存放在eth_hdr结构体中*/

/* prevent issues with byte alignment, must memcpy */

memcpy(&ether_type, (u_char*)eth_hdr + 12, 2);/*取出 ether_type*/

/*下面判断ether_type的类型,做不同操作*/

/* drop VLAN info if it exists before the IP info */

if (ether_type == htons(ETHERTYPE_VLAN)) {

dbg(4,"Processing as VLAN traffic...");

/* prevent issues with byte alignment, must memcpy */

memcpy(&ether_type, (u_char*)eth_hdr + 16, 2);

hl += 4;

}

if (ether_type == htons(ETHERTYPE_IP)) {

memcpy(&ip_hdr, (data + TCPR_ETH_H + hl), TCPR_IPV4_H);/*取IP头*/

node->family = AF_INET;

node->u.ip = ip_hdr.ip_src.s_addr;/*node存放的IP是源IP*/

proto = ip_hdr.ip_p;/*proto 存放连接层的协议,是下面判断流向的基础*/

hl += ip_hdr.ip_hl * 4;

} else if (ether_type == htons(ETHERTYPE_IP6)) {

memcpy(&ip6_hdr, (data + TCPR_ETH_H + hl), TCPR_IPV6_H);

node->family = AF_INET6;

node->u.ip6 = ip6_hdr.ip_src;

proto = ip6_hdr.ip_nh; /*proto 存放连接层的协议,是下面判断流向的基础*/

hl += TCPR_IPV6_H;

} else {

dbgx(2,"Unrecognized ether_type (%x)", ether_type);

}

/* copy over the source mac */

strncpy((char *)node->mac, (char *)eth_hdr->ether_shost, 6);

/*下面处理 TCP 的情况*/

if (proto == IPPROTO_TCP) {

/* memcpy it over to prevent alignment issues */

memcpy(&tcp_hdr, (data + TCPR_ETH_H + hl), TCPR_TCP_H);

/* ftp-data is going to skew our results so we ignore it */

if (tcp_hdr.th_sport == 20)

return (node);

/* set TREE->type based on TCP flags */

if (tcp_hdr.th_flags == TH_SYN) {

node->type = DIR_CLIENT;

}

else if (tcp_hdr.th_flags == (TH_SYN | TH_ACK)) {

node->type = DIR_SERVER;

}

else {

dbg(3, "is an unknown");

}

}

/*下面处理 UDP 的情况*/

else if (proto == IPPROTO_UDP) {

/* memcpy over to prevent alignment issues */

memcpy(&udp_hdr, (data + TCPR_ETH_H + hl), TCPR_UDP_H);

switch (ntohs(udp_hdr.uh_dport)) {/*由目的端口判断是dns协议的情况*/

case 0x0035:           /* dns */

/* prevent memory alignment issues */

memcpy(&dnsv4_hdr,

(data + TCPR_ETH_H + hl + TCPR_UDP_H), TCPR_DNS_H);

if (dnsv4_hdr.flags & DNS_QUERY_FLAG) {

/* bit set, response */

node->type = DIR_SERVER;

}

else {

/* bit not set, query */

node->type = DIR_CLIENT;

}

return (node);

break;

default:

break;

}

switch (ntohs(udp_hdr.uh_sport)) {/*由源端口判断是dns协议的情况*/

case 0x0035:           /* dns */

/* prevent memory alignment issues */

memcpy(&dnsv4_hdr,

(data + TCPR_ETH_H + hl + TCPR_UDP_H),

TCPR_DNS_H);

/*通过检查特定标志位的值,判断是哪个流向*/

if ((dnsv4_hdr.flags & 0x7FFFF) ^ DNS_QUERY_FLAG) {

node->type = DIR_SERVER;

}

else {

node->type = DIR_CLIENT;

}

return (node);

break;

default:

dbgx(3, "unknown UDP protocol: %hu->%hu", udp_hdr.uh_sport,

udp_hdr.uh_dport);

break;

}

}

/*下面处理 ICMP的情况*/

else if (proto == IPPROTO_ICMP) {

/* prevent alignment issues */

memcpy(&icmp_hdr, (data + TCPR_ETH_H + hl), TCPR_ICMPV4_H);

/* if port unreachable, then source == server, dst == client  */

if ((icmp_hdr.icmp_type == ICMP_UNREACH) &&

(icmp_hdr.icmp_code == ICMP_UNREACH_PORT)) {

node->type = DIR_SERVER;

dbg(3, "is a server with a closed port");

}

}

return (node);

}

/*

*从函数实现可以看出,基本上是根据协议规范解析packet,通过检测特定协议的特定标志位的值来判断该packet的流向

*/

1.1.1  实验结果

1.1.1.1  实验1

1.1.1.1  实验2

下面是使用 auto=router的实验情况:

1.1.1.1  实验3

本文使用了wireshark在局域网中随机抓取了一个包,使用auto=router拆分成功,准确率一般。结果如下:

1.1.1  发现该算法的一个问题

算法简单回顾:整个pcap包含的packet的源IP都被整合进入一颗红黑树,然后遍历整棵红黑树,算每个节点的比例,得出结果是C还是S。使用的时候,通过取packet的IP,看它是在红黑树的哪个节点,拿那个节点的值。

问题:

假设有一种情况,一个IP同时作为客户端和服务器(在本机上架设一个webserver,然后用本机的浏览器请求页面),这种情况下,本机IP事实上同时是C和S,但根据tcpprep的红黑树算法,它的最终结果要么是C要么是S。

转载于:https://www.cnblogs.com/jiayy/p/tcpreplay.html

tcpreplay 流量拆分算法研究相关推荐

  1. tcpreplay 发包速率控制算法研究

    一.  序 1.1  tcpreplay历史 Tcpreplay 的作者是Aaron Turner,该项目开始于2000年,早期的功能是对tcpdump等抓包工具生成的网络包(即pcap文件)的回放, ...

  2. 加密流量分析-2.研究背景

    研究背景 1.加密流量分类概述 1.1识别方法 1.2 识别粒度 1.3 识别对象等级 2.加密流量识别粒度相关研究 2.1加密与未加密流量分类 2.2 加密协议识别 2.2.1 IPSec 2.2. ...

  3. 各种搜索引擎算法研究

    各种搜索引擎算法研究 1.引言 万维网WWW(World Wide Web)是一个巨大的,分布全球的信息服务中心,正在以飞快的速度扩展.1998年WWW上拥有约3.5亿个文档[14],每天增加约1百万 ...

  4. Nginx + LUA下流量拦截算法

    前言 电商平台营销时候,经常会碰到的大流量问题,除了做流量分流处理,可能还要做用户黑白名单.信誉分析,进而根据用户ip信誉权重做相应的流量拦截.限制流量. Nginx自身有的请求限制模块ngx_htt ...

  5. 沉浸式 3D 场景下的多视点视频 增强算法研究

    沉浸式 3D 场景下的多视点视频 增强算法研究 研究内容 图像质量增强 为什么进行图像质量增强 图像有损压缩技术 多视点视频中的深度图像特点 视点数目增强 虚拟视点合成技术 视点外推 为什么进行视点数 ...

  6. 第七章 人工智能,7.1 基于深度强化学习与自适应在线学习的搜索和推荐算法研究(作者:灵培、霹雳、哲予)...

    7.1 基于深度强化学习与自适应在线学习的搜索和推荐算法研究 1. 搜索算法研究与实践 1.1 背景 淘宝的搜索引擎涉及对上亿商品的毫秒级处理响应,而淘宝的用户不仅数量巨大,其行为特点以及对商品的偏好 ...

  7. 基于RNA测序技术的转录组从头拼接算法研究

    基于RNA测序技术的转录组从头拼接算法研究 摘要: 生物信息学主要研究分子生物学领域,而对于分子生物学领域,转录组的从头拼接又是其核心内容,即利用转录组的测序片段拼接出整个转录组中的所有表达的转录体. ...

  8. 经典算法研究系列:二、Dijkstra 算法初探

    经典算法研究系列:二.Dijkstra 算法初探  July   二零一一年一月 ====================== 本文主要参考:算法导论 第二版.维基百科. 写的不好之处,还望见谅. 本 ...

  9. 基于图机器学习的微生物网络关系预测算法研究

    龙亚辉预答辩公告 浏览次数:410日期:2021-03-19编辑:院研究生秘书 预答辩公告 论文题目 基于图机器学习的微生物网络关系预测算法研究 答辩人 龙亚辉 指导教师 骆嘉伟 答辩委员会 主席 王 ...

最新文章

  1. Google Test(GTest)使用方法和源码解析——Listener技术分析和应用
  2. COM:细菌-真菌的平衡维持动植物健康
  3. HarmonyOS之AI能力·通用文字识别技术
  4. 基于visual Studio2013解决面试题之0802数字最多元素
  5. 让互联网更快的协议,QUIC在腾讯的实践及性能优化
  6. python三级联动菜单_VUE+element三级联动或树形菜单获取最后一项,并加入到表格中...
  7. 华为云苏光牛:生态建设是数据库产业发展非常重要的一环
  8. 钱大妈关闭所有北京门店:低估了北京市场的难度
  9. Python数据类型(3)
  10. STM32MP157实现串口接收数据上云-云数据库存储多设备数据界面显示实现
  11. 绑定流详解——网络测试仪实操
  12. 社团管理系统软件测试,软件测试大作业社团管理系统.doc
  13. html5在线编辑器效果和源码
  14. 最新QQ强制搜索Api接口
  15. NAS 详细搭建方案 -添加磁盘
  16. 服务器千兆网卡和万兆网卡有什么区别
  17. 基于B/S的网络考试系统的设计与实现(附:源码 论文 课件)
  18. ubuntu上安装QT
  19. Web项目实战分享——小米官网
  20. 风格迁移0-06:stylegan-源码无死角解读(2)-数据预处理process_reals

热门文章

  1. Linux下,编译程序遇到“undefined reference to XXX” 报错(可针对webots的编译,不同的文件夹下面不同的cpp,.h文件)
  2. 用js动态改变css样式表(亲测可以)
  3. Python常用的12个GUI框架
  4. python selenium 大众点评餐厅信息+用户评论 爬虫
  5. 比较传统数据与大数据
  6. Indy:Connection Closed Gracefully
  7. 爆笑!新一轮的淘宝差评
  8. 程序员必须克服的十大编程禁忌
  9. 将一个二值化的图片中的黑白区域反转(numpy快速完成)
  10. 最难忘的新年祝福,第一个让大家都惊喜的小程序(有趣、恶搞、好玩)