2023年4月

系列文章目录和关于我

零丶背景

最近在新公司第一次上手写代码,写了一个不是很难的业务逻辑代码,但是在我写单元测试的时候,发现自己对单元测试的理解的就是一坨,整个过程写得慢,还写得臭。造成这种局面我认为是因为:

  • 对Mockito api是不是很熟悉
  • 没有自己单元测试方法论,不知道怎样写好单元测试。

now,我将从这两个部分来学习一下单元测试,如何写,如何写好单元测试?

一丶为什么需要单元测试

在上一份工作,我基本上不咋写单元测试,觉得很麻烦,不如直接postman,swagger开冲,这种显然不容易覆盖到所有的case。

单元测试的好处:

  • 增强信心

    单元测试覆盖率越高,我们越对自己的代码有信心。

  • 揭示意图

    写单元测试的时候,我们是明确自己的代码到底是出于什么目的写的

  • 安全重构

    不只是重构,哪怕后续在原有功能上进行添加,通过执行之前存在单元测试有助于我们验证,我们没有影响到原有功能。

  • 快速反馈

    写单元测试的过程,我们其实有可能发现自己代码存在的缺陷,通过单元测试直白的报错,我们可以很快得到反馈,这个反馈速度是测试滴滴你所不具备的。

  • 定位缺陷

    单元测试并不能帮我们找出所有存在的bug(测试同事:没事,我会出手),但是我们发现bug后,可以将输入放在单元测试中进行回放,直到可以重现并定位到问题,然后使用这种情况的case来补充单元测试用例。

二丶引入依赖&这些依赖的作用

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.13.2</version>
   <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <version>5.3.1</version>
   <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline -->
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-inline</artifactId>
   <version>3.7.7</version>
   <scope>test</scope>
</dependency>
  • junit

    提供了许多方便使用的注解,标注在方法上

    image-20230422155916305

  • Mockito

    Mockito 是一种 Java Mock 框架,主要就是用来做 Mock 测试的,可以模拟出一个对象、模拟方法的返回值、模拟抛出异常,模拟静态方法等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。

    Mock 测试:比如我们的Service依赖其他的服务提供的接口方法,使用mock可以模拟出这个接口的表现(正常返回,抛出异常等到)从而让单元测试不那么依赖外部的服务。

  • powermock

    可以看作是mock增强版本,提供模拟私有方法等功能,我们这里没有进行引入。

三丶Mockito 常用功能

0.从一个例子开始

image-20230422165535432

image-20230422165608337

如上图,我们的MyService依赖于OtherClient,这个OtherClient可能由于网络原因会出现错误,或者其他情况抛出异常,我们的MyService需要进行处理。

1.@InjectMocks & @Mock &MockitoAnnotations.openMocks

image-20230422165957197

  • @InjectMocks:标记应进行注射的字段,类似于spring的依赖注入,但是这里会使用Mock产生的对象
  • @Mock :将字段标记为模拟字段,我们可以使用Mockito提供的方法来
    打桩
  • MockitoAnnotations.openMocks:开启Mockito注解的功能

2.打桩

打桩可以理解为 mock 对象规定它的行为,使其按照我们的要求来执行具体的操作。

2.1 指定入参让mock对象返回指定对象——thenReturn

//让client在query入参为1的时候,返回100为key,aaa为value的单键值对的map
Mockito.when(client.query(1)).thenReturn(new HashMap<>(Collections.singletonMap(100, "aaaa")));
Map<Integer, String> res = client.query(1);
Assert.assertEquals(1, res.size());
Assert.assertEquals(res.get(100), "aaaa");

2.2 指定入参让mock对象抛出异常——thenThrow

Mockito.when(client.query(2)).thenThrow(new RuntimeException("222"));
Assert.assertThrows("222", RuntimeException.class, () -> client.query(2));

2.3 指定任何参数都执行指定操作——Mockito.anyInt()

Mockito.when(client.query(Mockito.anyInt())).thenReturn(new HashMap<>());
Assert.assertEquals(0, client.query(-1).size());

2.4 参数匹配器——ArgumentMatcher

有时候,我们希望入参入参符合要的时候,mock对象进行什么操作。

如下,我们要求mock对象在输入参数是
1, 2, 3
的时候返回空map

HashSet<Integer> integers = new HashSet<>(Arrays.asList(1, 2, 3));
Mockito.when(client.query(Mockito.argThat(new ArgumentMatcher<Integer>() {
    @Override
    public boolean matches(Integer argument) {
        return integers.contains(argument);
    }
}))).thenReturn(Collections.emptyMap());
Assert.assertEquals(0,client.query(2).size());

2.5 控制mock对象返回结果——thenAnswer

有时候我们希望mock对象可以根据输出的不同返回不同的结果,符合我们要求的结果。

如下,我们使用thenAnswer根据入参返回不同的结果。

Mockito.when(client.query(Mockito.anyInt())).thenAnswer(new Answer<Object>() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        Integer argument = invocation.getArgument(0);
        String str = argument%2==0?"偶数":"奇数";
        return new HashMap<Integer,String>(Collections.singletonMap(argument,str));
    }
});
Assert.assertEquals("偶数", client.query(2).get(2));

2.6 让mock对象调用真实方法——thenCallRealMethod

上面都是说mock对象如何去控制输出,thenCallRealMethod可以让mock对象执行真实的逻辑。

Mockito.when(client.query(-1)).thenCallRealMethod();

2.7 验证——verify

verify可以让我们验证当前mock对象,比如下面验证client至少执行了四次query

//验证 client.query最起码调用了4次
Mockito.verify(client,Mockito.atLeast(4)).query(Mockito.anyInt());

2.8 mock静态方法——mockStatic

有时候静态方法也需要进行mock控制,可以使用

image-20230422176666662018

四丶一个有依赖的单元测试

0.还是这个例子

image-20230422165535432

image-20230422165608337

如上图,我们的MyService依赖于OtherClient,这个OtherClient可能由于网络原因会出现错误,或者其他情况抛出异常,我们的MyService需要进行处理。

1.确认需要mock什么

上面这个例子中,OtherClient是外部提供给我们的接口,它存在一定的机率失败,在单元测试的过程我们需要mock它的行为,而不是真的去调用外部接口。

2.定义对象,前置准备

image-20230422182821922

这里我们得明确 MyService是我们需要测试的,那就别mock它,OtherClient是外部依赖,需要进行mock控制其行为。

3.1mock方法->调用方法->验证方法

3.1 模拟OtherClient抛出异常

image-20230422183255421

3.2 模拟OtherClient返回空Map

image-20230422183622488

3.3模拟OtherClient返回非空Map

image-20230422184443824

深度学习--全连接层、高阶应用、GPU加速

  1. MSE均方差

  2. Cross Entropy Loss:交叉熵损失

Entropy 熵:

1948年,香农将统计物理中熵的概念,引申到信道通信的过程中,从而开创了信息论这门学科,把信息中排除了冗余后的平均信息量称为“信息熵”。香农定义的“熵”又被称为香农熵或信息熵,即

img

其中
img
标记概率空间中所有可能的样本,
img
表示该样本的出现几率,
img
是和单位选取相关的任意常数。

针对此问题,熵越大,不确定程度就越大,对于其中信息量的讨论
参考知乎

​ 在信息学里信息量大代表着数据离散范围小,不确定性小。香农作为一个信息学家,他关心的是信息的正确传递,所以信息熵代表着信息传递的不确定性的大小。所以在信息学上,使用香农公式算出来的这个值,在信息学上叫做信息熵值,在熵权法中叫做冗余度值或者叫偏离度值,它的本来含义是指一个确定无疑的信息源发送出来的信息,受到干扰以后,衡量偏离了原始精确信息的程度。离散度越大,计算得这个值越小,则收到的信息越不可靠,得到的信息越小。这个值越大,则收到的信息越可靠,得到的信息越多。

​ 在统计学里,就完全不是这样。统计学家不认为存在仅有一个的确定无疑的原始信息。而是认为收到的统计数字都是确信无疑的,只是由于发送主体可能是很多主体,或者是同一主体不同时间,不同地点,或者是统计渠道不同等等原因,得到了一组具有离散性的数值。在这种情况下,离散性越大,熵值越小,代表着信息量越大,所以权重越大。

a=torch.full([4],1/4)
#tensor([0.2500, 0.2500, 0.2500, 0.2500])

#计算交叉熵
-(a*torch.log2(a)).sum()
#tensor(2.)

​ 交叉熵在神经网络中作为损失函数,p表示真实标记的分布,q则为训练后的模型的预测标记分布,交叉熵损失函数可以衡量p与q的相似性。交叉熵作为损失函数还有一个好处是使用sigmoid函数在梯度下降时能避免均方误差损失函数学习速率降低的问题,因为学习速率可以被输出的误差所控制。

交叉熵计算:H(p,q)=
img

MNIST再实现

import  torch
import  torch.nn as nn
import  torch.nn.functional as F
import  torch.optim as optim
from    torchvision import datasets, transforms


batch_size=200
learning_rate=0.01
epochs=10

#加载数据集DataLoader(数据位置,batch_size,shuffle是否打乱,num_workers=4:4线程处理)
    #torchvision.datasets.MNIST(root,train,transform,download)   root指下载到的位置,train指是否下载训练集,transform指对图片进行转换后返回,download指是否下载
        #torchvision.transforms([transforms.ToTensor(),transforms.Normalize((mean),(std))])
            #transforms.ToTensor()做了三件事:1.归一化/255 2.数据类型转为torch.FloatTensor  3.shape(H,W,C)->(C,H,W)
            #transforms.Normalize((mean),(std)) :用均值和标准差对张量图像进行归一化

train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])),
    batch_size=batch_size, shuffle=True)



w1, b1 = torch.randn(200, 784, requires_grad=True),\
         torch.zeros(200, requires_grad=True)
w2, b2 = torch.randn(200, 200, requires_grad=True),\
         torch.zeros(200, requires_grad=True)
w3, b3 = torch.randn(10, 200, requires_grad=True),\
         torch.zeros(10, requires_grad=True)

torch.nn.init.kaiming_normal_(w1)
torch.nn.init.kaiming_normal_(w2)
torch.nn.init.kaiming_normal_(w3)


def forward(x):
    x = x@w1.t() + b1
    x = F.relu(x)
    x = x@w2.t() + b2
    x = F.relu(x)
    x = x@w3.t() + b3
    x = F.relu(x)
    return x



optimizer = optim.SGD([w1, b1, w2, b2, w3, b3], lr=learning_rate)
criteon = nn.CrossEntropyLoss()

for epoch in range(epochs):

    for batch_idx, (data, target) in enumerate(train_loader):
        data = data.view(-1, 28*28)

        logits = forward(data)
#        print(data.shape, target.shape,logits.shape)

        loss = criteon(logits, target)

        optimizer.zero_grad()
        loss.backward()
        # print(w1.grad.norm(), w2.grad.norm())
        optimizer.step()

        if batch_idx % 100 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.item()))


    test_loss = 0
    correct = 0
    for data, target in test_loader:
        data = data.view(-1, 28 * 28)
        logits = forward(data)
        test_loss += criteon(logits, target).item()

        pred = logits.data.max(1)[1]
        #print(pred)
        correct += pred.eq(target.data).sum()

    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

全连接层

import torch
import torch.nn as nn
import torch.nn.functional as F

x=torch.randn(1,784)
x.shape
#torch.Size([1, 784])

# nn.Linear(输入、输出)
layer1 = nn.Linear(784,200)
layer2 = nn.Linear(200,200)
layer3 = nn.Linear(200,10)

x=layer1(x)
x=F.relu(x,inplace=True)
x.shape
#torch.Size([1, 200])

x=layer2(x)
x=F.relu(x,inplace=True)
x.shape
#torch.Size([1, 200])

x=layer3(x)
x=F.relu(x,inplace=True)
x.shape
#torch.Size([1, 10])

网络定义的高阶用法

import torch
import torch.nn as nn
import torch.nn.functional as F
import  torch.optim as optim

class MLP(nn.Module):
    
    def __init__(self):
        super(MLP,self).__init__()
        
        self.model = nn.Sequential(
            nn.Linear(784,200),
            nn.ReLU(inplace=True),
            nn.Linear(200,200),
            nn.ReLU(inplace=True),
            nn.Linear(200,10),
            nn.ReLU(inplace=True),
        )

    def forward(self,x):
        x=self.model(x)
        return x

net= MLP()
optimizer = optim.SGD(net.parameters(),lr=learning_rate)
criteon = nn.CrossEntropyLoss()

其他的激活函数 SELU、softplus、

GPU加速

import  torch
import  torch.nn as nn
import  torch.nn.functional as F
import  torch.optim as optim
from    torchvision import datasets, transforms


batch_size=200
learning_rate=0.01
epochs=10

#加载数据集DataLoader(数据位置,batch_size,shuffle是否打乱,num_workers=4:4线程处理)
    #torchvision.datasets.MNIST(root,train,transform,download)   root指下载到的位置,train指是否下载训练集,transform指对图片进行转换后返回,download指是否下载
        #torchvision.transforms([transforms.ToTensor(),transforms.Normalize((mean),(std))])
            #transforms.ToTensor()做了三件事:1.归一化/255 2.数据类型转为torch.FloatTensor  3.shape(H,W,C)->(C,H,W)
            #transforms.Normalize((mean),(std)) :用均值和标准差对张量图像进行归一化

train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])),
    batch_size=batch_size, shuffle=True)


class MLP(nn.Module):

    def __init__(self):
        super(MLP, self).__init__()

        self.model = nn.Sequential(
            nn.Linear(784, 200),
            nn.LeakyReLU(inplace=True),
            nn.Linear(200, 200),
            nn.LeakyReLU(inplace=True),
            nn.Linear(200, 10),
            nn.LeakyReLU(inplace=True),
        )

    def forward(self,x):
        x=self.model(x)
        return x

    
##重点重点!!! 

device=torch.device('cuda:0')
net = MLP().to(device)
optimizer = optim.SGD(net.parameters(),lr=learning_rate)
criteon = nn.CrossEntropyLoss().to(device)


for epoch in range(epochs):

    for batch_idx, (data, target) in enumerate(train_loader):
        data = data.view(-1, 28*28)
        data,target = data.to(device),target.to(device)


        logits = net(data)
#        print(data.shape, target.shape,logits.shape)

        loss = criteon(logits, target)

        optimizer.zero_grad()
        loss.backward()
        # print(w1.grad.norm(), w2.grad.norm())
        optimizer.step()

        if batch_idx % 100 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.item()))


    test_loss = 0
    correct = 0
    for data, target in test_loader:
        data = data.view(-1, 28 * 28)
        data, target = data.to(device), target.to(device)
        logits = net(data)
        test_loss += criteon(logits, target).item()

        pred = logits.data.max(1)[1]
        #print(pred)
        correct += pred.eq(target.data).sum()

    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

前言

简单整理一下paralel,以上是并行的意思。

正文

我们在工作中常常使用task await 和 async,也就是将线程池进行了封装,那么还有一些更高级的应用。

是对task的封装,那么来看下paralel。

static void Main(string[] args)
{
	var ints= Enumerable.Range(1, 100);
	var result = Parallel.ForEach(ints, arg =>
	{
		Console.WriteLine(arg);
	});
	
	Console.Read();
}

可以看到结果是并行的。

那么来看下实现机制。

public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource> body)
{
	if (source == null)
	{
		throw new ArgumentNullException(nameof(source));
	}
	if (body == null)
	{
		throw new ArgumentNullException(nameof(body));
	}

	return ForEachWorker<TSource, object>(
		source, s_defaultParallelOptions, body, null, null, null, null, null, null);
}

进行参数检验,然后交给了ForEachWorker。

这是一个基本的代码思路,就是复杂的方法中可以先校验参数,然后具体实现交给另外一个方法。

然后通过不同的类型,进行分类:

然后看下具体实现是什么?

进去看就是一个taskreplicator:

看下run在做什么。

public static void Run<TState>(ReplicatableUserAction<TState> action, ParallelOptions options, bool stopOnFirstFailure)
{
	int maxConcurrencyLevel = (options.EffectiveMaxConcurrencyLevel > 0) ? options.EffectiveMaxConcurrencyLevel : int.MaxValue;

	TaskReplicator replicator = new TaskReplicator(options, stopOnFirstFailure);
	new Replica<TState>(replicator, maxConcurrencyLevel, CooperativeMultitaskingTaskTimeout_RootTask, action).Start();

	Replica nextReplica;
	while (replicator._pendingReplicas.TryDequeue(out nextReplica))
		nextReplica.Wait();

	if (replicator._exceptions != null)
		throw new AggregateException(replicator._exceptions);
}
  1. 创建了一个taskreplictor,起到管理作用

  2. 然后创建了一个Replica,然后这个start 是关键

  3. 然后通过while,让每一个Replica 都运行完毕才推出,达到同步的效果

if (replicator._exceptions != null)
	throw new AggregateException(replicator._exceptions);

可以看一下这个,这个是一个比较好的技巧。如果一个运行管理,不用抛出异常,之间在管理中进行运行处理总结。

比如结果,异常等。

那么就看下这个start。

protected Replica(TaskReplicator replicator, int maxConcurrency, int timeout)
{
	_replicator = replicator;
	_timeout = timeout;
	_remainingConcurrency = maxConcurrency - 1;
	_pendingTask = new Task(s => ((Replica)s).Execute(), this);
	_replicator._pendingReplicas.Enqueue(this);
}

public void Start()
{
	_pendingTask.RunSynchronously(_replicator._scheduler);
}

将会运行Execute,是同步的,而不是异步的,也就是说第一个task将会运行在当前线程。

那么看Execute在做什么?

public void Execute()
{
	try
	{
		if (!_replicator._stopReplicating && _remainingConcurrency > 0)
		{
			CreateNewReplica();
			_remainingConcurrency = 0; // new replica is responsible for adding concurrency from now on.
		}

		bool userActionYieldedBeforeCompletion;

		ExecuteAction(out userActionYieldedBeforeCompletion);

		if (userActionYieldedBeforeCompletion)
		{
			_pendingTask = new Task(s => ((Replica)s).Execute(), this, CancellationToken.None, TaskCreationOptions.None);
			_pendingTask.Start(_replicator._scheduler);
		}
		else
		{
			_replicator._stopReplicating = true;
			_pendingTask = null;
		}
	}
	catch (Exception ex)
	{
		LazyInitializer.EnsureInitialized(ref _replicator._exceptions).Enqueue(ex);
		if (_replicator._stopOnFirstFailure)
			_replicator._stopReplicating = true;
		_pendingTask = null;
	}
}

一段一段分析:

if (!_replicator._stopReplicating && _remainingConcurrency > 0)
{
	CreateNewReplica();
	_remainingConcurrency = 0; // new replica is responsible for adding concurrency from now on.
}

这里当_replicator 也就是任务复制器没有停止的时候。这里有两种情况会停止,一种是任务完成,一种是任务异常且设置参数异常时候停止。

_remainingConcurrency 指的是副本数,默认是int.max。

那么就复制一个副本。

protected override void CreateNewReplica()
{
	Replica<TState> newReplica = new Replica<TState>(_replicator, _remainingConcurrency, GenerateCooperativeMultitaskingTaskTimeout(), _action);
	newReplica._pendingTask.Start(_replicator._scheduler);
}

复制完副本后,那么就开始运行我们的action了。

protected override void ExecuteAction(out bool yieldedBeforeCompletion)
{
	_action(ref _state, _timeout, out yieldedBeforeCompletion);
}

这里传入了timeout,这个timeout并不是我们限制我们单个task的运行时间,而是当运行到一定时候后,这个task就停止运行,然后另外启动一个副本。

if (CheckTimeoutReached(loopTimeout))
{
	replicationDelegateYieldedBeforeCompletion = true;
	break;
}
if (userActionYieldedBeforeCompletion)
{
	_pendingTask = new Task(s => ((Replica)s).Execute(), this, CancellationToken.None, TaskCreationOptions.None);
	_pendingTask.Start(_replicator._scheduler);
}
else
{
	_replicator._stopReplicating = true;
	_pendingTask = null;
}

这个是为了符合操作系统的调度思想,跑的越久的,基本上优先级会低些。

那么看下这个_action主要在做什么吧。

while (myPartition.MoveNext())
{
	KeyValuePair<long, TSource> kvp = myPartition.Current;
	long index = kvp.Key;
	TSource value = kvp.Value;

	// Update our iteration index
	if (state != null) state.CurrentIteration = index;

	if (simpleBody != null)
		simpleBody(value);
	else if (bodyWithState != null)
		bodyWithState(value, state);
	else if (bodyWithStateAndIndex != null)
		bodyWithStateAndIndex(value, state, index);
	else if (bodyWithStateAndLocal != null)
		localValue = bodyWithStateAndLocal(value, state, localValue);
	else
		localValue = bodyWithEverything(value, state, index, localValue);

	if (sharedPStateFlags.ShouldExitLoop(index)) break;

	// Cooperative multitasking:
	// Check if allowed loop time is exceeded, if so save current state and return.
	// The task replicator will queue up a replacement task. Note that we don't do this on the root task.
	if (CheckTimeoutReached(loopTimeout))
	{
		replicationDelegateYieldedBeforeCompletion = true;
		break;
	}
}

就是拉取我们的enumerator的数据,然后simpleBody(value),进行运行我们写的action。

总结一下,其实Parallel 核心就是一个任务复制器,然后创建多个副本,拉取我们的数据,进行执行我们设置的action。

里面的主要功能,Parallel做到了限制副本数,因为我们知道task并不是越多越好。

第二个,如果长时间运行,那么Parallel是做了优化的,当达到timeout的时候,那么会重新启动一个副本(可以理解为一个线程)

第三点,Parallel 有一个foreach 进行迭代器的处理,这里不仅仅是让任务可以并行。

而且具备c# foreach的基本功能。

static void Main(string[] args)
{
	var ints= Enumerable.Range(1, 100);
	var result = Parallel.ForEach(ints,    (arg, state)
		=>
	{
		if (state.IsStopped)
		{
			return;   
		}
		
		if (arg > 18)
		{
			state.Break();
		}
	});
	if (result.IsCompleted)
	{
		Console.WriteLine("完成");
	}
	Console.Read();
}

可以进行中断。

还有一个函数,那就是stop,这个stop 比break 停止的快,break 要记录出,最小中断位置。

而stop 就是立马停止下来。

在上述中,我们知道可以传递一个taskschedule进行,那么这个taskschedule 是干什么的,对我们的任务调度有什么影响呢? 下一节,自我实现taskschedule。

本文首发于公众号:Hunter后端
原文链接:
Django笔记二十八之数据库查询优化汇总

这一篇笔记将从以下几个方面来介绍 Django 在查询过程中的一些优化操作,有一些是介绍如何获取 Django 查询转化的 sql 语句,有一些是理解 QuerySet 是如何获取数据的。

以下是本篇笔记目录:

  1. 性能方面
  2. 使用标准的数据库优化技术
  3. 理解 QuerySet
  4. 操作尽量在数据库中完成而不是在内存中
  5. 使用唯一索引来查询单个对象
  6. 如果知道需要什么数据,那么就立刻查出来
  7. 不要查询你不需要的数据
  8. 使用批量的方法

1、性能方面

1. connection.queries

前面我们介绍过 connection.queries 的用法,比如我们执行了一条查询之后,可以通过下面的方式查到我们刚刚的语句和耗时

>>> from django.db import connection
>>> connection.queries
[{'sql': 'SELECT polls_polls.id, polls_polls.question, polls_polls.pub_date FROM polls_polls',
'time': '0.002'}]

仅仅当系统的 DEBUG 参数设为 True,上述命令才可生效,而且是按照查询的顺序排列的一个数组

数组的每一个元素都是一个字典,包含两个 Key:sql 和 time

sql 为查询转化的查询语句
time 为查询过程中的耗时

因为这个记录是按照时间顺序排列的,所以 connection.queries[-1] 总能查询到最新的一条记录。

多数据库操作

如果系统用的是多个数据库,那么可以通过 connections['db_alias'].queries 来操作,比如我们使用的数据库的 alias 为 user:

>>> from django.db import connections
>>> connections['user'].queries

如果想清空之前的记录,可以调用 reset_queries() 函数:

from django.db import reset_queries
reset_queries()

2. explain

我们也可以使用 explain() 函数来查看一条 QuerySet 的执行计划,包括索引以及联表查询的的一些信息

这个操作就和 MySQL 的 explain 是一样的。

>>> print(Blog.objects.filter(title='My Blog').explain())
Seq Scan on blog  (cost=0.00..35.50 rows=10 width=12)
  Filter: (title = 'My Blog'::bpchar)

也可以加一些参数来查看更详细的信息:

>>> print(Blog.objects.filter(title='My Blog').explain(verbose=True, analyze=True))
Seq Scan on public.blog  (cost=0.00..35.50 rows=10 width=12) (actual time=0.004..0.004 rows=10 loops=1)
  Output: id, title
  Filter: (blog.title = 'My Blog'::bpchar)
Planning time: 0.064 ms
Execution time: 0.058 ms

之前在使用 Django 的过程中还使用到一个叫 silk 的工具,它可以用来分析一个接口各个步骤的耗时,有兴趣的可以了解一下。

2、使用标准的数据库优化技术

数据库优化技术指的是在查询操作中 SQL 底层本身的优化,不涉及 Django 的查询操作

比如使用 索引 index,可以使用 Meta.indexes 或者字段里的 Field.db_index 来添加索引

如果频繁的使用到 filter()、exclude()、order_by() 等操作,建议为其中查询的字段添加索引,因为索引能帮助加快查询

3、理解 QuerySet

1. 理解 QuerySet 获取数据的过程

1) QuerySet 的懒加载

一个查询的创建并不会访问数据库,直到获取这条查询语句的具体数据的时候,系统才会去访问数据库:

>>> q = Entry.objects.filter(headline__startswith="What")  # 不访问数据库
>>> q = q.filter(pub_date__lte=datetime.date.today())  # 不访问数据库
>>> q = q.exclude(body_text__icontains="food")  # 不访问数据库
>>> print(q)  # 访问数据库

比如上面四条语句,只有最后一步,系统才会去查询数据库。

2) 数据什么时候被加载

迭代、使用步长分片、使用len()函数获取长度以及使用list()将QuerySet 转化成列表的时候数据才会被加载

这几点情况在我们的第九篇笔记中都有详细的描述。

3) 数据是怎么被保存在内存中的

每一个 QuerySet 都会有一个缓存来减少对数据库的访问操作,理解其中的运行原理能帮助我们写出最有效的代码。

当我们创建一个 QuerySet 的之后,并且数据第一次被加载,对数据库的查询操作就发生了。

然后 Django 会保存 QuerySet 查询的结果,并且在之后对这个 QuerySet 的操作中会重复使用,不会再去查询数据库。

当然,如果理解了这个原理之后,用得好就OK,否则会对数据库进行多次查询,造成性能的浪费,比如下面的操作:

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

上面的代码,同样一个查询操作,系统会查询两遍数据库,而且对于数据来说,两次的间隔期之间,Entry 表可能的某些数据库可能会增加或者被删除造成数据的不一致。

为了避免此类问题,我们可以这样复用这个 QuerySet :

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # 查询数据库
>>> print([p.pub_date for p in queryset]) # 从缓存中直接使用,不会再次查询数据库

这样的操作系统就只执行了一遍查询操作。

使用数组的切片或者根据索引(即下标)不会缓存数据

QuerySet 也并不总是缓存所查询的结果,如果只是获取一个 QuerySet 部分数据,会查询有是否这个 QuerySet 的缓存
有的话,则直接从缓存中获取数据,没有的话,后续也不会将这部分数据缓存到系统中。

举个例子,比如下面的操作,在缓存整个 QuerySet 数据前,查询一个 QuerySet 的部分数据时,系统会重复查询数据库:

>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # 查询数据库
>>> print(queryset[5]) # 再次查询数据库

而在下面的操作中,整个 QuerySet 都被提前获取了,那么根据索引的下标获取数据,则能够从缓存中直接获取数据:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # 查询数据库
>>> print(queryset[5]) # 使用缓存
>>> print(queryset[5]) # 使用缓存

如果一个 QuerySet 已经缓存到内存中,那么下面的操作将不会再次查询数据库:

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

2. 理解 QuerySet 的缓存

除了 QuerySet 的缓存,单个 model 的 object 也有缓存的操作。

我们这里简单理解为外键和多对多的关系。

比如下面外键字段的获取,blog 是 Entry 的一个外键字段:

>>> entry = Entry.objects.get(id=1)
>>> entry.blog   # Blog 的实例被查询数据库获得
>>> entry.blog   # 第二次获取,使用缓存信息,不会查询数据库

而多对多关系的获取每次都会被重新去数据库获取数据:

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()   # 查询数据库
>>> entry.authors.all()   # 再次查询数据库

当然,以上的操作,我们都可以通过 select_related() 和 prefetch_related() 的方式来减少数据库的访问,这个的用法在前面的笔记中有介绍。

4、操作尽量在数据库中完成而不是在内存中

举几个例子:

  1. 在大多数查询中,使用 filter() 和 exclude() 在数据库中做过滤,而不是在获取所有数据之后在 Python 里的 for 循环里筛选数据
  2. 在同一个 model 的操作中,如果有涉及到其他字段的操作,可以用到 F 表达式
  3. 使用 annotate 函数在数据库中做聚合(aggregate)的操作

如果某些查询比较复杂,可以使用原生的 SQL 语句,这个操作也在前面有过一篇完整的笔记介绍过

5、使用唯一索引来查询单个对象

在使用 get() 来查询单条数据的时候,有两个理由使用唯一索引(unique)或 普通索引(db_index)

一个是基于数据库索引,查询会更快,

另一个是如果多条数据都满足查询条件,查询会慢得多,而在唯一索引的约束下则保证这种情况不会发生

所以使用下面的 id 进行匹配 会比 headline 字段匹配快得多,因为 id 字段在数据库中有索引且是唯一的:

entry = Entry.objects.get(id=10)

entry = Entry.objects.get(headline="News Item Title")

而下面的操作可能会更慢:

entry = Entry.objects.get(headline__startswith="News")

首先, headline 字段上没有索引,会导致数据库获取速度慢

其次,查询并不能保证只返回一个对象,如果匹配上来多个对象,且从数据库中检索并返回数百数千条记录,后果会很严重,其实就会报错,get() 能接受的返回只能是一个实例数据。

6、如果知道需要什么数据,那么就立刻查出来

能一次性查询所有需要的相关的数据的话,就一次性查询出来,不要在循环中做多次查询,因为那样会多次访问数据库

所以这就需要理解并且用到 select_related() 和 prefetch_related() 函数

7、不要查询你不需要的数据

1. 使用 values() 和 values_list() 函数

如果需求仅仅是需要某几个字段的数据,可以用到的数据结构为 dict 或者 list,可以直接使用这两个函数来获取数据

2. 使用 defer() 和 only()

如果明确知道只需要,或者不需要什么字段数据,可以使用这两个方法,一般常用在 textfield 上,避免加载大数据量的 text 字段

3. 使用 count()

如果想要获取总数,使用 count() 方法,而不是使用 len() 来操作,如果数据有一万条,len() 操作会导致这一万条数据都加载到内存里,然后计数。

4. 使用 exists()

如果仅仅是想查询数据是否至少存在一条可以使用 if QuerySet.exists() 而不是 if queryset 的形式

5. 使用 update() 和 delete()

能够批量更新和删除的操作就使用批量的方法,挨个去加载数据,更新数据,然后保存是不推荐的

6. 直接使用外键的值

如果需要外键的值,直接调用早就在这个 object 中的字段,而不是加载整个关联的 object 然后取其主键id

比如推荐:

entry.blog_id

而不是:

entry.blog.id

7. 如果不需要排序的结果,就不要order_by()

每一个字段的排序都是数据库的操作需要额外消耗性能的,所以如果不需要的话,尽量不要排序

如果在 Meta.ordering 中有一个默认的排序,而你不需要,可以通过 order_by() 不添加任何参数的方法来取消排序

为数据库添加索引,可以帮助提高排序的性能

8、使用批量的方法

1. 批量创建

对于多条 model 数据的创建,尽可能的使用 bulk_create() 方法,这是要优于挨个去 create() 的

2. 批量更新

bulk_update 方法也优于挨个数据在 for 循环中去 save()

3. 批量 insert

对于 ManyToMany 方法,使用 add() 方法的时候添加多个参数一次性操作比多次 add 要好

my_band.members.add(me, my_friend)

要优于:

my_band.members.add(me)
my_band.members.add(my_friend)

4. 批量 remove

当去除 ManyToMany 中的数据的时候,也是能一次性操作就一次性操作:

my_band.members.remove(me, my_friend)

要好于:

my_band.members.remove(me)
my_band.members.remove(my_friend)

如果想获取更多后端相关文章,可扫码关注阅读:
image

本文代码基于Python3.11解释器,除了第一次示例,代码将省略
import re
这个语句

所有示例代码均可以在我的github仓库中的 code.py文件内查看

[我的仓库](
PythonLearinig/正则表达式 at main · saopigqwq233/PythonLearinig (github.com)
)

搞清楚Python正则表达式语法,这一篇就够了

1.Python正则表达式匹配文本模式方法

正则表达式是一种快速从文本中匹配对应模式文本的表达式,在Python中所有的正则表达式函数都在模块
re
中。

其一般使用方法如下:

import re
mo1 = re.compile('Batman') # 先使用re的方法compile,compile的字符串参数便是一个正则表达式
# compile讲返回一个一个Regex对象,mo1就是对应正则表达式模式的对象
name1 = mo1.search('My favorite hero is Batman') # 使用mo1对象中search方法,这个方法的字符串参数就是需要被查找的字符串
# 匹配成功,那么将返回一个Match对象给name1,这个对象中有group()方法,它返回与正则表达式匹配的字符串(有些情况不全是字符串,我会在后面作解释)
# 匹配失败,返回None
print(name1.group())

输出结果如下:

2.直接查找模式

2.1直接查找

上面的代码就是直接查找模式,正则表达式字符串是'
Batman
',则需要在字符串'My favorite hero is Batman',寻找'
Batman
'

2.2管道匹配多种模式

有时,需要匹配的文本有多种可能,需要不同的正则表达式匹配模式,可以用‘
|
’这个符号来表示管道匹配,即匹配多种可能

示例如下:

mo2 = re.compile(r'Batman|Superman')
name2 = mo2.search('My favorite hero is Superman')
print(name2.group())

正则表达式是'
Batman|Superman
',那么在search()的字符串参数中,与之匹配的是'
Superman
',那么返回的Match对象赋给name2,其方法group()返回’Superman‘

输出结果如下:

2.3管道匹配多种分组模式

如果"我喜欢的英雄可能性有点大",正则表达式需要写成'Batman|Superman|Spiderman'吗?

可以,但可以用简洁的形式'
(Bat|Super|Spider)man
'

那么我们先看代码:

mo3 = re.compile('(Bat|Super|Spider)man')
name3 = mo3.search('My favorite hero is Spiderman')
print(name3.group())
print(name3.group(0))  # 0默认是整个匹配的字符串
print(name3.group(1))  # 1是匹配的第一个分组

search()进行匹配时,先匹配第一个分组’Bat‘’Super‘’Spider‘中的一个,再匹配’man‘;也可以认为是匹配’
Batman
‘'
Superman
''
Spiderman
'中的一个。

这是运行情况:

需要指出的是
,在正则表达式中出现分组时,可以在group中传入参数,参数作为索引,比如在上述代码的group(1),此方法将返回第一个分组,同时,无参数或者参数为0则默认返回整个匹配文本

search()只会返回含有第一个出现的匹配文本的对象

先来看看这段代码:

mo4 = re.compile('(Bat|Super)man')
name4 = mo4.search('I love Superman and Batman')
print(name4.group())  # 只输出第一个出现的Superman

运行结果如下:

这段代码中,可以正则表达式可以匹配的文本有’
Superman
‘和'
Batman
'两个,但是name4对象的group方法只返回了第一个出现的'
Superman
'。

后面会有找到所有匹配文本的方法

3.查找固定类型字符模式

3.1字符类型

缩写字符 匹配字符
\d 0~9的数字
\D 除了0~9的其它字符
\w 字母,数字,下划线
\W 除了字符数字下划线
\s 空格制表换行符
\S 除了空格制表换行符

从上面表格可以看出了,大写字母匹配的字符就是小写字母匹配字符的补集

3.2固定类型模式

使用上面的缩写字符,可以匹配指定类型的字符

如代码:

mo5 = re.compile(r'\d\d\d\d\d\d\d\d\d\d\d')
phone_number1 = mo5.search('我的电话号码是15600000000')
print(phone_number1.group())

r前缀用于表示字符串是一个原始字符串,避免转义。

比如,如果无r前缀,那么字符串中的'\n'将被解释为换行符,但是如果加上r前缀,那么会被解释为''和'n'两个字符。

这在正则表达式使很有效,因为正则表达式是按照两个字符''和'd‘来匹配一个数字型的字符,如果不加前缀r,那么我们需要在正则表达式中这样写'\\d','\'代表'',代码演示如下

mo = re.compile('\\d')
num = mo.search('abcd6ef')
print(num.group())

运行结果如下:

但是需要注意的是,以下几个在正则表达式中有特殊含义的字符即使前面有r,仍然要加转义字符''来匹配这些特殊字符

| + | . | $ | * | ^ | ? | { | } | ( | ) | [ | ] | \ | | |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |

比如我们知道'(' ')'可以分组,但是想要在文本中匹配'('')'时,即使加了r前缀,也需要''转义

4.分组模式

有时我们需要给查找到的电话号码分段,比如前面的+86前缀,这时,就可以用到分组模式

代码如下:

mo5 = re.compile(r'(\+86)(\d\d\d)(\d\d\d\d)(\d\d\d\d)')
phone_number1 = mo5.search('我的电话号码是+8618900000000')
print('电话号码'+phone_number1.group())
print('前缀'+phone_number1.group(1))))

在书写正则表达式的时候,给前缀+86分了一组,其后按照我的阅读习惯,344数量格式再分了三组

字符串 组索引
+86 1
189 2
0000 3
0000 4

运行结果是

5.可有可无的分组的模式

不过,我们通常在平时记录电话号码时可能没有+86这个前缀,这时我们书写正则表达式就可以使用后缀’?‘来修饰它前面的分组,表示前面这个分组在匹配文本时可有可无,示例代码如下:

mo6 = re.compile(r'(\+86)?(\d\d\d)(\d\d\d\d)(\d\d\d\d)')
phone_number2 = mo6.search('他输入了+8615600000000到电话框')
phone_number3 = mo6.search('另一个人输入18900000000')
print(phone_number2.group())
print(phone_number3.group())

在这个正则表达式中,我们对(+86)进行
可选匹配
,匹配结果有'+86',返回字符串会带'+86',反之不带

第一个文本匹配返回对象中会包含返回'+8615600000000'的方法

第二个文本匹配返回对象中会包含返回'18900000000'的方法

运行结果:

6.含有特殊字符的模式

如我们在
3.2
中对特殊字符的讨论,我们来分析一下下面的代码:

mo7 = re.compile(r'(\(\+\d\d\))(\d\d\d)(\d\d\d\d)(\d\d\d\d)')
phone_number4 = mo7.search('我的电话号码是(+86)15600000000')
print(phone_number4.group())

r'(\(\+\d\d))(\d\d\d)(\d\d\d\d)(\d\d\d\d)'
中,前缀r表示后面的字符串是原始字符串

总共分了四组,分别是:

模式 组索引
(+\d\d) 1
\d\d\d 2
\d\d\d\d 3
\d\d\d\d 4

组1匹配
小括号括起来的加号和两个数字

组2匹配
三个数字

组3、4匹配
四个数字

运行结果是:

7.接收任意个数的字符的模式

被'*'修饰的字符或分组可以匹配
0到多次
,即在search()的文本中可以不出现也可以出现多次,示例代码如下:

mo8 = re.compile(r'\d*%')
money = mo8.search('本期涨幅有143%')
print(money.group())
money = mo8.search('本期涨幅为?%')
print(money.group())

在第一个正则表达式中,
'\d*%'
可以匹配'143%',其中'1' '4' '3'都与'\d'匹配

在第二个正则表达式中,
'\d*%'
可以匹配'%',因为'%'前没有数字类型可以与'\d'匹配

下面是运行结果:

8.接收一个存在的连续字符模式

'+'修饰的字符或分组可以匹配
1到多次
,即在search()的文本中可以出现一次也可以出现多次,示例代码如下:

mo9 = re.compile(r'\d+')
numbers = mo9.search('第一产业增加值54779亿元')
print(numbers.group())

这个正则表达式中,可以匹配到'54779'这5个数字,其中每个数字型文本都与'\d'匹配

如果没有出现被'+'修饰的字符或者分组,会怎么样呢:

mo9 = re.compile(r'\d+亿元')
numbers = mo9.search('第一产业增加值????亿元')
print(type(numbers))

这里如果我们直接输出numbers.group()返回字符串会报错。马上来解释,现在我们用type()函数测试一下变量numbers的数据类型

运行结果:

可以看到,当没找到时,search()方法返回了'NoneType',无类型,不是一个Match对象,自然是无法通过该变量获得group()方法返回的字符串。

事实上,search()方法在找不到匹配文本时就会返回一个NoneType类型

*和+的区别

修饰字符 匹配文本出现次数
* 0到多次
+ 1到多次

这意味着*修饰的字符或者分组可以
不用出现

+修饰的字符或者分组
必须出现

9.字符匹配固定次数

'{}'修饰的字符或分组可以按次数匹配

9.1固定次数

如果'{}'括号内只有一个整数,如'\d{3}',表示只匹配
3个数字类型字符

代码示例如下:

mo10 = re.compile(r'(\+\d\d)?(\d){11}')
phone_number5 = mo10.search('电话号码是15600000000')
print(phone_number5.group())

这段代码中,正则表达式
r'(\+\d\d)?(\d){11}'
表示'+\d\d'是可选匹配,可有可无,后面将匹配连续的11个数字类型字符,也就是说'15600000000'将匹配'\d'11次,运行结果如下:

当然,如果被查找的文本中是类似于'+15600000000',到底是'+15'匹配'+\d\d',后面无法匹配11个数字字符,search()返回NoneType;还是'15600000000'匹配'\d{11}'呢?

我们试一试:

可以看到,匹配情况是刚刚描述的后者,即忽略'+',匹配后面的'\d{11}'

9.2次数范围

'{}'内可以用逗号把
两个升序整数
分开,比如'\d{11,13}',表示可以匹配11到13个数字字符

示例代码如下:

mo11 = re.compile(r'\+?\d{11,13}')
phone_number6 = mo11.search('电话号码是+8615600000000')
print(phone_number6.group())

正则表达式
r'\+?\d{11,13}'
表示'+'是可选匹配,而'\d{11,13}'将匹配
11到13个数字类型字符
,在被查找文本当中'+'匹配正则表达式的'\+?','8615600000000'匹配'\d{11,13}'

9.3贪心匹配与非贪心匹配

'{}'默认情况下匹配最多的字符,比如'\d{4,6}',被查找文本是'1234567',那么匹配结果是'123456',如果想要正则表达式匹配最少的字符,需要在'{}'后加上'?'修饰。这里?不再表示可选匹配。

也就是说,正则表达式如果是'\d{4,6}?',那么,匹配结果是'1234',返回最少的字符。

示例代码如下:

# 贪心匹配方式
mo12 = re.compile(r'\d{3,5}')
num1 = mo12.search('数字有34567')
print(num1.group())  # 匹配最多的数字

# 非贪心匹配
mo13 = re.compile(r'\d{3,5}?')
num2 = mo13.search('数字有34567')
print(num2.group())  # 匹配最少的数字

运行结果:

9.4注意

1)’{}‘内不可以出现浮点数,否则会报错

2)'{}'允许'{3,3}'这样的写法,和'{3}'同义

3)'{a,b}',整数a必须不大于b

*查找文本所有的匹配项findall()方法

上述用到的search()方法只能查找到第一个出现的匹配文本项,如何找到全部匹配项呢?

使用Match对象的findall()方法,此方法可以返回匹配结果组成的列表,代码示例如下:

mo14 = re.compile(r'\d{11}')
phone_number7 = mo14.findall('电话号码1:15600000000'
                             '电话号码2:19100000000'
                             '电话号码3:18700000000')
print(phone_number7)

我们想要在文本中找到所有和'\d{11}'能匹配的字符,'15600000000'等电话号码都可以和正则表达式匹配,findall()将返回一个包含这些匹配文本的字符串列表

运行结果如下:

与search()不同的是,
findall()直接返回一个列表
而不是Match对象,所以在上面千万别把
print(phone_number7)
写成
print(phone_number7.group())
了。

如果在正则表达式内用了分组,那么会返回元组的列表,元组由分组的字符串组成,
这个返回结果不含有在正则表达式中未分组的部分

代码如下:

mo15 = re.compile(r'电话号码\d:(\d{3})(\d{4})(\d{4})')
phone_number8 = mo15.findall('电话号码1:15600000000'
                             '电话号码2:19100000000'
                             '电话号码3:18700000000')
print(phone_number8)

这里,我们分别对电话号码 前三位,中间四位,最后四位 分组,那么单个匹配文本会被分成三个字符串,组成一个元组,而这些元组组合成一个列表

运行结果如下:

10.自定义匹配字符类型

10.1匹配指定字符

在正则表达式中使用'[]'可以自己定义匹配字符,比如我想找到一个句子里面所有元音开头的字母

代码示例如下:

mo16 = re.compile(r'\b[aeiouAEIOU]\w*')
vowel_word = mo16.findall('I am obviously angry with you')
print(vowel_word)

这里先介绍一下'\b'这个字符,这个字符将匹配单词的分界,也就是说将从一个单词开始匹配。

在这个字符串文本中,单词有'I' 'am' 'obviously' 'angry' 'with' 'you',

使用自定义匹配字符[aeiouAEIOU]匹配元音开头,'\w*'匹配除了空格,制表符,换行符外的字符。

10.2匹配指定的字符无需加\转义

在前面我们知道正则表达式的特殊字符前仍然需要加上''来转义表示原字符

但是,在指定字符匹配当中,无需加''转义,示例代码如下:

mo17 = re.compile(r'[*?+]+')
special_character = mo17.findall('*+?11*?')
print(special_character)

在这个正则表达式中,特殊字符'*' '?' '+'前并未加''转义,将匹配连续的几个指定字符组成的字符串,被匹配文本中'*+?'和'*?'符合

运行结果如下:

10.3匹指定字符外的字符

在指定匹配字符的前面加上^表示不匹配这些字符

代码如下:

mo18 = re.compile(r'\b[^aeiouAEIOU\n\t ]\w*')
non_vowel_word = mo18.findall('I am obviously angry with you')
print(non_vowel_word)

在这个正则表达式中,'^'表示不匹配元音字符和换行符,制表符和' '空格符。也就是说,这个正则表达式匹配非元音字母开头单词

运行结果如下:

11.^和$在正则表达式中的作用

11.1^的作用

在正则表达式前加上'^',将会怎么匹配呢?我们先看一下代码:

mo19 = re.compile(r'^(name):(\d)+')
name_phone1 = mo19.search('name:15600000000这是信息的格式')
print(name_phone1.group())
name_phone1 = mo19.search('信息的格式是name:15600000000')
print(type(name_phone1))

这里出现两个文本,但是只有第一个可以匹配成功,第二个匹配失败返回NoneType类型,这是为什么呢?是因为'^'在正则表达式开头的作用就是让匹配字符必须从被检查字符串开头开始匹配。第一个开头就是可以匹配的'name:156000000000',而第二个虽然也有这样的字符串,但是并非从开头开始匹配,便不会返回Match对象。

11.2$的作用

你知道我要说什么

mo20 = re.compile(r'(name):(\d)+$')
name_phone2 = mo20.search('信息的格式是name:15600000000')
print(name_phone2.group())
name_phone2 = mo20.search('name:15600000000这是信息的格式')
print(type(name_phone2))

与'^'相反,'$'字符将让正则表达式匹配字符串末尾的文本,比如上面的两个字符串,几乎可以直接推断出
可以匹配的是第一个

以下是运行结果:

12.通配字符‘.’,匹配除了换行符的所有字符

正则表达式中,'.'可以匹配一个任意除了换行符的字符。

mo21 = re.compile(r'.at')
words = mo21.findall('The cat in the hat sat on the flat mat')
print(words)

正则表达式将匹配末尾带有'at'的所有字符串,除了'at'位于一行开头这种情况,看看结果吧:

12.1 '.*'匹配所有的字符

通过前面的关于'*'我们知道知道被'*'修饰的正则表达式字符将匹配0到多个。
'*'同样可以修饰'.',来达到匹配除了换行符外所有的字符的效果

而经过'*'修饰的'.'存在贪心匹配和非贪心匹配的情况。非贪心即在'*'后加上'?'

12.1.1贪心匹配(匹配最多的字符串)

mo22 = re.compile(r'names:(.*) phone number:(.*)')
users_info = mo22.findall('names:Mike phone number:15600000000 '
                          'names:Jack phone number:18100000000 '
                          'names:John phone number:16200000000 ')
print(users_info)

正则表达式将匹配'names:'开头加上其后的所有字符,直到遇到换行符或者字符串最后为止。

注意我这里的被查找字符串是由空格分开而非换行符,为方便看每行内容我用三个 '' 分开了字符串。

先看看运行结果:

从返回结果来看

1)列表中只含一个元组,说明整个正则表达式只匹配了一个字符串

2)findall返回分组的匹配字符串,从中可以推断出,正则表达式
'names:'匹配了被查找字符串的'names:'
,正则表达式
'(. )'匹配了从'Mike'到'John'的所有字符
,正则表达式 ' phone number:'匹配了被查找字符的'phone number:' ,第二个 '(.
)'匹配了'16200000000'

12.1.2非贪心匹配(匹配最少的字符串)

在'*'修饰后加上'?'就可以改成非贪心匹配。代码如下:

mo23 = re.compile(r'names:(.*?) phone number:(.*?) ')
users_info = mo23.findall('names:Mike phone number:15600000000 '
                          'names:Jack phone number:18100000000 '
                          'names:John phone number:16200000000 ')
print(users_info)

先来看看运行结果:

列表中有三个元组,说明被查找的字符串中有三组字符串和正则表达式匹配成功。

1)第一组匹配:'names:'匹配'names:','(.*?)'匹配'Mike',空格匹配空格,'phone number'匹配'phone number','(.*?)'匹配'15600000000'

2)3)同1)

12.2 '.*'匹配换行符

事实上可以通过向compile()方法传关键字参数就可以让'.*'匹配换行符。

我们先看看不加关键字实参的情况

mo24 = re.compile('.*')
sentences = mo24.search("I can see empty streets.\nBut I can't sleep empty sheets\n")
print(sentences.group())

由于'.'不能匹配换行符,所以文本只能匹配到'I can see empty streets.'

来看看运行结果:

显然我们的判断没错

现在我们将关键字实参
re.DOTALL
传入compile()方法

mo25 = re.compile('.*', re.DOTALL)
sentences = mo25.search("I can see empty streets.\nBut I can't sleep empty sheets\n")
print(repr(sentences.group()))
print(sentences.group())

容我先解释一下
repr(str)
函数的作用,它返回字符串str的原字符串,不进行转义。

在这个正则表达式中,
'.*'将匹配所有的字符
,那么,search()将要返回的是,包含所有被查找文本字符的对象

我们来看看运行结果:

可以看到,repr()返回的字符串内含有所有被查找文本,包括换行符也被'.'匹配

13.模糊大小写

我们在输入验证码验证自己不是机器人时,往往大小写均可以让自己通过。那么在正则表达式当中,我们也想在匹配时忽略大小写。只需要向compile()方法传入关键字实参
re.IGNORECASE
即可,速记一下就是ignore case大写并且去空格

mo26 = re.compile(r'england|china|america', re.IGNORECASE)
country_name = mo26.search("THE PEOPLE'S REPUBLIC OF CHINA")
print(country_name.group())

由于大小写模糊,'CHINA'和正则表达式中的'china'匹配

运行结果:

14.更好得书写管理正则表达式

过于复杂的正则表达式将变得难以理解,那么可以通过以下几个方法让正则表达式易理解:

14.1多段 '' 连接

比如我们想在一串文本中找出大陆的电话号码,使用
r''
分行并写下注释,那么可以通过以下示例代码查找:

mo27 = re.compile(r'(\+86)?'  # 大陆电话前缀
                  r'(\d\d\d)'  # 电话前三位
                  r'(\d\d\d\d)'  # 电话中间四位
                  r'(\d\d\d\d)')  # 电话后四位
phone_number9 = mo27.search('电话号码是:15600000000')
print(phone_number9.group())

运行结果:

14.2关键字实参
re.VERBOSE

可以使用关键字实参和多行字符串
''' '''
标识,来使正则表达式更易读。

mo28 = re.compile(r'''(\+86)?  # 大陆电话前缀
                      (\d{3})  # 电话前三位
                      (\d{4})  # 电话中间四位
                      (\d{4})  # 电话后四位
                      ''', re.VERBOSE)
phone_number10 = mo28.search('电话号码是:15600000000')
print(phone_number10.group())

运行结果:

15.多个关键字实参

如果我们想要正则表达式匹配时模糊大小写,并且让'.'可以匹配到换行符,直接这样写
mo = re.compile(r'',re.DOTALL,re.IGNORECASE)

是不被允许的,因为
compile()最多只有两个参数

那么,如何解决呢?

可以在几个关键字实参之间用'|'间隔达到多个关键字实参的效果

mo29 = re.compile(r'nice to meet you,.*',re.DOTALL|re.IGNORECASE)
response = mo29.search('NICE to Meet You,Sir.\nHow can I help you?')
print(repr(response.group()))
print(response.group())

这样,既可以有 模糊大小写的效果,也有让 '.'匹配换行符的效果

运行效果:


感谢你阅读我的博客,如果你对我的内容有任何的意见、建议或者问题,欢迎在评论区留言,我会尽快回复。如果你发现了我的错误或者疏漏,也请不吝指正,我会及时修改。希望我的博客能对你有所帮助,也期待与你的交流和分享。