SQL Server 中WITH (NOLOCK)浅析 - 潇湘隐者 - 博客园

mikel阅读(618)

来源: SQL Server 中WITH (NOLOCK)浅析 – 潇湘隐者 – 博客园

概念介绍

  

开发人员喜欢在SQL脚本中使用WITH(NOLOCK), WITH(NOLOCK)其实是表提示(table_hint)中的一种。它等同于 READUNCOMMITTED 。 具体的功能作用如下所示(摘自MSDN):

   1: 指定允许脏读。不发布共享锁来阻止其他事务修改当前事务读取的数据,其他事务设置的排他锁不会阻碍当前事务读取锁定数据。允许脏读可能产生较多的并发操作,但其代价是读取以后会被其他事务回滚的数据修改。这可能会使您的事务出错,向用户显示从未提交过的数据,或者导致用户两次看到记录(或根本看不到记录)。有关脏读、不可重复读和幻读的详细信息,请参阅并发影响

   2: READUNCOMMITTED 和 NOLOCK 提示仅适用于数据锁。所有查询(包括那些带有 READUNCOMMITTED 和 NOLOCK 提示的查询)都会在编译和执行过程中获取 Sch-S(架构稳定性)锁。因此,当并发事务持有表的 Sch-M(架构修改)锁时,将阻塞查询。例如,数据定义语言 (DDL) 操作在修改表的架构信息之前获取 Sch-M 锁。所有并发查询(包括那些使用 READUNCOMMITTED 或 NOLOCK 提示运行的查询)都会在尝试获取 Sch-S 锁时被阻塞。相反,持有 Sch-S 锁的查询将阻塞尝试获取 Sch-M 锁的并发事务。有关锁行为的详细信息,请参阅锁兼容性(数据库引擎)

   3:  不能为通过插入、更新或删除操作修改过的表指定 READUNCOMMITTED 和 NOLOCK。SQL Server 查询优化器忽略 FROM 子句中应用于 UPDATE 或 DELETE 语句的目标表的 READUNCOMMITTED 和 NOLOCK 提示。

功能与缺陷

 

    使用WIHT(NOLOCK)有利也有弊,所以在决定使用之前,你一定需要了解清楚WITH(NOLOCK)的功能和缺陷,看其是否适合你的业务需求,不要觉得它能提升性能,稀里糊涂的就使用它。

 

    1:使用WITH(NOLOCK)时查询不受其它排他锁阻塞

    打开会话窗口1,执行下面脚本,不提交也不回滚事务,模拟事务真在执行过程当中

BEGIN TRAN

       UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1;

       --ROLLBACK

   

   打开会话窗口2,执行下面脚本,你会发现执行结果一直查询不出来(其实才两条记录)。当前会话被阻塞了

SELECT * FROM TEST;

    打开会话窗口3,执行下面脚本,查看阻塞情况,你会发现在会话2被会话1给阻塞了,会话2的等待类型为LCK_M_S:“当某任务正在等待获取共享锁时出现”



  SELECT wt.blocking_session_id                    AS BlockingSessesionId
        ,sp.program_name                           AS ProgramName
        ,COALESCE(sp.LOGINAME, sp.nt_username)     AS HostName
        ,ec1.client_net_address                    AS ClientIpAddress
        ,db.name                                   AS DatabaseName
        ,wt.wait_type                              AS WaitType
        ,ec1.connect_time                          AS BlockingStartTime
        ,wt.WAIT_DURATION_MS/1000                  AS WaitDuration
        ,ec1.session_id                            AS BlockedSessionId
        ,h1.TEXT                                   AS BlockedSQLText
        ,h2.TEXT                                   AS BlockingSQLText
  FROM sys.dm_tran_locks AS tl
  INNER JOIN sys.databases db
    ON db.database_id = tl.resource_database_id
  INNER JOIN sys.dm_os_waiting_tasks AS wt
    ON tl.lock_owner_address = wt.resource_address
  INNER JOIN sys.dm_exec_connections ec1
    ON ec1.session_id = tl.request_session_id
  INNER JOIN sys.dm_exec_connections ec2
    ON ec2.session_id = wt.blocking_session_id
  LEFT OUTER JOIN master.dbo.sysprocesses sp
    ON SP.spid = wt.blocking_session_id
  CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
  CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

 

 

clipboard

 

此时查看会话1(会话1的会话ID为53,执行脚本1前,可以用SELECT  @@spid查看会话ID)的锁信息情况,你会发现表TEST(ObjId=1893581784)持有的锁信息如下所示

 

clipboard[1]

   

打开会话窗口4,执行下面脚本.你会发现查询结果很快就出来,会话4并不会被会话1阻塞。

    SELECT * FROM TEST WITH(NOLOCK)

从上面模拟的这个小例子可以看出,正是由于加上WITH(NOLOCK)提示后,会话1中事务设置的排他锁不会阻碍当前事务读取锁定数据,所以会话4不会被阻塞,从而提升并发时查询性能。

 

2:WITH(NOLOCK) 不发布共享锁来阻止其他事务修改当前事务读取的数据,这个就不举例子了。

本质上WITH(NOLOCK)是通过减少锁和不受排它锁影响来减少阻塞,从而提高并发时的性能。所谓凡事有利也有弊,WITH(NOLOCK)在提升性能的同时,也会产生脏读现象。

如下所示,表TEST有两条记录,我准备更新OBJECT_ID=1的记录,此时事务既没有提交也没有回滚

clipboard[2]

BEGIN TRAN

UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1;

--ROLLBACK

此时另外一个会话使用WITH(NOLOCK)查到的记录为未提交的记录值

clipboard[3]

假如由于某种原因,该事务回滚了,那么我们读取到的OBJECT_ID=1的记录就是一条脏数据。

脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的。

 

WITH(NOLOCK)使用场景

 

什么时候可以使用WITH(NOLOCK)? 什么时候不能使用WITH(NOLOCK),这个要视你系统业务情况,综合考虑性能情况与业务要求来决定是否使用WITH(NOLOCK), 例如涉及到金融或会计成本之类的系统,出现脏读那是要产生严重问题的。关键业务系统也要慎重考虑。大体来说一般有下面一些场景可以使用WITH(NOLOCK)

   1: 基础数据表,这些表的数据很少变更。

   2:历史数据表,这些表的数据很少变更。

   3:业务允许脏读情况出现涉及的表。

   4:数据量超大的表,出于性能考虑,而允许脏读。

另外一点就是不要滥用WITH(NOLOCK),我发现有个奇怪现象,很多开发知道WITH(NOLOCK),但是有不了解脏读,习惯性的使用WITH(NOLOCK)。

 

WITH(NOLOCK)与 NOLOCK区别

 

为了搞清楚WITH(NOLOCK)与NOLOCK的区别,我查了大量的资料,我们先看看下面三个SQL语句有啥区别

    SELECT * FROM TEST NOLOCK

    SELECT * FROM TEST (NOLOCK);

    SELECT * FROM TEST WITH(NOLOCK);

上面的问题概括起来也就是说NOLOCK、(NOLOCK)、 WITH(NOLOCK)的区别:

1: NOLOCK这样的写法,其实NOLOCK其实只是别名的作用,而没有任何实质作用。所以不要粗心将(NOLOCK)写成NOLOCK

2:(NOLOCK)与WITH(NOLOCK)其实功能上是一样的。(NOLOCK)只是WITH(NOLOCK)的别名,但是在SQL Server 2008及以后版本中,(NOLOCK)不推荐使用了,”不借助 WITH 关键字指定表提示”的写法已经过时了。 具体参见MSDN http://msdn.microsoft.com/zh-cn/library/ms143729%28SQL.100%29.aspx

    2.1  至于网上说WITH(NOLOCK)在SQL SERVER 2000不生效,我验证后发现完全是个谬论。

    2.2  在使用链接服务器的SQL当中,(NOLOCK)不会生效,WITH(NOLOCK)才会生效。如下所示

clipboard[4]

    消息 4122,级别 16,状态 1,第 1 行

    Remote table-valued function calls are not allowed.

 

3.语法上有些许出入,如下所示

这种语法会报错
SELECT  * FROM   sys.indexes  WITH(NOLOCK) AS i
-Msg 156, Level 15, State 1, Line 1
-Incorrect syntax near the keyword 'AS'.

这种语法正常
SELECT  * FROM   sys.indexes  (NOLOCK) AS i

可以全部改写为下面语法

SELECT  * FROM   sys.indexes   i WITH(NOLOCK)


SELECT  * FROM   sys.indexes   i (NOLOCK)

 

WITH(NOLOCK)会不会产生锁

    很多人误以为使用了WITH(NOLOCK)后,数据库库不会产生任何锁。实质上,使用了WITH(NOLOCK)后,数据库依然对该表对象生成Sch-S(架构稳定性)锁以及DB类型的共享锁, 如下所示,可以在一个会话中查询一个大表,然后在另外一个会话中查看锁信息(也可以使用SQL Profile查看会话锁信息)

    不使用WTIH(NOLOCK)

clipboard[5]

  使用WITH(NOLOCK)

clipboard[6]

  从上可以看出使用WITH(NOLOCK)后,数据库并不是不生成相关锁。  对比可以发现使用WITH(NOLOCK)后,数据库只会生成DB类型的共享锁、以及TAB类型的架构稳定性锁.

另外,使用WITH(NOLOCK)并不是说就不会被其它会话阻塞,依然可能会产生Schema Change Blocking

会话1:执行下面SQL语句,暂时不提交,模拟事务正在执行

BEGIN TRAN

  ALTER TABLE TEST ADD Grade VARCHAR(10) ;

会话2:执行下面语句,你会发现会话被阻塞,截图如下所示。

SELECT * FROM TEST WITH(NOLOCK)

image

(1条消息)mysql-bin.000001文件的来源及处理方法_数据库_yushaolong1234的博客-CSDN博客

mikel阅读(546)

来源: (1条消息)mysql-bin.000001文件的来源及处理方法_数据库_yushaolong1234的博客-CSDN博客

用ports安装了MySQL以后,过一段时间发现/var空间不足了,查一下,会发现是mySQL-bin.000001、mySQL-bin.000002等文件占用了空间,那么这些文件是干吗的?这是数据库的操作日志,例如UPDATE一个表,或者DELETE一些数据,即使该语句没有匹配的数据,这个命令也会存储到日志文件中,还包括每个语句执行的时间,也会记录进去的。

这样做主要有以下两个目的:
1:数据恢复
如果你的数据库出问题了,而你之前有过备份,那么可以看日志文件,找出是哪个命令导致你的数据库出问题了,想办法挽回损失。
2:主从服务器之间同步数据
主服务器上所有的操作都在记录日志中,从服务器可以根据该日志来进行,以确保两个同步。

处理方法分两种情况:
1:只有一个mySQL服务器,那么可以简单的注释掉这个选项就行了。
vi /etc/my.cnf把里面的log-bin这一行注释掉,重启mysql服务即可。
2:如果你的环境是主从服务器,那么就需要做以下操作了。
A:在每个从属服务器上,使用SHOW SLAVE STATUS来检查它正在读取哪个日志。
B:使用SHOW MASTER LOGS获得主服务器上的一系列日志。
C:在所有的从属服务器中判定最早的日志,这个是目标日志,如果所有的从属服务器是更新的,就是清单上的最后一个日志。
D:清理所有的日志,但是不包括目标日志,因为从服务器还要跟它同步。
清理日志方法为:
PURGE MASTER LOGS TO ‘mysql-bin.010’;
PURGE MASTER LOGS BEFORE ‘2008-12-19 21:00:00’;

如果你确定从服务器已经同步过了,跟主服务器一样了,那么可以直接RESET MASTER将这些文件删除。

 

======================================

 

之前发现自己10G的服务器空间大小,用了几天就剩下5G了,自己上传的文件才仅仅几百M而已,到底是什么东西占用了这么大空间呢?今天有时间彻底来查了一下:

 

看下上面的目录web根目录是放在/home 里面的,所有文件加起来才不到300M,而服务器上已经占用了近5G空间,恐怖吧,最后经我一步一步查询得知,原来是这个文件夹占了非常多的空间资源:

 

原来如此,是mysql文件夹下的var目录占用空间最大,那里面是啥 内容呢?我们来看下:

 

发现了如此多的 mysql-bin.0000X文件,这是什么东西呢?原来这是mysql的操作日志文件.我才几十M的数据库,操作日志居然快3G大小了.

如何删除mysql-bin.0000X 日志文件呢?

红色表示输入的命令.

[root@jiucool var]# /usr/local/mysql/bin/mysql -u root -p
Enter password:  (输入密码)
Welcome to the MySQL monitor.  Commands end with ; or /g.
Your MySQL connection id is 264001
Server version: 5.1.35-log Source distribution

Type ‘help;’ or ‘/h’ for help. Type ‘/c’ to clear the current input statement.

mysql> reset master; (清除日志文件)
Query OK, 0 rows affected (8.51 sec)

mysql>

好了,我们再来查看下mysql文件夹占用多少空间?

[root@jiucool var]# du -h –max-depth=1 /usr/local/mysql/
37M     /usr/local/mysql/var
70M     /usr/local/mysql/mysql-test
15M     /usr/local/mysql/lib
448K    /usr/local/mysql/include
2.9M    /usr/local/mysql/share
7.6M    /usr/local/mysql/libexec
17M     /usr/local/mysql/bin
11M     /usr/local/mysql/docs
2.9M    /usr/local/mysql/sql-bench
163M    /usr/local/mysql/

好了,看一下,整个mysql 目录才占用163M大小!OK,没问题,既然mysql-bin.0000X日志文件占用这么大空间,存在的意义又不是特别大,那么我们就不让它生成吧.

[root@jiucool var]# find / -name my.cnf

找到了my.cnf 即mysql配置文件,我们将log-bin=mysql-bin 这条注释掉即可.

# Replication Master Server (default)
# binary logging is required for replication
#log-bin=mysql-bin

重启下mysql吧.

OK,至此,操作完成. 以后再不会因为就几十M的数据库大小生成N个G的日志文件啦.

这些个日志文件太恐怖了,我搬到这新VPS来才二十天左右,还不到一个月日志文件居然就近3个G大小,如果一两个月我不清除日志文件这还得了!

Sql Server 判断表是否存在方法总结 - willingtolove - 博客园

mikel阅读(804)

来源: Sql Server 判断表是否存在方法总结 – willingtolove – 博客园

 

正文

#使用场景:

1、在创建表之前,需要先判断该表是否已经存在;

2、在删除表之前,需要先判断该表是否已经存在;

#方法总结:

1、判断实体表是否存在的方法:

1)、方法一:

if Exists(select top 1 * from sysObjects where Id=OBJECT_ID(N'UserInfos') and xtype='U')
    print '表UserInfos 存在'
else 
    print '表UserInfos 不存在'

2)、方法二:

if OBJECT_ID(N'UserInfos',N'U') is not null
    print '表UserInfos 存在!'
else 
    print '表UserInfos 不存在!'

2、判断临时表是否存在的方法:

1)、方法一:

if exists (select * from tempdb.dbo.sysobjects where id = object_id(N'tempdb..#TempUsers') and type='U')
    print '临时表#TempUsers 存在!'
else 
    print '临时表#TempUsers 不存在!'

2)、方法二:

if OBJECT_ID(N'tempdb..#TempUsers',N'U') is not null
    print '临时表#TempUsers 存在!'
else 
    print '临时表#TempUsers 不存在!'

 

SQL Server之JSON 函数 - springsnow - 博客园

mikel阅读(895)

来源: SQL Server之JSON 函数 – springsnow – 博客园

 


SQL Server 2005开始支持XML数据类型,提供原生的XML数据类型、XML索引及各种管理或输出XML格式的函数。随着JSON的流行,SQL Server2016开始支持JSON数据类型,不仅可以直接输出JSON格式的结果集,还能读取JSON格式的数据。

下面是我们熟悉的SELECT及输出格式,后面对JSON的演示基于此SQL

一、 将查询结果输出JSON格式

1、FOR JSON AUTO:SELECT语句的结果以JSON输出。

要将SELECT语句的结果以JSON输出,最简单的方法是在后面加上FOR JSON AUTO:

 

2、FOR JSON AUTO,Root(’’) :为JOSN加上根节点

若要为FOR JSON加上Root Key,可以用ROOT选项来自定义ROOT 节点的名称:

3、FOR JSON PATH输出:可通过列别名来定义JSON对象的层次结构

若要自定义输出JSON格式的结构时,必须使用JSONPATH。

  • FOR JSON Auto,自动按照查询语句中使用的表结构来创建嵌套的JSON子数组,类似于For Xml Auto特性。
  • FOR JSON Path,通过列名或者列别名来定义JSON对象的层次结构,列别名中可以包含“.”,JSON的成员层次结构将会与别名中的层次结构保持一致。
    这个特性非常类似于早期SQL Server版本中的For Xml Path子句,可以使用斜线来定义xml的层次结构。

 

4、FOR JSON PATH+ROOT输出:为JOSN加上根节点

5、INCLUDE_NULL_VALUES:值null的字段需要显示出现。

为NULL的数据在输出JSON时,会被忽略,若想要让NULL的字段也显示出来,可以加上选项INCLUDE_NULL_VALUES,该选项也适用于AUTO。

6、列的别名,可以增加带有层级关系的节点。

比如下面的SQL,增加了一个“SN”节点,把栏位SERNUM和CLIMAT放在里面:

二、 解析JSON格式的数据

1、使用OPENJSON()函数:

2、通过WITH选项,自定义输出列:

三、JSON函数

复制代码
declare @param nvarchar(max);

set @param = N'{  
     "info":{    
       "type":1,  
       "address":{    
         "town":"Bristol",  
         "county":"Avon",  
         "country":"England"  
       },  
       "tags":["Sport", "Water polo"]  
    },  
    "type":"Basic"  
 }';
复制代码

1、ISJSON:测试字符串是否包含有效 JSON。

print iif(isjson(@param) > 0, 'OK', 'NO');

返回:OK

2、JSON_VALUE :从 JSON 字符串中提取标量值。

print json_value(@param, '$.info.address.town');
print json_value(@param, '$.info.tags[1]');

返回:Bristol,Water polo

3、JSON_QUERY :从 JSON 字符串中提取对象或数组。

print json_query(@param, '$.info');
复制代码
{    
       "type":1,  
       "address":{    
         "town":"Bristol",  
          "county":"Avon",  
          "country":"England"  
        },  
        "tags":["Sport", "Water polo"]  
}
复制代码

4、JSON_MODIFY :更新 JSON 字符串中属性的值,并返回已更新的 JSON 字符串。

print json_modify(@param, '$.info.address.town', 'London');

返回:

复制代码
{  
     "info":{    
       "type":1,  
       "address":{    
         "town":"London",  
         "county":"Avon",  
          "country":"England"  
        },  
        "tags":["Sport", "Water polo"]  
     },  
     "type":"Basic"  
  }
复制代码

四、注意事项

SQL2016 中的新增的内置JSON进行了简单介绍,主要有如下要点:

  • JSON能在SQLServer2016中高效的使用,但是JSON并不是原生数据类型;
  • 如果使用JSON格式必须为输出结果是表达式的提供别名;
  • JSON_VALUE 和 JSON_QUERY  函数转移和获取Varchar格式的数据,因此必须将数据转译成你需要的类型。
  • 在计算列的帮助下查询JSON可以使用索引进行优化。

关于.NET异常处理的思考 - 彭泽0902 - 博客园

mikel阅读(691)

来源: 关于.NET异常处理的思考 – 彭泽0902 – 博客园

年关将至,对于大部分程序员来说,马上就可以闲下来一段时间了,然而在这个闲暇的时间里,唯有争论哪门语言更好可以消磨时光,估计最近会有很多关于java与.net的博文出现,我表示要作为一个吃瓜群众,静静的看着大佬们发表心情。

以上的废话说的够多了,这里就不再废话了,还是切入正题吧。

在项目开发中,对于系统和代码的稳定性和容错性都是有对应的要求。实际开发项目中的代码与样例代码的区别,更多的是在代码的运行的稳定性、容错性、扩展性的比较。因为对于实现一个功能来说,实现功能的核心代码是一样的,可能只是在写法上优化而已,但是在实现某一个操作上使用的类来说,这一点是绝大多数时候是一样的。这样看来,我们在实际开发的过程中,需要考虑的问题比较多,已经不仅仅局限于某一具体的功能实现,更多的是代码的稳定性和扩展性考虑。

以上是在实际开发中需要面对的问题,笔者在最近的博文中,也在考虑这个异常到底需要怎么去写,以及异常到底需要怎么去理解,在博文中,也有不少的园友对异常的写法和处理提出了自己的意见,在这里我就写一下自己的一些理解,可能写的比较浅显和粗略,但是只当是一个引子,可以引出大佬们来谈谈自己的实际项目经验。希望对大家有一个帮助,也欢迎大家提出自己的想法和意见,分享自己的知识和见解。

一.DotNET异常的概述:

谈到异常,我们就需要知道什么叫做异常,万事万物如果我们想去学习,就应该知道我们要学习的东西是什么,这样在心里也好有一个大概的认知。异常是指成员没有完成它的名称宣称可以完成的行动。在.NET中,构造器、获取和设置属性、添加和删除事件、调用操作符重载和调用转换操作符等等都没有办法返回错误代码,但是在这些构造中又需要报告错误,那就必须提供异常处理机制。

在异常的处理中,我们经常使用到的三个块分别是:try块;catch块;finally块。这三个块可以一起使用,也可以不写catch块使用,异常处理块可以嵌套使用,具体的方法在下面会介绍到。

在异常的处理机制中,一般有三种选择:重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生;抛出一个不同的异常,想调用栈高一层代码提供更丰富的异常信息;让线程从catch块的底部退出。

有关异常的处理方式,有一些指导性的建议。

       1.恰当的使用finally块:

finally块可以保证不管线程抛出什么类型的异常都可以被执行,finall块一般用来做清理那些已经成功启动的操作,然后再返回调用者或者finally块之后的代码。

       2.异常捕捉需适当:

为什么要适当的捕捉异常呢?如下代码,因为我们不能什么异常都去捕捉,在捕获异常后,我们需要去处理这些异常,如果我们将所有的异常都捕捉后,但是没有预见会发生的异常,我们就没有办法去处理这些异常。

如果应用程序代码抛出一个异常,应用程序的另一端则可能预期要捕捉这个异常,因此不能写成一个”大小通吃“的异常块,应该允许该异常在调用栈中向上移动,让应用程序代码针对性地处理这个异常。

在catch块中,可以使用System.Exception捕捉异常,但是最好在catch块末尾重新抛出异常。至于原因在后面会讲解到。

复制代码
          try
            {
                var hkml = GetRegistryKey(rootKey);
                var subkey = hkml.CreateSubKey(subKey);
                if (subkey != null && keyName != string.Empty)
                    subkey.SetValue(keyName, keyValue, RegistryValueKind.String);
            }
            catch (Exception ex)
            {
                Log4Helper.Error("创建注册表错误" + ex);
                throw new Exception(ex.Message,ex);
            }
复制代码

       3.从异常中恢复:

我们在捕获异常后,可以针对性的写一些异常恢复的代码,可以让程序继续运行。在捕获异常时,需要捕获具体的异常,充分的掌握在什么情况下会抛出异常,并知道从捕获的异常类型派生出了那些类型。除非在catch块的末尾重新抛出异常,否则不要处理或捕获System.Exception异常。

      4.维持状态:

一般情况下,我们完成一个操作或者一个方法时,需要调用几个方法组合完成,在执行的过程中会出现前面几个方法完成,后面的方法发生异常。发生不可恢复的异常时回滚部分完成的操作,因为我们需要恢复信息,所有我们在捕获异常时,需要捕获所有的异常信息。

      5.隐藏实现细节来维持契约:

有时可能需要捕捉一个异常并重新抛出一个不同的异常,这样可以维系方法的契约,抛出的心异常类型地应该是一个具体的异常。看如下代码:

复制代码
FileStream fs = null;
            try
            {
                fs = FileStream();
              
            }
            catch (FileNotFoundException e)
            {
          //抛出一个不同的异常,将异常信息包含在其中,并将原来的异常设置为内部异常
                throw new NameNotFoundException();
            }
            catch (IOException e)
            {
 
               //抛出一个不同的异常,将异常信息包含在其中,并将原来的异常设置为内部异常
             throw new NameNotFoundException(); 
            } 
            finally 
            {
               if (fs != null) 
                { 
               fs.close(); 
            } 
            }
复制代码

以上的代码只是在说明一种处理方式。应该让抛出的所有异常都沿着方法的调用栈向上传递,而不是把他们”吞噬“了之后抛出一个新的异常。如果一个类型构造器抛出一个异常,而且该异常未在类型构造器方法中捕获,CLR就会在内部捕获该异常,并改为抛出一个新的TypeInitialztionException。

二.DotNET异常的常用处理机制:

在代码发生异常后,我们需要去处理这个异常,如果一个异常没有得到及时的处理,CLR会终止进程。在异常的处理中,我们可以在一个线程捕获异常,在另一个线程中重新抛出异常。异常抛出时,CLR会在调用栈中向上查找与抛出的异常类型匹配的catch块。如果没有任何catch块匹配抛出的异常类型,就发生一个未处理异常。CLR检测到进程中的任何线程有一个位处理异常,都会终止进程。

     1.异常处理块:

(1).try块:包含代码通常需要执行一些通用的资源清理操作,或者需要从异常中恢复,或者两者都需要。try块还可以包含也许会抛出异常的代码。一个try块至少有一个关联的catch块或finall块。

(2).catch块:包含的是响应一个异常需要执行的代码。catch关键字后的圆括号中的表达式是捕获类型。捕获类型从System.Exception或者其派生类指定。CLR自上而下搜素一个匹配的catch块,所以应该教具体的异常放在顶部。一旦CLR找到一个具有匹配捕获类型的catch块,就会执行内层所有finally块中的代码,”内层finally“是指抛出异常的tey块开始,到匹配异常的catch块之间的所有finally块。

使用System.Exception捕捉异常后,可以采用在catch块的末尾重新抛出异常,因为如果我们在捕获Exception异常后,没有及时的处理或者终止程序,这一异常可能对程序造成很大的安全隐患,Exception类是所有异常的基类,可以捕获程序中所有的异常,如果出现较大的异常,我们没有及时的处理,造成的问题是巨大的。

(3).finally块:包含的代码是保证会执行的代码。finally块的所有代码执行完毕后,线程退出finally块,执行紧跟在finally块之后的语句。如果不存在finally块,线程将从最后一个catch块之后的语句开始执行。

备注:异常块可以组合和嵌套,对于三个异常块的样例,在这里就不做介绍,异常的嵌套可以防止在处理异常的时候再次出现未处理的异常,以上这些就不再赘述。

    2.异常处理实例:

       (1).异常处理扩展方法:
复制代码
        /// <summary>
        ///  格式化异常消息
        /// </summary>
        /// <param name="e">异常对象</param>
        /// <param name="isHideStackTrace">是否隐藏异常规模信息</param>
        /// <returns>格式化后的异常信息字符串</returns>
        public static string FormatMessage(this Exception e, bool isHideStackTrace = false)
        {
            var sb = new StringBuilder();
            var count = 0;
            var appString = string.Empty;
            while (e != null)
            {
                if (count > 0)
                {
                    appString += "  ";
                }
                sb.AppendLine(string.Format("{0}异常消息:{1}", appString, e.Message));
                sb.AppendLine(string.Format("{0}异常类型:{1}", appString, e.GetType().FullName));
                sb.AppendLine(string.Format("{0}异常方法:{1}", appString, (e.TargetSite == null ? null : e.TargetSite.Name)));
                sb.AppendLine(string.Format("{0}异常源:{1}", appString, e.Source));
                if (!isHideStackTrace && e.StackTrace != null)
                {
                    sb.AppendLine(string.Format("{0}异常堆栈:{1}", appString, e.StackTrace));
                }
                if (e.InnerException != null)
                {
                    sb.AppendLine(string.Format("{0}内部异常:", appString));
                    count++;
                }
                e = e.InnerException;
            }
            return sb.ToString();
        }
复制代码
     (2).验证异常:
复制代码
       /// <summary>
        /// 检查字符串是空的或空的,并抛出一个异常
        /// </summary>
        /// <param name="val">值测试</param>
        /// <param name="paramName">参数检查名称</param>
        public static void CheckNullOrEmpty(string val, string paramName)
        {
            if (string.IsNullOrEmpty(val))
                throw new ArgumentNullException(paramName, "Value can't be null or empty");
        }

        /// <summary>
        /// 请检查参数不是空的或空的,并抛出异常
        /// </summary>
        /// <param name="param">检查值</param>
        /// <param name="paramName">参数名称</param>
        public static void CheckNullParam(string param, string paramName)
        {
            if (string.IsNullOrEmpty(param))
                throw new ArgumentNullException(paramName, paramName + " can't be neither null nor empty");
        }

        /// <summary>
        /// 检查参数不是无效,并抛出一个异常
        /// </summary>
        /// <param name="param">检查值</param>
        /// <param name="paramName">参数名称</param>
        public static void CheckNullParam(object param, string paramName)
        {
            if (param == null)
                throw new ArgumentNullException(paramName, paramName + " can't be null");
        }

        /// <summary>
        /// 请检查参数1不同于参数2
        /// </summary>
        /// <param name="param1">值1测试</param>
        /// <param name="param1Name">name of value 1</param>
        /// <param name="param2">value 2 to test</param>
        /// <param name="param2Name">name of vlaue 2</param>
        public static void CheckDifferentsParams(object param1, string param1Name, object param2, string param2Name)
        {
            if (param1 == param2) {
                throw new ArgumentException(param1Name + " can't be the same as " + param2Name,
                    param1Name + " and " + param2Name);
            }
        }

        /// <summary>
        /// 检查一个整数值是正的(0或更大)
        /// </summary>
        /// <param name="val">整数测试</param>
        public static void PositiveValue(int val)
        {
            if (val < 0)
                throw new ArgumentException("The value must be greater than or equal to 0.");
        }
复制代码
     (3).Try-Catch扩展操作:
复制代码
        /// <summary>
        ///     对某对象执行指定功能与后续功能,并处理异常情况
        /// </summary>
        /// <typeparam name="T">对象类型</typeparam>
        /// <param name="source">值</param>
        /// <param name="action">要对值执行的主功能代码</param>
        /// <param name="failureAction">catch中的功能代码</param>
        /// <param name="successAction">主功能代码成功后执行的功能代码</param>
        /// <returns>主功能代码是否顺利执行</returns>
        public static bool TryCatch<T>(this T source, Action<T> action, Action<Exception> failureAction,
            Action<T> successAction) where T : class
        {
            bool result;
            try
            {
                action(source);
                successAction(source);
                result = true;
            }
            catch (Exception obj)
            {
                failureAction(obj);
                result = false;
            }
            return result;
        }

        /// <summary>
        ///     对某对象执行指定功能,并处理异常情况
        /// </summary>
        /// <typeparam name="T">对象类型</typeparam>
        /// <param name="source">值</param>
        /// <param name="action">要对值执行的主功能代码</param>
        /// <param name="failureAction">catch中的功能代码</param>
        /// <returns>主功能代码是否顺利执行</returns>
        public static bool TryCatch<T>(this T source, Action<T> action, Action<Exception> failureAction) where T : class
        {
            return source.TryCatch(action,
                failureAction,
                obj => { });
        }

        /// <summary>
        ///     对某对象执行指定功能,并处理异常情况与返回值
        /// </summary>
        /// <typeparam name="T">对象类型</typeparam>
        /// <typeparam name="TResult">返回值类型</typeparam>
        /// <param name="source">值</param>
        /// <param name="func">要对值执行的主功能代码</param>
        /// <param name="failureAction">catch中的功能代码</param>
        /// <param name="successAction">主功能代码成功后执行的功能代码</param>
        /// <returns>功能代码的返回值,如果出现异常,则返回对象类型的默认值</returns>
        public static TResult TryCatch<T, TResult>(this T source, Func<T, TResult> func, Action<Exception> failureAction,
            Action<T> successAction)
            where T : class
        {
            TResult result;
            try
            {
                var u = func(source);
                successAction(source);
                result = u;
            }
            catch (Exception obj)
            {
                failureAction(obj);
                result = default(TResult);
            }
            return result;
        }

        /// <summary>
        ///     对某对象执行指定功能,并处理异常情况与返回值
        /// </summary>
        /// <typeparam name="T">对象类型</typeparam>
        /// <typeparam name="TResult">返回值类型</typeparam>
        /// <param name="source">值</param>
        /// <param name="func">要对值执行的主功能代码</param>
        /// <param name="failureAction">catch中的功能代码</param>
        /// <returns>功能代码的返回值,如果出现异常,则返回对象类型的默认值</returns>
        public static TResult TryCatch<T, TResult>(this T source, Func<T, TResult> func, Action<Exception> failureAction)
            where T : class
        {
            return source.TryCatch(func,
                failureAction,
                obj => { });
        }
复制代码

本文没有具体介绍try,catch,finally的使用,而是给出一些比较通用的方法,主要是一般的开发者对于三个块的使用都有一个认识,就不再做重复的介绍。

三.DotNET的Exception类分析:

CLR允许异常抛出任何类型的实例,这里我们介绍一个System.Exception类:

      1.Message属性:指出抛出异常的原因。

复制代码
[__DynamicallyInvokable]
public virtual string Message
{
    [__DynamicallyInvokable]
    get
    {
        if (this._message != null)
        {
            return this._message;
        }
        if (this._className == null)
        {
            this._className = this.GetClassName();
        }
        return Environment.GetRuntimeResourceString("Exception_WasThrown", new object[] { this._className });
    }
}
复制代码

由以上的代码可以看出,Message只具有get属性,所以message是只读属性。GetClassName()获取异常的类。GetRuntimeResourceString()获取运行时资源字符串。

     2.StackTrace属性:包含抛出异常之前调用过的所有方法的名称和签名。

复制代码
public static string StackTrace
{
    [SecuritySafeCritical]
    get
    {
        new EnvironmentPermission(PermissionState.Unrestricted).Demand();
        return GetStackTrace(null, true);
    }
}
复制代码

EnvironmentPermission()用于环境限制,PermissionState.Unrestricted设置权限状态,GetStackTrace()获取堆栈跟踪,具体看一下GetStackTrace()的代码。

复制代码
internal static string GetStackTrace(Exception e, bool needFileInfo)
{
    StackTrace trace;
    if (e == null)
    {
        trace = new StackTrace(needFileInfo);
    }
    else
    {
        trace = new StackTrace(e, needFileInfo);
    }
    return trace.ToString(StackTrace.TraceFormat.Normal);
}
复制代码
复制代码
public StackTrace(Exception e, bool fNeedFileInfo)
{
    if (e == null)
    {
        throw new ArgumentNullException("e");
    }
    this.m_iNumOfFrames = 0;
    this.m_iMethodsToSkip = 0;
    this.CaptureStackTrace(0, fNeedFileInfo, null, e);
}
复制代码

以上是获取堆栈跟踪方法的具体实现,此方法主要用户调试的时候。

     3.GetBaseException()获取基础异常信息方法。

复制代码
[__DynamicallyInvokable]
public virtual Exception GetBaseException()
{
    Exception innerException = this.InnerException;
    Exception exception2 = this;
    while (innerException != null)
    {
        exception2 = innerException;
        innerException = innerException.InnerException;
    }
    return exception2;
}
复制代码

InnerException属性是内在异常,这是一个虚方法,在这里被重写。具体看一下InnerException属性。

复制代码
[__DynamicallyInvokable]
public Exception InnerException
{
    [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    get
    {
        return this._innerException;
    }
}
复制代码

    4.ToString()将异常信息格式化。

复制代码
private string ToString(bool needFileLineInfo, bool needMessage)
{
    string className;
    string str = needMessage ? this.Message : null;
    if ((str == null) || (str.Length <= 0))
    {
        className = this.GetClassName();
    }
    else
    {
        className = this.GetClassName() + ": " + str;
    }
    if (this._innerException != null)
    {
        className = className + " ---> " + this._innerException.ToString(needFileLineInfo, needMessage) + Environment.NewLine + "   " + Environment.GetRuntimeResourceString("Exception_EndOfInnerExceptionStack");
    }
    string stackTrace = this.GetStackTrace(needFileLineInfo);
    if (stackTrace != null)
    {
        className = className + Environment.NewLine + stackTrace;
    }
    return className;
}
复制代码

在此方法中,将获取的异常信息进行格式化为字符串,this.GetClassName() 获取异常类的相关信息。

以上我们注意到[__DynamicallyInvokable]定制属性,我们看一下具体的实现代码:

[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public __DynamicallyInvokableAttribute()
{
}

以上我们主要注释部分,”图像边界“这个属性的相关信息,请参见《Via CLR C#》,这里就不做具体的介绍。

四.总结:

以上在对异常的介绍中,主要介绍了CLR的异常处理机制,一些较为通用的异常代码,以及对Exception类的介绍。在实际的项目中,我们一般不要将异常直接抛出给客户,我们在编写程序时,已经考虑程序的容错性,在程序捕获到异常后,尽量去恢复程序,或者将异常信息写入日志,让程序进入错误页。如果出现比较严重的异常,最后将异常抛出,终止程序。

(1条消息)索引(从零开始)必须大于或等于零,且小于参数列表的大小。_数据库_pyy的博客-CSDN博客

mikel阅读(1608)

来源: (1条消息)索引(从零开始)必须大于或等于零,且小于参数列表的大小。_数据库_pyy的博客-CSDN博客

System.FormatException”类型的异常在 mscorlib.dll 中发生,但未在用户代码中进行处理

其他信息: 索引(从零开始)必须大于或等于零,且小于参数列表的大小。

去掉“,{2}”

return string.Format(“[{0},{1}]]”, strb.ToString(), strData);

修改如下:
————————————————
版权声明:本文为CSDN博主「u010082526」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010082526/article/details/78920563

因一纸设计稿,我把竞品APP扒得裤衩不剩(中)_移动开发_Coder-Pig的猪栏-CSDN博客

mikel阅读(1134)

来源: 因一纸设计稿,我把竞品APP扒得裤衩不剩(中)_移动开发_Coder-Pig的猪栏-CSDN博客

严正声明:

1、相关破解技术仅限于技术研究使用,不得用于非法目的,否则后果自负。
2、笔者仅出于对技术的好奇,无恶意破坏APP,尊重原开发者的劳动成果,未用于商业用途。
0x1、无形之刃,最为致命 => 碎碎念
上一篇文章《因一纸设计稿,我把竞品APP扒得裤衩不剩(上)》是一篇比较简单的:

jsw => 技师文,呸,
jsw => 记述文,呸呸,
jsw => 技术文,呸呸呸,这什么垃圾输入法!
技术文,但是这评论区的风气,貌似有点不对???

冤枉啊,小弟真没去过这种地方,也没体验过这种“服务”,只是道听途说,可能:

我描述得「绘声绘色」,加之各位看官「浮想联翩」,才会觉得「煞有介事」。

em…那个,可以扶下我起来么,那个…跪久了…腿有点麻…

顺带恭喜下:FPX 3-0 G2,喜提S9总冠军,FPX牛逼!!!破音!!!

亚索的快乐你不懂~

哈哈,回到本文:

发现 => 很多童鞋对APP逆向很感兴趣;
但是 => 网上关于APP逆向文章的比较零散;
不知 => 如何入手,毕竟逆向的水可深了;
笔者 => 也只是个小白玩家,兴趣使然玩玩而已;
分享 => 目前会的一些「Android APP基础逆向姿势」;
还望 => 各位真丶逆向大佬轻喷;
如有 => 有更好的工具或者方法安利,欢迎评论区指出,谢谢~
顺带分享几个笔者常逛的逆向论坛:

看雪论坛:https://bbs.pediy.com/
吾爱破解:https://www.52pojie.cn/
蚁安网:https://bbs.mayidui.net/
逆向大佬姜维:http://www.520monkey.com/
贴心提醒:

此文内容较多,可能会有些枯燥,建议先点赞收藏,茶余饭后再慢慢品尝~

0x2、提莫队长,正在待命 => 硬件准备
在开始折腾Android APP逆向前,你需要:

1、一台「具有完整Root权限」的Android手机,注意是「完整Root」权限!!!

比如「魅族手机」在设置->安全->Root权限,中可以开启Root权限,但是却是「阉割的Root权限」,安装SuperSu重启后就一直卡气球。

2、怎么Root?根据自己的手机机型百度和逛各种搞机论坛吧(不要问我!)一般的常见的流程:

解BL锁(BootLoader) -> 刷第三方Recovery(如TWRP) -> 卡刷Maglisk 或 SuperSU(Android 8.0以前)

3、不要轻易尝试使用哪种「一键Root」的软件(大部分是毒瘤,如KingRoot),当然不是就不能用,可以过河拆桥,比如我的魅蓝E2的Root流程:

安装KingRoot(v5.0)授予Root权限,修复Root权限异常,此时就有完整Root权限了;
接着用「移除KingRoot」删掉KingRoot,此时还有完整Root权限;
安装SuperSu(v2.8.2),常规方式更新二进制文件,重启,Root完成。
4、推荐些能Root的手机?

Google Pixel亲儿子(真原生,香,就是性价比不高),小米,一加 等。

5、没钱买Android机或者已经有不能Root的手机了,可以试试「Android模拟器」

AS自带的AVD模拟器Root可以参见《搞机:AS自带模拟器AVD Root 和 Xposed安装》
也可以使用其他第三方的安卓模拟器,比如「夜游安卓模拟器、BlueStacks蓝叠」等。

0x3、一点寒芒先到,随后枪出如龙 => 概念与名词
在开始折腾APP逆向前,先来了解一些概念与名词~

① APK文件里都有什么?
获取APK的渠道:酷安、应用宝,豌豆荚等应用市场下载,有些还提供「应用历史版本」下载。
APK本质上是一个「压缩包」,把「.apk后缀」改为「.zip后缀」后解压,可以看到如下目录结构 (可能还有其他文件):

简单介绍下:

② 编译APK和反编译APK
所谓的「编译」,就是把「源码、资源文件等」按照一定的「规则」打包成APK,官网 提供了详细的编译构建过程图:

简述下大概流程:

Step 1:资源文件处理「AAPT」

assets会原封不动地打包在APK中;
res中每一个资源会赋予资源ID,以常量形式定义在R.java中,生成一个resource.arsc文件(资源索引表)。
Step 2:aidl文件「aidl」

将aidl后缀的文件转换为可用于进程通信的C/S端Java代码。
Step 3:Source Code「Java Compiler」

编译生成.class文件。
Step 4:代码混淆「ProGuard」(可选)

增加反编译难度,命名缩短为1-2个字母的名字,压缩(移除无效类、属性、方法等),优化bytecode移除没用的结构。
Step 5:转换为dex「dx.bat」

把所有claas文件转换为classes.dex文件,class -> Dalvik字节码,生成常量池,消除冗余数据等。(方法数超65535会生成多个dex文件)
Step 6:打包「ApkBuilder」

把resources.arsc、classes.dex、其他的资源一块打包生成未签名apk。
Step 7:签名「Jarsigner」

对未签名apk进行Debug或release签名。
Step 8:对齐优化「zipalign」

使apk中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问apk文件时会更快。
如果想了解更多编译构建流程可移步至:《10分钟了解Android项目构建流程》
而「反编译」则是反过来了,通过一些反编译工具,提取出源码,转换过程如下:

「APK ====> Dex ====> Jar(class文件)/Smali ==> Java源码」。

③ 加固和脱壳
APK可以说是每个Android开发仔的「心血结晶」,把各种自己觉得「牛逼哄哄的奇淫巧技」封装其中。但总有些「心怀叵测」想去搞你的APP,通过一些「反编译工具」获取你的源码,然后为所欲为:

加广告:你应用免费,给你加点广告,亦或者改成付费,然后下载量比你的多,气不气?
破解付费:你应用收费,Hook掉你的检测方法,发个破解包,还到处传播,气不气?
恶意攻击:逆向得出请求接口规律,批量短信验证注册,耗光你的短信池等,气不气?
这种「恶劣」的行径令人「气愤」像极了某类经典「动作电影」里的桥段:

男子上进,努力工作,妹子贤惠,料理家务;
妹子每天做好饭菜,等男子回来,一起吃饭,满怀憧憬,畅谈以后的二人世界;
酒饱饭后,温饱思XX,不可描述一番,却被「居心叵测」的邻居给盯上了;
和往常一样,男子出门上班,妹子在家做家务,晾衣服;
邻居 上线,用「谎言」诱骗妹子开门,接而挤门而入;
用「暴力和胁迫」,无视妹子的やめて和反抗,违背个人意愿;
粗暴地把衣服一件件褪去,仅剩下那「万恶的马赛克」;
守护着最后的一处「绝对领域」;
在几番不可描述后,把妹子占为己有,然后像玩物般戏耍。
看着妹子「因情绪过激而身体抽搐」,哭得「梨花带雨」,不禁让人「心生怜惜」,像我这种感性的蓝孩子:

总会忍不住抽上几张抽纸,“静静抹泪”,擦拭完,顿觉索然无味,一片空明,然后开始反思:

为什么那个邻居不是我?呸呸呸…

除了「同情女主」和「斥责坏人」外,应该如何避免这种事情的发生呢?

1、花点钱,请个「保镖」看门,坏人想进来要先过保镖这一关;
2、给妹子「加个锁」,让坏人无法不可描述,只能望而兴叹。
可以把例子中的「妹子」看做是我们编写的「APK」,而「请保镖」和「加锁的操作」则可以看做是「APK加固」,另外加固又称「加壳」,壳的定义:

一段专门负责「保护软件不被非法修改或反编译的程序」,一般先于程序运行,拿到控制权,然后完成它们保护软件的任务。

有「加壳」,自然也有「脱壳」,即去掉这层壳,拿到源码,也称为「砸壳」。

关于加固技术的发展,看雪上有篇:《一张表格看懂:市面上最为常见的 Android 安装包(APK)五代加固技术发展历程及优缺点比较》,不过图不怎么清晰,笔者重新排版了一下,有兴趣的读者可以看看:

④ 混淆与反混淆
「混淆」可以类比为上面「万恶的马赛克」,阻碍人类进步的绊脚石。而混淆则是增加了反编译的难度,同理,「反混淆」则对应「去除马赛克」,试图还原它原来的样子。

0x4、发动机已启动,随时可以出发 => 获得APP源码

加固虽然能在一定程度上「防止反编译和二次打包」,但加固后的APP可能会带来一些问题:

体积增大,启动速度变慢,兼容问题等

网上「免费加固」方案有很多,脱壳教程也是烂大街,而且有些恶心的第三方加固还会给你加点料(360加固锁屏广告),而使用「企业级的加固」,则需要支付不菲的费用,所以很多APP直接选择了「裸奔」。先来讲解一下未加固的怎么获取源码吧~

① 未加固(笔者使用的工具:apktool + jadx)
使用apktool:获取「素材资源,AndroidManifest.xml以及smail代码」
使用jadx:把「classes.dex」转换为「.java」代码
使用Jadx的注意事项:

使用jadx-gui可直接打开apk查看源码,但如果APK比较大(classes.dex有好几个),会直接卡死(比如微信),笔者的做法是命令行一个个dex文件去反编译,最后再把反编译的文件夹整合到同一个目录下。

这样的操作繁琐且重复,最适合批处理了,遂写了个反编译的批处理脚本(取需):

“””
自动解压apk,批量使用jadx进行反编译,结果代码汇总
“””
import os
import shutil
import zipfile
from datetime import datetime

apk_file_dict = {} # APK路径字典

# 遍历构造APK路径字典(构造文件路径列表,过滤apk,拼接)
def init_apk_dict(file_dir):
apk_path_list = list(filter(lambda fp: fp.endswith(“.apk”),
list(map(lambda x: os.path.join(file_dir, x), os.listdir(file_dir)))))
index_list = [str(x) for x in range(1, len(apk_path_list) + 1)]
return dict(zip(index_list, apk_path_list))

# 移动文件夹
def move_dir(origin_dir, finally_dir):
shutil.move(origin_dir, finally_dir)

# 如果文件夹存在删除重建
def deal_dir_existed(path):
if os.path.exists(path):
print(“检测到文件夹【%s】已存在,执行删除…” % path)
shutil.rmtree(path)
os.makedirs(path)

# 判断目录是否存在,不存在则创建
def is_dir_existed(path, mkdir=True):
if mkdir:
if not os.path.exists(path):
os.makedirs(path)
else:
return os.path.exists(path)

# 获取目录下的所有文件路径
def fetch_all_file(file_dir):
return list(map(lambda x: os.path.join(file_dir, x), os.listdir(file_dir)))

# 解压文件到特定路径中
def unzip_file(file_name, output_dir):
print(“开始解压文件…”)
f = zipfile.ZipFile(file_name, ‘r’)
for file in f.namelist():
f.extract(file, os.path.join(os.getcwd(), output_dir))
print(“文件解压完毕…”)

if __name__ == ‘__main__’:
print(“遍历当前目录下所有APK…”)
apk_file_dict = init_apk_dict(os.getcwd())
print(“遍历完毕…\n\n============ 当前目录下所有的APK ============\n”)
for (k, v) in apk_file_dict.items():
print(“%s.%s” % (k, v.split(os.sep)[-1]))
print(“\n%s” % (“=” * 45))
choice_pos = input(“%s” % “请输入需要反编译APK的数字编号:”)
print(“=” * 45, )
choice_apk = apk_file_dict.get(choice_pos)
apk_name = choice_apk.split(os.sep)[-1][:-4] # APK名字

# 创建相关文件夹
crack_dir = os.path.join(os.getcwd(), apk_name) # 工程根目录
deal_dir_existed(crack_dir)
crack_apktool_dir = os.path.join(crack_dir, “apktool” + os.sep) # APKTool反编译目录
deal_dir_existed(crack_apktool_dir)
crack_jadx_dir = os.path.join(crack_dir, “jadx” + os.sep) # JADX反编译目录
deal_dir_existed(crack_jadx_dir)
crack_temp_dir = os.path.join(crack_dir, “temp” + os.sep) # 解压后文件的临时存储路径
deal_dir_existed(crack_temp_dir)

# 利用APKTool提取资源文件
begin = datetime.now() # 计时
print(“APKTool提取资源文件…”)
os.system(“./apktool d %s -f -o %s” % (choice_apk, crack_apktool_dir))

# 复制一份AndroidManifest.xml、res、assets文件到外部
shutil.copy(os.path.join(crack_apktool_dir, “AndroidManifest.xml”), os.path.join(crack_dir, “AndroidManifest.xml”))
shutil.copytree(os.path.join(crack_apktool_dir, “res” + os.sep), os.path.join(crack_dir, “res” + os.sep))
shutil.copytree(os.path.join(crack_apktool_dir, “assets” + os.sep), os.path.join(crack_dir, “assets” + os.sep))
print(“资源文件提取完毕”)

# 利用jadx反编译源码
print(“JADX反编译提取源码…”)
choice_apk_zip = shutil.copy(choice_apk, choice_apk.replace(“.apk”, “.zip”))
unzip_file(choice_apk_zip, crack_temp_dir)
print(“开始批量反编译dex文件”)
for dex in list(filter(lambda fp: fp.endswith(“.dex”), fetch_all_file(crack_temp_dir))):
os.system(
“./jadx -d {0} {1}”.format(os.path.join(crack_jadx_dir, dex.split(os.sep)[-1][:-4]), dex))
print(“所有dex文件反编译完毕”)
# 将资源文件移入
shutil.move(os.path.join(crack_dir, “AndroidManifest.xml”), os.path.join(crack_jadx_dir, “AndroidManifest.xml”))
shutil.move(os.path.join(crack_dir, “res” + os.sep), os.path.join(crack_jadx_dir, “res” + os.sep))
shutil.move(os.path.join(crack_dir, “assets” + os.sep), os.path.join(crack_jadx_dir, “assets” + os.sep))
# 删除临时文件夹,压缩文件
shutil.rmtree(crack_temp_dir)
os.unlink(choice_apk_zip)
end = datetime.now()
print(“收尾操作~~~\n反编译完成,总耗时:%s秒” % (end – begin).seconds)

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
执行前,你需要把apktool相关的东西,丢到jadx/build/jadx/bin目录下,如图所示:

接着终端键入:python3 auto_extract_apk.py,回车后输入对应编号,回车开始编译:

静待片刻后:

Tips:这里没有把多个classes文件夹整合到一起,是因为有些APP会出现合并冲突。

打开反编译后的目录,有如下两个文件夹:

按照自己的需要用Android Studio打开其中一个就好了:

apktool目录:apktool反编译后的内容,主要用于smail动态调试。
jadx目录:反编译成Java的内容。
② 反混淆(simplefy、Deguard)
代码是拿到了,但是打开代码,「一堆的abcd」,跟到眼花,可以试下「反混淆」,方案有两类,一种是通过「代码逆推」出名字,另一种是通过「统计逆推」出名字。

第一种方案的工具有很多(Jeb2,simplify等),前者付费需破解,Java版本有限制,Mac配置有点麻烦,故笔者用的是后者:「Simplefy」,Github仓库:https://github.com/CalebFenton/simplify,使用方法也很简单:

打开终端依次键入:

# 拉取仓库代码
git clone –recursive https://github.com/CalebFenton/simplify.git

# 来到目录下
cd simplify

# 编译
./gradlew fatjar
1
2
3
4
5
6
7
8
编译后完,执行下述指令即可反混淆APK:

# 反混淆APK(需要反混淆的APK,反混淆后的APK名)
./gradlew build && cp xxx.apk yyy.apk
1
2
静待反混淆完毕,接着反编译批处理脚本走一波,打开MapFragment比对下:

相比混淆前,多了一些变量名,当然也不是完全的,偶尔还是有abcd,但是可读性稍微提高了些,比如查找的时候不用在一个个adcd排除,但是,编译挺耗时的,而且我的电脑风扇呼呼呼地响。

第二种是通过统计的方法,利用统计推断出名字:DEGUARD:http://apk-deguard.com/,打开官网:

选择需要反混淆的APK后,Upload上传,接着等待处理完成,!!!别关页面!!!

一般需等待1-10分钟,处理完成后,点击output.apk,把APK下载到本地,同样执行批处理脚本反编译一波,和simplefy反混淆后的代码对比下:

大同小异,另外,反混淆并不能100%还原,而且还可能有些小错误,比如下面的代码:

虽说反编译后的可读性有所提高,但建议还是搭配着混淆的源码看。

③ 脱壳(FDex2,反射助手,dumpDex)
终于来到很多同学期待的脱壳环节,先说明下,笔者只是「工具党」水平,不会Native层的,so文件调试!如果本节的工具,你脱不出来,或者脱出来有问题,笔者也是爱莫能助。看雪有很多帮人脱壳的大佬,可以在上面发个帖子求助下~

1、判断是哪种加固

解压apk后在assets目录下看到so文件,比如360加固宝:libjagu.so和libjiagu_x86.so,百度搜下名字就知道是哪家的加固了,也可以直接用后面讲的「MT文件管理器2.0」直接查看。

2、FDex2脱壳(只适用于Android 7或以下版本,可以脱市面上大多数免费加固,成功率较高,推荐)

有ROOT:安装「XposedInstaller」和「FDex2」
没ROOT:安装「VirtualXposed」「FDex2」
比如:这里有个「360免费版加固的APK」,直接用jadx反编译后导入AS,但是反编译后的classes:

只有这么一丢丢点东西,把「待脱壳应用」安装到手机上,接着用FDex2来脱壳
已Root玩家:XposedInstall启用FDex2插件重启后,按如下步骤脱:

Step 1:FDex2选中待脱壳应用:

Step 2:打开待脱壳应用,接着来到上图的dex输出目录:

Step 3:把整个目录拉到电脑上,这里直接用adb命令拉取:
adb root
adb pull /data/user/0/包名 电脑文件夹
1
2

Step 4:「剔除加固相关的dex」,用jadx-gui依次打开,看到下面这种,直接把dex删掉

Step 5:使用jadx命令反编译dex,顺带改名,命名规则:按照文件大小降序,示例如下:
# 按照文件从大到小排序!!!
jadx aaa.dex -d classes
jadx bbb.dex -d classes1
jadx ccc.dex -d classes2
1
2
3
4
Step 6:删掉没脱壳前反编译项目里的classes,把这几个复制到其中:

行吧,脱壳成功,这里其实还可以还原APK的(二次打包),等下再讲~
未root玩家,安装打开VirtualXposed,添加应用:Fdex2和待脱壳应用

如果炮制,只是dex的路径有些不一样。

3、反射大师(和FDex类似,下载地址:https://www.lanzous.com/b04xxlujg)

注意,同样只支持Android 7.0及以下,adb安装后,xposed启用插件,重启手机,接着打开反射大师:

Step 1:选中待脱壳APP,弹出对话框选择打开

Step 2:点击中间的六芒星,弹出如下对话框,长按「写出DEX」

Step 3:等待写出完毕,可以在/storage/emulated/0中找到导出的dex:

Step 4:pull到电脑上用jadx-gui打开看看:

行吧,脱壳成功,就是我们想要的dex了,另一个classes2.dex则是相关的~:

4、dumpDex脱壳(Github:https://github.com/WrBug/dumpDex/releases)

官方仓库的README.md中有一句:

可以的话建议自己编译,流程也很简单:

# 1、拉取项目代码到本地
git clone https://github.com/WrBug/dumpDex.git

# 2、AS中Open项目,等待编译完成

# 3、删掉build.gradle里签名相关的代码

# 4、点击顶部菜单栏Build -> Build APK,或者直接在终端./gradlew clean build

# 5、adb命令直接把编译生成的apk安装到手机上

# 6、接着来到如下左图路径,把对应的so,通过adb push到目录下:
adb push lib/armeabi-v7a/libnativeDump.so /data/local/tmp
adb push lib/arm64-v8a/libnativeDump.so /data/local/tmp/libnativeDump64.so
# 修改权限
adb shell
su
chmod 777 /data/local/tmp/libnativeDump.so
chmod 777 /data/local/tmp/libnativeDump64.so
# 临时关闭SELinux(重启后会失效,可调用getenforce查询)
setenfore 0

# 7、打开XposedInstaller看已经启用DumpDex插件,是的话重启手机

# 8、开机后,打开想脱壳的应用,不用理闪退,接着打开data/data/包名查看是否有Dump目录

# 9、进入如果出现下图所示的多个dex,说明脱壳成功,否则可能是脱壳失败
# (看是否有报错信息),或者不支持(比如360加固免费版只支持新版,不支持旧版)。
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

另外,脱出来的dex不一定就可用,比如某个用了「腾讯御安全」的应用:

用jadx-gui打开这的dex,一堆这样的错误:

出现这个的原因是「指令集被抽取」,打开smail文件你就知道了:

方法指令都被nop(零)替换了,工具党到这里就可以放弃了,要调试so文件。

Tips:本节用到的东西,都有给出比较官方的下载链接!!!你也可以到公号
「抠腚男孩」输入000,回复对应序号下载,谢谢~

参考文献:

Android打包流程
————————————————
版权声明:本文为CSDN博主「coder-pig」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/coder_pig/article/details/103615488

10分钟了解Android项目构建流程 - 掘金

mikel阅读(1251)

来源: 10分钟了解Android项目构建流程 – 掘金

前言

上两篇博客中提到了构建过程的问题,之前毕业在准备面试的过程中,对这个部分有过较为认真的学习,也进行了博客记录,但是实际工作过程中,如果是在写业务逻辑上,那么这方面的问题接触的就会比较少了。逐渐的淡忘了,其次,之前所写的文章条理性也不是很强,同时,最近准备进行Gradle插件的一系列博客的产出,其中将会涉及到很多与项目构建相关的内容。所以此文也将成为后续文章的一个铺垫。

构建过程

项目的构建: 当我们打开一个项目,我们可以看到的是我们写的Java Code文件or Other JVM Code,资源文件,Build配置文件,但是通过run the project,我们就可以得到一个在我们的Andoid设备上可以运行的Apk,上线应用市场,还需要我们对其进行签名处理,来确保我们App的唯一性和安全性。整个过程就是所谓的项目构建。

如何实现整个构建的过程,对于每一个构建的步骤,都需要相应的功能模块来进行,比如Java Code编译,如何打成dex包等等,而这Android则为我们提供了相应的工具,在Android Studio命令行窗口中,我们可以通过相应的命令行来进行控制,但是,整个构建过程涉及到很多的步骤,很多的工具的使用,如果都通过命令行来进行控制,势必会相当麻烦,因此Androd Studio等IDE则对整个过程进行了一个打包,当我们在Run project的时候,底层的打包工具就会被调用,打包流程都会自动执行。然后我们只需要对构建文件按照自己的需求进行相应的配置,就可以构建出自己所需要的项目。

那么,整个Andoid项目的构建过程中,都执行了那些构建的任务呢?

首先看一下,Google官方为我们提供的详细的构建过程图

构建过程概述

如果你接触Android开发已经有一段时间了,我想当你看到这张图的时候,就会觉得很清晰。但是更多的可能会一头雾水,如果之前没有阅读相关的资料的话,那么,接下来,将针对上述的构建过程,先给出一个概述,这样你将会整个构建流程在心中有一个框架,然后针对其中具体的细节,进行进一步详细的讲解。

图中绿色标注为其中用到的相应工具,蓝色代表的是中间生成的各类文件类型。

  • 首先aapt工具会将资源文件进行转化,生成对应资源ID的R文件和资源文件。
  • adil工具会将其中的aidl接口转化成Java的接口
  • 至此,Java Compiler开始进行Java文件向class文件的转化,将R文件,Java源代码,由aidl转化来的Java接口,统一转化成.class文件。
  • 通过dx工具将class文件转化为dex文件。
  • 此时我们得到了经过处理后的资源文件和一个dex文件,当然,还会存在一些其它的资源文件,这个时候,就是将其打包成一个类似apk的文件。但还并不是直接可以安装在Android系统上的APK文件。
  • 通过签名工具对其进行签名。
  • 通过Zipalign进行优化,提升运行速度(原理后文会提及)。
  • 最终,一个可以安装在我们手机上的APK了。

通过上述讲解,我想对于Android项目的整个构建过程,应该有了一个很清晰的框架了,下面将针对其中的具体的细节,和前面挖的一些坑,来进行更细致的分析,下图是一个Android项目构建过程的详细步骤图。

” alt=”详细构建过程” data-src=”https://user-gold-cdn.xitu.io/2018/1/25/1612d1948620cb96?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”734″ data-height=”800″ />

接下来的分析,我们还是按照上述构建过程概述的顺序和流程,进行具体的分析。

第1步:aapt打包资源文件,生成R.java和编译后的资源(二进制文件)

讲到资源文件的处理,我们先来看一下Android中的资源文件有那些呢?Android应用程序资源可以分为两大类,分别是assets和res:    1. assets类资源放在工程根目录的assets子目录下,它里面保存的是一些原始的文件,可以以任何方式来进行组织。这些文件最终会被原装不动地打包在apk文件中。如果我们要在程序中访问这些文件,那么就需要指定文件名来访问。例如,假设在assets目录下有一个名称为filename的文件,那么就可以使用以下代码来访问它:

AssetManager am= getAssets();    
InputStream is = assset.open("filename");  
复制代码

2. res类资源放在工程根目录的res子目录下,它里面保存的文件大多数都会被编译,并且都会被赋予资源ID。这样我们就可以在程序中通过ID来访问res类的资源。res类资源按照不同的用途可以进一步划分为以下10种子类型: layout(布局文件),drawable,xml,value,menu,raw,color,anim,animator,mipmap。 为了使得一个应用程序能够在运行时同时支持不同的大小和密度的屏幕,以及支持国际化,即支持不同的国家地区和语言,Android应用程序资源的组织方式有18个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在UI上,从而提高用户体验。由于Android应用程序资源的组织方式可以达到18个维度,因此就要求Android资源管理框架能够快速定位最匹配设备当前配置信息的资源来展现在UI上,否则的话,就会影响用户体验。为了支持Android资源管理框架快速定位最匹配资源,Android资源打包工具aapt在编译和打包资源的过程中,会执行以下两个额外的操作:

  • 赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中。
  • 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。包含了所有的id值的数据集合。在该文件中,如果某个id对应的是string,那么该文件会直接包含该值,如果id对应的资源是某个layout或者drawable资源,那么该文件会存入对应资源的路径。

为什么要转化为二进制文件?

  • 二进制格式的XML文件占用空间更小。这是由于所有XML元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。
  • 二进制格式的XML文件解析速度更快。这是由于二进制格式的XML元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度。 有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。

对于具体的一些操作流程,可以参考本人之前的一篇文章APK打包安装过程或者更偏向于源码层级的老罗的文章。(文后参考文献链接)

第2步:aidl

aidl,全名Android Interface Definition Language,即Android接口定义语言。是我们在编写进程间通信的代码的时候,定义的接口。 输入:aidl后缀的文件。输出:可用于进程通信的C/S端java代码,位于build/generated/source/aidl。

第3步:Java源码编译

我们有了R.java和aidl生成的Java文件,再加上工程的源代码,现在可以使用javac进行正常的java编译生成class文件了。

输入:java source的文件夹(另外还包括了build/generated下的:R.java, aidl生成的java文件,以及BuildConfig.java)。输出:对于gradle编译,可以在build/intermediates/classes里,看到输出的class文件。

第4步:代码混淆(proguard)

源码编译之后,我们可能还会对其进行代码的混淆,混淆的作用是增加反编译的难度,同时也将一些代码的命名进行了缩短,减少代码占用的空间。混淆完成之后,会生成一个混淆前后的映射表,这个是用来在反应我们的应用执行的时候的一些堆栈信息,可以将混淆后的信息转化为我们混淆前实际代码中的内容。 而这个过程使用的工具就是ProGuard,是一个开源的Java代码混淆器(obfuscation)。ADT r8开始它被默认集成到了Android SDK中。 其具备三个主要功能。

  • 压缩 – 移除无效的类、属性、方法等
  • 优化 – 优化bytecode移除没用的结构
  • 混淆 – 把类名、属性名、方法名替换为晦涩难懂的1到2个字母的名字 当然它也只能混淆Java代码,Android工程中Native代码,资源文件(图片、xml),它是无法混淆的。而且对于Java的常量值也是无法混淆的,所以不要使用常量定义平文的密码等重要信息。同时对于混淆,我们可以通过代码制定去混淆那些,不去混淆那些。
-keep public class com.rensanning.example.Test
复制代码

第5步:转化为dex

调用dx.bat将所有的class文件转化为classes.dex文件,dx会将class转换为Dalvik字节码,生成常量池,消除冗余数据等。由于dalvik是一种针对嵌入式设备而特殊设计的java虚拟机,所以dex文件与标准的class文件在结构设计上有着本质的区别,当java程序编译成class后,使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右。class文件结构和dex文件结构比对。

Dex和Class比对

第6步:apkbuilder

打包生成APK文件。旧的apkbuilder脚本已经废弃,现在都已经通过sdklib.jar的ApkBuilder类进行打包了。输入为我们之前生成的包含resources.arcs的.ap_文件,上一步生成的dex文件,以及其他资源如jni、.so文件。 大致步骤为 以包含resources.arcs的.ap_文件为基础,new一个ApkBuilder,设置DebugMode

apkBuilder.addZipFile(f);
apkBuilder.addSourceFolder(f);
apkBuilder.addResourcesFromJar(f);
apkBuilder.addNativeLibraries(nativeFileList);
apkBuilder.sealApk(); // 关闭apk文件
generateDependencyFile(depFile, inputPaths, outputFile.getAbsolutePath());

复制代码

第7步:对APK签名

对APK文件进行签名。Android系统在安装APK的时候,首先会检验APK的签名,如果发现签名文件不存在或者校验签名失败,则会拒绝安装,所以应用程序在发布之前一定要进行签名。签名信息中包含有开发者信息,在一定程度上可以防止应用被伪造。对一个APK文件签名之后,APK文件根目录下会增加META-INF目录,该目录下增加三个文件:

  • MANIFEST.MF
  • [CERT].RSA
  • [CERT]

Android系统就是根据这三个文件的内容对APK文件进行签名检验的。签名过程主要利用apksign.jar或者jarsinger.jar两个工具。将根据我们提供的Debug和Release两个版本的Keystore进行相应的签名。

MANIFEST.MF中包含对apk中除了/META-INF文件夹外所有文件的签名值,签名方法是先SHA1()(或其他hash方法)在base64()。存储形式是:Name加[SHA1]-Digest。

[CERT].SF是对MANIFEST.MF文件整体签名以及其中各个条目的签名。一般地,如果是使用工具签名,还多包括一项。就是对MANIFEST.MF头部信息的签名。

[CERT].RSA包含用私钥对[CERT].SF的签名以及包含公钥信息的数字证书。

第8步:zipalign优化

Zipalign是一个Android平台上整理APK文件的工具,它首次被引入是在Android 1.6版本的SDK软件开发工具包中。它能够对打包的Android应用程序进行优化, 以使Android操作系统与应用程序之间的交互作用更有效率,这能够让应用程序和整个系统运行得更快。用Zipalign处理过的应用程序执行时间达到最低限度,当设备运行APK应用程序时占更少的RAM。

  • Zipalign如何进行优化的呢?

调用buildtoolszipalign,对签名后的APK文件进行对齐处理,使APK中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问APK文件时会更快。同时也减少了在设备上运行时的内存消耗。如果对于为何提速不理解,那么可以看下内存对齐的规则以及作用该篇文章,对于内存对齐的好处有比较生动详细的解释。最终这样我们的APK就生成完毕了。

典型的APK中内容

  • AndroidManifest.xml 程序全局配置文件
  • classes.dex Dalvik字节码
  • resources.arsc 资源索引表
  • META-INF该目录下存放的是签名信息
  • res 该目录存放资源文件
  • assets该目录可以存放一些配置或资源文件

总结

至此,对于Andoid项目构建过程的分析已经完成,当然,并没与深入到源码层级的分析,本文的旨在对于构建过程流程上的了解和其中一些优化的原因所在,为后续通过Gradle插件hook构建过程来做一定的操作,做一个铺垫。

参考文章

Android APK 签名原理及方法

改善android性能工具篇【zipalign】

Android应用程序资源的编译和打包过程分析

Android资源管理框架(AssetManager)简要介绍和学习计划

Android代码混淆之ProGuard

作者:Jensen95
链接:https://juejin.im/post/5a69c0ccf265da3e2a0dc9aa
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

小猪的Python学习之旅 —— 3.正则表达式 - 掘金

mikel阅读(1076)

来源: 小猪的Python学习之旅 —— 3.正则表达式 – 掘金

引言

上一节学习了一波urllib库和BeautifulSoup的使用,爬取很多小网站 基本是得心应手的了,而一般我们想爬取的数据基本都是字符串,图片url, 或者是段落文字等,掌握字符串的处理显得尤为重要,说到字符串处理, 除了了解字符串相关的处理函数外,还需要 正则表达式 这枚字符串处理神器! 对于正则表达式,很多人开发者貌似都很抗拒,老说学来干嘛,要什么 正则表达式上网一搜就是啦,对此我只能说2333,爬取网页数据的时候, 你搜下给我看,不同的场景匹配字符串的正则表达式都是不一样的,掌握 正则表达式的编写就显得尤为重要了。本节通过一些有趣的例子帮你 快速上手正则表达式,其实真没想象中那么难!


re模块

Python中通过**re模块**使用正则表达式,该模块提供的几个常用方法:

1.匹配

re.match(pattern, string, flags=0)

  • 参数匹配的正则表达式要匹配的字符串标志位(匹配方法)
  • 尝试从字符串的开头进行匹配,匹配成功会返回一个匹配的对象, 类型是:<class '_sre.SRE_Match'> group与groups

re.search(pattern, string, flags=0)

  • 参数:同上
  • 扫描整个字符串,返回第一个匹配的对象,否则返回None

注意match方法和search的最大区别:match如果开头就不和正则表达式匹配, 直接返回None,而search则是匹配整个字符串!!

2.检索与替换

re.findall(pattern, string, flags=0)

  • 参数:同上
  • 遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回

re.finditer(pattern, string, flags=0)

  • 参数:同上
  • 遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回

re.sub(pattern, repl, string, count=0, flags=0)

  • 参数:repl替换为什么字符串,可以是函数,把匹配到的结果做一些转换; count替换的最大次数,默认0代表替换所有的匹配。
  • 找到所有匹配的子字符串,并替换为新的内容

re.split(pattern, string, maxsplit=0, flags=0)

  • 参数:maxsplit设置分割的数量,默认0代表所有满足匹配的都分割
  • 在正则表达式匹配的地方进行分割,并返回一个列表

3.编译成Pattern对象

对于会多次用到的正则表达式,我们可以调用re的compile()方法编译成 Pattern对象,调用的时候直接Pattern对象.xxx即可,从而提高运行效率。

附:flags(可选标志位)表

多个标志可通过按位OR(|)进行连接,比如:re.I|re.M

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B.
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

2.正则规则详解


1.加在正则字符串前的’r’

为了告诉编译器这个string是个raw string(原字符串),不要转义反斜杠! 比如在raw string里\n是两个字符,”和’n’,不是换行!

2.字符

字符 作用
. 匹配任意一个字符(除了\n)
[] 匹配[]中列举的字符
[^...] 匹配不在[]中列举的字符
\d 匹配数字,0到9
\D 匹配非数字
\s 匹配空白,就是空格和tab
\S 匹配非空白
\w 匹配字母数字或下划线字符,a-z,A-Z,0-9,_
\W 匹配非字母数字或下划线字符
- 匹配范围,比如[a-f]

3.数量

字符 作用(前面三个做了优化,速度会更快,尽量优先用前三个)
* 前面的字符出现了0次或无限次,即可有可无
+ 前面的字符出现了1次或无限次,即最少一次
? 前面的字符出现了0次或者1次,要么不出现,要么只出现一次
{m} 前一个字符出现m次
{m,} 前一个字符至少出现m次
{m,n} 前一个字符出现m到n次

4.边界

字符 作用
^ 字符串开头
$ 字符串结尾
\b 单词边界,即单词和空格间的位置,比如’er\b’
可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’
\B 非单词边界,和上面的\b相反
\A 匹配字符串的开始位置
\Z 匹配字符串的结束位置

5.分组

用**()表示的就是要提取的分组**,一般用于提取子串, 比如:^(\d{3})-(\d{3,8})$:从匹配的字符串中提取出区号和本地号码

字符 作用
匹配左右任意一个表达式
(re) 匹配括号内的表达式,也表示一个组
(?:re) 同上,但是不表示一个组
(?P<name>) 分组起别名,group可以根据别名取出,比如(?P<first>\d)
match后的结果调m.group(‘first’)可以拿到第一个分组中匹配的记过
(?=re) 前向肯定断言,如果当前包含的正则表达式在当前位置成功匹配,
则代表成功,否则失败。一旦该部分正则表达式被匹配引擎尝试过,
就不会继续进行匹配了;剩下的模式在此断言开始的地方继续尝试。
(?!re) 前向否定断言,作用与上面的相反
(?<=re) 后向肯定断言,作用和(?=re)相同,只是方向相反
(?<!re) 后向否定断言,作用于(?!re)相同,只是方向想法

附:group()方法与其他方法详解

不引入括号,增个表达式作为一个组,是group(0)

不引入**()的话,代表整个表达式作为一个组,group = group(0) 如果引入()**的话,会把表达式分为多个分组,比如下面的例子:

输出结果

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe1baa50f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”95″ data-height=”62″ />

除了group方法外还有三个常用的方法:

  • groups(): 从group(1)开始往后的所有的值,返回一个元组
  • start():返回匹配的开始位置
  • end():返回匹配的结束位置
  • span():返回一个元组组,表示匹配位置(开始,结束)

贪婪与非贪婪

正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。 比如:ret = re.match(r'^(\d+)(0*)$','12345000').groups()ß 我们的原意是想得到**(‘12345′,’000’)这样的结果,但是输出 ret我们看到的却是:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe20739ed?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”120″ data-height=”21″ />

,由于贪婪,直接把后面的 0全给匹配了,结果0*只能匹配空字符串了,如果想尽可能少的 匹配,可以在\d+后加上一个?问号*采用非贪婪匹配,改成: r’^(\d+?)(0)$’,输出结果就变成了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe3edaedf?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”118″ data-height=”20″ />

3.正则练习

例子1:简单验证手机号码格式

流程分析:

  • 1.开头可能是带0(长途),86(天朝国际区号),17951(国际电话)中的一个或者一个也没有:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe1a2a757?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”357″ data-height=”24″ />
  • 2.接着1xx,有13x,14x,15x,17x,18x,然后这个x也是取值范围也是不一样的: 13x:0123456789 14x:579 15x:012356789 17x:01678 18x:0123456789 然后修改下正则表达式,可以随便输个字符串验证下:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584013db9939?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”534″ data-height=”29″ />
  • 3.最后就是剩下部分的8个数字了,很简单:[0-9]{8} 加上:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584018e1df00?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”588″ data-height=”26″ />
^(0|86|17951)?(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])[0-9]{8}$
复制代码

例子2:验证身份证

流程分析:

身份证号码分为一代和二代,一代由15位号码组成,而二代则是由18个号码组成: 十五位:xxxxxx    yy mm dd   pp s 十八位:xxxxxx yyyy mm dd ppp s

为了方便了解,把这两种情况分开,先是十八位的:

  • 1.前6位地址编码(省市县),第一位从1开始,其他五位0-9
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58401791c7be?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”306″ data-height=”23″ />
  • 2.第7到10(接着的两位或者四位有):,范围是1800到2099:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584016d503e5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”300″ data-height=”23″ />
  • 3.第11到12,1-9月需要补0,10,11,12
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840146cfba4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”344″ data-height=”24″ />
  • 4.第13到14,首位可能是012,第二位为0-9,还要补上10,20,30,31
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58401af0fef3?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”513″ data-height=”25″ />
  • 5.第15到17顺序码,这里就是三个数字,对同年、同月、同日出生的人 编定的顺序号,奇数分给男的,偶数分给女的:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403a7b7578?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”550″ data-height=”27″ />
  • 6.第18位校验码,0到9或者x和X
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403b7fb295?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”673″ data-height=”22″ />

能推算出18的,那么推算出15的也不难了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403db90de6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”525″ data-height=”19″ />

最后用|组合下:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403dc1e7e6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”645″ data-height=”37″ />
^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{3}[0-9Xx]|[1-9]\d{5}\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{2}[0-9Xx]$
复制代码

另外,这里的正则匹配出的身份证不一定是合法的,判断身份是否 合法还需要通过程序进行校验,校验最后的校验码是否正确

扩展阅读:身份证的最后一位是怎么算出来的? 更多可见:第二代身份证号码编排规则

首先有个加权因子的表:(没弄懂怎么算出来的..) [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]

然后位和值想乘,结果相加,最后除11求余,比如我随便网上找的 一串身份证:411381199312150167,我们来验证下最后的7是对的吗?

sum = 47 + 19 + 110 + 35 +88 + 1 4 … + 6 * 2 = 282 sum % 11 = 7,所以这个是一个合法的身份证号。


例子3:验证ip是否正确

流程分析

ip由4段组成,xxx.xxx.xxx.xxx,访问从0到255,因为要考虑上中间的. 所以我们把第一段和后面三段分开,然后分析下ip的结构,可能是这几种情况: 一位数[1-9] 两位数[1-9][0-9] 三位数(100-199):1[0-9][0-9] 三位数(200-249):2[0-4][0-9] 三位数(250-255): 25[0-5] 理清了第一段的正则怎么写就一清二楚了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584040c72108?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”474″ data-height=”25″ />

然后后面三段,需要在前面加上一个一个**.**,然后这玩意是元字符, 需要加上一个反斜杠\,让他失去作用,后面三段的正则就是:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840455566b2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”445″ data-height=”19″ />

把两段拼接下即可得出完整的验证ip的正则表达式了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58405fc830ff?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”520″ data-height=”37″ />
^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$
复制代码

例子4:匹配各种乱七八糟的

  • 匹配中文[\u4e00-\u9fa5]
  • 匹配双字节字符[^\x00-\xff]
  • 匹配数字并输出示例
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584060a28da0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”428″ data-height=”48″ />

    输出结果

    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584060c2964e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”46″ data-height=”19″ />
  • 匹配开头结尾示例
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584065d7cfb2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”477″ data-height=”96″ />

    输出结果

    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58406ebc611c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”94″ data-height=”67″ />

4.正则实战

实战:抓一波城市编码列表

本来想着就抓抓中国气象局的天气就好了,然后呢,比如深圳天气的网页是: www.weather.com.cn/weather1dn/… 然后这个101280601是城市编码,然后网上搜了下城市编码列表,发现要么 很多是错的,要么就缺失很多,或者链接失效,想想自己想办法写一个采集 的,先搞一份城市编码的列表,不过我去哪里找数据来源呢?中国气象局 肯定是会有的,只是应该不会直接全部暴露出来,想想能不能通过一些间接 操作来实现。对着中国气象局的网站瞎点,结果不负有心人,我在这里: www.weather.com.cn/forecast/ 发现了这个:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584082bc4aca?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”251″ data-height=”166″ />

点进去后:www.weather.com.cn/textFC/hb.s… 然后,我觉得这可能是入手点:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840824188ba?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”1025″ data-height=”369″ />

F12打开开发者工具,不出所料:

这里有个超链接,难不成是北京所有的地区的列表,点击下进去看看: www.weather.com.cn/textFC/beij…

卧槽,果然是北京所有的地区,然后每个地区的名字貌似都有一个超链接, F12看下指向哪里?

到这里就豁(huo)然开朗了,我们来捋一捋实现的流程:

  • 1.先拿到第一层的城市列表链接用列表存起来
  • 2.接着遍历列表去访问不同的城市列表链接,截取不同城市的城市名,城市编码存起来

流程看上去很简单,接着来实操一波。

先是拿城市列表url

这个很容易拿,就直接贴代码了:

拿到需要的城市列表url:

接着随便点开一个,比如beijing.shtml,页面结构是这样的: 想要的内容是这里的超链接:

F12看下页面结构,层次有点多,不过没关系,这样更能够锻炼我们

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840b9222896?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”431″ data-height=”515″ />

入手点一般都是离我们想要数据最近地方下手,我看上了:conMidtab3 全局搜了一下,也就八个:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840bdedec87?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”516″ data-height=”49″ />

第一个直接就可以排除了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840bc5a0412?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”159″ data-height=”202″ />

接着其余的七个,然后发现都他么是一样的…,那就直接抓到第一个吧:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840d2e37a95?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”552″ data-height=”24″ />

输出下:

是我们想要的内容,接着里面的tr是我们需要内容,找一波:

输出下:

继续细扒,我们要的只是a这个东西:

输出下:

重复出现了一堆详情,很明显是我们不想要的,我们可以在循环的时候 执行一波判断,重复的不加入到列表中:

然后我们想拿到城市编码和城市名称这两个东西:

城市的话还好,直接调用tag对象的string直接就能拿到, 而城市编码的话,按照以前的套路,我们需要先[‘href’]拿到 再做字符串裁剪,挺繁琐的,既然本节学习了正则,为何不用 正则来一步到位,不难写出这样的正则:

匹配拿到**group(1)**就是我们要的城市编码:

输出内容:

卧槽,就是我们想要的结果,美滋滋,接着把之前拿到所有 的城市列表都跑一波,存字典里返回,最后赛到一个大字典 里,然后写入到文件中,完成。


========= BUG的分割线 =========

最后把数据打印出来发现只有428条数据,后面才发现conMidtab3那里处理有些 问题,漏掉了一些,限于篇幅,就不重新解释了,直接贴上修正完后的代码把…

import urllib.request
from urllib import error
from bs4 import BeautifulSoup
import os.path
import re
import operator

# 通过中国气象局抓取到所有的城市编码

# 中国气象网基地址
weather_base_url = "http://www.weather.com.cn"
# 华北天气预报url
weather_hb_url = "http://www.weather.com.cn/textFC/hb.shtml#"


# 获得城市列表链接
def get_city_list_url():
    city_list_url = []
    weather_hb_resp = urllib.request.urlopen(weather_hb_url)
    weather_hb_html = weather_hb_resp.read().decode('utf-8')
    weather_hb_soup = BeautifulSoup(weather_hb_html, 'html.parser')
    weather_box = weather_hb_soup.find(attrs={'class': 'lqcontentBoxheader'})
    weather_a_list = weather_box.findAll('a')
    for i in weather_a_list:
        city_list_url.append(weather_base_url + i['href'])
    return city_list_url


# 根据传入的城市列表url获取对应城市编码
def get_city_code(city_list_url):
    city_code_dict = {}  # 创建一个空字典
    city_pattern = re.compile(r'^<a.*?weather/(.*?).s.*</a>$')  # 获取城市编码的正则

    weather_hb_resp = urllib.request.urlopen(city_list_url)
    weather_hb_html = weather_hb_resp.read().decode('utf-8')
    weather_hb_soup = BeautifulSoup(weather_hb_html, 'html.parser')
    # 需要过滤一波无效的
    div_conMidtab = weather_hb_soup.find_all(attrs={'class': 'conMidtab', 'style': ''})

    for mid in div_conMidtab:
        tab3 = mid.find_all(attrs={'class': 'conMidtab3'})
        for tab in tab3:
            trs = tab.findAll('tr')
            for tr in trs:
                a_list = tr.findAll('a')
                for a in a_list:
                    if a.get_text() != "详情":
                        # 正则拿到城市编码
                        city_code = city_pattern.match(str(a)).group(1)
                        city_name = a.string
                        city_code_dict[city_code] = city_name
        return city_code_dict


# 写入文件中
def write_to_file(city_code_list):
    try:
        with open('city_code.txt', "w+") as f:
            for city in city_code_list:
                f.write(city[0] + ":" + city[1] + "\n")
    except OSError as reason:
        print(str(reason))
    else:
        print("文件写入完毕!")


if __name__ == '__main__':
    city_result = {}  # 创建一个空字典,用来存所有的字典
    city_list = get_city_list_url()

    # get_city_code("http://www.weather.com.cn/textFC/guangdong.shtml")

    for i in city_list:
        print("开始查询:" + i)
        city_result.update(get_city_code(i))

    # 根据编码从升序排列一波
    sort_list = sorted(city_result.items(), key=operator.itemgetter(0))

    # 保存到文件中
    write_to_file(sort_list)

复制代码

运行结果

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584121f34f1c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”405″ data-height=”116″ />

5.小结和几个API

本节对Python中了正则表达式进行了一波学习,练手,发现和Java里的正则 多了一些规则,正则在字符串匹配的时候是挺爽的,但是正则并不是全能 的,比如闰年二月份有多少天的那个问题,还需要程序另外去做判断! 正则还需要多练手啊,限于篇幅,就没有另外去抓各种天气信息了, 而且不是刚需,顺道提供两个免费可用三个和能拿到天气数据的API吧:

还有个中国气象局提供的根据经纬度获取天气的: e.weather.com.cn/d/town/inde…

人生苦短,我用Python,爬虫真好玩!期待下节爬虫框架scrapy学习~


来啊,Py交易啊

想加群一起学习Py的可以加下,智障机器人小Pig,验证信息里包含: PythonpythonpyPy加群交易屁眼 中的一个关键词即可通过;

验证通过后回复 加群 即可获得加群链接(不要把机器人玩坏了!!!)~~~ 欢迎各种像我一样的Py初学者,Py大神加入,一起愉快地交流学♂习,van♂转py。

作者:coder-pig
链接:https://juejin.im/post/5a576d976fb9a01c9b65dee6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。