2023年3月

首先,考虑下面这样一个流程图

主要的流程定义如下:

<process id="demo" name="demo" isExecutable="true">
    <startEvent id="sid-aee4f5b6-6b26-423d-85c3-499659fb523b"/>
    <manualTask id="sid-f10234c0-9056-4b68-8422-f967d08c1cac" activiti:exclusive="true" name="人工任务1"/>
    <manualTask id="sid-fad46cbb-9529-4685-83d1-95cf96dba9dc" activiti:exclusive="true" name="人工任务2">
        <extensionElements>
            <activiti:executionListener event="end" class="com.example.demo222.MyExecutionListener"/>
        </extensionElements>
    </manualTask>
    <userTask id="sid-7049a00c-8eb8-4018-9210-dba9ece4dcf7" name="用户任务" activiti:assignee="zhangsan">
        <extensionElements>
            <activiti:taskListener event="complete" class="com.example.demo222.MyTaskCompleteListener"/>
        </extensionElements>
    </userTask>
    <endEvent id="sid-5be26610-d40f-4745-87a9-f20462595a45"/>
    <sequenceFlow id="sid-3b11c1ae-cdc2-4899-9014-0aa6ecc948a3" sourceRef="sid-aee4f5b6-6b26-423d-85c3-499659fb523b" targetRef="sid-f10234c0-9056-4b68-8422-f967d08c1cac"/>
    <sequenceFlow id="sid-04d1ef58-433d-4bd5-ac73-dec836ee2856" sourceRef="sid-f10234c0-9056-4b68-8422-f967d08c1cac" targetRef="sid-fad46cbb-9529-4685-83d1-95cf96dba9dc"/>
    <sequenceFlow id="sid-428c8fef-fe25-4c09-8d27-78f109f8312f" sourceRef="sid-fad46cbb-9529-4685-83d1-95cf96dba9dc" targetRef="sid-7049a00c-8eb8-4018-9210-dba9ece4dcf7"/>
    <sequenceFlow id="sid-fb1b8968-35e5-4fde-85c9-8c8dbafcf75b" sourceRef="sid-7049a00c-8eb8-4018-9210-dba9ece4dcf7" targetRef="sid-5be26610-d40f-4745-87a9-f20462595a45"/>
</process>

两个人工任务,一个用户任务

依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M6</version>
</dependency>

流程执行监听器

package com.example.demo222;

import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;

public class MyExecutionListener implements ExecutionListener {
    @Override
    public void notify(DelegateExecution execution) {
        throw new RuntimeException("ExecutionListener报错了");
    }
}

任务监听器

package com.example.demo222;

import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;

public class MyTaskCompleteListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        throw new RuntimeException("TaskListener报错了");
    }
}

启动流程

package com.example.demo222;

import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.runtime.ProcessInstance;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DemoTest {
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;

    @Test
    void deploy() {
        repositoryService.createDeployment()
                .addClasspathResource("processes/demo.bpmn20.xml")
                .name("demo")
                .key("demo")
                .tenantId("10086")
                .deploy();
    }

    @Test
    public void start() {
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKeyAndTenantId("demo", "10086");
        System.out.println(processInstance);
    }
}

如此简单的一个流程,如此简单的代码,我以为流程会顺利启动成功,结果启动失败了

java.lang.RuntimeException: ExecutionListener报错了
	at com.example.demo222.MyExecutionListener.notify(MyExecutionListener.java:9) ~[classes/:na]
	at org.activiti.engine.impl.delegate.invocation.ExecutionListenerInvocation.invoke(ExecutionListenerInvocation.java:34) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.delegate.invocation.DelegateInvocation.proceed(DelegateInvocation.java:35) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.delegate.invocation.DefaultDelegateInterceptor.handleInvocation(DefaultDelegateInterceptor.java:25) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.bpmn.helper.ClassDelegate.notify(ClassDelegate.java:109) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.bpmn.listener.ListenerNotificationHelper.executeExecutionListeners(ListenerNotificationHelper.java:77) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.agenda.AbstractOperation.executeExecutionListeners(AbstractOperation.java:81) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.agenda.AbstractOperation.executeExecutionListeners(AbstractOperation.java:71) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation.handleActivityEnd(TakeOutgoingSequenceFlowsOperation.java:99) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation.handleFlowNode(TakeOutgoingSequenceFlowsOperation.java:84) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation.run(TakeOutgoingSequenceFlowsOperation.java:77) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.interceptor.CommandInvoker.executeOperation(CommandInvoker.java:73) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.interceptor.CommandInvoker.executeOperations(CommandInvoker.java:57) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.interceptor.CommandInvoker.execute(CommandInvoker.java:42) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.interceptor.TransactionContextInterceptor.execute(TransactionContextInterceptor.java:48) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:59) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.spring.SpringTransactionInterceptor$1.doInTransaction(SpringTransactionInterceptor.java:47) [activiti-spring-7.1.0.M6.jar:na]
	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) [spring-tx-5.3.22.jar:5.3.22]
	at org.activiti.spring.SpringTransactionInterceptor.execute(SpringTransactionInterceptor.java:45) [activiti-spring-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:29) [activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:44) [activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:39) [activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.RuntimeServiceImpl.startProcessInstanceByKeyAndTenantId(RuntimeServiceImpl.java:93) [activiti-engine-7.1.0.M6.jar:na]
	at com.example.demo222.DemoTest.start(DemoTest.java:32) [test-classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_333]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_333]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_333]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_333]
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725) [junit-platform-commons-1.8.2.jar:1.8.2]
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) [junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) [junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149) [junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140) [junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84) [junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at java.util.ArrayList.forEach(ArrayList.java:1259) ~[na:1.8.0_333]
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at java.util.ArrayList.forEach(ArrayList.java:1259) ~[na:1.8.0_333]
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53) ~[junit-platform-launcher-1.8.2.jar:1.8.2]
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57) ~[junit5-rt.jar:na]
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) ~[junit-rt.jar:na]
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) ~[idea_rt.jar:na]
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) ~[junit-rt.jar:na]
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235) ~[junit-rt.jar:na]
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54) ~[junit-rt.jar:na]

我一直以为启动流程和任务流转是分开的,流程启动以后就往后走,启动时启动,任务流转是流转,二者应该互不影响的,没想到流程节点执行时报错会导致流程启动失败。直到看了代码,我才直到我错了。流程启动会一直往下执行,直到遇到一个它无法自动处理的节点(比如:User Task)才停下来。

下面通过代码了解启动流程实例的过程:

创建流程实例后返回一个ExecutionEntity,这个ExecutionEntity就是ProcessInstance

然后,将流程实例(ExecutionEntity)丢到Agenda中

往Agenda里放的是一个ContinueProcessOperation,所以接下来看ContinueProcessOperation

本例中,整个流程有9个FlowElement

现在,程序运行到这里,当前FlowElement(对应代码中的currentFlowElement)是开始节点,即StartEvent

因为它是FlowNode,所以接下来走节点的处理逻辑

默认情况下,节点是同步执行的,如果在任务定义时指定activiti:async="true",那么将会异步执行

另外,我们还知道了,在流程执行过程中,是以同步方式调用监听器的,具体见org.activiti.engine.impl.bpmn.listener.ListenerNotificationHelper#executeExecutionListeners

这里涉及到行为Behavior了,在本例中,设计到下面这几种Behavior

Start节点对应的行为是NoneStartEventActivityBehavior,它的行为就是离开活动

通过往Agenda中放一个TakeOutgoingSequenceFlowsOperation来实现离开活动

org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation#leaveFlowNode

刚才是走的FlowNode,现在再看SequenceFlow

将下一个FlowElement设置为currentFlowElement,然后又是Context.getAgenda().planContinueProcessOperation(execution);

离开当前活动后,就来到了下一个活动,本例中下一个活动(Activity)是Manual Task

Manual Task是一个直接通过的活动

当走到第二个人工任务的时候,执行监听器时抛异常了,于是启动流程也失败了。假设他没有抛异常继续往下走就遇到一个User Task

那么在执行UserTaskActivityBehavior的时候,正常情况下是不会离开活动的,所以就停在这里了

所以,到这里,针对本例中的这个流程我们可以总结一下:

1、创建流程实例ExecutionEntity,放入Agenda中继续执行

2、一个流程包含很多FlowElement,FlowElement分两大类FlowNode和SequenceFlow。FlowNode有关联的ActivityBehavior。

3、从当前FlowElement开始,如果是FlowNode,则执行与之关联的行为,如果是SequenceFlow则继续执行

整个过程只有在FlowNode里才有可能停下来,像StartEvent和ManuTask这样的FlowNode它们的行为都是离开活动,遇到向UserTask这样的才会停下来

所以,通过源码的学习可以得出两个知识点:

1、启动流程实例的过程中不仅仅是启动就完了,它还会继续执行活动,直到遇到一个它无法自动通过的活动时才会停止,在这期间不抛异常才算流程启动成功

2、监听器是同步执行的,如果在监听器里抛异常了,会影响到活动的执行。就像本例中一样,在人工任务的完成监听器里抛异常导致流程实例启动失败

3、默认情况下,活动是同步执行的

gcc -O1 -Wall -m32 -lm -o btest bits.c btest.c decl.c tests.c
In file included from btest.c:16:0:
/usr/include/stdio.h:27:10: fatal error: bits/libc-header-start.h: No such file or directory

include <bits/libc-header-start.h>

      ^~~~~~~~~~~~~~~~~~~~~~~~~~

compilation terminated.
In file included from decl.c:1:0:
/usr/include/stdio.h:27:10: fatal error: bits/libc-header-start.h: No such file or directory

include <bits/libc-header-start.h>

      ^~~~~~~~~~~~~~~~~~~~~~~~~~

compilation terminated.
In file included from /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/limits.h:194:0,
from /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/syslimits.h:7,
from /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/limits.h:34,
from tests.c:3:
/usr/include/limits.h:26:10: fatal error: bits/libc-header-start.h: No such file or directory

include <bits/libc-header-start.h>

      ^~~~~~~~~~~~~~~~~~~~~~~~~~

compilation terminated.
Makefile:11: recipe for target 'btest' failed
make: *** [btest] Error 1

解决方法

解决方法

Installed systemd unit for VNC Server in Virtual Mode daemon
Start or stop the service with:
systemctl (start|stop) vncserver-virtuald.service
Mark or unmark the service to be started at boot time with:
systemctl (enable|disable) vncserver-virtuald.service

虚拟机配置vncServer

bitXor

仅使用&和~14步之内完成^

解决思路

通过列举异或(^),按位与(&)和取反(~)在不同情况下的计算结果可以得到如下结果

^

1 1 0 0
1 0 1 0
0 1 1 0

&

1 1 0 0
1 0 1 0
1 1 1 0

~

1 0
0 1

为了描述的方便,设两数为
x

y
。通过上述表格不难发现,
~(x & y)

(x^y)
的结果仅仅在
0 0
这一种情况下是不正确的,考虑解决这一问题
0 0
这种情况运算结果是
1
,正确结果应当为0,
~(~x & ~y)
得到的结果恰好可以仅将
0 0
这种情况结果为
0
,其他所有情况为
1
,再
&
,恰好可以在不变动其他情况的条件下修正
0 0
的结果

tmin

仅使用! ~ & ^ | + << >>,4步之内获取最小的32bit的二进制补码整数
w
位补码所能表示的范围为
\([-2^{w - 1}, 2^{w - 1} - 1]\)

1 << 31

isTmax

仅使用 ! ~ & ^ | +,10步之内判断一个数据是否为最大的32bit的二进制补码整数

我的最初想法是

int isTmax(int x) {
  return !(~(x + 1) ^ x);
}

我想的是需要用一种方式区分开最大值和其他值
这样想的理由是只有在x为最大值时,通过
~(x + 1)
这种溢出的运算才可以获取到最大值,然后与x异或最大值时为0,再取反得到1
可能这种通过运算溢出的方式不可行
其实回过头来想,判断一个数是否为最大值,只需要用最大值和它异或即可
我的方式也是想办法获得最大值,可是最大值可以直接表示
我忽略了一点,题目不允许使用超过255的数据,所以无法直接使用0x7fffffff,
而且无法使用移位操作,因此无法构造出最大值
所以只能考虑假设x为最大值,如果操作x构造出最大值
其实上面的做法中通过~(x + 1)是正确得到0x7fffffff的方法
但是答案不正确,是因为-1的存在
换言之,
x + 1 = ~x
, x == 2147483647 or -1

把这个问题真正解决了可以发现,应该是这么想,恰好发现2147483647+1的结果与2147483647正好是取反的关系
因此把2147483647+1反过来再与2147483647异或恰好可以得到0,这里其实想的并不是去构造最大值2147483647
而我想的是去构造2147483647,所以没有关注到这里会出问题,我实际上找的是
x + 1 = -x
这样的x,没想到除了2147483647,-1也是解,
因此考虑如何区分2147483647和-1,当我看到-1+1的结果是全0后,考虑到这样一个问题
只有!0=1,其他所有数!结果均不为1,前半部分只有2147483647和-1=0,其他均为1,后半部分只有-1为1,其他均为0
而|会使除-1以外的数据运算结果不变2147483647还是0,其他还是1,而-1会从0变成1,从而就单独把2147483647区分出来了

这道题如果允许使用移位符其实和下面的allOddBits就是一样的了

allOddBits

原理:x ^ x = 0
检测是不是奇数位全为1,只需要构造出奇数位全为1即可,
首先&一下是为了获得奇数位全为1的形式,如果x不符合条件,那么&之后的奇数位一定有为0的,^之后的结果就不是0

negate

负数的补码表示恰好为正数二进制取反加一

isAsciiDigit

不处在
0x30

0x39
范围的条件为
1.前面不是3
2.后面大于9
我在做的时候不会的是如何判断后面是不是大于9,暴力想法是判断它是不是10,11,12,13,14,15
但是应当从二进制的角度去想是不是大于9,大于9只需要首位为1,后面两位任意一位为1
(x & 0xF0) ^ 0x30
错误原因在于
& 0XF0
,当遇到一个位数大于8位的数字时,从第9位往高位的所有数字都会被
0xF0
填充的0变成0
因此只要低8位是0x30,前面不管是多少异或的结果都是0
其实0xF0的意图是想把低4位清0,以便比较低8位中的高4位,但是高位补的0使得结果出现错误
如果用我的想法,只要低8位处在合法范围内就会将这个数判断为合法的,原因就是高位的数据被0xF0补充的0所消灭了

conditional

需要根据x的是否为0获得y或z
通过运算可以得到如下结果

x !x !!x !!x - 1 ~(!!x - 1)
不是0 00000000000000000000000000000000 00000000000000000000000000000001 00000000000000000000000000000000 66666666666666666666666666666666666666666666666666666666666611
0 00000000000000000000000000000001 00000000000000000000000000000000 66666666666666666666666666666666666666666666666666666666666611 00000000000000000000000000000000

我的想法是首先把x转化成一个对应的逻辑值0或者1,也就是通过两次!运算

然后发现如果想取y那么只需要用全1和它进行&运算,同时用全0与z进行&运算,如果想取z则把这个运算返过来

所以目前的任务是把!!x的结果能够满足上述运算的效果,也就是要通过!!x获得一个全0和全1

但是通过上表可以发现,!!x有两种结果,而我们必须通过一个相同的操作获得全0或者全1,可以发现-1恰好可以满足,而该题不允许使用减号,因此通过加-1实现,而-1的二进制恰好为对0取反

在解决问题之后,我们回过头来看这个表格,可以发现,可以不经过!!这个过程,方法是利用数据左移是逻辑移位,右移是算数移动,所谓算数移位是指在向右移动时,左侧补充的值是符号位,如果想让全0还是全0,0000...001变成全1,只需要让!左移31位,再右移31位置,即可以相同的方式实现从!x到!!x-1的转换

isLessOrEqual

很容易想到判断x是否小于等于y,只需要判断x与y的差值是否是小于等于0的即可
x和y的符号有下面几种可能

x的符号 y的符号 x符号位 y符号位
+ + 0 0
+ - 0 1
- + 1 0
- - 1 1

上面通过减法的结果判断两数大小关系是正确的思路,但是在进行减法时,异号之间可能会发生溢出
而异号之间仅通过符号位就可以判断出大小关系了,不需要进行减法
通过上表可以发现可以区分同号和异号的是符号位异或的结果,同号符号位异或结果为0,异号符号位异或结果为1
而且可以发现,在异号时x是否小于等于y的结果与x的符号位恰好相同
通过上述得到的性质可以使在同号时使用减法来判断大小关系,在异号时通过符号位判断大小关系
符号位异或的结果就像是一个开关,在一种情况下打开一个,关闭另一个,另一种情况则正好相反

logicalNeg

这个问题没有想出来
最初的思路显然需要找到0和其他数据之间的区别,我认为这道题之所以没有想出来问题就出在这里
我的关注点始终放在了一个单独的数据上,从最终答案来看关注点应当是两个数据
我想的始终是对于一个非0的数据,怎么才可以将它与0区分开来,但是始终找不到可行的方法
如果把数据按照数轴列的方式列出来,或许我可以发现相反数的存在
0和-2147483647的相反数是它们本身,其他数的相反数符号位一定与原数符号位相反
通过符号位来进行区分
同时1bit的0和1之间的相互转化,可以使用!也可以使用^运算

howManyBits

题目中给了几个Example,看前几个的时候还是很明确的,但是到了-1就糊涂了,
在补码中

  • 对于正数,默认值都是0,因此最高位的1代表着数据表示的结束
  • 对于负数,因为负数的补码是正数取反得到的,因此默认值为1,因此最高位的0代表着数据表示的结束
    因此对于正数,找到最高位1所在位数+1即为答案,对于负数,找到最高位0所在位数+1即为答案
    显然-1对应的答案也是满足上述方法的,只是确实不太好理解,因为它只留下了一个符号位,既是符号位同时转为原码后也能代表+1
    正数和负数的寻找目标是不同的,但是可以对负数按位取反,从而将正负数的操作统一为寻找最高位的1
    到此为止,还是通过思考可以想出来的,后续的问题就是如何得到最高位1所在的位数
    单就统计位数这个问题我就没有相出什么办法,没想出来怎么可以实现计数
    方法是看看高16位是否存在1,如果存在,那么位数计数要从16开始(这个可以通过移位实现)
    之后把原来的数据变成原来的高16位或低16位,这取决于高16位是否存在1
    之后看高8位,按照相同的过程直到变成看高1位从而找到1
    判断一个数中有没有1等价于看这个数是否为0,通过!运算即可实现
    !!(x >> 16)
    :x的高16位是否存在1
  • 存在1,那么接下来的寻找空间就由原来的32bit变为高16bit,因此需要把x右移16位,而答案位数最低也是16了,因此还要对答案累积16,因此可以发现x要移动的位数和答案要累积的数值是相等的,所以直接对
    !!(x >> 16)
    进行移位
  • 不存在1,寻找空间由原来的32bit变为低16bit,x需要右移0位,答案位数最低为0
    上述两种情况都按照存在1的方式进行移位均可以得到正确结果
    如果
    !!(x >> 16)
    为1代表最高位1存在于高16位,假设最高位1位于17bit,那么此时bit_16的结果代表最高位1的后面一位
    bit_1代表在2位中最高位1是否存在于高1位,把bit_16到bit_1的结果相加得到的应当是最高位1所在位置的下一位,比如最高位1处在4,那么bit_16到bit_1的和就是3,处在4的下一位
    而我们的目标是最高位的1所处的位置,而非最高位1的下一位所处的位置
    因此还需要加上最高位1本身,但是这里不能直接+1,如果存在最高位1,直接加1确实可以,但是如果不存在最高1应当加0
    所以一个等效的办法是加上最后处理完成后的仅剩1位的x,x如果是1代表存在最高位1,那么加1,即加上它本身的位置,x如果为,说明不存在最高位1,加上0答案也是正确的

float_twice

  1. exp不为全0或全1,全0和全1有特殊用途

  2. 阶码E=exp-bias(偏置值),exp是无符号数据的值,bias有计算公式
    \(2^{k - 1} - 1\)
    ,在单精度浮点数中=127

  3. 尾数M存在规格化的问题,表示为1.xxx的形式,把原数中的小数点左移,通过阶码来表示这种移动

frac之所以向右侧补0,是因为frac表示的是小数点右侧的数值,是从左侧开始计数的,因此向右侧补0,就像小数点左侧是从右侧开始计数,向左侧补0

单精度格式分类

以下公式的立足点在于已知上图中的数据推导出原数,即从exp和f推导出E和M

  1. 规格化

    \(E = exp - Bias(exp = E + Bias)\)

    \(M = 1 + f(M = 1.f_{n - 1}f_{n - 2}...f_{0})\)
    ,所以f实际是将M小数点移到左侧最高位之后的位置,小数点右侧的内容

  2. 非规格化


    非规格化数的用途是:1.表示0; 2.表示非常接近0的数


    阶码域为全0时,即
    \(exp=0\)

    如果按照规格化的规定,应当有
    \(E = 0 - Bias = -Bias\)
    ,但是却规定
    \(E = 1 - Bias\)

    M = f, 即f表示的小数是多少M就是多少,与规格化的区别在于缺少了隐含的开头的1

  3. 无穷大

    exp为全1,frac为全0,s为0时代表正无穷,s为1时代表负无穷

  4. NaN

    exp为全1,frac不为0时,表示不能是实数和无穷的数值

解题

对于给定的一个数据,存在以下几种情况

  1. 规格化数

    exp+1即可,但要考虑是否会发生溢出

    如果结果变为全1,是不是要返回无穷,没道理变成Nan,变成无穷是不是还要把frac变成全0。答案是对的,规格化数的最大值的exp加1后应当变为无穷,确实要把frac修改为全0

exp为全0

  1. 非规格化数

    感觉上是对frac直接乘2,如果超过非规格化范围怎么办?

    *2就是对frac直接左移1位,如果超过了非规格化的范围,最左侧的1会移动到exp的范围内,很神奇的是此时不需要进行任何操作结果恰好是正确的,这里可能和非规格化标准的定义有关系

exp为全1

  1. 无穷大和无穷小

    直接返回原数即可

  2. NaN

    直接返回Nan即可

需要注意的是这里没有仅能使用8bit数据的限制了,可以使用32bit的数据了,所以提取数据就变得简单许多

问题是把unsigned uf的uf的二进制直接当作浮点数了,不是说给定一个值要先转化为二进制然后再根据E和exp,f和M的关系转换为浮点数,而是它的二进制本身就是浮点数,这点我一直理解的有问题。因为题目中采用的是无符号数,而且把这个无符号数就当作了浮点数,所以在给定一个数据时它的二进制表现形式就直接被当作浮点数了,所以这也就是为什么2147483647是NaN了,因为它的二进制表现形式是0x7fffffff,对应exp位置的是0xff,对应frac位置的又不为0,刚好是浮点数中NaN的表现形式

float_i2f

作为参数的int可能是一个负数,但是在修改为float形式时,都是观察正数的情况的,之后修改符号位为1

把int转化为float,采用unsigned形式存储并返回

浮点数包含以下几种情况:

  1. 规格化

  2. 非规格化

  3. 无穷

  4. NaN

int的表示范围为[-2147483648, 2147483647]

这个范围内的数转化为float时,不会出现2(0除外),3,4的情况

所以要处理的只是0和规格化数

由于不存在NaN和无穷的情况,所以exp还是很好处理的

难点在于frac的处理,一方面要考虑尾数不够23bit和超过23bit两种情况

不够23bit向左移,右侧自动补0不会出什么问题

如果超过23bit需要向右移,这时候出现一个问题是右侧被丢弃的位涉及到数据的舍入,IEEE默认的舍入的规则为向偶数舍入,通俗地说可以为4舍6入5取偶数,不够一半就舍弃,例如1.4->1,超过1半就升上去,例如1.6->2,刚好一半就向最接近的偶数靠近,例如1.5->2,2.5->2,对于3.5这种减少和增加都可以到达偶数的,默认升上去变为4

上面是以10进制为例的,2进制的规则见下方例子

相当于保留精度到整数

  • xxx.0xxxx: 直接舍弃小数部分,不需要进行操作

  • xxx.10000 (对应
    else flag = frac & 1
    )


    • xx1.100000: 升上去 (flag = 1)
    • xx0.100000: 已经是偶数不需要处理的 (flag = 0)
  • xxx.10001: 升上去 (对应
    if (m & 1) flag = 1
    )

float_f2i

int转float时,由于int本身的限制,转float时并不会出现非规格化,无穷和NaN的数据

但是float转int时,由于float本身那几种形式都有可能出现,所以分开讨论

  1. 规格化

    exp不为全0和全1

  2. 非规格化

    exp为全0,结果是0(
    此处其实还有疑惑,为何浮点数向整形转换会直接舍弃掉小数部分?例如浮点数0.9转换为int时直接变为0
    )

  3. 无穷

    exp为全1,frac为全0,返回题目规定值

  4. NaN

    exp为全1,frac不为全0,返回题目规定值

容易被忽视的一点是,在规格化时,当exp计算得出的e过大使得表示的数据超出int的范围时应当返回题目规定值,所以这也就涉及到了如何判断是否溢出

int的最大值是06666666666666666666666666666666666666666666666666666666666661,即
\(1.1...1 * 2^{30}\)
,所以我觉得当超过30时即会超出范围(
此处还有疑问,超过30即
\(>= 31\)
,但实际测试设置为
\(> 31\)
结果依然正确,尝试将溢出的范围设置更大时依然可以得到正确结果,尝试查看测试数据,但是没有找到

),巧合的是当浮点数为无穷和NaN时的e也是超出这个范围的,所以可以将操作进行合并

首先计算e,根据e即可区分几种情况

image

问题描述

参考上图所示迷宫,编写算法求一条从入口到出口的有效路径。

  1. 图中阴影方块代表墙(不可行走),白色方块代表通道(支持行走)。
  2. 所求路径必须是简单路径,即所求得的路径上不能重复出现同一通道块。

算法分析

初步分析

通常采用穷举法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路返回,换另一个方向继续探索,直到探索到出口为止。
为了保证在任何位置都能原路返回,显然需要用一个先进后出的栈来保存从入口到当前位置的路径。

求迷宫中一条路径的算法的基本思路是:

如果当前位置“可通”,则纳入“当前路径”,并继续朝下一个位置探索,即切换下一个位置为当前位置,如此重复直至到达出口;如果当前位置不可通,则应沿“来向”退回到前一通道块,然后朝“来向”之外的其他方向继续探索;如果该通道块的四周4个方块均不可通,则应从当前路径上删除该通道块。
所谓下一位置指的是当前位置四周(东、南、西、北)4个方向上相邻的方块。
**

具体流程

1.声明一个结构体patch表示每一个方块,含有两个成员:

(1)int type: 1-通道;0-墙;
(2)int flag: 1-未走;0-已走;-1-不可走。

2.创建一个矩阵表示迷宫,元素类型为结构体patch。

3.创建一个栈,用于存储当前路径依次所经过的每个patch的坐标信息(x, y)。

4.从当前位置cur出发(cur初始化为起点位置),然后基于cur按“东-南-西-北”4个方向顺序依次试探,即按选定的试探方向往前进一个patch到达next位置:

(1)若next“可走”,则将cur入栈,同时将cur对应patch的flag更新为0,然后将cur更新为next,然后重复4;
(2)若next“不可走”,则改变试探方向基于cur前进一个patch获取新next,然后重复(1);
(3)若cur的“东-南-西-北”4个方向均“不可走”,则代表当前位置cur对应patch不可通,将cur对应patch的flag设为-1,执行出栈操作,并将cur更新为出栈元素对应的位置,将新cur对应patch的flag更新为1,然后重复4。
(4)若next等于终点,则将cur和next均入栈并将二者对应patch的flag更新为0,寻找有效路径结束。
(5)寻找过程中,若当前位置cur重新回退至起点位置,代表所给迷宫无解。

5.栈内存储的从“栈底元素 - 栈顶元素”对应的patch序列即为有效路径。

代码实现

step1 : 结构体定义与创建

#include <iostream>
using namespace std;
#define MaxMazeSize 40   /* 迷宫的最大行列*/
#define MaxStackSize 100 /*栈的最大容量*/

/*声明一个结构体表示patch的坐标信息*/
typedef struct
{
    int x, y;
} Position;

/* 声明一个结构体patch表示每一个方块 */
typedef struct
{
    int type = 0; // 0-墙;1-通道
    int flag = 1; // 0-已走;1-未走(可走);-1-不可走(禁走)
} Patch;

/*声明栈结构体*/
typedef struct
{
    Position data[MaxStackSize];
    Position *top = data; // 默认初始化栈
} PosStack;

PosStack S; // 创建栈保存有效路径坐标信息
Patch maze[MaxMazeSize][MaxMazeSize]; // 创建迷宫(二维列表):元素类型为结构体patch
int rows, cols; // 迷宫的行数及列数
Position startPos, endPos; // 起点坐标 + 终点坐标

step2 : 迷宫初始化

/*初始化迷宫*/
void InitMaze()
{
    int walls;
    cout << "Please enter the number of rows and columns in the maze (separated by spaces):  ";
    cin >> rows >> cols;
    int k = 0;
    while (k < cols) // 设置迷宫外墙
    {
        maze[0][k].type = 0;
        maze[0][k].flag = -1;
        maze[rows - 1][k].type = 0;
        maze[rows - 1][k].flag = -1;
        k++;
    }
    k = 0;
    while (k < rows) // // 设置迷宫外墙
    {
        maze[k][0].type = 0;
        maze[k][0].flag = -1;
        maze[k][cols - 1].type = 0;
        maze[k][cols - 1].flag = -1;
        k++;
    }
    for (int i = 1; i < rows - 1; i++) // 内部区域全部初始化为通道
    {
        for (int j = 1; j < cols - 1; j++)
        {
            maze[i][j].type = 1;
            maze[i][j].flag = 1;
        }
    }
    cout << "Please enter the number of walls in the maze:  ";
    cin >> walls; // 用户自定义设置内部区域墙的数量
    cout << "Enter the coordinates of each wall (x and y are separated by spaces):\n";
    int x, y;
    for (int i = 0; i < walls; i++) // 用户自定义设置内部区域墙的位置
    {
        cin >> x >> y;
        maze[x][y].type = 0;
        maze[x][y].flag = -1;
    }
}

step3 : 展示迷宫

/*展示迷宫结构*/
void DisplayMaze(int rows, int cols)
{
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < cols; j++)
        {
            cout << "\t" << maze[i][j].type;
        }
        cout << endl;
    }
}

step4 : 判断某个位置对应的方块是否可走

/*给定坐标,判断该坐标对应patch是否可走*/
bool JudgeNext(Position next)
{
    if (next.x < 0 && next.y > rows - 1)
    { // 判断该坐标是否越界
        return false;
    }
    if (maze[next.y][next.x].type == 0)
    { // 判断该坐标对应patch是墙还是通道
        return false;
    }
    if (maze[next.y][next.x].flag <= 0)
    { // 判断该坐标对应patch是否可走
        return false;
    }
    return true;
}

step5 : 迷宫求解-寻找有效路径

/*迷宫求解*/
bool FindMazePath()
{
    bool reFlag = false;
    Position curPos = startPos; // 当前位置
    Position nextPos;        // 下一试探位置
    int direction;
    while (1)
    {
        direction = 1;
        while (direction <= 4)
        {
            if (direction == 1) // 东
            {
                nextPos.x = curPos.x + 1;
                nextPos.y = curPos.y;
            }
            else if (direction == 2) // 南
            {
                nextPos.x = curPos.x;
                nextPos.y = curPos.y + 1;
            }
            else if (direction == 3) // 西
            {
                nextPos.x = curPos.x - 1;
                nextPos.y = curPos.y;
            }
            else // 北
            {
                nextPos.x = curPos.x;
                nextPos.y = curPos.y - 1;
            }
            if((nextPos.x == endPos.x) && (nextPos.y == endPos.y)){ // 抵达终点
                *(S.top++) = curPos;
                *(S.top++) = nextPos;
                maze[curPos.y][curPos.x].flag = 0;
                maze[nextPos.y][nextPos.x].flag = 0;
                reFlag = true;
                break;
            }
            if (JudgeNext(nextPos)){
                break;
            }else{
                direction++; // 准备尝试下一试探方向
            }
        }
        if (direction > 4) // 当前位置不可通
        {
            maze[curPos.y][curPos.x].flag = -1;
            curPos = *(--S.top); // 执行出栈操作,并将当前位置更新为出栈patch对应位置
            maze[curPos.y][curPos.x].flag = 1;
        }else{  // next可走
            *(S.top++) = curPos;
            maze[curPos.y][curPos.x].flag = 0;
            curPos = nextPos;
        }
        if(reFlag){
            break; // 抵达终点,找到有效路径,终止寻找
        }
        if((curPos.x == startPos.x) && (curPos.y == startPos.y)){
            cout << "Maze without a solution!\n";
            break;
        }
    }
    return reFlag;
}

step6 : 主方法调用

int main()
{
    InitMaze();
    cout << "The maze is structured as follows:\n";
    DisplayMaze(rows, cols);
    cout << "Please enter the coordinates of the starting point (x and y are separated by spaces):  ";
    cin >> startPos.x >> startPos.y;
    cout << "Please enter the coordinates of the end point (x and y are separated by spaces):  ";
    cin >> endPos.x >> endPos.y;
    if(FindMazePath()){
        cout << "Successfully found an effective path, as shown below:\n";
        int length = S.top - S.data;
        Position tmp;
        for(int i = 0; i< length; i++){
            tmp = *(--S.top);
            maze[tmp.y][tmp.x].type = length - i;
        }
        DisplayMaze(rows, cols);
    }else{
        cout << "Failed to find a effective path!\n";
    }
    system("pause");
    return 0;
}

运行效果

case1 : 迷宫有解

image

case2 : 迷宫无解

image

前言

有某个线上项目,没有接入工商接口,每次录入公司的时候,都要去天眼查、企查查或者其他公开数据平台,然后手动录入,一两个还好说,数量多了的重复操作就很烦,而且,部分数据是包含超链接,一不注意就点进去,又多了一个步骤。
因此,我就用Quicker写了一个数据抓取脚本,用来抓取一些公开的工商数据,逻辑很简单,知识点只有基础html、json节点提取。
Quicker脚本分享地址:
https://getquicker.net/Sharedaction?code=f9963209-c56c-48b5-c379-08db2ab3ed80

实现逻辑

探索思路

  1. 天眼查的搜索框关联了一个快速查询的接口,可以根据关键字快速查询一个公司的基础信息,我们需要的是公司id,用来查询详细信息;
  2. 根据id是没法直接获取到公司的工商信息的(可能是我没花太长时间,没爬到相关的接口),但既然页面上是显示了的,那就能获取,不能爬接口就爬网页吧;
  3. 但是爬网页又遇到一个问题,直接通过get获取html文档的话,工商数据那一栏是没加载(无节点数据)的,初步估计设计上是嵌入式的延迟加载,要是浏览器载入加载后才能获取对应节点数据;
  4. 获取延迟加载的数据可以使用Quicker“浏览器控制”的等待浏览器加载完成实现,但这样还要调用浏览器进行模拟操作有点不合理,本来就不复杂的操作,搞得还有点麻烦了;
  5. 通过翻详情页面的html源码,我发现,工商数据并不是真正意义上的延迟加载,而是先获取到数据,挂载到资源,然后再响应式进行数据渲染,同时渲染的数据少点的话,比较节约资源,也就是说,实际上通过get获取到的html源码是包含了工商数据的,见下节点;
<script id="__NEXT_DATA__" type="application/json">{json}<script>
  1. 说来也奇怪,节点的类型是application/json的话,那就说明是数据通过json文件获取到,但我还是没查到到底是哪个接口获取的,有个包含了{id}.json的接口,但这个接口1是没返回数据,2是更改参数后会提示没权限,我相信深扒网页js脚本的话,应该是能找到方法的,但搞起来好麻烦,暂时不深究;

实现步骤

至此,开发逻辑明确,六步实现(实际上按照知识点来说,只有两个步骤,接口爬取和获取节点):

  • 第一步,根据关键字快速查询公司;
  • 第二步,直接使用快速查询到的第一个公司,拿到其id;
  • 第三步,使用id,get访问详情网页,获取网页源码;
  • 第四步,读取html公司数据节点;
  • 第五步,读取业务需求信息的json节点,重新组装拼接;
  • 第六步,展示数据;

效果展示

脚本截图
image
效果演示,功能很简单
image

结语

这个脚本就是促进生产力的一个很好表现

一:存储架构

根据存储设备所在的位置分类

image

1、DAS

DAS:
(Direct-Attached Storage)直连式存储。服务器使用专用线缆(例如SCSI)和存储设备(例如磁盘阵列)进行直连。

特点

  • 优点是储设备只能连接到一台主机使用,无法共享,成本较高,且安全性可靠性较低
  • 缺点是容量有限,不适合大规模的数据存储和共享。

使用场景
:个人电脑、小型企业、高性能计算环境等。

2、NAS

NAS:
(Network-Attached Storage)网络附加存储。服务器和存储设备非直连,而是通过
ip网络
进行连接,这样就实现了多台主机与存储设备之间的连接。

特点

  • 优点是易于管理、容量可扩展、能够实现共享存储和备份
  • 缺点存在IO瓶颈,性能较低,不适合高性能计算和数据库应用

使用场景
:文件共享、备份和存储等。

3、SAN

SAN:
(Storage Area Network)存储区域网络。基于NAS发展而来,通过
专用光纤通道交换机
访问数据,采用ISCSI、FC协议。

特点

  • 优点:解决了NAS的IO瓶颈问题,因为采用光纤、iSCSI等协议来连接设备,速度很快。
  • 缺点:价格昂贵、结构复杂、需要专业的维护和管理。

使用场景
:数据中心、虚拟化、云计算等环境

NAS和SAN区别
SAN:

可以理解为一种虚拟化存储的技术,它将存储设备从服务器中分离出来,形成一个独立的存储网络。

客户端访问这些存储设备,就像访问本地硬盘一样。因此,客户端可以对这些存储设备进行格式化、分区、挂载等操作。

NAS:

可以理解为一个存储服务器,它已经预先安装了操作系统和文件系统,并且已经格式化好了磁盘,因此客户端设备可以直接通过网络连接到NAS上来访问数据,而不需要进行格式化或设置文件系统等操作。

客户端设备可以通过网络共享协议(如SMB/CIFS、NFS、AFP等)访问NAS上的数据。

块存储和文件存储

  • 块存储:
    是将数据切分为固定大小的块(block),每个块都有唯一的地址,可以单独进行读写和处理。
    DAS和SAN使用的就是块存储。
  • 文件存储:
    是将数据以文件形式存储在一个统一的文件系统中,每个文件都有唯一的名称和路径,文件系统可以通过文件名或路径名来定位文件。
    NAS使用的就是文件存储。
    image

二:文件共享服务

UNC 格式:

UNC:
(Universal Naming Convention)通用命名规则。由微软公司发明,是一种用于在网络上指定文件或文件夹位置的命名约定,允许用户通过网络共享访问文件和文件夹。

UNC格式广泛使用在windows中,Linux中也支持这种格式,例如使用Samba软件包来实现文件和打印机的共享。

格式:
\\server\share
使用反斜杠(\)作为分隔符

  • server:表示共享资源所在的计算机名称或IP地址
  • share: 表示共享资源的名称

注意:

UNC路径
不能指定端口号
,因为端口号不是共享资源的一部分,所以使用UNC格式的路径时不能指定端口号。

\\10.0.0.22:446\share 是错误的

1、FTP

FTP:
(File Transfer Protocol )文件传输协议,属于应用层协议,是NAS存储架构的一种协议,基于CS结构。

FTP和NAS的区别:
FTP是一个应用层协议,用于实现跨主机传输文件,NAS是一种网络架构,NAS可以使用各种协议进行文件传输,包括FTP、SMB、NFS等,因此,FTP可以用于在NAS中传输文件,但它本身不属于NAS这种网络架构。

FTP的特点:
跨平台:windows、linux等操作系统都支持FTP协议。

FTP工作原理:
采用的是双端口模式,分为命令端口和数据端口,命令端口对应命令通道,数据端口对应数据通道。

  • 命令通道:客户端和服务端之间传输FTP命令和响应,以控制文件传输的整个过程。服务端的默认端口是tcp/21。
  • 数据通道:客户端和FTP服务器之间实际传输数据的通道。

FTP的两种工作模式:

  • 主动模式:FTP服务器主动连接客户端,这个时候FTP服务器的数据端口使用的是20端口。
  • 被动模式:客户端主动连接FTP服务器,这时候FTP服务器的数据端口是随机的。

说明:
两种工作模式是针对数据通道的建立来说的,无论是主动模式还是被动模式,FTP客户端都需要连接到FTP服务器的21号端口,以建立命令通道。

FTP的工作流程:
(1)FTP服务端开启对21端口的监听,等待客户端的连接。
(2)客户端发起连接,通过连接到服务端的21端口,建立命令通道。
(3)进行数据交互:

  • ​ 主动模式:服务端主动找客户端建立数据通道,这个时候服务端使用的数据端口是20端口,客户端随机。
  • ​ 被动模式:客户端主动连接服务端,这个时候双方使用的端口都是随机的。

FTP的实现:
windows

  • 客户端:浏览器 、资源管理器、Filezilla等
  • 服务端:FileZilla Server、IIS等

Linux

  • 客户端:ftp、wget、curl
  • 服务端:vsftpd、Wu-ftpd

Linx中用于搭建FTP服务器的工具:VSFTP

特点
:性能好、下载速度快、单机可支持15k并发量。红帽默认使用的ftp服务端工具就是vsftp

2、NFS

NFS:
(Network File System) 网络文件系统,基于内核的文件系统。Sun 公司开发,通过使用 NFS,用户和程序可以像访问本地文件一样访问远端系统上的文件,基于RPC(Remote Procedure Call Protocol )远程过程调用实现。

NFS工作原理:

  • 客户端发起挂载请求:客户端需要访问远程主机上的文件,它会向NFS服务器发起挂载请求。
  • 服务器返回挂载信息:服务器返回需要挂载的目录信息,包括目录的文件系统类型、权限和访问方式等。
  • 客户端进行挂载:客户端使用NFS协议挂载远程目录,将目录挂载到本地的一个挂载点上,此时客户端可以像访问本地文件一样访问远程文件。
  • 客户端读写文件:客户端通过挂载点访问远程文件,当客户端需要读写远程文件时,它会发送NFS请求到服务器,请求服务器读取或写入文件数据。
  • 服务器响应请求:服务器接收到客户端的请求后,会读取或写入相应的文件数据,并将结果返回给客户端。
  • 客户端卸载挂载点:当客户端不再需要访问远程文件时,它会卸载挂载点,断开与NFS服务器的连接。

NFS使用的端口:

  • Portmap(RPC bind)服务: Portmap服务使用TCP或UDP端口666666,它是NFS和其他RPC服务的注册和映射程序。
  • NFS服务:NFS服务使用TCP或UDP端口2049,它是NFS协议的主要端口。
  • Nlockmgr(Network Lock Manager)服务:Nlockmgr服务使用TCP或UDP端口32803,它用于提供NFS文件锁定服务。
  • Mountd(Mount Daemon)服务:Mountd服务使用TCP或UDP端口20048,它是NFS挂载协议的主要服务。

NFS的相关进程:

  • rpc.nfsd 最主要的NFS进程,管理客户端是否可登录
  • rpc.mountd 挂载和卸载NFS文件系统,包括权限管理
  • rpc.lockd 非必要,管理文件锁,避免同时写出错
  • rpc.statd 非必要,检查文件一致性,可修复文件

说明:CentOS 6 开始portmap进程由rpcbind代替

NFS的使用场景:

场景一:Linux和LInux之间实现文件共享:

服务端共享出某个目录后,客户端直接挂载进行使用。
image

场景二:Linux和Windows之间实现文件共享:

Linux作为服务端,windows挂载linux的共享目录为本地的一个磁盘,Windows需要开启NFS客户端功能。
image
image

NFS的使用场景:

NFS:适用于需要在Unix和Linux系统之间进行文件共享的环境,例如服务器集群和高性能计算环境。

3、samba

Samba是一种基于Windows的文件共享协议开发而来的软件,它可以在Windows、Linux和Unix系统之间共享文件和打印机。Samba可以让Linux或Unix系统像Windows一样作为文件服务器,从而方便Windows系统用户访问和使用共享文件和打印机。

SMB协议:
(Server Messages Block)信息服务块协议,由ibm开发,最早用在微软的dos系统上面,windows之间的文件共享就是使用SMB协议实现的。

CIFS:
(common internet file system)通用网络文件系统,基于smb协议开发而来的文件系统,可以理解为SMB协议的升级版。

samba服务使用的端口:

默认是445和139端口

  • 445端口:实现Internet文件共享
  • 139端口:文件和打印共享

samba的使用场景:
场景一:Linux作为服务端,Windows作为客户端实现文件共享
image

通过网络驱动映射器映射为windows本地的一个磁盘。
image

场景二:windows作为服务端,linux作为客户端实现文件共享
image

samba服务存在的问题:

因为以前永恒之蓝病毒和smb1的漏洞,运营商直接把139和445这两个端口给屏蔽了,即使手动在防火墙打开这两个端口也没法使用。

windows的文件共享又默认使用的是139和445端口,无法更改客户端的端口。

在linux作为服务端,windows作为客户端的时候,因为windows默认端口没法更改,且使用的是UNC路径进行访问。

解决方法:

方法一:配置本地端口映射转发

# https://blog.csdn.net/weixin_42552016/article/details/128421145
netsh interface portproxy add v4tov4 
		listenport=445 listenaddress=127.0.0.1 connectport=4450 connectaddress=116.116.132.151

image

方法二:使用端口转发驱动

# Multi Port Forwarder驱动
	# https://tubecast.webrox.fr/landrive/portmapping.html
	# https://www.verigio.com/products/multi-port-forwarder/default

image

4、使用场景

samba:
用于在Windows和Linux系统之间进行文件共享。

NFS:
支持多种操作系统,一般使用在linux和linux之间的文件共享,也可以实现windows和linux之间的文件共享。

FTP:
FTP不像Samba和NFS一样提供文件系统级别的共享,它只是提供了一种简单的方式来传输文件,客户端通过FTP客户端软件连接到FTP服务器,然后可以上传和下载文件。FTP一般使用在将文件从一个计算机上传到另一个计算机。

三:跨主机拷贝文件

1、SCP工具

scp是基于ssh协议开发的ssh客户端工具。

# 使用格式                 
   #   Pull: 	scp  [option]  /source_file   [user@]remote_host/dest_file
   #   Push: 	scp  [option]  [user@]remote_host/dest_file  /source_file
# 选项
	-r:复制文件夹
	-P PORT 指明remote host的监听的端口
# 说明 复制目录文件后面有无斜线的区别
	# 有斜线:复制文件夹里面的内容。 例如:scp /data/ 10.0.0.22:~/    表示复制data中的文件
	# 无斜线:复制整个文件夹 		 例如:scp /data  10.0.0.22:~/ 表示复制data整个目录

例如:

# 不写用户名默认使用的就是当前主机使用的用户
scp -r /data 10.0.0.22:/newdata   # 将本机的data目录推到10.0.0.22的newdata中

scp -r 10.0.0.22:/newdata /data   # 将10.0.0.22的newdata拉到本机的/data目录中

2、rsync工具

rsync是基于ssh协议开发的ssh客户端工具。有三种工作模式:

选项:

# -a         保留源文件的属性,但是无法保留acl和selinux属性  -a选项自带递归的功能
# -v         显示详细的过程
# --delete   保证两边的数据一样,如果目标文件存在某个源文件没有的文件,就会把目标文件的这个文件删除掉
# -t --times 保持mtime属性 强烈建议任何时候都加上"-t",否则目标文件mtime会设置为当前系统时间,导致下次更新,检查出mtime不同从而导致增量传输无效

(1)本地模式:
作用就类似于cp、mv等命令,实现本地文件系统的拷贝、重命名等作用。

# 格式: rsync [OPTION] SRC... [DEST]

# 例如:
    [root@LAP1 data]# rsync file1  file666666 # 实现文件拷贝功能
    [root@LAP1 data]# ls
    file1   file666666 

(2)基于传统的SSH协议模式
:类似于scp的作用,实现远程主机拷贝

# Pull:rsync [OPTION...] [USER@]HOST:SRC... [DEST]
# Push:rsync [OPTION...] SRC... [USER@]HOST:DEST

# 例如
	rsync  -av  /etc server1:/tmp    # 复制目录和目录下文件
	rsync  -av /etc/ server1:/tmp   # 只复制目录下文件 和scp一样
	rsync -av --delete source_file host:/dest_file  # 跨主机备份
		# rsync -av --delete /data/  10.0.0.12:/back

(3)作为一个独立服务模式

rsync作为一个独立的服务运行,

Pull:
	# rsync [OPTION...] [USER@]HOST::SRC... [DEST]
	# rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST] # 协议的形式访问,效果等同于上面

Push:
	# rsync [OPTION...] SRC... [USER@]HOST::DEST
	# rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST

例如:

rsync  -av  /etc server1:/tmp    # 复制目录和目录下文件
rsync  -av /etc/ server1:/tmp   # 只复制目录下文件 和scp一样
rsync -av --delete source_file host:/dest_file  # 跨主机备份

说明:
本地模式和ssh模式是通过本地或远程shell,而独立服务运行模式则是让远程主机上运行rsyncd服务,使其监听在一个端口上,等待客户端的连接。

四:文件定时同步

rsync + cron计划任务

可以实现最快每1分钟同步一次文件。

说明:
rsync使用的是基于传统的SSH协议的工作模式

例如:

root@ubuntu1804:~# crontab -e
# m h  dom mon dow   command
*/10 * * * * /usr/bin/rsync -av --delete /data/  10.0.0.12:/back

五:文件实时同步

监听文件的相关属性事件,文件发生变化的时候就触发同步,使用inotify或者sersync监听文件的变化。

1、inotify + rsync

inotify
:系统内核的一个监控服务,属于操作系统内核的一个特有机制,用于监控文件的信息变化。

inotify管理工具:
来自于inotify-tools软件包,软件包里面包含了两个主要的工具inotifywait和inotifywatch。

  • inotifywait: 在被监控的文件或目录上等待特定文件系统事件(open ,close,delete等)发生,常用于实时同步的目录监控(主要使用的就是这个工具)
  • inotifywatch:收集被监控的文件系统使用的统计数据,指文件系统事件发生的次数统计

rsync
:使用的是rsync的第三种工作模式(独立服务模式)。

例如:
image

# 1. 备份服务器启动 rsync 进程,进程启动后监听tcp的873端口。

# 2. 服务器的inotify发现数据发生变化后,就执行:
		rsync -av /data ehigh@192.168.0.104::/databackup    # 以服务的形式访问
		# rsync -av /data rsync://192.168.0.104/databackup  # 以协议的形式访问

2、sersync + rsync

sersync类似于inotify,同样用于监控,是基于inotify基础上开发而来,并且克服了inotify一个操作可能会产生重复的事件,这样可能会触发rsync的多次同步的问题。

sersync特点:

  • 会对对linux系统文件系统产生的临时文件和重复的文件操作进行过滤,在结合rsync同步的时候,节省了运行时耗和网络资源
  • 配置简单,提供了要给xml配置文件和一个二进制可执行文件
  • 采用多线程模式
  • 自带crontab功能
# sersync项目地址: https://code.google.com/archive/p/sersync/
# sersync下载地址: https://code.google.com/archive/p/sersync/downloads

例如:
image