开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring MVC源码解析(一)——概况

发表于 2017-12-05

一、Spring MVC

Spring MVC 基于模型-视图-控制器(Model-View-Controller, MVC)模式实现,并且很好的实现了软件设计中的开闭原则(即对扩展开放,对修改关闭),当因为业务需要对Spring MVC做些定制化处理时,就会发现Spring MVC对功能扩展是极其友好的、在后续的源码解析系列文章中我们会陆续看到Spring MVC在处理请求的各个步骤中都可以定制所需要的功能。

二、Spring MVC 整体框架

image

整个系列的文章都会围绕这张图进行,会对每一个步骤进行详细讲解。
首先我们先来看一下 DispatcherServlet diagram

可以看到蓝色的继承关系到 Servlet 相信大家在学习MVC框架之前对 HttpServlet
非常的熟悉,目前还有一些老项目在使用原生的 java servlet进行项目开发。我们看一下 servlet接口最重要的方法签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码 /**
* Called by the servlet container to allow the servlet to respond to
* a request.
*
* <p>This method is only called after the servlet's <code>init()</code>
* method has completed successfully.
*
* <p> The status code of the response always should be set for a servlet
* that throws or sends an error.
*
*
* <p>Servlets typically run inside multithreaded servlet containers
* that can handle multiple requests concurrently. Developers must
* be aware to synchronize access to any shared resources such as files,
* network connections, and as well as the servlet's class and instance
* variables.
* More information on multithreaded programming in Java is available in
* <a href="http://java.sun.com/Series/Tutorial/java/threads/multithreaded.html">
* the Java tutorial on multi-threaded programming</a>.
*
*
* @param req the <code>ServletRequest</code> object that contains
* the client's request
*
* @param res the <code>ServletResponse</code> object that contains
* the servlet's response
*
* @exception ServletException if an exception occurs that interferes
* with the servlet's normal operation
*
* @exception IOException if an input or output exception occurs
*
*/

public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;

这个方法是servlet处理web请求的入口。Spring MVC在DispatchSerlvet还分装了一层FrameWorkServlet用于统一处理所有不同方法类型(GET、POST等)的请求

三、各组件的基本介绍。

我们从一个Http请求的角度,来大致了解Spring MVC是处理请求的大致流程(例如,到controller方法加@ResponseBody时就不会有视图解析这一步)。

1、web container 接收到一个请求,容器调用已经注册好的DispatcherServlet,后者通过Request对象到RequestMapping
获取对应的 handler(即controller层实际调用的方法)。

2、执行interceptor的preHandler()方法。

3、执行第一步获取的Controller方法,并返回ModelAndView。

4、执行interceptor的postHandler()方法。

5、视图渲染,执行ViewResolve.resolveViewName()方法回去视图文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
复制代码/**
* Process the actual dispatching to the handler.
* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
* to find the first that supports the handler class.
* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
* themselves to decide which methods are acceptable.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
//判断请求是否是 multipart post (常见的有 post 表单提交的数据)
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
//通过request获取handler,包括 intercepter 信息
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
//获取 handler 适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//调用 intercepter.perHandler()方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
//调用controller方法发挥 ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//当 ModelAndView 中不包含视图时获取默认视图
applyDefaultViewName(processedRequest, mv);
//调用 intercepter.perHandler()方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//视图渲染并将渲染后的视图文件(html)或者 json 等写入Response body 返回给浏览器
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

总结

本文主要介绍了Spring MVC的一些概念以及请求执行的大致过程。后续的文章将继续分析Spring MVC的各个组件,以及如何根据自己的项目定制相应的功能。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java二进制相关基础

发表于 2017-12-05

转载请注明原创出处,谢谢!

说在前面

之前在JVM菜鸟进阶高手之路十(基础知识开场白)的时候简单提到了二进制相关问题,最近在看RocketMQ的源码的时候,发现涉及二进制的内容蛮多,jdk源码里面也是有很多涉及到二进制相关的操作,今天这篇文章仅仅是扫盲篇,后续会介绍灵活运用篇。

说明

任何东西都有规范,提到JAVA就会提到2个规范,JAVA语言规范、JVM规范。JAVA语言规范主要定义JAVA的语法、变量、类型、文法等等,JVM规范主要定义Class文件类型、运行时数据、帧栈、虚拟机的启动、虚拟机的指令集等等。

  • JAVA语言规范主要定义什么是JAVA语言。
  • JVM规范主要定义JVM内部实现,二进制class文件和JVM指令集等。

规范中数字的内部表示和存储

JAVA八种基本数据类型:
整形:byte,short,int,long
浮点型:float,double
布尔型:boolean
字符型:char

数据类型 所占位数
int 32bit
short 16bit
long 64bit
byte 8bit
char 16bit
float 32bit
double 64bit
boolean 1bit

**备注:**1字节=8位(1 byte = 8bit)

整数的表示

  • 源码:第一位为符号位(0表示正数,1表示负数)。
  • 反码:符号位不动,原码取反。
  • 负数补码:符号位不动,反码加1。
  • 正数补码:和源码相同。

**备注:**补码的好处:

  • 使用补码可以没有任何歧义的表示0。
  • 补码可以很好的参与二进制的运算,补码相加符号位参与运算,这样就简单很多了。

浮点数表示

在上图中,我们了解到Float与Double都是支持IEEE 754

我们以float来说明:

IEEE754单精度浮点格式共32位,包含三个构成字段:23位小数f,8位偏置指数e,1位符号s。将这些字段连续存放在一个32位字里,并对其进行编码。其中0:22位包含23位的小数f; 23:30位包含8位指数e;第31位包含符号s。

一个实数V在IEEE 754标准中可以用V=(-1)s×M×2E 的形式表示,说明如下:

  • 符号s(sign)决定实数是正数(s=0)还是负数(s=1),对数值0的符号位特殊处理。
  • 有效数字M(significand)是二进制小数,M的取值范围在1≤M<2或0≤M<1。
  • 指数E(exponent)是2的幂,它的作用是对浮点数加权。
符号位 指数位 小数位
1位 8位 23位

例如根据IEEE754,计算11000001000100000000000000000000的单精度浮点的值。

解题:

1 10000010 00100000000000000000000
符号位 指数 尾数由于指数不是全部为0 所以小数位附加1
1 10000010 **1.**00100000000000000000000
-1 2^(130-127) (2^0 + 2^-3)

结论:
-1 * (2^0 + 2^-3) * 2^(130-127) =-9

同样,你也可以验证一下十进制浮点数0.1的二进制形式是否正确,你会发现,0.1不能表示为有限个二进制位,因此在内存中的表示是舍入(rounding)以后的结果,即 0x3dcccccd, 十进制为0.100000001, 误差0.000000001由此产生了。

说到这里JVM菜鸟进阶高手之路十(基础知识开场白)的有些问题其实都解答了,所以涉及到钱的小数类型必须使用BigDecimal,禁止使用float和double。

进制的概念

我们常用的进制有二进制、八进制、十进制和十六进制,十进制是最主要的表达形式。

二进制是0和1;八进制是0-7;十进制是0-9;十六进制是0-9+A-F(大小写均可)。

位运算符

按位与(&)

两位全为1,结果才为1:

1
2
3
4
复制代码0&0=0; 
0&1=0;
1&0=0;
1&1=1;

用法:

  • 清零:如果想要一个单位清零,那么使其全部二进制为0,只要与一个各位都为零的数值想与,结果为零。
  • 取一个数中指定位:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。

例如:设X=1010 1110,取X的低4位,用X & 0000 1111 = 0000 1110 就可以得到。

按位或(|)

只要有一个为1,结果就为1:

1
2
3
4
复制代码0|0=0;  
0|1=1;
1|0=1;
1|1=1;

**用法:**常用来对一个数据的某些位置1;找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。

例如:将X=1010 0000 的低四位置1,用X | 0000 1111 =1010 1111 就可以得到。

异或运算(^)

**两个相应位为“异”(值不同),则该位结果为1,否则为0: **

1
2
3
4
复制代码0^0=0;
0^1=1;
1^0=1;
1^1=0;

用法:

  • 使特定位翻转:找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或就可以得到;
    例如:X=1010 1110,使X低4位翻转,用X ^ 0000 1111 = 1010 0001就可以得到
  • 与0相异或,保留原值
    例如:X ^ 0000 0000 = 1010 1110
  • 两个变量交换值的方法:
    1、借助第三个变量来实现: C=A; A=B; B=C;
    2、 利用加减法实现两个变量的交换:A=A+B; B=A-B;A=A-B;
    3、用位异或运算来实现:利用一个数异或本身等于0和异或运算符合交换律
    例如:A = A ^ B; B = A ^ B; A = A ^ B;

取反运算(~)

对于一个二进制数按位取反,即将0变1,1变0: ~1=0; ~0=1;

左移运算(<<)

  • 将一个运算对象的各二进制位全部左移若干位(左边的二进制丢弃,右边补零)
    2<<1 = 4 : 10 <<1 =100=4
  • 若左移时舍弃的高位不包括1,则每左移一位,相当于该数乘以2。

-14(二进制:1111 0010)<< 2= (1100 1000) (高位包括1,不符合规则)

右移运算(>>)

将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。操作数每右移一位,相当于该数除以2.

左补0 or 补1 得看被移数是正还是负。
例:4 >> 2 = 1
例:-14(1111 0010) >> 2 = -4 (1111 1100 )

无符号右移运算(>>>)

各个位向右移指定的位数。右移后左边突出的位用零来填充。移出右边的位被丢弃
各个位向右移指定的位数。右移后左边突出的位用零来填充。移出右边的位被丢弃
例如: -14>>>2

即-14(1111 1111 1111 1111 1111 1111 1111 0010)>>> 2
=(0011 1111 1111 1111 1111 1111 1111 1100)
= 1073741820

Java打印整数的二进制表示

1
2
3
4
5
复制代码int a = 1120429670;
for (int i = 0; i < 32; i++) {
int t = (a & 0x80000000 >>> i) >>> (31 - i);
System.out.print(t);
}

说明:

  • 0x80000000是数的十六进制表示,转成二进制表示为10000000000000000000000000000000
  • 运算的优先级,移位运算高于逻辑运算,>>>高于&
  • 位逻辑与运算 1&1 = 1 ,0&1 = 0
  • >>>无符号右移,移出部分舍弃,左边位补0;

欢迎积极留言讨论关于你在实际运用中了解到二进制的一些优秀实践,期待你的留言!!!

如果读完觉得有收获的话,欢迎点赞、关注、加公众号【匠心零度】。


个人公众号,欢迎关注,查阅更多精彩历史!!!

匠心零度公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我是怎样爬下6万共享单车数据并进行分析的(附代码) 高兴得

发表于 2017-12-05

共享经济的浪潮席卷着各行各业,而出行行业是这股大潮中的主要分支。如今,在城市中随处可见共享单车的身影,给人们的生活出行带来了便利。相信大家总会遇到这样的窘境,在APP中能看到很多单车,但走到那里的时候,才发现车并不在那里。有些车不知道藏到了哪里;有些车或许是在高楼的后面,由于有GPS的误差而找不到了;有些车被放到了小区里面,一墙之隔让骑车人无法获得到车。

那么有没有一个办法通过获得这些单车的数据,来分析这些车是否变成了僵尸车?是否有人故意放到小区里面让人无法获取呢?带着这些问题,笔者开始了研究如何获取这些数据。

01 从哪里获得数据

如果你能够看到数据,那么我们总有办法自动化的获取到这些数据。只不过获取数据的方式方法决定了获取数据的效率。

对于摩拜单车的数据分析这个任务而言,这个爬虫要能够在短时间内(通常是10分钟左右)获取到更多的数据,对于数据分析才有用处。那么数据来源于哪里?

最直接的来源是摩拜单车的APP。现代的软件设计都讲究前后端分离,而且服务端会同时服务于APP、网页等。在这种趋势下我们只需要搞清楚软件的HTTP请求就好了。一般而言有以下一些工具可以帮忙:

直接抓包:

  • Wireshark (在路由器或者电脑)
  • Shark for Root (Android)

用代理进行HTTP请求抓包及调试:

  • Fiddler 4
  • Charles
  • Packet Capture (Android)

由于我的手机没有root,在路由器上抓包又太多的干扰,对于https也不好弄。所以只能首先采用Fiddler或者Charles的方式试试。

挂上Fiddler的代理,然后在手机端不停的移动位置,看有没有新的请求。但遗憾的是似乎请求都是去拿高德地图的,并没有和摩拜车相关的数据。

那怎么一回事?试试手机端的。换成Packet Capture后果然就有流量了,在请求中找到了我最关心的那个:

这个API请求一看就很显然了,在postman中试了一下能够正确的返回信息,看来就是你了!

高兴得太早。

连续爬了几天的数据,将数据进行一分析,发现摩拜单车的GPS似乎一直在跳动,有时候跳动会超过几公里的距离,显然不是一个正常的值。

难道是他们的接口做了手脚返回的是假数据?我观察到即便在APP中,单车返回的数据也有跳动。有某一天凌晨到第二天早上,我隔段时间刷新一下我家附近的车,看看是否真的如此。

图片我找不到了,但是观察后得出的结论是,APP中返回的位置确实有问题。有一台车放在一个很偏僻的位置,一会儿就不见了,待会儿又回来了,和我抓下来的数据吻合。

而且这个跳动和手机、手机号、甚至移动运营商没有关系,说明这个跳动是摩拜接口的问题,也可以从另一方面解释为什么有时候看到车但其实那里没有车。

这是之前发的一个朋友圈的视频截图,可以看到在营门口附近有一个尖,在那里其实车是停住的,但是GPS轨迹显示短时间内在附近攒动,甚至攒动到很远,又回到那个位置。

这样的数据对于数据分析来讲根本没法用,我差点就放弃了。

随着微信小程序的火爆,摩拜单车也在第一时间出了小程序。我一看就笑了,不错,又给我来了一个数据源,试试。

用Packet Capture抓了一次数据后很容易确定API。抓取后爬取了两三天的数据,发现出现了转机,数据符合正常的单车的轨迹。

剩下事情,就是提高爬虫的效率了。

02 其他尝试

有时候直接分析APP的源代码会很方便的找到API入口,将摩拜的Android端的APP进行反编译,但发现里面除了一些资源文件有用外,其他的文件都是用奇虎360的混淆器加壳的。网上有文章分析如何进行脱壳,但我没有太多时间去钻研,也就算了。

摩拜单车的API之所以很容易抓取和分析,很大程度上来讲是由于API设计的太简陋:

  • 仅使用http请求,使得很容易进行抓包分析
  • 在这些API中都没有对request进行一些加密,使得自己的服务很容易被人利用。
  • 另外微信小程序也是泄露API的一个重要来源,毕竟在APP中request请求可以通过native代码进行加密然后在发出,但在小程序中似乎还没有这样的功能。

如果大家有兴趣,可以试着看一下小蓝单车APP的request,他们使用https请求,对数据的request进行了加密,要抓取到他们的数据难度会增加非常多。

当然了,如果摩拜单车官方并不care数据的事情的话,这样的API设计也是ok的。

声明:

此爬虫仅用于学习、研究用途,请不要用于非法用途。任何由此引发的法律纠纷自行负责。

03 目录结构

\analysis - jupyter做数据分析    \influx-importer - 导入到influxdb,但之前没怎么弄好    \modules - 代理模块    \web - 实时图形化显示模块,当时只是为了学一下react而已,效果请见这里    crawler.py - 爬虫核心代码   

importToDb.py - 导入到postgres数据库中进行分析 sql.sql - 创建表的sql start.sh - 持续运行的脚本

04 思路

核心代码放在crawler.py中,数据首先存储在sqlite3数据库中,然后去重复后导出到csv文件中以节约空间。摩拜单车的API返回的是一个正方形区域中的单车,我只要按照一块一块的区域移动就能抓取到整个大区域的数据。left,top,right,bottom定义了抓取的范围,目前是成都市绕城高速之内以及南至南湖的正方形区域。offset定义了抓取的间隔,现在以0.002为基准,在DigitalOcean 5$的服务器上能够15分钟内抓取一次。 def
start(self): left = 30.7828453209 top = 103.9213455517 right = 30.4781772402
bottom = 104.2178123382 offset = 0.002 if os.path.isfile(self.db_name): os.remove(self.db_name)
try: with sqlite3.connect(self.db_name) as c: c.execute(‘’’CREATE TABLE mobike
(Time DATETIME, bikeIds VARCHAR(12), bikeType TINYINT,distId INTEGER,distNum TINYINT, type TINYINT, x DOUBLE, y DOUBLE)’’’) except Exception as ex:
pass然后就启动了250个线程,至于你要问我为什么没有用协程,哼哼~~我当时没学~~~其实是可以的,说不定效率更高。由于抓取后需要对数据进行去重,以便消除小正方形区域之间重复的部分,最后的group_data正是做这个事情。 executor = ThreadPoolExecutor(max_workers=250)
print(“Start”) self.total = 0 lat_range = np.arange(left, right, -offset) for lat in lat_range:
lon_range = np.arange(top, bottom, offset) for lon in lon_range: self.total += 1
executor.submit(self.get_nearby_bikes, (lat, lon)) executor.shutdown() self.group_data()最核心的API代码在这里。小程序的API接口,搞几个变量就可以了,十分简单。 def get_nearby_bikes(self,
args): try: url = “https://mwx.mobike.com/mobike-api/rent/nearbyBikesInfo.do"
payload = “latitude=%s&longitude=%s&errMsg=getMapCenterLocation” % (args[0], args[1]) headers = {
‘charset’: “utf-8”, ‘platform’: “4”, “referer”:”https://servicewechat.com/wx40f112341ae33edb/1/",
‘content-type’: “application/x-www-form-urlencoded”, ‘user-agent’: “MicroMessenger/6.5.4.1000 NetType/WIFI Language/zh_CN”,
‘host’: “mwx.mobike.com”, ‘connection’: “Keep-Alive”, ‘accept-encoding’:
“gzip”, ‘cache-control’: “no-cache” }
self.request(headers, payload, args, url) except Exception as ex: print(ex)最后你可能要问频繁的抓取IP没有被封么?其实摩拜单车是有IP的访问速度限制的,只不过破解之道非常简单,就是用大量的代理。我是有一个代理池,每天基本上有8000以上的代理。在ProxyProvider中直接获取到这个代理池然后提供一个pick函数用于随机选取得分前50的代理。

请注意,我的代理池是每小时更新的,但是代码中提供的jsonblob的代理列表仅仅是一个样例,过段时间后应该大部分都作废了。在这里用到一个代理得分的机制。我并不是直接随机选择代理,而是将代理按照得分高低进行排序。每一次成功的请求将加分,而出错的请求将减分。

这样一会儿就能选出速度、质量最佳的代理。如果有需要还可以存下来下次继续用。class ProxyProvider: def __init__(self, min_proxies=200): self._bad_proxies = {} self._minProxies = min_proxies
self.lock = threading.RLock() self.get_list() def get_list(self): logger.debug(“Getting proxy list”)
r = requests.get(“https://jsonblob.com/31bf2dc8-00e6-11e7-a0ba-e39b7fdbe78b", timeout=10) proxies = ujson.decode(r.text) logger.debug(“Got %s proxies”, len(proxies))
self._proxies = list(map(lambda p: Proxy(p), proxies)) def pick(self): with self.lock: self._proxies.sort(key = lambda
p: p.score, reverse=True) proxy_len = len(self._proxies) max_range = 50 if proxy_len > 50 else proxy_len
proxy = self._proxies[random.randrange(1, max_range)] proxy.used() return proxy在实际使用中,通过proxyProvider.pick()选择代理,然后使用。如果代理出现任何问题,则直接用proxy.fatal_error()降低评分,这样后续就不会选择到这个代理了。 def
request(self, headers, payload, args, url): while True: proxy = self.proxyProvider.pick()
try: response = requests.request( “POST”,
url, data=payload, headers=headers, proxies={“https”: proxy.url},
timeout=5,verify=False ) with self.lock:
with sqlite3.connect(self.db_name) as c: try:
print(response.text) decoded = ujson.decode(response.text)[‘object’]
self.done += 1 for x in decoded:
c.execute(“INSERT INTO mobike VALUES (%d,’%s’,%d,%d,%s,%s,%f,%f)” % (
int(time.time()) * 1000, x[‘bikeIds’], int(x[‘biketype’]), int(x[‘distId’]),
x[‘distNum’], x[‘type’], x[‘distX’],
x[‘distY’])) timespend = datetime.datetime.now() - self.start_time
percent = self.done / self.total total = timespend / percent
print(args, self.done, percent * 100, self.done / timespend.total_seconds() * 60, total,
total - timespend) except Exception as ex:
print(ex) break except Exception as ex:
proxy.fatal_error()

抓取了摩拜单车的数据并进行了大数据分析。以下数据分析自1月19日整日的数据,范围成都绕城区域以及至华阳附近(天府新区)内。成都的摩拜单车的整体情况如下:

05 标准、Lite车型数量相当

摩拜单车在成都大约已经有6万多辆车,两种类型的车分别占有率为55%和44%,可见更为好骑的Lite版本的占有率在提高。(1为标准车,2为Lite车型)

06 三成左右的车没有移动过

数据分析显示,有三成的单车并没有任何移动,这说明这些单车有可能被放在不可获取或者偏僻地方。市民的素质还有待提高啊。

07 出行距离以3公里以下为主

数据分析显示3公里以下的出行距离占据了87.2%,这也十分符合共享单车的定位。100米以下的距离也占据了大量的数据,但认为100米以下的数据为GPS的波动,所以予以排除。

出行距离分布

08 骑行次数以5次以下居多

单车的使用频率越高共享的效果越好。从摩拜单车的数据看,在流动的单车中,5次以下占据了60%左右的出行。但1次、2次的也占据了30%左右的份额,说明摩拜单车的利用率也不是很高。

单车骑行次数

单车骑行次数

09 从单车看城市发展

从摩拜单车的热图分布来看,成都已经逐步呈现“双核”发展的态势,城市的新中心天府新区正在聚集更多的人和机会。

双核发展

原来的老城区占有大量的单车,在老城区,热图显示在东城区占有更多的单车,可能和这里的商业(春熙路、太古里、万达)及人口密集的小区有直接的联系。

老城区

而在成都的南部天府新区越来越多也茁壮的发展起来,商业区域和住宅区域区分明显。在晚上,大量的单车聚集在华阳、世纪城、中和,而在上班时间,则大量聚集在软件园附近。

软件园夜间

软件园白天

来源:钱塘大数据

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python 一个快速实现CLI 应用程序的脚手架

发表于 2017-12-05

今天跟大家分享一下如何快速实现一个Python CLI应用程序的脚手架,之所以会做这个是因为当时需要做一个运维的小工具希望用命令行的方式来使用,但是搜遍网上很多资料都没有系统讲解从开发、集成、发布、文档等一系列流程的文章。

工程结构


如上图,这就是一个比较规范的Python CLI应用项目了,下面一一讲下各文件的用途:

项目文档


这里我们用Sphinx来实现文档的自动生成,当然你要首先通过markdown和rst文件定义好文档的内容,然后进入docs目录执行 make html命令就可以在_build目录下生成对应的静态文件,如下图:

具体Sphinx如何使用以及配置后面会单独文章讲解

主工程


这里讲几个需要注意的地方
1、日志的配置:
这里可以全局设置日志的一些输出级别和格式化方式

2、cli文件
这里通过click库来实现

3、二进制文件打包

如上图,有时候我们的工程中会包含二进制文件,也就是非Python代码的文件,这时候如果还是像往常一样打包发布,安装的时候会发现无法找到此文件,所以需要在根目录的MANIFEST.in文件中加入

脚本


如下图,这里的make-release文件主要是用来自动控制版本的,如下图,通过Git 的提交记录了来作为项目的唯一版本号标识,再对init文件进行重新写入达到持续集成时版本号自增的目的。

单元测试


test文件夹中存放的就是项目的单元测试文件了,这里就不细展开讲了,后面会具体讲讲如何跟Jenkins集成实现静态代码检查

setup


最重要的就是setup.py这个文件了,项目最后打包发布到pypi仓库主要的配置信息都在这里了,如下图:

这个脚手架的项目地址:github.com/logan62334/…
项目会持续更新,可以点击阅读原文访问


FullStackEngineer的公众号,更多分享FullStackEngineer的公众号,更多分享

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java笔记-反射机制(一)

发表于 2017-12-04

Java反射机制(一)

结合Oracle官方通过JDK8编写的反射教程,复习一下反射的知识。结尾篇补一个小例子。

主要内容

这次博客的主要内容就是简单介绍反射的概念以及相关获取类信息的反射API。

反射的概念

反射是一种在运行时获取以及修改应用行为的一种工具。我个人的理解就是,new是一种正向的操作,知道现有系统中会出现什么。反射就是反着来,不知道系统中可能会需要什么样的类,通过全限定类名的方式,在需要的时候将它反射出来,同时可以通过反射获取类的内部信息。在Java框架的开发中,反射技术运用比较常见。

反射的优缺点

优点

  1. 强大的扩展性,用户可以通过全限定类名的方式去使用外部定义的类。
  2. 帮助IDE开发工具获取用户正在开发的code的信息,提示写出更正确的代码。
  3. 利于调试工具获取运行时信息以及测试类框架的使用比如Junit。

缺点

  1. 影响性能。 因为反射需要动态的解析类的信息,相比于非反射使用的方式要慢。
  2. 对安全环境有要求。 反射需要获取一定的运行时权限,在特定的安全环境下不一定存在。
  3. 暴露了内部的封装,可能会引起一些负面效果。比如不该被外部调用的私有方法,通过反射被调用了。

通过反射获取类信息

Java中除了基本类型就是引用类型。
boolean,int,long,float等就是基本类型
java.lang.String,Java.io.Serializable就是引用类型

获取java.lang.Class

对于每一种类型,Java提供了java.lang.Class这个类用于获取运行时类的属性和方法信息。同时java.lang.Class也可以用于创建类和对象。
如果是对象类型的话,可以通过其最上层父类Object提供的getClass()方法获取Class类。

1
复制代码"apple".getClass();

如果是基本类型或者对于一个普通的类来说,可以使用.class的方式来获取Class类,如下。

1
2
复制代码int.class;
java.io.PrintStream.class;

当获取到类的全限定类名后,可以通过Class.forName创建一个类,如下。

1
复制代码Class c = Class.forName("com.coderising.kailuncen.Main");

获取类的相关类信息

以下Api可以用于获取类的相关类信息。
获取类的父类信息:

1
复制代码Class.getSuperclass()

获取类的成员类信息,不包括私有的:

1
复制代码Class.getClasses()

获取类的所有成员类信息,包括私有的:

1
复制代码Class.getDeclaredClasses()

以下API可以返回声明了这些成员变量的类的Class信息。

1
2
3
4
复制代码Class.getDeclaringClass()
java.lang.reflect.Field.getDeclaringClass()
java.lang.reflect.Method.getDeclaringClass()
java.lang.reflect.Constructor.getDeclaringClass()

如果这个类是匿名类的话,可以通过如下API获取包含它的类的类信息。

1
复制代码Class.getEnclosingClass()

获取类的修饰符

类在运行期间可以被多种修饰符修饰,如下所示
访问限定符: public, protected, and private。
需要override的修饰符:abstract。
然后static,final,Annotations等。
反射API可以使用如下方法去访问他们。

1
2
复制代码 Class.getModifiers()
Class.getAnnotations();

获取类的成员信息

在oracle的教程中,整理了三个表格,介绍了如何获取类的成员信息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GitHub / GitLab Webhook 接口开发指南

发表于 2017-12-04

前一阵子要开发一个从 GitLab / GitHub 通过 Webhook 拉取文件并且上传指定 OSS 的接口,于是就找起了 GitHub 和 GitLab 的官方文档。

当然官方文档实在是太过冗长,尤其是公司自建的 GitLab 版本还有可能不一致,所以就只能看搭建的 GitLab 提供的文档信息,对于一些遇到的问题就看命看版本查了。

当然所幸除了接口版本以外并没有遇到什么太大的坑,遇到了问题也可以根据版本来查,可以说这绝对是严格遵照 RESTful API 的一个优点,这一点无论是 GitHub 还是 GitLab 都做得很好。

由于 GitLab 和 GitHub 用的是两套接口,所以这里分开来介绍,不过方法都是相同的:

拉取压缩包,解压缩,上传文件。另一种方法是 git clone 下来,但是感觉 git clone 依旧太麻烦,所以就选了拉取压缩包。

GitLab

下载压缩包的接口:docs.gitlab.com/ee/api/repo…

这里有两个参数:

1
2
复制代码id (required) - The ID or URL-encoded path of the project owned by the authenticated user
sha (optional) - The commit SHA to download defaults to the tip of the default branch

这里需要两个信息,project 的 id 与 sha,都可以通过 Webhook 获得:

1
复制代码const { checkout_sha: checkoutSha, project_id: projectId } = body

但是不同的是 id 是放在 参数(param)中的,而 sha 则是以 querystring 传入的(GitHub 与它不一样),如果不传入 sha,默认为 master 分支最新的代码。

当然,我们还会设置 secret token,这个 secret token 可以通过 header 中的 x-gitlab-token 获得。

然后我们就得到了 GitLab 的相关信息。

如果 API 中获取的内容需要权限,可以使用 PRIVATE-TOKEN 这一 header 传入 token。

GitHub

GitHub 的 Webhook 接口方式与 GitLab 有点不同,我们这里使用的还是 V3 版的 Restful 接口。

针对 GitHub 的 Webhook 最重要的是怎么计算出 secret token 与我们原先设定的相一致。GitHub 就没有 GitLab 那么简单了,我们需要先计算一番,具体可见Validating payloads from GitHub,如果看不懂 Ruby 没关系,下面提供一段 JavaScript:

1
2
3
4
5
复制代码const strBody = JSON.stringify(body); // 将整个 body 带出去变成 string
const sign = crypto.createHmac("sha1", secretKey) // secretKey 为你需要核对的 token
.update(strBody)
.digest("hex"); // 加密成十六进制
ctx.assert(ctx.header["x-hub-signature"].replace("sha1=", "") === sign, 403, "Wrong Sign");

然后把 header 中的 x-hub-signature 带有 sha1=value 中的 value 和 sign 作对比即可。

其次 GitHub 需要通过 header 中的 x-github-event 来判断 event 的类型,否则会出错,不同的 Event 传入的内容可能不一样,需要对 ping 做一些特殊处理,如果是 ping,直接返回 200,这样就可以过 GitHub 的检查了。

从 request body 中,你还可以找到 repository 信息和相对应的 commit 的信息,也可以获得 Repo Org 的信息(这里我们不需要仓库 Org,所以暂不表述)

1
复制代码const { repository, head, head_commit: headCommit } = body;

我们需要的接口是:

1
复制代码https://github.com/${org}/${repositoryName}/archive/${sha}.zip

其中:

1
2
复制代码repositoryName = repository.name
sha = headCommit.id

其中如果需要授权,在 header 中写入:

1
复制代码"Authorization": `token ${TOKEN}`

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

王者荣耀大数据运营总结

发表于 2017-12-04

导语 |围绕王者荣耀大数据运营,依托对局日志和好友关系,开展了王者周报、赛季总结和周年庆活动等项目。这些案例中,遇到了哪些挑战?每一个指标是如何计算的呢?

作者:曾志浩,腾讯微信游戏中心,数据分析工程师,专注于手游数据分析与挖掘,微信游戏中心用户画像、内容推荐等工作。

1. 整体框架

从数据开发的角度,此类运营项目主要会和策划同学、后台同学进行协作。前期工作主要是整理数据源、拆解数据指标,本文主要专注大数据计算过程,最后一步将结果同步给后台。框架如图所示。


2. 计算引擎


计算引擎,可以选择的是:Hive-SQL 或者原生的Map/Reduce,如何抉择?我将列一下这两个方式的优劣对比,欢迎拍砖~

Hive-SQL

优势:

前期很爽,开发难度较低,快速上线。

劣势(后续迭代将是梦魇):

  • SQL 实现复杂逻辑约束较多,局限了想象力。
  • SQL 不能轻松地自定义函数、对象,重复地开发,代码冗长。
  • 中间结果,无法复用。
  • 增加/修改 指标,需要大幅度改动,后续的实现越来越困难。
  • 调优的空间,较少。

原生Map/Reduce

优势:(Hive-SQL 的缺点,反过来就是,不再赘述。)

  • RDD 复用,读入一次数据源,后面在内存中Cache、迭代计算。

劣势:

  • 前期开发较慢,需要搭建较多的流程和抽象接口。

选型

  • Spark 常用算子:map, filter, flatMap, reduceByKey, join 等。
  • 编程语言 scala,语法简洁,函数式编程。
  • 工程部署 Tesla 平台,可视化界面,便捷地增加数据依赖、调度、调参、查看日志。

3. 如何展开诸多数据指标?

数据指标纷繁复杂,主要的解决方案包括:1.优化好友关系链计算;2.分治法;3.封装求和计算;4.封装取最大/最小的指标;5.避免改变RDD的核心数据结构;6. 稳健地运行。面临大数据量时,希望1-2介绍的内容能提供读者一些启发;3-5 将不同类型的计算,分别封装,简化 reduceByKey的表达,代码也会比较简练。在解决常见问题时,第6点作为一个参考。接下来,见招拆招。

优化好友关系链计算

业务背景: 王者周报中,好友出现了游戏好友非微信好友,这种情况不太能接受。

对局日志和好友关系进行关联运算,判断是否开黑;计算了每个用户的基础指标后,关联好友关系,PK得到神奇好友。这里的瓶颈在于关联运算,数据集体量庞大,就像霸天虎和威震天发生了碰撞。先看一下 join 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码  // join 调用 cogroup,生成 (key, Tuple(list(v1,v2))),然后再做 flatMap。
/**
* Return an RDD containing all pairs of elements with matching keys in `this` and `other`. Each
* pair of elements will be returned as a (k, (v1, v2)) tuple, where (k, v1) is in `this` and
* (k, v2) is in `other`. Uses the given Partitioner to partition the output RDD.
*/
def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))] = self.withScope {
this.cogroup(other, partitioner).flatMapValues( pair =>
for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, w)
)
}

/**
* For each key k in `this` or `other`, return a resulting RDD that contains a tuple with the
* list of values for that key in `this` as well as `other`.
*/
def cogroup[W](other: RDD[(K, W)], partitioner: Partitioner)
: RDD[(K, (Iterable[V], Iterable[W]))] = self.withScope {
if (partitioner.isInstanceOf[HashPartitioner] && keyClass.isArray) {
throw new SparkException("HashPartitioner cannot partition array keys.")
}
val cg = new CoGroupedRDD[K](Seq(self, other), partitioner)
cg.mapValues { case Array(vs, w1s) =>
(vs.asInstanceOf[Iterable[V]], w1s.asInstanceOf[Iterable[W]])
}
}

关键的执行是 cg.mapValues,还有后面一步的 flatMapValues,运行的日志显示并行度和 other 关系紧密。当A join B 时,对 B 进行合理的 hashPartition,可以提升运行效率。经过测试, join算子性能强劲:不发生数据倾斜的前提,可以快速完成十亿级和十亿级的 RDD 进行关联。补充一点题外话,顺藤摸瓜看一下其他核心算子:distinct -> reduceByKey -> combineByKey。所以 Spark 的 distinct算子不会导致数据倾斜。

1
2
3
4
5
6
7
复制代码  // distinct: 先将RDD 转为 Key-Value RDD,值为 null;然后调用 reduceByKey。
/**
* Return a new RDD containing the distinct elements in this RDD.
*/
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)
}

分治法

业务背景:周年庆,计算用户一年内的最高连胜、连败,一年内开黑最多的好友。

将一年的对局日志全部读入内存?考虑到王者的体量,打消了这个想法。分治法也许是突破口:先把每个月的对局日志合并计算,然后对12个月的中间结果再做一次合并。图示如下,每天的日志评估量级是2.5E,月活用户评估量级是1.2E,这个规模可以轻松应对。


[ 分治法解决周年庆日志庞大的难点 ]

封装求和计算

从一个简单例子来,求玩家的对局数和胜场数。数据源加上了字段含义,实际的代码会更简洁 data: Rdd[(String, (Int, Int))]。


此次,x._1, x._2 可读性、维护性很糟糕,业务中有一些同学写了大量此类Magic 代码,异常头疼。王者周报超过50个数据项,开发过程中指标变化、增删都是常事,所以将用户的数据指标封装在 UsrSimpleInfo 类。示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码class UsrSimpleInfo(mvp: Int, godlike: Int, three: Int, four: Int, five: Int,
kill: Int, dead: Int, assist: Int, battle: Int) {
val mvpCnt = mvp
val godLikeCnt = godlike
val threeKill = three
val fourKill = four
val fiveKill = five
val killCnt = kill
val deadCnt = dead
val assistCnt = assist
val battleCnt = battle

override def toString = "UsrSimpleInfo: " + mvpCnt + "/" + threeKill + "/" + fourKill + "/" + fiveKill +
"/" + killCnt + "/" + deadCnt + "/" + assistCnt + "/" + battleCnt

def +(that: UsrSimpleInfo) = new UsrSimpleInfo(
mvpCnt + that.mvpCnt, godLikeCnt + that.godLikeCnt,
threeKill + that.threeKill,
fourKill + that.fourKill, fiveKill + that.fiveKill,
killCnt + that.killCnt, deadCnt + that.deadCnt, assistCnt + that.assistCnt, battleCnt + that.battleCnt
)

def this() = this(0, 0, 0, 0, 0, 0, 0, 0, 0)
}

reduce 的写法如下。

封装取最大/最小的指标

业务背景:1. 用户最常用英雄的表现,2. 在一段时间里,用户最新的游戏昵称。

用户在指标A 最大时的其他数据项, Hive-SQL 需要先求用户指标A的最大值,然后再join 原始表,实现方式比较笨重。

类似前面的做法,把上述逻辑进行对象封装和函数重载:


避免改变RDD的核心数据结构

业务背景:用户每个对局模式的对局场次,每个英雄的使用场次和表现。

粗暴方法如下。


两个 map算子 都会对全量对局日志进行 transform,内存开销极大。Key-Value结构的RDD,修改Value是正常的,但是应该避免改变 Key。突破点在于:对局模式和英雄数枚举下来是比较少的,适合HashMap存储;最后reduceByKey 阶段做 Foldleft 合并数据。示例如下。

稳健地运行

产品发布之后,我发现“维稳”的压力很大。调试和运行的过程中,遇到了不少挑战。列举几个关键的节点。

  • 入库日志校验和依赖。
  • 运行监控。

Spark 任务的调度机制、内存分配,需要考虑多个影响因素。从 Spark UI 页面中,可以跟踪很多有价值的信息。任务根据 Action操作,划分到 Jobs,然后再进一步到 Stages。重点关注Stages阶段真实的执行顺序!Executors 显示了driver 节点和 data 节点的运行时信息。

  • 超时告警和错误告警。

4. 提升效果

运行耗时其实不适合作为评估指标,仅做参考。不考虑分配的内存/CPU资源,还有计算集群负载,都是耍流氓。

下面写的运行耗时,不包含准备数据,强调的是目前计算花费的时间和日志吞吐量,应对产品运营节奏不再是瓶颈。上一节提到的方法,通常会综合应用、随机应变。

  • 优化:join算子调优;将面向过程的计算封装成对象;避免改变RDD的核心数据结构。 王者周报涉及十亿级别的上报日志(包括5v5、3v3、1v1对局、英雄熟练度等)和庞大的关系链,计算耗时2.5小时-3小时。
  • 优化:将面向过程的计算封装成对象。 赛季总结的各项基础指标,只需要3-4小时完成。赛季结束的次日,通常可以体验/发布赛季总结。
  • 优化: 分治法。 周年庆: 在王者荣耀用户体量和活跃度下,基于一年的对局日志计算了最大连胜、连败和开黑最多的好友。
  • 优化:剪枝原始日志数据。 计算一个赛季的两两开黑情况,耗时50分钟。

5. 从产品角度解析王者数据运营

  • 相比罗列数据指标,不如将数据包装成概念。数据不在于多、不在于奇,而在于“情”。


[ 左一成绩单罗列数据,新版包装成每周一字 ]

  • 呈现用户个人的数据仅是初级层次,如何连接好友,需要更多的想象。
  • 王者大数据运营,在资源转化、曝光用户数、游戏用户的渗透,分享量、来着分享的访问,以及最常规的拉新、拉回流分析上,都具有上佳的表现。

以下为品体验二维码:


[ 周年庆 ]


[ S8赛季总结 ]


[ 王者周报 ]

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

10个Python面试常问的问题404 Not Found

发表于 2017-12-04

概述

Python是个非常受欢迎的编程语言,随着近些年机器学习、云计算等技术的发展,Python的职位需求越来越高。下面我收集了10个Python面试官经常问的问题,供大家参考学习。

类继承

有如下的一段代码:

1
2
3
4
5
6
7
8
9
10
复制代码class A(object):
def show(self):
print 'base show'

class B(A):
def show(self):
print 'derived show'

obj = B()
obj.show()

如何调用类A的show方法了。 方法如下:

1
2
复制代码obj.__class__ = A
obj.show()

__class__方法指向了类对象,只用给他赋值类型A,然后调用方法show,但是用完了记得修改回来。

方法对象

问题:为了让下面这段代码运行,需要增加哪些代码?

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class A(object):
def __init__(self,a,b):
self.__a = a
self.__b = b
def myprint(self):
print 'a=', self.__a, 'b=', self.__b


a1=A(10,20)
a1.myprint()

a1(80)

答案:为了能让对象实例能被直接调用,需要实现__call__方法

1
2
3
4
5
6
7
8
复制代码class A(object):
def __init__(self, a, b):
self.__a = a
self.__b = b
def myprint(self):
print 'a=', self.__a, 'b=', self.__b
def __call__(self, num):
print 'call:', num + self.__a

new和init

下面这段代码输入什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码class B(object):
def fn(self):
print 'B fn'
def __init__(self):
print "B INIT"


class A(object):
def fn(self):
print 'A fn'

def __new__(cls,a):
print "NEW", a
if a>10:
return super(A, cls).__new__(cls)
return B()

def __init__(self,a):
print "INIT", a

a1 = A(5)
a1.fn()
a2=A(20)
a2.fn()

答案:

1
2
3
4
5
6
复制代码NEW 5
B INIT
B fn
NEW 20
INIT 20
A fn

使用__new__方法,可以决定返回那个对象,也就是创建对象之前,这个可以用于设计模式的单例、工厂模式。__init__是创建对象是调用的。

Python list和dict生成

下面这段代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码ls = [1,2,3,4]
list1 = [i for i in ls if i>2]
print list1

list2 = [i*2 for i in ls if i>2]
print list2

dic1 = {x: x**2 for x in (2, 4, 6)}
print dic1

dic2 = {x: 'item' + str(x**2) for x in (2, 4, 6)}
print dic2

set1 = {x for x in 'hello world' if x not in 'low level'}
print set1

答案:

1
2
3
4
5
复制代码[3, 4]  
[6, 8]
{2: 4, 4: 16, 6: 36}
{2: 'item4', 4: 'item16', 6: 'item36'}
set(['h', 'r', 'd'])

全局和局部变量

下面这段代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
复制代码num = 9

def f1():
num = 20

def f2():
print num


f2()
f1()
f2()

答案:

1
2
复制代码9
9

num不是个全局变量,所以每个函数都得到了自己的num拷贝,如果你想修改num,则必须用global关键字声明。比如下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码num = 9

def f1():
global num
num = 20

def f2():
print num

f2()
f1()
f2()

# prints:
# 9
# 20

交换两个变量的值

一行代码交换两个变量值

1
2
复制代码a=8
b=9

答案:

1
复制代码(a,b) = (b,a)

默认方法

如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class A(object):
def __init__(self,a,b):
self.a1 = a
self.b1 = b
print 'init'
def mydefault(self):
print 'default'

a1 = A(10,20)
a1.fn1()
a1.fn2()
a1.fn3()

方法 fn1/fn2/fn3 都没有定义,添加代码,是没有定义的方法都调用mydefault函数,上面的代码应该输出

1
2
3
复制代码default
default
default

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码class A(object):
def __init__(self,a,b):
self.a1 = a
self.b1 = b
print 'init'
def mydefault(self):
print 'default'
def __getattr__(self,name):
return self.mydefault

a1 = A(10,20)
a1.fn1()
a1.fn2()
a1.fn3()

方法__getattr__只有当没有定义的方法调用时,才是调用他。当fn1方法传入参数时,我们可以给mydefault方法增加一个*args不定参数来兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码class A(object):
def __init__(self,a,b):
self.a1 = a
self.b1 = b
print 'init'
def mydefault(self,*args):
print 'default:' + str(args[0])
def __getattr__(self,name):
print "other fn:",name
return self.mydefault

a1 = A(10,20)
a1.fn1(33)
a1.fn2('hello')
a1.fn3(10)

包管理

一个包里有三个模块,mod1.py, mod2.py, mod3.py,但使用from demopack import *导入模块时,如何保证只有mod1、mod3被导入了。

答案:增加__init__.py文件,并在文件中增加:

1
复制代码__all__ = ['mod1','mod3']

闭包

写一个函数,接收整数参数n,返回一个函数,函数的功能是把函数的参数和n相乘并把结果返回。

答案:

1
2
3
4
5
6
7
8
9
复制代码def mulby(num):
def gn(val):
return num * val

return gn


zw = mulby(7)
print(zw(9));

性能

解析下面的代码慢在哪

1
2
3
4
5
复制代码def strtest1(num):
str='first'
for i in range(num):
str+="X"
return str

答案:python的str是个不可变对象,每次迭代,都会生成新的str对象来存储新的字符串,num越大,创建的str对象越多,内存消耗越大。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

哈希表的基本解释 哈希表

发表于 2017-12-04

哈希表

使用哈希表可以进行非常快速的查找操作。但是,哈希表究竟是什么玩意儿?很多人避而不谈,虽然知道经常用到,很多语言的内置数据结构像python中的字典,java中的HashMap,都是基于哈希表实现。但哈希表究竟是啥?

哈希是什么?

(如果觉得废话很多,可以直接看建立哈希表)

散列(hashing)是电脑科学中一种对资料的处理方法,通过某种特定的函数/算法(称为散列函数/算法)将要检索的项与用来检索的索引(称为散列,或者散列值)关联起来,生成一种便于搜索的数据结构(称为散列表)。也译为散列。旧译哈希(误以为是人名而采用了音译)。它也常用作一种资讯安全的实作方法,由一串资料中经过散列算法(Hashing algorithms)计算出来的资料指纹(data fingerprint),经常用来识别档案与资料是否有被窜改,以保证档案与资料确实是由原创者所提供。
—-Wikipedia

哈希函数

所有的哈希函数都具有如下一个基本特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。

哈希表

  • 若关键字为k,则其值存放在f(k)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数,按这个思想建立的表为散列表。
  • 对不同的关键字可能得到同一散列地址,即k1≠k2,而f(k1)=f(k2),这种现象称为冲突。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数f(k)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。
  • 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

建立哈希表

这看了一堆概念,老衲甚为头疼。总的来说,哈希表就是一个具备映射关系的表,你可以通过映射关系由键找到值。有没有现成的例子?当然有,不过你直接用就没意思了。

反正就是要实现f(k),即实现key-value的映射关系。我们试着自己实现以下:

1
2
3
4
5
6
7
8
9
10
11
复制代码class Map:
def __init__(self):
self.items=[]

def put(self,k,v):
self.items.append((k,v))

def get(self,k):
for key,value in self.items:
if(k==key):
return value

这样实现的Map,查找的时间复杂度为O(n)。
“这看上去与key没什么关系啊,这不是顺序查找么,逗我呢?”
这只是一个热身,好吧,下面我们根据定义,来搞一个有映射函数的:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class Map:
def __init__(self):
self.items=[None]*100

def hash(self,a):
return a*1+0

def put(self,k,v):
self.items[hash(k)]=v
def get(self,k):
hashcode=hash(k)
return self.items[hashcode]

“这hash函数有点简单啊”
是的,它是简单,但简单不代表它不是哈希函数,事实上,它是由一种叫直接定址法的方法建立的哈希函数,是一个线性函数:
hash(k)= a*k+b

“为啥初始化就指定了100容量?”
要指出的是,这个是必须的。你想通过下标存储并访问,对于数组来说,这不可避免。在JDK源码里,你也可以看到,Java的HashMap的初始容量设成了16。你可能说,你这hash函数,我只要key设为100以上,这程序就废了。是啊,它并不完美。这涉及到扩容的事情,稍后再讲。

直接定址法的优点很明显,就是它不会产生重复的hash值。但由于它与键值本身有关系,所以当键值分布很散的时候,会浪费大量的存储空间。所以一般是不会用到直接定址法的。

处理冲突

假如某个hash函数产生了一堆哈希值,而这些哈希值产生了冲突怎么办(实际生产环境中经常发生)?在各种哈希表的实现里,处理冲突是必需的一步。
比如你定义了一个hash函数:
hash(k)=k mod 10
假设key序列为:[15,1,24,32,55,64,42,93,82,76]

0 1 2 3 4 5 6 7 8 9
1 32 93 24 15 76
42 64 55
82

一趟下来,冲突的元素有四个,下面有几个办法。

开放定址法

开放定址法就是产生冲突之后去寻找下一个空闲的空间。函数定义为:

hash_table hash_table
其中,hash(key)是哈希函数,di是增量序列,i为已冲突的次数。

  • 线性探测法
    hash_table hash_table

即di=i,或者其它线性函数。相当于逐个探测存放地址的表,直到查找到一个空单元,然后放置在该单元。

[15,1,24,32,55,64,42,93,82,76]

可以看到,在55之前都还没冲突:

0 1 2 3 4 5 6 7 8 9
1 32 24 15

此时插入55,与15冲突,应用线性探测,此时i=1,可以得到:

0 1 2 3 4 5 6 7 8 9
1 32 24 15 55

再插入64,冲突不少,要取到i=3:

0 1 2 3 4 5 6 7 8 9
1 32 24 15 55 64

插入42,i=1:

0 1 2 3 4 5 6 7 8 9
1 32 42 24 15 55 64

插入93,i=5:

0 1 2 3 4 5 6 7 8 9
1 32 42 24 15 55 64 93

插入82,i=7:

0 1 2 3 4 5 6 7 8 9
1 32 42 24 15 55 64 93 82

插入76,i=4:

0 1 2 3 4 5 6 7 8 9
76 1 32 42 24 15 55 64 93 82

发现越到后面,冲突的越来越离谱,因为表的大小选择也很重要,此例中选择了10作为表的大小,所以容易产生冲突。一般来讲,越是质数,mod取余就越可能分布的均匀。

  • 平方探测

hash_table hash_table
这称作平方探测法,一个道理,也是查找到一个空单元然后放进去。这里就不一步一步说明了=。=

  • 伪随机探测
    di是一个随机数序列。
    “随机数?那get的时候咋办?也是随机数啊,怎么确保一致?”
    所以说了,是伪随机数。其实我们在计算机里接触的几乎都是伪随机数,只要是由确定算法生成的,都是伪随机。只要种子确定,生成的序列都是一样的。序列都一样,那不就可以了么=。=

链表法

这是另外一种类型解决冲突的办法,散列到同一位置的元素,不是继续往下探测,而是在这个位置是一个链表,这些元素则都放到这一个链表上。java的HashMap就采用的是这个。

再散列

如果一次不够,就再来一次,直到冲突不再发生。

建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表(注意:在这个方法里面是把元素分开两个表来存储)。

说了这么一堆,举个例子,用开放地址法(线性探测):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码class Map:
def __init__(self):
self.hash_table=[[None,None]for i in range(11)]

def hash(self,k,i):
h_value=(k+i)%11
if self.hash_table[h_value][0]==k:
return h_value
if self.hash_table[h_value][0]!=None:
i+=1
h_value=self.hash(k,i)
return h_value

def put(self,k,v):
hash_v=self.hash(k,0)
self.hash_table[hash_v][0]=k
self.hash_table[hash_v][1]=v
def get(self,k):
hash_v=self.hash(k,0)
return self.hash_table[hash_v][1]

“能不能不要定死长度?11个完全不够用啊”

这是刚才的问题,所以有了另外一个概念,叫做载荷因子(load factor)。载荷因子的定义为:
α= 已有的元素个数/表的长度

由于表长是定值, α与“填入表中的元素个数”成正比,所以, α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子 α的函数,只是不同处理冲突的方法有不同的函数。

所以当到达一定程度,表的长度是要变的,即resize=。=像java的HashMap,载荷因子被设计为0.75;超过0.8,cpu的cache missing会急剧上升。可以看下这篇讨论:
www.zhihu.com/question/22…

具体扩容多少,一般选择扩到已插入元素数量的两倍,java也是这么做的。

接着上面,再升级一下我们的map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码class Map:
def __init__(self):
self.capacity=11
self.hash_table=[[None,None]for i in range(self.capacity)]
self.num=0
self.load_factor=0.75

def hash(self,k,i):
h_value=(k+i)%self.capacity
if self.hash_table[h_value][0]==k:
return h_value
if self.hash_table[h_value][0]!=None:
i+=1
h_value=self.hash(k,i)
return h_value
def resize(self):
self.capacity=self.num*2 #扩容到原有元素数量的两倍
temp=self.hash_table[:]
self.hash_table=[[None,None]for i in range(self.capacity)]
for i in temp:
if(i[0]!=None): #把原来已有的元素存入
hash_v=self.hash(i[0],0)
self.hash_table[hash_v][0]=i[0]
self.hash_table[hash_v][1]=i[1]

def put(self,k,v):
hash_v=self.hash(k,0)
self.hash_table[hash_v][0]=k
self.hash_table[hash_v][1]=v
self.num+=1 #暂不考虑key重复的情况,具体自己可以优化
if(self.num/len(self.hash_table)>self.load_factor):# 如果比例大于载荷因子
self.resize()
def get(self,k):
hash_v=self.hash(k,0)
return self.hash_table[hash_v][1]

看上面的函数,可以看到resize是一个比较耗时的操作,更何况作为一个python小白的我写的不是很好。可以去看一下Java的HashMap的hash方法和resize方法,其中的思路要精妙的多。

关于哈希表,原理的东西都基本差不多了。可以看到,它本质要解决的是查找时间的问题。如果顺序查找的话,时间复杂度为O(n);而哈希表,时间复杂度则为O(1)!直接甩了一个次元有木有,这也就是为什么在大量数据存储查找的时候,哈希表得到大量应用的原因。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我与Nodejs重新认识的第2周 - Nodejs 底层

发表于 2017-12-04

书接上次:《我与Node.js重新认识的第一周 - Node.js 风格特点》。这次读了一些关于底层实现的东西:1. 《深浅》第3章 异步I/O - node.js是如何实现异步i/o的
2. Udemy 《Learn and Understand NodeJS Learn and Understand NodeJS》 Section 2&3

V8引擎

首先,学习node.js一定要了解V8引擎,他是一个可以把js直接编译成(处理器可以识别的)机器码的东西。

再详细点,V8是一个1. 开源的
2. 用C++写的
3. 根据ECMA标准实现JavaScript
4. 可以把JavaScript编译成处理器可以识别的机器码
5. 可以独立运行
6. 也可以嵌入其他C++应用

的JavaScript引擎。

Node.js与V8引擎

普通青年使用V8:运行V8, JavaScript -> V8 —compile—> Machine Code文艺青年则有这样一个大胆的想法:那些普通的js方法太没意思了,能力优先。如果我可以写一些C++代码,当做Add-on加到V8上,这样V8就有能力识别具有更多的Javascript命令了,就更强大了。比如说file相关的东西,本来js不能做,现在我用c++在底部实现好,然后告诉V8,当用户在js中写道file.open(xxx)的时候,就来用c++执行file open的功能,这样你的js(二b)就是有处理文件能力的js(牛b)了。
文艺青年的想法其实就是我们的Node.js:一个把V8引擎嵌进去的C++应用,这个C++应用实现了超级多的customized新功能,这些功能使得这个应用(Node.js)非常的适合服务器开发。
服务器开发都需要什么新功能呢 === Node.js实现的新功能都包括哪些方面呢:1. 管理可复用代码
2. 处理文件
3. 处理数据库
4. 互联网通信
5. 接受request,发送respond
6. 处理需要一定时间才能完成的工作

Node.js架构

  1. 第一层是C++ core,就是那些新加的customized功能 (其他讲解中,还会提到比如event loop,libuv等,这些之后说)。
  2. 第二层是JS core,这一层用js实现,基于/调用C++ core,让用户可以更好的使用那些C++功能,同时也实现了许多常用的功能。
    在node.js源码中,C++ core是在src文件夹内。JS core是在lib文件夹内。由此可见二者的层级关系。

Node.js 异步I/O

Node.js如何实现异步 I/O, 从JS到OS到底发生了哪些步骤(这个是udemy+《深浅》的合体版总结)

  1. 你写的js代码调用了node.js的JS core (比如 fs相关功能:github.com/nodejs/node…),并且你设定了一个回调函数
  2. JS core部分调用了C++ core (fs.js 调用的其中一个.cc: github.com/nodejs/node…)
  3. C++ core调用libuv
  4. C++ core调用libuv的方法来封装请求对象(异步I/O过程中重要的中间产物,中间指的是从js到os之间),其中包含了异步I/O中最重要的东西之一:回调函数。
  5. libuv把封装好的请求对象发到OS
  6. 发到OS中的线程池(thread pool)等待被执行
  7. 线程池中某一线程将发来的请求对象中包含的I/O操作进行执行,结果存在该请求对象的req->result属性中。然后提交完成状态,也就是类似通知说”我完成了!”,之后把线程归还给线程池
  8. 处于完成状态的请求对象,被观察者(图中的两个小人儿,不同类型的事件有不同的观察者)在事件循环(Event Loop:大while循环,每个循环叫一个tick)中提出来(通过libuv中方法来检查是否有执行完的请求),然后放到队列(Completed Events Queue)中
  9. 事件循环从观察者的队列中取出处于完成状态请求对象
  10. 取出其中包含的I/O操作执行结果以及回调函数,发到V8中执行。到这就达到了调用#1中设定的回调函数的目的。

注:a. V8引擎执行的JavaScript是同步的(sync)(stackoverflow.com/questions/2…)且单线程b. #1-#9 与 #10 是同时在工作的,I/O事件一个一个执行,然后最后发送到V8引擎中一个一个(因为JS是同步的)的执行回调函数c. 由于b,整个Node.js有了异步I/O的能力d. 事件驱动(event driven)、非阻塞(non-blocking)I/O的特点也就可以解释了。事件驱动就是指的#7-#10,事件被完成触发了之后各个步骤,直到最后执行回调函数。非阻塞I/O就是整个#1-#10这个过程,我们在V8中执行的JavaScript代码并不会因为事件的执行而停止,#1-#9和#10同时工作,一个执行I/O事件,一个得到通知执行回调函数。e. 从d的解释中,可以更好的理解上一篇文章中最后那段说Node.js是单线程/多线程。
下次写事件(Event)和事件发射器(Event Emitter)相关的东西。欢迎大家交流,指正~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…919920921…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%