iOS GPUImage源码解读

前言

GPUImage是iOS上一个基于OpenGL进行图像处理的开源框架,内置大量滤镜,架构灵活,可以在其基础上很轻松地实现各种图像处理功能。本文主要向大家分享一下项目的核心架构、源码解读及使用心得。

GPUImage有哪些特性

  1. 丰富的输入组件 摄像头、图片、视频、OpenGL纹理、二进制数据、UIElement(UIView, CALayer)
  2. 大量现成的内置滤镜(4大类) 1). 颜色类(亮度、色度、饱和度、对比度、曲线、白平衡…) 2). 图像类(仿射变换、裁剪、高斯模糊、毛玻璃效果…) 3). 颜色混合类(差异混合、alpha混合、遮罩混合…) 4). 效果类(像素化、素描效果、压花效果、球形玻璃效果…)
  3. 丰富的输出组件 UIView、视频文件、GPU纹理、二进制数据
  4. 灵活的滤镜链 滤镜效果之间可以相互串联、并联,调用管理相当灵活。
  5. 接口易用 滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用,并且内置了一个cache模块实现了framebuffer的复用。
  6. 线程管理 OpenGLContext不是多线程安全的,GPUImage创建了专门的contextQueue,所有的滤镜都会扔到统一的线程中处理。
  7. 轻松实现自定义滤镜效果 继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。

基本用法

// 获取一张图片
UIImage *inputImage = [UIImage imageNamed:@"sample.jpg"];
// 创建图片输入组件GPUImagePicture *sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES]; 
// 创建素描滤镜
GPUImageSketchFilter *customFilter = [[GPUImageSketchFilter alloc] init]; 
// 把素描滤镜串联在图片输入组件之后
[sourcePicture addTarget:customFilter];
// 创建ImageView输出组件GPUImageView *imageView = [[GPUImageView alloc]initWithFrame:mainScreenFrame];
[self.view addSubView:imageView];
// 把ImageView输出组件串在滤镜链末尾[customFilter addTarget:imageView];
// 调用图片输入组件的process方法,渲染结果就会绘制到imageView上[sourcePicture processImage];

效果如图:

%title插图%num%title插图%num

整个框架的目录结构

%title插图%num%title插图%num

核心架构

%title插图%num%title插图%num

基本上每个滤镜都继承自GPUImageFilter; 而GPUImageFilter作为整套框架的核心; 接收一个GPUImageFrameBuffer输入; 调用GLProgram渲染处理; 输出一个GPUImageFrameBuffer; 把输出的GPUImageFrameBuffer传给通过targets属性关联的下级滤镜; 直到传递至*终的输出组件;

核心架构可以整体划分为三块:输入、滤镜处理、输出 接下来我们就深入源码,看看GPUImage是如何获取数据、传递数据、处理数据和输出数据的

获取数据

GPUImage提供了多种不同的输入组件,但是无论是哪种输入源,获取数据的本质都是把图像数据转换成OpenGL纹理。这里就以视频拍摄组件(GPUImageVideoCamera)为例,来讲讲GPUImage是如何把每帧采样数据传入到GPU的。

GPUImageVideoCamera里大部分代码都是对摄像头的调用管理,不了解的同学可以去学习一下AVFoundation(传送门)。摄像头拍摄过程中每一帧都会有一个数据回调,在GPUImageVideoCamera中对应的处理回调的方法为:

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;

iOS的每一帧摄像头采样数据都会封装成CMSampleBufferRef; CMSampleBufferRef除了包含图像数据、还包含一些格式信息、图像宽高、时间戳等额外属性; 摄像头默认的采样格式为YUV420,关于YUV格式大家可以自行搜索学习一下(传送门):

%title插图%num%title插图%num

YUV420按照数据的存储方式又可以细分成若干种格式,这里主要是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange和kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange两种;

两种格式都是planar类型的存储方式,y数据和uv数据分开放在两个plane中; 这样的数据没法直接传给GPU去用,GPUImageVideoCamera把两个plane的数据分别取出:

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {   
 // 一大坨的代码用于获取采样数据的基本属性(宽、高、格式等等) 
    ......    if ([GPUImageContext supportsFastTextureUpload] && captureAsYUV) {   
      CVOpenGLESTextureRef luminanceTextureRef = NULL;         
      CVOpenGLESTextureRef chrominanceTextureRef = NULL;        
if (CVPixelBufferGetPlaneCount(cameraFrame) > 0) // Check for YUV planar inputs to do RGB conversion                    {
                           ......
// 从cameraFrame的plane-0提取y通道的数据,填充到luminanceTextureRef            
          glActiveTexture(GL_TEXTURE4); 
          err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef);   
          ......                  
       // 从cameraFrame的plane-1提取uv通道的数据,填充到chrominanceTextureRef              
       glActiveTexture(GL_TEXTURE5);   
       err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, bufferWidth/2, bufferHeight/2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &chrominanceTextureRef);          
            ......            
            // 把luminance和chrominance作为2个独立的纹理传入GPU 
            [self convertYUVToRGBOutput];   
             ......       
            }  
         } else {
               ...... 
          } 
  }

注意CVOpenGLESTextureCacheCreateTextureFromImage中对于internalFormat的设置; 通常我们创建一般纹理的时候都会设成GL_RGBA,传入的图像数据也会是rgba格式的; 而这里y数据因为只包含一个通道,所以设成了GL_LUMINANCE(灰度图); uv数据则包含2个通道,所以设成了GL_LUMINANCE_ALPHA(带alpha的灰度图); 另外uv纹理的宽高只设成了图像宽高的一半,这是因为yuv420中,每个相邻的2×2格子共用一份uv数据; 数据传到GPU纹理后,再通过一个颜色转换(yuv->rgb)的shader(shader是OpenGL可编程着色器,可以理解为GPU侧的代码,关于shader需要一些OpenGL编程基础(传送门)),绘制到目标纹理:

 // fullrange
 varying highp vec2 textureCoordinate;
 uniform sampler2D luminanceTexture;
 uniform sampler2D chrominanceTexture;
 uniform mediump mat3 colorConversionMatrix; 
 void main() {
     mediump vec3 yuv;
     lowp vec3 rgb;
     yuv.x = texture2D(luminanceTexture, textureCoordinate).r;
     yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
     rgb = colorConversionMatrix * yuv;
     gl_FragColor = vec4(rgb, 1);
 }
 // videorange
 varying highp vec2 textureCoordinate;
 uniform sampler2D luminanceTexture;
 uniform sampler2D chrominanceTexture;
 uniform mediump mat3 colorConversionMatrix; void main() {
     mediump vec3 yuv;
     lowp vec3 rgb;
     yuv.x = texture2D(luminanceTexture, textureCoordinate).r - (16.0/255.0);
     yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
     rgb = colorConversionMatrix * yuv;
     gl_FragColor = vec4(rgb, 1);
 }

注意yuv420fullrange和yuv420videorange的数值范围是不同的,因此转换公式也不同,这里会有2个颜色转换shader,根据实际的采样格式选择正确的shader; 渲染输出到目标纹理后就得到一个转换成rgb格式的GPU纹理,完成了获取输入数据的工作;

传递数据

GPUImage的图像处理过程,被设计成了滤镜链的形式;输入组件、效果滤镜、输出组件串联在一起,每次推动渲染的时候,输入数据就会按顺序传递,经过处理,*终输出。

%title插图%num%title插图%num

GPUImage设计了一个GPUImageInput协议,定义了GPUImageFilter之间传入数据的方法:

- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex {
    firstInputFramebuffer = newInputFramebuffer;
    [firstInputFramebuffer lock];
}

firstInputFramebuffer属性用来保存输入纹理; GPUImageFilter作为单输入滤镜基类遵守了GPUImageInput协议,GPUImage还提供了GPUImageTwoInputFilter, GPUImageThreeInputFilter等多输入filter的基类。

这里还有一个很重要的入口方法用于推动数据流转:

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    ......
    
    [self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]];

    [self informTargetsAboutNewFrameAtTime:frameTime];
}

每个滤镜都是由这个入口方法开始启动,这个方法包含2个调用 1). 首先调用render方法进行效果渲染 2). 调用informTargets方法将渲染结果推到下级滤镜

GPUImageFilter继承自GPUImageOutput,定义了输出数据,向后传递的方法:

- (void)notifyTargetsAboutNewOutputTexture;

但是这里比较奇怪的是滤镜链的传递实际并没有用notifyTargets方法,而是用了前面提到的informTargets方法:

- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime {
    ......    
    // Get all targets the framebuffer so they can grab a lock on it
    for (id<GPUImageInput> currentTarget in targets) {        if (currentTarget != self.targetToIgnoreForUpdates) {            NSInteger indexOfObject = [targets indexOfObject:currentTarget];            NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
            [self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
            [currentTarget setInputSize:[self outputFrameSize] atIndex:textureIndex];
        }
    }
    
    ......    
    // Trigger processing last, so that our unlock comes first in serial execution, avoiding the need for a callback
    for (id<GPUImageInput> currentTarget in targets) {        if (currentTarget != self.targetToIgnoreForUpdates) {            NSInteger indexOfObject = [targets indexOfObject:currentTarget];            NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
            [currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
        }
    }
}

GPUImageOutput定义了一个targets属性来保存下一级滤镜,这里可以注意到targets是个数组,因此滤镜链也支持并联结构。可以看到这个方法主要做了2件事情: 1). 对每个target调用setInputFramebuffer方法把自己的渲染结果传给下级滤镜作为输入 2). 对每个target调用newFrameReadyAtTime方法推动下级滤镜启动渲染 滤镜之间通过targets属性相互衔接串在一起,完成了数据传递工作。

%title插图%num

%title插图%num

处理数据

前面提到的renderToTextureWithVertices:方法便是每个滤镜必经的渲染入口。 每个滤镜都可以设置自己的shader,重写该渲染方法,实现自己的效果:

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates {
    ......

    [GPUImageContext setActiveShaderProgram:filterProgram];

    outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO];
    [outputFramebuffer activateFramebuffer];
    ......

    [self setUniformsForProgramAtIndex:0];
    
    glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
    glClear(GL_COLOR_BUFFER_BIT);

    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
    glUniform1i(filterInputTextureUniform, 2);	

    glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
    glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    ......
}

上面这个是GPUImageFilter的默认方法,大致做了这么几件事情: 1). 向frameBufferCache申请一个outputFrameBuffer 2). 将申请得到的outputFrameBuffer激活并设为渲染对象 3). glClear清除画布 4). 设置输入纹理 5). 传入顶点 6). 传入纹理坐标 7). 调用绘制方法

再来看看GPUImageFilter使用的默认shader:

 // vertex shader
 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;
 varying vec2 textureCoordinate; void main() {
     gl_Position = position;
     textureCoordinate = inputTextureCoordinate.xy;
 }
 // fragment shader
 varying highp vec2 textureCoordinate;
 uniform sampler2D inputImageTexture; void main() {
     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
 }

这个shader实际上啥也没做,VertexShader(顶点着色器)就是把传入的顶点坐标和纹理坐标原样传给FragmentShader,FragmentShader(片段着色器)就是从纹理取出原始色值直接输出,*终效果就是把图片原样渲染到画面。

输出数据

比较常用的主要是GPUImageView和GPUImageMovieWriter。

GPUImageView继承自UIView,用于实时预览,用法非常简单 1). 创建GPUImageView 2). 串入滤镜链 3). 插到视图里去 UIView的contentMode、hidden、backgroundColor等属性都可以正常使用 里面比较关键的方法主要有这么2个:

// 申明自己的CALayer为CAEAGLLayer+ (Class)layerClass  {    return [CAEAGLLayer class];
}
- (void)createDisplayFramebuffer {
    [GPUImageContext useImageProcessingContext];
    
    glGenFramebuffers(1, &displayFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, displayFramebuffer);
	
    glGenRenderbuffers(1, &displayRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer);
	
    [[[GPUImageContext sharedImageProcessingContext] context] renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
	
    GLint backingWidth, backingHeight;

    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
    
    ......

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer);
	
    ......
}

创建frameBuffer和renderBuffer时把renderBuffer和CALayer关联在一起; 这是iOS内建的一种GPU渲染输出的联动方法; 这样newFrameReadyAtTime渲染过后画面就会输出到CALayer。

GPUImageMovieWriter主要用于将视频输出到磁盘; 里面大量的代码都是在设置和使用AVAssetWriter,不了解的同学还是得去看AVFoundation; 这里主要是重写了newFrameReadyAtTime:方法:

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    ......

    GPUImageFramebuffer *inputFramebufferForBlock = firstInputFramebuffer;
    glFinish();

    runAsynchronouslyOnContextQueue(_movieWriterContext, ^{
        ......        
        // Render the frame with swizzled colors, so that they can be uploaded quickly as BGRA frames
        [_movieWriterContext useAsCurrentContext];
        [self renderAtInternalSizeUsingFramebuffer:inputFramebufferForBlock];
        
        CVPixelBufferRef pixel_buffer = NULL;        
        if ([GPUImageContext supportsFastTextureUpload]) {
            pixel_buffer = renderTarget;
            CVPixelBufferLockBaseAddress(pixel_buffer, 0);
        } else {
            CVReturn status = CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &pixel_buffer);            if ((pixel_buffer == NULL) || (status != kCVReturnSuccess)) {
                CVPixelBufferRelease(pixel_buffer);                return;
            } else {
                CVPixelBufferLockBaseAddress(pixel_buffer, 0);
                
                GLubyte *pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
                glReadPixels(0, 0, videoSize.width, videoSize.height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBufferData);
            }
        }
        
        ......        [assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer];        ......
    });
}

这里有几个地方值得注意: 1). 在取数据之前先调了一下glFinish,CPU和GPU之间是类似于client-server的关系,CPU侧调用OpenGL命令后并不是同步等待OpenGL完成渲染再继续执行的,而glFinish命令可以确保OpenGL把队列中的命令都渲染完再继续执行,这样可以保证后面取到的数据是正确的当次渲染结果。 2). 取数据时用了supportsFastTextureUpload判断,这是个从iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射(映射的创建可以参看获取数据中的CVOpenGLESTextureCacheCreateTextureFromImage),通过这个映射可以直接拿到CVPixelBufferRef而不需要再用glReadPixel来读取数据,这样性能更好。

*后归纳一下本文涉及到的知识点

1. AVFoundation 摄像头调用、输出视频都会用到AVFoundation 2. YUV420 视频采集的数据格式 3. OpenGL shader GPU的可编程着色器 4. CAEAGLLayer iOS内建的GPU到屏幕的联动方法 5. fastTextureUpload iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射

官网 webrtc_IOS 源码下载和编译

iOS

Development environment

An OS X machine is required for iOS development. While it’s possible to develop purely from the command line and text editors, it’s easiest to use XCode. Both methods will be illustrated here.

Getting the code

  1. Install Prerequisite software
  2. Set the target OS in your environment:
    export GYP_DEFINES="OS=ios"
  3. Create a working directory, enter it, and run:
    fetch --nohooks webrtc_ios
    gclient sync

    This will fetch a regular WebRTC checkout with the iOS-specific parts added. The same checkout can be used for both Mac and iOS development, depending on the OS you set in GYP_DEFINES (see above).

  4. You may want to disable Spotlight indexing for the checkout to speed up file operations.
See  Development  for generic instructions on how to update the code in your checkout.

Compiling the code

GYP is used to generate build instructions for ninja from the relevant .gyp files. Ninja is used to compile the source using the previously generated instructions. In order to configure GYP to generate build files for iOS certain environment variables need to be set. Those variables can be edited for the various build configurations as needed.
Building for iOS device:

export GYP_CROSSCOMPILE=1
export GYP_DEFINES="OS=ios target_arch=arm"
export GYP_GENERATOR_FLAGS="output_dir=out_ios"
export GYP_GENERATORS=ninja

Building for 64-bit iOS device:
As above, except with:

export GYP_DEFINES="OS=ios target_arch=arm64"
export GYP_GENERATOR_FLAGS="output_dir=out_ios64"

Building for simulator:
As above, except with:

export GYP_DEFINES="OS=ios target_arch=ia32"
export GYP_GENERATOR_FLAGS="output_dir=out_sim"

Building for 64-bit simulator:
As above, except with:

export GYP_DEFINES="OS=ios target_arch=x64"
export GYP_GENERATOR_FLAGS="output_dir=out_sim"
 

Building for OSX:
As above, except with:

export GYP_DEFINES="OS=mac target_arch=x64"
export GYP_GENERATOR_FLAGS="output_dir=out_mac"

Note that you can set output_dir to whatever you’d like. It will be created under src/.
Now run the gyp generator script from the source root (<working directory>/src):

webrtc/build/gyp_webrtc

Now to compile, just run ninja on the appropriate target. E.g.

ninja -C out_ios/Debug-iphoneos AppRTCDemo
ninja -C out_ios/Release-iphoneos AppRTCDemo
ninja -C out_sim/Debug-iphonesimulator AppRTCDemo

For interesting targets to build, see the .gyp files in webrtc/webrtc.gyp, webrtc/webrtc_examples.gyp, talk/libjingle.gyp, talk/libjingle_examples.gyp.

Some sample scripts are also available at talk/app/webrtc/objc/README.

Compiling with XCode

Compiling with XCode is not supported! What we do instead is compile using a script that runs ninja from XCode. In order to generate the relevant xcode project, add xcode-ninja to GYP_GENERATORS along with the targets you’re interested in. By using XCode in this manner, we get the build speed of ninja while at the same time getting access to the usual methods of deployment/debugging for iOS.

export GYP_GENERATOR_FLAGS="xcode_project_version=3.2 xcode_ninja_target_pattern=All_iOS xcode_ninja_executable_target_pattern=AppRTCDemo|libjingle_peerconnection_unittest|libjingle_peerconnection_objc_test output_dir=out_ios"

export GYP_GENERATORS="ninja,xcode-ninja"

When running the generator script, you should see an all.ninja.xcworkspace file. You should be able to select the desired target and platform in the XCode usual fashion and build / deploy. Note that you will need to rerun the GYP generator if you want to switch target platforms.

Deploying to device

It’s easiest to deploy to a device using XCode in xcode-ninja mode. Other command line tools exist as well, e.g. ios-deploy.

 在Python中如何实现单例模式。

 在Python中如何实现单例模式。
点评:这个题目在面试中出现的频率*高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性*强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
from functools import wraps
def singleton(cls):
    “””单例类装饰器”””
    instances = {}
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper
@singleton
class President:
    pass
扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有必然联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、代理、上下文环境等服务,它是对设计模式中代理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper函数)通常都会用functools模块中的wraps再加以装饰,这个装饰器*重要的作用是给被装饰的类或函数动态添加一个__wrapped__属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不需要装饰功能的时候,可以通过它来取消装饰器,例如可以使用President = President.__wrapped__来取消对President类做的单例处理。需要提醒大家的是:上面的单例并不是线程安全的,如果要做到线程安全,需要对创建对象的代码进行加锁的处理。在Python中可以使用threading模块的RLock对象来提供锁,可以使用锁对象的acquire和release方法来实现加锁和解锁的操作。当然,更为简便的做法是使用锁对象的with上下文语法来进行隐式的加锁和解锁操作。
方法二:使用元类实现单例模式。
class SingletonMeta(type):
    “””自定义单例元类”””
    def __init__(cls, *args, **kwargs):
        cls.__instance = None
        super().__init__(*args, **kwargs)
    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
        return cls.__instance
class President(metaclass=SingletonMeta):
    pass
扩展:Python是面向对象的编程语言,在面向对象的世界中,一切皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在定义类时,如果没有给一个类指定父类,那么默认的父类是object,如果没有给一个类指定元类,那么默认的元类是type。通过自定义的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__魔术方法,改变了President类的构造器那样。
关于单例模式,在面试中还有可能被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。
题目002:不使用中间变量,交换两个变量a和b的值。
点评:典型的送人头的题目,在其他编程语言中不使用中间变量交换两个变量的值可以使用异或运算,Python中还可以通过内置的字节码指令直接交换两个变量的值。
方法一:
a = a ^ b
b = a ^ b
a = a ^ b
方法二:
a, b = b, a
扩展:需要注意,a, b = b, a这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO指令来支持这个操作,类似的还有ROT_THREE,对于3个以上的元素,如a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis模块的dis函数来反汇编你的Python代码。
题目003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。
点评:这个题目在初中级Python岗位面试的时候经常出现,题目源于《Python Cookbook》这本书*章的第10个问题,有很多面试题其实都是这本书上的原题,所以建议大家有时间的话好好研读一下这本书。
def dedup(items):
    no_dup_items = []
    seen = set()
    for item in items:
        if item not in seen:
            no_dup_items.append(item)
            seen.add(item)
    return no_dup_items
当然,也可以像《Python Cookbook》书上的代码那样,把上面的函数改造成一个生成器。
def dedup(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)
扩展:由于Python中的集合底层使用哈希存储,所以集合的in和not in成员运算在性能上远远优于列表,所以上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable对象,因此上面的代码在列表元素不是hashable对象时会失效,要解决这个问题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable对象的函数。
题目004:假设你使用的是官方的CPython,说出下面代码的运行结果。
点评:下面的程序对实际开发并没有什么意义,但却是CPython中的一个大坑,这道题旨在考察面试者对官方的Python解释器到底了解到什么程度。
a, b, c, d = 1, 1, 1000, 1000
print(a is b, c is d)
def foo():
    e = 1000
    f = 1000
    print(e is f, e is d)
    g = 1
    print(g is a)
foo()
结果:
True False
True False
True
上面代码中a is b的结果是True但c is d的结果是False,这一点的确让人费解。这个结果是因为CPython出于性能优化的考虑,把频繁使用的整数对象用一个叫small_ints的对象池缓存起来造成的。small_ints缓存的整数值被设定为[-5, 256]这个区间,也就是说,如果使用CPython解释器,在任何引用这些整数的地方,都不需要重新创建int对象,而是直接引用缓存池中的对象。如果整数不在该范围内,那么即便两个整数的值相同,它们也是不同的对象。
CPython底层为了进一步提升性能还做了一个设定:对于同一个代码块中值不在small_ints缓存范围之内的整数,如果同一个代码块中已经存在一个值与其相同的整数对象,那么就直接引用该对象,否则创建新的int对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点可以自行证明。
扩展:如果你用PyPy(另一种Python解释器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。
题目005:Lambda函数是什么,举例说明的它的应用场景。
点评:这个题目主要想考察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。因为Lambda函数通常用在高阶函数中,主要的作用是通过传入或返回函数实现代码的解耦合。
Lambda函数也叫匿名函数,它功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写return关键字。Lambda函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。
面试的时候有可能还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现题目要求的功能,例如:用一行代码实现求阶乘的函数,用一行代码实现求*大公约数的函数等。
fac = lambda x: __import__(‘functools’).reduce(int.__mul__, range(1, x + 1), 1)
gcd = lambda x, y: y % x and gcd(y % x, x) or x
Lambda函数其实*为主要的用途是把一个函数传入另一个高阶函数(如Python内置的filter、map等)中来为函数做解耦合,增强函数的灵活性和通用性。下面的例子通过使用filter和map函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这filter和map函数没有跟特定的过滤和映射数据的规则耦合在一起。
items = [12, 5, 7, 10, 8, 19]
items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
print(items)    # [25, 49, 361]
当然,用列表的生成式来实现上面的代码更加简单明了,如下所示。
items = [12, 5, 7, 10, 8, 19]
items = [x ** 2 for x in items if x % 2]
print(items)    # [25, 49, 361]

说说Python中的浅拷贝和深拷贝。

说说Python中的浅拷贝和深拷贝。
点评:这个题目本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种面试题,在回答的时候一定要让你的答案能够超出面试官的预期,这样才能获得更好的印象分。所以回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝,*后还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。深拷贝可能会遇到两个问题:一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。Python通过copy模块中的copy和deepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。
deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,显然我们可以使用pickle模块的dumps和loads来做到,代码如下所示。
import pickle
my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
列表的切片操作[:]相当于实现了列表对象的浅拷贝,而字典的copy方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,首先是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”的对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法其实更加高效,这也就是设计模式中的原型模式。我们可以通过元类的方式来实现原型模式,代码如下所示。
import copy
class PrototypeMeta(type):
    “””实现原型模式的元类”””
    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为对象绑定clone方法来实现对象拷贝
        cls.clone = lambda self, is_deep=True: \
            copy.deepcopy(self) if is_deep else copy.copy(self)
class Person(metaclass=PrototypeMeta):
    pass
p1 = Person()
p2 = p1.clone()                 # 深拷贝
p3 = p1.clone(is_deep=False)    # 浅拷贝
题目007:Python是如何实现内存管理的?
点评:当面试官问到这个问题的时候,一个展示自己的机会就摆在面前了。你要先反问面试官:“你说的是官方的CPython解释器吗?”。这个反问可以展示出你了解过Python解释器的不同的实现版本,而且你也知道面试官想问的是CPython。当然,很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以,千万不要觉得面试官一定比你强,怀揣着这份自信可以让你更好的完成面试。
Python提供了自动化的内存管理,也就是说内存空间的分配与释放都是由Python解释器在运行时自动进行的,自动管理内存功能*大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
引用计数:对于CPython解释器来说,Python中的每一个对象其实就是PyObject结构体,它的内部有一个名为ob_refcnt 的引用计数器成员变量。程序在运行的过程中ob_refcnt的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被释放掉。
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
以下情况会导致引用计数加1:
对象被创建
对象被引用
对象作为参数传入到一个函数中
对象作为元素存储到一个容器中
以下情况会导致引用计数减1:
用del语句显示删除对象引用
对象引用被重新赋值其他对象
一个对象离开它所在的作用域
持有该对象的容器自身被销毁
持有该对象的容器删除该对象
可以通过sys模块的getrefcount函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此需要其他的垃圾回收算法对其进行补充。
标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着需要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count变量外,还有一个gc_ref变量,这个gc_ref是ref_count的一个副本,所以初始值为ref_count的大小。执行垃圾回收时,首先遍历链表A中的节点,并且将当前对象所引用的所有对象的gc_ref减1,这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,如果节点的gc_ref值为0,那么这个对象就被标记为“暂时不可达” (
GC_TENTATIVELY_UNREACHABLE) 并被移动到链表B中;如果节点的gc_ref不为0,那么这个对象就会被标记为“可达“ (GC_REACHABLE),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是需要释放内存的节点。
分代回收:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0、1、2,每一个新生对象都在第0代中,如果该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1代中,存在于第1代的对象将较少的被垃圾回收扫描到;如果在对第1代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc模块的get_threshold函数来获得,该函数返回一个三元组,分别表示多少次内存分配操作后会执行0代垃圾回收,多少次0代垃圾回收后会执行1代垃圾回收,多少次1代垃圾回收后会执行2代垃圾回收。需要说明的是,如果执行一次2代垃圾回收,那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值,可以通过gc模块的set_threshold函数来做到。
题目008:说一下你对Python中迭代器和生成器的理解。
点评:很多人面试者都会写迭代器和生成器,但是却无法准确的解释什么是迭代器和生成器。如果你也有同样的困惑,可以参考下面的回答。
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于定义协议或表示约定的关键字,像interface、protocol这些单词并不在Python语言的关键字列表中。Python语言通过魔法方法来表示约定,也就是我们所说的协议,而__next__和__iter__这两个魔法方法就代表了迭代器协议。生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。
面试中经常会让面试者写生成斐波那契数列的迭代器,下面给出参考代码,其他的迭代器可以如法炮制。
class Fib(object):
    def __init__(self, num):
        self.num = num
        self.a, self.b = 0, 1
        self.idx = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.idx < self.num:
            self.a, self.b = self.b, self.a + self.b
            self.idx += 1
            return self.a
        raise StopIteration()
如果用生成器的语法来改写上面的代码,代码会简单优雅很多。
def fib(num):
    a, b = 0, 1
    for _ in range(num):
        a, b = b, a + b
        yield a
可以通过for-in循环从迭代器对象中取出值,也可以使用next函数取出迭代器对象中的下一个值。
题目009:正则表达式的match方法和search方法有什么区别?
点评:正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用re模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过re模块的compile函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。如果一个正则表达式被频繁的使用,我们推荐后面这种方式,它会减少频繁编译同一个正则表达式所造成的开销。
match方法是从字符串的起始位置进行正则表达式匹配,返回Match对象或None。search方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
题目010:下面这段代码的执行结果是什么。
def multiply():
    return [lambda x: i * x for i in range(4)]
print([m(100) for m in multiply()])
运行结果:
[300, 300, 300, 300]
上面代码的运行结果很容易被误判为[0, 100, 200, 300]。首先需要注意的是multiply函数用生成式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i的结果。需要注意的是这里有闭包(closure)现象,multiply函数中的局部变量i的生命周期被延展了,由于i*终的值是3,所以通过m(100)调列表中的Lambda函数时会返回300,而且4个调用都是如此。
如果想得到[0, 100, 200, 300]这个结果,可以按照下面几种方式来修改multiply函数。
方法一:使用生成器,让函数获得i的当前值。
def multiply():
    return (lambda x: i * x for i in range(4))
print([m(100) for m in multiply()])
或者
def multiply():
    for i in range(4):
        yield lambda x: x * i
print([m(100) for m in multiply()])
方法二:使用偏函数,彻底避开闭包现象。
from functools import partial
from operator import __mul__
def multiply():
    return [partial(__mul__, i) for i in range(4)]
print([m(100) for m in multiply()])
温馨提示:Python面试宝典会持续更新,从基础到项目实战的内容都会慢慢覆盖到。虽然每天只更新5个题目,但是每道题扩散出的信息量还是比较大的,希望对找工作的小伙伴所有帮助。

Python中为什么没有函数重载?

Python中为什么没有函数重载?
点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。
首先Python是解释型语言,函数重载现象通常出现在编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。
题目012:用Python代码实现Python内置函数max。
点评:这个题目看似简单,但实际上还是比较考察面试者的功底。因为Python内置的max函数既可以传入可迭代对象找出*大,又可以传入两个或多个参数找出*大;*为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。
下面的代码仅供参考:
def my_max(*args, key=None, default=None):
    “””
    获取可迭代对象中*大的元素或两个及以上实参中*大的元素
    :param args: 一个可迭代对象或多个元素
    :param key: 提取用于元素比较的特征值的函数,默认为None
    :param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常
    :return: 返回可迭代对象或多个元素中的*大元素
    “””
    if len(args) == 1 and len(args[0]) == 0:
        if default:
            return default
        else:
            raise ValueError(‘max() arg is an empty sequence’)
    items = args[0] if len(args) == 1 else args
    max_elem, max_value = items[0], items[0]
    if key:
        max_value = key(max_value)
    for item in items:
        value = item
        if key:
            value = key(item)
        if value > max_value:
            max_elem, max_value = item, value
    return max_elem
题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
点评:送人头的题目,不解释。
def count_letters(items):
    result = {}
    for item in items:
        if isinstance(item, (int, float)):
            result[item] = result.get(item, 0) + 1
    return result
也可以直接使用Python标准库中collections模块的Counter类来解决这个问题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。
from collections import Counter
def count_letters(items):
    counter = Counter(items)
    return {key: value for key, value in counter.items() \
            if isinstance(key, (int, float))}
题目014:使用Python代码实现遍历一个文件夹的操作。
Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个生成器。可以通过这个生成器来获得文件夹下所有的文件和文件夹。
import os
g = os.walk(‘/Users/Hao/Downloads/’)
for path, dir_list, file_list in g:
    for dir_name in dir_list:
        print(os.path.join(path, dir_name))
    for file_name in file_list:
        print(os.path.join(path, file_name))
说明:os.path模块提供了很多进行路径操作的工具函数,在项目开发中也是经常会用到的。 如果题目明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
题目015:现有2元、3元、5元共三种面额的货币,如果需要找零99元,一共有多少种找零的方式?
点评:还有一个非常类似的题目:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个题目的思路是一样,如果用递归函数来写的话非常简单。
from functools import lru_cache
@lru_cache()
def change_money(total):
    if total == 0:
        return 1
    if total < 0:
        return 0
    return change_money(total – 2) + change_money(total – 3) + change_money(total – 5)
说明:在上面的代码中,我们用lru_cache装饰器装饰了递归函数change_money,如果不做这个优化,上面代码的渐近时间复杂度将会是 ,而如果参数total的值是99,这个运算量是非常巨大的。lru_cache装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。
温馨提示:Python面试宝典会持续更新,从基础到项目实战的内容都会慢慢覆盖到。虽然每天只更新5个题目,但是每道题扩散出的信息量还是比较大的,希望对找工作的小伙伴所有帮助。

Python中格式化字符串更酷的方式

Python中格式化字符串更酷的方式
Python中格式化字符串更酷的方式
在 Python 中,大家都习惯使用 %s 或 format 来格式化字符串,在 Python 3.6 中,有了一个新的选择 f-string。
使用对比
我们先来看下 Python 中已经存在的这几种格式化字符串的使用比较。
# %s
username = ‘tom’
action = ‘payment’
message = ‘User %s has logged in and did an action %s.’ % (username, action)
print(message)
# format
username = ‘tom’
action = ‘payment’
message = ‘User {} has logged in and did an action {}.’.format(username, action)
print(message)
# f-string
username = ‘tom’
action = ‘payment’
message = f’User {user} has logged in and did an action {action}.’
print(message)
f”{2 * 3}”
# 6
comedian = {‘name’: ‘Tom’, ‘age’: 20}
f”The comedian is {comedian[‘name’]}, aged {comedian[‘age’]}.”
# ‘The comedian is Tom, aged 20.’
相比于常见的字符串格式符 %s 或 format 方法,f-strings 直接在占位符中插入变量显得更加方便,也更好理解。
方便的转换器
f-string 是当前*佳的拼接字符串的形式,拥有更强大的功能,我们再来看一下 f-string 的结构。
f ‘ <text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> … ‘
其中 ‘!s’ 调用表达式上的 str(),’!r’ 调用表达式上的 repr(),’!a’ 调用表达式上的 ascii()。大家可以看看下面的例子。
class Person:
    def __init__(self, name, nickname):
        self.name = name
        self.nickname = nickame
    def __str__(self):
        return self.name
    def __repr__(self):
        return self.nickname
person = Person(‘王大锤’, ‘Wang Gangdan’)
print(f'{person!s}’)
print(f'{person!r}’)
print(f'{person.name!a}’)
print(f'{person.nickname!a}’)
性能
f-string 除了提供强大的格式化功能之外,还是这三种格式化方式中性能*高的实现。
>>> import timeit
>>> timeit.timeit(“””name = “Eric”
… age = 74
… ‘%s is %s.’ % (name, age)”””, number = 10000)
0.003324444866599663
>>> timeit.timeit(“””name = “Eric”
… age = 74
… ‘{} is {}.’.format(name, age)”””, number = 10000)
0.004242089427570761
>>> timeit.timeit(“””name = “Eric”
… age = 74
… f'{name} is {age}.'”””, number = 10000)
0.0024820892040722242
坦白的说,f-string 就是字符串 format 方法一个语法糖,但它进一步简化了格式化字符串的操作并带来了性能上的提升。使用 Python 3.6+ 的同学,使用 f-string 来代替你的 format 函数,以获得更强大的功能和更高的性能。

爬虫到底违法吗?你离违法还有多远?

爬虫到底违法吗?你离违法还有多远?
*近,国家依法查处了部分编写爬虫程序,盗取其他公司数据的不良企业。一时间风声鹤唳,关于爬虫程序是否违法的讨论遍布程序员圈子。那么到底编写爬虫程序是否违法呢?
其爬虫下载数据,一般而言都不违法,因为爬虫爬取的数据同行也是网站上用户打开页面能够看到的数据,但是如果符合下列条件的网站进行强行数据采集时,会具有法律风险。
采集的站点有声明禁止爬虫采集时。
2. 网站通过Robots协议拒*采集时。
Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol)。网站通过Robots协议告诉爬虫哪些页面可以抓取,哪些页面不能抓取。如果想查看一个网站的Robots协议,可以打开位于网站根目录下的robots.txt文件即可,例如:https://www.jd.com/robots.txt。
如果因为爬虫的问题产生官司,通常如果对方能够举证你的爬虫有破坏动产(如服务器)的行为,那么基本上打官司你会败诉并要求做出赔偿。
爬虫二十问
以下是业界某位大神关于爬虫相关问题的回答。
非爬虫方向的技术转行做爬虫是否可行?
答:可行,而且有一定的基础会很容易上手,至于深入的部分就看自己了。
2. 非技术转行做爬虫是否可行?
答:可行,但我认为较难,因为爬虫做深了以后是需要你了解各种相关领域知识的,而你现在对这些领域的东西一无所知,甚至可能连编程都还不知道怎么开始,起点会比有基础的人低很多。
3. 爬虫工作日常如何?加班多不多?
答:这个得看公司的,有些公司搞得都是些天天更新反爬的平台(比如工商信息相关的),那基本就是得一直盯着看会不会出问题,一不小心就会要加班。
4. 爬虫对于学生党的用处体现在哪些地方?
答:这个问题看个人,因为爬虫技术可用的地方太多了,没法一个一个地都拿出来说。比如你想搞个自动签到的工具,这其实本质上就是爬虫;比如你想搞个自动回复设定内容的机器人,这其实本质上也是爬虫。
5. 学到什么程度才能入职爬虫工程师?
答:我觉得首先发请求不用说了吧?抓包工具的使用也不用说了吧?熟练掌握XPath、正则表达式这种解析工具也是基本的,然后JSON之类的传输格式至少要了解过长啥样吧,再就是JS逆向总得会一点吧(从只改变量名函数名混淆级别的代码中找出加密参数生成部分的程度)。差不多会这些以后,再自己做几个项目,应聘个初级爬虫工程师没啥问题。
6. 如何成为一名优秀的爬虫工程师?
答:垂直爬虫做到后面本质上就是逆向,你需要有良好的逆向思维方式,并且对一些安全领域的骚东西也有一定的了解,这样你才能游刃有余地处理高难度的反爬。
7. 学爬虫的学习路线?
答:有一些Python基础就可以做爬虫了,主要是数据获取、数据解析、数据预处理、数据持久化这方面的东西,然后是一些三方库和框架,如Scrapy、Selenium WebDriver等。
8. 大约学习并从事爬虫几年才可以达到一个不错的高度?
答:这个问题也很看个人,我觉得主要看有没有需求逼迫成长吧。之前招人的时候,很多三年经验的也就比入门水平稍微好一点,他们在工作时遇到的难点几乎全是依靠自动化测试工具解决的,对逆向水平毫无增长。所以建议还是多依靠逆向手段去解决问题,成长速度会很快。
9. 薪资方面如何,在几年内可以达到15K?
答:同上,标15K及以上的招聘还是挺多的,看看招聘需求就知道大概到什么程度了。
10. 面试爬虫哪些技能点是加分项?
答:丰富且有深度的逆向经验、熟悉通信协议底层实现、有过哪些骚操作经历等,但主要还是逆向经验和反爬方面的经验。
11. 作为一名爬虫工程师,对该岗位的前景如何看待?
答:未来主要内容在App上的平台应该会越来越多,难度也会越来越高,所以对于爬虫工程师的逆向水平要求会越来越高,只会简单逆向甚至不会逆向的人找工作会越来越难。
12. 爬虫和数据挖掘是一样的吗?
答:不一样,爬虫只是将数据取回来,具体怎么分析才是数据挖掘的事情。
13. 爬虫是否和黑客差不多?
答:差很多,与上个问题类似,只不过“黑客”这个词太宽泛了,黑客也是有具体方向的。
14.千奇百怪的验证码只能对接打码平台吗?有啥其他办法?
答:自己破呗,逆向+机器学习。
15. 如何爬x平台?
答:涉及法律问题,这种针对某个平台的东西是不能细说的。
16. 爬虫违法吗?如何避免过线导致的违法?怎么规避法律风险?
答:算是擦边球吧,其实你即使遵守规则去爬别人的网站,只要人家想搞你,还是可以让你做的事情变成违法的。所以建议不要做太过分的事情,毕竟狗急了也会跳墙。
还有就是不要为一些明显是做灰黑产的人/公司写代码,一旦他们出事了,你也会被牵连。
知乎上之前那个很火的被抓了的人,从回答内容中来看其实就是做打码平台的那个微凉,他这一个平台据说赚了至少千万,主要应该是提供给做黑产的人使用了,这种情况下被抓是迟早的事。*好的避免违法的办法就是明显觉得不太好的事情就不要去碰,基本就不会有啥问题。
17. 如何有目的地爬取到真正想要的数据?
答:让需要数据的人提需求,如果你自己就是那个需要数据的人,那就去做市场调研,看看你需要的数据在哪里能找到。
18. 反爬虫*先进的技术是什么?*有效的技术是什么?
答:*先进的技术其实就是使用在PC平台上已经玩烂的各种反破解技术将行为监测点(设备指纹、用户操作等)隐藏起来,然后传给服务端做行为识别,如果操作非人类或者缺少某些东西就触发风控。
*有效的技术其实不是技术而是方法,这个方法就是账号收费,将你的数据变成需要花多少钱才能看到这样子的,就能做到啥高端技术都不用上、轻松提高爬虫方的获取数据成本的效果,当然这也需要结合良好的产品设计,否则普通用户的体验会很差。
19. 请问爬虫在x领域有哪些应用?
答:这个应该是对应领域的人自己思考一下自己拿到那些公开数据究竟可以做什么。
20. 需要大量账号的平台成本过高该怎么办?
答:人家就是依靠这种方式来提高你成本的,你如果觉得成本过高要么放弃要么换一条路线获取数据。

用 pprint 代替 print 更友好的打印调试信息

用 pprint 代替 print 更友好的打印调试信息
pprint 是 “pretty printer” 的简写,“pretty” 的含义是 “漂亮的、美观的”,因此 pprint 的含义便是:漂亮的打印。
这是个相当简单却有用的模块,主要用于打印复杂的数据结构对象,例如多层嵌套的列表、元组和字典等。
先看看 print() 打印的一个例子:
mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]
print(mylist)
[‘Beautiful is better than ugly.’, ‘Explicit is better than implicit.’, ‘Simple is better than complex.’, ‘Complex is better than complicated.’]
这是一个简单的例子,全部打印在一行里。如果对象中的元素是多层嵌套的内容(例如复杂的 字典 数据),再打印出那肯定是一团糟的,不好阅读。
使用 pprint 模块的 pprint() 替代 print(),可以解决如下痛点:
设置合适的行宽度,作适当的换行
设置打印的缩进、层级,进行格式化打印
判断对象中是否有无限循环,并优化打印内容
基本使用
pprint(object, stream=None, indent=1, width=80, depth=None, *,compact=False)
默认的行宽度参数为 80,当打印的字符小于 80 时,pprint() 基本上等同于内置函数 print(),当字符超出时,它会作美化,进行格式化输出。
import pprint
mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]
pprint.pprint(mylist)
# 超出80字符,打印的元素是换行的
[‘Beautiful is better than ugly.’,
 ‘Explicit is better than implicit.’,
 ‘Simple is better than complex.’,
 ‘Complex is better than complicated.’]
设置缩进
pprint.pprint(mylist, indent=4)
[   ‘Beautiful is better than ugly.’,
    ‘Explicit is better than implicit.’,
    ‘Simple is better than complex.’,
    ‘Complex is better than complicated.’]
设置打印行宽
mydict = {‘students’: [{‘name’:’Tom’, ‘age’: 18},{‘name’:’Jerry’, ‘age’: 19}]}
pprint.pprint(mydict)
# 正常打印
{‘students’: [{‘age’: 18, ‘name’: ‘Tom’}, {‘age’: 19, ‘name’: ‘Jerry’}]}
pprint.pprint(mydict, width=20)
# 行宽为 20
{‘students’: [{‘age’: 18,
 ‘name’: ‘Tom’},
              {‘age’: 19,
 ‘name’: ‘Jerry’}]}
pprint.pprint(mydict, width=70)
# 行宽为 70
{‘students’: [{‘age’: 18, ‘name’: ‘Tom’},
              {‘age’: 19, ‘name’: ‘Jerry’}]}
设置打印层级
newlist = [1, [2, [3, [4, [5]]]]]
pprint.pprint(newlist, depth=3)
# 超出的层级会用 … 表示
[1, [2, [3, […]]]]
用 pprint 替换 print
import pprint
print = pprint.pprint
mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]
print(mylist)
[‘Beautiful is better than ugly.’,
 ‘Explicit is better than implicit.’,
 ‘Simple is better than complex.’,
 ‘Complex is better than complicated.’]

Linux系统下如何运行.sh文件

在Linux系统下运行.sh文件有两种方法,比如我在root目录下有个datelog.sh文件

*种(这种办法需要用chmod使得文件具备执行条件(x): chmod u+x datelog.sh):

1、在任何路径下,输入该文件的*对路径/root/datelog.sh就可执行该文件(当然要在权限允许情况下)

%title插图%num

2、cd到datelog.sh文件的目录下,然后执行./datelog.sh

%title插图%num

第二种(这种办法不需要文件具备可执行的权限也可运行):

1、在该文件路径下sh加上文件名字即可,sh datelog.sh

%title插图%num

2、在任意路径下,sh 加上文件路径及文件名称:sh /root/ datelog.sh

%title插图%num

adb shell 执行sh脚本_Bash技巧:一个可以通过命令简写执行对应命令的Shell脚本

本篇文章介绍一个在 Linux 系统上可以通过命令简写执行对应命令的 shell 脚本。

假设这个 shell 脚本的名称为 tinyshell.sh。

在 Linux 下进行项目开发,经常会用到一些调试开发命令。

这些命令可能比较长,需要输入多个字符。

例如,Android 系统抓取全部 log 并包含 log 时间的命令是 adb logcat -b all -v threadtime。

抓取 log 是调试开发非常常见的操作,这个命令又很长,输入起来不方便。

为了简化输入,可以配置一些命令简写来对应比较长命令。

例如,配置 ala 对应 adb logcat -b all -v threadtime。

把 als 作为参数传递给当前的 tinyshell.sh 脚本,会执行该命令简写对应的命令。

这样只需要输入比较少的字符,就能执行比较长的命令。

实际上,这个功能类似于 bash 的 alias 别名,只是将这些别名统一放到该脚本来处理。

可以把 tinyshell.sh 脚本作为学习 shell 脚本的参考例子,独立维护更新,根据需要扩充更多的功能。

配置命令简写
如之前说明,可以用 ala 表示 adb logcat -b all -v threadtime 这个命令。

这个 ala 称之为 “命令简写”。

命令简写使用一些简单的字符来表示特定的命令。

可以在命令简写后面动态提供命令的参数。

为了方便动态添加、删除、查询命令简写,可以把这些命令简写保存在一个配置文件里面。

在执行 tinyshell.sh 脚本时,会读取配置文件内容,获取到各个配置项的值。

配置项的基本格式是:命令简写|命令内容

每个配置项占据一行。每一行默认以*个竖线 ‘|’ 隔开命令简写和命令内容。

一个参考的配置文件内容如下所示:

ll|ls –color=auto -lala|adb logcat -b all -v threadtimegl|git loggp|git pull –stat –no-tags $(git remote) $(git rev-parse –abbrev-ref HEAD)
这里配置的命令内容可以是系统支持的任意命令。

解析配置文件时,需要用到之前文章介绍的 parsecfg.sh 脚本。

要获取 parsecfg.sh 脚本的代码,可以查看之前的文章。

后面会提供具体测试的例子,可供参考。

脚本代码
列出 tinyshell.sh 脚本的具体代码如下所示。

在这个代码中,对大部分关键代码都提供了详细的注释,方便阅读。

这篇文章的后面也会对一些关键点进行说明,有助理解。

#!/bin/bash -i# 使用 bash 的 -i 选项,让该脚本在交互模式下运行.# 实现一个小型的 shell. 支持内置命令、命令简写. 如果提供这两种命令之外# 的其他命令,会尝试在 bash 中直接执行所给命令,可以执行系统支持的命令.# 命令简写指的是一些简单的字符,会对应一串实际要执行的命令.只要输入命令# 简写就可以执行对应的命令,减少需要输入的字符.命令简写在配置文件中配置.# 下面变量指定默认解析的配置文件名.该文件配置了命令简写、以及对应的命令.# 这个 tinyshellcmds.txt 文件需要预先配置好,放到指定路径的目录底下.# 直接修改这个配置文件,就可以动态添加或删除命令简写.不需要修改脚本代码.SHORT_COMMANDS=”${HOME}/.liconfig/tinyshellcmds.txt”# PARSECFG_filepath 是 parsecfg.sh 脚本里面的变量. 如果这个变量为空,# 说明还没有打开过配置文件,进入下面的分支打开默认的配置文件.if [ -z “$PARSECFG_filepath” ]; then # 导入解析配置文件的脚本,以便调用该脚本的函数来解析配置文件. source parsecfg.sh # 调用 parsecfg.sh 里面的 open_config_file() 函数解析配置文件. # 如果配置文件不存在,会返回 1,经过’!’操作符取反为 0,会退出执行. if ! open_config_file “$SHORT_COMMANDS”; then exit 2 fifi# 下面变量指定 tiny shell 的提示字符串.PROMPT=”TinySh>>> “# 下面使用 basename 命令来提取出脚本的文件名,去掉目录路径部分.show_help(){printf “USAGE $(basename $0) [option] [shortcmd [argument1 … [argumentn]]]OPTIONS option: 可选的选项参数. 支持的选项参数描述如下: -h: 打印这个帮助信息. -l: 打印配置文件本身的内容,会列出配置的命令简写和对应的命令. -v: 以键值对的方式列出命令简写和对应的命令. -i: 在配置文件中查找指定内容.后面跟着一个参数,指定要查找的内容. -e: 使用 vim 打开脚本的配置文件,以供编辑. -a: 新增或修改一个命令简写和对应的命令.后面跟着一个参数,用 单引号括起来,以指定命令简写和命令. 格式为: 命令简写|命令. 例如 -a ‘p|git pull’,如果p简写不存在则新增它,否则修改它. -d: 从脚本配置文件中删除一个命令简写和对应的命令.后面跟着一个 参数,指定要删除的命令简写.例如 -d s,会删除命令简写为 s 的行. shortcmd: 可选选项. 指定要直接执行的命令简写. 提供命令简写参数,不会进入 tiny shell. argument1 … argumentn: 可选选项. 指定该命令简写的参数. 命令简写对应一个命令,支持动态提供参数.NOTE 如果没有提供任何参数,默认会进入 tiny shell 解释器. 在 tiny shell 中 接收用户输入并执行对应的命令.直到读取到EOF、或者执行quit命令才会退出.”}# tiny shell 的内置命令数组. 这是一个关联数组. 数组元素的# 键名是内置命令名. 数组元素的键值是响应内置命令的函数名.declare -A BUILTIN_COMMAND=( [help]=”builtin_command_help” [quit]=”builtin_command_quit” [debug]=”builtin_command_debug” )# bash 的 help 命令默认会打印内置命令列表. 这里仿照这个行为,# 让 help 内置命令打印内置命令列表、以及配置文件包含的命令简写.builtin_command_help(){printf “下面列出 Tiny Shell 支持的内置命令列表和配置的命令简写列表.输入内置命令名或命令简写,会执行对应的命令.也可以输入系统自身支持的命令,会在 bash 中执行所给命令.内置命令列表: debug: 所给*个参数指定打开、或关闭调试功能. 其参数说明如下: on: 打开调试功能,会执行 bash 的 set -x 命令 off: 关闭调试功能,会执行 bash 的 set +x 命令 help: 打印当前帮助信息. quit: 退出当前 Tiny Shell.命令简写列表:” # 调用 parsecfg.sh 的 handle_config_option -v 打印命令简写列表 handle_config_option -v}# quit 内置命令. 执行该命令会退出整个脚本,从而退出当前 tiny shell.builtin_command_quit(){ exit}# debug 内置命令. 所给*个参数指定打开、或关闭调试功能.# debug on: 打开调试功能,会执行 bash 的 set -x 命令# debug off: 关闭调试功能,会执行 bash 的 set +x 命令builtin_command_debug(){ if [ $# -ne 1 ]; then echo “Usage: debug on/off” return 1 fi if [ “$1” == “on” ]; then set -x elif [ “$1” == “off” ]; then set +x else echo -e “Unknown argument: $1Usage: debug on/off” fi return}# 处理 tiny shell 内置命令.对于内置命令,会调用对应函数进行处理.# 该函数的返回值表示所给命令名是否内置命令.# 返回 0, 表示是内置命令. 返回 1, 表示不是内置命令.execute_builtin_command(){ # 在传递过来的参数中,*个参数是命令名,剩余的参数是该命令的参数. local cmdname=”$1″ # 从 BUILTIN_COMMAND 数组中获取所给命令对应的处理函数. # 如果所给命令不是内置命令,会获取为空. local cmdfunc=”${BUILTIN_COMMAND[“${cmdname}”]}” if [ -n “${cmdfunc}” ]; then # 将位置参数左移一位,移除命令名,剩下的就是该命令的参数. shift 1 ${cmdfunc} “$@” # 无论执行内置命令是否报错,都会返回 0,表示该命令是内置命令. return 0 else return 1 fi}# 处理 tiny shell 的命令简写.在所解析的配置文件中包含了支持的命令简写.# 该函数的返回值表示所给命令名是否命令简写.# 返回 0, 表示是命令简写. 返回 1, 表示不是命令简写.execute_short_command(){ # 判断所给的参数是否对应配置文件中的某个键名.如果是,将取出键值. local key=”$1″ # 从配置文件中获取所给命令简写对应要执行的命令 local cmd_value=$(get_value_by_key “${key}”) if test -n “${cmd_value}”; then # 将位置参数左移一位,移除命令简写,剩下的就是命令的参数. shift 1 # 下面要用 “$*” 来把所有参数组合成一个参数,再跟命令内容一起传入 # bach -c,确保 bash -c 把命令内容和所有参数都当成要执行的命令 bash -c “$cmd_value $*” # 打印命令简写,以及该简写对应的命令,以便查看具体执行了什么命令. # 先执行命令,再打印命令内容. 由于有些命令的输出很多,先打印命令 # 内容的话,需要拉动终端滚动条,才能找到打印的命令内容,不便于查看. echo -e “e[33m命令简写: ${key}. 命令: ${cmd_value} $*e[0m” return 0 else # 如果获取到的键值为空,表示所给键名不是有效的命令简写,返回 1 return 1 fi}# 处理所给的内容.这个内容可能是内置命令,命令简写,或者命令本身.handle_input_command(){ # 所给参数是要执行的命令名、以及命令参数. 如果命令名是配置的 # 命令简写,会把该命令简写替换成对应的命令,再进行对应的命令. local inputcmd=”$@” # if 语句可以直接判断命令返回值是否为 0,并不是只能搭配 [ 命令使用. # 注意: 由于有的 tiny shell 内置命令接收参数,下面的 ${cmd_line} # 不能用双引号括起来,否则多个参数会被当成一个参数. if execute_builtin_command ${inputcmd}; then # 先调用 execute_builtin_command 函数处理内置命令.如果所给 # 命令是内置命令,则调用对应的函数进行处理,且不再往下执行. return 0 elif execute_short_command ${inputcmd}; then # 调用 execute_short_command 函数处理命令简写. return 0 else # 对于 tiny shell 不能执行的命令,尝试用 bash -c 在 bash 中执行. bash -c “${inputcmd}” # 当 return 命令不加具体状态码时,它会返回上一条执行命令的状态码. return fi}# SIGINT 信号的处理函数.目前不做特殊处理,只是想在输入CTRL-C后,不会终止# 当前 tiny shell. 输入 CTRL-C 还是可以终止 tiny shell 启动的子 shell.sigint_handler(){ # 当输入 CTRL-C 后,终端只显示”^C”,但是不会自动换行,需要输入回车才会 # 换行,并重新输出提示字符串. 而在交互式Bash中,输入”^C”后,就会自动回 # 车,并输出提示字符串.这里模仿这个行为,先输出一个回车,再输出提示符. printf “${PROMPT}”}# 启动 tiny shell 解释器. 从标准输入不停读取、并执行所给命令.直到# 使用 CTRL-D 输入 EOF 为止, 或者输入 quit 命令退出当前解释器.start_tinyshell(){ # 执行 python 命令,默认会打印 python 版本号和一句帮助提示. # 这里仿照这个行为,打印 tiny shel 版本号和一句帮助提示. echo -e “Tiny shell 1.0.0Type ‘help’ for more information.” # 捕获SIGINT信号,以便输入 CTRL-C 后,不会退出当前的 tiny shell. # 注意: 由于子shell会继承父shell所忽略的信号,所以不能将 SIGINT 信号 # 设成忽略,而是要指定一个处理函数. 当前 shell 所捕获的信号不会被 # 子 shell 继承. 所以子 shell 还是可以被 CTRL-C 终止. 即,指定信号处理 # 函数后,当前 tiny shell 不会被CTRL-C终止.但是当前 tiny shell 执行的 # 命令会运行在子 shell 下,可以用 CTRL-C 终止运行在子 shell 下的命令. # 查看 man bash 对子 shell 的信号继承关系说明如下: # traps caught by the shell are reset to the values inherited from # the shell’s parent, and traps ignored by the shell are ignored trap “sigint_handler” SIGINT # 如果不使用 -e 选项,输入上光标键, read 会读取到 “^[[A”;输入下光标键, # read 会读取到 “^[[B”.而使用 -e 选项后,输入上下光标键,不会读取到乱码, # 但是在子shell中,也不会返回历史命令.因为shell脚本是在非交互模式下执行. # 可以使用 bash 的 -i 选项让脚本在交互模式下运行,例如: “#/bin/bash -i” while read -ep “${PROMPT}” input; do # 传递参数给函数时,参数要用双引号括起来,避免参数带有空格时,会拆分 # 成多个参数. 当输入CTRL-C时, tiny shell 捕获了这个信号,不会退出 # 当前的 tiny shell.但是read命令会被中断,此时读取到的 input 为空. # 不需要对空行做处理,所以下面先判断 input 变量值是否为空. if [ -n “${input}” ]; then handle_input_command “${input}” # 执行 history -s 命令把所给的参数添加到当前历史记录中.后续 # 通过上下光标键获取历史命令,就可以获取到新添加的命令.这个只 # 影响当前 tiny shell 的历史记录,不会写入外部shell的历史记录. history -s “${input}” fi done # 输出一个换行.当用户输入CTRL-D结束执行后,需要换行显示原先的终端提示符. echo}# 循环调用 getopts 命令处理选项参数.while getopts “hlvi:ea:d:” opt; do # 调用parsecfg.sh脚本处理选项的函数来处理 “lvi:ea:d:” 这几个选项. # 如果处理成功,就直接继续读取下一个选项,不再往下处理. # handle_config_option()函数要求传入的选项以’-‘开头,而getopts命令 # 返回的选项不带有’-‘,所以下面在 ${opt} 前面加上一个 ‘-‘. handle_config_option “-${opt}” “${OPTARG}” if [ $? -ne 127 ]; then continue fi case “$opt” in h) show_help ;; ?) echo “出错: 异常选项,请使用 -h 选项查看脚本的帮助说明.” ;; esacdone# $# 大于0,说明提供了命令参数. $# 等于OPTIND减去1,说明传入的参数都# 是以 ‘-‘ 开头的选项参数. 此时,直接结束执行,不需要再往下处理.# 下面的 -a 表示两个表达式都为真时才为真.表达式之间不要加小括号.# Shell里面的小括号有特殊含义,跟C语言的小括号有些区别,加上会有问题.if [ $# -gt 0 -a $# -eq $((OPTIND-1)) ]; then exit 0fiif [ $# -eq 0 ]; then # 当不带任何参数时,默认启用 tiny shell. start_tinyshellelse # 左移所给的命令参数,去掉已处理过的选项参数,只剩下非选项参数. shift $((OPTIND-1)) # 执行脚本时,如果提供了非选项参数,那么*个参数认为是命令简写, # 需要执行该命令简写对应的命令. *个参数之后的所有参数认为是 # 命令的参数. 即,可以在命令简写之后提供参数来动态指定一些操作. execute_short_command “$@”fiexit
代码关键点说明
使用 trap 命令捕获信号
在 bash 中,可以使用 trap 命令捕获信号,并指定信号处理函数。

捕获信号后,可以避免收到某个信号终止脚本执行。

当前 tinyshell.sh 脚本使用 trap 命令捕获 SIGINT 信号。

也就是 CTRL-C 键所发送的信号,避免按 CTRL-C 键会退出当前 tiny shell。

要注意的是,不能设置成忽略 SIGINT 信号。

在 bash 中,父 shell 所忽略的信号,也会被子 shell 所忽略。

除了内置命令之外,当前 tiny shell 所执行的命令运行在子 shell 下。

如果设置成忽略 SIGINT 信号,那么子 shell 也会忽略这个信号。

那么就不能用 CTRL-C 来终止子 shell 命令的执行。

例如,Android 系统的 adb logcat 命令会不停打印 log,需要按 CTRL-C 来终止。

此时,在 tiny shell 里面按 CTRL-C 就不能终止 adb logcat 的执行。

父 shell 所捕获的信号,子 shell 不会继承父 shell 所捕获的信号。

子 shell 会继承父 shell 的父进程的信号状态。

父 shell 的父进程一般是外部 bash shell 进程。

而 bash shell 进程默认捕获SIGINT并终止前台进程。

即,虽然当前 tiny shell 捕获了 SIGINT 信号,但是子 shell 并没有捕获该信号。

可以在 tiny shell 使用 CTRL-C 来终止子 shell 命令的执行。

使用 history -s 命令添加历史记录
在 tiny shell 执行命令后,默认不能用上下光标键查找到 tiny shell 自身执行的历史命令。

为了可以查找到 tiny shell 自身执行的历史命令,使用 history -s 命令添加命令到当前 shell 的历史记录。

这个命令只会影响当前 shell 的历史记录。

退出当前 shell 后,在外部 shell 还是看不到 tiny shell 所执行的命令。

由于这个 tiny shell 主要是为了执行命令简写。

这些命令简写只有 tiny shell 自身支持,不需要添加到 bash shell 的历史记录。

如果想要命令历史信息添加到外部 shell 的历史记录,可以在退出 tinyshell.sh 脚本之前,执行 history -w ~/.bash_history 命令把历史记录写入到 bash 自身的历史记录文件。

测试例子
把 tinyshell.sh 脚本放到 PATH 变量指定的可寻址目录下。

查看 tinyshell.sh 脚本代码,可知要解析的配置文件名是 tinyshellcmds.txt。

把前面贴出的命令简写配置信息写入 tinyshellcmds.txt 文件。

把这个文件放到 HOME 目录的 .liconfig 目录下。

之后,就可以开始执行 tinyshell.sh 脚本。

当前的 tinyshell.sh 脚本可以执行内置命令、命令简写对应的命令、系统自身支持的命令。

当不提供任何命令参数时,会进入 tiny shell。

在 tiny shell 中,会不停接收用户输入并执行对应命令。

直到读取到 EOF 、或者执行 quit 命令才会退出 tiny shell。

处理选项参数和直接处理命令简写的例子
下面是不进入 tiny shell,只处理选项参数和命令简写的例子:

$ tinyshell.sh -vkey=’gl’ value=’git log’key=’gp’ value=’git pull –stat –no-tags $(git remote) $(git rev-parse –abbrev-ref HEAD)’key=’ll’ value=’ls –color=auto -l’key=’ala’ value=’adb logcat -b all -v threadtime’$ tinyshell.sh ll-rwxrwxr-x 1 xxx xxx 964 11月 14 17:37 tinyshell.sh命令简写: ll. 命令: ls –color=auto -l
这里先执行 tinyshell.sh -v 命令,用键值对的形式列出支持的命令简写。

此时,只处理所给的选项参数,不会进入 tiny shell 里面。

tinyshell.sh ll 命令,提供了一个 ll 参数(两个小写字母 l)。

这个参数会被当成命令简写,然后执行该命令简写对应的命令。

执行结束后,不会进入 tiny shell 里面。

基于刚才列出的命令简写,可知 ll 对应 ls –color=auto -l 命令。

实际执行的也是这个命令。

进入 tiny shell 循环处理命令的例子
当不提供任何命令参数时,会进入 tiny shell 里面,循环处理命令。

具体例子如下所示:

$ tinyshell.shTiny shell 1.0.0Type ‘help’ for more information.TinySh>>> help下面列出 Tiny Shell 支持的内置命令列表和配置的命令简写列表.输入内置命令名或命令简写,会执行对应的命令.也可以输入系统自身支持的命令,会在 bash 中执行所给命令.内置命令列表: debug: 所给*个参数指定打开、或关闭调试功能. 其参数说明如下: on: 打开调试功能,会执行 bash 的 set -x 命令 off: 关闭调试功能,会执行 bash 的 set +x 命令 help: 打印当前帮助信息. quit: 退出当前 Tiny Shell.命令简写列表:key=’gl’ value=’git log’key=’gp’ value=’git pull –stat –no-tags $(git remote) $(git rev-parse –abbrev-ref HEAD)’key=’ll’ value=’ls –color=auto -l’key=’ala’ value=’adb logcat -b all -v threadtime’TinySh>>> date2019年 12月 31日 星期二 17:46:41 CSTTinySh>>> ll -Ctinyshell.sh命令简写: ll. 命令: ls –color=auto -l -C
当执行 tinyshell.sh 命令会进入 tiny shell 时,会打印一个 “TinySh>>>” 提示符。

在 tiny shell 中执行 help 命令可以查看支持的内置命令和命令简写。

在 tiny shell 中执行 date 命令打印当前的日期和时间。

当前的 tiny shell 自身不支持 date 命令。

这里执行了系统自身的 date 命令。

*后执行 ll -C 命令。

这里的 ll 是命令简写。后面的 -C 是对应命令的参数。

具体执行的命令是 ls –color=auto -l -C。

ls 命令的 -C 选项会多列显示文件名,覆盖了 -l 选项的效果。

由于 -l 选项的效果被覆盖,输出结果没有打印文件的详细信息,只列出文件名。

可以看到,在命令简写之后,可以再提供其他的命令参数。

即,可以只配置比较长的命令前缀部分,一些简单的参数可以动态提供。

不需要在配置文件中添加很多内容相似、只有细微差异的配置项。