开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring AOP系列(三) — 动态代理之JDK动态代理

发表于 2021-07-16

JDK动态代理

JDK动态代理核心是两个类:InvocationHandler和 Proxy

举个栗子

为便于理解,首先看一个例子:
希望实现这样一个功能:使用 UserService 时,只需关注自己的核心业务逻辑的实现,对于日志功能的打印,由系统的公共服务完成。
首先定义一个业务类的接口:UserService.java

1
2
3
4
5
6
7
8
9
java复制代码package com.proxy;
/**
* @author: create by lengzefu
* @description: com.proxy
* @date:2020-09-15
*/
public interface UserService {
void login();
}

实现该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package com.proxy;

/**
* @author: create by lengzefu
* @description: com.proxy
* @date:2020-09-15
*/
public class UserServiceImpl implements UserService {
@Override
public void login() {
System.out.println("用户登录...");
}
}

定义一个InvocationHandler实现类,该实现类有我们想要添加的公共日志

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
java复制代码package com.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

/**
* @author: create by lengzefu
* @description: com.proxy
* @date:2020-09-15
*/
public class LogHandler implements InvocationHandler {
// 被代理的对象,这里指的是UserServiceImpl对象
Object target;

// 构造函数,将被代理对象传进来
public LogHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(args);
after();
return result;
}

private void before() {
System.out.println("方法调用开始时间:" + new Date());
}

private void after() {
System.out.println("方法调用结束时间:" + new Date());
}
}

接下来看看如何在客户端中调用

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
java复制代码/**
* @Classname Client
* @Date 2020/9/12 2:40
* @Autor lengxuezhang
*/
public class Client {
public static void main(String[] args) {
// 设置变量可以保存动态代理类,默认名称以 $Proxy0 格式命名
// System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 1. 创建被代理的对象,UserService接口的实现类
UserService userService = new UserServiceImpl();
// 2. 获取对应的 ClassLoader
ClassLoader classLoader = userService.getClass().getClassLoader();
// 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService
Class[] interfaces = userService.getClass().getInterfaces();
// 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
// 这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
InvocationHandler logHandler = new LogHandler(userService);
/*
5.根据上面提供的信息,创建代理对象 在这个过程中,
a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
b.然后根据相应的字节码转换成对应的class,
c.然后调用newInstance()创建代理实例
*/
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);

// 调用代理的方法
proxy.login();
// 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
//ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");

}
}

开始一步步分析源码

源码分析

1.Proxy.newProxyInstance( ClassLoader loader, Class[] interfaces, InvocationHandler h)

Proxy.newProxyInstance( ClassLoader loader, Class[] interfaces, InvocationHandler h)产生了代理对象,首先分析下方法的三个参数

  • ClassLoader loader:类加载器,类加载器用于加载代理类。这里经过测试发现不论是传userService的类加载器,还是logHandler的类加载器,得到的结果都是一样。
  • Class[] interfaces:被代理类实现的接口集合。有了它,代理类就可以实现被代理类的所有接口。
  • InvocationHandler h:handler对象,不能为null。

接下来看一下方法的具体实现:

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
java复制代码 public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
//检验handler对象不能为空
Objects.requireNonNull(h);
//接口的类对象拷贝一份(接口也是一个对象,是一个类名为Class的对象)
final Class<?>[] intfs = interfaces.clone();
//安全检查,不是重点
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* Look up or generate the designated proxy class.
* 查询(在缓存中已经有)或生成指定的代理类的class对象。
*/
Class<?> cl = getProxyClass0(loader, intfs);

/*
* Invoke its constructor with the designated invocation handler.
* 用指定的调用处理程序(什么程序?)调用其(谁的?)构造函数
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
//返回constructorParams的公共构造函数
//参数constructorParames为常量值:private static final Class<?>[] constructorParams = { InvocationHandler.class };
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//这里生成了代理对象,关于这里的详解:https://www.cnblogs.com/ferryman/p/12089210.html
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

这段代码核心就是通过getProxyClass0(loader, intfs)得到代理类的Class对象,然后通过Class对象得到构造方法,进而创建代理对象。下一步看getProxyClass0这个方法。

2.getProxyClass0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//此方法也是Proxy类下的方法
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
//意思是:如果代理类被指定的类加载器loader定义了,并实现了给定的接口interfaces,
//那么就返回缓存的代理类对象,否则使用ProxyClassFactory创建代理类。
return proxyClassCache.get(loader, interfaces);
}

3. proxyClassCache.get

这里看到 proxyClassCache,有Cache便知道是缓存的意思,正好呼应了前面Look up or generate the designated proxy class。查询(在缓存中已经有)或生成指定的代理类的class对象这段注释。
proxyClassCache是个WeakCache类的对象,调用proxyClassCache.get(loader, interfaces); 可以得到缓存的代理类或创建代理类(没有缓存的情况)。先看下WeakCache类的定义(这里先只给出变量的定义和构造函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码//K代表key的类型,P代表参数的类型,V代表value的类型。
// WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache 说明proxyClassCache存的值是Class<?>对象,正是我们需要的代理类对象。
final class WeakCache<K, P, V> {

private final ReferenceQueue<K> refQueue
= new ReferenceQueue<>();
// the key type is Object for supporting null key
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map
= new ConcurrentHashMap<>();
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap
= new ConcurrentHashMap<>();
private final BiFunction<K, P, ?> subKeyFactory;
private final BiFunction<K, P, V> valueFactory;


public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}

其中map变量是实现缓存的核心变量,他是一个双重的Map结构: (key, sub-key) -> value。其中key是传进来的Classloader进行包装后的对象,sub-key是由WeakCache构造函数传入的KeyFactory()生成的。value就是产生代理类的对象,是由WeakCache构造函数传入的ProxyClassFactory()生成的。如下,回顾一下:

1
2
java复制代码private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

产生sub-key的KeyFactory代码如下,这个我们不去深究,只要知道他是根据传入的ClassLoader和接口类生成sub-key即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private static final class KeyFactory
implements BiFunction<ClassLoader, Class<?>[], Object>
{
@Override
public Object apply(ClassLoader classLoader, Class<?>[] interfaces) {
switch (interfaces.length) {
case 1: return new Key1(interfaces[0]); // the most frequent
case 2: return new Key2(interfaces[0], interfaces[1]);
case 0: return key0;
default: return new KeyX(interfaces);
}
}
}

通过sub-key拿到一个Supplier<Class<?>>对象,然后调用这个对象的get方法,最终得到代理类的Class对象。

好,大体上说完 WeakCache 这个类的作用,我们回到刚才proxyClassCache.get(loader, interfaces);这句代码。get是 WeakCache 里的方法。源码如下:

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
java复制代码//K和P就是WeakCache定义中的泛型,key是类加载器,parameter是接口类数组
public V get(K key, P parameter) {
//检查parameter不为空
Objects.requireNonNull(parameter);
//清除无效的缓存
expungeStaleEntries();
// cacheKey就是(key, sub-key) -> value里的一级key,
Object cacheKey = CacheKey.valueOf(key, refQueue);

// lazily install the 2nd level valuesMap for the particular cacheKey
//根据一级key得到 ConcurrentMap<Object, Supplier<V>>对象。如果之前不存在,则新建一个ConcurrentMap<Object, Supplier<V>>和cacheKey(一级key)一起放到map中。
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
if (valuesMap == null) {
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
if (oldValuesMap != null) {
valuesMap = oldValuesMap;
}
}

// create subKey and retrieve the possible Supplier<V> stored by that
// subKey from valuesMap
//这部分就是调用生成sub-key的代码,上面我们已经看过怎么生成的了
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
//通过sub-key得到supplier
Supplier<V> supplier = valuesMap.get(subKey);
//supplier实际上就是这个factory
Factory factory = null;

while (true) {
//如果缓存里有supplier ,那就直接通过get方法,得到代理类对象,返回,就结束了,一会儿分析get方法。
if (supplier != null) {
// supplier might be a Factory or a CacheValue<V> instance
V value = supplier.get();
if (value != null) {
return value;
}
}
// else no supplier in cache
// or a supplier that returned null (could be a cleared CacheValue
// or a Factory that wasn't successful in installing the CacheValue)
// lazily construct a Factory
//下面的所有代码目的就是:如果缓存中没有supplier,则创建一个Factory对象,把factory对象在多线程的环境下安全的赋给supplier。
//因为是在while(true)中,赋值成功后又回到上面去调get方法,返回才结束。
if (factory == null) {
factory = new Factory(key, parameter, subKey, valuesMap);
}

// 双重判空,实现线程安全?
if (supplier == null) {
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
// successfully installed Factory
supplier = factory;
}
// else retry with winning supplier
} else {
if (valuesMap.replace(subKey, supplier, factory)) {
// successfully replaced
// cleared CacheEntry / unsuccessful Factory
// with our Factory
supplier = factory;
} else {
// retry with current supplier
supplier = valuesMap.get(subKey);
}
}
}
}

4.Factory.get

supplier就是factory,所以接下来我们看看Factory类(Factory是WeakCache的内部类)的get方法。

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
java复制代码public synchronized V get() { // serialize access
// re-check
Supplier<V> supplier = valuesMap.get(subKey);
/重新检查得到的supplier是不是当前对象
if (supplier != this) {
// something changed while we were waiting:
// might be that we were replaced by a CacheValue
// or were removed because of failure ->
// return null to signal WeakCache.get() to retry
// the loop
return null;
}
// else still us (supplier == this)

// create new value
V value = null;
try {
//代理类就是在这个位置调用valueFactory生成的
//valueFactory就是我们传入的 new ProxyClassFactory()
//一会我们分析ProxyClassFactory()的apply方法
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
} finally {
if (value == null) { // remove us on failure
valuesMap.remove(subKey, this);
}
}
// the only path to reach here is with non-null value
assert value != null;

// wrap value with CacheValue (WeakReference)
//把value包装成弱引用
CacheValue<V> cacheValue = new CacheValue<>(value);

// put into reverseMap
// reverseMap是用来实现缓存的有效性
reverseMap.put(cacheValue, Boolean.TRUE);

// try replacing us with CacheValue (this should always succeed)
if (!valuesMap.replace(subKey, this, cacheValue)) {
throw new AssertionError("Should not reach here");
}

// successfully replaced us with new CacheValue -> return the value
// wrapped by it
return value;
}
}

5.ProxyClassFactory.apply

最后来到 ProxyClassFactory 的 apply 方法,代理类就是在这里生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
java复制代码//这里的BiFunction<T, U, R>是个函数式接口,可以理解为用T,U两种类型做参数,得到R类型的返回值
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// prefix for all proxy class names
//所有代理类名字的前缀
private static final String proxyClassNamePrefix = "$Proxy";

// next number to use for generation of unique proxy class names
//用于生成代理类名字的计数器
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
//验证代理接口,可不看
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
//生成的代理类的包名
String proxyPkg = null; // package to define proxy class in
//代理类访问控制符: public ,final
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
//验证所有非公共的接口在同一个包内;公共的就无需处理
//生成包名和类名的逻辑,包名默认是com.sun.proxy,类名默认是$Proxy 加上一个自增的整数值
//如果被代理类是 non-public proxy interface ,则用和被代理类接口一样的包名
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
//代理类的完全限定名,如com.sun.proxy.$Proxy0.calss
String proxyName = proxyPkg + proxyClassNamePrefix + num;

/*
* Generate the specified proxy class.
*/
//核心部分,生成代理类的字节码
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
//把代理类加载到JVM中,至此动态代理过程基本结束了
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}

6. 字节码文件分析

到这里其实已经分析完了,但是本着深究的态度,决定看看JDK生成的动态代理字节码是什么,于是将字节码保存到磁盘上的class文件中。代码如下:

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
java复制代码package com.sun.proxy;

import com.leng.proxy.dynamic.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements UserService {

private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

// 代理类的构造函数,其参数就是InvovationHandler实例
// Proxy.newInstance方法就是通过这个构造函数来创建代理实例的
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

// 固定的三个类方法 equals, toString, hashCode
public final boolean equals(Object var1) throws {
try {
return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

//接口代理方法
public final void login() throws {
try {
// invocation handler的invoke方法在这里被调用
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.leng.proxy.dynamic.UserService").getMethod("login", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

最后

  • 如果觉得有收获,三连支持下;
  • 文章若有错误,欢迎评论留言指出,也欢迎转载,转载请注明出处;
  • 个人vx:Listener27, 交流技术、面试、学习资料、帮助一线互联网大厂内推等

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java代码实现导出或者下载xml、word、pdf、exc

发表于 2021-07-16

java代码实现导出或者下载xml、word、pdf、excel功能

写在前面:将用户操作日志以xml、word、pdf、excel格式的文件导出。

1、导出xml

导出xml使用JAXB的注解实现,实体如下

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
typescript复制代码import javax.xml.bind.annotation.*;
import java.util.List;

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(name = "", propOrder = {
"title",
"log"
})
@XmlRootElement(name = "Body")
public class Submit {

private String title;

protected List<LogX> logX;

@XmlElementWrapper(name = "logList")
public List<LogX> getLog() {
return logX;
}

public void setLog(List<LogX> logX) {
this.logX = logX;
}

@XmlElement(name = "title", required = true,nillable=true)
public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}
}

LogX实体如下:

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
typescript复制代码import javax.xml.bind.annotation.*;
import java.io.Serializable;


@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(name = "", propOrder = {
"username",
"operationIp",
"createTime",
"operation",
"content"
})
@XmlRootElement(name = "log")
public class LogX implements Serializable {

private static final long serialVersionUID = 1L;

protected String content;

protected String createTime;

protected String operation;

protected String username;

protected String operationIp;

@XmlElement(name = "content", required = true,nillable = true)
public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

@XmlElement(name = "createTime", required = true,nillable = true)
public String getCreateTime() {
return createTime;
}

public void setCreateTime(String createTime) {
this.createTime = createTime;
}

@XmlElement(name = "operation", required = true,nillable = true)
public String getOperation() {
return operation;
}

public void setOperation(String operation) {
this.operation = operation;
}

@XmlElement(name = "username", required = true,nillable = true)
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

@XmlElement(name = "operationIp", required = true,nillable = true)
public String getOperationIp() {
return operationIp;
}

public void setOperationIp(String operationIp) {
this.operationIp = operationIp;
}
}

@XmlAccessorType:便于对象与xml文件之间的转换
@XmlType中:参数propOrder指定映射XML时的节点顺序,使用该属性时,必须列出JavaBean对象中的所有字段,否则会报错。
@XmlRootElement :xml 文件的根元素
@XmlElementWrapper:仅允许出现在集合属性上,使用该注解后,将会在原xml结点上再包装一层xml

@XmlElement:字段,方法,参数级别的注解。该注解可以将被注解的(非静态)字段,或者被注解的get/set方法对应的字段映射为本地元素,也就是子元素。参数 name用于指定映射时的节点名称,指定生成元素的名字,若不指定,默认使用方法名小写作为元素名;参数 required字段是否必须,默认为false;参数 nillable是否处理空数据,默认为false。
其他关于JAXB的注解,大家就根据需要自行百度一下~

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
ini复制代码        Submit submit = new Submit();
submit.setTitle(date2Str + "_logs");
//这里给loxList集合赋值
List<LogX> logXList = new ArrayList<>();
submit.setLog(logXList);
JAXBContext jc = null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
String fileName = submit.getTitle() + ".xml";
ByteArrayOutputStream bis = null;
FileOutputStream fileOutputStream = null;
try {
jc = JAXBContext.newInstance(Submit.class);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
StringWriter writer = new StringWriter();
marshaller.marshal(submit, writer);
String content = writer.toString().replaceAll(JoinConstant.JAXB_NILL_STR, "");
byte[] bs = content.getBytes("UTF-8");
File file2 = new File(fileName);
fileOutputStream = new FileOutputStream(file2);
file2.createNewFile();
fileOutputStream.write(bs);
fileOutputStream.close();
download(file2, res);
} catch (JAXBException e) {
e.printStackTrace();
throw new RuntimeException("xml生成出错", e);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
try {
if (null != bis) bis.close();
if (null != stream) stream.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

涉及到的download方法如下:

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
ini复制代码private void download(File file, HttpServletResponse res) {
String fileName = file.getName();
res.setHeader("content-type", "application/octet-stream");
res.setContentType("application/octet-stream;charset=UTF-8");
res.setHeader("Content-Disposition", "attachment; filename=" + fileName);
byte[] buff = new byte[1024];
BufferedInputStream bis = null;
OutputStream os = null;
try {
os = res.getOutputStream();
bis = new BufferedInputStream(new FileInputStream(
new File(file.getAbsolutePath())));
int i = bis.read(buff);
while (i != -1) {
os.write(buff, 0, buff.length);
os.flush();
i = bis.read(buff);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、导出word

使用freemarker技术在web后台导出word文档:
具体步骤为:

1、设计word模版
2、修改ftl模版
3、 填充数值,导出word模板
4、生成Word模板
生产word模版主要为:一个是把word文档另存为xml文档;然后把xml文档后缀改为ftl文档****
如下图,我所需要的模板为:
在这里插入图片描述
然后使用sublime打开后缀名为ftl的文件,通过sublime插件,将ftl文件格式化,然后找到如下图所示位置进行编辑
在这里插入图片描述
注意${log.xh}等其他的标签的修改

代码如下:

1
2
3
4
5
6
7
8
javascript复制代码        Map<String, Object> beanParams = new HashMap<String, Object>();
//这里给loglist集合赋值
List<LogView> loglist = new ArrayList<>();
String date2Str = DateSearchUtils.date2Str(new Date());
beanParams.put("title", date2Str+"日志信息");
beanParams.put("loglist", loglist);
beanParams.put("attention", "请注意确保所有信息的正确性");
WordExportUtil.WordExportUtil(request, response, WordExportUtil.WORD_2003, "日志信息列表导出", "templateFile.ftl", beanParams,getExcelDir());

涉及到的WordExportUtil.WordExportUtil()方法如下:

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
ini复制代码    public static String WORD_2007 = "WORD_2007";
public static String WORD_2003 = "WORD_2003";

/**
* 设置下载文件中文件的名称
* @param filename
* @param request
* @return
*/
public static String encodeFilename(String filename, HttpServletRequest request) {
String agent = request.getHeader("USER-AGENT");
try {
if ((agent != null) && (-1 != agent.indexOf("MSIE"))) {
String newFileName = URLEncoder.encode(filename, "UTF-8");
newFileName = StringUtils.replace(newFileName, "+", "%20");
if (newFileName.length() > 150) {
newFileName = new String(filename.getBytes("GB2312"), "ISO8859-1");
newFileName = StringUtils.replace(newFileName, " ", "%20");
}
return newFileName;
}
if ((agent != null) && (-1 != agent.indexOf("Mozilla")))
return MimeUtility.encodeText(filename, "UTF-8", "B");
return URLEncoder.encode(filename, "UTF-8");
} catch (Exception ex) {
return filename;
}
}
/**
* @param request HttpServletRequest
* @param response HttpServletResponse
* @param version Word_2003/Word_2007
* @param docFileName 生成的doc临时文件名
* @param templateFile freemark模板文件名
* @param beanParams 入参数据: Map<String, Object>类型
*/
public static void writeResponse(HttpServletRequest request, HttpServletResponse response, String version, String docFileName, String templateFile, Map<String, Object> beanParams,String getExcelDir) {
writeResponse(request, response, version, "temp", docFileName, "template", templateFile, beanParams, getExcelDir);
}

/**
* @param request HttpServletRequest
* @param response HttpServletResponse
* @param version Word_2003/Word_2007
* @param docTempDir 生成的doc临时文件目录
* @param docFileName 生成的doc临时文件名
* @param templateDir 存放freemark模板的目录
* @param templateFile freemark模板文件名
* @param beanParams 入参数据: Map<String, Object>类型
*/
public static void writeResponse( HttpServletRequest request, HttpServletResponse response, String version, String docTempDir, String docFileName, String templateDir, String templateFile, Map<String, Object> beanParams,String getExcelDir) {
Configuration config = new Configuration();
ServletContext sc = request.getSession().getServletContext();
InputStream is = null;
File previewFile = null;
try {
config.setDirectoryForTemplateLoading(new File(getExcelDir));
config.setObjectWrapper(new DefaultObjectWrapper());
Template template = config.getTemplate(templateFile, "UTF-8");
String date2Str = DateSearchUtils.date2Str(new Date());
String docFileName1 = date2Str+"_logs";
if (WORD_2007.equals(version)) {
docFileName1 = encodeFilename(docFileName1 + ".docx", request);
} else {
docFileName1 = encodeFilename(docFileName1 + ".doc", request);
}
String docName = "D:\\join\\excel"+docFileName;
FileOutputStream fos = new FileOutputStream(docName);
Writer out = new OutputStreamWriter(fos, "UTF-8");
template.process(beanParams, out);
out.flush();
out.close();
previewFile = new File(docName);
is = new FileInputStream(previewFile);
response.reset();
if (WORD_2007.equals(version)) {
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=UTF-8");
}else{
response.setContentType("application/vnd.ms-word;charset=UTF-8");
}
response.addHeader("Content-Disposition", "attachment;filename="+docFileName1);
byte[] b = new byte[1024];
int len;
while ((len=is.read(b)) >0) {
response.getOutputStream().write(b,0,len);
}
response.getOutputStream().flush();
response.getOutputStream().close();

} catch (Exception e) {
e.printStackTrace();
}finally{
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(previewFile!=null){
previewFile.delete();
}
}
}
3、导出pdf
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
ini复制代码// 新建文件
String date2Str = DateSearchUtils.date2Str(new Date());
String fileName = date2Str + "_logs.pdf";
Document document = new Document();
File file = new File(fileName);
PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);// 中文字体
com.itextpdf.text.Font FontChinese18 = new com.itextpdf.text.Font(bfChinese, 18, com.itextpdf.text.Font.BOLD);
//设置名字
Paragraph pg_bt = new Paragraph("日志列表", FontChinese18);
pg_bt.setAlignment(Element.ALIGN_CENTER);
document.add(pg_bt);
// Font fontChineseBold = new Font(bfChinese, 14, Font.BOLD);//内容字体特殊加粗
Font titleChinese = new Font(bfChinese, 18, Font.BOLD);// 标题字体
// Font noteChinese = new Font(bfChinese, 12, Font.BOLD);//设置内容加粗的区
Font contenttitleChinese = new Font(bfChinese, 12, Font.BOLD);// 内容小标题字体
//这里给logViews 集合赋值
List<LogView> logViews = new ArrayList<>();
// 每行加空白
pg_bt = new Paragraph(" ", titleChinese);
pg_bt.setAlignment(Element.ALIGN_LEFT);
document.add(pg_bt);
SimpleDateFormat start = new SimpleDateFormat("HH:mm");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
PdfPTable table5 = new PdfPTable(2);
int width5[] = {80, 20};
table5.setWidths(width5);
PdfPCell cell51 = new PdfPCell(new Paragraph("日志打印时间 : " + sdf.format(new Date()), contenttitleChinese));
PdfPCell cell52 = new PdfPCell(new Paragraph("", contenttitleChinese));
cell51.setBorder(0);
cell52.setBorder(0);
table5.addCell(cell51);
table5.addCell(cell52);
table5.setHorizontalAlignment(Element.ALIGN_LEFT);
document.add(table5);
//加入空行
Paragraph blankRow51 = new Paragraph(18f, " ", contenttitleChinese);
document.add(blankRow51);
int col = 6;
PdfPTable table = new PdfPTable(col);
BaseColor bc = new BaseColor(102, 204, 255);
//设置表格占PDF文档100%宽度
table.setWidthPercentage(100);
table.setWidths(new int[]{8, 12, 15, 20, 15, 30});
BaseColor lightGrey01 = new BaseColor(0xCC, 0xCC, 0xCC);
PdfPCell cell0 = toPdfPCell("序号", Element.ALIGN_CENTER);
cell0.setBackgroundColor(lightGrey01);
table.addCell(cell0);
PdfPCell cell1 = toPdfPCell("用户名", Element.ALIGN_CENTER);
cell1.setBackgroundColor(lightGrey01);
table.addCell(cell1);
PdfPCell cell2 = toPdfPCell("访问ip", Element.ALIGN_CENTER);
cell2.setBackgroundColor(lightGrey01);
table.addCell(cell2);
PdfPCell cell3 = toPdfPCell("创建时间", Element.ALIGN_CENTER);
cell3.setBackgroundColor(lightGrey01);
table.addCell(cell3);
PdfPCell cell4 = toPdfPCell("操作", Element.ALIGN_CENTER);
cell4.setBackgroundColor(lightGrey01);
table.addCell(cell4);
PdfPCell cell5 = toPdfPCell("日志内容", Element.ALIGN_CENTER);
cell5.setBackgroundColor(lightGrey01);
table.addCell(cell5);
if (logViews.size() > 0) {
for (LogView view : logViews) {
table.addCell(toPdfPCell(view.getXh(), Element.ALIGN_CENTER));
table.addCell(toPdfPCell(view.getUsername(), Element.ALIGN_CENTER));
table.addCell(toPdfPCell(view.getAccessEndIp(), Element.ALIGN_CENTER));
table.addCell(toPdfPCell(DateSearchUtils.longToDate(view.getCreateTime()), Element.ALIGN_CENTER));
table.addCell(toPdfPCell(view.getOperation(), Element.ALIGN_CENTER));
table.addCell(toPdfPCell(view.getContent(), Element.ALIGN_CENTER));
}
}
document.add(table);
document.close();
download(file, res);
} catch (DocumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

涉及到的toPdfPCell()方法如下:

1
2
3
4
5
6
7
8
java复制代码public PdfPCell toPdfPCell(String name, int align) throws DocumentException, IOException {
BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);// 中文字体
Font fontChinese = new Font(bfChinese, 12, Font.NORMAL);// 内容字体
PdfPCell cell = new PdfPCell(new Paragraph(name, fontChinese));
cell.setHorizontalAlignment(align);// 设置内容水平居中显示
cell.setVerticalAlignment(Element.ALIGN_MIDDLE); // 设置垂直居中
return cell;
}
4、导出excel

参考地址:easypoi.mydoc.io/

5、所需的maven依赖为:
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
xml复制代码<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>com.yulintu</groupId>
<artifactId>common-excel</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>5.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>RELEASE</version>
</dependency>

效果图如下:

6、导出的xml格式的文件内容如下:

在这里插入图片描述

7、导出的word格式的文件内容如下:

在这里插入图片描述

8、导出的pdf格式的文件内容如下:

在这里插入图片描述

9、导出的excel格式的文件内容如下:

在这里插入图片描述

写在最后:可能在进行这个功能实现时,我所采用的方法并不是最合理的,大家一起学习嘛

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mybatis的$和#详解分析

发表于 2021-07-16

Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘itlezhi’ in ‘where clause’

好久之前的mybatis源码阅读还有一点没有完成,今天准备来收个尾,结果一执行测试代码就报错了。

#01报错分析

报错代码与异常信息如下图:

第一瞬间我竟然不知道错在哪里了,感觉就是很正常的一句sql啊,我直接把错误信息“Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘itlezhi’ in ‘where clause’”百度一下,答案是把$换成#就好了。就马上试了下竟然成功了。

有的答案还说了sql中定义的类型是int型的可以不用加引号,但是如果是字符串类型的,必须加引号。从这句话中我总算知道我的sql错哪里了。

Mybatis的$把name参数映射到sql语句中竟然没有加引号,以至于sql报错。

#02联想测试

同时就想到之前经常听到的面试题:$与#的区别、mybatis如何防止sql注入等。如果我把参数itlezhi换成’itlezhi’ or 1=1就可以实现sql注入了。

经过测试确实可以,只不过这里我调用的selectOne方法,所以直接报错了,改成selectList方法则成功返回了所有的数据。

如果把$缓存#则一条数据都没有查出来,实现了防止sql注入的功能。那么在源码中是如何处理呢?

#03源码跟踪

从之前源码知道sql来源于,把$换成#之后直接跟进到创建boundSql的地方,可以看到在rootSqlNode的apply之后sql如下图:

通过debug的sqlBuilder可以看出来SqlNode的apply只处理了$,#{name}还没有进行处理。

那么#是在哪里处理的呢?就在紧接着的SqlSourceBuilder中。在上图中apply之后马上初始化了一个SqlSourceBuilder对象并执行了parse方法。

SqlSourceBuilder的parse方法源码如下图:

又见到GenericTokenParser这个类了。简单介绍下这个类,这个类可以当成一个占位符解析类,接受3个参数分别表示占位符起始、结束、处理解析结果类。至于处理逻辑在TextSqlNode的apply已经详细介绍过了。这次如上图就会解析#{}。处理逻辑也在上图源码中,解析出来的处理逻辑是把#{}的内容保存下来,并用?替换#{}。

我们可以看到处理完成后的结果如下图:

Sql保存的处理后的sql语句,这个sql是需要预处理才能执行的,parameterMappings中记录着需要预处理的参数。

然后继续跟进源码直到执行sql之前,生成Statement的源码如下图:

生成的boundSql在创建StatementHandler 时才使用。再通过生成的StatementHandler 创建出来Statement。

从上图可以看到Statement的关键属性。ClientPreparedStatement重写了toString方法,展示了预处理后的sql,实际上的sql是“select id,name,age,ids from member where id = 5 and name = ?”。

columnNames、columnMap分别表示预处理需要的参数和对应的值。columnNames集合中第一个元素值是1和columnMap中1对应的值相当于下面这句话:

preparedStatement.setString(1, “‘itlezhi’ or 1=1”);

所以最终的执行结果sql:“select id,name,age,ids from member where id = 5 and name = ‘’’itlezhi’’ or 1=1’”。这样的sql一般都查不出来数据了,实现了防止sql注入。

IT乐知——程序员的指路名师。这个世界几乎不合所有人的梦想。只是有些人可以学会遗忘,有些人却可以坚持。一切的成就不过是源于一个梦想和毫无根据的自信,只要能够坚持“探索、尝试”和保持“坚毅、自尊、仁慈”,就必定能够创出自己的天地。对于程序员来说,学习永无止境,成长永不停步。持续汲取知识,充盈身心,方可于IT行业驰骋。而IT乐知就可以帮程序员解决成长道路上的任何难题。

PROMPT温馨提示

Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!

更多Java面试题,小程序:IT面试题练习…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试中的数据库索引的那些灵魂拷问(2)

发表于 2021-07-16

继上一篇分析的索引问题后,我们知道了索引的作用和结构,这一篇文章将继续围绕索引的一些类型知识点进行分析

索引分为聚簇索引和非聚簇索引,那么这两者之间是有什么区别吗

索引示意图.png

上篇的B+树结构我们知道,叶子节点存放着一行的数据,这个是区分两者的重要特征,如图所示,左边的图是InnoDB的主键索引和二级索引,右边为MyISAM的索引,我们可知,对于MyISAM而言,叶结点包含索引字段值及指向数据页数据行的逻辑指针,也就是说表的数据和索引是分隔开的,主键索引和二级索引并无差
别,查找数据的时候需要根据索引存放的数据行指针进一步查找。对于InnoDB而言,索引有主键索引和普通索引的分别,聚簇索引根据主键来构建,叶子节点存在这一行的数据,普通索引存放这主键的以及构成索引的字段,也就是说在select查找的时候,如果是用的主键索引,则无需回表查询,可以直接返回所查的数据,如果使用的是普通索引查询,如果查询的字段恰好为字段本身,也无需回表查询,如果查询的字段不在构成索引字段本身内,则需要根据主键回表查询其他的字段。由此可见,对于mysql来说 (select *)本身也是会影响查询效率,最好是只查需要的字段,覆盖索引,避免回表

下面分析一下常见的索引类型

  • 主键和唯一索引
    对于一个表来说,唯一索引可以有多个,但是只有一个主键。主键就是唯一索引,但是唯一索引不一定是主键,唯一索引可以为空,但是空值只能有一个,主键不能为空,对于多列组成的唯一索引,需要保证具有唯一性
  • 联合索引

联合索引在日常数据库使用的时候也是经常被使用到的,我们以(a,b,c) 为例子构建一个联合索引,实际上构建了(a),(a,b),(a,b,c)三个索引,联合索引 有“最左前缀”原则,遇到范围查询(>、<、between、like)就会停止匹配。接下来就分析一下常见的几个问题。

例如下表:建立(a,b,c)的索引

1
2
3
4
5
6
7
8
9
less复制代码CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`a` varchar(15) NOT NULL,
`b` varchar(15) NOT NULL,
`c` varchar(15) NOT NULL,
`d` varchar(15) NOT NULL,
PRIMARY KEY (`id`),
KEY `index_a_b_c` (`a`,`b`,`c`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
  1. 如果颠倒a、b、c顺序是否会使用到索引

EXPLAIN SELECT * FROM test WHERE c='test' AND a='test' AND b='test'

image.png
可见MySQL会自动对查询的列进行调整,不影响索引的使用

  1. 如果放弃最左原则,也就是不查a,直接查询b、c
    EXPLAIN SELECT * FROM test WHERE c='test' AND b='test'
    image.png
    说明不使用第一个查询列的时候是无法触发索引的

根据上面的结果我们现在反推一些查询语句的时候应该如何建立索引

  1. SELECT * FROM test WHERE a > 1 and b = 2

如果上面这个查询语句建立的是(a,b)的时候是只能使用到a索引,如果是建立(b,a)的索引,MySQL的优化器会帮我们调整,从而使用到索引

  1. EXPLAIN SELECT * FROM test WHERE a IN (1,7,9) and b > 1

还是对(a,b)建立索引,因为IN在这里可以视为等值引用,不会中止索引匹配,所以还是(a,b)

  1. SELECT * FROM test WHERE a > 1 and b = 2 and c > 3;

对于上面的语句而言,建立(b,c)还是(b,a)都差不多,因为无法全部都涉及到,所以需要看具体使用情况

  1. 随便分析一个索引和排序的问题吧
1
2
vbnet复制代码EXPLAIN SELECT * FROM test WHERE  a =1 ORDER BY b;
EXPLAIN SELECT * FROM test WHERE a >1 ORDER BY b;

上面建立了(a,b)的索引,当a = 1的时候,b相对有序,可以避免再次排序,而第二个语句是一个范围查询,这个范围内b值是无序的,没有必要对(a,b)建立索引

image.png

image.png

共同进步,学习分享

欢迎大家关注我的公众号【写代码的小杨】,相关文章、学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!持续更新!!! 点关注,不迷路,小杨带你上高速

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

个人博客开发之blog-api项目统一结果集api封装 前言

发表于 2021-07-16

前言

由于返回json api 格式接口,所以我们需要通过java bean封装一个统一数据返回格式,便于和前端约定交互,

状态码枚举ResultCode

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
java复制代码package cn.soboys.core.ret;


import lombok.Getter;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 15:35
* 响应码枚举,对应HTTP状态码
*/
@Getter
public enum ResultCode {

SUCCESS(200, "成功"),//成功
//FAIL(400, "失败"),//失败
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "认证失败"),//未认证
NOT_FOUND(404, "接口不存在"),//接口不存在
INTERNAL_SERVER_ERROR(500, "系统繁忙"),//服务器内部错误
METHOD_NOT_ALLOWED(405,"方法不被允许"),

/*参数错误:1001-1999*/
PARAMS_IS_INVALID(1001, "参数无效"),
PARAMS_IS_BLANK(1002, "参数为空");
/*用户错误2001-2999*/


private Integer code;
private String message;

ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
}

结果体Result

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
java复制代码package cn.soboys.core.ret;

import lombok.Data;

import java.io.Serializable;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 15:47
* 统一API响应结果格式封装
*/
@Data
public class Result<T> implements Serializable {

private static final long serialVersionUID = 6308315887056661996L;
private Integer code;
private String message;
private T data;


public Result setResult(ResultCode resultCode) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
return this;
}

public Result setResult(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
this.setData(data);
return this;
}


}

响应结果方法工具类

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
java复制代码package cn.soboys.core.ret;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 16:30
* 响应结果返回封装
*/
public class ResultResponse {
private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";

// 只返回状态
public static Result success() {
return new Result()
.setResult(ResultCode.SUCCESS);
}

// 成功返回数据
public static Result success(Object data) {
return new Result()
.setResult(ResultCode.SUCCESS, data);


}

// 失败
public static Result failure(ResultCode resultCode) {
return new Result()
.setResult(resultCode);
}

// 失败
public static Result failure(ResultCode resultCode, Object data) {
return new Result()
.setResult(resultCode, data);
}



}

自定义解析controller拦截

注解@ResponseResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package cn.soboys.core.ret;

import java.lang.annotation.*;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 16:43
* 统一包装接口返回的值 Result
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseResult {
}

请求拦截ResponseResultInterceptor

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
java复制代码package cn.soboys.core.ret;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 17:10
* 请求拦截
*/
public class ResponseResultInterceptor implements HandlerInterceptor {

//标记名称
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//请求方法
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
//判断是否在对象上加了注解
if (clazz.isAnnotationPresent(ResponseResult.class)) {
//设置此请求返回体需要包装,往下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
//方法体上是否有注解
} else if (method.isAnnotationPresent(ResponseResult.class)) {
//设置此请求返回体需要包装,往下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
}
}
return true;
}
}

请求全局解析ResponseResultHandler

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
java复制代码package cn.soboys.core.ret;

import cn.soboys.core.utils.HttpContextUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 16:47
* 全局统一响应返回体处理
*/
@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

/**
* @param methodParameter
* @param aClass
* @return 此处如果返回false , 则不执行当前Advice的业务
* 是否请求包含了包装注解 标记,没有直接返回不需要重写返回体,
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
HttpServletRequest request = HttpContextUtil.getRequest();
//判断请求是否有包装标志
ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
return responseResultAnn == null ? false : true;
}

/**
* @param body
* @param methodParameter
* @param mediaType
* @param aClass
* @param serverHttpRequest
* @param serverHttpResponse
* @return 处理响应的具体业务方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (body instanceof Result) {
return body;
} else if (body instanceof String) {
return JSON.toJSONString(ResultResponse.success(body));
} else {
return ResultResponse.success(body);
}
}
}

具体详细内容请参考我这篇章 Spring Boot 无侵入式 实现RESTful API接口统一JSON格式返回

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如果面试官问你 JVM,额外回答逃逸分析技术会让你加分!

发表于 2021-07-16

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战」

我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复【资料】,即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板。

引言

我在面试别人的过程中,JVM 内存模型我几乎必问,虽然有人说问这些就是面试造航母,工作拧螺丝。如果你想当一名 CRUD 码农,你可以选择不用了解这些。

在 JVM 内存模型的问答中,有些人能说出对象是在堆上分配的。但当我问对象一定是在堆上存储的嘛时,大部分人都回答是,或者犹豫了。

其实能回答出对象是在堆上分配存储已算正确了。但随着 JIT 即时编译器的发展和逃逸分析技术的逐渐成熟,所有对象都分配到堆上也逐渐变得不那么绝对了。栈上分配,标量替换,锁消除等优化技术会发生一些微妙的变化。

我们知道,我们编写的 Java 源代码通过 javac 编译成字节码文件,然后类加载器将字节码文件加载到内存中,JVM 逐行读取解释字节码翻译成对应的机器指令执行。很明显,解释执行比那些可直接执行的二进制程序(例如 C 语言程序)慢得多。

所以为了提高效率,引入了 JIT (即时编译器)优化技术。Java 程序还是会通过解释器进行解释执行,但是如果某个方法或者代码块运行比较频繁的时候,JVM 认为这是热点代码,然后将热点代码翻译成本地机器指令,并且进行优化,缓存起来,下次再运行此段代码的时候直接运行而不用再解释。

JIT 中一个很重要的优化技术就是逃逸分析(Escape Analysis)。

逃逸分析

逃逸分析,其实就是分析一个对象是否会逃逸出方法,分析对象的动态作用域。如果一个对象在一个方法内定义,并且有可能被方法外部引用使用,那认为它逃逸了。

例如以下的 person 对象就发生了逃逸,即有可能会被方法外部引用。

1
2
3
4
java复制代码public Person personEscape() {
Person person = new Person();
return person;
}

所以为什么要进行逃逸分析,其实最终目的就是为程序做优化,提高运行性能。有如下优化技术点:

  • 栈上分配
  • 标量替换
  • 锁消除

JDK1.7 开始,逃逸分析默认是开启的,可以通过以下参数进行启停。

1
2
3
4
bash复制代码# 开启
-XX:+DoEscapeAnalysis
# 关闭
-XX:-DoEscapeAnalysis

栈上分配

如果分析一个对象没有逃逸出方法的时候,就有可能被分配到栈上。这样就不需要在堆中进行 GC 回收,提高了性能。

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
java复制代码package com.chenpi;

/**
* @Description
* @Author 陈皮
* @Date 2021/7/14
* @Version 1.0
*/
public class EscapeAnalysisTest {

public static void main(String[] args) {

long startTime = System.currentTimeMillis();

for (int i = 0; i < 10000000; i++) {
stackAlloc();
}

System.out.println((System.currentTimeMillis() - startTime) + "ms");
}

public static void stackAlloc() {
Person person = new Person("陈皮", 18);
}

}

class Person {

private String name;
private long age;

public Person(String name, long age) {
this.name = name;
this.age = age;
}
}

虚拟机参数设置开启逃逸分析,并且打印 GC 日志。

1
bash复制代码-Xms200m -Xmx200m -XX:+DoEscapeAnalysis -XX:+PrintGC

运行程序结果如下,消耗只需要 10 ms,并且没有 GC 。

1
bash复制代码10ms

关闭逃逸分析,并且打印 GC 日志。

1
bash复制代码-Xms200m -Xmx200m -XX:-DoEscapeAnalysis -XX:+PrintGC

运行程序结果如下,消耗时间增加了10多倍,并且伴随着多次的 GC 。

1
2
3
4
5
bash复制代码[GC (Allocation Failure)  51712K->784K(196608K), 0.0050396 secs]
[GC (Allocation Failure) 52496K->784K(196608K), 0.0030730 secs]
[GC (Allocation Failure) 52496K->752K(196608K), 0.0013993 secs]
[GC (Allocation Failure) 52464K->720K(196608K), 0.0018371 secs]
176ms

标量替换

  • 标量:不可再分解成更小数据的类型,例如基本数据类型就是标量。
  • 聚合量:可以再分解成其他聚合量或者标量的数据类型,例如对象引用类型。

如果一个对象不会发生逃逸,那么 JIT 可以优化把这个对象分解成若干个标量来代替。这就是标量替换。

1
2
3
4
5
java复制代码public void scalarReplace() {
Coordinates coordinates = new Coordinates(105.10, 80.22);
System.out.println(coordinates.longitude);
System.out.println(coordinates.latitude);
}

以上演示程序,coordinates 对象不会发生逃逸,所以 JIT 编译器可以使用标量替换进行优化。最终被优化成如下程序。

1
2
3
4
java复制代码public void scalarReplace() {
System.out.println(105.10);
System.out.println(80.22);
}

其实在现有的虚拟机中,并没有真正的实现栈上分配,其实是通过标量替换来实现的。

锁消除

为什么要消除锁呢?因为加锁会降低性能,那如何不用加锁是最好的。如果分析出加锁的对象不会发生逃逸,即只能被一个线程访问,JIT 是可以优化消除这个锁的。也称为同步省略。

1
2
3
4
5
java复制代码public void lockRemove() {
synchronized (new Object()) {
System.out.println("我是陈皮!");
}
}

以上演示程序,Object 对象不会发生逃逸,所以也只能当前线程访问到,所以 JIT 编译器可以进行优化锁消除。最终被优化成如下程序。

1
2
3
java复制代码public void lockRemove() {
System.out.println("我是陈皮!");
}

总结

但随着 JIT 即时编译器的发展和逃逸分析技术的逐渐成熟,所有对象都分配到堆上也逐渐变得不那么绝对了。通过逃逸分析技术,对象可能被分配到栈上,能减少 GC,提高程序性能。

但是开启逃逸分析的程序的性能一定高于没有开启逃逸分析的性能吗?其实不一定。逃逸分析技术其实也是很复杂的,所以也是一个会耗时的过程,如果经过逃逸分析之后,发现所有对象都逃逸了,就不能做优化处理,那这个逃逸分析的过程就消耗了时间,还不起优化作用,得不偿失。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【熬夜肝了】好多天-动态规划十连-超细腻解析

发表于 2021-07-15

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

【刷题打卡】周末肝了几道动态规划题,写一下我的心得笔记,故事开头,文章循序渐进,如果看官出现头疼不适,望休息,但是别放弃一定要看完!号外:每道题都有单元测试,看官们直接copy就可以debug了。

本文已更新到github

No.1

最简单的动态规划题目

侄女5岁现在开始学习加减法了,每次做数学都离不开她的小手指,超过5的就开始数另一个手的手指,真是汗颜啊

有一天,我问她

“1+1+1+1+1等于多少?”

“搬起小拇指,就开始数了,5!”

“那么再加一个1呢?”

“当然是6了” -脱口而出

“这次你怎么算这么快的?”

“刚刚不是5吗,加1就是6了啊”

“所以你不需要重新计算,因为你记得之前的答案是5!动态规划就是说:记住之前的东西可以节省时间”

玩归玩,闹归闹,别拿dp开玩笑!\color{red}{玩归玩,闹归闹,别拿dp开玩笑!}玩归玩,闹归闹,别拿dp开玩笑!

来瞅一眼科普中国科学百科的词条解释

动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。

看不完的童鞋跳过,咱整点简单点的

其实刚刚这道题应该算是最简单的动态规划题目了。

我们可以看出这道题有什么特点呢?

我们知道之所以最后一步加1会计算的那么快,大部分要归功于之前计算过答案是5,如果我们把问题归纳为一个子问题,我想要计算每一步的答案,就可以列出一个方程:f(x) = f(x -1) + 1, 大家别对f(x)发怵,就把它当做一个简单的方法。

其中,f(x)为第几步的值,设定一个初始值,x > 0, 则第一步

f(1) = 1;

f(2) = f(1) + 1;

…

f(6) = f(5) + 1

在程序的世界里,用什么东东来存一个可以记录之前结果值的数据结构呢?

显而易见:数组呗。直接利用下标来存,巴适, 这就是动态规划里的动态,规划就是限定一些边界和初始化。

看到这里,老铁,你就会动态规划了,来看第二题。

No.2

leecode 322: 你有三种硬币,2元、5元、7元,每种硬币足够多,买一本书需要27元,用最少的硬币组合

img

怎么感觉像是回到了小学应用题?

–简单分析一下: 最小硬币组合 -> 尽量用大的硬币

这傻不拉几的题谁出的,这么简单

7+7+7=21,21+2+2+2=27, 6枚硬币

卧槽

7+5+5+5+5=27, 5枚硬币

我们可以很暴力的想一想,从大往小的算,可以加起来不超过27,比如

7+7+7+7 > 27 (排除)

7+7+7+5 或者 7 +7 +7+2+2+2 6枚

….

穷举太慢了,还会涉及到很多的重复计算,如果我想要27以内的任何值最小的硬币组合呢,想想都头大了吧。

既然计算机可以通过内存保存之前的内容,又快,很明显,我们开一个数组来保存之前的状态。

重点预警

1.1. 动态规划组成部分1:确定状态

简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似数学题中x, y, z代表什么

解动态规划需要两个意识:

  • 最后一步
  • 子问题

最后一步

刚刚第一题不是说了嘛,最后一步的计算结果是5。对于这道题,虽然我们不知道最优策略是什么,但是最优策略肯定是K枚硬币,a1, a2, ….ak面值加起来是27

所以一定有一枚最后的硬币:ak.

除掉这么硬币,前面硬币的面值加起来是27-ak

关键点1:

  • 我们不关心前面的k-1枚硬币是怎么拼出27-ak的(可能有一种拼法,也可能有100种拼法),而且我们现在甚至还不知道ak和K, 但是我们确定前面的硬币拼出了27-ak

关键点2:

  • 因为是最优策略, 所以拼出27-ak的硬币数一定要最少,否则这就不是最优策略了

img

子问题

  • 所以我们就要求:最少用多少枚硬币可以拼出27-ak
  • 原问题是最少用多少枚硬币拼出27
  • 我们将原问题转化了一个子问题,而且规模更小:27-ak
  • 为了简化定义, 我们设状态f(x)=最少用多少枚硬币拼出x

等等,我们还不知道最后的那枚硬币ak是多少

  1. 很明显,最后的那枚硬币只能是2,5或者7
  2. 如果ak是2, f(27)应该是f(27-2)+1(1代表最后一枚硬币2)
  3. 如果ak是5, f(27)应该是f(27-5)+1(1代表最后一枚硬币5)
  4. 如果ak是7, f(27)应该是f(27-7)+1(1代表最后一枚硬币7)

所以使用最少的硬币数=f(27) = min{f(27-2)+1, f(27-5)+1, f(27-7)+1}

1.2. 动态规划组成部分2:转移方程

设状态f(x)=最少用多少枚硬币拼出x

对于任意的x : f(X) = min{f(X-2)+1, f(X-5)+1, f(X-7)+1}

1.3. 动态规划组成部分2:初始条件和边界情况

提出问题

  1. x-2, x-5, x-7 小于0怎么办?
  2. 什么时候停下来?

1.3.1

如果不能拼出Y, 就定义f[Y] = 正无穷

例如f[-1], f[-2] = 正无穷

例如:拼不出f[1]=min{f(-1)+1, f(-3)+1, f(-6)+1}

初始条件:f[0] = 0

2.4. 动态规划组成部分2:计算顺序

计算:f[1], f[2], … f[27]

当我们计算到f[x], f[x-2], f[x-5], f[x-7] 都已经得到结果了

如图:

img

上图7只需要一个硬币。

f[x] = 最少用多少枚硬币拼出x

f[x] = ∞ 表示无法用硬币拼出x

参考代码

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
java复制代码public  static  int coinChange(int [] A, int M ) {
int[] f = new int[M+1];
int n = A.length;
f[0] = 0;
int i,j;
for (i = 1; i<=M; i++) {
f[i] = Integer.MAX_VALUE;
for (j = 0; j< n;j++) {
// 边界条件判断
if (i >= A[j] && f[i - A[j]] != Integer.MAX_VALUE) {
f[i] = Math.min(f[i - A[j]] + 1, f[i]);
// System.out.println(i + "=" +f[i]);
}
}
}
if (f[M] == Integer.MAX_VALUE) {
f[M] = -1 ;
}
return f[M];
}
@Test
public void isCoinChange() {
int xx = {1,2,3};
int b = 6;
int i = coinChange(xx, b);
Assert.assertNotNull(i);
}

😄

核心代码就4行,是不是很简单 \color{red}{核心代码就4行,是不是很简单~}核心代码就4行,是不是很简单

No.3

leecode 62 :不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

img

看了上面的解题步骤,按部就班的来

2.1. 动态规划组成部分1:确定状态

最后一步

无论机器人用何种方式到达右下角,总有最后挪动的一步:-向右或者向下

如果所示,我们设右下角的坐标为(m-1,n-1)

那么最后一步的前一步机器人的位置在(m-2,n-1)或者(m-1,n-2)

子问题

那么,如果机器人有x种方式从左上角走到(m-2,n-1), 有Y种方式从左上角走到(m-1,n-2), 则机器人有X+Y的方式走到(m-1,n-1)

问题转化为,机器人有多少种 方式从左上角走到(m-2,n-1)或者(m-1,m-2)

如果走到是(m-2,n-1)如图:

img

我们可以直接干掉最后一列

同理如果是走到(m-1,n-2)行就减少一行。

状态:

设f[i][j]为机器人有多少种方式从左上角走到(i,j)

2.2. 动态规划组成部分2:转移方程

对于任意一个格子:

f[i][j] = f[i-1][j] + f[i][j-1]

1 2 3

1代表机器人有多少种方式走到[i][j]

2代表机器人有多少种方式走到f[i-1][j]

3代表机器人有多少种方式走到f[i][j-1]

2.3. 动态规划组成部分3:初始条件和边界情况

初始条件:f[0][0]=1,因为机器人只有一个方式到左上角

边界情况:i=0或j=0,则前一步只能有一个方向过来,也就是说第0行或者第0列,每走一步只有一种情况,则f[i][j] = 1,其他区域都满足转移方程。

3.4. 动态规划组成部分4:计算顺序

按行计算,为什么按行计算呢?

对于这道题来说,按行计算在计算到f[1][1]时,f[0][1]和f[1][0]都已经计算了,同样按列计算这两坐标也计算了,不用再次计算。

  • f[0][0] = 1
  • 计算第0行:f[0][0],f[0][1],…,f[0][n-1]
  • 计算第1行:f[1][0],f[1][1],…,f[1][n-1]
  • …
  • 计算第m-1行:f[m-1][0],f[m-1][1],…,f[m-1][n-1]

时间复杂度:O(mn)

img

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public int uniquePaths(int m, int n) {
int[][] f = new int[m][n];
int i,j;
for (i = 0; i<m; i++) {
for (j = 0; j< n; j++) {
if (i == 0 || j == 0) {
f[i][j] = 1;
} else {
f[i][j] = f[i -1][j] + f[i][j-1];
}
}
}
return f[m-1][n-1];
}

@Test
public void isUniquePaths() {
int i = uniquePaths(3, 3);
Assert.assertNotNull(i);
}

能看到这里的朋友,你已经超过80%的人,可能现在你的脑袋开始有点晕了,刷题就是这样,刷几道就会头疼,休息下就好了,这玩儿意儿看得就是坚持.

总结一下

什么题可以选择动态规划来做?

1.计数

  • 有多少种方式走到右下角
  • 有多少种方法选出k个数是的和是sum

2.求最大值最小值

  • 从左上角走到右下角路径的最大数字和
  • 最长上升子序列长度

3.求存在性

  • 取石子游戏,先手是否必胜
  • 能不能选出k个数使得和是sum

好叻!接下来咱们整第四道题。

No.4

leecode 5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:输入: “babad”

输出: “bab” 注意: “aba” 也是一个有效答案。

示例 2:输入: “cbbd”

输出: “bb”

看了之前的文章,我们就四步走吧

1.1. 动态规划组成部分1:确定状态

简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似数学题中x, y, z代表什么

在这道题中,我们定义f[i][j] 表示字符串 s 的第 i 到 j 个字母组成的串是否为回文串

解动态规划需要两个意识:

  • 最后一步
  • 子问题

最后一步

我们用示例一来讲解,下图是第一步和第二步的判断过程,很明显最后一步为下标为4的字母b与前面所有元素进行比较,得出最长的回文子串。

img

子问题

img

我们可以看到如果f[i] =f[j],要判定它是一个回文串,需要判定

f[i]j] = f[i+1]f[j-1] : 从i到j是一个回文串,那么从i+1到j-1一定也是一个回文串

也就是如上图需要判定a=a

1.2. 动态规划组成部分2:转移方程

对于从i到j长度的字符串,判定它是一个回文串:

f[i]j] = f[i+1]f[j-1]

同时我们也知道,f[i+1][j-1]这是一个已知的,因为最后一步的上一步已经将结果保存,也就是f[i+1][j-1] = f[i+2]f[j-2]

1.3. 动态规划组成部分3:初始条件和边界情况

当剩余判定字母个数<3 并且 f[i] = f[j],它一定是回文串。

对于字母本身来说f[i][i],从i到i的字符串,它也是回文。

1.4. 动态规划组成部分4:计算顺序

如上图,我们用j去匹配0~i (i < j)

它的时间复杂度是On^2,由于是二维数据空间复杂度也是On^2

当然也有其他的解决办法如中心扩散和马拉车。

参考代码

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
java复制代码// 动态规划
public String longestPalindrome(String s) {
// 特判
int len = s.length();
if (len < 2) {
return s;
}

int maxLen = 1;
int begin = 0;

// dp[i][j] 表示 s[i, j] 是否是回文串
boolean[][] dp = new boolean[len][len];
char[] charArray = s.toCharArray();

for (int i = 0; i < len; i++) {
dp[i][i] = true; // 对本身来说就是回文
}
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
if (charArray[i] != charArray[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1]; // 要满足这个条件,必需先满足j - i > 3考虑边界
}
}

// 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}

@Test
public void islongestPalindrome() {
String i = longestPalindrome("babaxaxab");
Assert.assertNotNull(i);
}

准备来一道非常实用的题目👍 👍 👍

No.5

leecode 10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*‘ 的正则表达式匹配。

‘.’ 匹配任意单个字符

‘*‘ 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = “aab” p = “c * a * b “ (*号无空格)

输出:true

解释:因为 ‘*‘ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

示例 2:

输入:s = “mississippi” p = “misis*p\.”

输出:false

解释:第二个*不能匹配si

2.1. 动态规划组成部分1:确定状态

wc,你倒是说说怎么确定要用动态规划来做啊?

  • 看题目,需要逐步匹配
  • 没有时间复杂度空间复杂度限制,你可以选择>On的
  • 跟前面题很类似,这里需要考虑* 和 .的情况。
  • 尝试根据步骤写出转移方程

在这道题中,我们定义f[i][j] 表示字符串 s 的前 i 个字符与 字符串p的前 j 个字符是否匹配

解动态规划需要两个意识:

  • 最后一步
  • 子问题

最后一步

我们用示例来讲解 :s = “aaab” p = “aa*b”

根据题目意思,我们知道s要被全匹配,我们直接用s去匹配p字符串的每一个字符,这样我们的最后一步就是用s字符串中的b字符去匹配p字符串的每一个字符,匹配最后一个字符为b是否相等。

子问题

有几种情况需要讨论,也就是用s去匹配p最后一个匹配的字符j

  • 如果都是字符,只需要判断:f[i][j] = f[i-1][j-1]
  • 如果是“.” ,说明可以匹配s的最后任意一个字符,也只需:

f[i][j] = f[i-1][j-1]

  • 如果是“*”,分两种情况一种是匹配零个字符,一种是匹配1个或多个字符

如果是匹配零个字符:f[i][j] = f[i][j-2] ,因为如果j是*,我们就可以对p的j-1匹配任意次数,当然零次就是j-2了。

​ 如果是匹配1个或多个字符:f[i][j] = f[i-1][j],匹配 s 末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配,简单点说,零个我们判定了,如果是一个我们扔掉一个字符,如果能匹配则保存,如果匹配两个,我们是知道匹配s的上一个字符的结果的,直接匹配就行,同样匹配多个也是如此。(匹配多个是根据前一个的匹配结果得出来的)

2.2. 动态规划组成部分2:转移方程

在子问题中我们分析的几种情况就是转移方程

2.3. 动态规划组成部分3:初始条件和边界情况

因为要涉及到i-1和j-1,注意边界

boolean[][] f = new boolean[m + 1][n + 1];

f[0][0]=true

2.4. 动态规划组成部分4:计算顺序

用s去匹配p字符串的每一个字符

它的时间复杂度是Onm,由于是二维数据空间复杂度是Onm

参考代码

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
java复制代码public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();

boolean[][] f = new boolean[m + 1][n + 1];
f[0][0] = true;
for (int i = 0; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p.charAt(j - 1) == '*') {
f[i][j] = f[i][j - 2];
if (matches(s, p, i, j - 1)) {
f[i][j] = f[i][j] || f[i - 1][j];
}
} else {
if (matches(s, p, i, j)) {
f[i][j] = f[i - 1][j - 1];
}
}
}
}
return f[m][n];
}

public boolean matches(String s, String p, int i, int j) {
if (i == 0) {
return false;
}
if (p.charAt(j - 1) == '.') {
return true;
}
return s.charAt(i - 1) == p.charAt(j - 1);
}
@Test
public void isisMatch() {
boolean i = isMatch("aa", "a*");
Assert.assertNotNull(i);
}

No.6

继续干,继续干❤️❤️❤️❤️

leecode 32. 最长有效括号

给你一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

输入:s = “(()”

输出:2

解释:最长有效括号子串是 “()”

示例 2:

输入:s = “)()())”

输出:4

解释:最长有效括号子串是 “()()”

示例 3:

输入:s = “”

输出:0

提示:

0 <= s.length <= 3 * 104

s[i] 为 ‘(‘ 或 ‘)’

看了之前的文章,我们就四步走吧

1.1. 动态规划组成部分1:确定状态

简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似数学题中x, y, z代表什么

wc,你倒是说说怎么确定要用动态规划来做啊?

  • 看题目,需要逐步验证最长长度
  • 没有时间复杂度空间复杂度限制,你可以选择>=On的
  • 跟前面题很类似,这里需要考虑生成括号的情况。
  • 尝试根据步骤写出转移方程

在这道题中,我们定义d[i]表示以下标 ii字符结尾的最长有效括号的长度

解动态规划需要两个意识:

  • 最后一步
  • 子问题

最后一步

我们用下图来讲解,i作为最后一个括号判断,我们只对为左括号做判断,左括号分两种情况,具体看子问题拆分。
lecoode 32.jpg

子问题

第一种情况为: …()

因为括号前面可能还有有效的括号,之前我们定义了d[i] 表示下标i字符结尾的最长有效括号的长度,所以可以推导出:

d[i] = d[i-2] + 2

第二种情况为: …))

321.jpg

如图,下标为2:i - d[i-1] -1

提出一个疑问:在下标2和5之前可能存在多个有效括号,其实都是d[i-1],因为我们定义的:d[i-1] 表示下标i-1字符结尾的最长有效括号的长度

322.jpg

最长有效括号的长度 :

d[i] = x + y

这里x = d[i - d[i-1] -2]

这里y = d[i-1] + 2

因此 :d[i] = d[i - d[i-1] -2] + d[i-1] + 2

1.2. 动态规划组成部分2:转移方程

d[i] 表示下标i字符结尾的最长有效括号的长度

d[i] = d[i - d[i-1] -2] + d[i-1] + 2

1.3. 动态规划组成部分3:初始条件和边界情况

跟之前有些题一样,我们需要判断第i字符去判断第i-1个字符,所以i从1开始遍历,判断数组i-2需要考虑越界。

① …() : i >=2

② …)) : i - d[i-1] > 0 对应 :())

1.4. 动态规划组成部分4:计算顺序

从左往右

它的时间复杂度是On,空间复杂度也是On

当然也有其他的解决办法如栈

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = Math.max(maxans, dp[i]);
}
}
return maxans;
}

@Test
public void isLongestValidParentheses() {
String s = "()(())";
longestValidParentheses(s);
}

No.7

这道题相对于就简单一些啦—来自秃顶的工程师👥👥👥👥👥

leecode 42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

421.jpg

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]

输出:6

解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

输入:height = [4,2,0,3,2,5]

输出:9

提示:

n == height.length

0 <= n <= 3 * 104

0 <= height[i] <= 105

看了之前的文章,我们就四步走吧

这道题相对而言,提升了难度,常规的题,我们的计算顺序一般是从左到右,或者从右到左,亦或是从上到下。

1.1. 动态规划组成部分1:确定状态

简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似数学题中x, y, z代表什么

wc,你倒是说说怎么确定要用动态规划来做啊?

  • 看题目,需要逐步验证最长长度
  • 没有时间复杂度空间复杂度限制,你可以选择>=On的
  • 跟前面题很类似,这里需要考虑每一步最高的高度取低位高度,因为可能形成低洼
  • 尝试根据步骤写出转移方程

在这道题中,我们定义d[i]表示以下标 i字符结尾的最多的有效雨水

解动态规划需要两个意识:

  • 最后一步
  • 子问题

最后一步

我们可以利用填满法来降低复杂度。去掉低洼的情况。

先从左到右来看,求出每个位置的最大高度(深度)

image.png

从右往左看,求出每个位置的最大高度

423.jpg

我们在来看它们重叠之后的效果

424.jpg

这样每个位置的最小值 - 高度就是每个位置的蓄水值。

子问题

我们存储了从左到右和从右到左每个位置的最大值

从重叠的图中可以看到最后一个位置的最小值为2,减去高度,蓄水值为0.

1.2. 动态规划组成部分2:转移方程

ans += Math.min(left_max[i], right_max[i]) - height[i];

1.3. 动态规划组成部分3:初始条件和边界情况

1.4. 动态规划组成部分4:计算顺序

从左到右 + 从右到左

它的时间复杂度是On,空间复杂度也是On

当然也有其他的解决办法如栈

参考代码

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
java复制代码public int trap(int[] height) {
if (height == null || height.length == 0)
return 0;
int ans = 0;
int size = height.length;
int[] left_max = new int[size];
int[] right_max = new int[size];
left_max[0] = height[0];
for (int i = 1; i < size; i++) {
left_max[i] = Math.max(height[i], left_max[i - 1]);
}
right_max[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--) {
right_max[i] = Math.max(height[i], right_max[i + 1]);
}
for (int i = 1; i < size - 1; i++) {
ans += Math.min(left_max[i], right_max[i]) - height[i];
}
return ans;
}

@Test
public void istrap() {
int[] candidates = {2, 0, 6, 1};
int i = trap(candidates);
Assert.assertNotNull(i);
}

No.8

这道题跟第五题很类似,如果看懂了第五题,那么这题肯定是信手拈来❤️❤️❤️❤️

leecode 44. 通配符匹配

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 ‘?’ 和 ‘*‘ 的通配符匹配。

‘?’ 可以匹配任何单个字符。

‘*‘ 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。

p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

示例 1:

输入:

s = “aa”

p = “a”

输出: false

解释: “a” 无法匹配 “aa” 整个字符串。

示例 2:

输入:

s = “aa”

p = “*“

输出: true

解释: ‘*‘ 可以匹配任意字符串。

示例 3:

输入:

s = “cb”

p = “?a”

输出: false

解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。

示例 4:

输入:

s = “adceb”

p = “ab”

输出: true

解释: 第一个 ‘‘ 可以匹配空字符串, 第二个 ‘‘ 可以匹配字符串 “dce”.

示例 5:

输入:

s = “acdcb”

p = “a*c?b”

输出: false

看了之前的文章,我们就四步走吧

1.1. 动态规划组成部分1:确定状态

简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似数学题中x, y, z代表什么

wc,你倒是说说怎么确定要用动态规划来做啊?

  • 看题目,需要逐步验证最长长度
  • 没有时间复杂度空间复杂度限制,你可以选择>=On的
  • 跟前面题很类似,这里需要考虑* 和 ?的情况。
  • 尝试根据步骤写出转移方程

在这道题中,我们定义f[i][j] 表示字符串 s 的前 i 个字符与 字符串p的前 j 个字符是否匹配

解动态规划需要两个意识:

  • 最后一步
  • 子问题

最后一步

我们用示例来讲解 :s = “aaab” p = “aa*b”

根据题目意思,我们知道s和p要全匹配,我们直接用s去匹配p字符串的每一个字符,这样我们的最后一步就是用s字符串中的b字符去匹配p字符串的每一个字符,匹配最后一个字符为b是否相等。

子问题

这是 ‘ 动态规划Day two - 正则表达式匹配‘ 的子问题分析

有几种情况需要讨论,也就是用s去匹配p最后一个匹配的字符j

  • 如果都是字符,只需要判断:f[i][j] = f[i-1][j-1]
  • 如果是“.” ,说明可以匹配s的最后任意一个字符,也只需:

f[i][j] = f[i-1][j-1]

  • 如果是“*”,分两种情况一种是匹配零个字符,一种是匹配1个或多个字符

如果是匹配零个字符:f[i][j] = f[i][j-2] ,因为如果j是*,我们就可以对p的j-1匹配任意次数,当然零次就是j-2了。

如果是匹配1个或多个字符:f[i][j] = f[i-1][j],匹配 s 末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配,简单点说,零个我们判定了,如果是一个我们扔掉一个字符,如果能匹配则保存,如果匹配两个,我们是知道匹配s的上一个字符的结果的,直接匹配就行,同样匹配多个也是如此。(匹配多个是根据前一个的匹配结果得出来的)

这道题基本上是如出一辙

有几种情况需要讨论,也就是用s去匹配p最后一个匹配的字符j

  • 如果都是字符,只需要判断:f[i][j] = f[i-1][j-1]
  • 如果是“?” ,说明可以匹配s的最后任意一个字符,也只需:

f[i][j] = f[i-1][j-1]

  • 如果是“”,分两种情况,第一种是不需要使用,第二种是需要使用*

实例 :s = “abc” p = “a*“。

在对s串的a遍历p串的*时,刚好满足dp[i][j] = dp[i][j-1] 此时i=1,j=2,

dp[1][2] = dp[1][1]=true。 —不需要使用*

在对s串的b遍历p串的时,刚好满足dp[i][j] = f[i-1][j],因为是可以匹配任意一个字母的。

dp[2][2] = dp[1][2]=true。—需要使用*

在对s串的c遍历p串的时,刚好满足dp[i][j] = f[i-1][j],因为是可以匹配任意一个字母的。

dp[3][2] = dp[2][2]=true。—不需要使用*

1.2. 动态规划组成部分2:转移方程

转移方程可以详见子问题分析

1.3. 动态规划组成部分3:初始条件和边界情况

因为要涉及到i-1和j-1,注意边界

boolean[][] f = new boolean[m + 1][n + 1];

f[0][0]=true

当f[0][j]时,需要判断j是否为*,因为*可以匹配null串

1.4. 动态规划组成部分4:计算顺序

用s串的每一个字符去匹配p串的每一个字符

它的时间复杂度是Onm,空间复杂度也是Onm

参考代码

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
java复制代码public boolean isWildcardMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 1; i <= n; ++i) {
if (p.charAt(i - 1) == '*') {
dp[0][i] = true;
} else {
break;
}
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p.charAt(j - 1) == '*') {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}

@Test
public void isisWildcardMatch() {
boolean i = isWildcardMatch("123", "1*");
Assert.assertNotNull(i);
}

No.9

这道题就很简单了,放松一下~👍👍👍👍

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

63.jpg

网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:

631.jpg

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]

输出:2

解释:

3x3 网格的正中间有一个障碍物。

从左上角到右下角一共有 2 条不同的路径:

\1. 向右 -> 向右 -> 向下 -> 向下

\2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

632.jpg

输入:obstacleGrid = [[0,1],[0,0]]

输出:1

提示:

m == obstacleGrid.length

n == obstacleGrid[i].length

1 <= m, n <= 100

obstacleGrid[i][j] 为 0 或 1

本题与


我是怎么向5岁侄女解释动态规划的?


里的不同路径1,大致相同,只是增加了障碍物,我们做一下没有障碍物的判断就行了。

2.1. 动态规划组成部分1:确定状态

最后一步

无论机器人用何种方式到达右下角,总有最后挪动的一步:-向右或者向下

如果所示,我们设右下角的坐标为(m-1,n-1)

那么最后一步的前一步机器人的位置在(m-2,n-1)或者(m-1,n-2)

子问题

那么,如果机器人有x种方式从左上角走到(m-2,n-1), 有Y种方式从左上角走到(m-1,n-2), 则机器人有X+Y的方式走到(m-1,n-1)

问题转化为,机器人有多少种 方式从左上角走到(m-2,n-1)或者(m-1,m-2)

如果走到是(m-2,n-1)如图:

634.jpg

我们可以直接干掉最后一列

同理如果是走到(m-1,n-2)行就减少一行。

状态:

设f[i][j]为机器人有多少种方式从左上角走到(i,j)

2.2. 动态规划组成部分2:转移方程

对于任意一个格子:

f[i][j] = f[i-1][j] + f[i][j-1]

1 2 3

1代表机器人有多少种方式走到[i][j]

2代表机器人有多少种方式走到f[i-1][j]

3代表机器人有多少种方式走到f[i][j-1]

2.3. 动态规划组成部分3:初始条件和边界情况

初始条件:f[0][0]=1,因为机器人只有一个方式到左上角

边界情况:i=0或j=0,则前一步只能有一个方向过来,也就是说第0行或者第0列,每走一步只有一种情况,则f[i][j] = 1,其他区域都满足转移方程。

如果遇到障碍物,f[i][j] = 0。

3.4. 动态规划组成部分4:计算顺序

按行计算,为什么按行计算呢?

对于这道题来说,按行计算在计算到f[1][1]时,f[0][1]和f[1][0]都已经计算了,同样按列计算这两坐标也计算了,不用再次计算。

  • f[0][0] = 1 如果第一个是障碍物f[0][0]=0
  • 计算第0行:f[0][0],f[0][1],…,f[0][n-1]
  • 计算第1行:f[1][0],f[1][1],…,f[1][n-1]
  • …
  • 计算第m-1行:f[m-1][0],f[m-1][1],…,f[m-1][n-1]

时间复杂度:O(mn)

635.jpg

参考代码

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
java复制代码class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if (obstacleGrid == null || obstacleGrid.length == 0) {
return 0;
}

// 定义 dp 数组并初始化第 1 行和第 1 列。
int m = obstacleGrid.length, n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}

// 根据状态转移方程 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 进行递推。
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}

简单说一下滚动数组的版本,当我们知道当前位置的最多路径数时,我们去求下一个位置的路径数,只需要知道左边和上边的可以了,空间复杂度为o(m)

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码  public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int n = obstacleGrid.length, m = obstacleGrid[0].length;
int[] f = new int[m];

f[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (obstacleGrid[i][j] == 1) {
f[j] = 0;

} else
if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
f[j] += f[j - 1];
}
}
}

return f[m - 1];

}

No.10

看到这次分享的最后一题啦,能看到这里的人呀,都是人才。❤️❤️❤️❤️

来一道比较简单的题目吧!!!

leecode 64. 最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

提示:

m == grid.length

n == grid[i].length

1 <= m, n <= 200

0 <= grid[i][j] <= 100

看了之前的题解,想想就明白了,这里用动态规划解题,考虑一下几点:

  1. 边界处理
  2. 中间的路径
  3. 这里的结果会有M*N次所以每个格子的结果都会作比较,我们取较小值就可以了

dp[i][j] 表示从左上角出发到 (i,j)(i,j) 位置的最小路径和
初始条件:dp[0][0]=grid[0][0]

当 i>0 j=0时dp[i][0]=dp[i-1][0] + grid[i][0]; // grid[i][0]为最后一个格子的值
当 i=0 j>0时dp[0][j-1]=dp[0][j-1] + grid[0][j];
当 i>0 j>0时dp[i-1][j-1]=min(dp[i-1][j],dp[i][j-1]) + grid[i][j];

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int rows = grid.length, columns = grid[0].length;
int[][] dp = new int[rows][columns];
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
}

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情 juejin.cn/post/693605…

❤️❤️❤️❤️

非常感谢人才们能看到这里,如果这个文章写得还不错,觉得有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!

如果本篇博客有任何错误,请批评指教,不胜感激 !

文末福利,最近整理一份面试资料《Java面试通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:GitHub github.com/Tingyu-Note…,更多内容陆续奉上。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《Python爬虫从入门到入狱》学习札记 Python

发表于 2021-07-15

本文正在参加「Python主题月」,详情查看 活动链接

千呼万唤始出来,掘金Python主题月,喜大普奔👏👏👏,恰逢整理技能树,Python爬虫也在其中,只是优先级较靠后。

时不待我,只争朝夕,趁着活动先把它肝了,整篇Python爬虫入门的学习总结,希望对有意学习Python爬虫却不知道从何入手的朋友有所裨益。内容较多,建议点赞收藏,闲暇时再细细品味。


0x1、侃侃而谈

1. 与Python的”一帘幽梦”

接触Python几年有矣,不过一直都处于初窥门径的阶段,主业Android开发,Python只是兴趣使然,偶尔写写自动化脚本,简化工作,偶尔帮人写下爬虫,捞点小钱,仅此而已。

除了够用外,笔者没往下深刨Python的原因:

  • 要恰饭 → 没有环境支持,导致没大的Python项目经验,跳槽得从0开始,尽管Android现在好像不怎么景气,还很卷,但是那个年限在那里,混口饭吃还是可以的;
  • 时间精力有限 → 一旦做出学习某块知识的选择,就付出了暂时无法学习其他知识的机会成本。既然下一份工作还是Android,肯定是优先巩固Android相关的,会Python是加分,但加不了多少分。

前年机缘巧合出了本Python爬虫入门的实体书,有幸被清华大学大数据研究院选做教材「嘿嘿」, 听说还往他们合作的高校推荐,不知道挂了我学位证的母校有没有位列其中呢?哈哈!

(恰逢Ptython主题月 + 我党100周年,送两本 庆祝下,评论区抽一波~)

尔后对Python的学习毫无建树,书出版后,陆续收到读者和老师的反馈,让我意识到实体书存在的两个问题:

  • 时效性:IT更新日新月异,内容和例子都是速朽的,更新只能重新出版,走流程时间太长;
  • 趣味性:书面化限制较多,不能白话文 + 表情包,没我博客的文章有趣;

脑子一热,开始在公号上更Python教程,对书本内容的更新、补充及扩展,坚持了一段时间。后面没啥空闲时间,认真写的文章没得到较好的反馈,对自己技术的提升也没帮助,就太监了。

(怀念当初实习在南方软件园肝Android教程肝到晚上十一二点,无忧无虑的日子真好啊~)

很庆幸在老东家摸鱼的时候遇见Python,学习Python,爱上Python,她 语法简单容易上手、代码优雅功能强大、类库丰富社区活跃,任谁看都不禁心猿意马啊!人生苦短,我用Python,这就是我和Python的一帘幽梦。


2、”如火如荼” 的Python课

Python本是一门开发语言,近几年却突然火了起来,学Python广告如雨后春笋般,席卷朋友圈、恰饭公号、抖音。铺天盖地的广告,让 Python 这个词走进了普罗大众的视野,看似掀起了一股 全民学Python的浪潮,实则是 一场割韭菜的狂欢。

怎么割?且听我娓娓道来:

① 吸引用户注意

标题和开头制造焦虑,如职场人最关心的成长、薪资、跳槽等问题,蹭热点,反问句式开头,傻雕剧情引入等,感受下:

  • 工作 3 年连一个实习生都不如……
  • 疫情期间,看到了同事的工资后,我退出了群聊
  • 说了 1000 次年后要辞职的那批人,现在动都不敢动
  • 足不出户的日子里,我发现自己被同龄人狠狠抛弃了
  • 爆红的李子柒,让我越想越后怕……
  • 来公司几个月连升两级,还不是因为长得漂亮……
  • 在职场,不会 python 有多吃亏?

② 花式戳痛点引需求

指出目标用户面临的现实痛点问题,感受下:

  • 老板太严苟,专门找员工茬
  • 钱少事多,拿着卖白菜的前,操着卖白X的心
  • 同事间勾心斗角,努力大半年还不如刚来的信任
  • 发展空间小,每天都做重复的事情,无效加班…
  • 职场上没竞争力,体力拼不过大学生,资源拼不过老同事
  • 你要悄悄学Python,然后惊艳所有人!
  • 求职工作有帮助,学完Python薪资加不停…

③ 扩大目标人群

受众越多,就可能带来更多的转化,感受下:

  • 你会想:学编程不就成了程序员吗?其实在这个时代真不是这样。
  • 做为职场必备技能,大部分行业都已在招聘 ID 中,纷纷给出了 XXX 这样的招聘条件
  • 下到13岁,上到68岁,大家都在学Python
  • 学Python可以帮助做金融的人XXX,帮助做电商的人XXX,帮助做社群的人XXX

④ 呈现产品差异化

突出产品不同于其他课程的点,让用户选择它的课程,感受下:

  • 零基础、情景教学、在线实操;
  • 编程小白从”不知道”到”知道”
  • 刷个短视频的时间就能学完一节课;
  • 三天入门Python,一行代码打开上帝模式!
  • 助教老师全程指导、像玩游戏一样

⑤ 强化产品功效

给用户造梦,强调收益,学完之后会怎样,感受下:

简直成为用户口中的大神,用几行代码就能解决他们处理半天的工作……

⑥ 价格拆解+福利,促成转化

临门一脚,给用户立即购买的理由,利用紧迫感来促成转化,如强调价格是专项价格,仅限前100,只要8.9元(现在有些课程直接0元学,可见割韭菜的竞争有多激烈)。

前六步其实只是套路的开始,更深的套路还在后面~

⑦ 强化用户学习动机

入群后给你科(xi)普(nao)学Python有多厉害,不断激发你的需求和兴趣,增强你的学习动机,感受下:

  • 晒学员案例,强调菜鸟学完有多牛逼,工作效率有多高;
  • 甩Python岗位薪资,告诉你学Python就业有多容易,薪资有多高;
  • 扩大目标受众,强调谁都能学,谁学都能赚,再不学就要被时代、被同龄人抛弃…

⑧ 让用户感觉能轻松做到

简单易行给用户参与感,将学习门槛和难度设置得很低:

  • 连软件都不需要下载,打开网页就可以参加学习,课程是文字交互式(聊天)的学习,对0基础同学非常友好;
  • 帮你把代码也写好了,你只需要点个 运行 就能出运行结果;
  • 课后练习题也只是复制粘贴,改动改动,就行了。

鼓励分享给用户成就感

  • 班主任会让学员在学习完一关后将代码的运行结果发到群里,发了的学员能获得课程资料包奖励;
  • 分享链接时还会显示每个人一共运行了几行代码,让人忍不住产生攀比心理;
  • 对于没有完成当天任务的人来说更是一种激励;

送优惠券触发用户转化

体验课临近结束,班主任趁热打铁,开始给大家介绍”正价课程”

  • 正价一门999,团购两门课程,总共有500块的优惠,除此之外还能叠加200的优惠券,最后只要1298就能喜提两门课程,不过优惠券数量有限,只有50张,先到先得;
  • 考虑到不少学员是略微拮据的学生党,还提出支持花呗和信用卡分期;
  • 学员报名后可以领取雨伞、帆布袋等小礼物,以统计礼物的名义,让学员在群里接龙晒单,营造出课程热卖的氛围;
  • 私聊没在群里说话的学员,说优惠券可能不够用,将优先给到已经完课的同学,敦促赶紧上完课的同时,安利你买课

利用 福格模型 (充分的动机 + 完成这一行为的能力 + 触发用户付诸行动的因素),两套组合拳下来,韭菜割得盘满钵满。

(上述内容摘自:攻陷朋友圈的8.9元Python小课,有哪些利用“人性”的新套路?,原文图文解读,阅读体验更佳~)

这让我想起了一位朋友(非IT从业者),上了几节课后觉得编程就那样,简单得不得了,花大价钱买了正课,扬言学几个月后要吊打我,不知道现在水平如何,咋也不敢问,万一等下真被吊打了,就不好了是吧?

想被割韭菜的朋友,听叔一句劝,Python课这水太深了,你把握不住!

  • 如果真是对Python感兴趣,想学来玩玩可以,建议你 直接白嫖 大佬们分享的视频教程(B站上一堆);
  • 如果想着学完这些良莠不齐的速成班后,能赚w,我劝你早点打消这个念头;

外行看热闹,内行看门道,Python课类型看似琳琅满目高大上:爬虫、数据分析、OA自动化办公、人工智能等,实际上就是教你 某几个Python库 的使用,如pandas,numpy,matplotlib等。而这些,网上都有,搜索引擎搜下关键词,一堆。

编程语言,只是一个工具,库更是如此,要学的东西多着呢,编程能力的养成是由时间、经历和智慧堆积而成的,上几个月速成班,等于人家工作几年,有点想得太美好了吧!


3、明哲保身避免 “牢狱之灾”

偶尔有大数据公司被端,无良流量自媒体为博眼球,夸大事实,一句 爬虫玩得好,牢饭吃得早,把想学爬虫萌新吓得瑟瑟发抖,生怕因为自己写的爬虫被拷进去,嘤嘤嘤,害怕,我:

说实话,以大部分萌新的技术能力着有些实想太多,这种妄下定论的方式跟 吃完虾再吃维生素C会砒霜中毒 理论一样,脱离剂量谈毒性——都是耍流氓。

从技术中立角度而言,爬虫技术本身并无违法违规之处,爬什么,怎么爬才是导致锒铛入狱的罪魁祸首。Github上有个库记录了国内爬虫开发者涉诉与违规相关的新闻、资料与法律法规:
github.com/HiddenStraw…

节省读者时间,直接归纳下:

① 无视robots协议,爬不给爬的数据

robots.txt,纯文本文件,网站管理者可在此文件中声明不想被搜索引擎访问的部分,或指定搜索引擎只收录的指定内容,语法简单:

  • 通配符(*) → 匹配0个或多个任意字符;
  • 匹配符($) → 匹配URL结尾的字符;
  • User-agent → 搜索引擎爬虫的名字,各大搜索引擎都有固定的名字,如百度Baisuspider,如果该项为*(通配符) 表示协议对任何搜索引擎爬虫均有效;
  • Disallow → 禁止访问的路径;
  • Allow → 允许访问的路径;

以掘金的robots.txt为例:

不过这个协议可以说是 君子协议,防君子不防小人,无视Robots协议随意抓取网站内容,将 涉嫌 构成对《反不正当竞争法》的第二条的违反,即违反诚实信用原则和商业道德的不正当竞争行为。

② 强行突破网站设置的技术措施

网站一般都会做下反爬,以减少爬虫批量访问对网站带来的巨大压力和负担。爬虫开发者通过技术手段绕过反爬,客观影响了网站的正常运行(甚至搞挂了),适用《反不正当竞争法》第十二条(四) 其他妨碍、破坏其他经营者合法提供的网络产品或者服务正常运行的行为。

而强行突破某些特定被爬放的技术措施,还可能构成刑事犯罪行为:

  • 《刑法》第二百八十五条:违反规定侵入国家事务、国防建设、尖端科学技术领域的计算机信息系统的,不论情节严重与否,构成非法侵入计算机信息系统罪。
  • 《刑法》第二百八十六条还规定,违反国家规定,对计算机信息系统功能进行删除、修改、增加、干扰,造成计算机信息系统不能正常运行,后果严重的,构成犯罪,处五年以下有期徒刑或者拘役;后果特别严重的,处五年以上有期徒刑。
  • 违反国家规定,对计算机信息系统中存储、处理或者传输的数据和应用程序进行删除、修改、增加的操作,后果严重的,也构成犯罪,依照前款的规定处罚。

这里还要警惕一点:为违法违规组织提供爬虫相关服务,间接的也可能要负刑事责任,案例里的极验破解者被抓就是模板,尽管技术本身无罪,但是你开发出来了,被犯罪分子利用了,一样有责任。( 这也是我之前写的一篇《如何用Python投机倒把几天“暴富”》 不提供投注脚本的原因~)

③ 爬取特定类型的信息

一、用户的个人隐私

抓取用户隐私信息,或在抓取后公开传播,对用户造成损坏后果的,可能侵犯了用户的隐私权。

二、用户的个人信息

  • 《民法总则》第111条:任何组织和个人需要获取他人个人信息的,应当依法取得并确保信息安全。不得非法收集、使用、加工、传输他人个人信息。
  • 《网络安全法》第四十四条:任何个人和组织不得窃取或者以其他非法方式获取个人信息。
  • 《刑法》修正案(九)第二百五十三条:违反国家有关规定,向他人出售或者提供公民个人信息,情节严重的,构成犯罪;在未经用户许可的情况下,非法获取用户的个人信息,情节严重的也将构成“侵犯公民个人信息罪”。

三、著作权法保护的作品

  • 就网页 访问行为 而言,爬虫本身进是对人类访问行为的模拟,爬虫访问人工能访问的信息,不构成侵权。绕过限制访问特定用户才能解除的信息,爬虫访问行为就有可能涉嫌破坏技术措施的违法或者侵权行为。
  • 就数据 保存行为 而言,抓取行为的本质上是对信息的复制,此行为有可能侵犯著作权人的复制权,我国对临时复制的行为持宽容态度。
  • 就数据 提取和使用行为 而言,如果爬虫控制者在自己的网站上公开传播抓取的信息,则可能进一步侵犯信息网络传播权。

四、商业秘密

《反不正当竞争法》第九条:以不正当手段获取他人商业秘密的行为即已经构成侵犯商业秘密。而后续如果进一步利用,或者公开该等信息,则构成对他人商业秘密的披露和使用,同样构成对权利人的商业秘密的侵犯。

五、反不正当竞争保护的数据

未经许可抓取、使用网站中数据的行为,损害了网站的竞争优势,从而构成不正当竞争。这里的数据可能是由用户生成的,无版权,但是作为网站的主要竞争力来源。如抓取大众点评、知乎等UGC模式网站上用户发布的信息,在自己的产品或服务中发布、使用该信息,则有较大风险构成不正当竞争。

案例还是建议各位看一看,担心自己写的爬虫违法,可以对号入座看一看,总结下爬的基本操守:

  • 先确定爬啥网站:国家事务,国防建设、尖端科学技术领域的不要碰;
  • 确定什么内容:个人隐私、个人信息、商业秘密不要碰;著作权法保护、不正当竞争保护的数据,自己偷着乐,不传播和用作盈利还好 (如数据分析参考下~)。
  • 爬取手段:温柔点,尽量别影响正常用户的使用,细水长流,把别人网站搞挂了,不搞你才怪。
  • robots协议:em…我是小人

天网恢恢,疏而不漏~

好吧,侃这一Part就到这里,讲了:自己接触Python的历程 + 很火的Python课广告解读 + 爬虫违法的范畴解读,相信可以打消一部分想学Python爬虫的小白萌新心中的顾虑。前戏有点多了,立马开始正文Python爬虫入门秀。


0x2、开胃小菜——学点Python基础

既然是学Python爬虫,肯定是要学点Python基础的,主要原因:

  • 可以让你把为数不多的精力花在爬虫学习上,而不是花在解低级的语法错误上;
  • 某些基础知识是Python爬虫的前置知识,如文件操作;

一个误区:

要把Python基础语法学得很完整才敢往下学Python爬虫,先不说这得何年何月,Python爬虫中涉及的Python基础语法都是比较简单的。高深语法,以后进阶再考虑,或者用到再查也不急,如并发相关。

一个建议

学习语法时做好笔记,大部分是API用法,心里有个印象,以后碰到在回来查就好!

《Python基础》 肝完了,读者可以跟着笔者的思维导图走一波,也可以自行搜索资料学习,笔者把基础部分划分为11个模块

  • Python开发环境搭建;
  • 注释与模块;
  • 基础常识;
  • 变量、常量和字符串;
  • 数据类型
  • 条件判断与循环
  • 函数
  • 异常与断言
  • 类与对象
  • 文件存储
  • 常用模块

具体章节脑图如下:


0x3、爬虫缘起——爬小姐姐

很多人学爬虫,都是从爬小姐姐开始的,笔者也不例外,毕竟 爱美之心,人皆有之。

《Python爬虫》 就肝了一点,就没有详细的思维导图咯,更多的只是 讲解思路,读者可自行搜索关键词学习,网上爬虫相关的文章泛滥,基本都能找到,是在找不到的可在评论区留言。


① 概念

1) 引出爬虫

比如,你最近想找一些美女壁纸作为电脑壁纸,然后发现了一个网站,上面有很多符合你口味的图片,如:

你 疯狂 打开图片详情,跳转到详情页,然后右键保存,毕竟只有小孩子才做选择,而你:

在保存了十来张后,你渐感有点憋不住,开始萌生这样的想法:

能不能整个东西,代替你做这些重复操作,让你的双手从键盘和鼠标上腾出来,做点其它的事情。

还真有,爬虫 就是这样一种东西:

按照特定规则,自动浏览或抓取万维网信息的程序或脚本。

除了支持这种网页自动化操作外,爬虫还可以用作数据爬取,有个误区要注意下:

不止Python能写爬虫,其他语言也支持,如JavaScript、Java和PHP等,只是刚好笔者会这个而已。

2) 基础知识

为了解放你的双手,你决定开始学习编写爬虫,但在此之前,你还得再 了解 一些Web的基础常识:

HTTP协议

  • URL的组成
  • HTTP请求报文 (请求行、请求头、请求正文)
  • HTTP响应报文 (状态行、响应头、响应正文)
  • Session和Cookie

Web基础

  • HTML:超文本标记语言,决定网页结构与内容,简单了解下标签语法;
  • CSS:层叠样式表,决定网页的表现形式,简单了解下三类CSS选择器(标签、类、id);
  • JavaScript:简称JS,一门脚本语言,决定网页行为,知道下就好,学js破解时再学习也可以;

② 爬虫初体验

对Web常识有个基本认知就可以往下学了,爬虫的编写一般分为四步:抓包 → 模拟请求 → 数据解析 → 数据保存。

1) 抓包 → Chrome开发者工具

Chrome浏览器自带,Windows按F12可以直接打开,切换到Network选项卡,F5刷新网页即可开始抓包:

可对特定类型的请求进行过滤,或者搜索自己所需的请求,这里要注意一点:

有些网页使用js动态渲染页面,Elements 是渲染后的网页结构,大部分模拟请求的库,是不支持js动态加载的。所以有时会出现 Elements 上有的结点,模拟请求的时候却拿不到,所以要以Network或Sources部分的请求响应结果为准!!!


2) 模拟请求 → requests库

抓包定位想爬的链接,接着就到模拟请求了,很多老教程会提下 urllib 库,在我看来,它的唯一优点就是 内置,不用另外导包,但真的不好用!建议直接上 requests 库,简单易用不是一点点。

拼装请求内容 前要对请求进行分析,流程如下:

  • 请求类型:GET、POST还是其他;
  • URL拼接规则:\xxx\yyy 类型就直接拼接,?xxx=yyy&zzz=ooo就传参 params;
  • 请求参数:一般POST方式才有,直接复制粘贴,塞字典里,传参 data;
  • 请求头:看Request Headers,一般最少要传User-Agent、Host,其他的看着粘贴,传参 headers;

解析完,就到拼装请求内容模拟请求的环节了,一个简单的代码示例如下:

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
Python复制代码import requests as req

base_url = "https://xxx.xxx"
page_url = base_url + "/yyy" # 请求URL

# 请求头
page_headers = {
'Host': 'xxx.xxx',
'Referer': base_url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/83.0.4103.97 Safari/537.36',
}

# 请求参数
page_data = {'id': 123}


def fetch_page():
try:
# POST请求类型
resp = req.post(page_url, headers=page_headers, data=page_data)
print("请求:", resp.url)
if resp is not None:
if resp.status_code == 200:
# 解析返回的数据
print(resp.content)
except Exception as e:
print(e)


if __name__ == '__main__':
fetch_page()

模拟请求,如果爬取结果与预期不符(内容不对或报错),排除程序本身的逻辑错误,常见的排查方向如下(*标注为不常见):

  • 1、请求链接是否正确 → 打印出来跟目标链接对比一下;
  • 2、重定向问题 → requests库默认自动处理重定向,请求前后打印的url不一致,可能是这个问题,可以添加参数 allow_redirects=False 禁用重定向,重定向链接一般在响应头的 Location 字段,也可以打印 history 跟踪重定向历史;
  • 3、请求头/请求参数是否漏传或误传,如有些站点要Referer(反盗链用),有些又不要,传了还可能报错;
  • 4、请求类型错误,有些接口只支持POST,不支持GET;
  • 5、请求次数过于频繁,有些站点限制了一段时间内某个IP或用户访问的频次;
  • 6、网页数据是js动态加载的
  • 7.*后端不走寻常路,如post请求参数包含中文,网站编码gb2312,需要对中文参数encode(“GB2312”)下等。
  • 8、Cookies问题:设置错或没设Cookies,简单的可使用Session(会话)自动处理Cookies;
  • 9、SSL证书验证问题:爬取站点不被CA认证的HTTPS证书,可能会报SSL错误,可添加参数 verify=False 跳过验证,配合 urllib3.disable_warnings() 把857警告也去掉;

以上就是笔者大概的排查过程,欢迎评论区补充,第5、6点解决方法会在反爬虫那里进行讲解。


3) 数据解析 → lxml库

模拟请求得到目标数据后,就要进行下一步了数据解析了,一般原始数据有三类:HTML、XML和JSON。

JSON用自带的**json库** 解析即可,Python基础的常用模块部分有讲解,另外两种可以用 lxml库 解析。

lxml 库,底层由C语言实现,支持XML和HTML的解析,使用 XPath表达式 定位结点,解析效率非常高。

库本身用法非常简单,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import requests as req
from lxml import etree

resp = req.get("https://www.baidu.com")

# 传入html文本,得到一个可进行xpath的对象
selector = etree.HTML(resp.text)

# 编写提取图标路径的XPath表达式
xpath_reg = '/html/head/link[@rel="icon"]/@href'

# 提取结点
results = selector.xpath(xpath_reg)

# 打印结点信息
print(results[0])

难的是 XPath语法,语法有:

  • 结点关系:父、子、同胞、先辈、后代
  • 选取结点:最有用、谓语、选取未知节点、选取若干路径、轴、运算符、函数

XPath的完整教程可参见:《XPath 教程》

Tips:调试小技巧,Chrome Elements选项卡,按下Ctrl+F,可在此直接验证XPath表达式是否能正确定位。还可以右键目标结点,Copy → Copy XPath直接获取XPath表达式,当然这种方式获取的XPath表达式一般不是最优的。


4) 数据存储 → 保存为二进制文件

利用lxml解析到了目标数据,接着就到数据存储了,目标数据可能是:图片、音视频、普通文本 等,普通文本(如小说)直接保存txt,其他文件无脑保存成二进制文件,示例如下:

1
2
3
4
python复制代码with open('out.txt', 'w+') as f:
f.write(resp.content)

# 二进制文件把文件模式从w+改成b+就好~

图片、音视频类可能还是一个资源链接,还需对这个链接进行模拟请求,才能获取到目标资源。

以上就是爬取一个简单网站的基本流程,读者可以找个简单壁纸/小说网站试试水,真的超简单~


③ 爬虫进阶

经过初体验,爬取简单网站,保存数据已不是什么难事,往下走就是针对各个方面的进阶了。


1) 抓包进阶

Chrome开发者工具抓包一般是够用的,还可以在上面进行js逆向,不过学点其他工具的使用也无妨。

两个最常见的PC抓包工具:Fidder(Windows建议) 和 Charles(Mac建议),原理都是 中间人代理,除了能抓浏览器的数据包外,还能抓PC、移动端的数据包,除此之外还有模拟请求等功能。两者用法和功能类似,二选一学习即可。

有时网页端或PC端不好分析,还可以尝试从移动端入手(手机、平板),上述两个抓包工具都可以抓移动端的请求(此处以Android为例 ):

  • 对于 普通的HTTP请求,手机Wifi和PC在一个局域网,抓包软件 打开允许远程发送请求、查看本机ip,然后手机设置下代理,就可以开始抓包了,如:

  • 而对于 HTTPS请求,则需要安装CA证书,以Charles为例:

浏览器访问下述链接下载安装证书:

  • 安装完证书能抓到HTTPS请求了,但是看不到请求内容,那可能是因为:

Android 7.0(Nougat,牛轧糖)开始,Android更改了对用户安装证书的默认信任行为,应用程序「只信任系统级别的CA」

解决方法,要么 用Android 7.0以前的设备,要么 手机root,把证书安装到系统证书中,具体操作步骤如下:

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
bash复制代码# 打开终端,输入下述命令把 cer或者der 证书转换为pem格式
openssl x509 -inform der -in xxx.cer -out xxx.pem

# 证书重命名,命名规则为:<Certificate_Hash>.<Number>
# Certificate_Hash:表示证书文件的hash值
# Number:为了防止证书文件的hash值一致而增加的后缀

# 通过下述命令计算出hash值
openssl x509 -subject_hash_old -in Fiddler.pem |head -1

# 重命名,hash值是上面命令执行输出的hash值,比如269953fb
mv Fiddler.pem <hash>.0

# adb push命令把文件复制到内部存储
adb push <hash>.0 /sdcard/

adb shell # 启动命令行交互
su # 获得超级用户权限
mount -o remount,rw /system # 将/system目录重新挂载为可读写
cp /sdcard/<hash>.0 /system/etc/security/cacerts/ # 把文件复制过去
cd /system/etc/security/cacerts/ # 来到目录下
chmod 644 <hash>.0 # 修改文件权限

# 重启设备
adb reboot
  • 如果将证书安装到系统后,还是抓不到HTTPS的话,可能是 APP设置了不走系统代理:

如 Flutter 中默认不走系统代理,又比如APP使用了 OKHttp 请求网络,设置了proxy(Proxy.NO_PROXY)或重写了proxySelector;

传统的中间人代理方式就走不通了,可以用电脑作为热点,然后手机连接此热点,用 Wireshark 抓包:

Wireshark直接抓的是本机网卡的进出流量,能抓到所有流量包,支持一到七层全解码,缺点就是为了抓特定TCP Flow/Session 流量要写一个长长的过滤条件表达式,对初学者不怎么友好,不过对协议研究也特别有帮助。

  • 还有,部分应用为了防止中间人抓包,会进行证书认证,你一用代理,APP就不能正常使用,分为两种:
  • 单向认证:只在APP端做证书校验,破解之法也简单,hook ssl校验相关代码,如使用Xposed插件 JustTrustMe,对于okhttp混淆加密的可以用 JustTrustMePlus
  • 双向认证:APP端和服务端都对证书进行校验,破解之法就繁琐一些了,通过 Frida Hook脚本把证书抠出来,一般是Hook java.security.KeyStore类的load()方法,因为它的形参就是需要的证书和密码。把它们配到抓包工具中,就可以抓双向认证的包了。还有一种不用抠证书的,就是Hook SSL加解密的类,不解密APP上显示啥?在此将数据保存为pcap格式,再用Wireshark打开查看明文数据。

Android设备大概的抓包技巧就上面这些了,Frida相关脚本自己Github搜搜,挺多的,工具也不局限上面这些。

另外移动端也有一些抓包相关的工具:HttpCanary(手机版的中间人抓包)、tcpdump(需要root权限,导出数据包,用Wireshark等协议工具查看结果)、Drony(创建了一个VP嗯,将流量都重定向到自身,无视无代理模式抓包,需配合抓包工具如Charles使用,高版本好像失效了~)

苹果手机没用过,具体的抓包手段就不知道了~


2) 模拟请求进阶

requests库 模拟请求,也是基本够用的了,硬要挑毛病的话,有下述两个问题:

  • 目前 不支持 HTTP 2.0;
  • 在不借助第三方库的情况下,只能发送 同步请求,即发起请求后堵塞,得等服务器响应或超时,才能进行下一个请求;

目前支持HTTP 2.0的Python模拟请求库貌似只有:httpx 和 python-hyper/h2

而同步请求效率不高的问题,解决方法有很多种:多线程、多进程、协程、替换成支持异步请求的库。

笔者的理解(可能有误):Python中由于**GIL(全局解释锁)**的存在,一个线程运行其他线程堵塞,使得多线程代码不是同时执行的,而是交替执行。这种 伪多线程 的不足,其实针对的 计算密集型 的任务的不足,对于爬虫这类 IO密集型 的操作,使用多线程还是能提高效率的。

多线程简易示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import threading

def load_url(page):
resp = req.post(page_url, headers=page_headers, data={"page": page})
print(resp.text)


if __name__ == '__main__':
page_list = [x for x in range(1, 100)] # 模拟生成100页
# 根据爬取页面数生成对应数量的线程
thread_list = [threading.Thread(target=load_url, args=(page,)) for page in page_list]
for t in thread_list:
t.start() # 启动线程
for t in thread_list:
t.join() # 等待每个线程执行完毕

当然,不建议使用这种直接创建一堆线程的情况,没有复用,可以引入线程池让其自行调度,简易示例如下:

1
2
3
4
5
6
7
8
9
python复制代码from multiprocessing.pool import ThreadPool

if __name__ == '__main__':
page_list = [x for x in range(1, 100)] # 模拟生成100页
pool = ThreadPool(10)
for p in page_list:
pool.apply_async(load_url, args=(p,))
pool.close()
pool.join()

并发操作,结果写入,记得 加锁!!!不然会出现乱序的情况。多进程(Process)和进程池(Pool)实现也是类似,就不复述了,说下 协程。

笔者的理解(可能有误):多线程和多进程任务切换存在开销,协程是定义了一个更小的操作单元,本质是单个线程,各种类型的任务被放到这个单线程里(IO或CPU密集计算),然后开发者可以自行调度任务的执行顺序,如遇到IO操作,切去执行其他任务,而不是堵塞。

Python 3.4 开始引入协程的概念(以生成器yield为基础),3.5 新增 asyncio库 作为Python标准库,使得协程的实现更加方便,其中最主要的是两个关键字:async(定义协程) 和 await(挂起堵塞方法的执行),简单过下asyncio库的用法,先是几个概念:

  • coroutine → 协程对象,用async关键字修饰一个方法,调用此方法不会立即执行,而是返回一个协程对象,可将此对象注册到事件循环中,等待调用;
  • task → 任务对象,对coroutine的进一步封装,包含任务的各种状态;
  • future → 将来执行或没有执行的任务的结果,和task没有本质区别;
  • event_loop → 事件循环,将函数注册到这个循环上,当满足发生条件时,调用对应方法;

一个多任务协程的简单代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码import asyncio
import random

# async定义协程方法
async def load_url(page):
print("加载页面:", page)
print("挂起页面:", page)
# await堵塞(将耗时等待的操作挂起,转去执行其他协程,直到其他协程挂起或执行完毕)
await asyncio.sleep(random.randint(0, 3))
print("解析完页面:", page)


if __name__ == '__main__':
cp_utils.is_dir_existed(out_dir)
page_list = [x for x in range(0, 100)] # 模拟生成100页
coroutine_list = [load_url(x) for x in page_list] # 构造任务,生成协程对象(coroutine)
loop = asyncio.get_event_loop() # 事件循环
# run_until_complete将协程注册到事件循环loop中,然后启动
loop.run_until_complete(asyncio.wait(coroutine_list))
loop.close()

还可以调用 ensure_future() 函数将写好协程对象转换为task对象,绑定一个任务完成的回调,在这个回调里可以通过task对象的result()即可获得返回结果,改动下代码:

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
python复制代码# async定义协程方法
async def load_url(page):
print("加载页面:", page)
print("挂起页面:", page)
# await堵塞(将耗时等待的操作挂起,转去执行其他协程,直到其他协程挂起或执行完毕)
await asyncio.sleep(random.randint(0, 3))
return "解析完页面:", page


def complete(t):
print("Task status:", t.result())


if __name__ == '__main__':
cp_utils.is_dir_existed(out_dir)
page_list = [x for x in range(0, 100)] # 模拟生成100页
# 构造任务,利用ensure_future函数生成task对象
task_list = [asyncio.ensure_future(load_url(x)) for x in page_list]
for task in task_list:
task.add_done_callback(complete) # 添加回调函数
loop = asyncio.get_event_loop() # 事件循环
# run_until_complete将协程注册到事件循环loop中,然后启动
loop.run_until_complete(asyncio.wait(task_list))
loop.close()
# 回调其实等同于下面直接拿
for task in task_list:
print(task.result())

很简单是吧,不过要注意一点:

await 后面要跟着一个协程对象,将requests请求的方法抽取出来并用async修饰,看似会在请求时,服务器没响应挂起,切换到其他协程。实际上,并不会,requests会把asyncio堵塞住,还是同步!!!

一种解决方法是:利用 loop.run_in_executor() 将同步操作变成异步操作,示例如下:

1
2
3
4
python复制代码async def load_page(page):
temp_loop = asyncio.get_event_loop()
result = await temp_loop.run_in_executor(None, req.post, page_url, {'page': page})
return result.text # 获取响应文本

不过传参有些麻烦,比如requests.post(),没有设置headers关键词参数,无法直接传,需要自己构造Requests实例然后通过Session实例的prepare_request()方法发起一个请求。所以建议还是使用支持异步操作请求方式的库:aiohttp,基于asyncio实现的异步HTTP框架,分为客户端和服务端量部分,我们用前者就够了,修改下上面请求的代码:

1
2
3
4
python复制代码async def load_page(page):
async with aiohttp.request('POST', page_url, data={'page': page}) as resp:
body_text = await resp.text()
print(body_text)

具体详细用法可参见官方文档:Welcome to AIOHTTP,除了aiohttp还有很多支持异步请求的网络库,如 httpx,篇幅限制就不在这里一一介绍了。

上面的多线程,多进程、协程、更换异步请求库提高爬取效率的手段,本质上还是在一台机子上跑爬虫程序,受本机的带宽、硬件影响。对于一些大型的爬取任务,还可以通过 分布式爬虫 提高爬取效率。


3) 数据解析进阶

初体验部分学了基于 XPath语法 解析html或xml的 lxml库,一笔带过其他两个常用解析库:

  • BeautifulSoup库:简单易用,需要解析器(安利lxml),支持结点选择,文档数搜索,大部分CSS选择器;
  • PyQuery库:API与jQuery类似,支持大部分CSS选择器,支持DOM操作(增删结点);

解析速度:lxml > PyQuery > BeautifulSoup,类似的解析库网上有很多,核心还是得掌握 XPath语法 和 CSS选择器语法。

此Part重点还是说下字符串提取神器——正则表达式,很多人对其都是望而生畏,其实不然,掌握正确语法+练习,写个简单提取目标字符串的正则还真不难。

Python 内置 re模块 处理正则表达式,提供了下述函数:

  • 匹配(找一个):match() → 从开头进行匹配、search() → 扫描整个字符串;
  • 检索与替换(找多个):findall() → 扫描整个字符串所有能匹配的、finditer()、split() → 分割;
  • 编译Pattern对象:多次调用相同正则,将正则表达式编译成Patter对象复用;
  • 分组:group() → 获得分组匹配内容,如group(1) 获得第一个匹配分组;

然后调用这些函数可以传入一个flags参数(标志位修饰符),来控制匹配模式,可使用多个,用|进行连接:

re模块的API就这么简单,接着是正则表达式的语法:(字符 → 数量 → 边界 → 分组 → 断言 → 贪婪与非贪婪)

所谓的断言就是:给指定位置加一个限定条件,规定此位置前/后的字符需满足条件才算匹配成功:

正则匹配默认是 贪婪匹配,就是匹配尽可能多的字符,示例如下:

是的,语法也是那么简单,多在爬取过程中尝试练习吧,从无脑**(.*?)**开始慢慢优化。另外,还有两点要注意下:

  • 需要匹配特殊字符串时,可在前面加反斜杠进行转义,如**\\(**;
  • 想避免多次转义造成的反斜杠困扰,可在正则表达式前加上**r修饰,告知编译器它是原始字符串**,不要转义反斜杠;


4) 数据存储进阶

初体验部分直接把文本保存成txt,其他保存成二进制文件,有些粗暴了,实际上还需要对数据进行细分。

类似于爬取豆瓣音乐Top250,数据是这样的:歌名-作者-发行日期-分类-评分-评分人数,可以用:csv 或 Excel 保存。对应的库:csv库 和 xlwt/xlrd库。

爬取贝壳网房源、拉钩职位等也是类似,配合数据分析基础三件套(库):pandas + numpy + matplotlib ,做下简单的数据分析及可视化,也是挺有趣的。

上述这种把数据保存成csv或Excel的形式还有一个好处,查看数据的成本低,没有编程的人也能直接看,没学习成本,不需要额外安装软件(手机或PC一般内置)。

除此之外,一种适合开发者的保存方式是把数据保存到 数据库 中,数据库一般分为两类:

  • 关系型数据库:通过二维表保存,存储行列组成的表,通过表与表的关联关系来体现;
  • 非关系型数据库,又称NoSQL,基于键值对,不需要经过SQL层解析,数据间没有耦合,性能非常好;

关系型数据库用 MySQL 比较多,得掌握点基础:

MySQL安装配置、数据库基本语法(增删改查)、Python连接操作数据库(如pymysql库)、特殊符号与表情存储问题、数据库可视化工具使用(如Navicat、DataGrip)

其次是非关系型数据库,常用的有 MongoDB (文档型数据库) 和 Redis (键值对型数据库):

MongoDB,由C++编写,基于分布式文件存储,灵活的文档模型,JSON格式存储最接近真实对象模型,笔者经常把爬取到的JSON数据直接塞里面,后续再对数据进行清洗,得掌握点基础:

MongoDB安装、Python连接操作数据库(如**PyMongo库**)

Redis,用ANSI C编写,支持网络,基于内存、可选持久化、Key-Value的NoSQL,速度快,分布式爬虫常常用来保存任务队列,得掌握点基础:

Redis安装配置、Python连接操作数据库(如**redis-py库**)

对了,有一些资源是没办法直接拿到的,可以利用一些第三方库来获取,如爬取油管、B站视频可以用 you-get库 解析下载。又比如一些提供报告的站点,不提供直接的报告PDF,而是把报告的每一页作为图片展示出来,如果想整合成一份PDF,就要自行处理了:把图片下下来,pillow库 去下alpha通道,然后用图片生成PDF的 img2pdf库 合成。

另外,不是一定就得把资源Download下来或者传CDN,占空间,可以只保存一个链接,偶尔看到有些盗版漫画APP的图片地址就是指向原站点地址的,2333,防盗链处理也简单,客户端请求图片带上原站点Referer就好了(一般)~

还有,CDN的资源,一般 是不限制IP访问频次的,所以爬取的时候可以先存链接,等爬取完再批量下载~


④ 简单反反爬虫

反爬虫就是站点通过一些技术手段,提高爬虫的爬取门槛,反爬的奇淫巧技有很多,笔者不是专门做这个的,所以只会一些简单的反反爬虫技巧,欢迎在评论区补充~

1) 未登录无法访问

登录态 的保持和验证一般是 Cookies,有些还会配合一个动态变化的token,未登录无法访问,那就保持登录态哇~

  • 简单不变的Cookies,浏览器登录了,抓下包,复制下Cookies,模拟访问的时请求头带上;
  • 抓下登录的接口,比较简单的话,就先模拟登录下,配合requests的Session,后续会话都是登录态;

还可以把Cookies序列化到本地,爬虫重新运行时反序列化设置下。而对于复杂且一直变化的Cookies,上述两种方法可能就不可行了,需要自己破解构造规则,或者放弃抓包,用自动化测试工具爬取。


2) 页面使用Ajax异步加载

常见于 分页 的网页,requests模拟访问原始页面链接只能拿到局部数据,原始页面滑动到底部会加载更多数据。

应对之法:直接爬这个Ajax数据接口,Chrome开发这工具直接过滤 XHR 类型请求,然后看请求参数,一般跟分页相关的(如第几页page、条数limit等),看响应结果,一般是能拿到总记录数的,自己换算下,自己本地构造一个参数列表,遍历传参模拟访问Ajax接口,即可得到所有数据。


3) 锁IP

User-Agent 这个常识就不用说了吧,服务端用来识别是否为合理真实的浏览器,请求头带上,可以从 fake-useragent库 里随便拿一个。

某个IP在一段时间内访问次数过于频繁,超过了服务端设置的阈值,就会被认定为爬虫进行封禁,后果就是,本机(或者整个局域网) 突然访问不了该站点。

一般过段时间就会解禁,但也会进入一个观察名单,更严格的访问频次限制。

应对之法:使用代理IP,代理IP常见的获取渠道有下面这些:

  • 免费代理:浪费时间,都被用烂了,基本秒失效,可用性极低,如西刺;
  • 付费代理:一分钱一分货,有些便宜的,还得自己写个 代理池 筛一筛,个人习惯使用XX云的 代理隧道;
  • ADSL:就是ADSL拨号上网,每次拨号都会换一个IP,上网租台动态拨号的 VPS主机。不建议把爬虫脚本托管到主机上(在上面爬站点),建议只把它作为一个 代理服务器(就是转发请求和响应),要把拨号服务器变成HTTP代理服务器可使用 Squid 或 TinyProxy。

注:ADSL拨号时,之前的IP会失效,可通过购买多个服务器来规避,自己的云服务器维护一个可用IP的队列,如用Redis存,key为每台拨号服务器,值为当前可用IP,暴露一个更新接口给拨号服务器调用。拨号服务器编写定时拨号脚本,拨号后调用此接口更新Redis里旧IP,定时的间隔根据拨号服务器的数量权衡,保证随时都有至少一台代理服务器能用。

对了,顺带提提代理IP根据隐藏级别的分类:

  • 透明代理:改动了数据包,还会告诉服务端你的真实IP;
  • 普通代理:改了了数据包,服务端有可能知道你用了代理IP,也有可能追查到你的真实IP;
  • 高匿代理:不改动数据包转发,服务端会认为是一个普通用户端访问,记录的是代理IP;

对了,不要想着用高匿代理就能为所欲为了,还是有办法溯源到你的真实IP的!不信可以 蒙古上单冲塔,快速并发访问下”牢狱之灾”提到的网站试试看,可能没过多久就有人请你去喝茶了~


4) 锁账号

跟锁IP是类似的,就是限制了一个账号的频次,应对之法:准备一堆账号,维护一个Cookies池。

这个也别想得太简单,有些朋友想着通过某些渠道购买一堆手机号码,批量注册,然后直接批量爬取,又送塔,别当服务端的风控是憨憨啊。

这种是最容易发现的了,一般都会有一个 养号 的过程,分时段注册(提前一段时间注册),然后每天做下日常(访问接口或模拟用户行为),让服务端觉得它是一个正常用户(所以,你会发现掘金经常有些不认识的”小号”给你点赞)。


?5) JavaScript

Tips:js破解没咋研究,没啥经验,我一般碰到这种就直接上自动化测试工具模拟了,后面有弄再回来补充吧…

某些接口的参数是一串看不懂的长字符串,且一直变化的,基本就是js加密;还有一些页面的数据加载也是通过js加密的(如大众点评字体反爬)。

客户端加密逻辑由js实现,源代码对用户是完全可见的,所以还会对js下述操作:

  • 压缩:去除不必要的空格、换行等内容、把一些可能公用的代码仅限处理实现分享,最后压缩成几行,使得代码可读性变得很差;
  • 混淆:使得js更难阅读和分析,有:变量混淆、字符串混淆、属性加密、控制流平坦化、僵尸代码、调试保护、多态编译、锁定域名、反格式化特殊编码等;
  • 加密:将一些核心逻辑使用诸如C/C++语言来编写,并通过JavaScript调用执行,从而起到二进制级别的防护作用;

6) 自动化测试工具模拟

如题,通过自动化测试工具,驱动浏览器执行特定动作,如点击、输入文本、滚动等,模拟人的操作的同时还能获取当前呈现的网页源代码(Elements)。

常用的两个库 Selenium 和 Pyppeteer,后者依赖Chromium内核,无需繁琐的环境配置,相比起前者效率也高一些。网上教程教程烂大街了,就不详细介绍了。提两点:

  • 如果想拦截浏览器加载页面时发起的请求,可以配合其他抓包工具使用,如 mitmproxy、browsermob-proxy 等;
  • 使用这两个库模拟浏览器访问,有很多特征是可以被站点通过JavaScript探测到的;

服务端检测到浏览器行为不对劲,怀疑是”假人”在访问,就会触发验证手段,最常见的就是各种类型的验证码:

  • 简单图片验证码:OCR识别,tesseract自己采集图片训练,或者使用第三方OCR;
  • 复杂图片验证码:打码平台;
  • 滑动验证码:分析滑块和缺口图片的规律,计算出两者的距离,用Selenium模拟滑动;

验证码一直在变,倒立文字、滑动转圈等,但终究是敌不过 人工打码,yyds!


7) 移动端爬取

在抓包进阶处提到 抓取手机数据包,操作起来还是有些麻烦的,可以用另一种变通的爬取手段 → 直接提取页面内容,就跟Selenium操作网页一样。

常用的两个工具:Appium 和 airtest,后者API更易用,基于图像识别技术定位UI元素 (Appium对混合开发页面无法直接定位),教程很多,不展开讲。

再往下走就是 APP逆向 范畴了,脱壳反编译APK源码,直接破解出加密规则,直接脚本模拟请求,或者直接写Xposed插件,对数据加载部分代码进行Hook,直接导出数据或将数据提交到自己的服务器等。


⑤ 爬虫框架

手撕爬虫久了,会发现写了很多重复冗余的代码,每次要爬个东西,都是复制拷贝之前的代码,然后改改改。

一种优化方案是抽取常用代码块封装,弄一个自己用起来趁手的模块。

另一种则是使用爬虫框架,其中包含了爬取的全部流程、异常处理和任务调度等,常用的两个爬虫框架 Scrapy 和 PySpider,此处以基于事件驱动网络框架Twisted 编写的Scrapy为例讲解(官方文档),笔者的学习路线:

了解架构由哪些模块组成 → 模块各自的功能 → 模块间的协作 → 各个模块的定制;

Scrapy架构图

模块功能解读:

  • Engine → 引擎,框架核心,控制数据流在框架内部的各组件间流转,在相应栋座发生时触发相关事件;
  • Scheduler → 调度器,请求调度,从引擎接收Request,添加到队列中,在后续引擎请求它们时提供给它;
  • Downloader → 下载器,获取页面数据并提供给引擎,然后提供给爬虫;
  • Downloader Middlewares → 下载中间件,下载器与引擎间的特定钩子,可在下载器把Responses传递给引擎前做一些处理;
  • Spider → 爬虫,产生Request,交给下载器下载后返回Response,解析提取出Item或需要另外跟进的URL类。
  • Spider Middlewares → 爬虫中间件,爬虫与引擎间的特定钩子,可在爬虫的输入和输出两个阶段做一些处理;
  • Item Pipeline → 项目管道,用于处理爬虫产生的Item,进行一些加工,如数据清洗、验证和持久化;

模块间的协作:

一直重复上述的流程,直到调度器中不存在任何Request时,程序才会停止,对于下载失败的URL,Scrapy也会重新下载。

每个模块的订制就不展开讲了,网上很多,只提下对接Selenium、对接HTTP接口调度库免去命令行启动:

Scrapy同样不具有执行js的能力,所以对动态渲染的页面也是无能为力,可以对接Selenium来处理。对接方法很简单(Pyppeteer也是类似):

自定义下载中间件,在process_qequest()方法中对抓取的请求进行处理,启动浏览器进行页面渲染,再将渲染后的结果构造成一个HtmlResponse对象返回。

每次都启动Scrapy爬虫都要在命令行敲 scrapy crawl 爬虫名,如果是部署在服务器上,你还得连SSH,然后键入命令,比较繁琐,可以通过常用的Scrapy HTTP接口调度库来简化:Scrapyrt和 Scrapyrd,前者更轻量,用法简单,不需要分布式多任务,可以直接用它实现远程Scrapy任务的调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码pip install scrapyrt    # 安装
scrapyrt # 在任意一个Scrapy项目运行如下命令即可启动HTTP服务
# 默认9080端口,也可以自行更换端口:scrapyrt -p 9081

# 接着就可以自行拼接请求了,如:
curl http://localhost:9080/crawl.json?spider_name=juejin&url=https://juejin.cn/

# GET请求支持参数如下:

spider_name:爬虫名称;
url:爬取链接;
callback: 回调函数名;
max_requests:最大请求数量;
start_requests:是否执行start_requests方法;

# POST请求Body可选配置参数如下:
spider_name:爬虫名称;
max_requests:最大请求数量;
request:JSON对象,支持Scrapy.Request类中所有的属性

# 返回结果是一个JSON格式的字符串,可自行解析判断~


⑥ 分布式爬虫

上面的爬取任务都是在 一台主机 进行上爬取,现在扩展到 多台主机 上爬取,这就是分布式爬虫。一般爬取海量数据时才会用,如爬取知乎整站用户数据、微博所有用户信息、链接所有房产信息、电商商品价格监控等。

把原先的爬取任务列表,拆分到多台主机上,有两个问题要解决:

  • ① 怎么避免多台主机执行了同样的爬取任务,即 爬取任务的去重;
  • ② 爬取中途有一台或多台主机抽风,爬取任务和数据怎么避免丢失?

问题②,好解,弄一个 共享爬取队列 就好,主机们都来这里拿任务,抽风前把任务又丢回队列里就好,至于数据丢失就更简单了,大家统一把爬取的数据都存到统一的数据库上就好了。

问题①,在共享爬取队列的基础上,对入队的任务做 唯一性校验,如Scarpy中就是弄了一个Request指纹,管理入队和出队,又得需要一个 调度器,还可以对任务优先级进行管理。

再接着,把 共享爬取队列 和 调度器 都放到同一台主机上,然后其他主机只执行爬取任务,这就是 主从结构,其他的主机又叫 从机。

共享爬取队列 涉及到并发访问,得找个支持并发的队列框架,常见的有:Redis、RabbitMQ、Celery、Kafka 等。

Redis就很好,set还支持去重,如果习惯用Scrapy写爬虫,不用自己实现,直接上 scrapy-redis,在Scrapy的基础上增加了Redis,还基于Redis的特性对组件进行了扩展:

  • Scheduler → 把Scrapy的queue换成Redis队列(集合变数据库),去重还是用的Request指纹,只不过用的Redis的set去重,每次从redis的request队列里根据优先级pop出一个request,发给spider处理。
  • Item Pipeline → 将爬取到的Item存入redis的item queue,修改过的Item Pipeline可以很方便地根据 key从items queue提取item,从而实现items processes集群(还能存到不同的数据库中,如MongoDb)。
  • Base Spider → 重写RedisSpider继承Spider和RedisMixin(从redis读url的类),继承这个RedisSpider生成的爬虫,调用setup_redis()获取连redis数据库,设置signals(信号)
  • 空闲时的signal,会调用spider_idle() → schedule_next_request() 保证爬虫一直是活的状态,且抛出DontCloseSpider异常;
  • 抓到Item的signal,会调用item_scraped() → schedule_next_request(),获取下一个request;

大概原理就是这样,然后是主机和从机的职责策略:

  • 主机(master):搭建Redis数据库,负责指纹判重、Request分配、数据存储;
  • 从机(slaver):负责执行爬虫程序,运行过程中提交新的Request给主机;

官方仓库 里example/project/example/spiders下有三个爬虫Demo类,改改就能用:

① dmoz.py:继承CrawlSpider,只是用来说明Redis的持久性,把爬虫停了再运行,爬取记录还是保留在Redis里;

② myspider_redis.py:继承RedisSpider,重写parse函数,支持分布式爬取,不再有start_urls,取而代之的是redis_key,即爬虫的启动命令,参考格式:redis_key = ‘myspider:start_urls’,根据指定格式,start_urls将从Master端的redis-cli里lpush到Redis数据库中,RedisSpider将从数据库里获取start_urls,执行方式如下:

  • Slaver端:scrapy runspider myspider_redis.py (爬虫处于等待状态)
  • Master端,在redis-cli中输入push指令,参考格式:lpush myspider:start_urls www.xxx.xx/
  • Slave端爬虫获取到请求,开始爬取;

③ mycrawler_redis.py,继承RedisCrawlSpider,支持分布式爬取,需遵守Rule规则,执行方式同上;

主从机最大的区别是settings.py文件的配置不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码# === 主机配置 ===

# 使用scrapy-redis里的调度器组件,不使用scrapy中默认的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 使用scrapy-redis里的去重组件,不使用scrapy中默认的去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 'scrapy_redis.pipelines.RedisPipeline 支持将数据存储到数据库中
ITEM_PIPELINES = {
'example.pipelines.ExamplePipeline': 300,
'scrapy_redis.pipelines.RedisPipeline': 400,
}

# 按优先级出列
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"

# 主机IP和端口
REDIS_HOST = "localhost"
REDIS_PORT = 6379

# === 从机配置 ===
REDIS_URL = 'redis://root:xxx@master端IP:6379'

基础的配置玩法就这样,读者可以试试,再往下走就是 分布式爬虫的部署与监控 ,也是我的技术盲区。

如何把爬虫部署到众多的云服务上正常运行,有两个问题要解决:

  • 环境配置:用到库和工具的安装(还要注意版本冲突问题),进行各种配置,SSH一个个敲,要哭;
  • 代码部署:爬虫脚本代码的更新,git pull一个个拉,要哭;

第一个问题,用 Docker 容器可以解决:

Docker 是基于Linux容器的封装,提供了简单易用的容器使用接口。而Linulx容器是一种虚拟化技术,不是模拟一个完整的系统,而是对进程进行隔离(进程外套一层)。使得进程访问到的各种资源都是虚拟的,从而达到与底层系统隔离的目的。可以理解为更轻量级的虚拟机,而且容器是进程级别的,相比虚拟机而言,启动更快、资源占用更少。

Docker镜像一般都会包含一个完整的操作系统,可以通过编写Dockerfile来定制Docker镜像。爬虫项目下新建Dockerfile文件(没有后缀),内容示例如下:

1
2
3
4
5
6
bash复制代码FROM python:3.6 # 使用Docker基础镜像,在此基础上运行Scrapy项目;
ENV PATH /user/local/bin:$PATH # 环境变量配置,将/user/local/bin:$PATH赋值给PATH
ADD . /code # 将本地代码放置到虚拟容器中,第一个参数代表当前路径,第二个参数代表虚拟容器中的路径;
WORKDIR /code # 指定工作目录;
RUN pip3 install -r requirements.txt # 执行某些命令来做一些准备工作,如安装依赖库;
CMD scrapy crawl TestSpider # 容器启动命令,容器运行时会执行此命令,可以在这里启动爬虫;

执行下镜像构建命令:docker build -t test:latest .,静待构建完毕,可以本地执行**docker run test** 测试下镜像。这个镜像文件可以直接拷贝到云服务器上载入,或者推到Docker Hub上,执行命令:docker run xxx/test,会自动下载和启动Docker镜像。

这Part就到这吧,没搞过,身边也没有可以问的人,看到有个不错的库:scrapydweb,感兴趣的可以试试康~


0x4、曲终人散——有缘再见

爆肝近一周,牺牲了很多摸鱼和打王者的时间,终于把Python爬虫入门内容串起来了,吐血…

是的,这些只是 入门姿势,笔者离进阶还差:js破解的经验积累、大型分布式爬虫的部署实践、自己设计开发一个爬虫框架,但也暂时先到这里了,算是还愿了吧,希望对想学Python爬虫的朋友有所帮助吧。

自学摸索过程中,都是看 青南 和 崔庆才 两位大佬的文章过来的,站在 巨佬 的肩膀上,让我少走了很多弯路,受益匪浅,感恩~

参考文献:

  • 攻陷朋友圈的8.9元Python小课,有哪些利用“人性”的新套路?
  • 金杜知识产权主题月 | 数据之争:网络爬虫涉及的法律问题(一)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【阿里开源】工作流框架 compileflow 上手使用

发表于 2021-07-15

compileflow 是什么

compileflow 是一个非常轻量、高性能、可集成、可扩展的流程引擎。

compileflow Process 引擎是淘宝工作流 TBBPM 引擎之一,是专注于纯内存执行,无状态的流程引擎,通过将流程文件转换生成 java 代码编译执行,简洁高效。当前是阿里业务中台交易等多个核心系统的流程引擎。

compileflow 能让开发人员通过流程编辑器设计自己的业务流程,将复杂的业务逻辑可视化,为业务设计人员与开发工程师架起了一座桥梁。

功能列表

  • 高性能:通过将流程文件转换生成 java 代码编译执行,简洁高效。
  • 丰富的应用场景:在阿里巴巴中台解决方案中广泛使用,支撑了导购、交易、履约、资金等多个业务场景。
  • 可集成:轻量、简洁的设计使得可以极其方便地集成到各个解决方案和业务场景中。
  • 完善的插件支持:流程设计目前有 IntelliJ IDEA、Eclipse 插件支持,可以在流程设计中实时动态生成 java 代码并预览,所见即所得。
  • 支持流程设计图导出 svg 文件和单元测试代码。
  • 支持基于 Java 反射和 Spring 容器的代码触发

快速上手

  • 引入 compileflow jar 依赖
1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba.compileflow</groupId>
<artifactId>compileflow</artifactId>
<version>1.0.0</version>
</dependency>
  • 使用 compileflow 绘制了简单的流程图

IDEA 插件

  • 查看编译出的流程业务Java代码(以下代码为compileflow自动根据流程图生成的)
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
java复制代码public class PigFlow implements ProcessInstance {

private java.lang.Integer price = null;

public Map<String, Object> execute(Map<String, Object> _pContext) throws Exception {
price = (Integer)DataType.transfer(_pContext.get("price"), Integer.class);
Map<String, Object> _pResult = new HashMap<>();
decision8();
//AutoTaskNode: 付款
((BizMock)ObjectFactory.getInstance("com.example.compileflow.bean.BizMock")).payMoney(price);
_pResult.put("price", price);
return _pResult;
}

private void decision8() {
//DecisionNode: 计算费用
bizMockCalMoney();
if (price>=100) {
//超过100
{
//ScriptTaskNode: 春哥请客 腿打折
IExpressContext<String, Object> nfScriptContext = new DefaultContext<>();
nfScriptContext.put("price", price);
price = (java.lang.Integer)ScriptExecutorProvider.getInstance().getScriptExecutor("QL").execute("price*2", nfScriptContext);
}
} else {
//不超过100
{
//ScriptTaskNode: 冷冷请客 打5折
IExpressContext<String, Object> nfScriptContext = new DefaultContext<>();
nfScriptContext.put("price", price);
price = (java.lang.Integer)ScriptExecutorProvider.getInstance().getScriptExecutor("QL").execute("(round(price*0.5,0)).intValue()", nfScriptContext);
}
}
}

private void bizMockCalMoney() {
price = ((BizMock)ObjectFactory.getInstance("com.example.compileflow.bean.BizMock")).calMoney(price);
}

}
  • 在设计好的 bpm 文件右键创建 单元测试

bpm单元测试

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Test
public void testProcess() throws Exception {
String code = "pig";
ProcessEngine<TbbpmModel> engine = ProcessEngineFactory.getProcessEngine();
System.out.println(engine.getJavaCode(code));
Map<String, Object> context = new HashMap<>();
context.put("price", 10);

Map<String, Object> execute = engine.execute(code, context);

System.out.println(execute);
}
  • 执行流程单元测试,输出目标过程
1
2
markdown复制代码假装在计算金额~~~~~~10
支付了~~~~~~5

总结

  • compileflow 极其容易上手,降低工作流学习的难度。
  • compileflow IDEA 设计插件在 2021 版本兼容性存在问题。
  • 自动生成的单元测试代码依赖版本较低不支持 Junit5

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot实现文件上传

发表于 2021-07-15

我们使用SpringBoot来实现文件的上传,本文中的SpringBoot环境和SpringBoot+MyBatis+通用Mapper中的环境基本一致,我们就不再重复造轮子了,而是直接上手这个功能。

依赖

在SpringBoot+MyBatis+通用Mapper中我们已经引入了Web开发相关的依赖,所以这里就不需要再次引入了。

配置

我们需要对SpringMVC的文件上传功能进行一些配置,比如MaxFileSize等等属性。
我们使用JavaConfig来进行配置,写一个配置类UploadConfig ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class UploadConfig {

@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 单个数据大小
factory.setMaxFileSize("10240KB");
/// 总上传数据大小
factory.setMaxRequestSize("102400KB");
return factory.createMultipartConfig();
}
}

然后我们在application.yml文件中自定义一些配置:

1
2
3
4
5
6
7
yaml复制代码file:
upload:
path: G:\temp\images\ #文件上传目标路径
allowTypes: #文件上传允许的类型
- image/jpeg
- image/png
- image/bmp

使用ConfigurationProperties将配置读取到Java文件中:

1
2
3
4
5
6
7
java复制代码@Data
@Component
@ConfigurationProperties(prefix = "file.upload")
public class UploadProperties {
private String path;
private List<String> allowTypes;
}

代码

在写文件上传的代码之前,我们还需要做一些准备工作,比如准备一些工具类,可以方便我们实现文件的上传功能。

编写工具类

  • 唯一ID生成器,可以生成唯一ID。
1
2
3
4
5
6
7
8
9
10
java复制代码public class IDUtils {

/**
* 唯一ID生成器,可以生成唯一ID
* @return
*/
public static String generateUniqueId() {
return UUID.randomUUID().toString()+System.currentTimeMillis();
}
}
  • 文件名称替换工具,用来替换文件名称,避免文件名称重复而导致名称相同的文件被覆盖掉。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class UploadUtils {

/**
* 文件名称替换工具,将文件名称替换为随机名称
* @param oldName
* @return
*/
public static String generateFileName(String oldName){
String suffix = oldName.substring(oldName.lastIndexOf("."));
return IDUtils.generateUniqueId()+suffix;
}
}

编写Web层

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RestController
@RequestMapping("upload")
public class UploadController {

@Autowired
private UploadService uploadService;

@PostMapping("image")
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) throws Exception {
return ResponseEntity.ok(uploadService.uploadImage(file));
}
}

编写Service层

  • Service层接口
1
2
3
4
5
6
7
8
9
java复制代码public interface UploadService {

/**
* 上传图片
* @param file
* @return
*/
String uploadImage(MultipartFile file) throws Exception;
}
  • Service层实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 @Service
public class UploadServiceImpl implements UploadService {

@Autowired
private UploadProperties uploadProperties;

@Override
public String uploadImage(MultipartFile file) throws IOException {
if(!uploadProperties.getAllowTypes().contains(file.getContentType())){
throw new IOException("文件上传类型错误!");
}
String fileName = UploadUtils.generateFileName(file.getOriginalFilename());
file.transferTo(new File(uploadProperties.getPath()+fileName));
return fileName;
}
}

测试

由于我们也没有写html文件,所以我们直接使用Postman这个软件来测试。

第一步:我们使用POST请求,请求路径为/upload/image。

【注意,我们需要将请求头中的内容设置为空】

在这里插入图片描述

第二步:我们在请求体中将文件携带过去。

在这里插入图片描述

第三步,发出请求。

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…605606607…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%