精华内容
下载资源
问答
  • 安卓的加固方案是从 19 年底开始写的,到现在为止差不多快一年了,写这目的还是学习怎么脱壳,前几月再看雪看到有人直接分析壳来学习,不过我感觉从加壳写起也是一种浪漫。因为个人原因,在类指令抽取壳那里为...

    db04e8e7ffe81bb92d50a74ec2d9861c.png

    安卓的加固方案是从 19 年底开始写的,到现在为止差不多快一年了,写这个目的还是学习怎么脱壳,前几个月再看雪看到有人直接分析壳来学习,不过我感觉从加壳写起也是一种浪漫。因为个人原因,在类指令抽取壳那里为半完成状态,在今年大概率没有时间接着修改了,在 java 层的加固就止于此吧!!!(PS: 以后有时间会接着修改) 

    环境配置:

    Android studio v3.5.3 

    华为G621-TL00 android v4.4.4 

    第一代壳:落地加载

    1、原理

    a、原理很简单,就是首先将我们的 dex 文件或者 apk 文件解密,然后利用DexClassLoader加载器将其加载进内存中,然后利用反射加载待加固的 apk 的 appkication,然后运行待加固程序即可,我画了个流程图详细说明如下: 

    57898e60e847381422f566781d45ff7a.png

    b、上面说了大概原理,现在来说明一下具体细节,我们知道,在一个 app 开始运行的时候,第一个加载的类是ActivityThread,该类有个关键属性currentActivityThread,通过该属性能够获取到一系列其他关键的属性,例如mPackages,通过该属性,我们可以获取到mClassLoader属性,通过替换该属性我们可以替换系统加载器,如下所示: 

    828747f316a217626132f0d3b7d03cba.png

    接着来说怎么获取待加固 apk 的 application,这个通过在脱壳 apk 的 AndroidManifest.xml 中使用meta-data来获取,如下所示: 

    e95b0f6e183f92e0f5f8c5fa445c9892.png

    在然后就是怎么替换 application,我们可以知道在 android.app.LoadedApk 类中有一个方法makeApplication可以生成一个 application,通过该方法生成一个 application,然后通过替换android.content.ContentProvider类中的mContext属性完成 application 的替换,如下图所示: 

    128dfdeadb36a6cd2de88a52cdb6a52e.png

    2、实际操作

    ps:因为第一代壳网上一大堆,所以讲得很粗略,同时这也不是本文的重点!!! 

    通过上面的代码我们可以得到脱壳 apk,然鹅待加固的 apk 放在哪里,网上大多放在脱壳 dex 的尾部,我又画了一张图,应该可以看图就懂了: 

    d61e19f13015c507075e40411d7f51bb.png

    这个我采用通过 python 读取二进制然后重新计算 chunksum 和签名字段实现,代码如下: 

    import binasciiimport hashlibimport zlibdef fixCheckSum(shell):    shell.seek(0x0c)    data = shell.read()    checksum = zlib.adler32(data)    strchecksum = str(hex(checksum))    strchecksum = strchecksum.replace('0x','')    b = bytes(strchecksum.encode('utf-8'))    a = bytearray(b)    c = binascii.hexlify(binascii.unhexlify(bytes(a))[::-1])    dataCheckSum = bytearray(c)    shell.seek(0x08)    shell.write(dataCheckSum)def fixSHA1(shell):    shell.seek(0x20)    signBytes = shell.read()    sha1 = hashlib.sha1()    sha1.update(signBytes)    sign = sha1.hexdigest()    tmp = bytes(sign.encode('utf-8'))    b = bytearray(tmp)    shell.seek(0x0c)    shell.write(b)def fixFileSize(shell,num):    b = bytearray()    for i in range(4):        number = int(num % 256)        b.append(number)        num = num >> 8    shell.seek(0x20)    shell.write(b)def IntToHex(num):    b = bytearray()    for i in range(4):        number = int(num % 256)        b.append(number)        num = num >> 8    b.reverse()    return bdef main():    sourceApk = open('sourceApk.apk','rb+',True)    unshell = open('unshell.dex','rb+',True)    filename = 'classes.dex'        tmpApk = sourceApk.read()    print('[*] 成功读取待加壳的APK文件')    sourceArray = bytearray(tmpApk)    tmpDex = unshell.read()    print('[*] 成功读取脱壳DEX文件')    unshellArray = bytearray(tmpDex)    print('[-] 待加壳APK文件开始加密,加密类型为:未加密')        sourceApkLen = len(sourceArray)    unshellLen = len(unshellArray)    print('[+] 加密后的APK大小为' + str(sourceApkLen) + 'Byte')    totalLen = sourceApkLen + unshellLen + 4        tmpByteArray = unshellArray + sourceArray    newdex = tmpByteArray + IntToHex(sourceApkLen)    print('[+] 所有二进制数据合成完毕')        shellTmp = open(filename,'wb+',True)    shellTmp.write(newdex)    shellTmp.close()    print('[+] 数据写入' + filename + '完毕')        shell = open(filename,'rb+',True)    fixFileSize(shell,len(newdex))    print('[+] 文件大小修改完毕')    fixSHA1(shell)    print('[+] 文件SHA-1签名头部修改完毕')    fixCheckSum(shell)    print('[+] 文件校验头头部修改完毕')    print('[+] 待加壳APK文件sourceApk.apk加壳完毕,加壳后DEX文件' + filename + '生成完毕')    shell.close()    if __name__ == '__main__':    main()

    将上述 apk 重新签名后,安装运行,如下图所示: 

    cbc9e032e76778c4f30666994f6c6659.png

    f75639f8f1017d2a0a2060dbd801b643.png

    3、遇到的问题

    运行时报错如下所示: 

    d2fdf8d53f5c6f90b588bd87291bff87.png

    解决方案:报错显示无法实例化 activity,经过检查是无法加载到正确格式的 dex 文件,检查你的解密代码,即使是你加密是象征型的异或了一个 0xff,解密时也不能因为异或 0xff 值不变而不异或 0xff。其次是打包成 apk 之前删除签名文件之后在签名!!! 

    第二代壳:不落地加载

    1、原理

    大体原理和第一代壳相同,和第一代壳不同的是,第一代壳将 dex 文件解密出来会保存到文件中,在通过 DexClassLoader 加载进内存中,而不落地加载直接重写DexClassLoader使其可以直接加载字节数组,避免写入文件中。我们要做的是重写DexClassLoader,而这涉及到三个函数defineClassfindClassloadClass,在一个类被加载的时候,会先后调用这三个函数加载一个类,所以我们需要重写这三个函数,但是我们怎么在重写的过程中操控 dex 中的类(通过字节数组加载进来的并不能直接操控)?

    其实系统的 DexClassLoader 加载 dex 进入内存的也必然是通过字节加载的,而在系统 so 中的libdvm.so中的openDexFile可以直接加载 dex 文件,那么现在清楚了,我们可以通过编写 s o文件调用openDexFile函数加载dex字节数组,值得注意的是,openDexFile函数返回值为一个int类型的 cookie,可以简单理解成一个 dex 文件的'身份码',通过该'身份码'即可操控这个 dex 文件,至于怎么调用该函数,可以通过dlopendlsym函数调用,相关代码如下所示: 

    50bc2fba6e9550d7270695cd05c2b1cb.png

    bec83f1b0b5f7a258eb28e36ec813077.png

    2、实际操作

    a、首先编写样本,这里我写了一个类和一个方法,作用就是打印一个特征字符串,如下所示: 

    f6038f1ae149f07a5f1b5b0e3b12a959.png

    b、将上面的样本打包成 apk 后提取出 dex 文件然后放置到 assest 文件夹下(该文件夹需要自己建立)供程序调用(ps:我这里图方便,没有对 dex 文件加密然后解密,有需要的可以加上),然后脱壳 apk 和上面的第一代壳没什么区别,唯一不同的是就是我们使用的是我们自己重写的DexClassLoader,如下图所示: 

    7bb4a89652675be08381a0a0b85308e0.png

    c、运行截图如下: 

    357fe55db05e7d5a12ab9a912bd45494.png

    3、遇到的问题

    a、报错java.lang.UnsatisfiedLinkError: Native method not found

    解决方案:在配置文件中添加

    packagingOptions{        pickFirst "lib/armeabi-v7a/libtwoshell.so"        pickFirst "lib/arm64-v8a/libtwoshell.so"        pickFirst "lib/x86/libtwoshell.so"        pickFirst "lib/x86_64/libtwoshell.so"    }

    如下所示: 

    3780c49b61e5f28b84e87d1d87da72f0.png

    b、运行到加载 dex 文件中的方法时,app 直接闪退 

    解决方案:重写的loadClass方法有问题,不能通过直接 super 调用父类方法,而是应该通过反射调用defineClassNative方法,如下所示: 

    21c1670c7052ae3ae698610b25ee5b32.png

    第三代壳:类指令抽取壳

    1、原理

    a、什么是类指令抽取壳,从名字就能看出来,就是把dex文件中的方法指令抽空,变成nop,然后在运行时再将指令还原!!! 

    b、指令抽取可以通过 010 修改,现在来说指令还原,其余代码和第二代基本一样,不一样的地方在加载完 dex 之后执行指令还原函数,指令还原现在有两种方法,第一种是通过读取maps文件获取加载的 dex 文件地址,然后对 dex 文件进行解析,找到被 nop 的指令处进行还原(ps:该种方法需要及其熟悉 dex 文件格式,不了解的可以看我之前的文章关于解析 dex 文件,因为我之前解析的时候用的是 python,改成 c 要大量时间,所以我选择了第二种方法);第二种方法就是通过免 root hook 系统函数(最简单的就是 deFindClass 函数)然后进行指令还原!!! 

    c、接下来就讲一下怎么通过 hook dexFindClass 函数来进行指令还原(PS:看懂下面的内容需要理解 dex 文件格式)。dexFindClass函数在libdvm.so库中,如下所示: 

    17d55eb0982cb6e98b95da05d5cc72b6.png

    免 root hook 框架有点多,我选择的是 android inline hook,原因很简单,很适合在so层使用,其他的经过我测试不知道为啥我写出来的没反应,该框架 github 地址:

    https://github.com/ele7enxxh/Android-Inline-Hook

    用法可以参考作者 github,该 inline hook 框架需要原函数地址、新函数地址和原始函数的二级指针,用法如下所示(怎么使用不是重点,接下来的才是重点,所以这里比较粗略): 

    b17d0d2e24385fcf4b4fddd414ba4723.png

    我们要 hook 的是dexFindClass函数,该函数定义在DexFile.h文件中,该函数返回值为一个类结构指针,第二个参数为类名字,通过该参数我们就可以指定类进行指令还原,如下所示: 

    75dbb39bd65631b8b0ebcef884f2c8bc.png

    e1a23237451157276531598173bd8b45.png

    上面我们得到的classDataOff,我们可以通过该地址获取到类数据,该偏移地址指向的是一个DexClassData结构,该结构的header存储了相关类信息,该结构的directMethods指针指向的方法的结构体,如下所示: 

    59ab53d05d66049a36d0355d18b97b42.png

    通过directMethods指针我们可以顺着找到DexMethod结构体,通过该结构体的methodIdx调用系统函数dexGetMethodIddexStringById可以获取到方法名字,精确还原方法指令,通过该结构的codOff(这是个偏移地址)可获取方法指令,该偏移地址指向DexCode结构,该结构即存储了方法指令,利用memcpy替换即可达到指令还原的效果,如下所示: 

    fc583166b65ce9eb41f344643ac42df0.png

    323e1355b198601c1e9c24965db6e48a.png

    2、实践操作

    java 层基本和第二代壳一样,只是多了一个调用 hook 的函数,so 层关键代码如下所示:(ps:不知道为啥 Android inline hook 稳定性很差,上一个测试 app 还得行,下一个就疯狂报错了,所以代码是基本完成了,但是 android inline hook 报错未解决,有时间我会修改) 

    be2ca29710b3c377f092da0b788f4555.png

    3、遇到的问题

    报错未定义函数,如下所示: 

    e223283a50b78c52ad05e2530b65ef9f.png

    解决方案:在 CmakeLists.txt 文件中将 jni 文件夹下面所有引用到的文件都包含进去,如下所示: 

    f92f38744c54dc8c8f3c01d061f35fc5.png

    后记及其相关链接

    我个人习惯了通过写加固来学习脱壳,可能时间比直接分析壳来得慢,但是这其中体验真的酸爽到爆炸,因为个人原因,最后的类指令抽取壳最后一点没弄完,算是一个小遗憾吧,20 年应该没时间来弥补这个遗憾了,希望 21 年我有时间来把这个遗憾补上吧!!! 

    源码 github 链接:

    https://github.com/windy-purple/androidshell

    参考链接: 

    Android 免 Root 权限通过 Hook 系统函数修改程序运行时内存指令逻辑:

    http://www.520monkey.com/archives/1115

    Android 逆向之旅—运行时修改内存中的 Dalvik. 指令来改变代码逻辑:

    http://www.520monkey.com/archives/815

    Android 中免 root 的 hook 框架 Legend 原理解析:

    https://www.jianshu.com/p/c5580215ee26

    https://github.com/asLody/legend

    https://github.com/ele7enxxh/Android-Inline-Hook

    Android APK 加固-完善内存 dex:

    https://www.cnblogs.com/ltyandy/p/11644056.html

    利用动态加载技术加固APK原理解析:

    https://www.jianshu.com/p/ce20fa304e1e

    Android 插件化框架之动态加载 Activity(一):

    https://www.jianshu.com/p/1035ffd9e9cf

    Android APK 加固之动态加载dex(一):

    https://blog.csdn.net/weixin_44045581/article/details/89811868

    Android 中实现「类方法指令抽取方式」加固方案原理解析:

    http://www.520monkey.com/archives/1118

    Android 中 apk 加固完善篇之内存加载 dex 方案实现原理(不落地方式加载):

    http://www.520monkey.com/archives/629

    5c97203b596d040427f8a0507d934927.png

    展开全文
  • 字符串中的每一个字符都是‘0’到‘9’之间的某一个字符,用来表示数字中的一位,因为数字最大是n位的,因此我们需要一个长度为n+1的字符串,字符串的最后一个是结束符号‘\0’,当实际数字不够n位的是哦互,在字符...

    【面试题012】打印1到最大的n位数 

    大数问题

    字符串中的每一个字符都是‘0’到‘9’之间的某一个字符,用来表示数字中的一位,因为数字最大是n位的,因此我们需要一个长度为n+1的字符串,字符串的最后一个是结束符号‘\0’,当实际数字不够n位的是哦互,在字符串的前半部分补0。

    我们要做的是,在字符串上面做模拟加法,然后把字符串表达的数字打印出来。

     

    怎么判断增加的字符串是不是达到我们要求的最大的数字啦,这里很关键,isOverflow做判断,

    打印函数,也得定制,因为当数字不够n位的时候,我们在数字的前门补0,

     

    方法一,是模拟加法运算的过程,

     

    方法二,是n位所有的十进制数其实就是n个从0到9的全排列,也就是说,我么把数字的每一个位都从0到9排列一遍,就得到了所有的十进制数,

     

    PrintNum.cpp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
     

    #include <iostream>
    #include <cstring>
    #include <cstdio>

    using namespace std;

    void PrintNumber(char *number);
    bool Increment(char *number);
    void Print1ToMaxOfNDigitsRecursively(char *number, int length, int index);

    /*方法一*/
    void Print1ToMaxOfNDigits_1(int n)
    {
        if(n <= 0)
        {
            return ;
        }
        char *number = new char[n + 1];
        memset(number, '0', n);
        number[n] = '\0';

        while(!Increment(number))
        {
            PrintNumber(number);
        }

        delete []number;
    }

    /*
     * 字符串number表示一个数字,在number上增加1
     * 如果做加法溢出,则返回true,否者为false
     */

    bool Increment(char *number)
    {
        bool isOverflow = false;
        int nTakeOver = 0;
        int nLength = strlen(number);

        for(int i = nLength - 1; i >= 0; i --)
        {
            int nSum = number[i] - '0' + nTakeOver;
            if(i == nLength - 1)
            {
                nSum ++;
            }

            if(nSum >= 10)
            {
                if(i == 0)
                {
                    isOverflow = true;
                }
                else
                {
                    nSum -= 10;
                    nTakeOver = 1;
                    number[i] = '0' + nSum;
                }
            }
            else
            {
                number[i] = '0' + nSum;
                break;
            }
        }
        return isOverflow;
    }

    /*方法二*/
    void Print1ToMaxOfNDigits_2(int n)
    {
        if(n <= 0)
        {
            return ;
        }
        char *number = new char[n + 1];
        number[n] = '\0';

        for(int i = 0; i < 10; ++i)
        {
            number[0] = i + '0';
            Print1ToMaxOfNDigitsRecursively(number, n, 0);
        }
        delete[] number;
    }

    void Print1ToMaxOfNDigitsRecursively(char *number, int length, int index)
    {
        if(index == length - 1)
        {
            PrintNumber(number);
            return ;
        }
        for(int i = 0; i < 10; ++i)
        {
            number[index + 1] = i + '0';
            Print1ToMaxOfNDigitsRecursively(number, length, index + 1);
        }
    }


    /*
     * 公共函数
     * 字符串number表示一个数字,数字有若干个0开头
     * 打印出这个数字,并且忽略开头的0
     */

    void PrintNumber(char *number)
    {
        bool isBeginning0 = true;
        int nLength = strlen(number);

        for(int i = 0; i < nLength; ++i)
        {
            if(isBeginning0 && number[i] != '0')
            {
                isBeginning0 = false;
            }
            if(!isBeginning0)
            {
                cout << number[i];
            }
        }
        cout << "\t";
    }

    /*测试代码*/
    void Test(int n)
    {
        printf("Test for %d begins:\n", n);

        Print1ToMaxOfNDigits_1(n);
        Print1ToMaxOfNDigits_2(n);

        printf("Test for %d ends.\n", n);
    }

    int main()
    {
        int n = 1;
        Print1ToMaxOfNDigits_1(n);
        Print1ToMaxOfNDigits_2(n);
        return 0;
    }

    运行结果:

    1       2       3       4       5       6       7       8       9           
    1       2       3       4       5       6       7       8       9 

     

    Makefile:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    .PHONY:clean  
    CPP=g++  
    CFLAGS=-Wall -g  
    BIN=test  
    OBJS=PrintNum.o  
    LIBS=  
    $(BIN):$(OBJS)  
        $(CPP) $(CFLAGS) $^ -o $@ $(LIBS)  
    %.o:%.cpp  
        $(CPP) $(CFLAGS) -c $< -o $@  
    clean:  
        rm -f *.o $(BIN)  

    转载于:https://www.cnblogs.com/codemylife/p/3695647.html

    展开全文
  • 很简单,把空白区域填充上一个个字符,***把字符当成空白***,这样就好理解了! 如下图: 看懂上一步后,那我们就开始找规律了。 这里先告诉大家可以使用一元一次线性方程求解! 我们先看上部分: 为了便于...

    使用for循环打印空心菱形图案

    打印空心菱形图案
    例: 首先我们要知道:
    1.外层循环控制的是图形的行数
    2.内层循环控制的是图形的列数

    在这里插入图片描述

    	看菱形图案的左侧有空白区域,这就造成了我们很难理解,空白区域该怎么弄,
    	才能打印出来呢?
    	很简单,把空白区域填充上一个个的字符,***把字符当成空白***,这样就好理解了!
    	如下图:
    

    在这里插入图片描述

    看懂上一步后,那我们就开始找规律了。
    这里先告诉大家可以使用一元一次线性方程求解!
    我们先看上半部分:
    为了便于计算,我们把菱形内部“ * ”也填充上。在这里插入图片描述
    紧接着计算 “ * ” 号
    如下图:
    在这里插入图片描述
    最后如何去控制空心
    呢?**
    我们可以在打印 “ * ”的时候去控制它
    第一个与最后一个打印“ * ”号;
    其他打印空白即可。
    下列代码演示:

    if(j==1 || j==(2*i-1)) {
    				System.out.print("*");
    			    }else {
    				System.out.print(" "); //字符串里面是一个空格!
    			    }
    

    接下来是完整的代码了!
    最好是 从1开始循环,不然从0开始你可能会晕的哦!
    先打印上半部分后打印下半部分,空白在每行前面,所以开始打印空白,后打印“ * ”号。
    空白与*都是控制列数,并列for循环即可!

                //上半部分
                 
    		for(int i= 1;i<=4;i++) {			//外层循环控制行数 
    			
    			
    			//线性求解即可!
    			//打印空白
    			for(int j=1;j<=4-i;j++) {		//内层循环控制列数
    				System.out.print(" ");      //空白
    			}
    			//打印“ * ”
    			for(int j=1;j<=2*i-1;j++) {		//内层循环控制列数
    				if(j==1 || j==(2*i-1)) {	//控制内部空白区域
    				System.out.print("*");
    			    }else {
    				System.out.print(" ");
    			    }
    			}
    			System.out.println();
    		}
    
    		//打印下半部分
    		
    		for(int i=1;i<=3;i++) {
    			for(int j=1;j<=i;j++) {
    				System.out.print(" ");
    			} 
    			
    			for(int j=1;j<=-2*i+7;j++) {
    				if(j==1 || j==-2*i+7) {
    				System.out.print("*");
    			}else {
    				System.out.print(" ");
    			}
    		}
    			System.out.println();
    
    	}
    
    展开全文
  • 我曾经有几年几乎每天都跟正则交道,刚接手项目的时候我对正则也是一无所知,花小时百度了一下,然后写了几 demo,就开始正式接手了。三年多时间,我用到的正则鲜有超出我最初小时百度到的知识的。 1、正则...

    很多人觉得正则很难,在我看来,这些人一定是没有用心。其实正则很简单,根据二八原则,我们只需要懂 20% 的内容就可以解决 80% 的问题了。我曾经有几年几乎每天都跟正则打交道,刚接手项目的时候我对正则也是一无所知,花半小时百度了一下,然后写了几个 demo,就开始正式接手了。三年多时间,我用到的正则鲜有超出我最初半小时百度到的知识的。

    1、正则基础

    1.1、基础语法

    (1)常用元字符

    语法 描述
    \b 匹配单词的开始或结束
    \d 匹配数字
    \s 匹配任意不可见字符(空格、换行符、制表符等),等价于[ \f\n\r\t\v]。
    \w 匹配任意 Unicode 字符集,包括字母、数字、下划线、汉字等
    . 匹配除换行符(\n)以外的任意字符
    ^ 或 \A 匹配字符串或行的起始位置
    $ 或 \Z 匹配字符串或行的结束位置

    (2)限定词(又叫量词)

    语法 描述
    * 重复零次或更多次
    + 重复一次或更多次
    ? 重复零次或一次
    {n} 重复 n 次
    {n,} 重复 n 次或更多次
    {n,m} 重复 n 到 m 次

    (3)常用反义词

    语法 描述
    \B 匹配非单词的开始或结束
    \D 匹配非数字
    \S 匹配任意可见字符, [^ \f\n\r\t\v]
    \W 匹配任意非 Unicode 字符集
    [^abc] 除 a、b、c 以外的任意字符

    (4)字符族

    语法 描述
    [abc] a、b 或 c
    [^abc] 除 a、b、c 以外的任意字符
    [a-zA-Z] a 到 z 或 A 到 Z
    [a-d[m-p]] a 到 d 或 m 到 p,即 [a-dm-p](并集)
    [a-z&&[def]] d、e 或 f(交集)
    [a-z&&[^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
    [a-z&&[^m-p]] a 到 z,减去 m 到 p:[a-lq-z](减去)

    以上便是正则的基础内容,下面来写两个例子看下:

    s = '123abc你好'
    re.search('\d+', s).group()
    re.search('\w+', s).group()
    复制代码

    结果:

    123
    123abc你好
    复制代码

    是不是很简单?

     

    1.2、修饰符

    修饰符在各语言中也是有差异的。

    Python 中的修饰符:

    修饰符 描述
    re.A 匹配 ASCII字符类,影响 \w, \W, \b, \B, \d, \D
    re.I 忽略大小写
    re.L 做本地化识别匹配(这个极少极少使用)
    re.M 多行匹配,影响 ^ 和 $
    re.S 使 . 匹配包括换行符(\n)在内的所有字符
    re.U 匹配 Unicode 字符集。与 re.A 相对,这是默认设置
    re.X 忽略空格和 # 后面的注释以获得看起来更易懂的正则。

    (1)re.A

    修饰符 A 使 \w 只匹配 ASCII 字符,\W 匹配非 ASCII 字符。

    s = '123abc你好'
    re.search('\w+', s, re.A).group()
    re.search('\W+', s, re.A).group()
    复制代码

    结果:

    123abc
    你好
    复制代码

    但是描述中还有 \d\D,数字不都是 ASCII 字符吗?这是什么意思?别忘了,还有 全角和半角

    s = '0123456789'    # 全角数字
    re.search('\d+', s, re.U).group()
    复制代码

    结果:

    0123456789
    复制代码

    (2)re.M 多行匹配的模式其实也不常用,很少有一行行规整的数据。

    s = 'aaa\r\nbbb\r\nccc'
    
    re.findall('^[\s\w]*?$', s)
    re.findall('^[\s\w]*?$', s, re.M)
    复制代码

    结果:

    ['aaa\r\nbbb\r\nccc']        # 单行模式
    ['aaa\r', 'bbb\r', 'ccc']    # 多行模式
    复制代码

    (3)re.S 这个简单,直接看个例子。

    s = 'aaa\r\nbbb\r\nccc'
    
    re.findall('^.*', s)
    re.findall('^.*', s, re.S)
    复制代码

    结果:

    ['aaa\r']
    ['aaa\r\nbbb\r\nccc']
    复制代码

    (4)re.X 用法如下:

    rc = re.compile(r"""
    \d+ # 匹配数字
    # 和字母
    [a-zA-Z]+
    """, re.X)
    rc.search('123abc').group()
    复制代码

    结果:

    123abc
    复制代码

    注意,用了 X 修饰符后,正则中的所有空格会被忽略,包括正则里面的原本有用的空格。如果正则中有需要使用空格,只能用 \s 代替。

    (5)(?aiLmsux) 修饰符不仅可以代码中指定,也可以在正则中指定。(?aiLmsux) 表示了以上所有的修饰符,具体用的时候需要哪个就在 ? 后面加上对应的字母,示例如下,(?a)re.A 效果是一样的:

    s = '123abc你好'
    re.search('(?a)\w+', s).group()
    re.search('\w+', s, re.A).group()
    复制代码

    结果是一样的:

    123abc
    123abc
    复制代码

    1.3、贪婪与懒惰

    当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。

    s = 'aabab'
    re.search('a.*b', s).group()    # 这就是贪婪
    re.search('a.*?b', s).group()   # 这就是懒惰
    复制代码

    结果:

    aabab
    aab
    复制代码

    简单来说:

    • 所谓贪婪,就是尽可能 的匹配;
    • 所谓懒惰,就是尽可能 的匹配。
    • *+{n,} 这些表达式属于贪婪;
    • *?+?{n,}? 这些表达式就是懒惰(在贪婪的基础上加上 ?)。

    2、正则进阶

    2.1、捕获分组

    语法 描述
    (exp) 匹配exp,并捕获文本到自动命名的组里
    (?Pexp) 匹配exp,并捕获文本到名称为 name 的组里
    (?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
    (?P=name) 匹配之前由名为 name 的组匹配的文本

    注意:在其他语言或者网上的一些正则工具中,分组命名的语法是 (?<name>exp)(?'name'exp) ,但在 Python 里,这样写会报错:This named group syntax is not supported in this regex dialect。Python 中正确的写法是:(?P<name>exp)

    示例一:

    分组可以让我们用一条正则提取出多个信息,例如:

    s = '姓名:张三;性别:男;电话:138123456789'
    m = re.search('姓名[::](\w+).*?电话[::](\d{11})', s)
    if m:
        name = m.group(1)
        phone = m.group(2)
        print(f'name:{name}, phone:{phone}')
    复制代码

    结果:

    name:张三, phone:13812345678
    复制代码

    示例二:

    (?P<name>exp) 有时还是会用到的, (?P=name) 则很少情况下会用到。我想了一个 (?P=name) 的使用示例,给大家看下效果:

    s = '''
    <name>张三</name>
    <age>30</age>
    <phone>138123456789</phone>
    '''
    
    pattern = r'<(?P<name>.*?)>(.*?)</(?P=name)>'
    It = re.findall(pattern, s)
    复制代码

    结果:

    [('name', '张三'), ('age', '30'), ('phone', '138123456789')]
    复制代码

    2.2、零宽断言

    语法 描述
    (?=exp) 匹配exp前面的位置
    (?<=exp) 匹配exp后面的位置
    (?!exp) 匹配后面跟的不是exp的位置
    (?<!exp) 匹配前面不是exp的位置

    注意:正则中常用的前项界定 (?<=exp) 和前项否定界定 (?<!exp) 在 Python 中可能会报错:look-behind requires fixed-width pattern,原因是 python 中 前项界定的表达式必须是定长的,看如下示例:

    (?<=aaa)	    # 正确
    (?<=aaa|bbb)	# 正确
    (?<=aaa|bb)	    # 错误
    (?<=\d+)	    # 错误
    (?<=\d{3})	    # 正确
    复制代码

    2.3、条件匹配

    这大概是最复杂的正则表达式了。语法如下:

    语法 描述
    (?(id/name)yes|no) 如果指定分组存在,则匹配 yes 模式,否则匹配 no 模式

    此语法极少用到,印象中只用过一次。

    以下示例的要求是:如果以 _ 开头,则以字母结尾,否则以数字结尾。

    s1 = '_abcd'
    s2 = 'abc1'
    
    pattern = '(_)?[a-zA-Z]+(?(1)[a-zA-Z]|\d)'
    
    re.search(pattern, s1).group()
    re.search(pattern, s2).group()
    复制代码

    结果:

    _abcd
    abc1
    复制代码

    2.4、findall

    Python 中的 re.findall 是个比较特别的方法(之所以说它特别,是跟我常用的 C# 做比较,在没看注释之前我想当然的掉坑里去了)。我们看这个方法的官方注释:

    Return a list of all non-overlapping matches in the string.
    
    If one or more capturing groups are present in the pattern, return 
    a list of groups; this will be a list of tuples if the pattern 
    has more than one group.
    
    Empty matches are included in the result.
    复制代码

    简单来说,就是

    • 如果没有分组,则返回整条正则匹配结果的列表;
    • 如果有 1 个分组,则返回分组匹配到的结果的列表;
    • 如果有多个分组,则返回分组匹配到的结果的元组的列表。

    看下面的例子:

    s = 'aaa123bbb456ccc'
    
    re.findall('[a-z]+\d+', s)          # 不包含分组
    re.findall('[a-z]+(\d+)', s)        # 包含一个分组
    re.findall('([a-z]+(\d+))', s)      # 包含多个分组
    re.findall('(?:[a-z]+(\d+))', s)    # ?: 不捕获分组匹配结果
    复制代码

    结果:

    ['aaa123', 'bbb456']
    ['123', '456']
    [('aaa123', '123'), ('bbb456', '456')]
    ['123', '456']
    复制代码

    零宽断言中讲到 Python 中前项界定必须是定长的,这很不方便,但是配合 findall 有分组时只取分组结果的特性,就可以模拟出非定长前项界定的效果了。

    结语

    其实正则就像是一个数学公式,会背公式不一定会做题。但其实这公式一点也不难,至少比学校里学的数学简单多了,多练习几次也就会了。

    展开全文
  • 说明Python 教程正在编写中,欢迎大家加微信 sinbam 提供意见、建议、纠错、催更。...如何查看如以下代码,字符串少半个引号,会抛出SyntaxError等, 并告知错误内容,并将箭头指向错误发生的位置。print('h...
  • 你必须知道的495C语言问题

    千次下载 热门讨论 2015-05-08 11:09:25
    例如定义一包含N指向返回指向字符的指针的函数的指针的数组? 1.22 如何声明返回指向同类型函数的指针的函数?我在设计一状态机,用函数表示每种状态,每函数都会返回一指向下一状态的函数的指针。可我...
  • 《你必须知道的495C语言问题》

    热门讨论 2010-03-20 16:41:18
    书中列出了C用户经常问的400多经典问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等各个方面的主题,并分别给出了解答,而且结合代码示例阐明要点。 《你必须知道的495C语言问题》结构...
  • 例如定义一包含N指向返回指向字符的指针的函数的指针的数组? 11  1.22 如何声明返回指向同类型函数的指针的函数?我在设计一状态机,用函数表示每种状态,每函数都会返回一指向下一状态的函数的指针...
  • 例如定义一包含N指向返回指向字符的指针的函数的指针的数组? 1.22 如何声明返回指向同类型函数的指针的函数?我在设计一状态机,用函数表示每种状态,每函数都会返回一指向下一状态的函数的指针。可我...
  • 例如定义一包含N指向返回指向字符的指针的函数的指针的数组? 1.22 如何声明返回指向同类型函数的指针的函数?我在设计一状态机,用函数表示每种状态,每函数都会返回一指向下一状态的函数的指针。可我...
  • 书中列出了C用户经常问的400多经典问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等各个方面的主题,并分别给出了解答,而且结合代码示例阐明要点。 本书结构清晰,讲解透彻,是各高校相关...
  • 书中列出了C用户经常问的400多经典问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等各个方面的主题,并分别给出了解答,而且结合代码示例阐明要点。  本书结构清晰,讲解透彻,是各高校...
  • 作者在网络版CFAQ列表的基础上进行了大幅度的扩充和丰富,结合代码示例,权威而且详细深入地解答了实际学习和工作中最常遇到的495C语言问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等...
  • 书中列出了C用户经常问的400多经典问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等各个方面的主题,并分别给出了解答,而且结合代码示例阐明要点。 本书结构清晰,讲解透彻,是各高校相关...
  • 例如定义一包含N指向返回指向字符的指针的函数的指针的数组? 1.22 如何声明返回指向同类型函数的指针的函数?我在设计一状态机,用函数表示每种状态,每函数都会返回一指向下一状态的函数的指针。可我...
  • python type()函数

    2014-07-07 11:26:00
    怎么把一个变量的类型写入文件?a = 3type(a)貌似返回的是type类型,不能打印,也不能用文件的write怎么半,或者怎么转换成...通过str(type(a))的方式,得到一个字符串"<type 'int'>"(Python2.6下是这样的...
  • 补发上周六的记录

    2017-11-30 16:06:38
    半个学期都过了,不能给他们做个小小的学习报告,实在是太可惜啦。我想,干脆自己画个爱心给他们好了,而且不管代码写得怎么样,最好得是自己想的。 我就着手画了个图,打算按着这个图来编。虽然写这个好像也
  • 电竞爱好者都清楚,一顺心顺手、质量上乘的机械键盘到底有多么重要,就好像上了战场的战士,手里拿着普通手枪,那肯定不过手里拿冲锋枪的敌人,而机械键盘对于电竞爱好者来说,就是那冲锋枪。很多人都说,游戏...
  • 这几乎是最简单的正则表达式了,它可以精确匹配这样的字符串:由两个字符组成,前一个字符是h,后一个是i。通常,处理正则表达式的工具会提供一个忽略大小写的选项,如果选中了这个选项,它可以匹配hi,HI,Hi,hI这四种...
  • 括号,这是一道普通的DFS+字符串处理题,但对于与我这种平时不怎么碰STL的人来说,光是看题解就足足看了半个小时(我有多弱就不解释了吧),最后硬是照着别人的题解才勉强出来了。 但说实话,这道题带给我的小知识...
  • 1、可以删除半个中文字符,破除乱码。 2、进行快速全局替换。支持多文件操作。 3、可以对所选择行进行整行左移或者整行右移。 4、可以对选定行按设定行宽进行左对齐、居中、右对齐。 5、删除所有空行、删除开头...
  • 4.5.4 怎么打出线程栈信息。 开源框架 4.5.5 简单讲讲tomcat结构,以及其类加载器流程,线程模型等。 4.5.6 tomcat如何调优,涉及哪些参数 。 4.5.7 讲讲Spring加载流程。 4.5.8 Spring AOP的实现原理。 4.5.9...
  • 疯狂的程序员

    热门讨论 2012-07-18 18:05:32
    一直过了半个月,这事情才总算落实,还像模像样跟人家签了代理合同。其实这家广告公司整个还没有他们寝室大,公司就两人:一个男的,一个女的。没办法,绝影想就这么一个小的公司,人家在容易就范。人家想,就这么一...
  • 最新Java面试宝典pdf版

    热门讨论 2011-08-31 11:29:22
    3、编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证汉字不被截取半个,如“我ABC”,4,应该截取“我AB”,输入“我ABC汉DEF”,6,应该输出“我ABC”,而不是“我ABC+汉...
  • Java面试宝典-经典

    2015-03-28 21:44:36
    3、编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证汉字不被截取半个,如“我ABC”,4,应该截取“我AB”,输入“我ABC汉DEF”,6,应该输出“我ABC”,而不是“我ABC+汉...

空空如也

空空如也

1 2 3 4
收藏数 75
精华内容 30
关键字:

半个字符怎么打