首页 文章 精选 留言 我的

精选列表

搜索[技巧],共4620篇文章
优秀的个人博客,低调大师

SSH 技巧

SSH 是 Linux 下进行远程连接的基本工具。 一般情况下我们可以通过 ssh username@xxx.xxx.xxx.xxx 登录远程服务器,如果要管理多台服务器,这样太长了。 可以在家目录的 .ssh 中新建config文件,设置别名。 比如我的个人云主机, # Host 可跟多个表示别名 Host cloud alias HostName 120.163.xxx.323 User ubuntu Port 4722 # 私钥路径 IdentityFile ~/.ssh/id_rsa 这样执行 ssh cloud 或 ssh alias 就无密码登录云主机了 1. 执行远程命令 ssh cloud "df -h" image.png 2. 用分号分隔多个命令,用引号引起来 ssh cloud "df -h; ps;" 3. 遇到需要交互的命令,加 -t 参数 $ ssh -t cloud top 比如 sudo 开头的可能需要用户输入密码,需要 TTY。 添加 -t 参数后,ssh 会保持登录状态,直到你退出需要交互的命令。 image.png 再举个实际例子:ssh -t flux sudo -u fueladminprd ssh 192.168.1.100 这句话实际执行了两步: ssh -t flux #登录名为flux的服务器,因为需要交互式,加上-t sudo -u fueladminprd ssh 192.168.1.100 # 以 fueladminprd 用户在 flux 上执行 ssh 192.168.1.100 为了安全,flux 就是跳板机,192.168.1.100 为实际测试服务器。 4. 执行多行命令 $ ssh cloud "echo 'haha' > pwd > ls " 可以用单引号或双引号开头,然后写上几行命令,最后再用相同的引号来结束。> 开头的就是输入下一行命令 如果需要在命令中使用引号,可以混合使用单双引号。 image.png 5. 在命令中使用变量 在远程服务器执行 ls node,本地定义变量a,传入到命令中。达到一样的效果。 在下图的命令中为 bash 指定了 -c 参数 $ a=node $ ssh cloud bash -c " ' > ls $a > ' " image.png 6. 执行本地脚本 本地创建demo.sh,内容是:ls node 运行 ssh cloud < demo.sh 通过重定向 stdin,本地的脚本 demo.sh 在远程服务器上被执行。 image.png 7. 为脚本传入参数 修改 demo.sh 内容为: ls node echo $0 echo $1 echo $2 执行 ssh cloud 'bash -s'< demo.sh aa bb cc bash 就是 $0 第一个参数。 image.png 参考: http://www.openssh.com/https://www.cnblogs.com/sparkdev/p/6842805.htmlhttps://www.zcfy.cc/article/top-20-openssh-server-best-security-practices

优秀的个人博客,低调大师

Android mipmap技巧

你现在使用Android Studio来新建一个项目,你会发现有如下的目录结构: image.png 怎么会有这么多mipmap(贴图)开头的文件夹,而且它们的命名规则和drawable(可绘制)文件夹很相似,也是hdpi、mdpi、xhdpi等等,并且里面还真是放的图片,难道Android项目中放置图片的位置已经改了。mipmap文件夹只是用来放置应用程序的icon的,仅此而已。 Android是极度建议我们在每一种分辨率的文件夹下面都放一个相应尺寸的icon的。将icon放置在mipmap文件夹还可以让我们程序的launcher图标自动拥有跨设备密度展示的能力,比如说一台屏幕密度是xxhdpi的设备可以自动加载mipmap-xxxhdpi下的icon来作为应用程序的launcher图标,这样图标看上去就会更加细腻。 除此之外,对于每种密度下的icon应该设计成什么尺寸其实Android也是给出了最佳建议,icon的尺寸最好不要随意设计,因为过低的分辨率会造成图标模糊,而过高的分辨率只会徒增APK大小。建议尺寸如下表所示: 密度类型 代表的分辨率(px) 屏幕密度(dpi) 换算(dp/px) 比例 低密度(ldpi) 240x320 120 1dp=0.75px 3 中密度(mdpi) 320x480 160 1dp=1px 4 高密度(hdpi) 480x800 240 1dp=1.5px 6 超高密度(xhdpi) 720x1280 320 1dp=2px 8 超超高密度(xxhdpi) 1080x1920 480 1dp=3px 12 首先我准备了一张270*480像素的图片: christmas.jpg 将图片命名为christmas.jpg,然后把它放在mipmap-xxhdpi文件夹下面。为什么要放在这个文件夹下呢?是因为我的手机屏幕的密度就是xxhdpi的。那么怎么才能知道自己手机屏幕的密度呢?使用如下方法先获取到屏幕的dpi值: float xdpi = getResources().getDisplayMetrics().xdpi; float ydpi = getResources().getDisplayMetrics().ydpi; Log.e("tag", "-----xdpi=" + xdpi); //xdpi=397.565 Log.e("tag", "-----ydpi=" + ydpi); //ydpi=396.24 其中xdpi代表屏幕宽度的dpi值,ydpi代表屏幕高度的dpi值,通常这两个值都是近乎相等或者极其接近的,在我的手机上这两个值都约等于397。那么397又代表着什么意思呢?我们直接参考下面这个表格就知道了: dpi范围 密度 0dpi ~ 120dpi ldpi 120dpi ~ 160dpi mdpi 160dpi ~ 240dpi hdpi 240dpi ~ 320dpi xhdpi 320dpi ~ 480dpi xxhdpi 480dpi ~ 640dpi xxxhdpi 从表中可以看出,397dpi是处于320dpi到480dpi之间的,因此属于xxhdpi的范围。 图片放好了之后,下面我在布局文件中引用这张图片,如下所示: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/christmas" /> </RelativeLayout> 在ImageView控件中指定加载christmas这张图,并把ImageView控件的宽高都设置成wrap_content,这样图片有多大,我们的控件就会有多大。 现在运行一下程序,效果如下所示: image.png 由于我的手机分辨率是1080*1920像素的,而这张图片的分辨率是270*480像素的,刚好是手机分辨率的四分之一,因此从上图中也可以看出,christmas图片的宽和高大概都占据了屏幕宽高的四分之一左右,大小是比较精准的。 下面我们尝试做点改变,将christmas.png这张图移动到mipmap-xhdpi文件夹下,注意不是复制一份到mipmap-xhdpi文件夹下,而是将图片移动到mipmap-xhdpi文件夹下,然后重新运行一下程序,效果如下图所示 image.png 嗯?怎么感觉图片好像变大了一点,是错觉吗? 那么我们再将这张图移动到mipmap-mdpi文件夹下试试,重新运行程序,效果如下图所示: image.png 这次肯定不是错觉了,这实在是太明显了,图片被放大了! 那么为什么好端端的一张图片会被自动放大呢?而且这放大的比例是不是有点太过份了。其实不然,Android所做的这些缩放操作都是有它严格的规定和算法的。可能有不少做了很多年Android的朋友都没去留意过这些缩放的规则,因为这些细节太微小了,那么本篇的探索里面,我们就来把这些细节理理清楚。 首先解释一下图片为什么会被放大,当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么mipmap-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用christmas这张图时,如果mipmap-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果mipmap-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,去了mipmap-xxxhdpi文件夹,然后发现这里也没有christmas这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去mipmap-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是mipmap-xhdpi -> mipmap-hdpi -> mipmap-mdpi -> mipmap-ldpi。 总体匹配规则就是这样,那么比如说现在终于在mipmap-mdpi文件夹下面找到christmas这张图了,但是系统会认为你这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。 那么同样的道理,如果系统是在mipmap-xxxhdpi文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作。所以,我们可以尝试将christmas这张图移动到mipmap-xxxhdpi文件夹下面将会得到这样的结果: image.png 可以看到,现在图片的宽和高都达到不手机屏幕的四分之一,说明图片确实是被缩小了。 另外,刚才在介绍规则的时候提到了一个mipmap-nodpi文件夹,这个文件夹是一个密度无关的文件夹,放在这里的图片系统就不会对它进行自动缩放,原图片是多大就会实际展示多大。但是要注意一个加载的顺序,mipmap-nodpi文件夹是在匹配密度文件夹和更高密度文件夹都找不到的情况下才会去这里查找图片的,因此放在mipmap-nodpi文件夹里的图片通常情况下不建议再放到别的文件夹里面。 图片被放大的原因现在我们已经搞清楚了,那么接下来还有一个问题,就是放大的倍数是怎么确定的呢? 还是看一下刚才的 dpi范围-密度 表格: dpi范围 密度 0dpi ~ 120dpi ldpi 120dpi ~ 160dpi mdpi 160dpi ~ 240dpi hdpi 240dpi ~ 320dpi xhdpi 320dpi ~ 480dpi xxhdpi 480dpi ~ 640dpi xxxhdpi 可以看到,每一种密度的dpi范围都有一个最大值,这个最大值之间的比例就是图片会被系统自动放大的比例。 口说无凭,下面我们来通过实例验证一下,修改布局文件中的代码,如下所示: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/christmas" android:src="@mipmap/christmas" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:onClick="buttonClick" android:text="获取图片宽高" /> <TextView android:layout_width="wrap_content" android:layout_alignParentRight="true" android:layout_marginTop="100dp" android:id="@+id/tvWidthHeight" android:layout_marginRight="6dp" android:layout_height="wrap_content" /> </RelativeLayout> 可以看到,我们添加了一个按钮,并给按钮注册了一个点击事件。然后在MainActivity中处理这个点击事件: public class MainActivity extends AppCompatActivity { private AppCompatImageView imageView; private TextView tvWidthHeight; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageView = findViewById(R.id.christmas); tvWidthHeight = findViewById(R.id.tvWidthHeight); float xdpi = getResources().getDisplayMetrics().xdpi; float ydpi = getResources().getDisplayMetrics().ydpi; Log.e("tag", "-----xdpi=" + xdpi); //xdpi=397.565 Log.e("tag", "-----ydpi=" + ydpi); //ydpi=396.24 } public void buttonClick(View view) { tvWidthHeight.setText("图片宽:" + imageView.getWidth() + "\n图片高:" + imageView.getHeight()); } } 这里在点击事件中分别获取图片的宽和高并使用TextView显示出来。代码修改这么多就可以了,然后将图片移动到mipmap-mdpi文件夹下。 下面我们来开始分析,mdpi密度的最高dpi值是160,而xxhdpi密度的最高dpi值是480,因此是一个3倍的关系,那么我们就可以猜测,放到mipmap-mdpi文件夹下的图片在xxhdpi密度的设备上显示会被放大3倍。对应到christmas这张图,原始像素是270*480,放大3倍之后就应该是810*1440像素。下面运行程序,效果如下图所示: image.png 验证通过。我们再来试验一次,将图片移动到mipmap-xxxhdpi目录下。xxxhdpi密度的最高dpi值是640,480是它的0.75倍,那么我们就可以猜测,放到mipmap-xxxdpi文件夹下的图片在xxhdpi密度的设备上显示会被缩小至0.75倍。270*480的0.75倍应该是202.5*360,由于像素不支持小数点,那么四舍五入就应该是203*360像素。重新运行程序,效果如下图所示: image.png 再次验证通过。如果你有兴趣的话可以使用其它几种dpi的mipmap文件夹来试一试,应该都是适配这套缩放规则的。这样我们就把图片为什么会被缩放,以及具体的缩放倍数都搞明白了,mipmap相关的细节你已经探究的非常细微了。 讲一讲我们在实际开发当中会遇到的场景。根据Android的开发建议,我们在准备图片资源时尽量应该给每种密度的设备都准备一套,这样程序的适配性就可以达到最好。但实际情况是,公司的UI们通常就只会给一套图片资源,想让他们针对每种密度的设备都设计一套图片资源,并且还是按照我们上面讲的缩放比例规则来设计,就有点想得太开心了。没错,这个就是现实情况,那么在这种情况下,我们应该将仅有的这一套图片资源放在哪个密度的文件夹下呢? 可以这样来分析,根据我们刚才所学的内容,如果将一张图片放在低密度文件夹下,那么在高密度设备上显示图片时就会被自动放大,而如果将一张图片放在高密度文件夹下,那么在低密度设备上显示图片时就会被自动缩小。那我们可以通过成本的方式来评估一下,一张原图片被缩小了之后显示其实并没有什么副作用,但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的。 我们仍然可以通过例子来直观地体会一下,首先将christmas.png图片移动到mipmap-xxhdpi目录下,运行程序后我们通过Profiler来观察程序内存使用情况: image.png 可以看到,程序所占用的内存大概稳定在35.6M左右。然后将christmas.png图片移动到mipmap-mdpi目录下,重新运行程序,结果如下图所示: image.png 现在涨到38M了,占用内存明显增加了。 通过这个例子同时也验证了一个问题,我相信有不少比较有经验的Android程序员可能都遇到过这个情况,就是当你的项目变得越来越大,有的时候加载一张mipmap-hdpi下的图片,程序就直接OOM崩掉了,但如果将这张图放到mipmap-xhdpi或mipmap-xxhdpi下就不会崩掉,其实就是这个道理。 那么经过上面一系列的分析,答案自然也就出来了,图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支,而UI在设计图片的时候也应该尽量面向高密度屏幕的设备来进行设计。就目前来讲,最佳放置图片资源的文件夹就是mipmap-xxhdpi。那么有的朋友可能会问了,不是还有更高密度的mipmap-xxxhdpi吗?干吗不放在这里?这是因为,市面上480dpi到640dpi的设备实在是太少了,如果针对这种级别的屏幕密度来设计图片,图片在不缩放的情况下本身就已经很大了,基本也起不到节省内存开支的作用了。 4人点赞 Android 作者:as_pixar 链接:https://www.jianshu.com/p/7fa3417d2ca4 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

优秀的个人博客,低调大师

python 循环技巧

关于循环的小伎俩 不管是while还是for,所发起的循环,在python编程中是经常被用到的。特别是for,一般认为,它要比while快,而且也容易写(是否容易,可能因人而异,但是,执行时间快,是的确的),因此在实践中,for用的比较多点,不是说while就不用,比如前面所列举而得那个猜数字游戏,在业务逻辑上,用while就更容易理解(当然是限于那个游戏的业务需要而言)。另外,在某些情况下,for也不是简单地把对象中的元素遍历一遍,比如有有隔一个取一个的要求,等等。 在编写代码的实践中,为了对付循环中的某些要求,需要用一些其它的函数,比如前面已经介绍过的range就是一个被看做循环中的计数器的好东西。 range 专门对range()这个内置函数 展示一下它的for循环中,做为计数器的使用。 还记得曾经在教程中有一个问题:[列出100以内被3整除的数] 下面引用那个问题的代码和运行结果。 #! /usr/bin/env python #coding:utf-8 aliquot = [] for n in range(1,100): if n%3 == 0: aliquot.append(n) print aliquot 代码运行结果: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99] 这个问题, 改写一下 >>> aliquot = [ x for x in range(1,100) if x%3==0 ] #用list解析,本质上跟上面无太大差异 >>> aliquot [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99] >>> aliquot = range(3,100,3) #这种方法更简单。这是博客中一网友提供。 >>> aliquot [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99] 如果有一个由字母组成的字符串,只想隔一个从字符串中取一个字母。可以这样来实现,这是range()的一个重要用途。 >>> one = "Ilikepython" >>> new_list = [ one[i] for i in range(0,len(one),2) ] >>> new_list ['I', 'i', 'e', 'y', 'h', 'n'] 当然,间隔的举例,是可以任意指定的。还是前面那个问题,还可以通过下面的方式,选出所有能够被3整除的数。 >>> all_int = range(1,100) >>> all_int [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] >>> aliquot = [ all_int[i] for i in range(len(all_int)) if all_int[i]%3==0 ] >>> aliquot [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99] zip 对zip进行介绍,它也会常常被用到循环之中。 zip是用于并行遍历的函数。 比如有两个list,元素是由整数组成,如果计算对应位置元素的和。一种方法是通过循环,分别从两个list中取出元素,然后求和。 >>> list1 = range(2,10,2) >>> list1 [2, 4, 6, 8] >>> list2 = range(11,20,2) >>> list2 [11, 13, 15, 17, 19] >>> result = [ list1[i]+list2[i] for i in range(len(list1)) ] >>> result [13, 17, 21, 25] zip完成上面的任务,是这么做的: >>> list1 [2, 4, 6, 8] >>> list2 [11, 13, 15, 17, 19] >>> for a,b in zip(list1,list2): ... print a+b, ... 13 17 21 25 zip()的作用就是把list1和list2两个对象中的对应元素放到一个元组(a,b)中,然后对这两个元素进行操作。 >>> list1 [2, 4, 6, 8] >>> list2 [11, 13, 15, 17, 19] >>> zip(list1,list2) [(2, 11), (4, 13), (6, 15), (8, 17)] 对这个功能, 可以理解为,将两个list压缩成为(zip)一个list,只不过找不到配对的就丢掉了。 能够压缩,也能够解压缩,用下面的方式就是反过来了。 >>> result = zip(list1,list2) >>> result [(2, 11), (4, 13), (6, 15), (8, 17)] >>> zip(*result) [(2, 4, 6, 8), (11, 13, 15, 17)] 列位注意观察,解压缩得到的结果,跟前面压缩前的结果相比,第二项就少了一个元素19,因为在压缩的时候就丢掉了。 这似乎跟for没有什么关系呀。别着急,思考一个问题,看看如何求解: 问题描述:有一个dictionary,myinfor = {"name":"hiekay","site":"hiekay.github.io","lang":"python"},将这个字典变换成:infor = {"hiekay":"name","hiekay.github.io":"site","python":"lang"} 解法有几个,如果用for循环,可以这样做 。 >>> infor = {} >>> for k,v in myinfor.items(): ... infor[v]=k ... >>> infor {'python': 'lang', 'hiekay.github.io': 'site', 'hiekay': 'name'} 下面用zip()来试试: >>> dict(zip(myinfor.values(),myinfor.keys())) {'python': 'lang', 'hiekay.github.io': 'site', 'hiekay': 'name'} 原来这个zip()还能这样用。是的,本质上是这么回事情。如果将上面这一行分解开来,就明白其中的奥妙了。 >>> myinfor.values() #得到两个list ['python', 'hiekay', 'hiekay.github.io'] >>> myinfor.keys() ['lang', 'name', 'site'] >>> temp = zip(myinfor.values(),myinfor.keys()) #压缩成一个list,每个元素是一个tuple >>> temp [('python', 'lang'), ('hiekay', 'name'), ('hiekay.github.io', 'site')] >>> dict(temp) #这是函数dict()的功能,将上述列表转化为dictionary {'python': 'lang', 'hiekay.github.io': 'site', 'hiekay': 'name'} 至此,是不是明白zip()和循环的关系了呢?有了它可以让某些循环简化。特别是在用python读取数据库的时候(比如mysql),zip()的作用更会显现。 enumerate 如果要对一个列表,想得到其中每个元素的偏移量(就是那个脚标)和对应的元素,怎么办呢?可以这样: >>> mylist = ["hiekay",703,"python"] >>> new_list = [] >>> for i in range(len(mylist)): ... new_list.append((i,mylist[i])) ... >>> new_list [(0, 'hiekay'), (1, 703), (2, 'python')] enumerate的作用就是简化上述操作:enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。 >>> enumerate(mylist) <enumerate object at 0xb74a63c4> #出现这个结果,用list就能显示内容.类似的会在后面课程出现,意味着可迭代。 >>> list(enumerate(mylist)) [(0, 'hiekay'), (1, 703), (2, 'python')]

优秀的个人博客,低调大师

python 调试技巧

文章说明 编程过程中,出现BUG不可避免,所以经常要进行调试. 在廖雪峰的python3教程中,有一篇专门讲调试的文章,简单列举了调试的几种方法,建议先进行阅读,了解一下。 为更方便新手学习,我结合自身开发情况,进一步补充一下。 一.print大法 如果不是确定要记录到日志,一般用的最多的还是print。 print()内置函数,一般最初学习python的时候,就接触到了,这里再列举一下几种调试常用的print用法。 1.直接打印数字 print(111111111) 2.直接打印文字 print("进来了") 3.直接打印变量 print(name) 4.打印文字+变量 print("进来了"+name) 5.打印文字+变量 print("进来了" , name) 6.打印文字+变量 print("进来了,名字是%s,年龄是%d" % (name , age)) 二.IDE断点调试 如图所示,为PyCharm的调试。 (注:图为网页分享,并非原创。找不到原图链接了,抱歉。) 三.终极武器logging python有一个标准库logging,用来管理日志。 通过整个标准库,可以进一步构建自己的日志模块。 如下是一个自定义的日志模板。 (注:整个模块是网友分享,并非原创。找不到原文链接了,抱歉。) import time import logging from logging.handlers import TimedRotatingFileHandler #################################### #日志级别大小关系为:CRITICAL > ERROR > WARNING > INFO > DEBUG > NOTSET,当然也可以自己定义日志级别。 print_level = logging.DEBUG #打印级别控制 file_path = "D:/TestDemo/" #定义日志存放路径 file_name = time.strftime('%Y%m%d', time.localtime(time.time())) #文件名称 #################################### class Log(): # 构造函数 def __init__(self, name): self.filename = file_path + file_name + '.log' # 日志文件名称 self.name = name # 为%(name)s赋值 #创建日志器 self.logger = logging.getLogger(self.name) # 控制记录级别 self.logger.setLevel(print_level) # 控制台日志 self.console_handler = logging.StreamHandler() console_format = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s[line:%(lineno)d] - %(message)s') self.console_handler.setFormatter(console_format) # 文件日志 self.filetime_handler = TimedRotatingFileHandler(self.filename, 'D', 1, 30) #保留30天,1月保存一个文件 file_format = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s[line:%(lineno)d] - %(message)s') self.filetime_handler.setFormatter(file_format) # 为logger添加的日志处理器 self.logger.addHandler(self.console_handler) self.logger.addHandler(self.filetime_handler) #测试 def debug(self, msg): self.logger.debug(msg) #信息 def info(self, msg): self.logger.info(msg) #警告 def warning(self, msg): self.logger.warning(msg) #错误 def error(self, msg): self.logger.error(msg) #重大错误 def critical(self, msg): self.logger.critical(msg) #抛出异常 def exception(self, msg): self.logger.exception(msg) #关闭控制台日志 def close_console(self): self.logger.removeHandler(self.console_handler) # 关闭文件日志 def close_filetime(self): self.logger.removeHandler(self.filetime_handler) 本文如有帮助,敬请留言鼓励。 本文如有错误,敬请留言改进。

优秀的个人博客,低调大师

Python小技巧

One For All!!! 导包 取别名 from math as foobar from module1 import open as open1 from module2 import open as open2 查找Python的模块位置 >>> import sys,pprint >>> pprint.pprint(sys.path) 导入自定义的模块 import sys sys.path.append('模块绝对路径') 赋值魔法(序列解包) >>> x,y,z = 1,2,3 >>> print x,y,z 1 2 3 >>> x,y = y,x >>> print x,y,z 2 1 3 >>> values = 1,2,3 >>> values (1, 2, 3) >>> x,y,z = values >>> x 1 >>> y 2 >>> z 3 >>> data = {'name':'zdf','name1':'zdf1'} >>> data {'name': 'zdf', 'name1': 'zdf1'} >>> key,value = data.popitem() >>> key 'name' >>> value 'zdf' >>> 第一次导入模块时会执行模块里的程序,这可能不是你所想要的,所以你必须告知模块本身是作为程序运行还是导入模块,可以通过 __ name__变量实现这一点 def hello(): print('Hello,world!') print('Hello,world!') def test(): hello() if __name__ == '__main__' : test() 当作为程序运行时,__ name__等于__ main__,当作为导入模块时,__ name__等于模块名称 运行模块程序的方法 python -m '模块名称' args

优秀的个人博客,低调大师

并发技巧清单

如何尽量保证线程安全 可变状态是至关重要的。 所有并发访问都可以归结为如何协调对并发状态的访问,可变状态越少,越容易确保线程安全性。 尽量将域声明为final类型,除非需要它们是可变的。 不可变对象一定是线程安全的。 不可变对象能极大地降低并发编程的复杂性。它们更为简单且安全,可以任意共享而无须使用加锁或保护性复制等机制。 封装有助于管理复杂性。 在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量,但为什么要这样做? 将数据封装在对象中,更易于维护不变性条件:将同步机制封装在对象中,更易于遵循同步策略。 用锁保护每个可变变量。 当保护同一个不变性条件中的所有变量时,要使用同一个锁。 在执行复合操作期间,要持有锁。 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会可能出问题。 不要故作聪明地推断不需要使用同步。 在设计过程中考虑线程安全,或者在文档中明确地指出塔不是线程安全的。 将同步策略文档化。 使用ThreadLocal保存状态变量 避免死锁 1.对于资源的加锁时间必须足够短,也就是必要时进行锁 2.访问资源过程中的锁需要按照一致的顺序进行获取,否则需要提升出一个更大的锁来确保资源的获取 3.尽量通过封装的形式,避免将锁暴露给外部,从而造成不必要的资源死锁 4.多数情况下,死锁是由于获取锁的顺序错误锁导致的。 5.避免一个线程同时获取多个锁。 6.避免一个线程在锁内同时占用多个资源,尽量保持一个锁只占用一个资源。 7.尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。 8. 原子性 原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为"不可被中断的一个或一系列操作" 。 在多处理器上实现原子操作就变得有点复杂。 对象类型 对象地址原子读写,线程安全。 并发读不可变状态,线程安全。 并发读写可变状态,非线程安全。 基本类型 int,char数值读写,线程安全。 long,double高低位,非线程安全。 i++ 等组合操作,非线程安全。 可见性 final 初始化final字段确保可见性 volatile 读写volatile字段确保可见性 synchronized 同步块内读写字段确保可见性 happen before 遵守happen before次序可见性 可排序性 Happen Before法则 程序次序法则 如果A一定在B之前发生,happen before 监视器法则 对一个监视器的解锁一定发生在后续对同一个监视器加锁之前 Volatile变量法则 写volatile变量一定发生在后续对它读之前 线程启动法则 Thread.start一定发生在线程中的动作之前 线程终结法则 线程中的任何动作一定发生在以下操作的动作之前。 其它线程检测到这个线程已经终止,从Thread.join调用成功返回,Thread.isAlive()返回false 中断法则 一个线程调用另一个线程的interrupt一定发生在另一线程发现中断之前 终结法则 一个对象的构造函数结束一定发生在对象的finalizer之前 传递性 A发生在B之前,B发生在C之前,A一定发生在C之前 如果多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 什么是线程安全性 在线程安全性的定义中,最核心的概念就是正确性。 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调用代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。 无状态对象一定是线程安全的。 熟练使用线程安全类 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。 并非所有的数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。 对非原子的64位操作,如long,double类型的变量,在多线程下存在这样的共享变量时,请把变量定义成volatile 加锁机制既可以确保可见性又可以确保原子性 ,volatile变量只可以确保可见性。 当访问共享的可变数据时,通常需要同步,一种避免使用同步方式就是不共享数据。 满足不可变对象的条件: 对象创建以后其状态不能修改 对象的所有域都是final类型 对象是正确创建的(在对象创建的期间,this引用没有逸出) final域能确保初始化过程中的安全性 不变模式 在并行程序开发过程中,同步操作似乎是必不可少的。当多线程对同一个对象进行读写操作时,为了保证数据对象对一致性 和正确性,有必要对对象使用同步。故而同步操作对性能有相当对消耗。为了尽可能的去除这些同步操作,提高并行程序的 性能,可以使用一种不可变的对象,依靠对象的不变性,可以确保其在没有使用同步操作的多线程环境下依然始终保持 内部状态的一致性和正确性。 不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,则它的内部状态将永远不会发生改变。所以,没有 一个线程可以修改其内部状态和数据。 不变模式使用场景介绍 当对象被创建后,其内部状态和数据不再发生任何变化 对象需要被共享、被多线程频繁访问 JAVA的并发控制方式 内部锁 重入锁 读写锁 ThreadLocal变量 信号量 隐藏比较深的数据竞争 竞争情况描述 线程T1 线程T2 编译器将这个表达式扩展成temp=x ,和 x=temp+1 x+=1 x+=2 下标i和j相同时可能会出现数据竞争 a[i]+=1 a[j]+=1 指针q和p指向同一个目标时可能会出现数据竞争 *q+=2 *p+=1 foo函数可能使用参数对一个共享变量进行修改 foo(1) foo(2) 即使在指令集,硬件还是会将【edi】对更新操作扩展成独立对对读操作和写操作改 add [edi],1 add [edi] ,2 欢迎工作一到五年的Java工程师朋友们加入Java架构开发:468947140 点击链接加入群聊【Java-BATJ企业级资深架构】:https://jq.qq.com/?_wv=1027&k=5zMN6JB 本群提供免费的学习指导 架构资料 以及免费的解答 不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

优秀的个人博客,低调大师

Docker 调试技巧

『重用』容器名 但我们在编写/调试Dockerfile的时候我们经常会重复之前的command,比如这种docker run --name jstorm-zookeeper zookeeper:3.4,然后就容器名就冲突了。 1 2 3 4 $ docker run --name jstorm-zookeeper zookeeper:3.4 ... $ docker run --name jstorm-zookeeper zookeeper:3.4 docker: Error response from daemon: Conflict. The name "/jstorm-zookeeper" is already in use by container xxxxxxxxx 可以在运行docker run时候加上--rmflag, 容器将在退出之后销毁。无需手动docker rm CONTAINER 1 2 3 4 5 6 7 $ docker run --name jstorm-zookeeper zookeeper:3.4 --rm # reuse $ docker create --name jstorm-zookeeper zookeeper:3.4 $ docker start jstorm-zookeeper # no error debug Dockerfile 在写Dockerfile的时候,通常并不会一气呵成。有的时候容器启动就crash 直接退出,有的时候build image 就会失败,或者想验证Dockerfile是否符合预期,我们经常要debug Dockerfile。 如果build 失败可以直接 查看stdout的错误信息,拆分指令,重新build。 logs查看 stdout 所有容器内写到stdout的内容都会被捕获到host中的一个history文件中, 可以通过docker logs CONTAINER查看。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ docker run -d --name jstorm-zookeeper zookeeper:3.4 $ docker logs jstorm-zookeeper ZooKeeper JMX enabled by default Using config: /conf/zoo.cfg 2016-12-18 05:55:27,717 [myid:] - INFO [main:QuorumPeerConfig@124] - Reading configuration from: /conf/zoo.cfg 2016-12-18 05:55:27,725 [myid:] - INFO [main:DatadirCleanupManager@78] - autopurge.snapRetainCount set to 3 2016-12-18 05:55:27,725 [myid:] - INFO [main:DatadirCleanupManager@79] - autopurge.purgeInterval set to 0 2016-12-18 05:55:27,726 [myid:] - INFO [main:DatadirCleanupManager@101] - Purge task is not scheduled. 2016-12-18 05:55:27,728 [myid:] - WARN [main:QuorumPeerMain@113] - Either no config or no quorum defined in config, running in standalone mode 2016-12-18 05:55:27,746 [myid:] - INFO [main:QuorumPeerConfig@124] - Reading configuration from: /conf/zoo.cfg 2016-12-18 05:55:27,747 [myid:] - INFO [main:ZooKeeperServerMain@96] - Starting server 2016-12-18 05:55:27,766 [myid:] - INFO [main:Environment@100] - Server environment:zookeeper.version=3.4.9-1757313, built on 08/23/2016 06:50 GMT 2016-12-18 05:55:27,766 [myid:] - INFO [main:Environment@100] - Server environment:host.name=dbc742dd5688 2016-12-18 05:55:27,767 [myid:] - INFO [main:Environment@100] - Server environment:java.version=1.8.0_111-internal 即使是容器已经退出的也可以看到,所以可以通过这种方式来分析非预期的退出。这些文件一直保存着,直到通过docker rm把容器删除。文件的具体路径可以通过docker inspect CONTAINER获得。 (然后osx上你并找不到这些文件,因为其实osx的docker实际是运行在”VM”中,具体就不展开了,但是可以通过screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/ttytouch上”VM”的tty) 在使用docker logs的时候加一些参数来过滤log,默认输出所有log。 1 2 3 4 5 6 7 Options: --details Show extra details provided to logs -f, --follow Follow log output --help Print usage --since string Show logs since timestamp --tail string Number of lines to show from the end of the logs (default "all") -t, --timestamps Show timestamps attach实时查看stdout 如果你想实时查看容器的输出你可以用docker attach CONTAINER命令。 默认会绑定stdin,代理signals, 所以如果你ctrl-c容器通常会退出。很多时候大家并不想这样,只是想分离开,可以ctrl-p ctrl-q。 执行任意command 可以通过docker exec CONTAINER COMMAND,来在容器内执行任意 command,比如 cat 一些东西来debug。 1 2 3 4 5 6 $ docker run -d --name jstorm-zookeeper zookeeper:3.4 $ docker exec jstorm-zookeeper java -version openjdk version "1.8.0_111-internal" OpenJDK Runtime Environment (build 1.8.0_111-internal-alpine-r0-b14) OpenJDK 64-Bit Server VM (build 25.111-b14, mixed mode) 也可以直接通过 exec 在容器内启动一个 shell 更方便地调试容器,不必一条条执行docker exec。 1 2 3 $ docker exec -it jstorm-zookeeper /bin/bash bash-4.3# pwd /zookeeper-3.4.9 docker exec只能在正在运行的容器上使用,如果已经停止了退出了就不行了,就只好用docker logs了。 重写entrypoint和cmd 每个Docker镜像都有entrypoint和cmd, 可以定义在Dockerfile中,也可以在运行时指定。这两个概念很容易混淆,而且它们的试用方式也不同。 entrypoint比cmd更”高级”,entrypoint作为容器中pid为1的进程运行(docker不是虚拟机,只是隔离的进程。真正的linux中pid为1的是init)。cmd只是entrypoint的参数: 1 <ENTRYPOINT> "<CMD>" 当我们没有指定entrypoint时缺省为/bin/sh -c。所以其实entrypoint是真正表达这个docker应该干什么的,通常大家有一个shell 脚本来代理。entrypoint和cmd都可以在运行的时候更改,通过更改来看这样设置entrypoint是否优雅合理。 1 2 3 4 5 6 7 8 $ docker run -it --name jstorm-zookeeper --entrypoint /bin/bash zookeeper:3.4 bash-4.3# top Mem: 320212K used, 1725368K free, 89112K shrd, 35532K buff, 130532K cached CPU: 0% usr 0% sys 0% nic 100% idle 0% io 0% irq 0% sirq Load average: 0.20 0.06 0.02 5/195 7 PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND 1 0 root S 6220 0% 0 0% /bin/bash 7 1 root R 1516 0% 2 0% top 任何docker run命令中在image名后的内容都作为cmd的内容传给entrypoint当参数。 暂停容器 使用docker pause可以暂停容器中所有进程。这非常有用。 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 $ docker run -d --name jstorm-zookeeper zookeeper:3.4 && sleep 0.1 && docker pause jstorm-zookeeper && docker logs jstorm-zookeeper a24405a53ddd9b7d94d9e77fe2b5a67639a251d681aa2f34fcb0cc96f347ba48 jstorm-zookeeper ZooKeeper JMX enabled by default Using config: /conf/zoo.cfg 2016-12-18 16:17:47,720 [myid:] - INFO [main:QuorumPeerConfig@124] - Reading configuration from: /conf/zoo.cfg 2016-12-18 16:17:47,730 [myid:] - INFO [main:DatadirCleanupManager@78] - autopurge.snapRetainCount set to 3 2016-12-18 16:17:47,730 [myid:] - INFO [main:DatadirCleanupManager@79] - autopurge.purgeInterval set to 0 2016-12-18 16:17:47,730 [myid:] - INFO [main:DatadirCleanupManager@101] - Purge task is not scheduled. 2016-12-18 16:17:47,731 [myid:] - WARN [main:QuorumPeerMain@113] - Either no config or no quorum defined in config, running in standalone mode 2016-12-18 16:17:47,757 [myid:] - INFO [main:QuorumPeerConfig@124] - Reading configuration from: /conf/zoo.cfg 2016-12-18 16:17:47,757 [myid:] - INFO [main:ZooKeeperServerMain@96] - Starting server $ docker unpause jstorm-zookeeper && docker logs jstorm-zookeeper jstorm-zookeeper ZooKeeper JMX enabled by default Using config: /conf/zoo.cfg 2016-12-18 16:17:47,720 [myid:] - INFO [main:QuorumPeerConfig@124] - Reading configuration from: /conf/zoo.cfg 2016-12-18 16:17:47,730 [myid:] - INFO [main:DatadirCleanupManager@78] - autopurge.snapRetainCount set to 3 2016-12-18 16:17:47,730 [myid:] - INFO [main:DatadirCleanupManager@79] - autopurge.purgeInterval set to 0 2016-12-18 16:17:47,730 [myid:] - INFO [main:DatadirCleanupManager@101] - Purge task is not scheduled. 2016-12-18 16:17:47,731 [myid:] - WARN [main:QuorumPeerMain@113] - Either no config or no quorum defined in config, running in standalone mode 2016-12-18 16:17:47,757 [myid:] - INFO [main:QuorumPeerConfig@124] - Reading configuration from: /conf/zoo.cfg 2016-12-18 16:17:47,757 [myid:] - INFO [main:ZooKeeperServerMain@96] - Starting server 2016-12-18 16:18:09,039 [myid:] - INFO [main:Environment@100] - Server environment:zookeeper.version=3.4.9-1757313, built on 08/23/2016 06:50 GMT 2016-12-18 16:18:09,040 [myid:] - INFO [main:Environment@100] - Server environment:host.name=a24405a53ddd 2016-12-18 16:18:09,040 [myid:] - INFO [main:Environment@100] - Server environment:java.version=1.8.0_111-internal 2016-12-18 16:18:09,040 [myid:] - INFO [main:Environment@100] - Server environment:java.vendor=Oracle Corporation 2016-12-18 16:18:09,040 [myid:] - INFO [main:Environment@100] - Server environment:java.home=/usr/lib/jvm/java-1.8-openjdk/jre 2016-12-18 16:18:09,040 [myid:] - INFO [main:Environment@100] - Server environment:java.class.path=/zookeeper-3.4.9/bin/../build/classes:/zookeeper-3.4.9/bin/../build/lib/*.jar:/zookeeper-3.4.9/bin/../lib/slf4j-log4j12-1.6.1.jar:/zookeeper-3.4.9/bin/../lib/slf4j-api-1.6.1.jar:/zookeeper-3.4.9/bin/../lib/netty-3.10.5.Final.jar:/zookeeper-3.4.9/bin/../lib/log4j-1.2.16.jar:/zookeeper-3.4.9/bin/../lib/jline-0.9.94.jar:/zookeeper-3.4.9/bin/../zookeeper-3.4.9.jar:/zookeeper-3.4.9/bin/../src/java/lib/*.jar:/conf: 2016-12-18 16:18:09,040 [myid:] - INFO [main:Environment@100] - Server environment:java.library.path=/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib 2016-12-18 16:18:09,041 [myid:] - INFO [main:Environment@100] - Server environment:java.io.tmpdir=/tmp 2016-12-18 16:18:09,041 [myid:] - INFO [main:Environment@100] - Server environment:java.compiler=<NA> 2016-12-18 16:18:09,043 [myid:] - INFO [main:Environment@100] - Server environment:os.name=Linux 2016-12-18 16:18:09,043 [myid:] - INFO [main:Environment@100] - Server environment:os.arch=amd64 2016-12-18 16:18:09,044 [myid:] - INFO [main:Environment@100] - Server environment:os.version=4.4.27-moby 2016-12-18 16:18:09,044 [myid:] - INFO [main:Environment@100] - Server environment:user.name=zookeeper 2016-12-18 16:18:09,044 [myid:] - INFO [main:Environment@100] - Server environment:user.home=/home/zookeeper 2016-12-18 16:18:09,044 [myid:] - INFO [main:Environment@100] - Server environment:user.dir=/zookeeper-3.4.9 2016-12-18 16:18:09,057 [myid:] - INFO [main:ZooKeeperServer@815] - tickTime set to 2000 2016-12-18 16:18:09,057 [myid:] - INFO [main:ZooKeeperServer@824] - minSessionTimeout set to -1 2016-12-18 16:18:09,058 [myid:] - INFO [main:ZooKeeperServer@833] - maxSessionTimeout set to -1 2016-12-18 16:18:09,076 [myid:] - INFO [main:NIOServerCnxnFactory@89] - binding to port 0.0.0.0/0.0.0.0:2181 top和stats获得容器中进程的状态 docker top CONTAINER和在容器里执行top的效果类似。 1 2 3 4 5 6 7 $ docker top jstorm-zookeeper PID USER TIME COMMAND 24593 dockrema 0:01 /usr/lib/jvm/java-1.8-openjdk/jre/bin/java -Dzookeeper.log.dir=. ..... $ docker stats jstorm-zookeeper CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS jstorm-zookeeper 0.00% 24.86 MiB / 1.951 GiB 1.24% 648 B / 648 B 0 B / 0 B 20 通过inspect查看容器的详细信息 docker inspect CONTAINER饭后镜像和容器的详细信息。比如: State —— 容器的当先状态 LogPath —— history(stdout) file 的路径 Config.Env —— 环境变量 NetworkSettings.Ports —— 端口的映射关系 环境变量非常有用很多问题都是环境变量引起的。 history查看 image layers 可以看到各层创建的指令,大小和哈希。可以用来检查这个image是否符合你的预期。

优秀的个人博客,低调大师

Gradle常用技巧

转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN Gradle http://google.github.io/android-gradle-dsl/current/index.html https://docs.gradle.org/current/userguide/java_plugin.html 打包多个版本 开发过程中我们经常需要打包多个版本的apk,最为常见的,一个是release版本,一个是debug版本,他们可能使用的api也有所区别,手动改起来总是很麻烦。 我们可以通过Gradle,配置多个版本,他们有各自的参数来区分不同的版本。如下,在 app/build.gradle 系统默认会给我生成release版本,我们可以手动自己添加一个版本,我这里命名为debug,分别添加了三种类型的参数。 apply plugin: 'com.android.application' android { …… buildTypes { release { …… buildConfigField("boolean", "isDebug", "false") } debug { // 添加了boolean类型的参数 buildConfigField("boolean", "isDebug", "true") // 添加了String类型的参数 buildConfigField("String", "coder", "\"kongqw\"") // 添加了int类型的参数 buildConfigField("int", "age", "26") } } } …… dependencies { …… } 添加完成后Rebuild,会在 BuildConfig 下看到我们添加的参数 因为是静态变量,取值时直接用类名点变量名即可 上述属于在Java代码中添加字段,同样的,Gradle也支持添加xml属性,类似这样 apply plugin: 'com.android.application' android { …… defaultConfig { …… } buildTypes { release { …… } debug { …… resValue("bool", "is_debug", "true") resValue("string", "coder", "\"kongqw\"") resValue("integer", "age", "26") } } } dependencies { …… } 添加完以后Rebuild,会在generated.xml 下生产如下字段 但是要避免和string.xml文件里的字段重复 在xml中使用 android:text="@string/coder" 或者再Java中使用 String coder = getString(R.string.coder); 添加工程build时间 有时候,测试和产品总是会拿着手机跑过来问你,“有没有更新?”、“帮我看一下我装的是不是最新的版本?”,总是很烦,我们可以利用Gradle,获取到工程的Build时间,在测试版本打印出来,可以为工程师节省不少时间。 Gradle支持直接添加方法 apply plugin: 'com.android.application' android { …… defaultConfig { …… resValue("string", "build_time", getDate()) // BuildTime buildConfigField("String", "buildTime", "\"" + getDate() + "\"") } buildTypes { release { …… } debug { …… } } } def getDate() { return new Date().format("yyyy-MM-dd HH:mm:ss") } dependencies { …… } 判断的时候调用 BuildConfig.buildTime 或者 getString(R.string.build_time) 不同版本包名不同 我们都知道,相同包名,不同签名,在一台手机上是无法同时安装的。前面我们配置了两个不同的版本,release版本肯定是用正式签名,debug版本我们通常都会使用测试签名。那么问题来了,测试人员如果已经在手机上安装了上线的版本,再装测试版,就会有冲突,必须要卸载一个,那么最好的办法就是修改测试版包名。这样包名不同,两个版本同时安装,也不会有冲突。 同样的,Gradle依然可以轻松的帮我们做到。 apply plugin: 'com.android.application' android { …… defaultConfig { …… } buildTypes { release { …… } debug { …… applicationIdSuffix ".debug" } } } dependencies { …… } 可以看到我们在 debug 版本添加了applicationIdSuffix,其值为 .debug,顾名思义,是debug版本在包名后面添加.debug后缀。 多渠道打包 多渠道打包,打包多个市场的apk,用来统计。 首先在AndroidManifest文件添加meta-data <?xml version="1.0" encoding="utf-8"?> <manifest ……> <application ……> <meta-data android:name="PRODUCT" android:value="${CHANNEL_VALUE}"/> <activity ……> …… </activity> </application> </manifest> 在app/build.gradle添加productFlavors,如下所示,添加 XIAO_MI和GOOGLE_PLAY两个渠道 apply plugin: 'com.android.application' android { …… defaultConfig { …… } buildTypes { release { …… } debug { …… } } productFlavors { xiaomi { manifestPlaceholders = [CHANNEL_VALUE: "XIAO_MI"] } googlePlay { manifestPlaceholders = [CHANNEL_VALUE: "GOOGLE_PLAY"] } } } dependencies { …… } 获取渠道 try { ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); String channel = applicationInfo.metaData.getString("PRODUCT"); Log.i(TAG, "onCreate: 渠道 :" + channel); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); }

优秀的个人博客,低调大师

MapReduce开发技巧

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq1010885678/article/details/50922796 数据类型的选择 自定义数据类型 参考:Hadoop提交作业自定义排序和分组 MapWritable/SortedMapWritable Hadoop中可传输的Map集合,和Java中的Map用法差不多,但是可以用与mapper和reducer之间的数据传输 Map输出不同类型的Value 使用自定义的数据类型继承自GenericWritable可以实现在mapper中输出多个不同类型的value //使用这个数据类型将可以输出IntWritable和Text两种类型的value public class MultiValueWritable extends GenericWritable{ private static Class[] CLASSES = new Class{ IntWritable.class, Text.class } public MultiValueWritable(){ } public MultiValueWritable(Writable value){ set(value); } protected Class[] getTypes(){ return CLASSES; } } mapper中context.write的时候可以使用如下的格式: context.write(key,new MultiValueWritable(new Text("1"))); context.write(key,new MultiValueWritable(IntWritable Text(1))); reducer的Values迭代器中可以通过这种方式来判断value是那种数据类型: Writable value = value.get(); if(value instanceof Text){ ... } 选择合适的InputFormat/OutputFormat 基本上每个InputFormat都会有一个对应的OutputFormat TextInputFormat 默认的输入格式,按行读取,key为每行偏移量,value为行的内容 NLineInputFormat 可以指定一次数据文件多少行的内容: //设置一次读取50行的内容 NLineInputFormat.setNumLinesPerSplit(job,50); SequenceFileInputFormat 输入的格式为keylen,key,valuelen,value,适合用于多个job之间的数据连接 DBInputFormat 处理数据库输入,待使用测试 自定义的InputFormat 参考:自定义分片策略解决大量小文件问题 同时处理不同类型的输入 参考:多个Mapper和Reducer处理多个输入 Partitioner的选择 TotalOrderPartitioner 对所有reducer中的结果进行排序,默认情况下每个reducer中的内容都是各自排序互不影响的 自定义partitioner 参考:Hadoop作业中自定义分区和归约 KeyFieldBasedPartitioner 在分区的时候mapper的key部分会参与计算 配合参数 map.output.key.field.separator num.key.fields.for.partition 指定分隔符和要参与分区的字符索引 例如:key=”name-price”,指定map.output.key.field.separator=”-“,num.key.fields.for.partition=1表示key的price部分参与分区计算 二次排序 setSortComparatorClass map中每个分区调用进行排序,reduce中shuffle之后再次调用 setGroupingComparatorClass 第二次排序,属于同一组的顺序记录并放入同一个value迭代器 分布式缓存的使用 参考:MapReduce中的DistributedCache 作者:@小黑

优秀的个人博客,低调大师

Go语言小技巧

1 反射 val := reflect.ValueOf(v) kind := val.Kind() if kind == reflect.String { pValue := val.String() } else if kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 { pValueInt := val.Int() pValue := strconv.FormatInt(pValueInt, 10) } else if kind == reflect.Float32 || kind == reflect.Float64 { pValueFloat := val.Float() pValue := strconv.FormatFloat(pValueFloat, 'f', -1, 64) } else if kind == reflect.Slice { vSlice, ok := v.([]interface{}) if ok { } } else if kind == reflect.Map { vMap, ok := v.(map[string]interface{}) if ok { } }

优秀的个人博客,低调大师

Gradle 使用技巧(一)

前言 Gradle是使用Groovy语言构建,所以既然是语言,那么就有他的语法,而不是简单的配置项。关于Gradle有时间会出一系列的文章。 1. 自定义apk输出名称 (3.0) 本配置为Android studio3.0及以上的配置,3.0以下版本适用,下面的配置会将所有的Variants都输出。 // 便利所有的Variants,all是迭代遍历操作符,相当于for applicationVariants.all { variant ->// 遍历得出所有的variant variant.outputs.all {// 遍历所有的输出类型,一般是debug和replease // 定义apk的名字,拼接variant的版本号 def apkName = "app_${variant.versionName}" // 判断是否为空 if (!variant.flavorName.isEmpty()) { apkName += "_${variant.flavorName}" } // 赋值属性 outputFileName = apkName + ".apk" } } 配置到buildTypes的release和debug下面 Gradle配置输出APK名称.png 2. 配置签名 我们在使用百度地图和微信相关的API的时候,是需要我们填入相关的的app签名进行校验后才能进行操作,而默认我们在调试模式下使用的是AS为我们提供的android.keystore,所幸,Gradle为我们提供了相关的方法,我们可以为debug和release指定密钥文件和前面。我们需要先在app.gradle的android节点做做相关的配置。 2.1 配置签名信息 android{ ... signingConfigs { debug { try { storeFile file("../签名文件.jks") storePassword "密码" keyAlias "别名" keyPassword "密码" v1SigningEnabled true v2SigningEnabled false } catch (ex) { ex.printStackTrace() throw new InvalidUserDataException("秘钥用户名或密码错误${ex.getMessage()}") } } release { try { storeFile file("../签名文件.jks") storePassword "密码" keyAlias "别名" keyPassword "密码" v1SigningEnabled true v2SigningEnabled false } catch (ex) { throw new InvalidUserDataException("秘钥用户名或密码错误") } } } ... } signingConfigs 是处于android节点中 这里需要注意的是storeFile的路径是指当前app.gradle的相对路径。 关于v1SigningEnabled和v2SigningEnabled APK Signature Scheme v2是Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。一般情况下都会选择关闭。 2.2 在相应的buildTypes中指定签名 buildTypes{ release{ signingConfig signingConfigs.release ... } debug{ signingConfig signingConfigs.debug ... } } 虽然不指定这一步,我们依然能够正常运行,但是为了出不必要的幺蛾子,我们还是把它加上吧。 3. release和debug设置全局变量 3.1 全局debug开关 写程序就会要有log,有log我们就需要在打release包的时候就需要手动的设置debug开关为false,但是在某些情况下我们可能会忘记关闭的。所以可以选择在gradle中通过buildConfigField定义变量,然后再需要的时候直接进行引用。 buildTypes { release { ... buildConfigField "boolean", "isDebug", "false" ... } debug { ... buildConfigField "boolean", "isDebug", "true" ... } } 以上,我们在两种模式中都创建了一个叫做isDebug的布尔值的变量,根据类型不同设置值,然后需要重新编译一下。 编译完成后直接调用BuildConfig.isDebug作为开关设置即可 Debug开关.png 关于BuildConfig的内容,可以看下面的截图 BuildCofig信息.png 3.2 API地址的配置 既然有了全局debug开关的设置,那么就肯定有这么一个需求:debug和release下使用不同的API请求地址,其实也就是和前面一个套路,直接通过buildConfigField定义变量。 buildTypes { release { ... buildConfigField "boolean", "isDebug", "false" buildConfigField "String", "API", "http://192.168.1.1/release" ... } debug { ... buildConfigField "boolean", "isDebug", "true" buildConfigField "String", "API", "\"http://192.168.1.1/debug\"" ... } } String类型一定要记得加双引用转义。 String类型一定要记得加双引用转义。 String类型一定要记得加双引用转义。 配置API地址.png 后续敬请期待! 免为其难的关注一下公众号吧!! 生活实在是太苦啦 FullScreenDeveloper

优秀的个人博客,低调大师

Python快速编程技巧

参与文末每日话题讨论,赠送异步新书 异步图书君 而在本文中,我们将看到更多特殊的例子,以便让你更加熟悉Scrapy的两个最重要的类——Request和Response。 1.1 需要登录的爬虫 通常情况下,你会发现自己想要抽取数据的网站存在登录机制。大部分情况下,网站会要求你提供用户名和密码用于登录。你可以从http://web:9312/dynamic(从dev机器访问)或http://localhost:9312/ dynamic(从宿主机浏览器访问)找到我们要使用的例子。如果使用"user"作为用户名,"pass"作为密码的话,你就可以访问到包含3个房产页面链接的网页。不过现在的问题是,要如何使用Scrapy执行相同的操作? 让我们使用Google Chrome浏览器的开发者工具来尝试理解登录的工作过程(见图1.1)。首先,打开Network选项卡(1)。然后,填写用户名和密码,并单击Login(2)。如果用户名和密码正确,你将会看到包含3个链接的页面。如果用户名和密码不匹配,将会看到一个错误页。 图1.1 登录网站时的请求和响应 当按下Login按钮时,会在Google Chrome浏览器开发者工具的Network选项卡中看到一个包含Request Method: POST的请求,其目的地址为http://localhost:9312/dynamic/login。 当你单击该请求时(3),可以看到发送给服务端的数据,包括Form Data(4),其中包含了我们输入的用户名和密码。这些数据都是以文本形式传输给服务端的。Chrome浏览器只是将其组织起来,向我们更好地显示这些数据。服务端的响应是302 Found(5),使我们跳转到一个新的页面:/dynamic/gated。该页面只有在登录成功后才会出现。如果尝试直接访问http://localhost:9312/dynamic/gated,而不输入正确的用户名和密码的话,服务端会发现你在作弊,并跳转到错误页,其地址是http:// localhost:9312/dynamic/error。服务端是如何知道你和你的密码的呢?如果你单击开发者工具左侧的gated(6),就会发现在Request Headers区域下面(7)设置了一个Cookie值(8)。 总之,即使是一个单一的操作,比如登录,也可能涉及包括POST请求和HTTP跳转的多次服务端往返。Scrapy能够自动处理大部分操作,而我们需要编写的代码也很简单。 我们从第3章中名为easy的爬虫开始,创建一个新的爬虫,命名为login,保留原有文件,并修改爬虫中的name属性(如下所示): classLoginSpider(CrawlSpider): name = 'login' 我们需要通过执行到http://localhost:9312/dynamic/login的POST请求,发送登录的初始请求。这将通过Scrapy的FormRequest类实现该功能。要想使用该类,首先需要引入如下模块。 from scrapy.httpimport FormRequest 然后,将start_urls语句替换为start_requests()方法。这样做是因为在本例中,我们需要从一些更加定制化的请求开始,而不仅仅是几个URL。更确切地说就是,我们从该函数中创建并返回一个FormRequest。 # Start with a login requestdefstart_requests(self): return [ FormRequest( "http://web:9312/dynamic/login", formdata={"user":"user","pass": "pass"} )] 虽然听起来不可思议,但是CrawlSpider(LoginSpider的基类)默认的parse()方法确实处理了Response,并且仍然能够使用第3章中的Rule和LinkExtractor。我们只编写了非常少的额外代码,这是因为Scrapy为我们透明处理了Cookie,并且一旦我们登录成功,就会在后续的请求中传输这些Cookie,就和浏览器执行的方式一样。接下来可以像平常一样,使用scrapy crwal运行。 <strong>$ scrapy crawl login</strong><strong>INFO: Scrapy 1.0.3 started (bot: properties)</strong><strong>...</strong><strong>DEBUG: Redirecting (302) to &lt;GET .../gated> from &lt;POST .../login ></strong><strong>DEBUG: Crawled (200) &lt;GET .../data.php></strong><strong>DEBUG: Crawled (200) &lt;GET .../property_000001.html> (referer: .../data.</strong><strong>php)</strong><strong>DEBUG: Scraped from &lt;200 .../property_000001.html></strong><strong>{'address': [u'Plaistow, London'],</strong><strong> 'date': [datetime.datetime(2015, 11, 25, 12, 7, 27, 120119)],</strong><strong> 'description': [u'features'],</strong><strong> 'image_urls': [u'http://web:9312/images/i02.jpg'],</strong><strong>...</strong><strong>INFO: Closing spider (finished)</strong><strong>INFO: Dumping Scrapy stats:</strong><strong> {...</strong><strong> 'downloader/request_method_count/GET': 4,</strong><strong> 'downloader/request_method_count/POST': 1,</strong><strong>...</strong><strong> 'item_scraped_count': 3,</strong> 我们可以在日志中看到从dynamic/login到dynamic/gated的跳转,然后就会像平时那样抓取Item了。在统计中,可以看到1个POST请求和4个GET请求(一个是前往dynamic/gated索引页,另外3个是房产页面)。 如果使用了错误的用户名和密码,将会跳转到一个没有任何项目的页面,并且此时爬取过程会被终止,如下面的执行情况所示。 <strong>$ scrapy crawl login</strong><strong>INFO: Scrapy 1.0.3 started (bot: properties)</strong><strong>...</strong><strong>DEBUG: Redirecting (302) to &lt;GET .../dynamic/error > from &lt;POST .../</strong><strong>dynamic/login></strong><strong>DEBUG: Crawled (200) &lt;GET .../dynamic/error></strong><strong>...</strong><strong>INFO: Spider closed (closespider_itemcount)</strong> 这是一个简单的登录示例,用于演示基本的登录机制。大多数网站都会拥有一些更加复杂的机制,不过Scrapy也都能够轻松处理。比如,一些网站要求你在执行POST请求时,将表单页中的某些表单变量传输到登录页,以便确认Cookie是启用的,同样也会让你在尝试暴力破解成千上万次用户名/密码的组合时更加困难。图1.2所示即为此种情况的一个示例。 图1.2 使用一次性随机数的一个更加高级的登录示例的请求和响应情况 比如,当访问http://localhost:9312/dynamic/nonce时,你会看到一个看起来一样的页面,但是当使用Chrome浏览器的开发者工具查看时,会发现页面的表单中有一个叫作nonce的隐藏字段。当提交该表单时(提交到http://localhost:9312/ dynamic/nonce-login),除非你既传输了正确的用户名/密码,又提交了服务端在你访问该登录页时给你的nonce值,否则登录不会成功。你无法猜测该值,因为它通常是随机且一次性的。这就表示要想成功登录,现在就需要请求两次了。你必须先访问表单页,然后再访问登录页传输数据。当然,Scrapy同样拥有内置函数可以帮助我们实现这一目的。 我们创建了一个和之前相似的NonceLoginSpider爬虫。现在,在start_requests()中,将返回一个简单的Request(不要忘记引入该模块)到表单页面中,并通过设置其callback属性为处理方法parse_welcome()手动处理响应。在parse_welcome()中,使用了FormRequest对象的辅助方法from_response(),以创建从原始表单中预填充所有字段和值的FormRequest对象。FormRequest.from_response()粗略模拟了一次在页面的第一个表单上的提交单击,此时所有字段留空。 该方法对于我们来说非常有用,因为它能够毫不费力地原样包含表单中的所有隐藏字段。我们所需要做的就是使用formdata参数填充user和pass字段以及返回FormRequest。下面是其相关代码。 # Start on the welcome pagedefstart_requests(self): return [ Request( "http://web:9312/dynamic/nonce", callback=self.parse_welcome) ]# Post welcome page's first form with the given user/passdefparse_welcome(self, response): return FormRequest.from_response( response, formdata={"user":"user","pass": "pass"} ) 我们可以像平时一样运行爬虫。$ scrapy crawl noncelogin</strong> <strong>INFO: Scrapy 1.0.3 started (bot: properties)</strong><strong>...</strong><strong>DEBUG: Crawled (200) &lt;GET .../dynamic/nonce></strong><strong>DEBUG: Redirecting (302) to &lt;GET .../dynamic/gated > from &lt;POST .../</strong><strong>dynamic/login-nonce></strong><strong>DEBUG: Crawled (200) &lt;GET .../dynamic/gated></strong><strong>...</strong><strong>INFO: Dumping Scrapy stats:</strong><strong> {...</strong><strong> 'downloader/request_method_count/GET': 5,</strong><strong> 'downloader/request_method_count/POST': 1,</strong><strong>...</strong><strong> 'item_scraped_count': 3,</strong> 可以看到,第一个GET请求前往/dynamic/nonce页面,然后是POST请求,跳转到/dynamic/nonce-login页面,之后像前面的例子一样跳转到/dynamic/gated页面。关于登录的讨论就到这里。该示例使用两个步骤完成登录。只要你有足够的耐心,就可以形成任意长链,来执行几乎所有的登录操作。 5.2 使用JSON API和AJAX页面的爬虫 有时,你会发现自己在页面寻找的数据无法从HTML页面中找到。比如,当访问http://localhost:9312/static/时(见图5.3),在页面任意位置右键单击inspect element(1, 2),可以看到其中包含所有常见HTML元素的DOM树。但是,当你使用scrapy shell请求,或是在Chrome浏览器中右键单击View Page Source(3, 4)时,则会发现该页面的HTML代码中并不包含关于房产的任何信息。那么,这些数据是从哪里来的呢? 图1.3 动态加载JSON对象时的页面请求与响应 与平常一样,遇到这类例子时,下一步操作应当是打开Chrome浏览器开发者工具的Network选项卡,来看看发生了什么。在左侧的列表中,可以看到加载本页面时Chrome执行的请求。在这个简单的页面中,只有3个请求:static/是刚才已经检查过的请求;jquery.min.js用于获取一个流行的Javascript框架的代码;而api.json看起来会让我们产生兴趣。当单击该请求(6),并单击右侧的Preview选项卡(7)时,就会发现这里面包含了我们正在寻找的数据。实际上,http://localhost:9312/properties/api.json包含了房产的ID和名称(8),如下所示。 [{ "id":0, "title":"better set unique family well"},... { "id":29, "title":"better portered mile"}] 这是一个非常简单的JSON API的示例。更复杂的API可能需要你登录,使用POST请求,或返回更有趣的数据结构。无论在哪种情况下,JSON都是最简单的解析格式之一,因为你不需要编写任何XPath表达式就可以从中抽取出数据。 Python提供了一个非常好的JSON解析库。当我们执行import json时,就可以使用json.loads(response.body)解析JSON,将其转换为由Python原语、列表和字典组成的等效Python对象。 我们将第3章的manual.py拷贝过来,用于实现该功能。在本例中,这是最佳的起始选项,因为我们需要通过在JSON对象中找到的ID,手动创建房产URL以及Request对象。我们将该文件重命名为api.py,并将爬虫类重命名为ApiSpider,name属性修改为api。新的start_urls将会是JSON API的URL,如下所示。 start_urls = ( 'http://web:9312/properties/api.json',) 如果你想执行POST请求,或是更复杂的操作,可以使用前一节中介绍的start_requests()方法。此时,Scrapy将会打开该URL,并调用包含以Response为参数的parse()方法。可以通过import json,使用如下代码解析JSON对象。 defparse(self, response): base_url ="http://web:9312/properties/" js = json.loads(response.body) for iteminjs: id = item["id"] url = base_url +"property_%06d.html" % id yield Request(url, callback=self.parse_item) 前面的代码使用了json.loads(response.body),将Response这个JSON对象解析为Python列表,然后迭代该列表。对于列表中的每一项,我们将URL的3个部分(base_url、property_%06d以及.html)组合到一起。base_url是在前面定义的URL前缀。%06d是Python语法中非常有用的一部分,它可以让我们结合Python变量创建新的字符串。在本例中,%06d将会被变量id的值替换(本行结尾处%后面的变量)。id将会被视为数字(%d表示视为数字),并且如果不满6位,则会在前面加上0,扩展成6位字符。比如,id值为5,%06d将会被替换为000005,而如果id为34322,%06d则会被替换为034322。最终结果正是我们房产页面的有效URL。我们使用该URL形成一个新的Request对象,并像第3章一样使用yield。然后可以像平时那样使用scrapy crawl运行该示例。 <strong>$ scrapy crawl api</strong><strong>INFO: Scrapy 1.0.3 started (bot: properties)</strong><strong>...</strong><strong>DEBUG: Crawled (200) &lt;GET ...properties/api.json></strong><strong>DEBUG: Crawled (200) &lt;GET .../property_000029.html></strong><strong>...</strong><strong>INFO: Closing spider (finished)</strong><strong>INFO: Dumping Scrapy stats:</strong><strong>...</strong><strong> 'downloader/request_count': 31, ...</strong><strong> 'item_scraped_count': 30,</strong> 你可能会注意到结尾处的状态是31个请求——每个Item一个请求,以及最初的api.json的请求。 1.2.1 在响应间传参 很多情况下,在JSON API中会有感兴趣的信息,你可能想要将它们存储到Item中。在我们的示例中,为了演示这种情况,JSON API会在给定房产信息的标题前面加上"better"。比如,房产标题是"Covent Garden",API就会将标题写为"Better Covent Garden"。假设我们想要将这些"better"开头的标题存储到Items中,要如何将信息从parse()方法传递到parse_item()方法呢? 不要感到惊讶,通过在parse()生成的Request中设置一些东西,就能实现该功能。之后,可以从parse_item()接收到的Response中取得这些信息。Request有一个名为meta的字典,能够直接访问Response。比如在我们的例子中,可以在该字典中设置标题值,以存储来自JSON对象的标题。 title = item["title"]yield Request(url, meta={"title": title},callback=self.parse_item) 在parse_item()内部,可以使用该值替代之前使用过的XPath表达式。 l.add_value('title', response.meta['title'], MapCompose(unicode.strip, unicode.title)) 你会发现我们不再调用add_xpath(),而是转为调用add_value(),这是因为我们在该字段中将不会再使用到任何XPath表达式。现在,可以使用scrapy crawl运行这个新的爬虫,并且可以在PropertyItems中看到来自api.json的标题。 1.3 30倍速的房产爬虫 有这样一种趋势,当你开始使用一个框架时,做任何事情都可能会使用最复杂的方式。你在使用Scrapy时也会发现自己在做这样的事情。在疯狂于XPath等技术之前,值得停下来想一想:我选择的方式是从网站中抽取数据最简单的方式吗? 如果你能从索引页中抽取出基本相同的信息,就可以避免抓取每个房源页,从而得到数量级的提升。 比如,在房产示例中,我们所需要的所有信息都存在于索引页中,包括标题、描述、价格和图片。这就意味着只抓取一个索引页,就能抽取其中的30个条目以及前往下一页的链接。通过爬取100个索引页,我们只需要100个请求,而不是3000个请求,就能够得到3000个条目。太棒了! 在真实的Gumtree网站中,索引页的描述信息要比列表页中完整的描述信息稍短一些。不过此时这种抓取方式可能也是可行的,甚至也能令人满意。 在我们的例子中,当查看任何一个索引页的HTML代码时,就会发现索引页中的每个房源都有其自己的节点,并使用itemtype="http://schema.org/Product"来表示。在该节点中,我们拥有与详情页完全相同的方式为每个属性注解的所有信息,如图5.4所示。 图5.4 从单一索引页抽取多个房产信息 我们在Scrapy shell中加载第一个索引页,并使用XPath表达式进行测试。 <strong>$ scrapy shell http://web:9312/properties/index_00000.html</strong> 在Scrapy shell中,尝试选取所有带有Product标签的内容: <strong>>>> p=response.xpath('//*[@itemtype="http://schema.org/Product"]')</strong><strong>>>> len(p)</strong><strong>30</strong><strong>>>> p</strong><strong>[&lt;Selector xpath='//*[@itemtype="http://schema.org/Product"]' data=u'&lt;li </strong><strong>class="listing-maxi" itemscopeitemt'...]</strong> 可以看到我们得到了一个包含30个Selector对象的列表,每个对象指向一个房源。在某种意义上,Selector对象与Response对象有些相似,我们可以在其中使用XPath表达式,并且只从它们指向的地方获取信息。唯一需要说明的是,这些表达式应该是相对XPath表达式。相对XPath表达式与我们之前看到的基本一样,不过在前面增加了一个'.'点号。举例说明,让我们看一下使用.//*[@itemprop="name"][1]/text()这个相对XPath表达式,从第4个房源抽取标题时是如何工作的。 <strong>>>> selector = p[3]</strong><strong>>>> selector</strong><strong>&lt;Selector xpath='//*[@itemtype="http://schema.org/Product"]' ... '></strong><strong>>>> selector.xpath('.//*[@itemprop="name"][1]/text()').extract()</strong><strong>[u'l fun broadband clean people brompton european']</strong> 可以在Selector对象的列表中使用for循环,抽取索引页中全部30个条目的信息。 为了实现该目的,我们再一次从第3章的manual.py着手,将爬虫重命名为"fast",并重命名文件为fast.py。我们将复用大部分代码,只在parse()和parse_items()方法中进行少量修改。最新方法的代码如下。 defparse(self, response): # Get the next index URLs and yield Requests next_sel = response.xpath('//*[contains(@class,"next")]//@href') for url in next_sel.extract(): yield Request(urlparse.urljoin(response.url, url)) # Iterate through products and create PropertiesItems selectors = response.xpath( '//*[@itemtype="http://schema.org/Product"]') for selector in selectors: yieldself.parse_item(selector, response) 在代码的第一部分中,对前往下一个索引页的Request的yield操作的代码没有变化。唯一改变的内容在第二部分,不再使用yield为每个详情页创建请求,而是迭代选择器并调用parse_item()。其中,parse_item()的代码也和原始代码非常相似,如下所示。 def parse_item(self, selector, response): # Create the loader using the selector l = ItemLoader(item=PropertiesItem(), selector=selector) # Load fields using XPath expressions l.add_xpath('title', './/*[@itemprop="name"][1]/text()', MapCompose(unicode.strip, unicode.title)) l.add_xpath('price', './/*[@itemprop="price"][1]/text()', MapCompose(lambda i: i.replace(',', ''), float), re='[,.0-9]+') l.add_xpath('description', './/*[@itemprop="description"][1]/text()', MapCompose(unicode.strip), Join()) l.add_xpath('address', './/*[@itemtype="http://schema.org/Place"]' '[1]/*/text()', MapCompose(unicode.strip)) make_url = lambda i: urlparse.urljoin(response.url, i) l.add_xpath('image_urls', './/*[@itemprop="image"][1]/@src', MapCompose(make_url)) # Housekeeping fields l.add_xpath('url', './/*[@itemprop="url"][1]/@href', MapCompose(make_url)) l.add_value('project', self.settings.get('BOT_NAME')) l.add_value('spider', self.name) l.add_value('server', socket.gethostname()) l.add_value('date', datetime.datetime.now()) return l.load_item() 我们所做的细微变更如下所示。 ItemLoader现在使用selector作为源,而不再是Response。这是ItemLoaderAPI一个非常便捷的功能,能够让我们从当前选取的部分(而不是整个页面)抽取数据。 XPath表达式通过使用前缀点号(.)转为相对XPath。 我们必须自己编辑Item的URL。之前,response.url已经给出了房源页的URL。而现在,它给出的是索引页的URL,因为该页面才是我们要爬取的。我们需要使用熟悉的.//*[@itemprop="url"][1]/@href这个XPath表达式抽取出房源的URL,然后使用MapCompose处理器将其转换为绝对URL。 小的改变能够节省巨大的工作量。现在,我们可以使用如下代码运行该爬虫。 <strong>$ scrapy crawl fast -s CLOSESPIDER_PAGECOUNT=3</strong><strong>...</strong><strong>INFO: Dumping Scrapy stats:</strong><strong> 'downloader/request_count': 3, ...</strong><strong> 'item_scraped_count': 90,...</strong> 和预期一样,只用了3个请求,就抓取了90个条目。如果我们没有在索引页中获取到的话,则需要93个请求。这种方式太明智了! 如果你想使用scrapy parse进行调试,那么现在必须设置spider参数,如下所示。 <strong>$ scrapy parse --spider=fast http://web:9312/properties/index_00000.html</strong><strong>...</strong><strong>>>> STATUS DEPTH LEVEL 1 &lt;&lt;&lt;</strong><strong># Scraped Items --------------------------------------------</strong><strong>[{'address': [u'Angel, London'],</strong><strong>... 30 items...</strong><strong># Requests ---------------------------------------------------</strong><strong>[&lt;GET http://web:9312/properties/index_00001.html>]</strong> 正如期望的那样,parse()返回了30个Item以及一个前往下一索引页的Request。请使用scrapy parse随意试验,比如传输--depth=2。 本文摘自《精通Python爬虫框架Scrapy》 Scrapy是使用Python开发的一个快速、高层次的屏幕抓取和Web抓取框架,用于抓Web站点并从页面中提取结构化的数据。本书以Scrapy 1.0版本为基础,讲解了Scrapy的基础知识,以及如何使用Python和三方API提取、整理数据,以满足自己的需求。 延伸推荐 2018年1月重磅新书 小学生开始学Python,最接近AI的编程语言:安利一波Python书单 政策升温:大家都在学大数据,一大波好书推荐 一本基于Python语言的Selenium自动化测试书 8本新书,送出一本你喜欢的 AI经典书单| 入门人工智能该读哪些书? 点击关键词阅读更多新书: Python|机器学习|Kotlin|Java|移动开发|机器人|有奖活动|Web前端|书单 在“异步图书”后台回复“关注”,即可免费获得2000门在线视频课程;推荐朋友关注根据提示获取赠书链接,免费得异步图书一本。赶紧来参加哦! 点击阅读原文,查看本书更多信息 扫一扫上方二维码,回复“关注”参与活动! 点击下方阅读原文,查看更多内容

优秀的个人博客,低调大师

hive 使用技巧笔记

来源: http://michael-roshen.iteye.com/blog/2115268 例子: INSERT OVERWRITE TABLE prices_collected_${hiveconf:wid_version} select pc.collect_id as product_id , regexp_extract(pc.price,'(\\d*\\.?\\d+)',1) as price , pc.region, '' as location_area_code, '' as city_code, from_unixtime(unix_timestamp() , 'yyyy-MM-dd hh:mm:ss') as created_at, from_unixtime(unix_timestamp() , 'yyyy-MM-dd hh:mm:ss') as updated_at from products_compared_${hiveconf:wid_version} as pc 1.根据hive执行的参数来动态的设置表名称prices_collected_${hiveconf:wid_version} hive -hiveconf wid_version='4' 则可以通过${hiveconft:wid_version}来接收参数,生成prices_collected_4这张表 2. 使用正则表达式获取需要的信息,如:获取一段字符串中的数字 regexp_extract(pc.price,'(\\d*\\.?\\d+)',1) as price 注意hive中需要使用双斜杠来处理正则表达式 3. 获取系统时间 from_unixtime(unix_timestamp() , 'yyyy-MM-dd hh:mm:ss') as created_a 使用from_unixtime(unix_timestamp() , 'yyyy-MM-dd hh:mm:ss')获取系统时间,格式可以根据需要调整 4. 多个表进行join的时候,可能会报错 使用set hive.auto.convert.join=false;解决 5. 创建表 create table if not exists brands ( name string, created_at string, updated_at string ) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' ESCAPED BY '\\' STORED AS TEXTFILE; 以文本方式进行存储,"\\"进行转义,"\t"作为换行符 6.到处hive中的某个表中的数据到本地,执行hive命令如下: hive -hiveconf local_path=/home/hive/hive_data/products_24_1 -hiveconf hive_table=products_24_1 -hiveconf columnstr=' name , created_at, updated_at, "released" as status ' -f /home/hive/export_hive_table_to_local.sql 需要执行的参数依次是 1.导出到本地的位置local_path 2.导出hive中的哪个表hive_table 3. 导出products_24_1 表中的哪些字段 colunmstr 4. 根据上面的参数,在本地创建products_24_1 表,使用-f来指定调用的文件 /home/hive/export_hive_table_to_local.sql 文件内容如下: insert overwrite local directory '${hiveconf:local_path}' ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' ESCAPED BY '\\' STORED AS TEXTFILE select ${hiveconf:columnstr} from ${hiveconf:hive_table}; 7.将本地文件导入到psql数据库中, hive对pg的支持不好,不能用sqoop来进行数据的导入,可以先将hive中的数据读到本地,在使用python脚本来进行文件的写入 Python代码 definsert_to_pg(conn,table_name,file_path,insert_columns=None): conn=psycopg2.connect(conn) cursor=conn.cursor() ifos.path.isfile(file_path): datafile=ReadFileProgress(file_path) cursor.copy_from(file=datafile,table=table_name,sep='\t',null='\\N',size=81920,columns=insert_columns) datafile.close() Python代码 #!/usr/bin/python ##_*_coding:utf-8_*_ importos,sys importpsycopg2 classReadFileProgress: def__init__(self,filename): self.datafile=open(filename) self.totalRecords=0 self.totalBytes=os.stat(filename).st_size self.readBytes=0 self.datafile.readline() i=0 fori,linenumerate(self.datafile): pass self.totalRecords=i+1 sys.stderr.write("Numberofrecords:%d\n"%(self.totalRecords)) self.datafile.seek(0) self.datafile.readline() self.perc5=self.totalBytes/20.0 self.perc5count=0 self.lastPerc5=0 sys.stderr.write("Writingrecords:0%") defcountBytes(self,size=0): self.readBytes+=size if(self.readBytes-self.lastPerc5>=self.perc5): self.lastPerc5=self.readBytes if(int(self.readBytes/self.perc5)==5): sys.stderr.write("25%") elif(int(self.readBytes/self.perc5)==10): sys.stderr.write("50%") elif(int(self.readBytes/self.perc5)==15): sys.stderr.write("75%") else: sys.stderr.write(".") sys.stderr.flush() defreadline(self,size=None): countBytes(size) returnself.datafile.readline(size) defread(self,size=None): self.countBytes(size) returnself.datafile.read(size) defclose(self): sys.stderr.write("100%\n") self.datafile.close() 8. 从pg上导出指定表 Python代码 defdo_export(conn,table_name,file_path,columns=None): conn=psycopg2.connect(conn) cursor=conn.cursor() cursor.copy_to(file=file(file_path,'w'),table=table_name,sep='\t',null='\\N',columns=columns) cursor.close() conn.commit() sys.stdout.write("Transactionfinishedsuccessfully.\n") 9. 则select语句中也可以通过hiveconf来传递参数,执行hive命令 hive -hiveconf name='hello hive' INSERT OVERWRITE TABLE companies select '${hiveconf:name}' as name from companies_old 本文转自 SimplePoint 51CTO博客,原文链接:http://blog.51cto.com/2226894115/1898261,如需转载请自行联系原作者

优秀的个人博客,低调大师

Appium知识技巧收集

1、真机调试打开USB调试模式 2、启动脚本提示apk包Could not make a string,是释放string.json出错,由于apk损坏导致,验证是安装到真机上开启APP 3、Activity要写对,否则提示不存在Activity,建议写完整名称,完整包名类似com.xxx.xxx.Activity;启动Activity要写对,否则提示XXX never XXX。包名参数与Activity参数可以在AndroidManifest.xml(获取方法http://code.google.com/p/android-apktool/)中查看,包名: android:name="com.xxx.xxx.SplashActivity" android:launchMode="singleTop" android:screenOrientation="portrait" ,本例中是com.xxx.xxx.SplashActivity 4、Appium支持一个Webdriver元素定位方法的子集 find by "tag name" (i.e., 通过UI的控件类型) find by "name" (i.e., 通过元素的文本, 标签, 或者开发同学添加的id标示, 比如accessibilityIdentifier) find by "xpath" (i.e., 具有一定约束的路径抽象标示, 基于XPath方式) 5、Appium 在 Mac OS X 上安装使用文档,参考:http://testerhome.com/topics/166,Windows平台,参考:http://testerhome.com/topics/155,Linux平台,参考:http://testerhome.com/topics/160,Android平台,参考:http://testerhome.com/topics/153 6、iOS模拟器—>硬件—>设备—>iPhone 7、【坑】Appium在MacOS10.9以及iOS7上面的问题:启动appium脚本没有问题。安装好应用之后log中会报出500,同时instruments会显示simulator session timeout。 8、生成build/Test.appa的方法,进入到目录下面编译 xcodebuild -sdk iphonesimulator6.0 9、安装路径问题 全局路径,也就是带上参数 -g 的安装模式。这个命令会把模块安装在 $PREFIX/lib/node_modules 下,可通过命令 npm root -g 查看全局模块的安装目录。 package.json 里定义的bin会安装到 $PREFIX/bin 目录下,如果模块带有 man page 会安装到 $PREFIX/share/man 目录下。 本地路径,不带 -g 参数的。从当前目录一直查找到根目录/下有没有 node_modules 目录,有模块安装到这个目录下的 node_modules 目录里,如果没有找到则把模块安装到当前目录 node_modules 目录下。package.josn 定义的 bin 会安装到 node_modules/.bin 目录下,man page 则不会安装。 最新内容请见作者的GitHub页:http://qaseven.github.io/

优秀的个人博客,低调大师

UI测试常用技巧

如何在当前界面中查找第一个ListView中的带有文本属性为Apps的子控件。 UiObjectappItem=newUiObject(newUiSelector() .className("android.widget.ListView").instance(1) .childSelector(newUiSelector().text("Apps"))); 如何查找当前显示界面中的取消按钮和确认按钮: UiObjectcancelButton=newUiObject(newUiSelector().text("Cancel")); UiObjectokButton=newUiObject(newUiSelector().text("OK")); 查找到的UiObject实例可以在其他测试代码中重用。需要注意的是:每次使用UiObject做操作的时候uiautomator都会在当前屏幕重新查找该控件。 如下代码uiautomator工具在当前界面查找文本内容为“OK”的控件。如果存在并且可用则模拟用户点击该控件。 if(okButton.exists()&&okButton.isEnabled()) { okButton.click(); } 还可以限制仅仅查找特定类型的控件。例如如下代码只查找文本为“Cancel”和“OK”的android.widget.Button类型控件。 UiObjectcancelButton=newUiObject(newUiSelector().text("Cancel") .className("android.widget.Button")); UiObjectokButton=newUiObject(newUiSelector().text("OK") .className("android.widget.Button")); UiCollection 代表控件的集合。获取UiCollection的方式和UiObject一样,通过UiSelector查找。UiCollection对应Android系统中的ViewGroup以及子控件。 如下代码演示如何通过UiSelector来获取包含视频集合的UiCollection。 UiCollectionvideos=newUiCollection(newUiSelector() .className("android.widget.FrameLayout")); 如果每个视频是放到LinearLayout中的,则可以通过如下方式获取视频的数目: intcount=videos.getChildCount(newUiSelector() .className("android.widget.LinearLayout")); 如果需要查找标签为“CuteBabyLaughing”的视频,并点击。则可以通过如下方式: UiObjectvideo=videos.getChildByText(newUiSelector() .className("android.widget.LinearLayout"),"CuteBabyLaughing"); video.click(); 同样还可以模拟其他用户操作。例如,模拟选择视频的操作如下: UiObjectcheckBox=video.getChild(newUiSelector() .className("android.widget.Checkbox")); if(!checkBox.isSelected())checkbox.click(); UiScrollable 代表可滚动的控件。可以用UiScrollable来模拟水平或者垂直滚动的UI元素。如果需要操作的元素在屏幕外需要滚动屏幕才能看到的情况下需要使用UiScrollable。 例如,下面的代码显示了如何模拟滚动到“Settings”菜单并点击“Abouttablet”菜单的操作。 UiScrollablesettingsItem=newUiScrollable(newUiSelector() .className("android.widget.ListView")); UiObjectabout=settingsItem.getChildByText(newUiSelector() .className("android.widget.LinearLayout"),"Abouttablet"); about.click() 最新内容请见作者的GitHub页:http://qaseven.github.io/

优秀的个人博客,低调大师

Greenplum深度优化技巧

以下内容根据演讲PPT以及现场分享整理而成。 本次的分享将主要对云上的Greenplum进行介绍。有的同学往往有一些疑问就是Greenplum产品有什么优势?为什么要使用Greenplum?Greenplum能解决什么问题?在今天的分享中也将探讨这几个问题。最后还会提到一些阿里云Greenplum小组所做的工作。 本次的分享主要分为三个方面: 一、ApsaraDB for GP的定位 二、ApsaraDB for GP内核定制 三、未来规划 一、ApsaraDB for GP的定位 首先提出两个问题:Greenplum的优势是什么?它能解决什么问题? 阿里云的Greenplum产品叫做ApsaraDB for GP,我们给这个产品的定位只有两个字:性能,也就是阿里云的ApsaraDB for GP在性能方面一定要比其他的分析性的数据库解决方案更高。 在我看来,Greenplum的优势在于这样的几点:首先,Greenplum使用了MPP的架构,除此之外,它还使用了列存和压缩。列存和压缩在现在看来是比较普通的技术,但是其实像IBM的DB2数据库也才在近几年支持了列存和压缩技术,而Greenplum从很早就支持了这些技术,这些技术的在性能上会有很大的优势。而且Greenplum不仅能够支持复杂的SQL,而且它的查询优化器能力也大大超出预期。Greenplum产品性能的定位是在秒级或者小于秒级能够实现对于复杂查询的响应,Greenplum执行器的性能其实是来自多年来Postgre的积淀,要远远超过其他的解决方案。另外Greenplum使用的是本地高效存储和高速网络,当然最好是万兆网络,还有预置稳定资源,不会出现性能的波动,简单而言就是Greenplum可以高效地解决大数据分析的需求。 MPP架构处理举例 举一个例子:Select count(*) from customer group by city 这是一条很简单的SQL语句,比方是在数据库中按照城市分组查询每个城市的用户数量。 对比MySQL的执行过程来看,首先MySQL数据库会对所有的数据记录进行扫描,对用户按照城市进行分组,然后对于每组用户的数量进行统计,最后将结果返回给用户。对于1亿条记录而言,这时即使IO不出现问题,那么机器的CPU也会被占满。对于MySQL而言,这样的一个简单SQL语句就可以使机器的CPU占用率达到100%,而且这样执行的速度也是非常慢的。 而Greenplum则采取多核进行计算处理,比方在扫描数据的时候将会采用多个进程,多个CPU去处理,在Hash的时候也会使用多个CPU,这样速度就会变快。当数据量太大,多到一个机器处理不了的时候,就需要MPP了。所以Greenplum使用MPP架构解决了对于大数据量的问题,对于同样的一条SQL语句,Greenplum将会启动多个实例和进程去做同样的事情,执行同样的流程,但是处理的是不同的数据分片,如此就实现了多机器的查询计算,这也就是Greenplum的最核心能力,也是与传统数据库不同之处。 列存与压缩原理举例 再举一个SQL语句的例子:Select count(*) from customer where status = valid group by city 对于这条语句,DBA在对像MySQL这样的传统的数据库进行优化的时候,可能会选择使用索引。但是其实这种情况下使用索引是不恰当的,使用索引只能够过滤20%左右的数据,而增加的性能开销却是很大的,所以不应该使用索引。 列存方式则将会把status单独存储在一个列存块,经过压缩以后,占用的存储空间也会大大降低,同时也可以单独对status数据取出并进行处理。其实在这种情况下列存近似等于开销极小的索引,而且效率非常高。 Greenplum VS Hadoop Greenplum的Orca优化器,使得其在响应时间上比Hadoop的离线计算产品性能更加优秀。 Greenplum的 SQL Runtime是从Postgre几十年的沉淀演变来的,并且是由C语言构建的,比Hadoop的SQL Runtime效率高很多。还有Greenplum使用的是本地存储,而Hadoop使用的则是分布式存储。所以总体来看,Greenplum的性能可以达到Hadoop的5-30倍。 ApsaraDB for GP VS AWS Redshift 在亚马逊上有一款卖的非常好的产品,名字叫“Redshift”,它被称为亚马逊“有史以来卖的最好的云服务”,其实ApsaraDB for GP在功能上与Redshift很像,而且ApsaraDB for GP有很多Redshift不具备的特性。 在使用ApsaraDB for GP产品时,推荐使用ECS VPC的架构。架构搭建完成之后的一个问题就是:如何将ECS上的数据导入到ApsaraDB for GP进行分析呢?其实可以通过写程序的方式将数据导入到主节点上,也可以使用EMR图去主动地抓取并分析数据。EMR与OSS结合得非常好,将数据存放在OSS上之后,可以通过执行SQL语句使得所有的子节点并发地拉取数据。当然也可以通过应用服务器直接将数据或者日志上传到OSS上面去,对于其他的阿里云服务可也以通过阿里云CDB服务将数据定向迁移到OSS上面,再通过Greenplum进行数据拉取,进而使用可视化工具对数据进行分析。 二、ApsaraDB for GP 内核定制 接下来分享阿里云在Greenplum上做的一些优化工作。在阿里云ApsaraDB for GP架构设计中,子节点分散地存储在不同的物理机上面,每2个或者是16个子节点为一组。对于子节点组采取了资源限制,但是组内节点之间可以弹性共享资源,这样就降低资源倾斜造成的影响,也便于管理,同时节点之间使用万兆网络进行数据传输。 这样架构的一个优点是支持OSS外部表读写,可以在OSS外部创建一张表,使用简单的SQL语句就可以将数据导入到Greenplum的子节点上,同时也可以很简单地将数据从Greenplum导回到OSS。简而言之,就是让OSS充当数据存储中心,让Greenplum作为数据分析中心。 而且Greenplum支持将一些语法作为插件提供,很多这样插件目前已经提交开源社区,很快大家就可以用到了。而且未来ApsaraDB for GP将会完善对于JSON的支持,如果社区动作较慢,我们会考虑在阿里云自己的产品中优先支持。而对于OOM的很难提前监控的问题,阿里云采取的方法就是利用外部脚本监控cgroup中的内存统计,发生内存水位较高时,将实例移入公共cgroup,同时发出cancel query信号给内核;当水位下降时移回实例的cgroup。 三、未来规划 谈到未来规划,第一点就是必须要满足客户需求。另外还要继续对于列存进行优化,希望在未来能够在某些场景下将性能进一步提高。对于CPU优化方面,我们希望在未来能对GP执行器的静态编译进行进一步优化。最后我们希望与Greenplum厂商、用户和开发者一起为Greenplum社区的发展贡献力量,一起推动Greenplum的发展。

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册