后端小马
微服务架构:Nacos本地缓存 PK 微服务优雅下线
前言在上篇文章《微服务:剖析一下源码,Nacos的健康检查竟如此简单》中讲了当微服务突然挂掉的解放方案:调整健康检查周期和故障请求重试。朋友看了文章,建议再聊聊正常关闭服务时如何让微服务优雅下线。为什么说是优雅下线?我们知道在分布式应用中为了满足CAP原则中的A(可用性),像Nacos、Eureka等注册中心的客户端都会进行实例列表的缓存。当正常关闭应用时,虽然可以主动调用注册中心进行注销,但这些客户端缓存的实例列表还是要等一段时间才会失效。上述情况就有可能导致服务请求到已经被关闭的实例上,虽然通过重试机制可以解决掉这个问题,但这种解决方案会出现重试,在一定程度上会导致用户侧请求变慢。这时就需要进行优雅的下线操作了。下面我们先从通常关闭进程的几种方式聊起。方式一:基于kill命令Spring Cloud本身对关闭服务是有支持的,当通过kill命令关闭进程时会主动调用Shutdown hook来进行当前实例的注销。使用方式:kill Java进程ID这种方式是借助Spring Cloud的Shutdown hook机制(本质是Spring Boot提供,Spring Cloud服务发现功能进行具体注销实现),在关闭服务之前会对Nacos、Eureka等服务进行注销。但这个注销只是告诉了注册中心,客户端的缓存可能需要等几秒(Nacos默认为5秒)之后才能感知到。这种Shutdown hook机制不仅适用于kill命令,还适用于程序正常退出、使用System.exit()、终端使用Ctrl + C等。但不适用于kill -9 这样强制关闭或服务器宕机等场景。这种方案虽然比直接挂掉要等15秒缩短了时间,相对好一些,但本质上并没有解决客户端缓存的问题,不建议使用。方式二:基于/shutdown端点在Spring Boot中,提供了/shutdown端点,基于此也可以实现优雅停机,但本质上与第一种方式相同,都是基于Shutdown hook来实现的。在处理完基于Shutdown hook的逻辑之后,也会进行服务的关闭,但同样面临客户端缓存的问题,因此,也不推荐使用。这种方式首先需要在项目中引入对应的依赖:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>然后在项目中配置开启/shutdown端点:management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown然后停服时请求对应的端点,这里采用curl命令示例:curl -X http://实例服务地址/actuator/shutdown方式三:基于/pause端点Spring Boot同样提供了/pause端点(Spring Boot Actuator提供),通过/pause端点,可以将/health为UP状态的实例修改为Down状态。基本操作就是在配置文件中进行pause端点的开启:management:
endpoint:
# 启用pause端点
pause:
enabled: true
# pause端点在某些版本下依赖restart端点
restart:
enabled: true
endpoints:
web:
exposure:
include: pause,restart然后发送curl命令,即可进行服务的终止。注意这里需要采用POST请求。关于/pause端点的使用,不同的版本差异很大。笔者在使用Spring Boot 2.4.2.RELEASE版本时发现根本无法生效,查了Spring Boot和Spring Cloud项目的Issues发现,这个问题从2.3.1.RELEASE就存在。目前看应该是在最新版本中Web Server的管理改为SmartLifecycle的原因,而Spring Cloud对此貌似放弃了支持(有待考察),最新的版本调用/pause端点无任何反应。鉴于上述版本变动过大的原因,不建议使用/pause端点进行微服务的下线操作,但使用/pause端点的整个思路还是值得借鉴的。基本思路就是:当调用/pause端点之后,微服务的状态会从UP变为DOWN,而服务本身还是可以正常提供服务。当微服务被标记为DOWN状态之后,会从注册中心摘除,等待一段时间(比如5秒),当Nacos客户端缓存的实例列表更新了,再进行停服处理。这个思路的核心就是:先将微服务的流量切换掉,然后再关闭或重新发布。这就解决了正常发布时客户端缓存实例列表的问题。基于上述思路,其实自己也可以实现相应的功能,比如提供一个Controller,先调用该Controller中的方法将当前实例从Nacos中注销,然后等待5秒,再通过脚本或其他方式将服务关闭掉。方式四:基于/service-registry端点方式三中提到的方案如果Spring Cloud能够直接支持,那就更好了。这不,Spring Cloud提供了/service-registry端点。但从名字就可以知道专门针对服务注册实现的一个端点。在配置文件中开启/service-registry端点:management:
endpoints:
web:
exposure:
include: service-registry
base-path: /actuator
endpoint:
serviceregistry:
enabled: true访问http://localhost:8081/actuator 端点可以查看到开启了如下端点:{
"_links": {
"self": {
"href": "http://localhost:8081/actuator",
"templated": false
},
"serviceregistry": {
"href": "http://localhost:8081/actuator/serviceregistry",
"templated": false
}
}
}通过curl命令来进行服务状态的修改:curl -X "POST" "http://localhost:8081/actuator/serviceregistry?status=DOWN" -H "Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8"执行上述命令之前,查看Nacos对应实例状态为:可以看到实例详情中的按钮为“下线”也就是说目前处于UP状态。当执行完上述curl命令之后,实例详情中的按钮为“上线”,说明实例已经下线了。上述命令就相当于我们在Nacos管理后台手动的操作了实例的上下线。当然,上述情况是基于Spring Cloud和Nacos的模式实现的,本质上Spring Cloud是定义了一个规范,比如所有的注册中心都需要实现ServiceRegistry接口,同时基于ServiceRegistry这个抽象还定义了通用的Endpoint:@Endpoint(id = "serviceregistry")
public class ServiceRegistryEndpoint {
private final ServiceRegistry serviceRegistry;
private Registration registration;
public ServiceRegistryEndpoint(ServiceRegistry<?> serviceRegistry) {
this.serviceRegistry = serviceRegistry;
}
public void setRegistration(Registration registration) {
this.registration = registration;
}
@WriteOperation
public ResponseEntity<?> setStatus(String status) {
Assert.notNull(status, "status may not by null");
if (this.registration == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found");
}
this.serviceRegistry.setStatus(this.registration, status);
return ResponseEntity.ok().build();
}
@ReadOperation
public ResponseEntity getStatus() {
if (this.registration == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found");
}
return ResponseEntity.ok().body(this.serviceRegistry.getStatus(this.registration));
}
}我们上面调用的Endpoint便是通过上面代码实现的。所以不仅Nacos,只要基于Spring Cloud集成的注册中心,本质上都是支持这种方式的服务下线的。小结很多项目都逐步在进行微服务化改造,但一旦因为微服务系统,将面临着更复杂的情况。本篇文章重点基于Nacos在Spring Cloud体系中优雅下线来为大家剖析了一个微服务实战中常见的问题及解决方案。
后端小马
Spring Cloud集成Nacos服务发现源码解析?翻了三套源码,保质保鲜!
前言前面文章我们介绍了Nacos的功能及设计架构,这篇文章就以Nacos提供的服务注册功能为主线,来讲解Nacos的客户端是如何在Spring Cloud进行集成和实现的。本会配合源码分析、流程图整理、核心API解析等维度来让大家深入浅出、系统的来学习。Spring Boot的自动注册故事要从头Spring Boot的自动注入开始。很多朋友大概都了解过Spring Boot的自动配置功能,而Spring Cloud又是基于Spring Boot框架的。因此,在学习Nacos注册业务之前,我们先来回顾一下Spring Boot的自动配置原理,这也是学习的入口。Spring Boot通过@EnableAutoConfiguration注解,将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。上述过程是通过@Import(AutoConfigurationImportSelector.class)导入的配置功能,AutoConfigurationImportSelector中的方法getCandidateConfigurations,得到待配置的class的类名集合,即所有需要进行自动配置的(xxxAutoConfiguration)类,这些类配置于META-INF/spring.factories文件中。最后,根据这些全限定名类上的注解,如:OnClassCondition、OnBeanCondition、OnWebApplicationCondition条件化的决定要不要自动配置。了解了Spring Boot的基本配置之后,我们来看看Nacos对应的自动配置在哪里。Spring Cloud中的Nacos自动配置查看Spring Cloud的项目依赖,本人引入依赖对应的jar包为spring-cloud-starter-alibaba-nacos-discovery-2021.1.jar;对应的pom依赖为:<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>查看jar包中META-INF/spring.factories文件的内容:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\
com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\
com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\
com.alibaba.cloud.nacos.NacosServiceAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration可以看到EnableAutoConfiguration类对应了一系列的Nacos自动配置类。其中NacosServiceRegistryAutoConfiguration是用来封装实例化Nacos注册流程所需组件的,装载了对三个对象NacosServiceRegistry、NacosRegistration、NacosAutoServiceRegistration,这三个对象整体都是为了Nacos服务注册使用的。@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class,
AutoServiceRegistrationAutoConfiguration.class,
NacosDiscoveryAutoConfiguration.class })
public class NacosServiceRegistryAutoConfiguration {
@Bean
public NacosServiceRegistry nacosServiceRegistry(
NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosServiceRegistry(nacosDiscoveryProperties);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosRegistration nacosRegistration(
ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers,
NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
return new NacosRegistration(registrationCustomizers.getIfAvailable(),
nacosDiscoveryProperties, context);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}
}其中NacosServiceRegistry封装的就是注册流程,它继承自ServiceRegistry:public class NacosServiceRegistry implements ServiceRegistry<Registration> {...}查看该类源码,可以看到该类中实现了服务注册、注销、关闭、设置状态、获取状态5个功能。我们要追踪的服务注册功能,便是通过它提供的register方法来实现的。至此,我们可以梳理一下Nacos客户端在Spring Cloud中集成并实例化的处理流程。 Spring Cloud的ServiceRegistry接口上面提到NacosServiceRegistry集成自ServiceRegistry,那么ServiceRegistry又是何方神圣呢?ServiceRegistry接口是Spring Cloud的类,来看一下ServiceRegistry接口的定义:public interface ServiceRegistry<R extends Registration> {
void register(R registration);
void deregister(R registration);
void close();
void setStatus(R registration, String status);
<T> T getStatus(R registration);
}可以看出ServiceRegistry接口中定义了服务注册、注销、关闭、设置状态、获取状态五个接口。如果看其他服务发现框架对Spring Cloud进行集成时,基本上都是实现的这个接口。也就是说,ServiceRegistry是Spring Cloud提供的一个服务发现框架集成的规范。对应的框架安装规范实现对应的功能即可进行集成。 我们可以看到Eureka、Zookeeper、Consul在Spring Cloud中集成也都是实现了该接口,同时,如果你需要自定义服务发现功能,也可以通过实现该接口来达到目的。NacosServiceRegistry服务注册实现暂且不关注其他的辅助类,直接来看NacosServiceRegistry#register方法,它提供了服务注册的核心业务逻辑实现。我们把该类的辅助判断去掉,直接展示最核心的代码如下:@Override
public void register(Registration registration) {
// 获取NamingService
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
// 构造实例,封装信息来源于配置属性
Instance instance = getNacosInstanceFromRegistration(registration);
// 将实例进行注册
namingService.registerInstance(serviceId, group, instance);
}上述代码中NamingService已经属于Nacos Client项目提供的API支持了。关于Nacos Client的API流程查看,可直接查看Nacos对应的源码,NamingService#registerInstance方法对应的流程图整理如下: 上述流程图还可以继续细化,这个我们在后续章节中进行专门讲解,这里大家知道大概的调用流程即可。Spring Cloud服务注册链路下面我们来梳理一下Spring Cloud是如何进行服务注册的,其中流程的前三分之二部分几乎所有的服务注册框架都是一样的流程,只有最后一部分进行实例注册时会调用具体的框架来进行实现。直接来看整个调用的链路图: 图中不同的颜色代表这不同的框架,灰色表示业务代码,浅绿色表示SpringBoot框架,深绿色表示Spring框架,浅橙色表示SpringCloud框架,其中这一部分也包含了依赖的Nacos组件部分,最后浅紫色代表着Nacos Client的包。核心流程分以下几步:第一步,SpringBoot在启动main方法时调用到Spring的核心方法refresh;第二步,在Spring中实例化了WebServerStartStopLifecycle对象。重点说一下WebServerStartStopLifecycle对象,它的start方法被调用时会发布一个ServletWebServerInitializedEvent事件类,这个事件类继承自WebServerInitializedEvent。后面用来处理服务注册的类AbstractAutoServiceRegistration同时也是一个监听器,专门用来监听WebServerInitializedEvent事件。第三步,AbstractApplicationContext的finishRefresh中会间接调用DefaultLifecycleProcessor的startBeans方法,进而调用了WebServerStartStopLifecycle的start方法。就像上面说的,触发了ServletWebServerInitializedEvent事件的发布。第四步,AbstractAutoServiceRegistration监听到对应的事件,然后基于Spring Cloud定义的ServiceRegistry接口进行服务注册。上面的描述省略了一些部分细节,但整个流程基本上就是SpringBoot在启动时发布了一个事件,Spring Cloud监听到对应的事件,然后进行服务的注册。小结为了这篇文章,肝了好几天。Spring Cloud源码、Spring Boot源码、Nacos源码都翻了个遍。最终为大家分享了Nacos或者说是Spring Cloud中服务发现的实现机制及流程。之所以写这篇文章,也是想倡导大家更多的走进源码,而不是仅仅在使用。你学到了吗?
后端小马
微服务:剖析一下源码,Nacos的健康检查竟如此简单
前言前面我们多次提到Nacos的健康检查,比如《微服务之:服务挂的太干脆,Nacos还没反应过来,怎么办?》一文中还对健康检查进行了自定义调优。那么,Nacos的健康检查和心跳机制到底是如何实现的呢?在项目实践中是否又可以参考Nacos的健康检查机制,运用于其他地方呢?这篇文章,就带大家来揭开Nacos健康检查机制的面纱。Nacos的健康检查Nacos中临时实例基于心跳上报方式维持活性,基本的健康检查流程基本如下:Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。原理很简单,关于代码层的实现,下面来就逐步来进行解析。客户端的心跳实例基于心跳上报的形式来维持活性,当然就离不开心跳功能的实现了。这里以客户端心跳实现为基准来进行分析。Spring Cloud提供了一个标准接口ServiceRegistry,Nacos对应的实现类为NacosServiceRegistry。Spring Cloud项目启动时会实例化NacosServiceRegistry,并调用它的register方法来进行实例的注册。@Override
public void register(Registration registration) {
// ...
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}catch (Exception e) {
// ...
}
}在该方法中有两处需要注意,第一处是构建Instance的getNacosInstanceFromRegistration方法,该方法内会设置Instance的元数据(metadata),通过源元数据可以配置服务器端健康检查的参数。比如,在Spring Cloud中配置的如下参数,都可以通过元数据项在服务注册时传递给Nacos的服务端。spring:
application:
name: user-service-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
heart-beat-interval: 5000
heart-beat-timeout: 15000
ip-delete-timeout: 30000其中的heart-beat-interval、heart-beat-timeout、ip-delete-timeout这些健康检查的参数,都是基于元数据上报上去的。register方法的第二处就是调用NamingService#registerInstance来进行实例的注册。NamingService是由Nacos的客户端提供,也就是说Nacos客户端的心跳本身是由Nacos生态提供的。在registerInstance方法中最终会调用到下面的方法:@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
serverProxy.registerService(groupedServiceName, groupName, instance);
}其中BeatInfo#addBeatInfo便是进行心跳处理的入口。当然,前提条件是当前的实例需要是临时(瞬时)实例。对应的方法实现如下:public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
//fix #1733
if ((existBeat = dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
dom2Beat.put(key, beatInfo);
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}在倒数第二行可以看到,客户端是通过定时任务来处理心跳的,具体的心跳请求由BeatTask完成。定时任务的执行频次,封装在BeatInfo,回退往上看,会发现BeatInfo的Period来源于Instance#getInstanceHeartBeatInterval()。该方法具体实现如下:public long getInstanceHeartBeatInterval() {
return this.getMetaDataByKeyWithDefault("preserved.heart.beat.interval", Constants.DEFAULT_HEART_BEAT_INTERVAL);
}可以看出定时任务的执行间隔就是配置的metadata中的数据preserved.heart.beat.interval,与上面提到配置heart-beat-interval本质是一回事,默认是5秒。BeatTask类具体实现如下:class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
@Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.get("clientBeatInterval").asLong();
boolean lightBeatEnabled = false;
if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.has(CommonParams.CODE)) {
code = result.get(CommonParams.CODE).asInt();
}
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
Instance instance = new Instance();
instance.setPort(beatInfo.getPort());
instance.setIp(beatInfo.getIp());
instance.setWeight(beatInfo.getWeight());
instance.setMetadata(beatInfo.getMetadata());
instance.setClusterName(beatInfo.getCluster());
instance.setServiceName(beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ex) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
}
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}在run方法中通过NamingProxy#sendBeat完成了心跳请求的发送,而在run方法的最后,再次开启了一个定时任务,这样周期性的进行心跳请求。NamingProxy#sendBeat方法实现如下:public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
}
Map<String, String> params = new HashMap<String, String>(8);
Map<String, String> bodyMap = new HashMap<String, String>(2);
if (!lightBeatEnabled) {
bodyMap.put("beat", JacksonUtils.toJson(beatInfo));
}
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
params.put("ip", beatInfo.getIp());
params.put("port", String.valueOf(beatInfo.getPort()));
String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);
return JacksonUtils.toObj(result);
}实际上,就是调用了Nacos服务端提供的"/nacos/v1/ns/instance/beat"服务。在客户端的常量类Constants中定义了心跳相关的默认参数:static {
DEFAULT_HEART_BEAT_TIMEOUT = TimeUnit.SECONDS.toMillis(15L);
DEFAULT_IP_DELETE_TIMEOUT = TimeUnit.SECONDS.toMillis(30L);
DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5L);
}这样就呼应了最开始说的Nacos健康检查机制的几个时间维度。服务端接收心跳分析客户端的过程中已经可以看出请求的是/nacos/v1/ns/instance/beat这个服务。Nacos服务端是在Naming项目中的InstanceController中实现的。@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {
// ...
Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);
if (instance == null) {
// ...
instance = new Instance();
instance.setPort(clientBeat.getPort());
instance.setIp(clientBeat.getIp());
instance.setWeight(clientBeat.getWeight());
instance.setMetadata(clientBeat.getMetadata());
instance.setClusterName(clusterName);
instance.setServiceName(serviceName);
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(clientBeat.isEphemeral());
serviceManager.registerInstance(namespaceId, serviceName, instance);
}
Service service = serviceManager.getService(namespaceId, serviceName);
// ...
service.processClientBeat(clientBeat);
// ...
return result;
}服务端在接收到请求时,主要做了两件事:第一,如果发送心跳的实例不存在,则将其进行注册;第二,调用其Service的processClientBeat方法进行心跳处理。processClientBeat方法实现如下:public void processClientBeat(final RsInfo rsInfo) {
ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();
clientBeatProcessor.setService(this);
clientBeatProcessor.setRsInfo(rsInfo);
HealthCheckReactor.scheduleNow(clientBeatProcessor);
}ClientBeatProcessor同样是一个实现了Runnable的Task,通过HealthCheckReactor定义的scheduleNow方法进行立即执行。scheduleNow方法实现:public static ScheduledFuture<?> scheduleNow(Runnable task) {
return GlobalExecutor.scheduleNamingHealth(task, 0, TimeUnit.MILLISECONDS);
}再来看看ClientBeatProcessor中对具体任务的实现:@Override
public void run() {
Service service = this.service;
// logging
String ip = rsInfo.getIp();
String clusterName = rsInfo.getCluster();
int port = rsInfo.getPort();
Cluster cluster = service.getClusterMap().get(clusterName);
List<Instance> instances = cluster.allIPs(true);
for (Instance instance : instances) {
if (instance.getIp().equals(ip) && instance.getPort() == port) {
// logging
instance.setLastBeat(System.currentTimeMillis());
if (!instance.isMarked()) {
if (!instance.isHealthy()) {
instance.setHealthy(true);
// logging
getPushService().serviceChanged(service);
}
}
}
}
}在run方法中先检查了发送心跳的实例和IP是否一致,如果一致则更新最后一次心跳时间。同时,如果该实例之前未被标记且处于不健康状态,则将其改为健康状态,并将变动通过PushService提供事件机制进行发布。事件是由Spring的ApplicationContext进行发布,事件为ServiceChangeEvent。通过上述心跳操作,Nacos服务端的实例的健康状态和最后心跳时间已经被刷新。那么,如果没有收到心跳时,服务器端又是如何判断呢?服务端心跳检查客户端发起心跳,服务器端来检查客户端的心跳是否正常,或者说对应的实例中的心跳更新时间是否正常。服务器端心跳的触发是在服务实例注册时触发的,同样在InstanceController中,register注册实现如下:@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// ...
final Instance instance = parseInstance(request);
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}ServiceManager#registerInstance实现代码如下:public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
// ...
}心跳相关实现在第一次创建空的Service中实现,最终会调到如下方法:public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
Service service = getService(namespaceId, serviceName);
if (service == null) {
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}在putServiceAndInit方法中对Service进行初始化:private void putServiceAndInit(Service service) throws NacosException {
putService(service);
service = getService(service.getNamespaceId(), service.getName());
service.init();
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}service.init()方法实现:public void init() {
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
entry.getValue().setService(this);
entry.getValue().init();
}
}HealthCheckReactor#scheduleCheck方法实现:public static void scheduleCheck(ClientBeatCheckTask task) {
futureMap.computeIfAbsent(task.taskKey(),
k -> GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
}延迟5秒执行,每5秒检查一次。在init方法的第一行便可以看到执行健康检查的Task,具体Task是由ClientBeatCheckTask来实现,对应的run方法核心代码如下:@Override
public void run() {
// ...
List<Instance> instances = service.allIPs(true);
// first set health status of instances:
for (Instance instance : instances) {
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
if (!instance.isMarked()) {
if (instance.isHealthy()) {
instance.setHealthy(false);
// logging...
getPushService().serviceChanged(service);
ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
}
}
}
}
if (!getGlobalConfig().isExpireInstance()) {
return;
}
// then remove obsolete instances:
for (Instance instance : instances) {
if (instance.isMarked()) {
continue;
}
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
// delete instance
deleteIp(instance);
}
}
}在第一个for循环中,先判断当前时间与上次心跳时间的间隔是否大于超时时间。如果实例已经超时,且为被标记,且健康状态为健康,则将健康状态设置为不健康,同时发布状态变化的事件。在第二个for循环中,如果实例已经被标记则跳出循环。如果未标记,同时当前时间与上次心跳时间的间隔大于删除IP时间,则将对应的实例删除。小结通过本文的源码分析,我们从Spring Cloud开始,追踪到Nacos Client中的心跳时间,再追踪到Nacos服务端接收心跳的实现和检查实例是否健康的实现。想必通过整个源码的梳理,你已经对整个Nacos心跳的实现有所了解。
后端小马
学习Nacos?咱先把服务搞起来,实战教程~
前言前面已经写不少Nacos相关的文章了,比如《Spring Cloud集成Nacos服务发现源码解析?翻了三套源码,保质保鲜!》,而且目前也计划写一个Spring Cloud的技术解析专栏,一个技术框架一个技术框架的为大家拆解分析原理和实现。既然拿Nacos作为一个开始,那么我们这篇文章就来补充一下Nacos Server的部署以及Nacos Client的调用,直观的了解一下Nacos都包含了什么功能。这是使用Nacos的基础,也是后续进行深度剖析的依据。强烈建议一起学习一下。Nacos Server的部署关于Nacos Server的部署,官方手册中已经进行了很详细的说明,对应链接地址(https://nacos.io/zh-cn/docs/deployment.html )。其他方式的部署我们暂且不说,我们重点说明通过源码的形式进行构建和部署,这也是学习的最好方式。Nacos部署的基本环境要求:JDK 1.8+,Maven 3.2.x+,准备好即可。从Github上下载源码:// 下载源码
git clone https://github.com/alibaba/nacos.git
// 进入源码目录
cd nacos/
// 执行编译打包操作
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
// 查看生成的jar包
ls -al distribution/target/
// 进入到打包好的文件目录,后续可执行启动
cd distribution/target/nacos-server-$version/nacos/bin经过上述命令进入到bin目录下,通常有不同环境的启动脚本:shutdown.cmd shutdown.sh startup.cmd startup.sh执行对应环境的脚本即可进行启动,参数standalone代表着单机模式运行:// Linux/Unix/Mac
sh startup.sh -m standalone
// ubuntu
bash startup.sh -m standalone
// Windows
startup.cmd -m standalone上述操作适用于打包和部署,也适用于本地启动服务,但如果是学源码,则可以直接执行console(nacos-console)中的main方法(Nacos类)即可。执行main方法启动,默认也是集群模式,可通过JVM参数来指定单机启动:-Dnacos.standalone=true如果为了方便,也可以直接在启动类的源码中直接添加该参数:@SpringBootApplication(scanBasePackages = "com.alibaba.nacos")
@ServletComponentScan
@EnableScheduling
public class Nacos {
public static void main(String[] args) {
// 通过环境变量的形式设置单机启动
System.setProperty(Constants.STANDALONE_MODE_PROPERTY_NAME, "true");
SpringApplication.run(Nacos.class, args);
}
}经过上述步骤,我们已经可以启动一个Nacos Server了,后面就来看如何使用。Nacos管理后台在启动Nacos Server时,控制台会打印如下日志信息: ,--.
,--.'|
,--,: : | Nacos
,`--.'`| ' : ,---. Running in stand alone mode, All function modules
| : : | | ' ,'\ .--.--. Port: 8848
: | \ | : ,--.--. ,---. / / | / / ' Pid: 47395
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.1.190:8848/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'通过上面的日志,可以看出启动的模式为“stand alone mode”,端口为8848,管理后台为:http://192.168.1.190:8848/nacos/index.html 。这里我们直接访问本机服务:http://127.0.0.1:8848/nacos/index.html 。 默认情况下,用户和密码都是nacos。登录成功之后,大家可以随便点点,其中包含配置管理、服务管理、权限管理、命名空间、集群管理几个板块。 可以看到默认的命名空间为public,默认的用户为nacos。此时执行一条curl命令,进行模拟服务注册:curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080'执行之后,在此查看管理后台,会发现服务列表中已经添加了一条记录。 点击这条记录的详情,可以看到更多信息。 由于我们上面是通过一个命令注册的服务,这个服务并不存在,Nacos Server会定时检查服务的健康状态。你会发现,过了一会儿这个服务就不见了。这便是Nacos Server发现服务“挂掉”了,将其移除了。 其他的类似操作,大家可以尝试着通过curl命令或客户端工具进行尝试,同时配合管理后台看对应的数据。服务发现命令:curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=nacos.naming.serviceName'发布配置命令:curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld"获取配置命令:curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test"Spring Cloud集成Nacos最后,我们再以一个简单的实例来讲Nacos集成到Spring Cloud项目当中,看是否能够将服务注册成功。关于Spring Cloud之间服务的调用,我们后面文章再专门讲解。首先,新建一个Spring Boot项目,引入Spring Cloud和Spring Cloud Alibaba的依赖,完整的pom.xml文件如下:<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud</groupId>
<artifactId>spring-cloud-alibaba-nacos</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-alibaba-nacos</name>
<description>springcloud alibaba nacos集成</description>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.4.2</spring-boot.version>
<spring-cloud.version>2020.0.0</spring-cloud.version>
<cloud-alibaba.version>2021.1</cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
其中dependencyManagement中定义了Spring Cloud和Spring Cloud Alibaba的依赖的版本信息。这里需要注意的是Spring Cloud和Spring Boot的版本之间是有限制的。这个可以在https://spring.io/projects/spring-cloud中进行查看。然后,在yml中配置nacos注册中心服务器地址:spring:
application:
name: nacos-spring-cloud-learn
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848此时,启动服务,查看Nacos Server的控制台,会发现,像前面直接执行curl命令的效果一样,成功的将服务注册到Nacos中了。小结本文通过给大家讲解如何部署Nacos服务、如何集成到SpringCloud,为后面更进一步学习SpringCloud做好准备。其中也涉及到一些实践经验和坑。
后端小马
一个实例,轻松演示Spring Cloud集成Nacos实例
前言学习一个技术框架,最快速的手段就是将其集成到项目中,体验一下它的功能。在这个过程中,你还踩到很多坑。而排坑的过程,又是一次能力的提升。前面我们写了一些列Nacos的文章,经过《学习Nacos?咱先把服务搞起来,实战教程》的介绍,我们已经可以把Nacos Server给启动起来了。这篇文章,我们就来学习一下如何将Nacos集成到Spring Cloud项目中,同时实例演示一下,基于Nacos的微服务之间的两种调用形式。集成与版本为了演示这个案例,大家首先要将Nacos Server跑起来。同时会构建两个微服务:服务提供方(Provider)和服务消费方(Consumer)。然后,通过两个服务之间的调用及配合查看Nacos Server中的注册信息来进行验证。我们知道,Nacos隶属于Spring Cloud Alibaba系列中的组件。所以,在进行集成之前,有一件事一定要注意,那就是要确保Spring Cloud、Spring Boot、Spring Cloud Alibaba版本的一致。不然发生一些莫名其妙的异常。关于版本信息可以在https://spring.io/projects/spring-cloud中进行查看。这里采用Spring Boot的版本为2.4.2,Spring Cloud采用2020.0.0、Spring Cloud Alibaba采用2021.1。如你采用其他版本,一定确保对照关系。Nacos服务提供者依赖配置创建项目Spring Boot的项目spring-cloud-alibaba-nacos-provider1,在pom文件中添加定义依赖的版本限制:<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.4.2</spring-boot.version>
<spring-cloud.version>2020.0.0</spring-cloud.version>
<cloud-alibaba.version>2021.1</cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>然后添加依赖:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>其中actuator为健康检查依赖包,nacos-discovery为服务发现的依赖包。配置文件提供者添加配置(application.yml)server:
port: 8081
spring:
application:
name: user-service-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848其中Nacos Server的地址和端口号默认是127.0.0.1:8848。name用来指定此服务的名称,消费者可通过注册的这个名称来进行请求。业务代码在编写业务代码之前,我们先来看一下提供者的启动类:// 版本不同,低版本需要明确使用@EnableDiscoveryClient注解
//@EnableDiscoveryClient
@SpringBootApplication
public class NacosProviderApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderApplication.class, args);
}
}注意上面的注释部分,此版本已经不需要@EnableDiscoveryClient注解了,而较低的版本需要添加对应的注解。下面新建一个UserController服务:@RestController
@RequestMapping("/user")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/getUserById")
public UserDetail getUserById(Integer userId) {
logger.info("查询用户信息,userId={}", userId);
UserDetail detail = new UserDetail();
if (userId == 1) {
detail.setUserId(1);
detail.setUsername("Tom");
} else {
detail.setUserId(2);
detail.setUsername("Other");
}
return detail;
}
}其中用到的实体类UserDetail为:public class UserDetail {
private Integer userId;
private String username;
// 省略getter/setter
}然后启动服务,查看Nacos Server,会发现已经成功注册。Nacos服务消费者消费者的创建与提供者基本一致,唯一不同的是调用相关的功能。创建项目创建Spring Boot项目spring-cloud-alibaba-nacos-consumer1,pom中的依赖与提供者基本一致,但还需要在它的基础上增加两个依赖:<!-- consumer需要额外添加负载均衡的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!-- 基于Feign框架进行调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>其中loadbalancer是用来做服务调用负载均衡的,如果不添加此依赖,在调用的过程中会出现如下一次:java.net.UnknownHostException: user-provider
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:196) ~[na:1.8.0_271]
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394) ~[na:1.8.0_271]
at java.net.Socket.connect(Socket.java:606) ~[na:1.8.0_271]
at java.net.Socket.connect(Socket.java:555) ~[na:1.8.0_271]而openfeign是用来实现基于feign框架的微服务调用,也就是让服务之间的调用更加方便。这个框架是可选的,如果你想基于RestTemplate方式进行调用,则不需要此框架的依赖。配置文件消费者添加配置(application.yml):spring:
application:
name: user-service-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://user-service-provider同样server-addr指定注册Nacos Server的地址和端口。而配置中定义的service-url中便用到了服务提供者的服务名称user-service-provider。业务代码关于启动类上的注解,与提供者一样,如果根据使用的版本决定是否使用@EnableDiscoveryClient注解。创建UserController:@RestController
@RequestMapping("/order")
public class UserController {
@Resource
private UserFeignService userFeignService;
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String userProvider;
@GetMapping("getUserInfo")
public UserDetail getUserInfo() {
int userId = 1;
ResponseEntity<UserDetail> result = restTemplate.getForEntity(userProvider + "/user/getUserById?userId=" + userId, UserDetail.class);
return result.getBody();
}
@GetMapping("getUserInfo1")
public UserDetail getUserInfoByFeign() {
return userFeignService.getUserById(2);
}
}上述代码中展示了两种方式的请求,其中注入的RestTemplate和getUserInfo方法是一组,注入的UserFeignService和getUserInfoByFeign方法是一组。前者是基于RestTemplate方式请求,后者是基于Feign框架的模式进行请求的。先来看基于RestTemplate方式的配置,需要先来实例化一下RestTemplate:@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}注意,这里使用了@LoadBalanced注解,RestTemplateCustomizer会给标有@LoadBalance的RestTemplate添加一个拦截器,拦截器的作用就是对请求的URI进行转换获取到具体应该请求哪个服务实例ServiceInstance。如果缺少这个注解,也会报上面提到的异常。基于Feign的模式对应的UserFeignService如下:@FeignClient(name = "user-service-provider")
public interface UserFeignService {
/**
* 基于Feign的接口调用
*
* @param userId 用户ID
* @return UserDetail
*/
@GetMapping(value = "/user/getUserById")
UserDetail getUserById(@RequestParam Integer userId);
}
其中@FeignClient通过name属性指定调用微服务的名称,下面定义的方法则对应提供者的接口。其中@FeignClient通过name属性指定调用微服务的名称,下面定义的方法则对应提供者的接口。启动服务,查看Nacos Server的注册情况。结果验证此时,本地分别请求两个URL地址:http://localhost:8080/order/getUserInfo
http://localhost:8080/order/getUserInfo1访问一下,可以成功的返回结果:// getUserInfo对应结果
{
"userId": 1,
"username": "Tom"
}
// getUserInfo1对应结果
{
"userId": 2,
"username": "Other"
}至此,Spring Cloud集成Nacos实例演示完毕,完整的源代码地址:https://github.com/secbr/spring-cloud 。小结经过上述实例,我们成功的将Nacos集成到了Spring Cloud当中。相对来说,整个过程还是比较简单的,在实践时,大家唯一需要注意的就是版本问题。Spring Cloud的不同版本,内容和用法调整较大,多参考官方文档的说明。
后端小马
你也对阅读源码感兴趣,说说我是如何阅读Nacos源码的
前言最近写了一些列的Nacos源码相关文章,很多朋友都感兴趣的在问:你最近在阅读什么源码,如何阅读源码?今天这篇文章就以Nacos源码阅读来展开聊聊。在读这篇文章的时候呢,要看你想获得什么了。因为这篇文章亦是在写如何阅读Nacos源码,也是在写如何阅读源码。不要被技术栈所束缚,要提炼属于自己的方法。看你所欲,取你所需。阅读源码的目的不清楚大家为什么要阅读源码,就聊聊个人阅读源码的目的,或许可以拿来借鉴。学习底层原理与实现阅读某一个框架的源码,最重要的目的就是更深入的学习它的底层实现及原理。这里的底层实现和原理相对来说要宏观一些,比如阅读Nacos源码我就是想知道,它是如何实现服务注册、服务发现以及那些服务实例是如何存储的。像文章《微服务的灵魂摆渡者——Nacos,来一篇原理全攻略》便是来源于此类阅读。你也可以像我一样,阅读之后绘制成流程图、架构图、数据结构图,甚至整理成文章等帮助自己学习和理解。学习优秀的代码设计这一项包含的点就太多了,比如架构设计、功能实现理念、优秀代码示范、设计模式、算法等等。凡是能看到的,比较优秀的实践,都可以学习。以Nacos为例,简单的一个实例注销的入口方法,你能看到多少值得学习的内容? 上图是我一眼看过去,代码给我最直观的感受。然后就可以对照自己项目中的代码,思考一下是否能够达到这么高的标准?是否能进行改造?再看一个Nacos Client中的例子,在Client中调用Server的API时,会涉及到重试机制和多个Server选一个进行注册的逻辑。看看Nacos是如何实现的。 暂且不说算法的优劣,看到这里是不是感觉又学到了一种实现?而且你也知道了Nacos Client在调用Server时到底是怎么处理请求重试和异常的。有意思吧。当然,这个层面还有一些更深入的,比如一致性算法等很多解决方案的内容。学习知识点的运用这个层面有一个很好的点大家一定要把握住。那就是你可能看过很多文章在写某个知识点,而且也写了一些简单的实例。但如果你没有实践的机会,或者没在大型项目中运用,看源码中的实现和思考就非常有意思了。比如Nacos中对String.intern方法的使用,就没你想象中的那么简单。而且深入思考一下,还会发现并不是每个场景都适合,只有在字符串变化不大的情况下才适合缓存到常量池中。这里再举一个Nacos中对常见知识点的运用,看看咱们思考的维度是否一样。ServiceManager中有下图这么几个成员变量: 上面的知识点可能你已经背的滚瓜烂熟了,但你见过怎么用么?你见过怎么结合起来使用吗?怎么支持分布式、高并发的场景吗?只要你仔细分析一下,阅读一下源码的实现,你就能得到答案,也算是一次实践。从源码中可学的内容太多了,我这里就不逐一讲解了,后面会逐步形成系列文章的形式把我看到的源码中的技术和思想分享给大家。如何阅读源码有了阅读源码的目标,下一步就是执行了。这里就分享一些我阅读源码的方法,不一定适合你,但可以参考和改进。这里全部以Nacos为例,后续不再做特殊说明。代码的下载开源项目可以直接拉取源代码,Nacos的源代码有两个平台可以获取:GitHub和码云。码云库作为同步,定时更新。这里采用GitHub作为源码来源,说不定啥时候还可以贡献一些代码。可以直接执行git命令拉取开源库代码:git clone git@github.com:alibaba/nacos.git但个人并不建议这样直接拉取代码,可以从nacos的仓库fork到自己的GitHub账号下。这样既可以保持与主干的同步,又可以方便的修改一些内容。项目结构下载完成之后,直接通过IDEA打开项目,待依赖类库引入完毕,可看到如下项目结构: 此时可以大概了解一下目录结构,基本都能见名知意。address:地址服务相关;api:naming和config的api抽取;auth:权限控制相关;cmdb:支持对接第三方CMDB获取CMDB数据等;client:为客户端代码;common:共用工具类;config:Nacos配置中心的实现;consistency:一致性实现;console:Nacos控制台相关实现;console-uri:控制台UI部分实现;core:属性加载,初始化,监听器相关;distribution:发布相关;example:示例;istio:对istio的支持,如k8s等;naming:Nacos的核心功能,动态服务发现;项目的启动与测试要阅读源码跟踪流程,肯定要先把项目启动起来。由于Nacos是基于Spring Boot来构建的,只需执行对应入口类的main方法即可。在console项目中,执行Nacos的main方法即可启动项目。Nacos默认启动的是集群模式,研究代码先启动单机模式即可。这里通过在main方法中添加参数指定单机模式:public class Nacos {
public static void main(String[] args) {
// 通过环境变量的形式设置单机启动
System.setProperty(Constants.STANDALONE_MODE_PROPERTY_NAME, "true");
// 通过环境变量的形式设置关闭权限校验
System.setProperty("nacos.core.auth.enabled", "false");
SpringApplication.run(Nacos.class, args);
}
}程序启动之后,在client项目中的单元测试(test)中可以看到NamingTest和ConfigTest类,直接执行单元测试中的方法即可连接刚启动的Server进行代码跟踪调试了。此时,关于不同API的操作,也可参看官方文档的说明进行调用验证了,这就不做演示了。项目流程的梳理完成了代码的下载和启动之后,那么如何来梳理源代码的业务逻辑呢?很多朋友可能会遇到一些困惑,比如看不懂,或看了就忘了,每次都得重新梳理一遍。这个种状况在初期阶段很容易出现,也算是正常情况。我的处理方式是,先将fork的代码打一个分支(branch),比如我会打一个comment的分支。在这个分支上,每看到一行代码有点难度的代码或者需要备注的代码,就会在上面添加注释。关于Nacos本人fork的代码地址https://github.com/secbr/nacos,其中comment分支中在逐步添加注释,对Nacos源码感兴趣的朋友可以看一下。 比如上图是client请求服务器进行注册的部分代码逻辑,我会将梳理过的路径通过注释的形式进行描述。这样每次看到就不用再次梳理了。当对上述逻辑看得多了,也就不会忘记了。就这样逐个逻辑,逐个类的添加注释,当享受梳理流程图或架构图时,只用将注释内容进行提炼和概况即可。再看一下client注册时,server对应的代码注释: 至此,关于阅读源码的核心部分已经完事。剩下的就是逐个业务流程的梳理,逐个技术点的学习。如果遇到无法理解的部分,最好通过debug模式跟踪一下,看看执行到此处时,具体的数据都是什么。小结个人觉得阅读源码是每个程序员必备的技能,也是站在巨人肩膀上提升自己的必要手段。只有看得更多,才知道什么是更好的写法和实现。当然,每个人现阶段的能力有限,有很多技术点或设计思想当前阶段可能无法看到,但不要紧,你也可以拿我来做个垫背的,毕竟我是计划写一个源码解析系列的。
后端小马
微服务之:服务挂的太干脆,Nacos还没反应过来,怎么办?
前言我们知道通过Nacos等注册中心可以实现微服务的治理。但引入了Nacos之后,真的就像理想中那样所有服务都由Nacos来完美的管理了吗?Too young,too simple!今天这篇文章就跟大家聊聊,当服务异常宕机,Nacos还未反应过来时,可能会发生的状况以及现有的解决方案。Nacos的健康检查故事还要从Nacos对服务实例的健康检查说起。Nacos目前支持临时实例使用心跳上报方式维持活性。Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。如果服务突然挂掉在正常业务场景下,如果关闭掉一个服务实例,默认情况下会在关闭之前主动调用注销接口,将Nacos服务端注册的实例清除掉。如果服务实例还没来得注销已经被干掉,比如正常kill一个应用,应用会处理完手头的事情再关闭,但如果使用kill -9来强制杀掉,就会出现无法注销的情况。针对这种意外情况,服务注销接口是无法被正确调用的,此时就需要健康检查来确保该实例被删除。通过上面分析的Nacos健康检查机制,我们会发现服务突然挂掉之后,会有15秒的间隙。在这段时间,Nacos服务端还没感知到服务挂掉,依旧将该服务提供给客户端使用。此时,必然会有一部分请求被分配到异常的实例上。针对这种情况,又该如何处理呢?如何确保服务不影响正常的业务呢?自定义心跳周期针对上面的问题,我们最容易想到的是解决方案就是缩短默认的健康检查时间。原本15秒才能发现服务异常,标记为不健康,那么是否可以将其缩短呢?这样错误影响的范围便可以变小,变得可控。针对此,Nacos 1.1.0之后提供了自定义心跳周期的配置。如果你基于客户端进行操作,在创建实例时,可在实例的metadata数据中进行心跳周期、健康检查过期时间及删除实例时间的配置。相关示例如下:String serviceName = randomDomainName();
Instance instance = new Instance();
instance.setIp("1.1.1.1");
instance.setPort(9999);
Map<String, String> metadata = new HashMap<String, String>();
// 设置心跳的周期,单位为毫秒
metadata.put(PreservedMetadataKeys.HEART_BEAT_INTERVAL, "3000");
// 设置心跳超时时间,单位为毫秒;服务端6秒收不到客户端心跳,会将该客户端注册的实例设为不健康:
metadata.put(PreservedMetadataKeys.HEART_BEAT_TIMEOUT, "6000");
// 设置实例删除的超时时间,单位为毫秒;即服务端9秒收不到客户端心跳,会将该客户端注册的实例删除:
metadata.put(PreservedMetadataKeys.IP_DELETE_TIMEOUT, "9000");
instance.setMetadata(metadata);
naming.registerInstance(serviceName, instance);如果是基于Spring Cloud Alibaba的项目,可通过如下方式配置:spring:
application:
name: user-service-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
heart-beat-interval: 1000 #心跳间隔。单位为毫秒。
heart-beat-timeout: 3000 #心跳暂停。单位为毫秒。
ip-delete-timeout: 6000 #Ip删除超时。单位为毫秒。在某些Spring Cloud版本中,上述配置可能无法生效。也可以直接配置metadata的数据。配置方式如下:spring:
application:
name: user-service-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
metadata:
preserved.heart.beat.interval: 1000 #心跳间隔。时间单位:毫秒。
preserved.heart.beat.timeout: 3000 #心跳暂停。时间单位:毫秒。即服务端6秒收不到客户端心跳,会将该客户端注册的实例设为不健康;
preserved.ip.delete.timeout: 6000 #Ip删除超时。时间单位:秒。即服务端9秒收不到客户端心跳,会将该客户端注册的实例删除;其中第一种配置,感兴趣的朋友可以看一下NacosServiceRegistryAutoConfiguration中相关组件的实例化。在某些版本中由于NacosRegistration和NacosDiscoveryProperties实例化的顺序问题会导致配置未生效。此时可考虑第二种配置形式。上面的配置项,最终会在NacosServiceRegistry在进行实例注册时通过getNacosInstanceFromRegistration方法进行封装:private Instance getNacosInstanceFromRegistration(Registration registration) {
Instance instance = new Instance();
instance.setIp(registration.getHost());
instance.setPort(registration.getPort());
instance.setWeight(nacosDiscoveryProperties.getWeight());
instance.setClusterName(nacosDiscoveryProperties.getClusterName());
instance.setEnabled(nacosDiscoveryProperties.isInstanceEnabled());
// 设置Metadata
instance.setMetadata(registration.getMetadata());
instance.setEphemeral(nacosDiscoveryProperties.isEphemeral());
return instance;
}其中setMetadata方法即是。通过Nacos提供的心跳周期配置,再结合自身的业务场景,我们就可以选择最适合的心跳检测机制,尽最大可能避免对业务的影响。这个方案看起来心跳周期越短越好,但这样会对Nacos服务端造成一定的压力。如果服务器允许,还是可以尽量缩短的。Nacos的保护阈值在上述配置中,我们还要结合自身的项目情况考虑一下Nacos保护阈值的配置。在Nacos中针对注册的服务实例有一个保护阈值的配置项。该配置项的值为0-1之间的浮点数。本质上,保护阈值是⼀个⽐例值(当前服务健康实例数/当前服务总实例数)。⼀般流程下,服务消费者要从Nacos获取可⽤实例有健康/不健康状态之分。Nacos在返回实例时,只会返回健康实例。但在⾼并发、⼤流量场景会存在⼀定的问题。比如,服务A有100个实例,98个实例都处于不健康状态,如果Nacos只返回这两个健康实例的话。流量洪峰的到来可能会直接打垮这两个服务,进一步产生雪崩效应。保护阈值存在的意义在于当服务A健康实例数/总实例数 < 保护阈值时,说明健康的实例不多了,保护阈值会被触发(状态true)。Nacos会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,但这样也⽐造成雪崩要好。牺牲了⼀些请求,保证了整个系统的可⽤。在上面的解决方案中,我们提到了可以自定义心跳周期,其中能够看到实例的状态会由健康、不健康和移除。这些参数的定义也要考虑到保护阈值的触发,避免雪崩效应的发生。SpringCloud的请求重试即便上面我们对心跳周期进行了调整,但在某一实例发生故障时,还会有短暂的时间出现Nacos服务没来得及将异常实例剔除的情况。此时,如果消费端请求该实例,依然会出现请求失败。为了构建更为健壮的应用系统,我们希望当请求失败的时候能够有一定策略的重试机制,而不是直接返回失败。这个时候就需要开发人来实现重试机制。在微服务架构中,通常我们会基于Ribbon或Spring Cloud LoadBalancer来进行负载均衡处理。除了像Ribbon、Feign框架自身已经支持的请求重试和请求转移功能。Spring Cloud也提供了标准的loadbalancer相关配置。关于Ribbon框架的使用我们在这里就不多说了,重点来看看Spring Cloud是如何帮我们实现的。异常模拟我们先来模拟一下异常情况,将上面讲到的先将上面的心跳周期调大,以方便测试。然后启动两个provider和一个consumer服务,负载均衡基于Spring Cloud LoadBalancer来处理。此时通过consumer进行请求,你会发现LoadBalancer通过轮训来将请求均匀的分配到两个provider上(打印日志)。此时,通过kill -9命令将其中一个provider关掉。此时,再通过consumer进行请求,会发现成功一次,失败一次,这样交替出现。解决方案我们通过Spring Cloud提供的LoadBalancerProperties配置类中定义的配置项来对重试机制进行配置,详细的配置项目可以对照该类的属性。在consumer的application配置中添加retry相关配置:spring:
application:
name: user-service-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
loadbalancer:
retry:
# 开启重试
enabled: true
# 同一实例最大尝试次数
max-retries-on-same-service-instance: 1
# 其他实例最大尝试次数
max-retries-on-next-service-instance: 2
# 所有操作开启重试(慎重使用,特别是POST提交,幂等性保障)
retry-on-all-operations: true上述配置中默认retry是开启的。max-retries-on-same-service-instance指的是当前实例尝试的次数,包括第一次请求,这里配置为1,也就是第一次请求失败就转移到其他实例了。当然也可以配置大于1的数值,这样还会在当前实例再尝试一下。max-retries-on-next-service-instance配置的转移请求其他实例时最大尝试次数。retry-on-all-operations默认为false,也就是说只支持Get请求的重试。这里设置为true支持所有的重试。既然涉及到重试,就需要保证好业务的幂等性。当进行上述配置之后,再次演示异常模拟,会发现即使服务挂掉,在Nacos中还存在,依旧可以正常进行业务处理。关于Ribbon或其他同类组件也有类似的解决方案,大家可以相应调研一下。解决方案的坑在使用Spring Cloud LoadBalancer时其实有一个坑,你可能会遇到上述配置不生效的情况。这是为什么呢?其实是因为依赖引入的问题,Spring Cloud LoadBalancer的重试机制是基于spring-retry的,如果没有引入对应的依赖,便会导致配置无法生效。而官方文档业务未给出说明。<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>另外,上述实例是基于Spring Cloud 2020.0.0版本,其他版本可能有不同的配置。小结在使用微服务的时候并不是将Spring Cloud的组件集成进去就完事了。这篇文章我们可以看到即便集成了Nacos,还会因为心跳机制来进行一些折中处理,比如调整心跳频次。同时,即便调整了心跳参数,还需要利用其它组件来兼顾请求异常时的重试和防止系统雪崩的发生。关注一下吧,持续更新微服务系列实战内容。
后端小马
要学习微服务的服务发现?先来了解一些科普知识吧
为什么要使用服务发现功能?当调用REST API 或Thrift API的服务时,我们在构建请求时通常需要知道服务实例的IP和端口。在传统应用中,服务实例的地址信息相对固定,可以从配置文件中读取。而这些地址也只是只会偶尔更新。但在现代应用程序中,往往是基于云的微服务架构,此时获取服务实例的IP和端口便是一个需要解决的难题。如下图所示:上图中,服务实例实例的IP是动态分配。同时,还面临着服务的增减、故障以及升级等变化。这对于客户端程序来说,就需要使用更精确的服务发现机制。目前,服务发现模式主要有两种:客户端发现模式和服务端发现模式。先来看一下客户端发现模式。客户端发现模式使用客户端发现模式时,客户端负责判断服务实例的可用性和请求的负载均衡。服务实例存储在注册表中,也就是说注册表是服务实例的数据库。客户端通过查询服务注册表,获得服务实例列表,然后使用负载均衡算法从中选择一个,然后发起请求。下图为这种模式的架构图:此种模式下,当服务实例启动时,会将自己的地址信息注册到服务注册表,当服务停止时从服务注册表中移除。这期间,通常使用心跳机制来定刷新服务实例的注册。Netflix Eureka就是一个服务注册表组件,它提供了基于REST API的服务实例注册和查询功能。Netflix Ribbon是一个IPC客户端,可配合Eureka实现对服务实例请求的负载均衡。客户端发现模式的优点是相对简单,除服务注册表外,不需要其他部分做改动。同时,由于客户端知道所有的可用实例,可以做出更明智的、基于特定应用场景的负载均衡决策,比如使用一致性哈希算法。这种模式的缺点是将客户端和服务注册表功能耦合在了一起,必须为每种编程语言和框架的客户端实现服务发现逻辑。服务器端发现模式另外一种服务发现模式就是服务器发现模式。下图中展示了该模式的结构:客户端通过负载均衡器向服务发起请求,负载均衡器查询服务注册表,并将请求路由到可用的服务实例。与客户端发现相比,服务实例是通过服务注册表进行注册和注销的。AWS的ELB(Elastic Load Balancer)就是服务器端发现路由器的示例。ELB通常用于负载均衡来自外网的流量,但你也可以使用ELB来负载均衡私有云(VPV)内部的流量。客户端使用DNS名称,通过ELB发送请求(Http或TCP),ELB在已注册的弹性计算云(EC2)实例或EC2容器服务(ECS)的容器之间进行负载均衡。这种实现并没有单独的服务注册表,而是将EC2实例和ECS容器注册到ELB自身上。Http服务器和负载均衡器(比如,Nginx plus和Nginx)也可以用作服务器端发现的负载均衡器。比如,使用Consul模板动态配置Nginx反向代理。Consul可以从存储在Consul服务注册表中的配置数据中定时重新生成任意配置文件。每当文件改变时,可以运行一个任意shell命令。比如,Consul模板生成一个nginx.conf文件,用于配置反向代理,然后执行命令告诉Nginx去重新加载配置。某些部署环境(例如Kubernetes和Marathon)会在集群中的每个主机上运行一个代理。这个代理扮演服务器端发现负载平衡器的角色。客户端向服务发出请求时,会通过代理进行路由,透明地将请求转发到集群中某个服务实例。服务器端发现模式最大的优点是,服务发现的实现细节从客户端抽离出来了,客户端只用发送请求到负载均衡器即可。这样就无需为每种编程语言和框架的客户端实现服务发现逻辑。而且,某些部署环境已经免费提供了该功能。当然,这种模式也有一些缺点,如果部署环境未提供负载均衡器,你还需要搭建和管理一个额外的高可用系统组件。服务注册表服务注册表是服务发现的关键,它是一个包含服务实例地址信息的数据库。服务注册表需要具有高可用性和实时更新性。客户端可以缓存从注册表获得的服务实例地址信息。但这些信息会过时,因此,服务注册表也需要是集群模式,且集群之间还需要通过协议维持一致性。Netflix Eureka是一个服务注册表组件,它提供了基于REST API形式的服务实例注册和查询功能。一个服务实例可以通过POST请求将自己注册到注册表中;可以通过PUT请求,每隔30秒刷新它的注册信息;可以通过Http的DELETE请求或超时机制来删除实例的注册信息;可以使用Http的GET请求来检索注册的服务实例。常见的服务注册表组件有:etcd、Consul、Apache Zookeeper、Nacos等。服务注册的选项服务实例必须通过注册表进行注册或注销,通常有几种不同方式来处理注册和注销。一种是服务实例自己注册,即自我注册模式;另一种是基于其他系统组件来管理服务实例的注册,即第三方注册模式。先来看一下自我注册模式。自我注册模式当使用自我注册模式时,服务实例负责在服务注册表中进行自身的注册和注销。如果需要,服务实例还需要发送心跳请求以避免因超时而被注销。下图展示了这种模式的结构图:Netflix OSS Eureka客户端就是这种模式的示例,Eureka客户端负责处理服务实例所有的注册和注销事项。在Spring Cloud项目中,实现了包括服务发现的各种模式,基于此可以很轻松的实现自动注册服务实例到Eureka。你只需在Java配置类上使用@EnableEurekaClient注解即可。自我注册模式的优点是使用起来非常简单,不需要任何其他系统组件。缺点是服务实例与服务注册表紧密耦合,需要在每种编程语言和框架中实现注册功能。另外一种方式可以让服务和注册表解耦的方式就是第三方注册模式。第三方注册模式当使用第三方注册模式时,服务实例不再负责将自己注册到服务注册表。这一功能由第三方组件作为服务注册商来处理。服务注册商通过轮询或订阅事件来跟踪实例的变化。当发现新的可用服务实例时,会将服务实例注册到服务注册表中。同时,也会注销已经停止的服务实例。下图展示了这种模式的结构:开源项目Registrator便是一个示例,它可以基于Docker容器自动注册和注销服务实例。Registrator支持多种注册表,包括etcd和Consul。NetflixOSS Prana项目是另外一个示例,它主要用于非JVM语言编写的服务,是与服务实例并行的Sidecar应用程序。Prana基于Netflix Eureka注册和注销服务实例。第三方注册模式的优点是服务与服务注册表分离,无需每种编程语言和框架的客户端实现服务注册逻辑,而是在专用服务内以集中方式处理服务实例注册。这种模式缺点是,除非部署环境提供内置服务,否则还需要额外搭建和管理一个高度可用的系统组件。总结在微服务应用程序中,服务实例运行状态会动态更改,实例会动态分配地址。因此,为了使客户端可以正常请求服务,必须使用服务发现机制。而本文正是围绕服务发现中的两种模式(客户端发现和服务器端发现)、服务注册表及其两种途径(自我注册模式和第三方注册模式)、反向代理服务器等知识点进行讲解。只有科普了以上基础知识,我们才能更好的学习和认识微服务中的服务发现功能。
后端小马
微服务的灵魂摆渡者——Nacos,来一篇原理全攻略
前言Nacos在微服务系统的服务注册和发现领域,势头迅猛是肉眼可见的。在微服务系统中,服务的注册和发现又是一个灵魂的存在。没有注册中心的存在,成百上千服务之间的调用复杂度不可想象。如果你计划或已经在使用Nacos了,但仅停留在使用层面,那这篇文章值得你一读。本文我们先从服务发现机制说起,然后讲解Nacos的基本介绍、实现原理、架构等,真正做到深入浅出的了解Nacos。服务注册与发现说起Nacos,不得不先聊聊微服务架构中的服务发现。关于服务发现其实已经在《要学习微服务的服务发现?先来了解一些科普知识吧》一文中进行了全面的讲解。我们这里再简要梳理一下。在传统应用中,一个服务A访问另外一个服务B,我们只需将服务B的服务地址和端口在服务A的静态配置文件中进行配置即可。但在微服务的架构中,这种情况就有所变化了,如下图所示:上图中,服务实例的IP是动态分配。同时,还面临着服务的增减、故障、升级等变化。这种情况,对于客户端程序来说,就需要使用更精确的服务发现机制。为了解决这个问题,于是像etcd、Consul、Apache Zookeeper、Nacos等服务注册中间件便应运而生。Nacos简介Nacos一般读作/nɑ:kəʊs/,这个名字来源于“Dynamic Naming and Configuration Service”。其中na取自“Naming”的前两个字母,co取自“Configuration”的前两个字母,而s则取自“Service”的首字母。Nacos的功能官方用一句话来进行了说明:“一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。”也就是说Nacos不仅提供了服务注册与发现功能,还提供了配置管理的功能,同时还提供了可视化的管理平台。官方文档中还提到“服务(Service)是Nacos世界的一等公民。”,也就是说在Nacos是围绕着Service转的。如果查看源码,会发现Nacos的核心API中定义了两个接口NamingService和ConfigService。服务注册与发现围绕着NamingService展开,而配置管理则围绕着ConfigService展开。官网给出了Nacos的4个核心特性:服务发现和服务健康监测、动态配置服务、动态DNS服务、服务及其元数据管理。我们主要来讲服务发现功能。Nacos的Server与ClientNacos注册中心分为Server与Client,Nacos提供SDK和openApi,如果没有SDK也可以根据openApi手动写服务注册与发现和配置拉取的逻辑。Server采用Java编写,基于Spring Boot框架,为Client提供注册发现服务与配置服务。Client支持包含了目前已知的Nacos多语言客户端及Spring生态的相关客户端。Client与微服务嵌套在一起。Nacos的DNS实现依赖了CoreDNS,其项目为nacos-coredns-plugin。该插件提供了基于CoreDNS的DNS-F客户端,开发语言为go。Nacos注册中的交互流程作为注册中心的功能来说,Nacos提供的功能与其他主流框架很类似,基本都是围绕服务实例注册、实例健康检查、服务实例获取这三个核心来实现的。 以Java版本的Nacos客户端为例,服务注册基本流程:服务实例启动将自身注册到Nacos注册中心,随后维持与注册中心的心跳;心跳维持策略为每5秒向Nacos Server发送一次心跳,并携带实例信息(服务名、实例IP、端口等);Nacos Server也会向Client主动发起健康检查,支持TCP/Http;15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例;服务消费者通过注册中心获取实例,并发起调用;其中服务发现支持两种场景:第一,服务消费者直接向注册中心发送获取某服务实例的请求,注册中心返回所有可用实例,但一般不推荐此种方式;第二、服务消费者向注册中心订阅某服务,并提交一个监听器,当注册中心中服务发生变化时,监听器会收到通知,消费者更新本地服务实例列表,以保证所有的服务均可用。Nacos数据模型关于数据模型,官网描述道:Nacos数据模型的Key由三元组唯一确定,Namespace默认是空串,公共命名空间(public),分组默认是DEFAULT_GROUP。 上面的图为官方提供的图,我们可以进一步细化拆分来看一下: 如果还无法理解,我们可以直接从代码层面来看看Namespace、Group和Service是如何存储的:/**
* Map(namespace, Map(group::serviceName, Service)).
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();也就是说Nacos服务注册表结构为:Map<namespace, Map<group::serviceName, Service>>。Nacos基于namespace的设计是为了做多环境以及多租户数据(配置和服务)隔离的。如果用户有多套环境(开发、测试、生产等环境),则可以分别建三个不同的namespace,比如上图中的dev-namespace和prod-namespace。Nacos服务领域模型在上面的数据模式中,我们可以定位到一个服务(Service)了,那么服务的模型又是如何呢?官网提供了下图: 从图中的分级存储模型可以看到,在服务级别,保存了健康检查开关、元数据、路由机制、保护阈值等设置,而集群保存了健康检查模式、元数据、同步机制等数据,实例保存了该实例的ip、端口、权重、健康检查状态、下线状态、元数据、响应时间。此时,我们忽略掉一对多的情况,整个Nacos中数据存储的关系如下图: 可以看出,整个层级的包含关系为Namespace包含多个Group、Group可包含多个Service、Service可包含多个Cluster、Cluster中包含Instance集合。对应的部分源码如下:// ServiceManager类,Map(namespace, Map(group::serviceName, Service))
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
// Service类,Map(cluster,Cluster)
private Map<String, Cluster> clusterMap = new HashMap<>();
// Cluster类
private Set<Instance> persistentInstances = new HashSet<>();
private Set<Instance> ephemeralInstances = new HashSet<>();
// Instance类
private String instanceId;
private String ip;
private int port;
private double weight = 1.0D;
//...其中,实例又分为临时实例和持久化实例。它们的区别关键是健康检查的方式。临时实例使用客户端上报模式,而持久化实例使用服务端反向探测模式。临时实例需要能够自动摘除不健康实例,而且无需持久化存储实例。持久化实例使用服务端探测的健康检查方式,因为客户端不会上报心跳,自然就不能去自动摘除下线的实例。小结我们从微服务系统中为什么使用服务发现讲起,然后介绍了Nacos、Nacos的实现机制、底层数据模型以及部分源码实现。在使用过程中除了关注服务注册与发现、健康检查之外,对于服务的数据模型中Namespace、Group、Service和Instance也需要重点关注。当理解了这些背后的工作原理,对于上层应用的整合以及配置便可以轻松运用了。
后端小马
微服务之吐槽一下Nacos日志的疯狂输出
前言目前公司系统采用Spring Cloud架构,其中服务注册和发现组件用的Nacos,最近运维抱怨说,磁盘不够用,日志增长的太快。简单排查一下,罪魁祸首竟然是Nacos。按理说Nacos作为服务注册中心,不会应该会产生太多日志的,本身涉及的服务也不多,但几天就会产生1G以上的日志,的确有点疯狂。这篇文章就聊聊Nacos的日志系统。事件背景经过排查,其中输出最多的日志为{nacos.home}/logs/access_log.yyyy-mm-dd.log格式的日志。日志中包含了微服务系统调用Nacos及集群之间通信的日志,比如心跳(/nacos/v1/ns/instance/beat)、获取服务列表(/nacos/v1/ns/instance/list)、状态检查(/nacos/v1/ns/service/status)等。我们知道Nacos是基于Spring Boot实现的,access_log日志是Spring Boot提内置的Tomcat的访问日志。关于该项日志的配置,没有保留最大天数,也没有日志大小的控制。而且随着Nacos Server与各个服务直接的心跳、获取、注册等会不停的产生访问日志,微服务越多,日志增长越快。这些日志打印会迅速占用完磁盘空间,带来资源浪费和运维成本。解决方案上述的access_log日志输出Nacos是提供了控制开关的,在Nacos的conf目录下application.properties配置文件中,默认有以下配置:#*************** Access Log Related Configurations ***************#
### If turn on the access log:
server.tomcat.accesslog.enabled=true
### The access log pattern:
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i %{Request-Source}i
### The directory of access log:
server.tomcat.basedir=可以看到,关于访问日志支持关闭、日志输出格式以及日志输出的目录。在测试环境,我们可以直接将enabled的配置项设置为false,直接关闭该日志的输出。server.tomcat.accesslog.enabled=false但在生产环境,这样操作就有一定的风险了。当关闭之后,生产出现问题时需要根据日志进行排查,就会找不到对应的日志。此时,只能通过其他方式进行处理,比如在Linux操作系统下通过编写crontab来完成日志的定时删除。对应的脚本示例如下:#!/bin/bash
logFile="/data/nacos/bin/logs/nacos_del_access.log"
# 保留14天日志
date=`date -d "$date -14 day" +"%Y-%m-%d"`
# 具体位置可调整
delFilePath="/data/nacos/bin/logs/access_log.${date}.log"
if [ ! -f "${logFile}" ];then
echo 'access log文件打印日志频繁. /etc/cron.daily/nacosDelAccessLogs.sh 会定时删除access日志文件' >>${logFile}
fi
# 日志文件存在, 则删除
if [ -f "${delFilePath}" ];then
rm -rf ${delFilePath}
curDate=`date --date='0 days ago' "+%Y-%m-%d %H:%M:%S"`
echo '['${curDate}'] 删除文件'${delFilePath} >>${logFile}
fi虽然问题解决了,但很明显并不优雅,这也是Nacos Server日志输出的问题之一。日志级别动态调整关于Nacos Server日志的输出级别,在1.1.3版本之前,同样会打印大量的日志,而且没办法动态的进行调整。在此版本之后,日志输出得到了优化,并且支持通过API的形式来进行日志级别的调整,示例如下:# 调整naming模块的naming-raft.log的级别为error:
curl -X PUT '$nacos_server:8848/nacos/v1/ns/operator/log?logName=naming-raft&logLevel=error'
# 调整config模块的config-dump.log的级别为warn:
curl -X PUT '$nacos_server:8848/nacos/v1/cs/ops/log?logName=config-dump&logLevel=warn'客户端日志业务系统集成的客户端在1.1.3版本之后,也进行了优化,避免日志大量打印(主要涉及心跳日志、轮询日志等)。在业务系统的application.yml配置文件中,可通过日志级别设置来进行控制:# 日志级别,可以指定到具体类
logging:
level:
com.alibaba.nacos: warn也可以通过启动时的JVM参数来进行控制,默认是info级别:-Dcom.alibaba.nacos.naming.log.level=warn -Dcom.alibaba.nacos.config.log.level=warn上述示例分别指定了Naming客户端和Config客户端的日志级别,适用于1.0.0及以上版本。更细的日志配置查看conf目录下的nacos-logback.xml配置,你会发现Nacos相关的日志配置项非常多,如果因项目需要进行更精细化的配置,可在此文件中进行直接配置。以naming-server对应的append配置为例,看一下默认的配置:<appender name="naming-server"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/naming-server.log</file>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/naming-server.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
<maxFileSize>1GB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>7GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<Pattern>%date %level %msg%n%n</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>这里根据自己的需要,可调整输出的日志格式、日志文件分割、日志保留日期及日志压缩等处理。小结关于Nacos的日志输出就聊这么多,整体而言相关的日志输出有些过于多了,而且在灵活配置方面还有待提升。基于目前的现状我们可以通过自定义或定时任务等配合完成日志输出与管理。
后端小马
Nacos知识汇总
学习Nacos的基础实现原理,拆解提炼Nacos源码,汇总整理Nacos相关文章,为大家提供Nacos系列内容的学习。
后端小马
学会这些Python美图技巧,就等着女朋友夸你吧
一、前言Python中有许多用于图像处理的库,像是Pillow,或者是OpenCV。而很多时候感觉学完了这些图像处理模块没有什么用,其实只是你不知道怎么用罢了。今天就给大家带了一些美图技巧,让你的图美翻全场,朋友圈赞不绝口,女朋友也夸你,富贵你好厉害啊!二、模块安装我们主要使用到OpenCV和Pillow,另外我们还会使用到wordcloud和paddlehub,我们先安装一下:pip install opencv-python
pip install pillow
python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple
pip install -i https://mirror.baidu.com/pypi/simple paddlehub
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ myqr
另外我使用的Python环境是3.7,知道这些我们就可以开始进行我们的美图之旅了。三、图片美化1、祛痘还在为痘痘烦难,不敢拍照吗?有了这个你就不用怕了(虽然有p图软件,但是大家不要揭穿我):import cv2
level = 22 # 降噪等级
img = cv2.imread('girl.jpg') # 读取原图
img = cv2.bilateralFilter(img, level, level*2, level/2) # 美颜
cv2.imwrite('result.jpg', img)
实际上,在光滑的脸蛋上,痘痘就可以视为一个噪点,而我们可以通过降噪的方式达到祛痘祛斑的效果,在OpenCV中就提供了相应的滤镜,我们只需要调用即可。原图和实现效果图对比如下:可以看到脸上的斑明显是变少了。绅士们应该可以注意到,脖子下面的皮肤光滑了许多。不过头发细节被抹除了不少。我们可以通过调节level参数,调节效果。如果想效果更好,可以结合人脸识别,进行局部的祛痘处理。2、词云——我不只是一张图其实词云已经是老生常谈了,但是作美图中的姣姣者,还是有必要列出来的,因为一张词云所能包含的信息太多了:from PIL import Image
import numpy as np
from wordcloud import WordCloud, ImageColorGenerator
# 读取背景图片
mask = np.array(Image.open('rose.png'))
# 定义词云对象
wc = WordCloud(
# 设置词云背景为白色
background_color='white',
# 设置词云最大的字体
max_font_size=30,
# 设置词云轮廓
mask=mask,
# 字体路径,如果需要生成中文词云,需要设置该属性,设置的字体需要支持中文
#font_path='msyh.ttc'
)
# 读取文本
text = open('article.txt', 'r', encoding='utf-8').read()
# 生成词云
wc.generate(text)
# 给词云上色
wc = wc.recolor(color_func=ImageColorGenerator(mask))
# 保存词云
wc.to_file('result.png')
其中article.txt为我们的词云的文本素材,而rose.png则是词云轮廓(该图片背景应该为严格的白色或者透明),原图和实现效果如下:还是非常美的。更多详细内容可以参考blog.csdn.net/ZackSock/ar…。3、风格迁移——努力变成你喜欢的样子风格迁移,顾名思义就是将某一张图片的风格迁移到另一张图片上。比如我拍了一张白天的图片,但是我想要一张夜景的图片,那我们该怎么做呢?当然是等到晚上再拍了,不过除了这个方法,我们还可以下载一张夜景图片,将夜景效果迁移到我们的原图上。风格迁移的实现需要使用深度学习才能实现,但是像我这样的菜鸡肯定是不会深度学习的啦,所以我们直接使用paddlehub中已经实现好的模型库:import cv2
import paddlehub as hub
# 加载模型库
stylepro_artistic = hub.Module(name="stylepro_artistic")
# 进行风格迁移
im = stylepro_artistic.style_transfer(
images=[{
# 原图
'content': cv2.imread("origin.jpg"),
# 风格图
'styles': [cv2.imread("style.jpg")]
}],
# 透明度
alpha = 0.1
)
# 从返回的数据中获取图片的ndarray对象
im = im[0]['data']
# 保存结果图片
cv2.imwrite('result.jpg', im)
原图风格图和效果图如下:4、图中图——每一个像素都是你这个相比上面的要复杂一些,我们需要准备图库,将这些图作素材,然后根据图片某个区域的主色调进行最适当的替换,代码如下:import os
import cv2
import numpy as np
def getDominant(im):
"""获取主色调"""
b = int(round(np.mean(im[:, :, 0])))
g = int(round(np.mean(im[:, :, 1])))
r = int(round(np.mean(im[:, :, 2])))
return (b, g, r)
def getColors(path):
"""获取图片列表的色调表"""
colors = []
filelist = [path + i for i in os.listdir(path)]
for file in filelist:
im = cv2.imdecode(np.fromfile(file, dtype=np.uint8), -1)
dominant = getDominant(im)
colors.append(dominant)
return colors
def fitColor(color1, color2):
"""返回两个颜色之间的差异大小"""
b = color1[0] - color2[0]
g = color1[1] - color2[1]
r = color1[2] - color2[2]
return abs(b) + abs(g) + abs(r)
def generate(im_path, imgs_path, box_size, multiple=1):
"""生成图片"""
# 读取图片列表
img_list = [imgs_path + i for i in os.listdir(imgs_path)]
# 读取图片
im = cv2.imread(im_path)
im = cv2.resize(im, (im.shape[1]*multiple, im.shape[0]*multiple))
# 获取图片宽高
width, height = im.shape[1], im.shape[0]
# 遍历图片像素
for i in range(height // box_size+1):
for j in range(width // box_size+1):
# 图块起点坐标
start_x, start_y = j * box_size, i * box_size
# 初始化图片块的宽高
box_w, box_h = box_size, box_size
box_im = im[start_y:, start_x:]
if i == height // box_size:
box_h = box_im.shape[0]
if j == width // box_size:
box_w = box_im.shape[1]
if box_h == 0 or box_w == 0:
continue
# 获取主色调
dominant = getDominant(im[start_y:start_y+box_h, start_x:start_x+box_w])
img_loc = 0
# 差异,同主色调最大差异为255*3
dif = 255 * 3
# 遍历色调表,查找差异最小的图片
for index in range(colors.__len__()):
if fitColor(dominant, colors[index]) < dif:
dif = fitColor(dominant, colors[index])
img_loc = index
# 读取差异最小的图片
box_im = cv2.imdecode(np.fromfile(img_list[img_loc], dtype=np.uint8), -1)
# 转换成合适的大小
box_im = cv2.resize(box_im, (box_w, box_h))
# 铺垫色块
im[start_y:start_y+box_h, start_x:start_x+box_w] = box_im
j += box_w
i += box_h
return im
if __name__ == '__main__':
# 获取色调列表
colors = getColors('表情包/')
result_im = generate('main.jpg', '表情包/', 50, multiple=5)
cv2.imwrite('C:/Users/zaxwz/Desktop/result.jpg', result_im)
关于实现,我后续会写文章详细分析。我们看看效果图:图片我们还是可以看出人物的,但是某些地方颜色不太对,这就是根据我们图库来的了。我们方法图片就能看到上面几百张小图片。(当然你放大上面的图是看不到的,因为分辨率太低)5、切换背景——带你去旅行最近大家都宅家里,照片拍了不少,可惜背景全是沙发。遇到我就是你女朋友的福气,看我如何10行代码换图片背景:from PIL import Image
import paddlehub as hub
# 加载模型
humanseg = hub.Module(name='deeplabv3p_xception65_humanseg')
# 抠图
results = humanseg.segmentation(data={'image':['xscn.jpeg']})
# 读取背景图片
bg = Image.open('bg.jpg')
# 读取原图
im = Image.open('humanseg_output/xscn.png').convert('RGBA')
im.thumbnail((bg.size[1], bg.size[1]))
# 分离通道
r, g, b, a = im.split()
# 将抠好的图片粘贴到背景上
bg.paste(im, (bg.size[0]-bg.size[1], 0), mask=a)
bg.save('xscn.jpg')
下面看看我们的效果:6、九宫格——一张照片装不下你的美很多人发照片都喜欢发九宫格,但是一般又没那么多照片,这个时候就需要用表情包占位了。对于技术宅,这种不合理的方式是绝不容许的,于是我们写下如下代码:from PIL import Image
# 读取图片
im = Image.open('xscn.jpeg')
# 宽高各除 3,获取裁剪后的单张图片大小
width = im.size[0]//3
height = im.size[1]//3
# 裁剪图片的左上角坐标
start_x = 0
start_y = 0
# 用于给图片命名
im_name = 1
# 循环裁剪图片
for i in range(3):
for j in range(3):
# 裁剪图片并保存
crop = im.crop((start_x, start_y, start_x+width, start_y+height))
crop.save(str(im_name) + '.jpg')
# 将左上角坐标的 x 轴向右移动
start_x += width
im_name += 1
# 当第一行裁剪完后 x 继续从 0 开始裁剪
start_x = 0
# 裁剪第二行
start_y += height
我们执行上面的代码后,就能生成名为1~9的图片,这些图片就是我们的九宫格图片,下面看看测试效果:不得不说,小松菜奈是真的美。7、图片二维码——冰冷的图里也饱含深情有话想说又不敢说?来试试二维码吧,小小的图饱含深情:from MyQR import myqr
myqr.run(
words='http://www.baidu.com', # 包含信息
picture='lbxx.jpg', # 背景图片
colorized=True, # 是否有颜色,如果为False则为黑白
save_name='code.png' # 输出文件名
)效果图如下:因为上面的二维码经过我的特殊处理,在你扫码的时候会发现上面是码中码中码,要扫很多遍才能获得最后结果,大家可以发挥自己的想象力,做出点有趣的东西。
后端小马
Python+Selenium 爬虫详解
前言在实现爬虫时,解决反爬问题是我们经常要面对的。如果使用传统的 Requests 模块进行爬虫,我们要详细研究请求方法、请求参数等内容。而如果我们使用 Selenium 进行爬虫,我们只需要关注用户的操作,我们可以模拟人操作浏览器。这样我们就可以减少很多应付反爬的内容。文章内容如下:下载 Web DriverSelenium 的简单使用Selenium 中的选择器Selenium 中的动作链使用 Selenium 实现自动登录使用 Selenium 实现图片爬虫下载 WebDriverSelenium 原本是用来进行网站自动化测试的工具,后来人们发现了 Selenium 在爬虫方面的潜力,因此它也能胜任一些爬虫工作。在我们使用 Selenium 时,会打开一个浏览器,然后模拟人鼠标和键盘操作,因此这种方式的爬虫难以被察觉。Selenium 支持许多浏览器,本次 Chat 将使用 Chrome 浏览器作为例子。首先我们需要下载浏览器驱动,下载地址如下:chromedriver.storage.googleapis.com/index.html下载驱动时需要对应你的浏览器版本,比如我的版本如下:可以看到版本是 87,因此我们需要找到如下链接:选择对应版本的浏览器驱动,放在 Python 的根目录下即可。接下来我们还需要下载 Selenium 模块,只需要执行下面语句就好了:pip install selenium
下面我们看看如何使用。Selenium 的简单使用下载好驱动后,我们就可以开始使用了。如果你把驱动放在 Python 的根目录下,那你可以直接运行下面的代码:from selenium.webdriver import Chrome
# 创建浏览器(浏览器驱动)
chrome = Chrome()
# 打开百度的页面
chrome.get('https://www.baidu.com')运行后程序会帮我们自动打开浏览器,并打开百度的页面。我们可以看一下每句代码的意思。首先我们需要导入需要使用的浏览器驱动类:from selenium.webdriver import Chrome因为要使用的是 Chrome,所有这里导入 Chrome。我们可以创建一个浏览器对象(驱动),任何调用 get 方法,传入页面 url。我们还可以找到百度的搜索框进行搜索:from selenium.webdriver import Chrome
# 导入 selenium 中的键
from selenium.webdriver.common.keys import Keys
# 创建一个浏览器
chrome = Chrome()
# 打开百度页面
chrome.get('https://www.baidu.com')
# 找到输入框并输入关键词,再按 ENTER
chrome.find_element_by_xpath('//*[@id="kw"]').send_keys('星际穿越', Keys.ENTER)上面的代码在后面我们会详细说,我们先看一下效果:可以看到我们打开了一个页面。但是我们是怎么找到元素的呢?其中 find_element_by_xpath 是寻找元素的操作,而 send_keys 对元素的具体操作。下面我们先来看看寻找元素的操作。Selenium 中的选择器在 Selenium 中有很多种选择器,我们可以通过 css、tag、xpath 等属性进行选择。在上面的例子种我们就是通过 xpath 进行选择。下面我们看看具体代码吧:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
chrome = Chrome()
chrome.get('https://www.baidu.com')
# 通过 xpath 查找元素
chrome.find_element_by_xpath('xpath')
# 通过 id 查找元素
chrome.find_element_by_id('id')
# 通过 class_name 查找元素
chrome.find_element_by_class_name('class')
# 通过标签名查找数据
chrome.find_element_by_tag_name('tag')
# 通过 name 属性查找数据
chrome.find_element_by_name('name')上面我们没有实际查找内容,只是写了一些方法。我们只需要在网页源码中找到需要查找的元素的属性就可以调用相应的方法。比如下面我们查看一下百度输入框的属性:可以看到输入框的 class 是 s_ipt,这样我们就可以通过 class 找到输入框:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
chrome = Chrome()
chrome.get('https://www.baidu.com')
# 通过类名找到输入框
chrome.find_element_by_class_name('s_ipt').send_keys('星际穿越', Keys.ENTER)
可以看到这次也成功了:我们还可以通过下面的方法找到元素列表:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
chrome = Chrome()
chrome.get('https://www.baidu.com')
chrome.find_elements_by_xpath('xpath')
chrome.find_elements_by_id('id')
chrome.find_elements_by_class_name('class')
chrome.find_elements_by_tag_name('tag')
chrome.find_elements_by_name('name')
这次我们只是将方法名加了个 s,但是这次返回的是元素集合,我们可以通过循环拿到单个元素的内容,比如下面的代码:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
chrome = Chrome()
chrome.get('https://www.baidu.com')
imgs = chrome.find_elements_by_tag_name('img')
for img in imgs:
print(img.get_attribute('src'))
我们找到百度页面的所有 img 标签,然后把 img 的 src 输出:https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/baiduyun@2x-e0be79e69e.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/zhidao@2x-e9b427ecc4.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/baike@2x-1fe3db7fa6.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/tupian@2x-482fc011fc.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/baobaozhidao@2x-af409f9dbe.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/wenku@2x-f3aba893c1.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/jingyan@2x-e53eac48cb.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/yinyue@2x-c18adacacb.png
https://www.baidu.com/img/fddong_e2dd633ee46695630e60156c91cda80a.gif
https://www.baidu.com/img/fddong_e2dd633ee46695630e60156c91cda80a.gif
https://www.baidu.com/img/flexible/logo/pc/result.png
https://www.baidu.com/img/flexible/logo/pc/result@2.png
https://www.baidu.com/img/flexible/logo/pc/peak-result.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/qrcode/qrcode@2x-daf987ad02.png
https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/qrcode/qrcode-hover@2x-f9b106a848.png
Process finished with exit code 0
可以看到效果达到了。不过还有许多选择方式,这里就不细讲了。Selenium 中的动作链动作链就是模仿人的动作,比如你要登录需要先输入用户名,然后输入密码,然后再点击登录按钮。这就是一系列动作,我们可以通过动作链来实现这些动作,下面我们看看一个简单的例子:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
chrome = Chrome()
chrome.get('https://www.baidu.com')
# 创建动作链
ac = ActionChains(chrome)
# 找到要操作的标签
input = chrome.find_element_by_class_name('s_ipt')
ac.move_to_element(input).send_keys('星际穿越', Keys.ENTER).perform()我们先创建了一个 ActionChians(动作链),我们需要将浏览器传入,表示对浏览器进行的一些列动作。然后调用 move_to_element 方法,把光标移动到 input 框。该方法需要一个元素对象,因此在移动光标前我们先通过 class_name 找到了 input 框,再把 input 框传入 move_to_element 方法。然后再输入内容,并点击 ENTER 键。这正好对应了我们搜索时的操作。最后我们需要执行这些操作,这需要我们调用 perform 方法。动作链中还有其它一些操作,我们可以简单看一下:# 点击元素
ac.click()
# 双击元素
ac.double_click()
# 点击鼠标右键
ac.context_click()
# 按住鼠标不放
ac.click_hold()
# 释放鼠标左键
ac.release()
# 将元素拖拽然后松开
ac.drag_and_drop()从上面的方法可以看出,我们很多操作都需要先找到元素,然后再执行。当然我们可以选择使用动作连来实现对浏览器的操作,也可以使用元素自身的方法实现对浏览器的操作,比如:# 找到元素
elem = chrome.find_element_by_class_name('name')
# 点击元素
elem.click()现在我们知道了通过动作链实现浏览器的一些操作,我们实现一个简单的爬虫。使用 Selenium 实现自动登录首先我们需要知道登录的具体步骤,我们可以看一下登录页面:可以看到并没有直接显示用户名密码的 input 框,这个时候就需要我们自己找了。在二维码下方可以看到一个账号密码登录的字样,我们点击之后发现了输入框,然后后面的操作就很简单了:找到了“账号密码登录”,并点击找到用户名密码的 input 框,并输入用户名密码点击登录(或者点击回车)下面我们用动作连来实现一个模拟登录的操作:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
chrome = Chrome()
# 打开 QQ 空间
chrome.get('https://qzone.qq.com/')
# 切换 iframe
chrome.switch_to.frame('login_frame')
# 点击登录
chrome.find_element_by_xpath('//*[@id="switcher_plogin"]').click()
# 找到用户名和密码输入框
chrome.find_element_by_xpath('//*[@id="u"]').send_keys('qq 号')
chrome.find_element_by_xpath('//*[@id="p"]').send_keys('密码', Keys.ENTER)如果你运行上面的代码会发现找不到元素 //*[@id="switcher_plogin"],我们可以查看一下源码:会发现二维码部分被包含在一个 iframe 标签里面,因此我们是无法直接找到元素的,我们需要先切换到 iframe 中才能找到我们需要的元素,于是我将代码修改为下面:from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
chrome = Chrome()
# 打开 QQ 空间
chrome.get('https://qzone.qq.com/')
# 切换 iframe
chrome.switch_to.frame('login_frame')
# 点击登录
chrome.find_element_by_xpath('//*[@id="switcher_plogin"]').click()
# 找到用户名和密码输入框
chrome.find_element_by_xpath('//*[@id="u"]').send_keys('qq 号')
chrome.find_element_by_xpath('//*[@id="p"]').send_keys('密码', Keys.ENTER)会发现这次我们成功实现了自动登录。使用 Selenium 实现图片爬虫下面我们再来看一个爬虫,我们使用 Selenium 实现图片爬虫。其实图片爬虫不是 Selenium 擅长的部分,因为 Selenium 是基于浏览器的爬虫。因此会打开浏览器,当我们爬取的页面较多时 Selenium 的效率就比较低。但是我们还是可以学习一下。我们可以把上面通过 tag_name 查找的代码修改一下:import requests
from selenium.webdriver import Chrome
chrome = Chrome()
# 打开 QQ 空间
chrome.get('https://www.fabiaoqing.com/')
imgs = chrome.find_elements_by_tag_name('img')
name = 0
for img in imgs:
url = img.get_attribute('src')
try:
with open('img/%s.jpg' % name, 'wb') as f:
f.write(requests.get(url).content)
except Exception as e:
pass
name += 1这里我们借助了 requests 模块,需要额外安装:pip install requests我们使用 request 发送请求,获取图片的二进制内容,然后写入到一个文件当中。对于 requests 的操作,我们可以详细看一下:import requests
resp = requests.get('url')
resp.content我们首先导入了 requests 模块,然后调用 get 方法,传入要请求的 url,然后服务器会给我们返回一个响应信息。其中 resp.content 就是响应的二进制数据,因为是图片文件,因此我们需要使用二进制写入的模式打开文件:with open('1.jpg', 'wb') as f运行成功后就会发现本地多了许多图片,这就是我们爬取的图片。当然我们上面的操作非常简单,只是单纯寻找 img 标签,对于更复杂的网站,我们可以分析元素结构然后找到自己需要的标签并获取 url 进行爬取。
后端小马
Python实现5毛钱特效
一、前言请务必看到最后。Python牛已经不是一天两天的事了,但是我开始也没想到,Python能这么牛。前段时间接触了一个批量抠图的模型库,而后在一些视频中找到灵感,觉得应该可以通过抠图的方式,给视频换一个不同的场景,于是就有了今天的文章。我们先看看能实现什么效果,先来个正常版的,先看看原场景:下面是我们切换场景后的样子:看起来效果还是不错的,有了这个我们就可以随意切换场景,坟头蹦迪不是梦。另外,我们再来看看另外一种效果,相比之下要狂放许多:二、实现步骤我们都知道,视频是有一帧一帧的画面组成的,每一帧都是一张图片,我们要实现对视频的修改就需要对视屏中每一帧画面进行修改。所以在最开始,我们需要获取视频每一帧画面。在我们获取帧之后,需要抠取画面中的人物。抠取人物之后,就需要读取我们的场景图片了,在上面的例子中背景都是静态的,所以我们只需要读取一次场景。在读取场景之后我们切换每一帧画面的场景,并写入新的视频。这时候我们只是生成了一个视频,我们还需要添加音频。而音频就是我们的原视频中的音频,我们读取音频,并给新视频设置音频就好了。具体步骤如下:读取视频,获取每一帧画面批量抠图读取场景图片对每一帧画面进行场景切换写入视频读取原视频的音频给新视频设置音频因为上面的步骤还是比较耗时的,所以我在视频完成后通过邮箱发送通知,告诉我视频制作完成。三、模块安装我们需要使用到的模块主要有如下几个:pillow
opencv
moviepy
paddlehub
我们都可以直接用pip安装:pip install pillow
pip install opencv-python
pip install moviepy
其中OpenCV有一些适配问题,建议选取3.0以上版本。在我们使用paddlehub之前,我们需要安装paddlepaddle:具体安装步骤可以参见官网。用paddlehub抠图参考:别再自己抠图了,Python用5行代码实现批量抠图。我们这里直接用pip安装cpu版本的:# 安装paddlepaddle
python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple
# 安装paddlehub
pip install -i https://mirror.baidu.com/pypi/simple paddlehub
有了这些准备工作就可以开始我们功能的实现了。四、具体实现我们导入如下包:import cv2 # opencv
import mail # 自定义包,用于发邮件
import math
import numpy as np
from PIL import Image # pillow
import paddlehub as hub
from moviepy.editor import *
其中Pillow和opencv导入的名称不太一样,还有就是我自定义的mail模块。另外我们还要先准备一些路径:# 当前项目根目录,系统自动获取当前目录
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "."))
# 每一帧画面保存的地址
frame_path = BASE_DIR + '\\frames\\'
# 抠好的图片位置
humanseg_path = BASE_DIR + '\\humanseg_output\\'
# 最终视频的保存路径
output_video = BASE_DIR + '\\result.mp4'
接下来我们按照上面说的步骤一个一个实现。(1)读取视频,获取每一帧画面在OpenCV中提供了读取帧的函数,我们只需要使用VideoCapture类读取视频,然后调用read函数读取帧,read方法返回两个参数,ret为是否有下一帧,frame为当前帧的ndarray对象。完整代码如下:def getFrame(video_name, save_path):
"""
读取视频将视频逐帧保存为图片,并返回视频的分辨率size和帧率fps
:param video_name: 视频的名称
:param save_path: 保存的路径
:return: fps帧率,size分辨率
"""
# 读取视频
video = cv2.VideoCapture(video_name)
# 获取视频帧率
fps = video.get(cv2.CAP_PROP_FPS)
# 获取画面大小
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
size = (width, height)
# 🦍获取帧数,用于给图片命名
frame_num = str(video.get(7))
name = int(math.pow(10, len(frame_num)))
# 读取帧,ret为是否还有下一帧,frame为当前帧的ndarray对象
ret, frame = video.read()
while ret:
cv2.imwrite(save_path + str(name) + '.jpg', frame)
ret, frame = video.read()
name += 1
video.release()
return fps, size在标🦍处,我获取了帧的总数,然后通过如下公式获取比帧数大的整十整百的数:frame_name = math.pow(10, len(frame_num))这样做是为了让画面逐帧排序,这样读取的时候就不会乱。另外我们获取了视频的帧率和分辨率,这两个参数在我们创建视频时需要用到。这里需要注意的是opencv3.0以下版本获取帧率和画面大小的写法有些许差别。(2)批量抠图批量抠图需要用到paddlehub中的模型库,代码很简单,这里就不多说了:def getHumanseg(frames):
"""
对帧图片进行批量抠图
:param frames: 帧的路径
:return:
"""
# 加载模型库
humanseg = hub.Module(name='deeplabv3p_xception65_humanseg')
# 准备文件列表
files = [frames + i for i in os.listdir(frames)]
# 抠图
humanseg.segmentation(data={'image': files})我们执行上面函数后会在项目下生成一个humanseg_output目录,抠好的图片就在里面。(3)读取场景图片这也是简单的图片读取,我们使用pillow中的Image对象:def readBg(bgname, size):
"""
读取背景图片,并修改尺寸
:param bgname: 背景图片名称
:param size: 视频分辨率
:return: Image对象
"""
im = Image.open(bgname)
return im.resize(size)这里的返回的对象并非ndarray对象,而是Pillow中定义的类对象。(4)对每一帧画面进行场景切换简单来说就是将抠好的图片和背景图片合并,我们知道抠好的图片都在humanseg_output目录,这也就是为什么最开始要准备相应的变量存储该目录的原因:def setImageBg(humanseg, bg_im):
"""
将抠好的图和背景图片合并
:param humanseg: 抠好的图
:param bg_im: 背景图片,这里和readBg()函数返回的类型一样
:return: 合成图的ndarray对象
"""
# 读取透明图片
im = Image.open(humanseg)
# 分离色道
r, g, b, a = im.split()
# 🦍复制背景,以免源背景被修改
bg_im = bg_im.copy()
# 合并图片
bg_im.paste(im, (0, 0), mask=a)
return np.array(bg_im.convert('RGB'))[:, :, ::-1]在标🦍处,我们复制了背景,如果少了这一步的话,生成的就是我们上面的“千手观音效果”了。其它步骤都很好理解,只有返回值比较长,我们来详细看一下:# 将合成图转换成RGB,这样A通道就没了
bg_im = bg_im.convert('RGB')
# 将Image对象转换成ndarray对象,方便opencv读取
im_array = np.array(bg_im)
# 此时im_array为rgb模式,而OpenCV为bgr模式,我们通过下面语句将rgb转换成bgr
bgr_im_array = im_array[:, :, ::-1]最后bgr_im_array就是我们最终的返回结果。(5)写入视频为了节约空间,我并非等将写入图片放在合并场景后面,而是边合并场景边写入视频:def writeVideo(humanseg, bg_im, fps, size):
"""
:param humanseg: png图片的路径
:param bgname: 背景图片
:param fps: 帧率
:param size: 分辨率
:return:
"""
# 写入视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('green.mp4', fourcc, fps, size)
# 将每一帧设置背景
files = [humanseg + i for i in os.listdir(humanseg)]
for file in files:
# 循环合并图片
im_array = setImageBg(file, bg_im)
# 逐帧写入视频
out.write(im_array)
out.release()上面的代码也非常简单,执行完成后项目下会生成一个green.mp4,这是一个没有音频的视频,后面就需要我们获取音频然后混流了。(6)读取原视频的音频因为在opencv中没找到音频相关的处理,所以选用moviepy,使用起来也非常方便:def getMusic(video_name):
"""
获取指定视频的音频
:param video_name: 视频名称
:return: 音频对象
"""
# 读取视频文件
video = VideoFileClip(video_name)
# 返回音频
return video.audio然后就是混流了。(7)给新视频设置音频这里同样使用moviepy,传入视频名称和音频对象进行混流:def addMusic(video_name, audio):
"""实现混流,给video_name添加音频"""
# 读取视频
video = VideoFileClip(video_name)
# 设置视频的音频
video = video.set_audio(audio)
# 保存新的视频文件
video.write_videofile(output_video)其中output_video是我们在最开始定义的变量。(8)删除过渡文件在我们生产视频时,会产生许多过渡文件,在视频合成后我们将它们删除:def deleteTransitionalFiles():
"""删除过渡文件"""
frames = [frame_path + i for i in os.listdir(frame_path)]
humansegs = [humanseg_path + i for i in os.listdir(humanseg_path)]
for frame in frames:
os.remove(frame)
for humanseg in humansegs:
os.remove(humanseg)最后就是将整个流程整合一下。(8)整合我们将上面完整的流程合并成一个函数:def changeVideoScene(video_name, bgname):
"""
:param video_name: 视频的文件
:param bgname: 背景图片
:return:
"""
# 读取视频中每一帧画面
fps, size = getFrame(video_name, frame_path)
# 批量抠图
getHumanseg(frame_path)
# 读取背景图片
bg_im = readBg(bgname, size)
# 将画面一帧帧写入视频
writeVideo(humanseg_path, bg_im, fps, size)
# 混流
addMusic('green.mp4', getMusic(video_name))
# 删除过渡文件
deleteTransitionalFiles()(9)在main中调用我们可以把前面定义的路径也放进了:if __name__ == '__main__':
# 当前项目根目录
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "."))
# 每一帧画面保存的地址
frame_path = BASE_DIR + '\\frames\\'
# 抠好的图片位置
humanseg_path = BASE_DIR + '\\humanseg_output\\'
# 最终视频的保存路径
output_video = BASE_DIR + '\\result.mp4'
if not os.path.exists(frame_path):
os.makedirs(frame_path)
try:
# 调用函数制作视频
changeVideoScene('jljt.mp4', 'bg.jpg')
# 当制作完成发送邮箱
mail.sendMail('你的视频已经制作完成')
except Exception as e:
# 当发生错误,发送错误信息
mail.sendMail('在制作过程中遇到了问题' + e.__str__())这样我们就完成了完整的流程。五、发送邮件邮件的发送又是属于另外的内容了,我定义了一个mail.py文件,具体代码如下:import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart # 一封邮件
def sendMail(msg):
#
sender = '发件人'
to_list = [
'收件人'
]
subject = '视频制作情况'
# 创建邮箱
em = MIMEMultipart()
em['subject'] = subject
em['From'] = sender
em['To'] = ",".join(to_list)
# 邮件的内容
content = MIMEText(msg)
em.attach(content)
# 发送邮件
# 1、连接服务器
smtp = smtplib.SMTP()
smtp.connect('smtp.163.com')
# 2、登录
smtp.login(sender, '你的密码或者授权码')
# 3、发邮件
smtp.send_message(em)
# 4、关闭连接
smtp.close()里面的邮箱我是直接写死了,大家可以自由发挥。为了方便,推荐发件人使用163邮箱,收件人使用QQ邮箱。另外在登录的时候直接使用密码比较方便,但是有安全隐患。最后送大家一套2020最新Pyhon项目实战视频教程,点击此处 进来获取 跟着练习下,希望大家一起进步哦!六、总结老实说上述程序的效率非常低,不仅占空间,而且耗时也比较长。在最开始我切换场景选择的是遍历图片每一个像素,而后找到了更加高效的方式取代了。但是帧画面的保存,和png图片的存储都很耗费空间。另外程序设计还是有许多不合理的地方,像是ndarray对象和Image的区分度不高,另外有些函数选择传入路径,而有些函数选择传入文件对象也很容易让人糊涂。最后说一下,我们我们用上面的方式不仅可以做静态的场景切换,还可以做动态的场景切换,这样我们就可以制作更加丰富的视频。
后端小马
用Python开发一个Web框架
一、Web框架首先我们今天要做的事是开发一个Web框架。可能听到这你就会想、是不是很难啊?这东西自己能写出来?如果你有这种疑惑的话,那就继续看下去吧。相信看完今天的内容你也能写出一个自己的Web框架。1.1、Web服务器要知道什么是Web框架首先要知道Web服务器的概念。Web服务器是一个无情的收发机器,对它来说,接收和发送是最主要的工作。在我们用浏览器打开网页时,如果不考虑复杂情况,我们可以理解为我们在向服务器要东西,而服务器接到了你的请求后,根据一些判断,再给你发送一些内容。仔细一想,其实这一个套接字(Socket)。1.2 Web框架那Web框架是什么呢?Web框架其实就是对Web服务器的一个封装,最原始的服务器只有一个原生的Socket,它可以做一些基本的工作。但是想用原生Socket做Web开发,那你的事情就多了去了。而Web框架就是对Socket的高级封装,不同的Web框架封装程度不同。像Django是封装地比较完善的一个框架,而Flask则要轻便得多。那他们只会封装Socket吗?我们接着往下看!1.3 MVC和MTV现在大多数框架都是MCV模式或者类MCV模式的。那MCV的含义是什么呢?具体含义如下:model:模型层view:视图层controller:控制层(业务逻辑层)下面我们来具体解释一下:模型很好理解,就是我们常说的类,我们通常会将模型和数据库表对应起来。视图层关注的是我们展示的页面效果,主要就是html、css、js等。控制层,其实把它称作业务逻辑层要更好理解。也就是决定我要显示什么数据。如果拿登录的业务来看。数据库中用户表对应的类就是属于模型层,我们看到的登录页面就是视图层,而我们处理判断登录的用户名密码等一系列内容就是业务逻辑层的内容。那MTV又是什么呢?其实MTV就是MCV的另一种形式,model是一样的,而T的含义是Template,对应View。比较奇怪的就是MTV中的View,它对应Controller。其实MVC和MTV没有本质区别。1.4、框架封装的内容在大多数框架中我们都不会去关注Socket本身,而更多的是去关注MTV三个部分。在本文,我们会去自己实现Template和View两个部分。Template部分很好理解,就是我们通常的html页面。但是我们最终要实现的是动态页面(页面中的数据是动态生成的),因此我们需要对传统的html页面进行一些改造。这部分的工作需要我们定义一些特征标记,以及对html进行一些渲染工作。而View部分我们会实现两个功能,一个是路由分发,另一个是视图函数。路由分发的工作就是让我们对应不同的url,响应不同的内容。比如我请求http://www.test.com/login.html会返回登录页面,如果请求http://www.test.com/register.html则返回注册页面。而视图函数则是针对每个请求的处理。后面我们会再提到。知道了上面这些知识后,我们就可以着手开发我们的Web框架了。二、实现一个Web服务器服务器是Web框架的基础,而Socket是服务器的基础。因此我们还需要了解一下Socket的使用。2.1 socket的使用在python中socket的操作封装在socket.socket类中。我们先看下面这段代码,如何再来逐一解释:import socket
# 创建一个服务端socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定ip和端口
server.bind(('127.0.0.1', 8000))
# 监听是否有请求
server.listen(1)
# 接收请求
conn, addr = server.accept()
# 接收数据
data = conn.recv(1024)
print(addr)
print(data.decode('utf-8'))
在我们做操作前,我们需要创建一个socket对象。在创建是我们传入了两个参数,他们规定了如下内容:socket.AF_INET:ipv4socket.SOCK_STREAM:TCP协议有了socket对象后,我们使用bind方法绑定ip和端口。其中127.0.0.1表示本机ip,绑定后我们就可以通过指定ip和端口访问了。因为是服务器,所以我们需要使用listen监听是否有请求,listen方法是阻塞的,他会一直等待。当监听到请求后,我们可以通过accept方法接收请求。accept方法会返回连接和请求的地址信息(ip和端口)。然后通过conn.recv就可以获取客户端发来的数据了。recv方法中传入的参数是接收的最大字节数。在网络传输过程中,数据都是二进制形式传输的,因此我们需要对数据进行解码。我们可以编写一个client来测试一下:import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
client.send("你好".encode('utf-8'))
我们依次运行服务端,和客户端。会发现客户端输出如下内容:('127.0.0.1', 49992)
你好
可以看到客户端成功将数据发送给了服务端。2.2、实现Web服务器上面只是简单看一下socket的使用,那种程度的服务器还不能满足我们网站的需求。我们来看看它有些上面问题:没有响应只能接收一个请求关于没有响应的问题很好解决,我们只需要在服务端加下面两句代码:conn.send('你好'.encode('utf-8'))
conn.close()
现在我们运行服务端,客户端你已经可以删除了。因为微软已经帮我们实现了一个客户端,就是鼎鼎大名的IE浏览器。我们打开IE浏览器在url输入:http://127.0.0.1:8000/就可以看到如下页面:可能有些人就发现了,这个其实是用utf-8编码然后用gbk解码的“你好”。这个其实就是我们编写的服务器返回的内容。但是如果你再次访问这个页面,浏览器就会无情地告诉你“无法访问此页面”。因为我们服务端已经停止了,我们可以给我们的服务器加个while循环:import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(1024)
conn.send("你好".encode('gbk'))
conn.close()
这样我们就可以一直访问了。但是实际上它还是有问题,因为它同一时间只能接收一个连接。想要可以同时接收多个连接,就需要使用多线程了,于是我我把服务端修改为如下:import socket
from threading import Thread
def connect(conn):
data = conn.recv(1024)
conn.send("你好".encode('gbk'))
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
run_server()
我们在accept部分不停地接收连接,然后开启一个线程给请求返回数据。这样我们就解决了一次请求服务器就会停止的问题了。为了方便使用,我用面向对象的方式重写了服务器的代码:import socket
from threading import Thread
class Server(socket.socket):
"""
定义服务器的类
"""
def __init__(self, ip, host, connect_num, *args, **kwargs):
super(Server, self).__init__(*args, **kwargs)
self.ip = ip
self.host = host
self.connect_num = connect_num
@staticmethod
def conn(conn):
# 获取请求参数
request = conn.recv(1024)
conn.send("你好".encode('gbk'))
conn.close()
def run(self):
"""
运行服务器
:return:
"""
# 绑定ip和端口
self.bind((self.ip, self.host))
self.listen(self.connect_num)
while True:
conn, addr = self.accept()
t = Thread(target=Server.conn, args=(conn,))
t.start()
这样我们只需要编写下面的代码就能运行我们的服务器了:import socket
from server import Server
my_server = Server('127.0.0.1', 8000, 5, socket.AF_INET, socket.SOCK_STREAM)
my_server.run()
现在我们的服务端写好了,我们再来关注一下Template和View部分。三、模板在上面我们的服务端只返回了一个简单的字符串,下面我们来看看如何让服务器返回一个html页面。3.1 返回html页面其实想要返回html非常简单,我们只需要先准备一个html页面,我们创建一个template模板,并在目录下创建index.html文件,内容如下:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Do not go gentle into that good night!</h1>
</body>
</html>
我们只写了一个h标签,然后在Server类中的conn方法做一点简单的修改:@staticmethod
def conn(conn):
# 获取请求参数
request = conn.recv(1024)
with open('template/index.html', 'rb') as f:
conn.send(f.read())
conn.close()
我们把原本发送固定字符串改成了发送从文件中读取的内容,我们再次在IE中访问http://127.0.0.1:8000/可以看到如下页面:这样我们想要返回不同的页面只需要修改html文件就好了。但是上面的方式还不能让我们动态地显示数据,因此我们还需要继续修改。3.2 模板标记想要动态显示数据,我们肯定需要对html的内容进行二次处理。为了方便我们二次处理,我们可以定义一些特殊标记,我们把它们称作【模板标记】。比如下面这个html文件:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>username: %%username%%</h1>
<h1>password: %%password%%</h1>
</body>
</html>
其中%%username%%就是我们定义的模板标记。我们只需要在服务端找到这些标记,然后替换就好了。于是我将conn方法修改为如下:@staticmethod
def conn(conn):
# 获取请求参数
request = conn.recv(1024)
html = render('template/index.html', {'username': 'zack', 'password': '123456'})
conn.send(html.encode('gbk'))
conn.close()
这次我们不再是直接把html的内容发送出去了,而是把模板的路径交由render函数进行读取并渲染。我们来看看render函数:def render(template_path, params={}):
with open(template_path, 'r') as f:
html = f.read()
# 找到所有模板标记
if params:
markers = re.findall('%%(.*?)%%', html)
for marker in markers:
tag = re.findall('%%' + marker + '%%', html)[0]
if params.get('%s' % marker):
html = html.replace(tag, params.get('%s' % marker))
else:
html = html.replace(tag, '')
return html
我们的render函数接收两个参数,分别是模板路径代码,和用来渲染的参数。我们使用正则表达式找出特殊标记,然后用对应的变量进行替换。最后再把渲染后的结果返回,我们来访问一下:http://127.0.0.0:8000/可以看到如下页面:可以看到我们渲染成功了。在我们知道如何渲染页面后,我们就可以从数据库取数据,然后再渲染到页面上了。不过这里就不再细说下去了。四、路由系统和视图函数在上面的例子中,我们都是只能返回一个页面。接下来我们就来实现一个可以根据url来返回不同页面的框架。4.1 路由系统其实路由系统就是一个url和页面的对应关系,为了方便修改,我们另外创建一个urls.py文件,内容大致如下:from views import *
urlpatterns = {
'/index': index,
'/login': login,
'/register': register,
'/error': error
}
在里面我们写了一个字典。而且还导入了一个views模块,这个模块我们稍后会创建。我们来看看它的作用,首先我们需要知道,字典的值是一个个函数。知道这点后我们就能很简单地猜测到,其实这个urlpatterns就是url和函数地对应关系。下面我们来把views模块创建一下。4.2 视图函数我们的视图视图函数通常需要一个参数,就是我们的请求内容。我们可以封装成一个request类,我为了方便就直接接收字符串:import re
def render(template_path, params={}):
with open(template_path, 'r') as f:
html = f.read()
# 找到所有模板标记
if params:
markers = re.findall('%%(.*?)%%', html)
for marker in markers:
tag = re.findall('%%' + marker + '%%', html)[0]
if params.get('%s' % marker):
html = html.replace(tag, params.get('%s' % marker))
else:
html = html.replace(tag, '')
return html
def index(request):
return render('template/index.html', {'username': 'zack', 'password': '123456'})
def login(request):
return render('template/login.html')
def register(request):
return render('template/register.html')
def error(request):
return render('template/error.html')
我们在视图函数定义了一系列函数,这样我们就可以针对不同的url发送不同的响应了。另外我把render函数移到了views模块。那我们要怎样才能让视图函数来处理不同的请求呢?这个时候我们就需要想一下谁是第一个拿到请求的。我想你应该也想到了,就是我们的Socket服务器,所有我们还要回到Server类。4.3、请求参数我们到现在还没有看到IE给我们服务器发的东西,现在我们来看一看:b'GET / HTTP/1.1\r\nAccept: text/html, application/xhtml+xml, image/jxr, */*\r\nAccept-Language: zh-Hans-CN,zh-Hans;q=0.5\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko\r\nAccept-Encoding: gzip, deflate\r\nHost: 127.0.0.1:8000\r\nConnection: Keep-Alive\r\n\r\n'
把上面的内容整理后:b'GET / HTTP/1.1
\r\n
Accept: text/html, application/xhtml+xml, image/jxr, */*
\r\n
Accept-Language: zh-Hans-CN,zh-Hans;q=0.5
\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
\r\
nAccept-Encoding: gzip, deflate
\r\n
Host: 127.0.0.1:8000
\r\n
Connection: Keep-Alive
\r\n\r\n'
可以看到它们都是由\r\n拆分的字符串,而且在第一行就有我们的请求url的信息。可能你看不出,但是经验丰富的我一眼就看出了,因为我请求的url是http://127.0.0.1:8000/,所以我们的请求url是/。知道这些后,那接下来的工作就是字符串处理了。我把Server的conn函数修改为如下:@staticmethod
def conn(conn):
# 获取请求参数
request = conn.recv(1024)
# 在request中提取method和url
method, url, _ = request.decode('gbk').split('\r\n')[0].split(' ')
# 在路由系统中找到对应的视图函数,并把请求参数传递过去
html = urls.urlpatterns.get(url)(request)
conn.send(html.encode('gbk'))
conn.close()
我们先是通过字符串分割的方式提取出url,然后在路由系统中匹配视图函数,把请求参数传递给视图函数,视图函数就会帮我们渲染一个html页面,我们把html返回给浏览器。这样我们就实现了一个相对完整的web框架了!当然,这个框架是不能使用到生成中的,大家可以通过这个案例来理解Web框架的各个部分。可能有些机智的读者尝试用Chrome或者Edge浏览器访问上面的服务器,但是却被拒绝了。因为我们的响应信息只是并没有包含响应头,Chrome认为我们响应的东西是不正规的,因此不让我们访问。大家可以尝试着自己解决一下这个问题。
后端小马
Python20行代码实现视频字符化
前言我们经常在B站上看到一些字符鬼畜视频,主要就是将一个视频转换成字符的样子展现出来。看起来是非常高端,但是实际实现起来确是非常简单,我们只需要接触opencv模块,就能很快的实现视频字符化。但是在此之前,我们先看看我们实现的效果是怎样的:上面就是截取的一部分效果图,下面开始进入我们的主题。一、OpenCV的安装及图片读取在Python中我们只需要用pip安装即可,我们在控制台执行下列语句:pip install opencv-python
安装完成就可以开始使用。我们先读取一个图片:import cv2
im = cv2.imread('jljt') # 读取图片
cv2.imshow('im', im) # 显示图片
cv2.waitKey(0) # 等待键盘输入
cv2.destroyAllWindows() # 销毁内存
首先我们使用cv2.imread方法读取图片,该方法返回一个ndarray对象。然后调用imshow方法显示图像,调用后会出现一个窗口,因为这个窗口只会出现一瞬间,所以我们调用waitKey等待输入,传入0表示无限等待。因为opencv是使用c++编写的,所以我们需要销毁内存。二、OpenCV中的一些基础操作我们将视频字符化的思路就是先将视频转换为一帧一帧的图像,然后对图像进行字符化处理,最后展示出来就是字符视频的效果了。在我们生成字符画之前,我们还要看一些OpenCV的操作。(1)灰度转换灰度处理是一个非常常用的操作,我们原始的图片是有BGR三个图层(在OpenCV中,图像是以BGR形式读取)。我们进行灰度处理直观上看就是将图片变成黑白,而本质上是将图片的三个图层通过计算,变成一个图层。而这种计算是不需要我们做的,我们只需要调用OpenCV中的函数即可:import cv2
# 读取图片
im = cv2.imread('jljt.jpg')
# 灰度转换
grey = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
效果图和原图对比如下:左边为原图,右边为灰度转换后的图像。(3)改变图片大小因为字符化后图像会比较大,所以我们需要先缩小图片,我们调用cv2.resize即可改变图像大小:import cv2
# 读取图像
im = cv2.imread('jljt.png')
# 改变图像大小
re = cv2.resize(im, (100, 40))
cv2.imshow('11', re)
cv2.waitKey(0)
cv2.destroyAllWindows()
(2)逐帧读取视频我们可以通过VideoCapture读取视频,然后调用其中的方法读取每一帧。import cv2
# 读取视频
video = cv2.VideoCapture('jljt.mp4')
# 读取帧,该方法返回两个参数,第一个为是否还有下一帧,第二个为帧的ndarray对象
ret, frame = video.read()
while ret:
# 循环读取帧
ret, frame = video.read()
有了上面的操作,我们就可以开始我们下一步的工作了。三、图片字符化对于只有一个通道的图片,我们可以把它当成一个矩形,这个矩形最小单位就是一个像素。而字符化的过程就是用字符替代像素点的过程。所以我们要遍历图像的每个像素点,但是我们应该用什么字符取代呢?我们颜色有一个参照表,而opencv将这个参数表切割成256份,代表不同的程度,我们也可以做一个参照表,不过表中的内容不是颜色,而是字符。上图为颜色表,我们可以使颜色表和字符表建立映射关系。假如字符表如下:mqpka89045321@#$%^&*()_=||||}我们可以得到下列公式:经过变换可以求得相应颜色对应字符表中的字符:这个公式不理解也没关系,只需要会用即可。下面就是我们完整的代码了:import cv2
str = 'mqpka89045321@#$%^&*()_=||||}' # 字符表
im = cv2.imread('jljt.jpg') # 读取图像
grey = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) # 灰度转换
grey = cv2.resize(grey, (50, 18)) # 缩小图像
str_img = '' # 用于装字符画
for i in grey: # 遍历每个像素
for j in i:
index = int(j / 256 * len(str)) # 获取字符坐标
str_img += str[index] # 将字符添加到字符画中
str_img += '\n'
print(str_img)
生成如下字符画:因为尺寸比较小的关系,看出来的效果不是很好,我们调节好大小就好了。四、视频转字符我们知道图片转字符,自然视频转字符就不是什么问题了,我们只需要在逐帧读取中执行图片字符化操作即可。import os
import cv2
str = 'mqpka89045321@#$%^&*()_=||||}' # 字符表
video = cv2.VideoCapture('jljt.mp4') # 读取视频
ret, frame = video.read() # 读取帧
while ret: # 逐帧读取
str_img = '' # 字符画
grey = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) # 灰度转换
grey = cv2.resize(grey, (100, 40)) # 该表大小
for i in grey: # 遍历每个像素点
for j in i:
index = int(j / 256 * len(str)) # 获取字符坐标
str_img += str[index] # 将字符添加到字符画中
str_img += '\n'
os.system('cls') # 清除上一帧输出的内容
print(str_img) # 输出字符画
ret, frame = video.read() # 读取下一帧
cv2.waitKey(5)
这样我们就会每个5毫秒执行一帧画面,在我们使用pycharm执行时,会发现并没有执行清屏操作,所以我们需要到命令行运行。最终效果就是我们的字符视频了:在选取字符表时我们需要注意主体的颜色,如果主体颜色较浅,则字符表的尾部应该为一些复杂字符,如:$%#@&。字符表头部为一些简单字符,如:-|/等。如果主体颜色较深,而背景颜色较浅,则反之。当然这没有唯一的标准,大家可以慢慢调节。感兴趣的读者,可以关注我的个人公众号:ZackSock,看到抠鼻屎的就是我没错了。
后端小马
Python使用tkinter模块实现推箱子游戏
前段时间用C语言做了个字符版的推箱子,着实是比较简陋。正好最近用到了Python,然后想着用Python做一个图形界面的推箱子。这回可没有C那么简单,首先Python的图形界面我是没怎么用过,在网上找了一大堆教材,最后选择了tkinter,没什么特别的原因,只是因为网上说的多。接下来就来和大家分享一下,主要分享两点,第一就是这个程序的实现过程,第二点就是我在编写过程中的一些思考。一、介绍这次的推箱子不同与C语言版的,首先是使用了图形界面,然后添加了背景音乐,还有就是可以应对多种不同的地图。我内置了三张地图,效果图如下:比上次的高级多了,哈哈。二、开发环境我也不知道这么取名对不对,这里主要讲的就是使用到的模块。因为Python不是我的强项,所以我只能简单说一下。首先我使用的是Python3.7,主要用了两个模块,tkinter和pygame。其中主要使用的还是tkinter,而pygame是用来播放音乐的。(因为没去了解pygame,所有界面我是用tkinter写的)。库的导入我使用的是pycharm,导入非常方便。如果使用其它软件可以考虑用pip安装模块,具体操作见博客:www.cnblogs.com/banzhen/p/i…。pip install tkinter
pip install pygame
三、原理分析1、地图地图在思想方面没有太大改变,还是和以前一样使用二维数组表示。不过我认为这样确实不是非常高效的做法,不过这个想法也是在我写完之后才有的2、移动在移动方面我修改了很多遍,先是完全按照原先的算法。这个确实也实现了,不过只能在第一关有效,在我修改地图之后发现了一系列问题,然后根据问题发现实际遇到的情况要复杂很多。因为Python是用强制缩进替代了{},所以代码在观看中会有些难度,希望大家见谅。移动的思想大致如下:/**
* 0表示空白
* 1表示墙
* 2表示人
* 3表示箱子
* 4表示终点
* 5表示已完成的箱子
* 6表示在终点上的人
*/
一、人
1、移动方向为空白
前方设置为2
当前位置为0
2、移动方向为墙
直接return
3、移动方向为终点
前面设置为6
当前位置设置为0
4、移动方向为已完成的箱子
4.1、已完成箱子前面是箱子
return
4.2、已完成箱子前面是已完成的箱子
return
4.3、已完成箱子前面是墙
return
4.4、已完成箱子前面为空白
已完成箱子前面设置3
前方位置设置为6
当前位置设置为0
4.5、已完成箱子前面为终点
已完成箱子前面设置为5
前方位置设置为6
当前位置设置为0
5、前方为箱子
5.1、箱子前方为空白
箱子前方位置设置为3
前方位置设置为2
当前位置设置为0
5.2、箱子前方为墙
return
5.3、箱子前方为箱子
return
5.4、箱子前方为已完成的箱子
return
5.5、箱子前方为终点
箱子前方位置设置为5
前方位置设置为2
当前位置设置为0
二、在终点上的人
1、移动方向为空白
前方设置为2
当前位置设置为4
2、移动方向为墙
直接return
3、移动方向为终点
前面设置为6
当前位置设置为4
4、移动方向为已完成的箱子
4.1、已完成箱子前面是箱子
return
4.2、已完成箱子前面是已完成的箱子
return
4.3、已完成箱子前面是墙
return
4.4、已完成箱子前面为空白
已完成箱子前面设置3
前方位置设置为6
当前位置设置为4
4.5、已完成箱子前面为终点
已完成箱子前面设置为5
前方位置设置为6
当前位置设置为4
5、前方为箱子
5.1、箱子前方为空白
箱子前方位置设置为3
前方位置设置为2
当前位置设置为4
5.2、箱子前方为墙
return
5.3、箱子前方为箱子
return
5.4、箱子前方为已完成的箱子
return
5.5、箱子前方为终点
箱子前方位置设置为5
前方位置设置为2
当前位置设置为4
首先,人有两种状态,人可以站在空白处,也可以站在终点处。后面我发现,人在空白处和人在终点唯一的区别是,人移动后,人原先的位置一个设置为0,即空白,一个设置为4,即终点。所以我在移动前判断人背后的东西,就可以省去一般的代码了。上面的逻辑可以改为如下:/**
* 0表示空白
* 1表示墙
* 2表示人
* 3表示箱子
* 4表示终点
* 5表示已完成的箱子
* 6表示在终点上的人
*/
if(当前位置为2):
#即人在空白处
back = 0
elif(当前位置为6):
#即人在终点处
back = 4
1、移动方向为空白 (可移动)
前方设置为2
当前位置为back
2、移动方向为墙
直接return
3、移动方向为终点 (可移动)
前面设置为6
当前位置设置为back
4、移动方向为已完成的箱子
4.1、已完成箱子前面是箱子
return
4.2、已完成箱子前面是已完成的箱子
return
4.3、已完成箱子前面是墙
return
4.4、已完成箱子前面为空白 (可移动)
已完成箱子前面设置3
前方位置设置为6
当前位置设置为back
4.5、已完成箱子前面为终点 (可移动)
已完成箱子前面设置为5
前方位置设置为6
当前位置设置为back
5、前方为箱子
5.1、箱子前方为空白 (可移动)
箱子前方位置设置为3
前方位置设置为2
当前位置设置为back
5.2、箱子前方为墙
return
5.3、箱子前方为箱子
return
5.4、箱子前方为已完成的箱子
return
5.5、箱子前方为终点 (可移动)
箱子前方位置设置为5
前方位置设置为2
当前位置设置为back
四、文件分析目录结构如下,主要有三个文件BoxGame、initGame和Painter。test文件的话就是测试用的,没有实际用处。然后讲一下各个文件的功能:BoxGame:作为游戏的主入口,游戏的主要流程就在里面。老实说我Python学习的内容比较少,对Python的面向对象不是很熟悉,所有这个流程更偏向于面向过程的思想。initGame:初始化或存储一些数据,如地图数据,人的位置,地图的大小,关卡等Painter:我在该文件里定义了一个Painter对象,主要就是用来绘制地图除此之外就是图片资源和音乐资源了。五、代码分析1、BoxGamefrom tkinter import *
from initGame import *
from Painter import Painter
from pygame import mixer
#创建界面并设置属性
#创建一个窗口
root = Tk()
#设置窗口标题
root.title("推箱子")
#设置窗口大小,当括号中为"widhtxheight"形式时,会判断为设置宽高这里注意“x”是重要标识
root.geometry(str(width*step) + "x" + str(height*step))
#设置边距, 当括号中为"+left+top"形式,会判断为设置边距
root.geometry("+400+200")
#这句话的意思是width可以改变0,height可以改变0,禁止改变也可以写成resizable(False, False)
root.resizable(0, 0)
#播放背景音乐
mixer.init()
mixer.music.load('bgm.mp3') #加载音乐
mixer.music.play() #播放音乐,歌曲播放完会自动停止
#创建一个白色的画板,参数分别是:父窗口、背景、高、宽
cv = Canvas(root, bg='white', height=height*step, width=width*step)
#绘制地图
painter = Painter(cv, map, step)
painter.drawMap()
#关联Canvas
cv.pack()
#定义监听方法
def move(event):
pass
#绑定监听事件,键盘事件第一个参数固定为"<Key>",第二个参数为方法名(不能加括号)
root.bind("<Key>", move)
#进入循环
root.mainloop()
因为move的代码比较长,就先不写出来,后面讲解。BoxGame主要流程如下:导入模块创建窗口并设置属性播放背景音乐创建画板在画板上绘制地图将画板铺到窗口上让窗口关联监听事件游戏循环了2、initGame#游戏需要的一些参数
mission = 0
mapList = [
[
[0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 4, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 1, 1, 1],
[1, 1, 1, 3, 0, 3, 4, 1],
[1, 4, 0, 3, 2, 1, 1, 1],
[1, 1, 1, 1, 3, 1, 0, 0],
[0, 0, 0, 1, 4, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0]
],
[
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 1, 0],
[1, 1, 4, 0, 3, 1, 1, 0, 1, 1],
[1, 4, 4, 3, 0, 3, 0, 0, 2, 1],
[1, 4, 4, 0, 3, 0, 3, 0, 1, 1],
[1, 1, 1, 1, 1, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 0]
],
[
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 4, 4, 1, 0, 0],
[0, 1, 1, 0, 4, 1, 1, 0],
[0, 1, 0, 0, 3, 4, 1, 0],
[1, 1, 0, 3, 0, 0, 1, 1],
[1, 0, 0, 1, 3, 3, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
],
[
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 3, 4, 4, 3, 0, 1],
[1, 2, 3, 4, 5, 0, 1, 1],
[1, 0, 3, 4, 4, 3, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
]
]
map = mapList[3]
#人背后的东西
back = 0
#地图的宽高
width, height = 0, 0
#地图中箱子的个数
boxs = 0
#地图中人的坐标
x = 0
y = 0
#画面大小
step = 30
def start():
global width, height, boxs, x, y, map
# 做循环变量
m, n = 0, 0
for i in map:
for j in i:
# 获取宽,每次内循环的次数都是一样的,只需要第一次记录width就可以了
if (n == 0):
width += 1
#遍历到箱子时箱子数量+1
if (j == 3):
boxs += 1
#当为2或者6时,为遍历到人
if (j == 2 or j == 6):
x, y = m, n
m += 1
m = 0
n += 1
height = n
start()
因为我还没有实现关卡切换,所以这里的mapList和mission没有太大用处,主要参数有一下几个:back:人背后的东西(前面分析过了)width、height:宽高boxs:箱子的个数x、y:人的坐标step:每个正方形格子的边长,因为我对Canvas绘制图片不熟悉,所以固定图片为30px因为initGame中没有定义类,所以在引用时就相当于执行了其中的代码。3、Painterfrom tkinter import PhotoImage, NW
#在用Canvas绘制图片时,图片必须是全局变量
img = []
class Painter():
def __init__(self, cv, map, step):
"""Painter的构造函数,在cv画板上,根据map画出大小为step的地图"""
#传入要拿来画的画板
self.cv = cv
#传入地图数据
self.map = map
#传入地图大小
self.step = step
def drawMap(self):
"""用来根据map列表绘制地图"""
#img列表的长度
imgLen = 0
global img
#循环变量
x, y = 0, 0
for i in self.map:
for j in list(i):
#记录实际位置
lx = x * self.step
ly = y * self.step
# 画空白处
if (j == 0):
self.cv.create_rectangle(lx, ly, lx + self.step, ly+self.step,
fill="white", width=0)
# 画墙
elif (j == 1):
img.append(PhotoImage(file="imgs/wall.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 2):
img.append(PhotoImage(file="imgs/human.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
# 画箱子
elif (j == 3):
img.append(PhotoImage(file="imgs/box.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 4):
img.append(PhotoImage(file="imgs/terminal.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 5):
img.append(PhotoImage(file="imgs/star.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
elif (j == 6):
img.append(PhotoImage(file="imgs/t_man.png"))
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
x += 1
x = 0
y += 1
这里说一下,cv的方法,这里用到了两个,一个是create_image一个是create_rectangle:#绘画矩形
cv.create_rectangle(sx, sy, ex, ey, key=value...)
1、前两个参数sx、sy(s代表start)为左上角坐标
2、后两个参数ex、ey(e代表end)表示右下角坐标
3、而后面的key=value...表示多个key=value形式的参数(顺序不固定)
如:
#填充色为红色
fill = "red"
#边框色为黑色
outline = "black"
#边框宽度为5
width = 5
具体使用例如:
#在左上角画一个边长为30,的黑色矩形
cv.create_rectangle(0, 0, 30, 30, fill="black", width=0)
然后是绘制图片:#这里要注意img必须是全局对象
self.cv.create_image(x, y, anchor=NW, img)
1、前两个参数依旧是坐标,但是这里不一定是左上角坐标,x,y默认是图片中心坐标
2、anchor=NW,设置anchor后,x,y为图片左上角坐标
3、img是一个PhotoImage对象(PhotoImage对象为tkinter中的对象),PhotoImage对象的创建如下
#通过文件路径创建PhotoImage对象
img = PhotoImage(file="img/img1.png")
因为我自己也不是非常了解,所以更细节的东西我也说不出来了。然后是实际坐标的问题,上面说的坐标都是以数组为参考。而实际绘图时,需要用具体的像素。在绘制过程中,需要绘制两种,矩形、图片。矩形:矩形需要两个坐标。当数组坐标为(1,1)时,因为单元的间隔为step(30),所以对应的像素坐标为(30, 30)。(2,2)对应(60,60),即(x*step,y*step),而终点位置为(x*step+step,y*step+step)。图片:绘制图片只需要一个坐标,左上角坐标,这个是前面一样为(x*step, y*step)。上面还有一个重要的点,我在最开始定义了img列表,用于装图片对象。开始我尝试用单个图片对象,但是在绘制图片的时候只会显示一个,后面想到用img列表代替,然后成功了。(因为我学的不是非常扎实,也解释不清楚)。在绘制图片时有以下两个步骤:#根据数组元素,创建相应的图片对象,添加到列表末尾
img.append(PhotoImage(file="imgs/wall.png"))
#在传入图片对象参数时,使用img[imgLen - 1],imgLen为列表当前长度,而imgLen-1就是最后一个元素,即刚刚创建的图片对象
self.cv.create_image(lx, ly, anchor=NW, image=img[imgLen - 1])
4、movedef move(event):
global x, y, boxs, back, mission,mapList, map
direction = event.char
#判断人背后的东西
# 在空白处的人
if (map[y][x] == 2):
back = 0 #讲back设置为空白
# 在终点上的人
elif (map[y][x] == 6):
back = 4 #将back设置为终点
#如果按的是w
if(direction == 'w'):
#获取移动方向前方的坐标
ux, uy = x, y-1
#如果前方为墙,直接return
if(map[uy][ux] == 1):
return
# 前方为空白(可移动)
if (map[uy][ux] == 0):
map[uy][ux] = 2 #将前方设置为人
# 前方为终点
elif (map[uy][ux] == 4):
map[uy][ux] = 6 #将前方设置为终点
# 前方为已完成的箱子
elif (map[uy][ux] == 5):
#已完成箱子前面为箱子已完成箱子或者墙都不能移动
if (map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5 or map[uy - 1][ux] == 1):
return
# 已完成前面为空白(可移动)
elif (map[uy - 1][ux] == 0):
map[uy - 1][ux] = 3 #箱子向前移动
map[uy][ux] = 6 #已完成箱子处原本是终点,人移动上去之后就是6了
boxs += 1 #箱子移出,箱子数量要+1
#已完成箱子前面为终点(可移动)
elif (map[uy - 1][ux] == 4):
map[uy - 1][ux] = 5 #前方的前方设置为已完成箱子
map[uy][ux] = 6 #前方的箱子处原本是终点,人移动上去后是6
# 前方为箱子
elif (map[uy][ux] == 3):
# 箱子不能移动
if (map[uy - 1][ux] == 1 or map[uy - 1][ux] == 3 or map[uy - 1][ux] == 5):
return
# 箱子前方为空白
elif (map[uy - 1][ux] == 0):
map[uy - 1][ux] = 3
map[uy][ux] = 2
# 箱子前方为终点
elif (map[uy - 1][ux] == 4):
map[uy - 1][ux] = 5
map[uy][ux] = 2
boxs -= 1
#前面只是改变了移动方向的数据,当前位置还是2或6,此时把当前位置设置为back
map[y][x] = back
#记录移动后的位置
y = uy
# 清除屏幕,并绘制地图
cv.delete("all")
painter.drawMap()
if(boxs == 0):
print("游戏结束")
这里只讲了一个方向的,因为其它方向代码非常类似也就列出来了。唯一的区别就是前方的坐标和前方的前方的坐标具体如下:向前:前方ux,uy=x,y-1,前方的前方ux,uy-1向下:前方ux,uy=x,y+1,前方的前方ux,yu+1向左:前方ux,uy=x-1,y,前方的前方ux-1,uy向右:前方ux,uy=x+1,y,前方的前方ux+1,uy六、总结因为本身对Python语言的不了解,在写博客中难免会有解释不清楚或者错误的地方,非常抱歉,希望大家见谅。
后端小马
Python生成字符视频
一、前言在之前也写过生成字符视频的文章,但是使用的是命令行窗口输出,效果不是很好,而且存在卡顿的情况。于是我打算直接生成一个mp4的字符视频。大致思路和之前一样:Python20行代码实现视频字符化。下面来看一个效果图:二、OpenCV的操作图像我们先来看一些基本操作。首先我们需要安装OpenCV,执行下面语句:pip install opencv-python
之后就可以使用了。2.1、读取和显示我们直接看代码:import cv2
# 读取图片
img = cv2.imread("1.jpg")
# 显示图片
cv2.imshow("img", img)
cv2.waitKey()
cv2.destroyAllWindows()
其中waitKey是等待输入的函数,因为imshow之后显示一瞬间,所以我们需要调用它。而destroyAllWindows是释放窗口。2.2、灰度转换灰度转换就是将图片转换成黑白图片(灰色),这样可以方便我们处理像素。代码如下:import cv2
img = cv2.imread("1.jpg")
# 灰度转换
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
我们还可以直接以灰度形式读入:import cv2
# 以灰度形式读入
img = cv2.imread("1.jpg", 0)
2.4、获取图片尺寸并修改尺寸我们直接看代码:import cv2
img = cv2.imread("1.jpg", 0)
# 获取图片的高宽
h, w = img.shape
# 缩放图片
res = cv2.resize(img, (w//2, h//2))
因为img的shape属性是一个元组,所以我们可以直接自动拆包。然后调用cv2.resize函数,第一个参数传入图片,第二个参数传入修改后的尺寸。2.5、绘制文字绘制文字我们需要调用cv2.putText函数,代码如下:import cv2
img = cv2.imread('1.jpg')
# 绘制文字
cv2.putText(
# 背绘制的图片
img,
# 要绘制的文字
'Hello',
# 文字左下角的坐标
(100, 500),
# 字体
cv2.FONT_HERSHEY_SIMPLEX,
# 字体大小缩放
20,
# 文字颜色
(0, 0, 0),
# 文字粗细
10
)
我们只需要注意这些参数就好了。2.6、读取视频读取视频的操作一般是通用的,代码如下:import cv2
# 读取视频
cap = cv2.VideoCapture('1.mp4')
# 获取视频的帧率
fps = cap.get(cv2.CAP_PROP_FPS)
# 循环读取图片的每一帧
while True:
# 读取下一帧
ret, frame = cap.read()
if not ret:
break
else:
pass
cap.release()
上面我们获取的视频的帧,在写入视频的时候我们需要用到。2.7、写入视频写入视频的操作也是常规代码:import cv2
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter('11.mp4', fourcc, fps, (w, h))
# 写入视频
writer.write(frame)
***
write.release()
有了这些知识,我们就可以开始下一步工作了。三、像素映射成字符对于只有一个通道的图片,我们可以把它当成一个矩形,这个矩形最小单位就是一个像素。而字符化的过程就是用字符替代像素点的过程。所以我们要遍历图像的每个像素点,但是我们应该用什么字符取代呢?我们颜色有一个参照表,而opencv将这个参数表切割成256份,代表不同的程度,我们也可以做一个参照表,不过表中的内容不是颜色,而是字符。上图为颜色表,我们可以使颜色表和字符表建立映射关系。假如字符表如下:mqpka89045321@#$%^&*()_=||||}我们可以得到下列公式:经过变换可以求得相应颜色对应字符表中的字符:这个公式不理解也没关系,只需要会用即可。下面就是我们像素转字符的代码:def pixel2char(pixel):
char_list = "@#$%&erytuioplkszxcv=+---. "
index = int(pixel / 256 * len(char_list))
return char_list[index]
这个字符表是可以自己定义的。四、生成字符图片现在我们只需要将像素逐个转换成字符就好了,代码如下:def get_char_img(img, scale=4, font_size=5):
# 调整图片大小
h, w = img.shape
re_im = cv2.resize(img, (w//scale, h//scale))
# 创建一张图片用来填充字符
char_img = np.ones((h//scale*font_size, w//scale*font_size), dtype=np.uint8)*255
font = cv2.FONT_HERSHEY_SIMPLEX
# 遍历图片像素
for y in range(0, re_im.shape[0]):
for x in range(0, re_im.shape[1]):
char_pixel = pixel2char(re_im[y][x])
cv2.putText(char_img, char_pixel, (x*font_size, y*font_size), font, 0.5, (0, 0, 0))
return char_img
这里我们使用了一个np.ones函数,它的作用我们理解为生成一个黑色图片。生成的尺寸我们先除了scale,如何再乘font_size。scale是原图的缩小程度,因为像素有很多,所以我们需要先把图片缩小。而为了让我们的字体显示更清楚,我们需要把生成的字符图片放大。因此需要注意,虽然我们生成的图片看起来单调,但是当font_size设置为5时,得到的图片已经比较大了。因此当你生成长时间的视频时,会花费比较多的时间,生成的视频也比较大。我们来测试一下上面的函数:import cv2
import numpy as np
def pixel2char(pixel):
char_list = "@#$%&erytuioplkszxcv=+---. "
index = int(pixel / 256 * len(char_list))
return char_list[index]
def get_char_img(img, scale=4, font_size=5):
# 调整图片大小
h, w = img.shape
re_im = cv2.resize(img, (w//scale, h//scale))
# 创建一张图片用来填充字符
char_img = np.ones((h//scale*font_size, w//scale*font_size), dtype=np.uint8)*255
font = cv2.FONT_HERSHEY_SIMPLEX
# 遍历图片像素
for y in range(0, re_im.shape[0]):
for x in range(0, re_im.shape[1]):
char_pixel = pixel2char(re_im[y][x])
cv2.putText(char_img, char_pixel, (x*font_size, y*font_size), font, 0.5, (0, 0, 0))
return char_img
if __name__ == '__main__':
img = cv2.imread('dl.jpg', 0)
res = get_char_img(img)
cv2.imwrite('d.jpg', res)
效果如下:可以看到效果还是很不错的。五、生成字符视频有了上面的代码,我们就可以对整个视频进行转换了。将视频转换成字符视频的代码如下:def generate(input_video, output_video):
# 1、读取视频
cap = cv2.VideoCapture(input_video)
# 2、获取视频帧率
fps = cap.get(cv2.CAP_PROP_FPS)
# 读取第一帧,获取转换成字符后的图片的尺寸
ret, frame = cap.read()
char_img = get_char_img(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), 4)
# 创建一个VideoWriter,用于保存视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(output_video, fourcc, fps, (char_img.shape[1], char_img.shape[0]))
while ret:
# 读取视频的当前帧,如果没有则跳出循环
ret, frame = cap.read()
if not ret:
break
# 将当前帧转换成字符图
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
char_img = get_char_img(gray, 4)
# 转换成BGR模式,便于写入视频
char_img = cv2.cvtColor(char_img, cv2.COLOR_GRAY2BGR)
writer.write(char_img)
writer.release()
下面是卡卡西经典战役的字符视频片段:完整代码如下:import cv2
import numpy as np
def pixel2char(pixel):
char_list = "@#$%&erytuioplkszxcv=+---. "
index = int(pixel / 256 * len(char_list))
return char_list[index]
def get_char_img(img, scale=4, font_size=5):
# 调整图片大小
h, w = img.shape
re_im = cv2.resize(img, (w//scale, h//scale))
# 创建一张图片用来填充字符
char_img = np.ones((h//scale*font_size, w//scale*font_size), dtype=np.uint8)*255
font = cv2.FONT_HERSHEY_SIMPLEX
# 遍历图片像素
for y in range(0, re_im.shape[0]):
for x in range(0, re_im.shape[1]):
char_pixel = pixel2char(re_im[y][x])
cv2.putText(char_img, char_pixel, (x*font_size, y*font_size), font, 0.5, (0, 0, 0))
return char_img
def generate(input_video, output_video):
# 1、读取视频
cap = cv2.VideoCapture(input_video)
# 2、获取视频帧率
fps = cap.get(cv2.CAP_PROP_FPS)
# 读取第一帧,获取转换成字符后的图片的尺寸
ret, frame = cap.read()
char_img = get_char_img(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), 4)
# 创建一个VideoWriter,用于保存视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(output_video, fourcc, fps, (char_img.shape[1], char_img.shape[0]))
while ret:
# 读取视频的当前帧,如果没有则跳出循环
ret, frame = cap.read()
if not ret:
break
# 将当前帧转换成字符图
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
char_img = get_char_img(gray, 4)
# 转换成BGR模式,便于写入视频
char_img = cv2.cvtColor(char_img, cv2.COLOR_GRAY2BGR)
writer.write(char_img)
writer.release()
if __name__ == '__main__':
generate('in.mp4', 'out.mp4')
我们只需要修改generate的参数就好了。
后端小马
Python读取excel中的图片
一、读取excel文件我们先来看看如何读取excel文件,读取excel文件的方式很多。这里选择openpyxl模块,安装语句如下:pip install openpyxl我们还需要用到一些其它模块,具体如下:pip install pyzbar
pip install pillow
pip install numpy下面我们就可以开始操作了。在Excel中,有工作簿、表、单元等。这里简单说一下,工作簿就是一个excel文件,表的话就是我们excel左下角切换的sheet1、sheet2,单元就是一个格子。下面我们来读取一个excel文件:from openpyxl import load_workbook
# 加载excel
wb = load_workbook("111.xlsx")
# 切换到第一张表
ws = wb[wb.sheetnames[0]]
# 获取A3单元
cell = ws['A3']
# 输出A3单元的值
print(cell.value)openpyxl的更多操作可以看看官方的文档openpyxl.readthedocs.io/en/stable/t…。二、读取excel中的图片读取excel中的图片有多种方式,本文会分享两种方式。(1)使用zipfile模块excel本身是一个压缩文件,我们把excel的后缀改成zip后,手动解压就会看到在xl/media目录下有一些图片文件,这些图片就是excel种插入的图片。因此我们就可以通过解压的方式读取excel种的图片,具体代码如下:import os
from zipfile import ZipFile
# 解压目录
unzip_path = "./unzip"
if not os.path.exists(unzip_path):
os.mkdir(unzip_path)
with ZipFile("111.xlsx") as f:
for file in f.namelist():
# 解压图片部分的文件
if file.startswith("xl/media"):
f.extract(file, path=unzip_path)详细讲解可以参考blog.csdn.net/ZackSock/ar…。(2)使用openpyxl读取上面的操作可以获取excel中的图片,但是有个缺点。就是我们不知道哪个图片来自哪个单元,在有些情况下知道图片来自哪个单元是很有比较的。下面我们就来解决这个问题:from openpyxl import load_workbook
wb = load_workbook("111.xlsx")
ws = wb[wb.sheetnames[0]]
# 遍历表中所有托
for image in ws._images:
print(image)我们先读取了一个表,然后调用_images获取表中的所有图片。但是这个图片我们还不能操作,具体对图片的操作我们下一节再看。我们先看看怎么知道图片来自哪个单元,我们可以输出图片的anchor._from:from openpyxl import load_workbook
wb = load_workbook("111.xlsx")
ws = wb[wb.sheetnames[0]]
for image in ws._images:
# 输出图片的位置信息
print(image.anchor._from)具体输入内容如下:<openpyxl.drawing.spreadsheet_drawing.AnchorMarker object>
Parameters:
col=0, colOff=1, row=0, rowOff=1其中col表示行号,row表示列号。根据这些信息,我们就可以知道图片的单元了。比如col=0,row=0,表示的应该是A1单元。如果col=1,row=1,表示的应该是B2单元。三、对读取的图片进行处理对图片处理的操作有很多,这里要看具体需要。这里我分享一下把excel中图片转换成pillow图片和ndarray对象的操作。转换后,我们就可以用numpy和pillow对图片进行各种操作。import numpy as np
from PIL import Image
from openpyxl import load_workbook
wb = load_workbook("111.xlsx")
ws = wb[wb.sheetnames[0]]
for image in ws._images:
# 将图片转换成Pillow中的图片对象
img = Image.open(image.ref).convert("RGB")
# 将Pillow中的图片对象转换成ndarray数组
img = np.array(img)如果我们excel中的图片是二维码,我们就可以进行下面的操作:import numpy as np
from PIL import Image
from pyzbar import pyzbar
from openpyxl import load_workbook
wb = load_workbook("111.xlsx")
ws = wb[wb.sheetnames[0]]
for image in ws._images:
# 转换成容易操作的图片对象
img = Image.open(image.ref).convert("RGB")
img = np.array(img)
# 解析二维码
data = pyzbar.decode(img)
if data:
text = data[0].data.decode('utf-8')
print(text)
else:
print("未识别到内容")今天的内容就到这里。
后端小马
代码解放双手,用 Python 控制你的输入设备
前言Python 中提供了很多模块可以用于控制输入设备,像是传统的 win32gui,或者是用于游戏开发的 Pygame。其中 win32gui 更贴切的说是基于 Windows 的编程,它的操作丰富多样,可以获取每个窗口,也可以获取窗口的题柄等。而 Pygame 的长处在于 2D 游戏的开发。而今天要讲的 pynput 则不同,它操作非常简单,而且里面包含的内容也更贴切输入设备,其中非常重要的两个模块就是 mouse 和 keyboard,分别提供了控制鼠标和键盘的类,下面我们就来看看一些具体操作。1. 控制鼠标我们先来安装这个模块,安装起来非常简单,我们直接使用 pip 安装:pip install pynput接下来就可以使用该模块了。我们导入 mouse 模块:from pynput import mouse在 mouse 模块中提供了一个 Controller 类,该类就是我们的鼠标控制器,我们创建该类的对象就可以鼠标键盘:from pynput import mouse
# 创建一个鼠标
m = mouse.Controller()获取了鼠标对象后,我们就可以获取一些属性,或者进行一些操作。1.1 获取鼠标位置我们可以获取鼠标的位置信息,也就是当前鼠标所在的坐标:from pynput import mouse
# 创建一个鼠标
m = mouse.Controller()
# 输出鼠标的位置
print(m.position)输出结果为一个元组。1.2 定位鼠标我们也可以直接修改鼠标的位置:from pynput import mouse
# 创建鼠标
m = mouse.Controller()
# 将鼠标移动到左上角
m.position = (0, 0)这种方式是直接定位鼠标,我们还可以根据当前位置移动鼠标。1.3 移动鼠标移动鼠标调用的是 move 函数:from pynput import mouse
# 创建鼠标
m = mouse.Controller()
# 将鼠标移动到左上角
m.move(50, -50)第一个参数为 x 移动的值,第二个参数为 y 移动的值。另外一般鼠标上都会有三个控制按钮,左键、右键和滚轮,下面我们看看如何操作它们。1.4 点击鼠标我们点击按钮时都会先按下按钮,然后再松开按钮:from pynput import mouse
# 创建鼠标
m = mouse.Controller()
# 按下鼠标右键
m.press(mouse.Button.right)
# 松下鼠标右键
m.release(mouse.Button.right)在 mouse 提供了 Button 类,里面内置了左键和右键的常量,我们直接使用就可以了。除了上面的方法,我们还可以直接调用 click 方法,点击鼠标:from pynput import mouse
# 创建鼠标
m = mouse.Controller()
# 点击鼠标左键
m.click(mouse.Button.left)1.5 双击鼠标双击也是个非常常用的操作,我们同样可以使用 click 方法:from pynput import mouse
# 创建鼠标
m = mouse.Controller()
# 点击鼠标左键
m.click(mouse.Button.left, 2)click 方法接收两个参数,第一个为按钮,第二个为非必选参数,含义为点击的次数。1.6 滚动滚轮对于像 Excel 表这种大型的表格,我们经常需要上下左右滚动,而 mouse 模块中就提供了这样的方法:from pynput import mouse
# 创建鼠标
m = mouse.Controller()
# 滚动鼠标,第一个参数为 y 滚动的数值,第二个参数为 x 滚动的数值
m.scroll(0, -10)1.7 监听鼠标的事件鼠标中的事件有三个,点击事件、移动事件、滚动事件,我们看看如何监听鼠标的事件:from pynput import mouse
def on_move(x, y):
"""鼠标移动的监听方法 x,y 为移动后的位置"""
print('鼠标移动到了{0}'.format((x, y)))
def on_click(x, y, button, pressed):
"""鼠标点击的监听方法 x,y 为坐标,button 为按钮,pressed 为是否是按下"""
if pressed:
print('点击了({0}, {1})'.format(x, y))
else:
print('鼠标在({0}, {1})松开'.format(x, y))
def on_scroll(x, y, dx, dy):
"""鼠标滚动的监听方法 x,y 为作为,dx,dy 为滚动幅度"""
print('鼠标在{0}, 向右滚动{1}, 向下滚动{2}'.format((x, y), dx, dy))
# 创建一个监听者
with mouse.Listener(
# 关联监听方法(不加括号)
on_move=on_move,
on_click=on_click,
on_scroll=on_scroll) as listener:
# 阻塞线程
listener.join()我们的 mouse 模块提供了 Listener 类,该类的对象就是我们的监听者。当我们触发某个事件时,监听者就会执行关联好的方法。2. 控制键盘在 pynput 中提供了 keyboard 模块,该模块中提供了与 mouse 模块类似的一些类,这些类可以用于控制键盘。其中 keyboard 中也有一个 Controller 类,该类对象就是我们的键盘控制器。from pynput import keyboard
# 创建一个键盘
kb = keyboard.Controller()我们可以通过上述代码创建一个键盘控制器。有了控制器我们就可以操作这个键盘了。2.1 按下并松开某个键这里同样是调用 press 和 release 方法:from pynput import keyboard
# 创建一个键盘
kb = keyboard.Controller()
# 按下 a 键
kb.press('a')
# 松开 a 键
kb.release('a')上面我们是通过传入字符的方式按按钮,这里智能点击单个字符的按钮。在 keyboard 模块中 Key 类中,提供了大量预设的按钮,我们可以直接使用:from pynput import keyboard
# 创建键盘
kb = keyboard.Controller()
# 按下大小写锁定
kb.press(keyboard.Key.caps_lock)
# 松开大小写锁定
kb.release(keyboard.Key.caps_lock)上面就是我们 press 和 release 的用法了。2.2 按下两个按钮我们可以通过多次调用 press 的方法按下几个按钮,当然我们还有一种简便写法:from pynput import keyboard
# 创建一个键盘
kb = keyboard.Controller()
# 按下 shift+a
with kb.pressed(keyboard.Key.shift):
kb.press('a')
kb.release('a')上面的效果就是我们打出了一个 A。2.3 打字理论上来说,press 和 release 方法可以完成键盘大多数操作,打字也不例外,但是出于效率的考虑我们可以使用 type 方法:from pynput import keyboard
# 创建键盘
kb = keyboard.Controller()
# 打字
kb.type('Hello world')在我们打中文字的时候,输入法并不会影响我们的操作。当时当我们打英文时,如果输入法是中文模式,则会是我们平时打拼音的效果。2.4 事件监听键盘的监听同样是由 keyboard 中 Listener 类实现的:from pynput import keyboard
# 按下按钮
def on_press(key):
print('按下了{0}'.format(key))
# 松开按钮
def on_release(key):
print('松开了{0}'.format(key))
# 监听
with keyboard.Listener(
on_press=on_press,
on_release=on_release) as listener:
listener.join()监听步骤同鼠标一样,这里就不再赘述了。3. 使用 pynput 完成游戏辅助器pynput 本身并不具备什么高难度的操作,我们更多的时用它实现一些重复循环的动作,而在玩游戏时,我们刷副本就是一个大量重复的动作。这时候,我们就可以用程序替代我们工作。我们这里以 PokeMMO 为例,我们先看看游戏本身的键盘分布:上面这些键就是我们要操作的一些键。下面我们需要实现自动刷怪的功能。如果对这个游戏不了解的话也不要紧,这里只是将其作为一个例子,重要的是将问题转换成代码的步骤。3.1 刷怪流程我们首先要知道整个流程是什么,也就是从第一次开始,到第二次重复这段流程的操作。如果对照 PokeMMO 的话,流程如下:第一次的时候自动调整位置,让人物飞到医院从医院出发,到野区使用技能引出野怪(该技能可以使用四次),并战斗循环使用技能引出野怪(四次)回医院补充技能值在医院门口调整位置上面就是一次完整的流程,我们将每个流程拆分开慢慢实现。3.2 将每一个步骤都划成一个流程除了上面的整体流程外,我们每一个步骤都可以划分为一个具体的流程,也就是一系列操作。比如我们的第一个步骤,我们做如下操作:点击宠物选择飞空术选择城市这样就把我们的第一个步骤划成了一个流程,我们将每个步骤都封装成一个函数,从小到大,实现完整的流程。3.3 调整位置、让人物飞医院我们对照键盘,将这一流程转化为代码:import time
from pynput import keyboard
# 创建一个键盘
kb = keyboard.Controller()
def fly():
"""定义方法,让人物回到医院"""
# 选择宠物
kb.type('3')
time.sleep(0.5)
# 选择飞空术
kb.type('s')
time.sleep(0.5)
# 选择城市,因为飞空术花的时间比较长,这里休眠 7 秒
kb.type('l')
kb.type('l')
time.sleep(7)这样我们就实现了第一个步骤。3.4 从医院出发,到野区这个步骤无非就是上下左右键的改变,根据不同路线每个按键按的时长不一样,我们可以将路线封装成一个列表:# 0,1,2,3 分别表示上右下左
Lmz_weed = [
# 向左移动 6 秒
(3, 6),
# 向上移动 0.4 秒
(0, 0.4)
]该列表的元素为元组,每个元组代表一条路线。而元组中的内容则是方向和按下的时长。于是我们通过循环的方式,行走任何路线:def to_weed(road):
"""根据路线移动"""
# 使用自行车
m_keyboard.type('c')
# 根据线路移动
for i in road:
key = None
if i[0] == 0:
# 向上移动
key = Key.up
elif i[0] == 1:
# 向右移动
key = Key.right
elif i[0] == 2:
# 向下移动
key = Key.down
elif i[0]== 3:
# 向左移动
key = Key.left
# 按下相应方向
m_keyboard.press(key)
time.sleep(i[1])
m_keyboard.release(key)上面就是我们行走的函数。3.5 使用技能引出野怪该步骤可以分为下列流程:选择宠物使用技能攻击宠物循环四次我们将引出宠物和攻击宠物分别封装成两个函数:def earthquake():
"""使用地震击败怪物"""
# 使用地震
m_keyboard.type('f')
time.sleep(0.5)
m_keyboard.type('q')
time.sleep(0.5)
m_keyboard.type('e')
time.sleep(37)
def sweet():
"""使用香气甜甜"""
# 选择第二个宠物使用香甜气息
m_keyboard.type('2')
time.sleep(0.5)
m_keyboard.type('s')
time.sleep(15)写出了上面两个函数,我们只需要用一个简单的循环就可以完成整个流程:for i in range(4):
# 引出宠物
sweet()
# 攻击
earthquake()3.6 回医院补充技能值这需要我们先回到医院,然后进入医院,然后同护士对话:def to_hospital():
"""去医院"""
# 选择第三个宠物飞空
fly()
# 走进医院
m_keyboard.press(Key.up)
time.sleep(7)
m_keyboard.release(Key.up)
# 对话
for i in range(8):
time.sleep(1.2)
m_keyboard.type('a')
time.sleep(1)
for i in range(3):
time.sleep(1)
m_keyboard.type('b')
# 走出医院
m_keyboard.press(Key.down)
time.sleep(7)
m_keyboard.release(Key.down)
# 对准宠物位置
fly()在对话的时候,我们模拟日常操作,不断按 a,然后不断按 b。3.7 完整流程完整流程就是我们用我们的每个步骤依次调用相应的方法,然后整合在一起:time.sleep(3)
flag = True
while(True):
"""循环刷野怪"""
if flag:
to_hospital()
flag = False
to_weed(Lmz_weed)
for i in range(4):
sweet()
earthquake()
to_hospital()看到这里会有许多困惑的地方,主要原因是对游戏不熟悉。不过这个程序本身不是重点,重点是将整个操作化为一系列流程,然后将某一流程切割成一系列操作,最后通过代码实现这些操作。4. 总结哪些情况我们可以用 pynput 帮助我们呢?大量重复的操作,像是消息轰炸就是比较典型的一种操作。另外一些我们人类无法企及的手速,这时候我们就可以用电脑来实现。另外大家可以自己发掘 pynput 的作用。
后端小马
Java去除文档阴影
一、前言文稿扫描大家用的都比较频繁、想是各种证件、文件都可以通过扫描文稿功能保存到手机。相比直接拍照,在扫描文稿时,程序会对图像进行一些矫正。比如去除阴影、修正倾斜、旋转矫正等。进行这些处理后的图片要更加容易识别。今天就来讨论以下去除阴影的操作。二、实现原理1. 图像在开始实现前,我们来了解一些图像相关的知识。这里讨论RGB图像,也就是我们俗称的彩色图像。图像可以被看作是一个height×width的数组,每一个数表示一个像素。如果是彩色图像,每个像素会包含RBG三个值,最低字节表示G、次低字节表示B、第三字节表示R。比如像素值为:0x00ff00其RBG值分别为:R: 0x00
G: 0xff
B: 0x00如果想要从原像素中取RGB的值,可以使用按位与操作,示例如下:// pixel是从图像中取出来的数
int[] rgb = new int[3];
rgb[0] = (pixel & 0xff0000) >> 16;
rgb[1] = (pixel & 0xff00) >> 8;
rgb[2] = (pixel & 0xff); 因为获取R和G的时候,保留的是高位,我们希望得到的是一个低位的数据,因此向右移一定位。2. 灰度转换有时候,为了方便处理会把图像转换成灰度图像。转换成灰度图像的方法有很多,一种非常简单的办法就是让rgb三个通道都为同样的值,这个值就是rgb三个值的均值。3.阈值处理阈值处理是今天关键部分,阈值处理的思想非常简单,就是当图像像素值大于阈值时将其处理为最大值,当像素小于等于阈值时将其处理为0。这样可以得到一张完全的黑白图像。在文稿中,文字部分可以看作是黑色,背景部分可以看作是白色,而阴影则是介于黑白之间的值。如果想要去除阴影,则需要对图像进行阈值处理,把阈值设定为小于阴影的值。比如下图:左图是原图,其中灰色部分为阴影,需要去除。这时我们对图像进行阈值处理,把阈值设定为50,那么阴影部分就会被设置成255,文字部分和背景部分变换都不大,这样就实现了文稿的阴影去除工作。三、代码实现1.读取图像首先来看看如何读取图像以及如何访问图像的像素,这里使用ImageIO类。代码如下:import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
public class DocumentDealing {
public static void main(String[] args) throws Exception {
String imagePath = "D:/images/imgs/10000.jpeg";
BufferedImage bi = ImageIO.read(new File(imagePath));
//获取图片宽高
int width = bi.getWidth();
int height = bi.getHeight();
System.out.println("width:" + width + ",height:" + height);
//获取坐标为(0, 0)位置的像素
int pixel = bi.getRGB(0, 0);
System.out.println("pixel" + pixel);
//获取rgb值
int[] rgb = new int[3];
rgb[0] = (pixel & 0xff0000) >> 16;
rgb[1] = (pixel & 0xff00) >> 8;
rgb[2] = pixel & 0xff;
System.out.println(
"r:" + rgb[0] +
"\tg:" + rgb[1] +
"\tb:" + rgb[2]
);
}
public static int[] getRgb(BufferedImage bi, int x, int y) {
int[] rgb = new int[3];
int pixel = bi.getRGB(x, y);
rgb[0] = (pixel & 0xff0000) >> 16;
rgb[1] = (pixel & 0xff00) >> 8;
rgb[2] = (pixel & 0xff);
return rgb;
}
}我们可以通过下面代码读取图片,其中imagePath是图片路径:BufferedImage bi = ImageIO.read(new File(imagePath));BufferedImage可以获取图片的宽、高、某个点的像素等。为了方便,编写一个getRgb来把pixel转成一个rgb数组。代码输出结果如下:width:400,height:400
pixel-2853206
r:212 g:118 b:1702.阈值处理知道了上面的基本操作后,就可以开始进行阈值处理了。阈值处理就是求rgb均值mean,如果mean大于阈值,则把像素设置为0xffffff,否则设置为0。具体代码如下:import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
public class DocumentDealing {
public static void main(String[] args) throws Exception {
String imagePath = "C:/Users/Administrator/Desktop/document.jpg";
threshold(imagePath, "result.jpg", 50);
}
public static void threshold(String imagePath, String savePath, int threshold) throws Exception{
//读取图片
BufferedImage bi = ImageIO.read(new File(imagePath));
//读取宽高
int width = bi.getWidth();
int height = bi.getHeight();
//遍历图片像素
for(int y = 0; y < height; y ++){
for(int x = 0; x < width; x ++){
int[] rgb = getRgb(bi, x, y);
//计算rgb均值
int grayScale = (rgb[0] + rgb[1] + rgb[2]) / 3;
//如果均值大于阈值,则赋值将该像素设置为0xffffff(全白),否则赋值为0(全黑)
if(grayScale > threshold){
bi.setRgb(x, y, 0xffffff);
}else{
bi.setRgb(x, y, 0);
}
}
}
//保存图片
ImageIO.write(bi, "jpg", new File(savePath));
}
public static int[] getRgb(BufferedImage bi, int x, int y){
int[] rgb = new int[3];
int pixel = bi.getRGB(x, y);
rgb[0] = (pixel & 0xff0000) >> 16;
rgb[1] = (pixel & 0xff00) >> 8;
rgb[2] = (pixel & 0xff);
return rgb;
}
}下图是原图和处理后的结果:左图中有两处阴影,右侧则去除了阴影。最终效果图与设定的阈值有关系,当阈值设置不恰当时,会导致结果图比原图更糟糕,或者导致最终文字目标也被去除了。这里可以用循环来解决,代码如下:public static void main(String[] args) throws Exception {
String imagePath = "C:/Users/Administrator/Desktop/document.jpg";
for (int i = 50; i < 127; i++) {
threshold(imagePath, "imgs/result" + i + ".jpg", i);
}
}读者可以自行测试。有时候之间阈值处理不能很好的去除阴影,这个时候会结合一些其它办法。包括滤波操作、形态学处理等。感兴趣的读者可以去了解以下。
后端小马
Python快速构建神经网络
一、前言机器学习一直是Python的一大热门方向,其中由神经网络算法衍生出来的深度学习在很多方面大放光彩。那神经网络到底是个个什么东西呢?说到神经网络很容易让人们联想到生物学中的神经网络,而且很多时候也会把机器学习的神经网络和生物神经网络联系起来。但是其实人类至今都没有完全理解生物神经网络的运作,更不要谈用计算机实现生物神经网络了。相比之下,机器学习中的神经网络更像是一个数学函数。我们输入一组数据,然后神经网络会给我们返回一个结果。像下面这个简单的函数:我们给定一个x,就能得到一个y。只不过神经网络的函数要比上面的函数复杂得多。不过其实神经网络的基础就是上面的函数。下面我们就带大家快速搭建一个神经网络。二、机器学习在学习神经网络之前,我们需要了解一些机器学习的知识。2.1、什么是机器学习?假如我有下面一组数据:1, 3, 5, 7, 9
现在让你说出下一个数字可能是什么。对于人类智慧来说,我们可以很快地说出11。但是对计算机来说却不是那么简单,因为计算机是不会思考的。那计算机要怎么学习呢?这就需要人来指引了。在机器学习中,人类需要告诉机器如何学习。然后通过人类告诉的学习方法来学习,并得到一个模型。当然机器学习还有其它一些形式,我们不继续讨论。2.2、如何学习?对于机器学习来说,如何学习是一个非常重要的问题。其中已经出现了许多优秀的算法,这些算法的作用都是告诉机器如何学习。比如线性回归、逻辑回归、K近邻、决策树、神经网络等。机器学习算法可以说是机器学习的灵魂。我们今天要实现的神经网络也是一种机器学习算法,他是建立在逻辑回归的基础之上的,而逻辑回归又建立在线性回归之上。因此线性回归和逻辑回归也是今天要学习的内容。2.3、机器学习中的问题机器学习的问题通常分为两大类,一个类是分类,一类是回归。它们两者的区别是结果是否离散。比如一个动物分类问题,我们得到的结果只可能是一个确定的动物。不会得到一个介于猫狗之间的动物。而回归问题的结果通常是一个数值,比如房价预测问题。我们可能得到0-100万之间任意一个数值,也可能得到一个类似40.023242的小数。其中线性回归就是解决回归问题的一大利器,而逻辑回归则是用于分类问题。下面我们就来看看这两个算法。三、线性回归和逻辑回归可能你会好奇,为什么逻辑回归名字里有个回归却是不是解决回归问题的。相信看完下面的内容就不会有这个疑惑了。3.1、线性回归在前言中,我们介绍了一个简单的函数:y=wx+by = wx + by=wx+b其实它就是线性回归的基础。线性回归算法就是找到一组最优的w b,让我们得到最接近真实的结果。我们还是用上面的数据:1, 3, 5, 7, 9
对于上面这组数据,我们是要找序号和数值之间的关系,我们可以把上面的数据理解为:x, y
1, 1
2, 3
3, 5,
4, 7,
5, 9
其中x表示序号,y表示具体的数值。我们稍加运算就可以得到下面这个函数:y=2x−1y = 2x - 1y=2x−1我们得到了最优的一组参数w=2, b = -1,通过这个函数我们就可以预测后面后面一千、一万个数字。不过有时候我们会有多个x,这时我们就可以把上面的函数推广为:y=w1x1+w2x2+...+wnxn+by = w_1x_1 + w_2x_2 + ...+w_nx_n + by=w1x1+w2x2+...+wnxn+b这时候我们需要求得参数就多多了。下面我们来实际写一个线性回归的程序。3.2、线性回归实战这里我们需要使用到scikit-learn模块,安装如下:pip install scikit-learn
然后我们就可以开始写代码了。线性回归算法的实现被封装在了sklearn.linear_model中的LinearRegression,我们可以直接使用:import numpy as np
from sklearn.linear_model import LinearRegression
# 准备x的数据
X = np.array([
[1],
[2],
[3],
[4],
[5]
])
# 准备y的数据
y = np.array([1, 3, 5, 7, 9])
# 创建线性回归模块
lr = LinearRegression()
# 填充数据并训练
lr.fit(X, y)
# 输出参数
print("w=", lr.coef_, "b=", lr.intercept_)
首先我们需要准备X和y的数据,这里我们使用的是ndarray数组。这里需要注意,我们y的的数据长度为5,则X的数据需要是5*n。准备好数据后我们需要创建线性回归模型,然后调用fit方法填充我们准备好的数据,并训练。训练完成后我们可以查看一下模块的参数,其中coef_表示w,而intercept_表示b。因为w可以有多个,所以它应该是个数组,下面是输出结果:w= [2.] b= -1.0
和我们人工智慧得到的结果是一样的。我们还可以调用predict方法预测后面的数据:import numpy as np
from sklearn.linear_model import LinearRegression
X = np.array([
[1],
[2],
[3],
[4],
[5]
])
y = np.array([1, 3, 5, 7, 9])
lr = LinearRegression()
lr.fit(X, y)
y_predict = lr.predict(np.array([[100]]))
print(y_predict)
这里同样需要注意X的数据是二维的。3.3、逻辑回归逻辑回归可以理解为线性回归+特殊函数。我们可以思考下面这个问题。现在需要写一个程序来判断每个人的分数是否及格,计分标准为:总分=40%数学+30%语文+30%英语。总分大于等于60为及格,其余为不及格。虽然是个很简单的问题,但是我们还是需要讨论一下。首先我们可以把计算总分的公式写成下面的形式:y=0.4x1+0.3x2+0.3x3+by = 0.4x_1 + 0.3x_2 + 0.3x_3 + by=0.4x1+0.3x2+0.3x3+b对于这个公式,我们可以得到0-100之间的任何一个数字。但是我想要得到的只有两个结果,及格或者不及格。我们可以简单理解为-1和1。那我们怎么把上面的结果映射到-1和1上呢?这就需要使用一个特殊的函数了,我们把这个函数叫做激活函数。我们可以用下面这个函数作为激活函数:f(x)=y−60∣y−60∣f(x) = \frac{y-60}{|y-60|}f(x)=∣y−60∣y−60这样就可以把所有分数映射到-1和1上了。(上面的函数在y=60处无定义,严格上来讲上面的激活函数是不适用的)逻辑回归的图示如下:先通过一个线性模型得到一个结果,然后再通过激活函数将结果映射到指定范围。不过在实际应用中,我们通常会使用Sigmoid、Tanh和ReLU函数。下面是几个激活函数的图像:下面我们来写一个逻辑回归的例子。3.4、逻辑回归实战我们用逻辑回归解决是否几个的问题,逻辑回归的实现封装在linear_model.LogisticRegression中,同样可以直接使用,我们直接上代码:import numpy as np
from sklearn.linear_model import LogisticRegression
# 准备X的数据
X = np.array([
[60],
[20],
[30],
[80],
[59],
[90]
])
# 准备y的数据
y = np.array([1, 0, 0, 1, 0, 1])
# 创建逻辑回归模型
lr = LogisticRegression()
# 填充数据并训练
lr.fit(X, y)
# 准备用于测试的数据
X_test = np.array([
[62],
[87],
[39],
[45]
])
# 判断测试数据是否及格
y_predict = lr.predict(X_test)
print(y_predict)
代码和线性回归只有一些细微的差别。在代码中,我们用0表示不及格,1表示及格。下面是我们测试数据输出的结果:[1 1 0 0]
可以看到所有结果都预测正确了。有了上面的知识,我们就可以开始实现一个神经网络了。四、神经网络神经网络是建立在逻辑回归之上的,可以说神经网络就是一个逻辑回归的集合。4.1、神经网络想必大家都听说过,神经网络是由大量的神经元组成的。不过你可能不知道,机器学习中的神经元就是我们前面学的逻辑回归。我们可以看下面这张图:可以看到和之前的逻辑回归很像,但是这里使用了很多激活函数,而且参数数量也要多得多。至于为什么要使用这么多激活函数可以说就是为了让得到的函数非常复杂。如果我们的函数非常简单,比如下面这组数据:假如用下面的函数作为我们的模型:会得到下面这张图像: 可以看到有许多点都不在直线上,所以预测的数据会有很多误差。这个时候我们可以考虑二次,如:神经网络的可解释性比之前两个算法要差得多,因为神经网络通常有成百上千个参数,我们会得到一个非常复杂的模型。虽然不能理解参数的含义,但是这些参数通常会给我们一个很好的结果。不过这也真是神经网络的神奇之处。4.2、输入层、隐层、输出层神经网络通常会有三个部分,输入层由我们的特征数决定。而输出层由我们分类数量决定,如图x部分为输入层,y部分为输出层,而中间部分为隐藏层:隐藏层通常会特别复杂,我们可以通过调节隐层的层数和节点数来调整模型的复杂度。4.3、神经网络实战使用scikit-learn,我们可以很快搭建一个神经网络。接下来我们用scikit-learn中自带的数据集来实现一个神经网络:from sklearn.datasets import load_iris
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
# 加载数据集
iris_data = load_iris()
# 拆分数据集
X_train, X_test, y_train, y_test = train_test_split(iris_data['data'], iris_data['target'], test_size=0.25, random_state=1)
# 创建神经网络模型
mlp = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[4, 2], random_state=0)
# 填充数据并训练
mlp.fit(X_train, y_train)
# 评估模型
score = mlp.score(X_test, y_test)
print(score)
这里我们使用的是scikit-learn自带的鸢尾花数据,我们用train_test_split将数据集分割成了两个部分,分别是训练集的特征和目标值,以及测试集的特征和目标值。然后我们创建MLPClassifier类的实例,实际上它就是一个用于分类的多重感知机。我们只需要关注hidden_layer_sizes参数即可,它就是我们神经网络的层数和节点数。因为是一个二分类问题,所以这里的输出层有两个节点。下面输出的结果:0.9210526315789473
我们调用mlp.score评估模型的好坏,92%的准确率也算是一个非常优秀的结果了。
后端小马
WordCloud生成卡卡西忍术词云
前言本想果断的说,卡卡西是火影里面最帅的人物。但是出于对大家的尊重,我把这句话改成:“卡卡西是动漫界最帅的人物”,不接受任何反驳。一、项目介绍在介绍之前,先给大家来个用香克斯图片做的效果图。这是我用香克斯的图片作为轮廓,将《霍乱时期的爱情》作为文字素材做的一个词云。看起来还是有几分帅气的。主要使用到的模块有三个,wordcloud、jieba、imageio,其中wordcloud作为主要的模块,今天给大家详细讲解一个具体用法。二、wordcloud模块讲解在wordcloud模块中,我们将会使用到两个对象。一个是WordCloud对象,也就是“词云”对象。第二个是ImageColorGenerator对象,也就是“图像颜色产生器”对象。具体的使用后续慢慢讲解。1、生成一个简单词云在具体讲解之前,我们先说一下词云的生成步骤。准备文本数据创建词云对象通过文本数据生成词云保存词云文件安装上面的步骤,我们写出如下代码:import wordcloud
# 1、准备文本
sentence = 'Do not go gentle into that good night!'
# 2、创建词云对象
wc = wordcloud.WordCloud()
# 3、通过文本数据生成词云
wc.generate(sentence)
# 4、保存图片
wc.to_file("test_wc.png")
生成的词云如下:当然,水印可不是我生成的。这个词云比较简单,而且正正方方,背景也是单调的黑色。这可不符合我高贵的身份,于是乎我们对词云进行一些改进。2、WordCloud的参数和方法下面列出了一下比较常用的参数:参数参数类型参数介绍widthint(default=400)词云的宽heightint(default=200)词云的高background_colorcolor value(default="black")词云的背景颜色font_pathstring字体路径masknd-array(default=None)图云背景图片stopwordsset要屏蔽的词语max_font_sizeint(default=None)字体的最大大小min_font_sizeint(default=None)字体的最小大小max_wordsnumber(default=200)要显示词的最大个数contour_widthint轮廓粗细contour_colorcolor value轮廓颜色scalefloat(default=1)按照原先比例扩大的倍数还有一些不常用的参数没有提到。下面看几个WordCloud常用的方法,这里就讲三个:方法名称传入参数方法描述generatetext根据文本生成词云recolor[random_state, color_func, colormap]对现有输出重新着色to_filefilename输出到文件3、生成一个带形状的词云在了解具体参数之后,我们就可以完成一个更为复杂的图云了。具体步骤比之前多了一步:准备文本数据生成图片的nd-array创建词云对象通过文本数据生成词云保存词云文件在写代码之前,先准备好一张图片。这里当然选取卡卡西了:先把准备好的图片素材复制到项目目录下面,和执行的py文件同级(图片背景必须是透明或者全白,不能有其它杂色)。接下来我们开始写代码了:import wordcloud, imageio
# 1、准备文本数据
sentence = "旗木卡卡西,日本漫画《火影忍者》及其衍生作品中的男性角色。火之国木叶隐村的精英上忍,原木叶暗部成员,四代目火影波风水门的弟子,第七班队长,漩涡鸣人、宇智波佐助、春野樱的老师。年仅12岁就成为上忍的天才忍者,后左眼移植宇智波带土的写轮眼,因使用写轮眼复制了上千种忍术而被称为“拷贝忍者”、“写轮眼卡卡西”,其名号响彻各国。"
# 2、生成图片的nd-array,传入图片路径
im = imageio.imread('kkx.png')
# 3、创建词云对象
wc = wordcloud.WordCloud(
#设置宽为600
width=600,
#设置高为800
height=800,
#设置背景颜色
background_color='white',
#设置字体,如果文本数据是中文一定要设置,不然就是方块
font_path='msyh.ttc',
#设置图片的形状
mask=im,
#设置轮廓粗细
contour_width=1,
#设置轮廓颜色
contour_color='black'
)
# 4、通过文本数据生成词云
wc.generate(sentence)
# 5、保存词云文件
wc.to_file('wc.png')
生成词云效果如下:不得不说,效果确实不尽人意,没有轮廓完全看不出这是什么东西。仔细观察会发现,这里的词全是一大段一大段的,还有很多句子。所有导致词云密度受到很大影响。我们可以继续对这个词云进行美化,这就需要用到分词模块jieba。三、jieba分词模块简介jieba模块的功能就是对句子进行词语提取,我们调用jieba.cut()方法,然后生成一个可迭代的generator对象,具体是什么我也不知道。在实验过程中,我发现这个对象应该是个迭代器。因为使用的不是非常多,这里就讲解一个非常简单的例子:import jieba
# 准备要分词的句子
sentence = '爱因斯坦是最伟大的科学家之一'
# 使用精确模式分词
word = jieba.cut(sentence)
# 将返回的generator用空格拼接成字符串
str = " ".join(word)
# 输出分词后的结果
print(str)
输出结果为:爱因斯坦 是 最 伟大 的 科学家 之一
我们刚刚使用的是默认的精确模式,除此之外还有许多其它模式,这里不做讲解,如果想对jieba模块深入了解可以访问其项目地址 https://github.com/fxsjy/jieba 。四、jieba和wordcloud结合使用我们只需要通过我们的jieba,将相应的文本转成一个个词。然后我们有了所以需要的数据,接下来就按照上面的步骤,生成一个由词语组成的词云:import wordcloud, imageio, jieba
# 1、准备文本数据
sentence = "旗木卡卡西,日本漫画《火影忍者》及其衍生作品中的男性角色。火之国木叶隐村的精英上忍,原木叶暗部成员,四代目火影波风水门的弟子,第七班队长,漩涡鸣人、宇智波佐助、春野樱的老师。年仅12岁就成为上忍的天才忍者,后左眼移植宇智波带土的写轮眼,因使用写轮眼复制了上千种忍术而被称为“拷贝忍者”、“写轮眼卡卡西”,其名号响彻各国。"
# 用jieba将句子分词
word = jieba.cut(sentence)
words = " ".join(word)
# 2、生成图片的nd-array,传入图片路径
im = imageio.imread('kkx.png')
# 3、创建词云对象
wc = wordcloud.WordCloud(width=600,height=800,background_color='white',font_path='msyh.ttc', mask=im,contour_width=1,contour_color='black')
# 4、通过文本数据生成词云
wc.generate(words)
# 5、保存词云文件
wc.to_file('wc.png')
这次就比之前更加紧密了。效果图如下:但是这个还是感觉少了几分神色,其原因在于我们生成的词云文字颜色是随机的,而最开始给大家看的案例其颜色使按照图片原本的颜色给相应区域的文字设置相应的颜色。五、按照图片颜色绘制词云大招步骤还是一样的,正如我标题所说的。我是要绘制一个卡卡西的忍术词云,因此我准备了一个文件旗木卡卡西.txt。具体其内容就是卡卡西的忍术合集。这次我们的大致步骤和之前差不多,只是把准备文本数据从之前的string改成了txt文件。import wordcloud, jieba, imageio
# 1、准备文本
f = open('kkx.txt', encoding='utf-8')
kkx = f.read()
kkx = jieba.cut(kkx)
kkx = " ".join(kkx)
# 2、生成图片的nd-array,传入图片路径
im = imageio.imread('kkx.png')
# 3、获取一个图形颜色生成器
image_color = wordcloud.ImageColorGenerator(im)
# 4、创建词云对象
wc = wordcloud.WordCloud(
width=600,
height=800,
background_color='white',
font_path='msyh.ttc',
mask=im,
stopwords={'之术'},
contour_width=1,
contour_color='black',
)# 5、根据文本生成词云
wc.generate(kkx)
# 根据图片颜色重绘
rwc = wc.recolor(color_func=image_color)
rwc.to_file('qmkkx.png')
其代码主要有两个部分,一个是使用wordcloud.ImageColorGenerator() 获取图片颜色生成器,另外就是WordCloud中的recolor() 方法重绘词云。效果图如下:其中第一张是上面代码生成的词云,但是因为密度有点低,我另外用其它文本生成了一个词云作为观看使用。就此我们就完成了卡卡西词云的绘制。
后端小马
图解爬虫,用几个最简单的例子带你入门Python爬虫
一、前言爬虫一直是Python的一大应用场景,差不多每门语言都可以写爬虫,但是程序员们却独爱Python。之所以偏爱Python就是因为她简洁的语法,我们使用Python可以很简单的写出一个爬虫程序。本篇博客将以Python语言,用几个非常简单的例子带大家入门Python爬虫。二、网络爬虫如果把我们的因特网比作一张复杂的蜘蛛网的话,那我们的爬虫就是一个蜘,我们可以让这个蜘蛛在网上任意爬行,在网中寻找对我们有价值的“猎物”。首先我们的网络爬虫是建立在网络之上的,所以网络爬虫的基础就是网络请求。在我们日常生活中,我们会使用浏览器浏览网页,我们在网址栏输入一个网址,点击回车在几秒时间后就能显示一个网页。我们表面上是点击了几个按钮,实际上浏览器帮我们完成了一些了的操作,具体操作有如下几个:向服务器发送网络请求浏览器接收并处理你的请求浏览器返回你需要的数据浏览器解析数据,并以网页的形式展现出来我们可以将上面的过程类比我们的日常购物:和老板说我要杯珍珠奶茶老板在店里看看有没有你要的东西老板拿出做奶茶的材料老板将材料做成奶茶并给你上面买奶茶的例子虽然有些不恰当的地方,但是我觉得已经能很好的解释什么是网络请求了。网络请求是什么之后,我们就可以来了解一下什么是爬虫了。实际上爬虫也是网络请求,通常情况下我们通过浏览器,而我们的爬虫则是通过程序来模拟网络请求这一过程。但是这种基础的网络请求还算不上是爬虫,爬虫通常都是有目的的。比如我想写一个爬取美女图片,我们就需要对我们请求到的数据进行一些筛选、匹配,找到对我们有价值的数据。而这一从网络请求到数据爬取这整个过程才是一个完整的爬虫。有些时候网站的反爬虫做的比较差,我们可以直接在浏览器中找到它的API,我们通过API可以直接获取我们需要的数据,这种相比就要简单许多。三、简单的爬虫简单的爬虫就是单纯的网络请求,也可以对请求的数据进行一些简单的处理。Python提供了原生的网络请求模块urllib,还有封装版的requests模块。相比直线requests要更加方便好用,所以本文使用requests进行网络请求。3.1、爬取一个简单的网页在我们发送请求的时候,返回的数据多种多样,有HTML代码、json数据、xml数据,还有二进制流。我们先以百度首页为例,进行爬取:import requests
# 以get方法发送请求,返回数据
response = requests.get('http://www.baidu.com')
# 以二进制写入的方式打开一个文件
f = open('index.html', 'wb')
# 将响应的字节流写入文件
f.write(response.content)
# 关闭文件
f.close()
下面我们看看爬取的网站打开是什么样子的:这就是我们熟悉的百度页面,上面看起来还是比较完整的。我们再以其它网站为例,可以就是不同的效果了,我们以CSDN为例:可以看到页面的布局已经完全乱了,而且也丢失了很多东西。学过前端的都知道,一个网页是由html页面还有许多静态文件构成的,而我们爬取的时候只是将HTML代码爬取下来,HTML中链接的静态资源,像css样式和图片文件等都没有爬取,所以会看到这种很奇怪的页面。3.2、爬取网页中的图片首先我们需要明确一点,在爬取一些简单的网页时,我们爬取图片或者视频就是匹配出网页中包含的url信息,也就是我们说的网址。然后我们通过这个具体的url进行图片的下载,这样就完成了图片的爬取。我们有如下url:img-blog.csdnimg.cn/20200516143…,我们将这个图片url来演示下载图片的代码:import requests
# 准备url
url = 'https://img-blog.csdnimg.cn/2020051614361339.jpg'
# 发送get请求
response = requests.get(url)
# 以二进制写入的方式打开图片文件
f = open('test.jpg', 'wb')
# 将文件流写入图片
f.write(response.content)
# 关闭文件
f.close()
可以看到,代码和上面网页爬取是一样的,只是打开的文件后缀为jpg。实际上图片、视频、音频这种文件用二进制写入的方式比较恰当,而对应html代码这种文本信息,我们通常直接获取它的文本,获取方式为response.text,在我们获取文本后就可以匹配其中的图片url了。我们以下列topit.pro为例:import re
import requests
# 要爬取的网站
url = 'http://topit.pro'
# 获取网页源码
response = requests.get(url)
# 匹配源码中的图片资源
results = re.findall("<img[\\s\\S]+?src=\"(.+?)\"", response.text)
# 用于命名的变量
name = 0
# 遍历结果
for result in results:
# 在源码中分析出图片资源写的是绝对路径,所以完整url是主站+绝对路径
img_url = url+result
# 下载图片
f = open(str(name) + '.jpg', 'wb')
f.write(requests.get(img_url).content)
f.close()
name += 1
上面我们就完成了一个网站的爬取。在匹配时我们用到了正则表达式,因为正则的内容比较多,在这里就不展开了,有兴趣的读者可以自己去了解一下,这里只说一个简单的。Python使用正则是通过re模块实现的,可以调用findall匹配文本中所有符合要求的字符串。该函数传入两个参数,第一个为正则表达式,第二个为要匹配的字符串,对正则不了解的话只需要知道我们使用该正则可以将图片中的src内容拿出来。四、使用BeautifulSoup解析HTMLBeautifulSoup是一个用来分析XML文件和HTML文件的模块,我们前面使用正则表达式进行模式匹配,但自己写正则表达式是一个比较繁琐的过程,而且容易出错。如果我们把解析工作交给BeautifulSoup会大大减少我们的工作量,在使用之前我们先安装。4.1、BeautifulSoup的安装和简单使用我们直接使用pip安装:pip install beautifulsoup4模块的导入如下:from bs4 import BeautifulSoup下面我们就来看看BeautifulSoup的使用,我们用下面HTML文件测试:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img class="test" src="1.jpg">
<img class="test" src="2.jpg">
<img class="test" src="3.jpg">
<img class="test" src="4.jpg">
<img class="test" src="5.jpg">
<img class="test" src="6.jpg">
<img class="test" src="7.jpg">
<img class="test" src="8.jpg">
</body>
</html>
上面是一个非常简答的html页面,body内包含了8个img标签,现在我们需要获取它们的src,代码如下:from bs4 import BeautifulSoup
# 读取html文件
f = open('test.html', 'r')
str = f.read()
f.close()
# 创建BeautifulSoup对象,第一个参数为解析的字符串,第二个参数为解析器
soup = BeautifulSoup(str, 'html.parser')
# 匹配内容,第一个为标签名称,第二个为限定属性,下面表示匹配class为test的img标签
img_list = soup.find_all('img', {'class':'test'})
# 遍历标签
for img in img_list:
# 获取img标签的src值
src = img['src']
print(src)
解析结果如下:1.jpg
2.jpg
3.jpg
4.jpg
5.jpg
6.jpg
7.jpg
8.jpg
正好就是我们需要的内容。4.2、BeautifulSoup实战我们可以针对网页进行解析,解析出其中的src,这样我们就可以进行图片等资源文件的爬取。下面我们用梨视频为例,进行视频的爬取。主页网址如下:www.pearvideo.com/。我们右键检查可以看到如下页面:我们可以先点击1处,然后选择需要爬取的位置,比如2,在右边就会跳转到相应的位置。我们可以看到外层套了一个a标签,在我们实际操作是发现点击2的位置跳转了网页,分析出来跳转的网页应该就是a标签中的herf值。因为herf值是以/开头的,所以完整的URL应该是主站+href值,知道了这个我们就可以进行下一步的操作了,我们先从主站爬取跳转的url:import requests
from bs4 import BeautifulSoup
# 主站
url = 'https://www.pearvideo.com/'
# 模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'
}
# 发送请求
response = requests.get(url, headers=headers)
# 获取BeautifulSoup对象
soup = BeautifulSoup(response.text, 'html.parser')
# 解析出符合要求的a标签
video_list = soup.find_all('a', {'class':'actwapslide-link'})
# 遍历标签
for video in video_list:
# 获取herf并组拼成完整的url
video_url = video['href']
video_url = url + video_url
print(video_url)
输出结果如下:https://www.pearvideo.com/video_1674906
https://www.pearvideo.com/video_1674921
https://www.pearvideo.com/video_1674905
https://www.pearvideo.com/video_1641829
https://www.pearvideo.com/video_1674822
我们只爬取一个就好了,我们进入第一个网址查看源码,发现了这么一句:var contId="1674906",liveStatusUrl="liveStatus.jsp",liveSta="",playSta="1",autoPlay=!1,isLiving=!1,isVrVideo=!1,hdflvUrl="",sdflvUrl="",hdUrl="",sdUrl="",ldUrl="",srcUrl="https://video.pearvideo.com/mp4/adshort/20200517/cont-1674906-15146856_adpkg-ad_hd.mp4",vdoUrl=srcUrl,skinRes="//www.pearvideo.com/domain/skin",videoCDN="//video.pearvideo.com";
其中srcUrl就包含了视频文件的网站,但是我们肯定不能自己一个网页一个网页自己找,我们可以使用正则表达式:import re
# 获取单个视频网页的源码
response = requests.get(video_url)
# 匹配视频网址
results = re.findall('srcUrl="(.*?)"', response.text)
# 输出结果
print(results)
结果如下:['https://video.pearvideo.com/mp4/adshort/20200516/cont-1674822-14379289-191950_adpkg-ad_hd.mp4']
然后我们就可以下载这个视频了:with open('result.mp4', 'wb') as f:
f.write(requests.get(results[0], headers=headers).content)
完整代码如下:import re
import requests
from bs4 import BeautifulSoup
# 主站
url = 'https://www.pearvideo.com/'
# 模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'
}
# 发送请求
response = requests.get(url, headers=headers)
# 获取BeautifulSoup对象
soup = BeautifulSoup(response.text, 'html.parser')
# 解析出符合要求的a标签
video_list = soup.find_all('a', {'class':'actwapslide-link'})
# 遍历标签
video_url = video_list[0]['href']
response = requests.get(video_url)
results = re.findall('srcUrl="(.*?)"', response.text)
with open('result.mp4', 'wb') as f:
f.write(requests.get(results[0], headers=headers).content)
到此我们就从简单的网页到图片再到视频实现了几个不同的爬虫。
后端小马
Python实现扫码工具
Python实现扫码工具二维码作为一种信息传递的工具,在当今社会发挥了重要作用。从手机用户登录到手机支付,生活的各个角落都能看到二维码的存在。那你知道二维码是怎么解析的吗?有想过自己实现一个扫码工具吗?如果想的话就继续看下去吧!一、案例分析我们先思考一下,实现扫码工具需要写什么操作。在扫码过程中我们需要打开摄像头,如何由手机或者电脑识别二维码。所以我们要实现两个关键的步骤:调用摄像头、识别二维码。这两个操作分别对应了两个模块,它们就是opencv和pyzbar,其中opencv是英特尔的计算机视觉处理模块,而pyzbar则是用于解析二维码的模块。二、环境环境包括python环境和模块。我的环境如下:系统:Windows 10
python:python 3.7.9
opencv:opencv-python-4.4.0.44
pyzbar:pyzbar-0.1.8
模块安装很简单,我们直接用pip安装,先安装opencv模块:pip install opencv-python
然后是pyzbar模块:pip install pyzbar
在未指定安装版本时,系统会自动安装最新版。安装好模块后,我们就可以来实现扫码工具了。三、识别二维码有了pyzbar模块后,我们识别二维码的工作就非常简单了,首先需要准备一张二维码。有了二维码后就可以开始解析了,具体步骤如下:读取二维码图片解析二维码中的数据在解析出的数据中提取data信息实现代码如下:import cv2
from pyzbar import pyzbar
# 1、读取二维码图片
qrcode = cv2.imread('qrcode.jpg')
# 2、解析二维码中的数据
data = pyzbar.decode(qrcode)
print(data)
# 3、在数据中解析出二维码的data信息
text = data[0].data.decode('utf-8')
print(text)
在上面我们解析了两次,第一次获取了一个data,我们先来看看data长什么样子:[Decoded(data=b'http://weixin.qq.com/r/vC_fhynEKnRVrW3k93qu', type='QRCODE', rect=Rect(left=140, top=113, width=390, height=390), polygon=[Point(x=140, y=113), Point(x=140, y=503), Point(x=530, y=503), Point(x=530, y=113)])]
可以看到是一个列表,而且列表的第一个数据包含url的信息。所以我们需要通过下面的代码再次解析:text = data[0].data.decode('utf-8')
这样我们就能拿到二维码中包含的信息了。为了方便后续使用,可以将上面的代码写成一个函数:def scan_qrcode(img_path):
qrcode = cv2.imread(img_path)
data = pyzbar.decode(qrcode)
return data[0].data.decode('utf-8')
接下来我们再看看如何调用摄像头。四、调用摄像头在opencv中提供了一个VideoCapture类用于读取视频,同样可以用来调用摄像头。调用摄像头的步骤如下:调用摄像头循环在循环内读取一帧画面显示当前读取的画面等待键盘输入判断是否按退出键q按了推出键则退出,没按则继续循环具体代码如下:import cv2
# 调用摄像头
cap = cv2.VideoCapture(0)
while True:
# 读取一帧画面
ret, frame = cap.read()
# 显示当前帧
cv2.imshow('scan qrcode', frame)
# 等待键盘输入
key = cv2.waitKey(10)
# 当按下q键时关闭摄像头
if key == ord('q'):
break
# 销毁所有窗口
cv2.destroyAllWindows()
你们可以自己尝试运行一下上面的代码,效果就像是打开了自己的前置摄像头。现在调用了摄像头,我们可以把两部分的代码结合起来。五、实现扫码工具我们扫码工具的主体部分是调用摄像头的操作,我们需要对读取到的每一帧画面进行解析,当解析出结果后输出并退出。具体代码如下:import cv2
from pyzbar import pyzbar
def scan_qrcode(qrcode):
data = pyzbar.decode(qrcode)
return data[0].data.decode('utf-8')
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
cv2.imshow('scan qrcode', frame)
# 解析二维码
text = None
try:
text = scan_qrcode(frame)
except Exception as e:
pass
if text:
print(text)
break
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
上面我们把scan_qrcode函数修改了一下,从原来的传入图片路径到直接传入图片对象。因为通过VideoCapture对象获取的图片帧和通过cv2.imread获取的图片是同一数据类型。上面关键步骤在解析二维码的操作。首先定义一个text,因为解析过程中如果没有二维码会出现异常,所以用try-except语句处理。如何通过if判断text的内容,只有当我们真正解析到了数据,程序才会输出结果,并退出程序。
后端小马
程序员分手手册,教你如何恢复单身
一、前言首先声明,下面的言论纯属胡扯,请不要当真。在大家的印象当中,程序员是一个高薪职业,经常认为程序员是一个精英群体。现在我就告诉你们,这是真的。也正是因为这样,程序员非常受欢迎,通常一个程序员会有10到11个女生追(此处数字为二进制)。所以大多数程序员都不是单身,这也是程序员非常苦恼的地方。所以很多程序员都想方设法和女朋友分手,看到这篇文章的你幸运了,今天让你学以致用,写个分手小程序,让你享受单身的自由。二、哈?去旅游?昨天是5月20号,之所以不在昨天发还是出于对人身安全的考虑。程序员小汪有这么一个苦恼。他说:“我那个女朋友啊,烦得很,天天粘着我,代码都没时间打了。就昨天,还说要去旅游,她是想peach(屁吃)吧,我那么大个项目(实现登录注册),哪有时间陪她去旅游啊!”。听到了小汪的抱怨,我思索了一番,问:“你渴望单身吗?”。小汪回答:“那当然最好了,这样我就能安心写我的项目了”。于是我就给程序员小汪出了个主意,小汪以下面这个姿态来给我报喜:看样子他是成功了。我问小汪:“安排上了?”小汪略显沮丧:“失败了,不知道哪个流程出了问题”。我让小汪给我看看我让他做的东西,看完后我摇了摇头:“这个不行,太好看了,再丑点就能成功。”我让小汪改了改代码,过了一段时间,小汪又换了一副容颜。这次的他容光泛发,从他的表情来看,这回没问题了:他脸庞还有些许红润,嘴角带着血丝,但是仍掩盖不了他内心的喜悦。他说:“感谢大哥的教导,已经分了。虽然屏幕被砸了,键盘被崴了,鼠标也没尾巴了,但是都值了。”三、免责声明看到这里很多程序员同胞们肯定很好奇,我到底让小汪做了些啥。大家别急,我会告诉大家的。在此之前需要大家阅读以下声明:通过使用内容随之而来的风险与作者无关。访问者可将本文提供的内容或服务用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯本网站及相关权利人的合法权利。最后有啥问题请不要祖安作者,万分感谢。四、事情经过我给小汪的提议是,既然他女朋友想去旅游那你可以展现一下男人的魅力(抠门),详细描述你们可以在他们的聊天中得知:小汪说:“你旅游是去干啥啊?除了吃就是拍照,吃哪都可以吃啊!”汪妻答曰:“刚恋爱的时候都叫人小甜甜,现在还凶人家。人家就是想要拍照嘛!”这句话正好就中了小汪下的圈套,小汪讲道:“那好啊,我给你拍总行了吧!”说完,小汪坐到电脑前。汪妻以为他在订绿皮火车票,内心暗喜。于是在一旁刷起了抖音。万万没想到,小汪却打开了pycharm,一旁刷抖音的汪妻并没有注意到。首先,小汪按win+R输入了cmd,然后在命令行输入了下面两段代码:python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple
pip install -i https://mirror.baidu.com/pypi/simple paddlehub小汪默念道:“大哥是说要先安装paddlepaddle和paddlehub,看着是这两句。”小汪思索了一会儿:“抠图要怎么写来者?”他打开浏览器,输入了这个网址:别再自己抠图了,Python用5行代码实现批量抠图。“大哥写的太好了,一下就看懂了”,于是他继续往下写:from PIL import Image
import paddlehub as hub
# 加载注释
humanseg = hub.Module(name='deeplabv3p_xception65_humanseg')
# 抠图,执行后会生成一个humanseg_output目录,png图片与原图片同名
results = humanseg.segmentation(data={'image':['master.jpg']})
# 读取png图片
im = Image.open('humanseg_output/master.png')
# 通道分离
r, g, b, a = im.split()
# 读取背景图片
bg = Image.open('bg.jpg')
# 获取粘贴位置
size1 = im.size
size2 = bg.size
im.resize((size2[1], size2[1]))
x = size2[0]-size1[0]
y = size2[1]-size1[1]
# 将png图片粘贴到背景上
bg.paste(im, (x, y), mask=a)
# 保存结果图
bg.save('result.jpg')小汪运行了程序,发现居然报错了,跑来问我,发现是有模块没安装,他又在cmd中执行下列代码:pip install pillow这下运行没问题了,小汪摸着胡须,挠着头上所剩无几的头发,我估摸着,这一挠又挠死了几个字节的毛囊。他看着下面这几张图片(图中并非小汪女朋友):小汪心中想着,这下应该没问题了,于是就把实现好的图片给女朋友看。汪妻大怒:“你不想带我去玩你就说啊,还要弄一个程序糊弄我!”,接着扇了小汪一大嘴巴子。小汪暗喜,可是小汪并没能高兴太久。汪妻刷到了这么一条抖音:“有个程序员男朋友是什么体验?巴拉巴拉~”,在看到代码的那一刻,汪妻觉得自己误会小汪了,于是又和小汪和好了,小汪含泪接受了女朋友的道歉。出门后的小汪又找到我,我又给小汪出了个主意。小汪回到家,打开浏览器进入OpenCV官网:opencv.org/releases/ 。下载了对应版本的软件,然后安装,他在安装目录找到source\data\haarcascades目录,拿出了haarcascade_frontalface_default.xml文件,小汪有些迷惑,也没管太多,只是用浏览器打开了这个页面:OpenCv识别小罗伯特唐尼。心想,有啥不会的查一下就好了。然后小汪在cmd执行下面两句代码:pip install opencv-python
pip install opencv-contrib-python然后小汪想着我说的话,把女朋友的脸换成一个丑一点的人就好了。又写下了如下代码:import cv2
def face_detect(im):
"""检测人脸"""
im = cv2.imread(im)
grey = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
face_detector = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
faces = face_detector.detectMultiScale(grey)
# 返回一张人脸
return faces[0]写到这里,小汪发现haarcascade_frontalface_default.xml原来是在这里使用的,于是他把文件复制到项目下来。他心里想到:“这就可以检测人脸了!”。但是还不够,他继续写:def change_face(im, face_loc, face_im):
x, y, w, h = face_loc
# 读取女朋友图片
im = cv2.imread(im)
# 读取要换的脸
face_im = cv2.imread(face_im)
face_im = cv2.resize(face_im, (w, h))
# 人脸区域切换
im[y:y + h, x:x + w] = face_im
# 保存
cv2.imwrite('result.jpg', im)小汪又在main中写到:if __name__ == '__main__':
face = face_detect('master.jpg')
change_face('master.jpg', face, 'face.jpg')运行后结果图出来了,小汪点开结果图,大声笑了起来:汪妻被笑声吸引,看到屏幕上的图片勃然大怒。崴了键盘,砸了屏幕,扯断了鼠标,还把小汪给胖揍了一顿,小汪血流不止,但是还止不住笑声。汪妻果断提出分手。到此事情算是画上了一个完美的句号。五、事情结束了?很多人疑惑,小汪为什么要这么做,大家都觉得小汪这样做很激进。但是其实小汪才是最清醒的那个,小汪在日记中写道:“别人笑我太疯癫,我笑他人看不穿”。就在这时,小汪的女朋友回来了:“我刚刚刷到了一个抖音,原来是我不懂程序员的幽默,我错了,你能原谅我吗?”
后端小马
Python识别图片中的文字
一、前言不知道大家有没有遇到过这样的问题,就是在某个软件或者某个网页里面有一篇文章,你非常喜欢,但是不能复制。或者像百度文档一样,只能复制一部分,这个时候我们就会选择截图保存。但是当我们想用到里面的文字时,还是要一个字一个字打出来。那么我们能不能直接识别图片中的文字呢?答案是肯定的。二、Tesseract文字识别是ORC的一部分内容,ORC的意思是光学字符识别,通俗讲就是文字识别。Tesseract是一个用于文字识别的工具,我们结合Python使用可以很快的实现文字识别。但是在此之前我们需要完成一个繁琐的工作。(1)Tesseract的安装及配置Tesseract的安装我们可以移步到该网址 https://digi.bib.uni-mannheim.de/tesseract/,我们可以看到如下界面:有很多版本供大家选择,大家可以根据自己的需求选择。其中w32表示32位系统,w64表示64位系统,大家选择合适的版本即可,可能下载速度比较慢,大家可以选择链接:pan.baidu.com/s/1jKZe_ACL… 提取码:ayel下载。安装时我们需要知道我们安装的位置,将安装目录配置到系统path变量当中,我们路径是D:\CodeField\Tesseract-OCR。我们右击我的电脑/此电脑->属性->高级系统设置->环境变量->Path->编辑->新建然后将我们的路径复制进去即可。添加好系统变量后后我们还需要依次点确定,这样才算配置好了。(2)下载语言包Tesseract默认是不支持中文的,如果想要识别中文或者其它语言需要下载相应的语言包,下载地址如下: https://tesseract-ocr.github.io/tessdoc/Data-Files ,进入网站后我们往下翻:其中有两个中文语言包,一个Chinese-Simplified和Chinese-Traditional,它们分别是简体中文和繁体中文,我们选择需要的下载即可。下载完成后我们需要放到Tesseract的路径下的tessdata目录下,我们路径是D:\CodeField\Tesseract-OCR\tessdata。(3)其它模块下载除了上面的步骤,我们还需要下载两个模块:pip install pytesseract
pip install pillow
第一个是用于文字识别的,第二个是用于图片读取的。接下来我们就可以进行文字识别了。三、文字识别(1)单张图片识别接下来的操作就要简单的多,下面是我们要识别的图片:接下来就是我们文字识别的代码:import pytesseract
from PIL import Image
# 读取图片
im = Image.open('sentence.jpg')
# 识别文字
string = pytesseract.image_to_string(im)
print(string)
识别结果如下:Do not go gentle into that good night!
因为默认是支持英文的,所以我们可以直接识别,但是当我们要识别中文或其它语言时就需要做些修改:import pytesseract
from PIL import Image
# 读取图片
im = Image.open('sentence.png')
# 识别文字,并指定语言
string = pytesseract.image_to_string(im, lang='chi_sim')
print(string)
在识别时,我们设置lang='chi_sim',也就是把语言设置为简体中文,只有当你的tessdata目录下有简体中文包该设置才会生效。下面是我们用来识别的图片:识别结果如下:不 要 温 顺 的 走 进 那 个 良 夜
图片内容被准确识别出来了。有一点我们需要知道,在我们将语言设置为简体中文或其它语言后,Tesseract还是可以识别出英文字符。(2)批量图片识别既然我们把单张图片识别列出来了,就肯定还有批量图片识别这个功能,这就需要我们准备一个txt文件了,比如我有text.txt文件,内容如下:sentence1.jpg
sentence2.jpg
我们将代码修改为如下:import pytesseract
# 识别文字
string = pytesseract.image_to_string('text.txt', lang='chi_sim')
print(string)
但是这样自己写一个txt文件难免有些麻烦,因此我们又可以进行如下修改:import os
import pytesseract
# 文字图片的路径
path = 'text_img/'
# 获取图片路径列表
imgs = [path + i for i in os.listdir(path)]
# 打开文件
f = open('text.txt', 'w+', encoding='utf-8')
# 将各个图片的路径写入text.txt文件当中
for img in imgs:
f.write(img + '\n')
# 关闭文件
f.close()
# 文字识别
string = pytesseract.image_to_string('text.txt', lang='chi_sim')
print(string)
这样我们只需要传入一个文字图片的根目录就可以批量进行识别了。在测试过程中发现,Tesseract对手写体、行楷等飘逸的字体识别不准确,对一些复杂的字识别也有待提升。但是宋体、印刷体等笔画严谨的字体识别准确率很高。另外如果图片的倾斜大于一定的角度,识别结果也会有很大差别。
后端小马
图像隐写,如何在图像中隐藏二维码
一、前言在某个App中有一个加密水印的功能,当帖子的主人开启了之后。如果有人截图,那么这张截图中就是添加截图用户、帖子ID、截图时间等信息,而且我们无法用肉眼看出这些水印。这可以通过今天要介绍的隐写技术来实现,我们会通过这种技术,借助Python语言和OpenCV模块来实现在图像中隐藏二维码的操作。而且这个二维码无法通过肉眼看出。二、隐写隐写是一种类似于加密却又不同于加密的技术。通常情况下,加密是对数据本身进行一个转换,得到的结果是一堆人无法解读的数据,比如“你好”进行md5加密后的结果是“7eca689f0d3389d9dea66ae112e5cfd7”,如果光看“7eca689f0d3389d9dea66ae112e5cfd7”我们不知道内容,但是我们知道这应该是加密后的数据。隐写的目的同样是让只有接收方才能获取数据,但是隐写通常更加隐蔽,隐写更注重于不让第三方知道我发送的数据中有额外信息。就像我们在电影中经常看到的一些剧情,一场看似普通的对话却隐含了许多外人不知道的信息,这实际上就是一种隐写。再比如“This is a pig”,看上去像一个普通的句子,如果通信双方规定“T、i、s”这些占三线格上两个的字母表示0,而“p、g”这种占三线格下两格的字母表示1,那么这句话就可以翻译成“0000000101”。而今天我们要介绍的是“最低有效位”隐写。三、位平面分解在介绍“最低有效位”隐写之前,需要了解一些图像相关的知识。这里包括数字图像、位平面、位平面分解。3.1 图像在计算机中,图像被表示为一个数字矩阵,每个数字被称为一个像素,它们的取值在[0, 255]区间,可以用8个二进制来表示。这个矩阵大小由图像分辨率决定,如果是480×480分辨率的图像,那么这个矩阵大小就是480×480。如果是彩色图像,会用三个大小相同的矩阵合起来表示,它们分别表示图像R(红色)、G(绿色)、B(蓝色)的程度,也就是俗称的RGB图像。我们可以用OpenCV来读取图像,OpenCV的安装如下:pip install opencv-python安装完成后就可以读取图像:# 导入模块
import cv2
# 读取图像
img = cv2.imread('test.jpg')
# 输出图像
print(img)其中test.jpg就是我们的图像名称或者图像路径。上面代码输出结果如下:[[[ 72 220 234]
[ 72 220 234]
[ 73 221 235]
...
[ 87 147 176]
[ 87 147 176]
[ 87 147 176]]]因为输出过长,这里省略了一部分内容。3.2 位平面在前面我们说了一个图像是一个数字矩阵,比如:[[2, 2]
[3, 4]]我们可以理解为一张简单的图像,现在我们把图像的像素值写成二进制形式:[[0000 0010, 0000 0010],
[0000 0011, 0000 0100]]我们把四个像素的最高位取出,得到新的图像:[[0, 0]
[0, 0]]这个过程的图示如下:这里取出来的图像就叫位平面,因为是取出第7位(从左到右依次是7-0)组成的图像,所以叫第7位平面,也叫最高位平面。而第0位平面也叫“最低有效位”位平面。如果取出第1位,得到的图像为:[[1, 1],
[1, 0]]这个图像叫第1位平面。这里需要注意一点,就是每个位平面的实际值应该乘一个权重,这个权重位i^2,即第7位平面的权重位7^2。3.3 位平面分解下面我们看看如何分解位平面,分解位平面可以用cv2.bitwise_and函数来实现。我们需要传入一个图像以及一个分解因子,各个位平面的分解因子如下:分解因子作用0x80分解第7位平面0x40分解第6位平面0x20分解第5位平面0x10分解第4位平面0x08分解第3位平面0x04分解第2位平面0x02分解第1位平面0x11分解第0位平面比如分解第7位平面的操作为:import cv2
# 读取图像
img = cv2.imread('test.jpg', 0)
# 分解第7位平面
layer = cv2.bitwise_and(img, 0x80)其它位平面的分解只需要对照表进行修改即可。3.4 位平面合成假如我们以及分解出来8个位平面,分别是M0、M1、...、M7。我们只需要将各个位平面乘上对应的权重,然后相加就能恢复原图,即:如果我们只对M1-M7进行合成,得到的A`与A的差距最多为1,因此我们可以让A`≈A。此时图像A`的第0个位平面可以用于隐藏数据。四、图像隐写这里我们使用一种叫“最低有效位”位平面隐写的技术来实现二维码的隐藏。其原理就是把图像“最低有效位”位平面设置为0,此时图像与原图像像素相差最大为0,人肉眼无法看出区别。然后我们可以在图像的最低有效位任意设置值,此时图像与原图像素相差最大仍是1。这样我们就可以用“最低有效位”位平面来隐写数据。在前面我们合成原图时用M1-M7,而M0位平面则全为0,这时我们可以用最低有效位存储数据。假如我们的数据矩阵为M,该矩阵为一个0-1矩阵。而二维码就是一个黑白矩阵,我们可以把黑当作0,白当作1,这样我们让M为一个二维码的矩阵。现在我们通过下面的公式来合成:这个A就是带有隐写信息的图像。代码实现如下:import cv2
# ①读取图像
img = cv2.imread('test.jpg', 0)
# ②把最低有效位清空
img -= cv2.bitwise_and(img, 0x01)
# ③准备需要隐写的信息M
M = cv2.imread('qrcode.jpg', 0)
M = cv2.resize(M, img.shape)
# 把二维码转换成0-1矩阵
_, M = cv2.threshold(M, 30, 1, cv2.THRESH_BINARY)
# ④将要隐写的数据设置到图像最低有效位
img += M
# ⑥以无损的方式保存隐写后的
cv2.imwrite('dst.png', img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])最后保存的dst.png就是我们隐写后的图像。
后端小马
Python生成九宫格图片
一、前言大家在朋友圈应该看到过用一张图片以九宫格的方式显示,效果大致如下:要实现上面的效果非常简单,我们只需要截取图片的九个区域即可。今天我们就要带大家使用Python来实现一下九宫格图片的生成。在开始之前,我们需要安装一下Pillow模块,语句如下:pip install pillow
下面我们先来看看一些简单的图片操作。二、图片基本操作今天我们会使用到三个操作,分别是读取图片、保存图片和截取图片。下面我们分别来看看。2.1 读取图片在Pillow中,我们最常用的就是Image子模块。其中读取图片的操作就是通过Image.open函数来实现。Image.open函数会返回一个图片对象,我们来看看具体的代码:from PIL import Image
# 读取图片
img = Image.open('lbxx.jpg')
Pillow模块是PIL模块的python3版本,因此我们导入模块时是使用下面语句:from PIL import Image
后面我们就可以通过操作img对象来实现对图片的操作。2.2 截取图片在Image对象中,有一个crop方法,可以用于剪切图片。它接收一个box参数,表示要截取的区域。参数是一个元组,元素内容分别是左上角x,y坐标,右下角x,y坐标。图片中的坐标系是以左上角为原点的,如图:假如我们需要截取图片如下区域:那我们的参数应该如下:img.crop((x1, y1, x2, y2))
我们来看看具体的代码:from PIL import Image
# 读取图片
img = Image.open('lbxx.jpg')
# 截取图片的(0, 0, 300, 300)区域
box = img.crop((0, 0, 300, 300))
# 显示截取的区域
box.show()
2.3 保存图片保存图片的操作非常简单,我们只需要调用img的save方法即可,我们直接看代码:from PIL import Image
img = Image.open('lbxx.jpg')
box = img.crop((0, 0, 300, 300))
# 保存图片
box.save('1.jpg')
我们直接调用save方法,传入保存的路径即可。三、生成九宫格图片知道了上面的操作,下面的操作无非就是截取图片的九个区域,然后保存即可。具体代码如下:from PIL import Image
# 读取图片
im = Image.open('lbxx.jpg ')
# 宽高各除 3,获取裁剪后的单张图片大小
width = im.size[0]//3
height = im.size[1]//3
# 裁剪图片的左上角坐标
start_x = 0
start_y = 0
# 用于给图片命名
im_name = 1
# 循环裁剪图片
for i in range(3):
for j in range(3):
# 裁剪图片并保存
crop = im.crop((start_x, start_y, start_x+width, start_y+height))
crop.save('imgs/' + str(im_name) + '.jpg')
# 将左上角坐标的 x 轴向右移动
start_x += width
im_name += 1
# 当第一行裁剪完后 x 继续从 0 开始裁剪
start_x = 0
# 裁剪第二行
start_y += height
我们先创建一个imgs目录,然后运行程序就可以在imgs下看到截取好的图片。不过上面的代码还有些不便之处,就是我们需要手动创建imgs目录。我们可以借助os模块来帮我们自动创建改目录,修改后的代码如下:import os
from PIL import Image
# 读取图片
im = Image.open('1kkx.jpg')
# 宽高各除 3,获取裁剪后的单张图片大小
width = im.size[0]//3
height = im.size[1]//3
# 裁剪图片的左上角坐标
start_x = 0
start_y = 0
# 用于给图片命名
im_name = 1
# 循环裁剪图片
for i in range(3):
for j in range(3):
# 裁剪图片并保存
crop = im.crop((start_x, start_y, start_x+width, start_y+height))
# 判断文件夹是否存在
if not os.path.exists('imgs'):
os.mkdir('imgs')
crop.save('imgs/' + str(im_name) + '.jpg')
# 将左上角坐标的 x 轴向右移动
start_x += width
im_name += 1
# 当第一行裁剪完后 x 继续从 0 开始裁剪
start_x = 0
# 裁剪第二行
start_y += height
我们进行了一个简单的判断,如何再决定要不要创建文件夹。最终效果是一样的。
后端小马
如何用Python发送邮件?
一、前言相信邮箱对许多人来说只是一个全是推销邮件的垃圾桶,或者接收验证码的一个工具。但是邮箱其实还有很多作用,其中最重要的作用就是消息交流。现在我们传递消息的方式有很多种,像是比较流行QQ、微信,或者微博、知乎这种社交软件。甚至我们还可以剑走偏锋的方式,比如支付宝、淘宝这种软件进行交流。但是这些软件通常都需要我们登录,而且要在相应的客户端才能进行操作。而邮件则不一样,很多语言都提供了邮件相关操作的API,我们只需要有一个邮箱,就可以很随意的发送邮件。而且邮件的监管相比其它软件要松地多。那说了这么多,我们能用邮件做些什么呢?在我的实际工作学习中,我喜欢把邮件当作一个提醒工具。有时候一些程序的执行需要很长时间,这个适合就可以在程序运行成功后给我们的手机发邮件。这样我们就能很及时的进行下一步的工作。二、准备工作在发送邮件之前,我们需要先获取一个邮箱的授权码。这个授权码相当于你的邮箱密码,通常可以在网页版邮箱的设置中获取。这里以163邮箱为例,首先登录邮箱:mail.163.com/。登录后可以看到如下页面:POP3/SMTP/IMAP选项,然后会看到如下页面:点击开启,然后按照要求发送短信即可获取授权码。这个授权码只会显示一次,因此需要保存好。获取授权码后,我们就可以开始发送邮件了。三、发送邮件在python中自带了smtplib模块用于发送邮件,但是使用起来比较复杂。我们今天直接使用封装好的yagmail模块进行邮件的发送。我们先来安装yagmail:pip install yagmail
接下来的使用就非常简单了,基本步骤大致如下:准备用于发送邮件的邮箱创建SMTP对象准备要发送的内容发送邮件关闭连接具体代码如下:import yagmail
# 1、准备用于发送邮件的邮箱
username = "sockwz@163.com"
password = "你的授权码"
# 2、创建SMTP对象
yag = yagmail.SMTP(user=username, password=password, host="smtp.163.com")
# 3、准备要发送的内容
content = [
"这是一封邮件"
]
# 4、发送邮件
yag.send(to="2930777518@qq.com", subject="测试邮件", contents=content)
# 5、关闭连接
yag.close()
这里又几点需要注意:(1)STMP其中SMTP其实是一种邮箱协议,我们使用yagmail.SMTP创建SMTP对象,它给我们封装了底层的细节。我们只需要把用于发送邮件的邮箱和授权码给它,已经邮箱服务器ip给它就好了。这里又出现了一个邮箱服务器的概念,通常发送邮件的流程是:客户端A -> 邮箱服务器 -> 客户端B。其中邮箱服务器充当了邮递员的身份,我们需要告诉程序要哪个邮递员发邮件。因为我们使用的是SMTP协议,而且是163的邮箱,因此我们需要填163邮箱服务器的ip。通常情况下邮箱服务器ip格式为:协议名.邮箱公司名.com。当然这个不是固定的,具体的可以在网页版的设置中查看,比如网易邮箱的ip如下:(2)邮箱内容邮箱内容需要是一个列表。(3)发送邮件我们发送邮件的代码如下:yag.send(to="2930777518@qq.com", subject="测试邮件", contents=content)
这里我们使用了三个参数,其中to是接收方的邮箱。subject是邮件的主体,contents是邮箱内容。yag.send函数还有很多其它参数,这里就不再细说了。四、发送附件附件的发送非常简单,我们只需要在contents参数中写入附件的参数即可,比如下面这样:import yagmail
username = "sockwz@163.com"
password = "你的授权码"
yag = yagmail.SMTP(user=username, password=password, host="smtp.163.com")
content = [
# 附件的路径
"xyql.jpg"
]
yag.send(to="2930777518@qq.com", subject="测试邮件", contents=content)
yag.close()
因为在我的程序下有一个叫xyql.jpg的图片,所以我直接写就好了。当然有时候我们还需要让图片直接显示在邮件中,这种情况我们只需要调用一下yagmail.inline函数即可,代码如下:import yagmail
username = "sockwz@163.com"
password = "你的授权码"
yag = yagmail.SMTP(user=username, password=password, host="smtp.163.com")
content = [
'娜娜酱',
yagmail.inline("xyql.jpg")
]
yag.send(to="2930777518@qq.com", subject="测试邮件", contents=content)
yag.close()
这里需要注意一下,在测试过程种发现不能直接单独发内敛图片,而需要配一些文字发送,不然会被邮箱服务器退信。下面我们分别看看附件和内敛图片的区别:区别还是很明显的。五、发送html邮件yagmail本身就是将文字作为html发送的,因此只要我们发送html邮件不需要做什么改变。但是为了方便,我们还是把html写一个单独的文件,比如文件index.html:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我是一个测试页面</title>
</head>
<body>
<h1>这是一个测试标题</h1>
<p style="color: red">这是一个测试内容</p>
</body>
</html>
显示效果如下:效果很简单,然后我们把上面的代码搬过来,稍作修改:username = "sockwz@163.com"
password = "你的授权码"
yag = yagmail.SMTP(user=username, password=password, host="smtp.163.com")
content = [
# 直接从html文件中读取内容
open('index.html', 'r', encoding='utf-8').read()
]
yag.send(to="2930777518@qq.com", subject="测试邮件", contents=content)
yag.close()
上面我们直接用open读取html的内容,然后发送。下面是接收到的效果图:可以看到邮件正常接收。上面这些操作就可以满足我们工作的大多数需求了,大家可以自己定制一个提醒程序。
后端小马
Python10行以内代码能有什么高端操作
前言Python凭借其简洁的代码,赢得了许多开发者的喜爱。因此也就促使了更多开发者用Python开发新的模块,从而形成良性循环,Python可以凭借更加简短的代码实现许多有趣的操作。下面我们来看看,我们用不超过10行代码能实现些什么有趣的功能。一、生成二维码二维码作为一种信息传递的工具,在当今社会发挥了重要作用。而生成一个二维码也非常简单,在Python中我们可以通过MyQR模块了生成二维码,而生成一个二维码我们只需要2行代码,我们先安装MyQR模块,这里选用国内的源下载:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ myqr安装完成后我们就可以开始写代码了:from MyQR import myqr # 注意大小写
myqr.run(words='http://www.baidu.com') # 如果为网站则会自动跳转,文本直接显示,不支持中文我们执行代码后会在项目下生成一张二维码。当然我们还可以丰富二维码:from MyQR import myqr
myqr.run(
words='http://www.baidu.com', # 包含信息
picture='lbxx.jpg', # 背景图片
colorized=True, # 是否有颜色,如果为False则为黑白
save_name='code.png' # 输出文件名
)另外MyQR还支持动态图片。二、生成词云词云是数据可视化的一种非常优美的方式,我们通过词云可以很直观的看出一些词语出现的频率高低。使用Python我们可以通过wordcloud模块生成词云,我们先安装wordcloud模块:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ wordcloud然后我们就可以写代码了:from wordcloud import WordCloud
wc = WordCloud() # 创建词云对象
wc.generate('Do not go gentle into that good night') # 生成词云
wc.to_file('wc.png') # 保存词云执行代码后生成如下词云:然这只是最简单的词云,词云更详细的操作可以参见WordCloud生成卡卡西忍术词云。三、批量抠图抠图的实现需要借助百度飞桨的深度学习工具paddlepaddle,我们需要安装两个模块就可以很快的实现批量抠图了,第一个是PaddlePaddle:python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple还有一个是paddlehub模型库:pip install -i https://mirror.baidu.com/pypi/simple paddlehub更详细的安装事项可以参见飞桨官网:www.paddlepaddle.org.cn/接下来我们只需要5行代码就能实现批量抠图:import os, paddlehub as hub
humanseg = hub.Module(name='deeplabv3p_xception65_humanseg') # 加载模型
path = 'D:/CodeField/Workplace/PythonWorkplace/GrapImage/' # 文件目录
files = [path + i for i in os.listdir(path)] # 获取文件列表
results = humanseg.segmentation(data={'image':files}) # 抠图抠图效果如下:其中左边为原图,右边为抠图后填充黄色背景图。四、文字情绪识别在paddlepaddle面前,自然语言处理也变得非常简单。实现文字情绪识别我们同样需要安装PaddlePaddle和Paddlehub,具体安装参见三中内容。然后就是我们的代码部分了:import paddlehub as hub
senta = hub.Module(name='senta_lstm') # 加载模型
sentence = [ # 准备要识别的语句
'你真美', '你真丑', '我好难过', '我不开心', '这个游戏好好玩', '什么垃圾游戏',
]
results = senta.sentiment_classify(data={"text":sentence}) # 情绪识别
# 输出识别结果
for result in results:
print(result)识别的结果是一个字典列表:{'text': '你真美', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.9602, 'negative_probs': 0.0398}
{'text': '你真丑', 'sentiment_label': 0, 'sentiment_key': 'negative', 'positive_probs': 0.0033, 'negative_probs': 0.9967}
{'text': '我好难过', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.5324, 'negative_probs': 0.4676}
{'text': '我不开心', 'sentiment_label': 0, 'sentiment_key': 'negative', 'positive_probs': 0.1936, 'negative_probs': 0.8064}
{'text': '这个游戏好好玩', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.9933, 'negative_probs': 0.0067}
{'text': '什么垃圾游戏', 'sentiment_label': 0, 'sentiment_key': 'negative', 'positive_probs': 0.0108, 'negative_probs': 0.9892}其中sentiment_key字段包含了情绪信息,详细分析可以参见Python自然语言处理只需要5行代码。五、识别是否带了口罩这里同样是使用PaddlePaddle的产品,我们按照上面步骤安装好PaddlePaddle和Paddlehub,然后就开始写代码:import paddlehub as hub
# 加载模型
module = hub.Module(name='pyramidbox_lite_mobile_mask')
# 图片列表
image_list = ['face.jpg']
# 获取图片字典
input_dict = {'image':image_list}
# 检测是否带了口罩
module.face_detection(data=input_dict)执行上述程序后,项目下会生成detection_result文件夹,识别结果都会在里面,识别效果如下:六、简易信息轰炸Python控制输入设备的方式有很多种,我们可以通过win32或者pynput模块。我们可以通过简单的循环操作来达到信息轰炸的效果,这里以pynput为例,我们需要先安装模块:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ pynput在写代码之前我们需要手动获取输入框的坐标:from pynput import mouse
# 创建一个鼠标
m_mouse = mouse.Controller()
# 输出鼠标位置
print(m_mouse.position)可能有更高效的方法,但是我不会。获取后我们就可以记录这个坐标,消息窗口不要移动。然后我们执行下列代码并将窗口切换至消息页面:import time
from pynput import mouse, keyboard
time.sleep(5)
m_mouse = mouse.Controller() # 创建一个鼠标
m_keyboard = keyboard.Controller() # 创建一个键盘
m_mouse.position = (850, 670) # 将鼠标移动到指定位置
m_mouse.click(mouse.Button.left) # 点击鼠标左键
while(True):
m_keyboard.type('你好') # 打字
m_keyboard.press(keyboard.Key.enter) # 按下enter
m_keyboard.release(keyboard.Key.enter) # 松开enter
time.sleep(0.5) # 等待 0.5秒我承认,这个超过了10行代码,而且也不高端。使用前QQ给小号发信息效果如下:七、识别图片中的文字我们可以通过Tesseract来识别图片中的文字,在Python中实现起来非常简单,但是前期下载文件、配置环境变量等稍微有些繁琐,所以本文只展示代码:import pytesseract
from PIL import Image
img = Image.open('text.jpg')
text = pytesseract.image_to_string(img)
print(text)其中text就是识别出来的文本。如果对准确率不满意的话,还可以使用百度的通用文字接口。八、绘制函数图像图标是数据可视化的重要工具,在Python中matplotlib在数据可视化中发挥重要作用,下面我们来看看使用matplotlib如何绘制一个函数图像:import numpy as np
from matplotlib import pyplot as plt
x = np.arange(1,11) # x轴数据
y = x * x + 5 # 函数关系
plt.title("y=x*x+5") # 图像标题
plt.xlabel("x") # x轴标签
plt.ylabel("y") # y轴标签
plt.plot(x,y) # 生成图像
plt.show() # 显示图像生成图像如下:九、人工智能下面给大家介绍的是独家的AI人工智能,一般不外传的。这个人工智能可以回答许多问题,当然人工智能现在还在发展阶段,想要理解人类的语言还差很多。废话不多说,下面来看看我们的人工智能Fdj:while(True):
question = input()
answer = question.replace('吗', '呢')
answer = answer.replace('?', '!')
print(answer)下面我们来看看简单的测试:你好吗?
我好呢!
你吃饭了吗?
我吃饭了呢!
你要睡了吗?
我要睡了呢!看来我们“小复”还是比较智能的。
后端小马
OpenCV如何去除图片中的阴影
一、前言如果你自己打印过东西,应该有过这种经历。如果用自己拍的图片,在手机上看感觉还是清晰可见,但是一打印出来就是漆黑一片。比如下面这两张图片:因为左边的图片有大片阴影,所有打印出来的图片不堪入目(因为打印要3毛钱,所以第二张图片只是我用程序模拟的效果)。那有什么办法可以解决吗?答案是肯定的,今天我们就来探讨几个去除阴影的方法。二、如何去除阴影?首先为了方便处理,我们通常会对图片进行灰度转换(即将图片转换成只有一个图层的灰色图像)。然后我们分析一下,在上面的图片中有三个主色调,分别是字体颜色(黑色)、纸张颜色(偏白)、阴影颜色(灰色)。知道这点后我们就好办了。我们只需要把灰色和白色部分都处理为白色就好了。那要我怎么才知道白色和灰色区域呢?对于一个8位的灰度图,黑色部分的像素大致在0-30左右。白色和灰色应该在31-255左右(这个范围只是大致估计,实际情况需要看图片)。如图:左边是原图,右边是处理后的图片。我们将灰色和接近白色的部分都处理成了白色。那下面我们就开始处理吧。三、numpy的ndarray数组可能有些读者没有接触过numpy,这里简单说一下。numpy是一个第三方的模块,用它我们可以很方便的处理多维数组(ndarray数组)。而图片在OpenCV中的存储方式正好是ndarray,所以我们对数组的操作就是对图片的操作。在使用之前我们需要安装一下OpenCV模块:pip install opencv-python
在安装OpenCV时会自动安装numpy。下面我们主要是看看布尔索引的操作,先看下面代码:import numpy as np
# 创建一个元素为1, 0, 1, 1的ndarray数组
arr = np.array([1, 0, 1, 1])
# 判断数组中有没有0
res = arr == 0
# 将数组中为0的元素赋值为10
arr[res] = 10
如果没有接触过numpy会不太理解上面的语法。我们来详细说一下:创建ndarray数组:我们通过np.array可以将现有的列表装换成一个ndarray对象,这个很好理解判断数组中有没有0:我们可以直接用ndarray对象来判断,比如:arr == 0,他会返回一个元素结构和数量一样的ndarray对象。但是返回的对象原始类型式bool,我们来看看res的输出: python复制代码[False True False False] 从结果可以看出,我们比较arr==0就是对数组中每个元素进行比较,并返回比较的布尔值。将数组中为0的元素赋值为10:而最难理解的arr[res]操作。它其实就是拿到res中为True的视图,比如上面的结果是第二个为True则只会返回第二个元素的视图。我们执行下面的代码: python复制代码arr[res] = 10 就是把对应res为True的部分赋值为10,也就是将arr中值为0的部分赋值为10。下面是arr最后的结果:[ 1 10 1 1]
可以看到原本的0处理为了1。四、去除阴影现在我们知道了布尔索引,我们可以对图片进行处理了。我们只需要读取图片,然后将像素值大于30的部分处理为白色就好了。下面是我们的代码:import cv2
# 读取图片
img = cv2.imread('page.jpg', 0)
# 将像素值大于30的部分修改为255(白色)
img[img > 30] = 255
# 保存修改后的图片
cv2.imwrite('res.jpg', img)
上面的代码非常简单,我们使用cv2.imread函数读取图片,第一个参数是图片路径,第二个参数表示读取为灰度图。我们来看看效果图:可以看到阴影部分被很好地去除了。有些字比较模糊,我们可以通过调节灰白色地范围调整。比如:img[img > 40] = 255
具体的值就要根据要处理的图片来决定了。五、改进对于上面地处理,还可以做一个小小地改进。我们可以让纸张颜色不那么白,我们来看改进后的代码:import cv2
import numpy as np
img = cv2.imread('page.jpg', 0)
# 计算灰白色部分像素的均值
pixel = int(np.mean(img[img > 140]))
# 把灰白色部分修改为与背景接近的颜色
img[img > 30] = pixel
cv2.imwrite('res.jpg', img)
在上面的代码中我们不再是将灰白色部分设置为255,而是事先计算了一个数值。pixel = int(np.mean(img[img > 140]))
猜测阴影部分的颜色值小于140,因此先索引出图像中大于140的部分。然后求平均值,这样我们算出来的大致就是原图的背景颜色,然后将图片不是文字的部分处理为背景颜色,就是最终结果了。下面是我们的效果图:可以看到这次效果要更好了。但是因为背景都是一个颜色,所以看起来还是会有一些差别。不过有一点需要说一下,上面的操作只适用于比较简单的图片,比如试卷这种。
后端小马
Python玩转各种多媒体,视频、音频到图片
前言我们经常会遇到一些对于多媒体文件修改的操作,像是对视频文件的操作:视频剪辑、字幕编辑、分离音频、视频音频混流等。又比如对音频文件的操作:音频剪辑,音频格式转换。再比如我们最常用的图片文件,格式转换、各个属性的编辑等。因为多媒体文件的操作众多,本文选取一些极具代表性的操作,以代码的形式实现各个操作。一、图片操作操作图片的模块有许多,其中比较常用的两个就是Pillow和 opencv,两个模块各有优势。其中opencv是计算机视觉处理的开源模块,应用的范围更加广泛,从图像处理到视频处理,再到物体检测等。而pillow相比直线就单纯的多,其大多数操作都是围绕图像而展开的。1.1、格式转换图片格式有多种多样,最直观的感受就是图片后缀。而格式之间的差异不仅仅是后缀的差异,最为明显的就是png格式图片,同其它图片有着最为直观的区别。下面我们就看看在Python中如何转换格式,我们先安装pillow模块:pip install pillow
然后看看如何导入模块以及如何读取图像:from PIL import Image
# 读取图像
img = Image.open('ycjc.jpg')
# 显示图像
img.show()
我们有了上面的基础操作之后,就可以开始进行格式转换了,我们用有村大妹子的图片作为素材:我们可以看到这是一张白色背景的图片,我们将它转成png看看效果:from PIL import Image
# 读取图像
img = Image.open('ycjc.jpg')
# 格式转换,其中A为透明度
png = img.convert('RGBA')
# 保存图像,因为是RGBA格式,所以后缀应该为png
png.save('ycjc.png')
输出的图片我就不放了,我们观察输出图片会发现,白色背景好像变透明了。不要怀疑,这只是心理作用,其实图片看上去是不会又任何变化的。但是实际上图片从原来的RGB三个色道变成了RGBA四个色道,我们会发现,图片变大了:虽然A色道的透明度全部都是0,但是实际上还是存在这个色道,所以大小方面有了明显的增加。更多转换模式可以产考超全Python图像处理讲解(多图预警)。1.2、图片裁剪图片裁剪的操作也非常常用,我们来看看pillow如何裁剪图片:from PIL import Image
# 读取图像
img = Image.open('ycjc.jpg')
# 裁剪图像,调用crop方法,传入裁剪区域的元组
img_crop = img.crop((100, 100, 400, 400))
# 保存裁剪后的图像
img_crop.save('ycjc_crop.jpg')
我们调用crop方法,传入裁剪区域的元组进行裁剪,元组的内容为左上角的坐标(前两个参数)即右下角的坐标(后两个参数)。效果图如下:1.3、截屏虽然算不上是图像操作,但是还是个非常实用的操作。我们实现截屏是通过ImageGrap类实现的:from PIL import ImageGrab
# 截取全屏
im = ImageGrab.grab()
# 保存图像
im.save('win.png')
除了截取全屏我们也可以区域截屏:im =ImageGrab.grab((300, 100, 1400, 600))
参数元组含义与crop方法一致。除了上述操作,还有更多像是添加滤镜、对比度调节、亮度调节、色彩调节等,由于内容众多,所以不在本文详细讨论。二、音频操作音频的操作也比较繁多,我们最常用到的就是音频剪辑和音量调节了。我们这里使用pydub模块来进行音频文件的操作。2.1、pydub的安装以及读取音频安装我们还是使用pip:pip install pydub然后我们来读取一个wav文件:from pydub import AudioSegment
# 读取wav格式的音频文件
music = AudioSegment.from_wav('百年孤独.wav')这样我们就完成了音频文件的读取,wav文件是一种未经压缩的文件,我们可以通过pydub直接读取。读取其它类型的文件pydub同样提供了相应的方法:music = AudioSegment.from_mp3('music.mp3')
music = AudioSegment.from_ogg("music.ogg")
music = AudioSegment.from_flv("music.flv")因为在实际操作过程中遇到了一个未找明缘由的错误,所以本文的音频操作只针对wav格式。2.2、音频剪切音频剪辑的实现我们是通过类似ndarray的中括号操作的:# 截取前20秒
clip = music[:20*1000]
# 截取后20秒
clip = music[-20000:]
# 从第20秒截取到第40秒
clip = music[20*1000:40*1000]剪切好的片段我们可以另外存一个文件:# 保存文件为clip.mp3,格式为mp3
clip.export('clip.mp3', format='mp3')3.3、增加/减少音量音量的控制我们只需要用音频对象加一个常数即可:# 音量减5
music -= 5
# 音量加5
music += 53.4、音频拼接我们先看看重复拼接:# 在音频文件末尾重复拼接该音频
music = music*2拼接后的音频的效果就是原音频循环两次。接下来我们看看拼接不同的音频:# 裁剪前20秒音频
clip1 = music[:20*1000]
# 裁剪后20秒音频
clip2 = music[-20*1000:]
# 拼接音频
clip = clip1 + clip23.5、 交叉渐入渐出交叉渐入渐出是一种比较柔和的音频专场方式,在两个音频切换的间歇会有一个重合,用代码实现如下:# 截取前20秒
begin = music[20*1000:40*1000]
# 截取后20秒
end = music[-20*1000:]
# 添加交叉渐入渐出 效果
clip = begin.append(end, crossfade=1500)我们可以看到我们一共裁剪了40秒,在生成的文件我们可以看到只有38秒,因为转场的时候有个重合的效果。当然还有更多的操作,大家可以自己去了解。三、视频操作视频的操作可以通过moviepy和opencv进行,我们先分别安装两个模块:pip install opencv-python
pip install moviepy3.1、视频剪辑相比之下moviepy操作视频要更便利,我们看看使用moviepy如何剪辑视频:from moviepy.editor import *
# 剪切视屏bws.mp4中第50秒到第60秒
clip = VideoFileClip('bws.mp4').subclip(50, 60)
# 将剪切的片段保存
clip.write_videofile("clip.mp4")3.2、提取音频文件在VideoFileClip类中,音频文件作为其中的一个参数,我们可以直接获取:from moviepy.editor import *
# 读取视频文件
video = VideoFileClip('bws.mp4')
# 获取其中音频
audio = video.audio
# 保存音频文件
audio.write_audiofile('audio.mp3')3.3、混流我们还可以将音频同视频混流,在moviepy中,提供了一个读取音频文件的类,我们设置视频的音频需要创建这个类的对象:from moviepy.editor import *
# 读取视频
video = VideoFileClip('bws.mp4')
# 读取音频
audio = AudioFileClip('百年孤独.mp3')
# 设置视频的音频
video = video.set_audio(audio)
# 保存新的视频文件
video.write_videofile('bws_audio.mp4')3.4、逐帧提取画面我们都知道,视频是由一帧一帧的图片组成的,我们也可以将画面一帧一帧提取出来:import cv2
# 读取视频
video = cv2.VideoCapture('bws.mp4')
# 逐帧读取,当还有画面时ret为True,frame为当前帧的ndarray对象
ret, frame = video.read()
i = 0
# 循环读取
while ret:
i += 1
cv2.imwrite('v'+str(i) + '.jpg', frame)
ret, frame = video.read()上述代码就能将视屏的每一帧以图片的形式保存下来。3.5、截取gif截取gif和截取视频没有什么区别,不过为了减少gif的大小,我们通常会对视频进行尺寸缩放:from moviepy.editor import *
# 读取视频
video = VideoFileClip('bws.mp4')
# 裁剪视频,并缩小一半
video = video.subclip(20, 30).resize((0.5))
# 保存gif图片
video.write_gif('bws.gif')在上面subclip方法中,我们可以传入元组,例如:video.subclip((1, 20), (2, 30))其含义为从1分20秒截取到2分30秒。关于多媒体的操作还有很多,到此就实现了一些比较常用,也比较实用的操作,另外还花掉了我几个小时的时间。在排查环境中的错误是确实比较麻烦,但是总归还是实验的全部代码,写作不易啊~
后端小马
Python实现坦克大战
一、前言前段时间,也就是国庆节。在寝室闲来无事,用pygame写了一个小游戏,就是标题写的《坦克大战》。这个游戏写了两个版本,第一个版本是按照书上的思想来写的,发现写到后面的时候代码太乱了。于是我又从头开始,用比较合理的面向对象思想重新写了一个版本。说比较合理也只是符合我自己的思想,所以难免会有一些不合理的设计,水平有限,希望各位读者能够包涵一下。二、开发环境我们先来看看我的开发环境,用的东西还是比较简单的:其中我还用了一些第三方模块,但是在游戏主体中没有使用,所以就先不介绍了。下面我们来看看游戏实现了些什么功能。三、项目介绍3.1 项目截图我们主程序入口在main.py文件,在安装好pygame模块后就能直接运行。下面是运行截图:下面是子弹击中墙壁的爆炸效果:下面是多个敌方坦克的效果图:我们再来看看项目的各个文件。3.2 项目文件下面是项目目录:(1)resources其中resources是资源文件,音频、图片等都在resources目录。而tools中提供了两个小工具,因为只是供个人临时使用的,这里不过多解释了。(2)main.py而main.py则是项目的主入口,代码很短:from tank_war import TankWar
if __name__ == '__main__':
tankWar = TankWar()
tankWar.run_game()
我们直接创建了TankWar的实例,然后调用run_game方法运行游戏。(3)tank_war.pytank_war.py中写了我们坦克大战游戏主体的模块,里面的TankWar类定义了游戏主体的一切行为。包括初始化屏幕、初始化pygame模块、创建敌方坦克、绘制地图、检测碰撞、监听事件等。(4)sprites.py在pygame中提供了一个sprite类用于创建有图像的物体。而sprites中定义的都是sprite的子类,因此也都是有图片的类。其中包括坦克基类、英雄类(我方坦克)、敌人类(敌方坦克)、子弹类、墙类等。而各个类中定义了各自的行为,例如:坦克类有发射子弹的行为、移动的行为、爆炸的行为等。(5)settings.pysettings.py中定义了一些设置信息,包括子弹的数量、子弹的速度、坦克的速度、地图信息、图片信息等。我们可以通过修改settings.py来调整游戏的一些设置,因为还没有写设置相关的操作,所以需要修改源码。因为代码比较多,这里就不介绍代码了。游戏还要许多不足之处,后续会继续更新。项目已上传GitHub,欢迎各位来fork。今天就介绍到这里了~
后端小马
来,我教你用Python做个音乐海报
前言前段时间在一个朋友那么得到的灵感,想到可以用音乐播放页面作为一张海报图片。其实接下来要讲的和海报还是有差距的,而具体实现也只是简单的图片粘贴,但是在效果上还是不错的。效果图如下,希望大家喜欢:左边是原图,右边是需要添加到中间的图,也是图的主角。其实如果直接用ps实现上面的图是非常简单的,反倒是用代码实现有点曲折,不过实现过程还是非常有趣的,希望这篇博客可以可以让你学到知识。用Pillow创建圆形图在上面的图片中,中间是一个圆形图片,而Pillow本身是没有提供生成圆形图片的方法(也可能是我没找到),所以就需要自己实现。在实现之前,我们先安装Pillow模块:pip install pillow要创建圆形图,我们先根据原图的大小,创建一个RGBA模式的透明图:# 该方法传入三个参数,第一个为模式,第二个为大小的元组,第三个为颜色
im = Image.new('RGBA', (300, 300), (255, 255, 255, 0))上述代码是创建了一个完全透明的300*300的图片,我们在该图片上绘制一个最大的圆:# 获取绘制者
drawer = ImageDraw.Draw(im)
# 绘制一个黄色的圆,ellipse方法传入三个参数,第一个为包含该圆的最小正方形的区域,第二个为颜色,第三个为边宽
drawer.ellipse((0, 0, 300, 300), fill=(255, 255, 0), width=0)生成图片如下:我们准备一张300*300的正方形图片,然后遍历图片的每个像素,如果像素值的A==0(即像素不透明)那我们就将图片该区域的像素值设置为透明。代码如下:# 打开要转换成圆形的图片,我们事先把图片裁剪好
pic = Image.open(img_path).convert('RGBA')
# 遍历图片的每个像素
for i in range(300):
for j in range(300):
# 获取该像素点的像素
r, g, b, a = im.getpixel((i, j))
# 当rgb值不是黄色时,即像素值为透明时
if (r, g, b) != (255, 255, 0):
# 将原图的像素值设置为透明
pic.putpixel((i, j), (255, 255, 255, 0))我们的pic就是圆形图片了,完整代码如下:# 背景图中圆的直径
radius = 533
# 图片的大小
circle_size = (radius, radius)
def generate_circle_image(img_path):
# 创建一个透明的正方形
im = Image.new('RGBA', circle_size, (255, 255, 255, 0))
# 获取绘画者
drawer = ImageDraw.Draw(im)
# 在透明的正方形上画一个黄色的圆
drawer.ellipse((0, 0, circle_size[0], circle_size[1]), fill=(255, 255, 0), width=0)
# 打开要转换成圆形的图片,我们事先把图片裁剪好
pic = Image.open(img_path).convert('RGBA')
# 修改图片大小,让图片和圆大小一样
re_pic = pic.resize(circle_size, Image.ANTIALIAS)
# 遍历图片的每个像素
for i in range(circle_size[0]):
for j in range(circle_size[1]):
r, g, b, a = im.getpixel((i, j))
if (r, g, b) != (255, 255, 0):
re_pic.putpixel((i, j), (255, 255, 255, 0))
return re_pic在上面的方法中,我们改进了一些代码,之前我们需要使用指定大小的图片作为素材,现在只需要是正方形图片就可以了。生成海报原本我以为直接将圆形图片粘贴到背景图片上就可以了,但是试过之后发现效果和我想的不太一样,反正就是没成功,效果如下:是粘贴上去了没错,而且图片也是透明效果,但是从这个效果来看粘贴只是像素替换,而不是图片叠加。于是我又想到了遍历像素的办法。我通过特殊手段获取了上面正方形离左边的像素,和离上边的像素(其实就是用ps看了一下)。我尝试过图形检测和像素判断的办法,想自动识别中间圆的位置,但是效果不佳(反正就是失败了),所以只能无耻的用ps查看像素。回到正题,我们用遍历像素的办法不需要遍历整个图片,只需要从(left, top)像素开始,遍历到(left+radius, top+radius)区域即可,也就是遍历正方形区域(left、top和radius都是我通过ps获取的)。我们先将背景图片拷贝一份,然后在副本上进行粘贴。然后遍历粘贴后的图片,如果像素值为透明,我们就将原图该位置的像素替换至副本处,实现原理和上面一样,代码如下:def generate_music_post(circle_im, bg_im):
"""
传入圆形图片和背景图片生成音乐海报
circle_im:圆形图片
bg_im:背景图片
return:生成的图片
"""
# 拷贝副本
bg_copy = bg_im.copy()
# 将圆形图片粘贴到副本上
bg_copy.paste(circle_im, (left, top))
# 遍历像素正方形区域
for i in range(left, left+radius):
for j in range(top, top+radius):
# 获取像素值
color = bg_copy.getpixel((i, j))
# 如果像素透明。color的值为(r,g,b,a),color[3]为a的值,即透明值
if color[3] == 0:
# 将原图像素替换至副本透明处
bg_copy.putpixel((i, j), bg_im.getpixel((i, j)))
# 返回合成后的图片
return bg_copy这样就完成了。完整代码如下:from PIL import Image, ImageDraw
left = 273 # 圆离左边的距离
top = 573 # 圆离上边的距离
radius = 533 # 圆的直径
circle_size = (radius, radius) # 圆的外接正方形的大小
def generate_circle_image(img_path):
# 创建一个透明的正方形
im = Image.new('RGBA', circle_size, (255, 255, 255, 0))
# 获取绘画者
drawer = ImageDraw.Draw(im)
# 在透明的正方形上画一个黄色的圆
drawer.ellipse((0, 0, circle_size[0], circle_size[1]), fill=(255, 255, 0), width=0)
# 打开要转换成圆形的图片,我们事先把图片裁剪好
pic = Image.open(img_path).convert('RGBA')
re_pic = pic.resize(circle_size, Image.ANTIALIAS)
# 遍历图片的每个像素
for i in range(circle_size[0]):
for j in range(circle_size[1]):
r, g, b, a = im.getpixel((i, j))
if (r, g, b) != (255, 255, 0):
re_pic.putpixel((i, j), (255, 255, 255, 0))
return re_pic
def generate_music_post(circle_im, bg_im):
"""
传入圆形图片和背景图片生成音乐海报
circle_im:圆形图片
bg_im:背景图片
return:生成的图片
"""
# 拷贝副本
bg_copy = bg_im.copy()
# 将圆形图片粘贴到副本上
bg_copy.paste(circle_im, (left, top))
# 遍历像素正方形区域
for i in range(left, left+radius):
for j in range(top, top+radius):
# 获取像素值
color = bg_copy.getpixel((i, j))
# 如果像素透明。color的值为(r,g,b,a),color[3]为a的值,即透明值
if color[3] == 0:
# 将原图像素替换至副本透明处
bg_copy.putpixel((i, j), bg_im.getpixel((i, j)))
# 返回合成后的图片
return bg_copy
# 生成圆形图片
pic = generate_circle_image('girl.jpeg')
# 以RGBA模式读取背景图片
bg_im = Image.open('music.jpg').convert('RGBA')
# 生成音乐海报
music_post = generate_music_post(pic, bg_im)
music_post.show()另外,这个例子还可以更加智能。我们可以使用OpenCV识别主体图片的人脸,然后根据人脸区域计算一个比较适合的正方形区域,这样我们就不必传入正方形(不过还要考虑人脸识别的精确度等问题)。
后端小马
Python实现用手机监控远程控制电脑
一、前言很多时候,我们都有远程控制电脑的需求。比如正在下载某样东西,需要让电脑在下载完后关机。或者你需要监控一个程序的运行状况等。今天我们就来用Python实现一个远程监控并控制电脑的小程序。二、实现原理听起来远程控制电脑好像很高级的样子,但是实现起来其实非常简单。实现原理如下:运行程序,让程序不停地读取邮件用手机给电脑发送邮件判断是否读取到指定主题的邮件,如果有,则获取邮件内容根据邮件内容,执行预设的函数与其说是学习如何远程控制电脑,还不如说是学习如何读取邮件。当然,上面的的流程只实现了远程控制电脑,而没实现对电脑的监控。而监控的操作可以以截图的方式来进行。我们可以预设一个指令,当读取到邮件内容为grab时,我们就发送电脑截图。如何将电脑截图发送给手机邮箱,这样就达到了监控的效果。关于如何发送邮件可以参考博客:如何用Python发送邮件?。这里就不再详细说了。下面我们看看如何读取邮件。三、读取邮件读取邮件需要使用到imbox模块,安装语句如下:pip install imbox
读取邮件的代码如下:from imbox import Imbox
def read_mail(username, password):
with Imbox('imap.163.com', username, password, ssl=True) as box:
all_msg = box.messages(unread=True)
for uid, message in all_msg:
# 如果是手机端发来的远程控制邮件
if message.subject == 'Remote Control':
# 标记为已读
box.mark_seen(uid)
return message.body['plain'][0]
首先我们用with语句,打开邮箱。然后通过下面语句获取所有的未读邮件:all_msg = box.messages(unread=True)
获取未读邮件后,对邮件进行遍历。将主题为“Reomte Control”的邮件标记为已读,并返回文本内容。这里需要注意,因为我们筛选出了主题为“Remote Control”的邮件,因此我们在用手机发邮件的时候需要将主题设置为“Remote Control”,这样可以避免其它邮件的干扰。四、截图截图需要使用到PIL模块,安装如下:pip install pillow
截图的代码很简单:from PIL import ImageGrab
def grab(sender, to):
# 截取电脑全屏
surface = ImageGrab.grab()
# 将截屏保存为surface.jpg
surface.save('surface.jpg')
# 将截屏发送给手机
send_mail(sender, to, ['surface.jpg'])
其中send_mail的代码如下:import yagmail
def send_mail(sender, to, contents):
smtp = yagmail.SMTP(user=sender, host='smtp.163.com')
smtp.send(to, subject='Remote Control', contents=contents)
关于发送邮件的介绍可以参考上面提到的博客。五、关机关机的操作非常简单,我们可以用python来执行命令行语句即可。代码如下:import os
def shutdown():
# 关机
os.system('shutdown -s -t 0')
除了关机,我们还可以执行很多操作。对于一些复杂的操作,我们可以预编写一些bat文件,这里就不演示了。六、完整代码上面我们编写了各个部分的代码,然后再来看看主体部分的代码:def main():
# 电脑用来发送邮件已经电脑读取的邮箱
username = 'sockwz@163.com'
password = '********'
# 手机端的邮箱
receiver = '2930777518@qq.com'
# 读取邮件的时间间隔
time_space = 5
# 注册账户
yagmail.register(username, password)
# 循环读取
while True:
# 读取未读邮件
msg = read_mail(username, password)
if msg:
# 根据不同的内容执行不同操作
if msg == 'shutdown':
shutdown()
elif msg == 'grab':
grab(username, receiver)
time.sleep(time_space)
其中:yagmail.register(username, password)
会使用到keyring模块,安装如下:pip install keyring
后面我们可以根据自己的需求编写一些其它功能。下面是完整的代码:import os
import time
import yagmail
from imbox import Imbox
from PIL import ImageGrab
def send_mail(sender, to, contents):
smtp = yagmail.SMTP(user=sender, host='smtp.163.com')
smtp.send(to, subject='Remote Control', contents=contents)
def read_mail(username, password):
with Imbox('imap.163.com', username, password, ssl=True) as box:
all_msg = box.messages(unread=True)
for uid, message in all_msg:
# 如果是手机端发来的远程控制邮件
if message.subject == 'Remote Control':
# 标记为已读
box.mark_seen(uid)
return message.body['plain'][0]
def shutdown():
os.system('shutdown -s -t 0')
def grab(sender, to):
surface = ImageGrab.grab()
surface.save('surface.jpg')
send_mail(sender, to, ['surface.jpg'])
def main():
username = 'sockwz@163.com'
password = '你的授权码'
receiver = '2930777518@qq.com'
time_space = 5
yagmail.register(username, password)
while True:
# 读取未读邮件
msg = read_mail(username, password)
if msg:
if msg == 'shutdown':
shutdown()
elif msg == 'grab':
grab(username, receiver)
time.sleep(time_space)
if __name__ == '__main__':
main()
后端小马
美翻你的朋友圈,Python生成蒙太奇图片
一、前言我们有时候会听到这么一个词--“蒙太奇”,但却不知道这个词是什么意思。蒙太奇原为建筑学术语,意为构成、装配。而后又延伸为一种剪辑理论:当不同镜头拼接在一起时,往往又会产生各个镜头单独存在时所不具有的特定含义。这就是我们经常听到了蒙太奇手法,在电影《飞屋环游记》中皮克斯运用蒙太奇手法,用一个不到5分钟的短片展现了主角的大半人生,感动无数观众。下面我们就看看今天的内容同蒙太奇有何关系。二、效果展示说这么多都是虚的,下面我们看看效果实现的效果,到底什么是蒙太奇马赛克图片,这里用小松菜奈的照片作为测试:最左边的是蒙太奇图缩小的效果,第二个则是正常大小显示的效果,第三张是原图,第四张是截取的某个区域的细节。从图四可以很容易看出,我们的蒙太奇图片是使用许多不同的图片拼接而成的。三、代码实现程序的实现分为几个步骤,首先我们需要准备工作,一个是我们的底图,也就是上面的图三。另外就是需要一个图片集,这个图片集的选取有几个规范,首先不能有gif图和png图片,其次就是图片的颜色尽量丰富,图片数量也多一些,这样效果会更好。另外就是选取长宽比接近1的图片效果会更好。然后就是我们代码部分的工作了:图片预处理获取颜色的主色调列表遍历底图的每个像素块在色调列表中寻找与当前色调块最相近的图片将图片修改大小后粘贴到当前遍历的色调块保存图片大家对于上面的步骤或许还有些疑问,这些疑问在具体实现中细说。先看看我们要用到的一些模块:import os
import cv2
import math
import numpy as np
其中opencv的安装如下:pip install opencv-python
3.1、图片预处理人工挑图片还是比较麻烦的,所以我们只要求人先挑好一些图片,然后我们将不符合规范的图片删除即可:def renameImages(path):
//获取图片路径列表
filelist = [path + i for i in os.listdir(path)]
//用数字给图片命名
img_num = str(len(filelist))
name = int(math.pow(10, len(img_num)))
//遍历列表
for file in filelist:
//删除gif和png图片
if file.endswith('.gif') or file.endswith('.GIF') or file.endswith('.png') or file.endswith('.PNG'):
os.remove(file)
continue
# 对图片以数字编号重命名
os.rename(file, path + str(name) + '.jpg')
name += 1
执行上面的方法后我们就把合适的图片筛选出来了。3.2、获取颜色的主色调列表获取主色调列表前我们需要先获取主色调,这里直接使用bgr值的平均值作为主色调:def getDominant(im):
"""获取主色调"""
b = int(round(np.mean(im[:, :, 0])))
g = int(round(np.mean(im[:, :, 1])))
r = int(round(np.mean(im[:, :, 2])))
return (b, g, r)
通常RGB模式的图片我们接触的比较多,但是在OpenCV中图片是以BGR模式读取,每个字母的含义是一样的,只是顺序不同,这里需要注意一下。接下来我们获取主色调列表:def getColors(path):
"""获取图片列表的色调表"""
colors = []
# 获取图片列表
filelist = [path + i for i in os.listdir(path)]
# 遍历列表
for file in filelist:
# 读取图片
im = cv2.imdecode(np.fromfile(file, dtype=np.uint8), -1)
try:
# 获取图片主色调
dominant = getDominant(im)
except:
continue
# 将主色调添加到色调列表中
colors.append(dominant)
return colors
有了色调列表,我们对比颜色的操作就可以直接同色调列表进行了。3.3、寻找主色调最接近的图片我是通过比较两张图片主色调的BGR值,然后将差的绝对值相加的方式获得色调的差异:def fitColor(color1, color2):
"""返回两个颜色之间的差异大小"""
# 求出b通道之间的差异
b = color1[0] - color2[0]
# 求出g通道之间的差异
g = color1[1] - color2[1]
# 求出r通道之间的差异
r = color1[2] - color2[2]
# 返回绝对值的和
return abs(b) + abs(g) + abs(r)
3.4、遍历,寻找并粘贴这里就是我们的方法主体了,内容比较多,我们先看看代码:def generate(im_path, imgs_path, box_size, multiple=1):
"""生成图片"""
# 读取图片列表
img_list = [imgs_path + i for i in os.listdir(imgs_path)]
# 读取图片
im = cv2.imread(im_path)
im = cv2.resize(im, (im.shape[1]*multiple, im.shape[0]*multiple))
# 获取图片宽高
width, height = im.shape[1], im.shape[0]
# 遍历图片像素
for i in range(height // box_size+1):
for j in range(width // box_size+1):
# 图块起点坐标
start_x, start_y = j * box_size, i * box_size
# 初始化图片块的宽高
box_w, box_h = box_size, box_size
# 截取当前遍历到的图块
box_im = im[start_y:, start_x:]
if i == height // box_size:
box_h = box_im.shape[0]
if j == width // box_size:
box_w = box_im.shape[1]
if box_h == 0 or box_w == 0:
continue
# 获取主色调
dominant = getDominant(im[start_y:start_y+box_h, start_x:start_x+box_w])
img_loc = 0
# 差异,同主色调最大差异为255*3
dif = 255 * 3
# 遍历色调表,查找差异最小的图片
for index in range(colors.__len__()):
if fitColor(dominant, colors[index]) < dif:
dif = fitColor(dominant, colors[index])
# 色调列表同图片列表的位置是一致的,所以我们获取色调下标即可
img_loc = index
# 读取差异最小的图片,img_list[img_loc]为差异最小的图片
box_im = cv2.imdecode(np.fromfile(img_list[img_loc], dtype=np.uint8), -1)
# 转换成合适的大小
box_im = cv2.resize(box_im, (box_w, box_h))
# 铺垫色块
im[start_y:start_y+box_h, start_x:start_x+box_w] = box_im
j += box_w
i += box_h
# 返回结果图
return im
首先我们看看传入的参数都是什么含义:im_path : 底图的路径
imgs_path : 图片列表的根目录
box_size : 像素块的大小
multiple=1 : 图片的缩放大小,默认为1
前面两个参数非常好理解。对于box_size参数的解释就是效果图四种,每张照片的尺寸,因为我全部以正方形处理,所以只有一个大小。而multiple参数则是缩放大小,当我们底图为50*50没有设置缩放时,结果图也是50*50,当我们将缩放设置为2,结果图则为100*100。因为图片太小的话看不到像素块中的图片,所以利用缩放让效果更好,但是缩放值设置过大的话图片内存会大许多。其它部分的解释都在代码中了。最后再给大家看一张效果图:因为实现效果不是非常乐观,所以给大家看一张朦胧的效果图。
后端小马
超全Python图像处理讲解(多图预警)
一、Image模块1.1 、打开图片和显示图片对图片的处理最基础的操作就是打开这张图片,我们可以使用Image模块中的open(fp, mode)方法,来打开图片。open方法接收两个参数,第一个是文件路径,第二个是模式。主要的模式如下:mode(模式)bands(通道)说明“1”1数字1,表示黑白二值图片,每个像素用0或1共1位二进制码表示“L”1灰度图“P”1索引图“RGB”324位真彩图“RGBA”4“RGB”+透明通道“CMYK”4印刷模式图像更多的模式也就不说了,关于模式的模式的详细介绍我也不知道。这个open方法返回一个Image对象,mode也不是必须参数。打开图片代码如下:from PIL import Image
# 打开图片
im = Image.open('test.jpg')
# 显示图片
im.show()当然显示图片不是我们的重点,我们获取Image对象之后,就可以获取它的一些信息了。print('图像的格式:', im.format)
print('图像的大小:', im.size)
print('图像的宽度:', im.width)
print('图像的高度:', im.height)
# 传入坐标的元组
print('获取某个像素点的颜色值:', im.getpixel(100, 100))在我的环境中运行结果如下:图像的格式: JPEG
图像的大小: (3968, 2976)
图像的宽度: 3968
图像的高度: 2976
获取某个像素点的颜色值: (198, 180, 132)1.2、创建一个简单的图像在Image模块中,提供了创建图像的方法。主要是通过**Image.new(mode, size, color)**实现,该方法传入三个参数:mode:图像的创建模式size:图像的大小color:图像的颜色用该方法可以创建一个简单的图像,之后我们可以通过save方法将图像保存:from PIL import Image
# 创建一个简单的图像
im = Image.new('RGB', (100, 100), 'red')
# 保存这个图像
im.save('red.png')生成图片如下:1.3、图像混合(1)透明度混合透明度混合主要是使用**Image中的blend(im1, im2, alpha)**方法,对该方法的解释如下:im1:Image对象,在混合的过程中,透明度设置为(1-apha)im2:Image对象,在混合的过程中,透明度设置为(apha)alpha:透明度,取值是0-1。当透明度为0是,显示im1对象;当透明度为1时,显示im2对象注意:im1和im2的大小必须一样,且mode都为RGB代码实现如下:from PIL import Image
# 打开im1
im1 = Image.open('pic.jpg').convert(mode='RGB')
# 创建一个和im1大小一样的图像
im2 = Image.new('RGB', im1.size, 'red')
# 混合图片,并显示
Image.blend(im1, im2, 0.5).show()下面为原图和混合图的对比:不得不说,我家艾斯真滴帅。(2)遮罩混合接下来就是很迷的时刻了,我们可以通过Image.composite(im1, im2, mask)方法实现遮罩混合。三个参数都是Image对象,该方法的作用就是使用mask来混合im1和im2。我是听不懂,你们能听懂最好给我讲一下。具体实现如下:# 这句代码写了好多遍,我真不想写了
from PIL import Image
# 打开图像1
im1 = Image.open('pic1.jpg')
# 打开图像2
im2 = Image.open('pic2.jpg')
# 重新设置im2的大小
im2.resize(im1.size)
# 将图像2的三个色道分离,其中r、g、b都为Image对象
r, g, b = im2.split()
# 遮罩混合
Image.composite(im1, im2, b).show()注意:im1、im2和mask的大小必须一样im1、im2和遮罩混合效果对比如下:依旧是我帅气的艾斯。1.4、图像缩放(1)按像素缩放按像素缩放通过Image.eval(im1, fun)方法实现,其中im1为我们老生常谈的Image对象了;第二个为一个方法(函数),该函数传入一个参数,即像素点。该函数会对图片中每个像素点进行函数内的操作。下面我们对来简单使用一下这个方法:from PIL import Image
# 打开一张图像
im = Image.open('抠鼻屎.jpg')
# 对该图像每个像素点进行*2处理
Image.eval(im, lambda x:x*2).show()这里我使用的lambda表达式,当然一般也都是用lambda表达式,不过你也可以像下面这样写:# 定义一个方法
def func(x):
return x*2
# 对图像im每个像素点进行func中的操作,其中func不能加()
Image.eval(im, func)效果图如下:细心的读者应该可以发现,这个抠鼻屎的图片和笔者头像并不完全一样。在血色方面,笔者的头像确实要差几分。注意:笔者在日常生活中可不是天天在大街上抠鼻屎的那种。(2)按尺寸缩放按尺寸缩放是通过Image对象的thumbnail()方法实现的,这里不同于前面直接通过Image调用方法,而是使用Image的具体实例im2调用thumbnail方法,从而对im2直接进行处理。具体代码如下:from PIL import Image
# 打开图像
im1 = Image.open('xx.jpg')
# 复制图像
im2 = im1.copy()
# 将复制后的图像进行缩放,传入一个元组
im2.thumbnail((100, 100))
# 输出图像大小
print("im1的大小", im1.size)
print('im2的大小', im2.size)
这里缩放图像并不会对图像进行变形,即显示效果是一样的。这里就不放效果图了,输入结果如下:im1的大小 (960, 960)
im2的大小 (100, 100)
1.5、图像的剪切与粘贴(1)图像粘贴粘贴的实现主要是通过Image对象的paste(im, box, mask)方法,其中im为Image对象;box为要粘贴到的区域;mask为遮罩(我也不知道啥是遮罩)。其中box的参数有三种形式:(x1, y1):将im左上角对齐(x1,y1)点,其余部分粘贴,超出部分抛弃(x1, x2, y1, y2):将im粘贴至此区域None:此时im必须与源图像大小一致(2)裁剪图像裁剪主要通过Image对象的crop(box)方法实现,box同粘贴中一致。接下来我们做一个小练习,想将图像某个区域剪切下来,然后粘贴到另一个图像上:from PIL import Image
# 打开图像
im = Image.open('nnz.jpg')
# 复制两份
im1 = im.copy()
im2 = im.copy()
# 剪切图片
im_crop = im1.crop((200, 200, 400, 400))
# 粘贴图片
im2.paste(im_crop, (30, 30))
im2.show()原图和效果图对比如下:貌美如花的娜娜子。1.4、图像旋转和格式转换(1)图像旋转图像旋转就非常简单了,简单的一句代码,通过Image对象调用rotate(),该方法返回被旋转图像的一个副本:from PIL import Image
im = Image.open('nnz.jpg')
# 旋转90度然后显示
im.rotate(90).show()顺时针逆时针就不要问我了。(2)格式转换convert:转换图像的模式transpose:转换图像的格式convert之前已经使用过了,这里就简单演示一下transpose的作用,transpose主要传入一些Image中的常量:from PIL import Image
# 打开图像
im = Image.open('nnz.jpg')
# 这里我也不知道注释啥了,总之效果和rotate(90)效果一样
im.transpose(Image.ROTATE_90).show()效果图我也就不放了,给大家列出一些可以传入的常量和该常量的作用:常量作用Image.FILP_TOP_BOTTOM上下翻转Image.FILP_LEFT_RIGHT左右翻转Image.ROTATE_90翻转90°Image.ROTATE_180翻转180°Image.TRANSPOSE颠倒我也不知道这是哪门子的格式转换。1.5、分离和合并(1)分离这个是之前使用过的,通过Image对象的split()方法,将图像的RGB三个通道分离,并返回三个Image对象:from PIL import Image
# 打开图像
im = Image.open('nnz.jpg')
# 分离通道,返回3个Image对象
r, g, b = im.split()三个通道的效果图如下:(2)合并合并是通过Image.merge(mode, bands)方法实现的,其中mode为模式,bands为通道列表,传入一个列表类型数据。下面我实现以下小新多年来的愿望:from PIL import Image
# 打开小新.jpg和娜娜子.jpg
im1 = Image.open('娜娜子.jpg')
im2 = Image.open('小新.jpg')
# 让im2大小和im1一样
im2.resize(im1.size)
# 将两个图像分别分离
r1, g1, b1 = im1.split()
r2, g2, b2 = im2.split()
# 合并图像
im3 = Image.merge('RGB', [r1, g2, b1])
im3.show()效果图如下,看到这么美的图片,小新一定会感谢我的:到这里,我们就把Image模块的大致内容讲解完了,接下来我们来了解PIL中更丰富的功能。二、ImageFilterImageFilter中提供了很多常用的滤镜功能,2.1、高斯模糊高斯模糊也叫高斯平滑,是啥我也不知道,反正听名字就是模糊。我们结合上面的内容完成一个小案例:from PIL import Image, ImageFilter
# 打开图像
im1 = Image.open('iron_man.jpg')
# 创建一个im1两倍宽的图像
img = Image.new('RGB', (im1.width*2, im1.height), 'red')
# 高斯模糊处理
im2 = im1.filter(ImageFilter.GaussianBlur)
# 将im1粘贴到img上
img.paste(im1, (0, 0))
# 将im2(高斯模糊后的图像)粘贴到img上
img.paste(im2, (im1.width, 0))
img.show()为了考虑小新的感受,下面不再用娜娜子作为素材。我选取了一张钢铁侠的图片,运行结果如下:希望各位读者不要误会,他俩真没说你帅,他俩只说笔者一个人帅。2.2、其它滤镜除了高斯模糊,ImageFilter中还提供了许多其它滤镜:滤镜值滤镜名词BLUR模糊效果CONTOUR轮廓DETAIL细节EDGE_ENHANCE边缘增强EDGE_ENHANCE_MORE边缘增强plusEMBOSS浮雕效果FIND_EDGES寻找边缘SMOOTH平滑笔者用一张美女图片,测试了上面几个滤镜的效果,发现9张图是看起来是完全一样的。虽然完全一样,但是笔者还是打算将这次测试的结果作为我慈善事业的一部分,分享给各位读者。其中1为高斯模糊,2-9分别为表格中的8个滤镜。三、ImageChops模块(图像合成)ImageChops模块中,提供了很多图像合成的方法。这些方法是通过计算通道中像素值来实现的,不同的方法有不同的计算方式。3.1、加法运算加法运算通过**ImageChops.add(image1, image2, scale=1.0, offset=0)**方法实现,合成公式如下:out = (im1 + im2)/scale + offset我也看不懂,其中scale和offset是有默认值的。所以使用时我们可以省略参数,具体实现如下:from PIL import Image, ImageChops
# 打开图像
im1 = Image.open('im1.jpg')
im2 = Image.open('im2.jpg')
# 合成图像并显示
im3 = ImageChops.add(im1, im2)
im3.show()实验结果产不忍赌,效果图如下:3.2、减法运算加法运算通过**ImageChops.subtract(image1, image2, scale=1.0, offset=0)**方法实现,合成公式如下:out = (im1 - im2)/scale + offset其使用和add方法是一致的,代码如下:from PIL import Image, ImageChops
# 打开图像
im1 = Image.open('xscn.jpg')
im2 = Image.open('xscn2.jpg')
# 合成图像并显示
im3 = ImageChops.subtract(im1, im2)
im3.show()原本是不想放效果图的,但是运行后,发现效果图比较美,所以想和大家分享一下:希望大家读到这篇博客的时候是独自一人的深夜。3.3、其它函数因为大多数函数的使用都比较简单,所以后续的函数也不单独拿出来讲了,具体功效可以看下列表:函数名参数作用计算公式darker(变暗)(image1, image2)对比两种图片的像素,取两种图片中对应像素的较小值。(去亮留暗)min(im1, im2)lighter(变亮)同上对比两种图片的像素,取两种图片中对应像素的较大值。(去暗留亮)max(im1, im2)invert(反色)(image)将max(255)减去每个像素的值max-imagemultiply(叠加)(image1, image2)两种图片互相叠加。如果和黑色叠加,将获得一张很色图片im1*im2/maxscreen(屏幕)同上先反色后叠加max-((max-im1)*(max-im2)/max)difference(比较)同上各个像素做减法,取绝对值。如果像素相同结果为黑色abs(im1-im2)演示代码如下:from PIL import Image, ImageChops
# 打开图像
im1 = Image.open("im1.jpg")
im2 = Image.open("im2.jpg")
# 对图像进行各种操作
im3 = ImageChops.darker(im1, im2)
im3.save('darker.jpg')
im3 = ImageChops.lighter(im1, im2)
im3.save('lighter.jpg')
im3 = ImageChops.invert(im1)
im3.save('invert.jpg')
im3 = ImageChops.multiply(im1, im2)
im3.save('multiply.jpg')
im3 = ImageChops.screen(im1, im2)
im3.save('screen.jpg')
im3 = ImageChops.difference(im1, im2)
im3.save('difference.jpg')其中,我选取的素材im1和im2都是上面使用到的那两张,效果图如下:这样,我的女神就被我毁的体无完肤了。四、ImageEnhance模块(色彩、亮度)ImageEnhance提供了许多函数,用于调整图像的色彩、对比度、亮度、清晰度等。调整图像的步骤如下:确定要调整的参数,获取特定的调整器调用调整器的enhance方法,传入参数进行调整。注意:所有调整器都实现同一个接口,该接口中包含一个方法enhance其中enhance方法接收一个参数factor,factor是一个大于0的数。当factor为1时,返回原图,当factor小于1返回减弱图,大于1返回增强图。各个获取色彩调整器的方法如下:方法名称方法作用ImageEnhance.Color()获取颜色调整器ImageEnhance.Contrast()获取对比度调整器ImageEnhance.Brightness()获取亮度调整器ImageEnhance.Sharpness()获取清晰度调整器虽然是很想偷懒,不去做实验,但是想想还是做了如下实验,代码如下:from PIL import Image, ImageEnhance
# 打开im1
im1 = Image.open("gtx.jpg")
# 获取颜色(各种)调整器
enhance_im1 = ImageEnhance.Color(im1)
#enhance_im1 = ImageEnhance.Contrast(im1)
#enhance_im1 = ImageEnhance.Brightness(im1)
#enhance_im1 = ImageEnhance.Sharpness(im1)
# 减弱颜色(以及其它属性)
im2 = enhance_im1.enhance(0.5)
# 增强颜色(以及其它属性)
im3 = enhance_im1.enhance(1.5)
# 获取原图大小
w, h = im1.size
# 创建一个原图大小3倍的图片
img = Image.new("RGB", (w*3, h))
# 将减弱的图片放在最左边
img.paste(im2, (0, 0))
# 将原图放在中间
img.paste(im1, (w, 0))
# 将增强后的图片放在最右边
img.paste(im3, (w*2, 0))
# 显示图片
img.show()其中,我们只需要修改获取调整器的代码就可以了,获取其它调制器的代码我注释了。然后看看效果图:这种不伤大雅的工作,让我唐尼叔做再适合不过了。另外再讲一个调节亮度的函数,但是这个函数时Image中的函数point(),而不是ImageEnhance的。该函数传入一个参数,使用方法和Image.eval()类似,使用示例如下:from PIL import Image
# 打开图像
im1 = Image.open('gtx.jpg')
# 变暗操作
im2 = im1.point(lambda x:x*0.5)
# 变量操作
im3 = im1.point(lambda x:x*1.5)
# 获取原图大小
w, h = im1.size
# 创建一个原图大小3倍的图片
img = Image.new("RGB", (w*3, h))
# 将减弱的图片放在最左边
img.paste(im2, (0, 0))
# 将原图放在中间
img.paste(im1, (w, 0))
# 将增强后的图片放在最右边
img.paste(im3, (w*2, 0))
# 显示图片
img.show()效果图如下:五、ImageDraw模块该模块提供了许多绘制2D图像的功能,我们可以通过绘制获取一个全新的图像,也可以在原有的图像上进行绘制。在我们使用该模块进行绘制时,我们需要先获取ImageDraw.Draw对象,获取方式如下:from PIL import ImageDraw
# 构造函数中,im为一个Image对象
drawer = ImageDraw.Draw(im)我们获取ImageDraw.Draw对象后就可以进行相应的绘制了。5.1、绘制简单形状在绘制之前,我们先创建一个空白的图片:from PIL import Image, ImageDraw
# 创建一个300*300的白色图片
im = Image.new("RGB", (300, 300), "white")
# 获取ImageDraw.Draw对象
drawer = ImageDraw.Draw(im)后续的绘制都可以使用对象drawer绘制。(1)绘制直线"""
xy:起点坐标和终点坐标(x1, y1, x2, y2)
fill:填充色。"red"、"blue"...
width:轮廓粗细
joint:连接方式,可以是曲线
"""
line(xy, fill, width, joint)
# 绘制直线
drawer.line((50, 50, 150, 150), fill='green',width=2)(2)绘制矩形"""
xy:左上角坐标和右下角坐标(x1, y1, x2, y2)
fill:填充色。"red"、"blue"...
outline:轮廓色。同上
width:轮廓粗细
"""
rectangle(xy, fill, outline, width)
# 使用示例
drawer.rectangle((50, 50, 150, 150), fill='green', outline='red', width=3)(3)绘制圆弧"""
xy:包含圆弧所在圆的矩形的左上角坐标和右下角坐标(x1, y1, x2, y2)
start:起始角度
end:终止角度
fill:填充色。"red"、"blue"...
width:轮廓粗细
"""
arc(xy, start, end, fill, width)
# 使用示例
drawer.arc((50, 50, 150, 150), start=0, end=90, fill='green', width=3)对于xy参数的解释如图所示:(4)绘制椭圆"""
xy:包含椭圆(或圆)的矩形的左上角坐标和右下角坐标(x1, y1, x2, y2)
fill:填充色。"red"、"blue"...
outline:轮廓颜色
width:轮廓粗细
"""
ellipse(xy, fill, outline, width)
# 使用示例
drawer.ellipse((50, 50, 150, 150),fill='green', outline='red', width=3)(5)绘制弦
"""
xy:弦所在椭圆的矩形的左上角坐标和右下角坐标(x1, y1, x2, y2)
start:开始角度
end:终点角度
fill:填充色。"red"、"blue"...
outline:轮廓颜色
width:轮廓粗细
"""
chord(xy, start, end, fill, outline, width)
# 使用示例
drawer.chord((50, 50, 150, 150),start=0, end=90, fill='green', outline='red', width=3)(6)绘制扇形
"""
xy:扇形所在椭圆的矩形的左上角坐标和右下角坐标(x1, y1, x2, y2)
start:开始角度
end:终点角度
fill:填充色。"red"、"blue"...
outline:轮廓颜色
width:轮廓粗细
"""
pieslice(xy, start, end, fill, outline, width)
# 使用示例
drawer.pieslice((50, 50, 150, 150),start=0, end=90, fill='green', outline='red', width=3)(7)绘制多边形"""
xy:多边形各个点坐标的元组/列表(x1, y1, x2, y2)
fill:填充色。"red"、"blue"...
outline:轮廓颜色
"""
pieslice(xy, fill, outline)
# 使用示例
drawer.polygon((50, 50, 150, 150, 150, 200, 200, 250, 50, 50), fill='green', outline='red')(8)绘制点"""
xy:点的坐标
fill:填充色。"red"、"blue"...
"""
point(xy, fill)
# 使用示例
drawer.point((100, 100), fill='black')除了上面这些简单图形外,我们还可以使用Draw绘制文字。5.2、绘制文字绘制文字和绘制图形是一样的:"""
xy:起点坐标
text:绘制的文本
fill:填充色。"red"、"blue"...
...其中绘制文字还有许多其它参数
"""
text(xy, text, fill)
# 使用示例
drawer.text((100, 100), text='zack' fill='red')当我们绘制中文时,上述代码会报错,因为默认编码是不支持中文的。我们可以在C:/Windows/Fonts目录下找到字体文件,我们选择一个支持中文的。我这里直接是将字体文件复制到项目底下来了,代码如下:from PIL import Image, ImageDraw, ImageFont
# 创建一个图像用于绘制文字
im = Image.new("RGB", (300, 300), "white")
drawer = ImageDraw.Draw(im)
# 获取字体对象
imFont = ImageFont.truetype('simkai.ttf', 30)
# 绘制文字时设置字体
drawer.text((50, 100),text="啥",font=imFont,fill="red")
im.show()我们使用了ImageFont.truetype()函数获取字体对象,在获取时我们可以设置字体大小。
后端小马
Python实现高级电影特效,CXK也能影分身
一、前言前几天写了个实现特效的博客,感觉有点差强人意,只是简简单单的换背景应用场景不是非常多,今天就来实现一个更加复杂的特效“影分身”。下面有请我们本场的主演,坤制作人为我们表演他拿手的鸡你太美。关于实现原理,和上一篇没有本质区别,同样是逐帧处理,但是这里还是详细说一下。二、实现原理首先我们要准备一个视频,作为我们的素材。然后我们要逐帧提取视频中的图像,接下来我们利用paddlehub逐帧抠取人像。这样就有了我们的主体,和分身了。最后我们需要在写入视频的时候对图像进行处理,我直接在原图像上粘贴了两个人物分身,最后合成的视频效果就是上面的效果了。当然我们还需要添加音频,所以最后我们需要读取音频并将新视频同音频混流。我们将整个过程分为以下几个步骤:逐帧提取图像批量抠图合成图像(影分身)写入视频读取音频混流最终我们就能实现一个完整的视频了。三、模块安装为了方便,我们全都使用pip安装:pip install pillow
pip install opencv-python
pip install moviepy
# 安装paddlepaddle
python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple
# 安装paddlehub
pip install -i https://mirror.baidu.com/pypi/simple paddlehub也就不废话了,如果安装过程中出了什么问题可以自行百度或者联系博主,我会尽量解答的,毕竟我也只是个菜鸡。四、代码实现我们先看看导入的一些模块:import cv2
import math
import numpy as np
from PIL import Image
import paddlehub as hub
from moviepy.editor import *我们按照上面的步骤,一步一步来。4.1、逐帧提取图像这就需要使用到我们的opencv了,具体代码如下:def getFrame(video_name, save_path):
"""
传入视频名称,将图像帧保存到save_path下
"""
# 读取视频
video = cv2.VideoCapture(video_name)
# 获取视频帧率
fps = video.get(cv2.CAP_PROP_FPS)
# 获取画面大小
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
size = (width, height)
# 获取帧数
frame_num = str(video.get(7))
name = int(math.pow(10, len(frame_num)))
ret, frame = video.read()
while ret:
cv2.imwrite(save_path + str(name) + '.jpg', frame)
ret, frame = video.read()
name += 1
video.release()
return fps, size这里我们只需要注意OpenCV版本需要在3.0以上,如果是低版本的话会出现兼容问题。4.2、批量抠图批量抠图需要使用到我们的paddhub模型库,而抠图的实现也只需要几行代码:def getHumanseg(frames):
"""
对frames路径下所以图片进行抠图
"""
# 加载模型库
humanseg = hub.Module(name='deeplabv3p_xception65_humanseg')
# 遍历路径下文件
files = [frames + i for i in os.listdir(frames)]
# 抠图
humanseg.segmentation(data={'image': files})我们调用该方法后会在目录下生成humanseg_output目录,抠好的图像就在里面。4.3、合成图像(影分身)这里需要使用到我们的Pillow模块,该模块中提供了图像粘贴的函数:def setImageBg(humanseg, bg_im):
"""
将抠好的图和背景图片合并
:param humanseg:
:param bg_im:
:return:
"""
# 读取透明图片
im = Image.open(humanseg)
# 分离色道
r, g, b, a = im.split()
# 在图片右边粘贴一个人物分身
bg_im.paste(im, (bg_im.size[0]//3, 0), mask=a)
# 在图片左边粘贴一个人物分身
bg_im.paste(im, (-bg_im.size[0]//3, 0), mask=a)
# 将图形转换成opencv能正常读取的类型,并返回
return np.array(bg_im.convert('RGB'))[:, :, ::-1]上面主要就是使用paste函数。4.4、写入视频写入视频的操作同样是OpenCV来实现的:def writeVideo(humanseg_path, frames, fps, size):
"""
传入抠好的人像,和原图像,以及原视频帧率,大小,写入新视频
"""
# 写入视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('green.mp4', fourcc, fps, size)
# 将每一帧设置背景
humanseg = [humanseg_path + i for i in os.listdir(humanseg_path)]
frames = [frames + i for i in os.listdir(frames)]
for i in range(humanseg.__len__()):
# 读取原图像
bg_im = Image.open(frames[i])
# 设置分身
im_array = setImageBg(humanseg[i], bg_im)
# 写入视频
out.write(im_array)
out.release()到这里我们就实现了一个视频,但是现在还没有声音,接下来就需要我们用moviepy进行音频的混流了。4.5、混流我们混流的操作就是先获取音频,然后再混流,而音频我们只需要读取原视频的音频即可:def getMusic(video_name):
"""
获取指定视频的音频
"""
# 读取视频文件
video = VideoFileClip(video_name)
# 返回音频
return video.audio其中VideoFileClip是moviepy中的一个视频处理的类。下面我们来添加音乐:def addMusic(video_name, audio):
"""实现混流,给video_name添加音频"""
# 读取视频
video = VideoFileClip(video_name)
# 设置视频的音频
video = video.set_audio(audio)
# 保存新的视频文件
video.write_videofile(output_video)output_video是我们自己定义的一个存放文件保存路径的变量,需要注意,该全路径(路径+名称)不能和原视频相同。4.6、实现特效也就是将整个流程整合到一起:def changeVideoScene(video_name):
"""
:param video_name: 视频的文件
:param bgname: 背景图片
:return:
"""
# 读取视频中每一帧画面
fps, size = getFrame(video_name, frames)
# 批量抠图
getHumanseg(frames)
# 将画面一帧帧写入视频
writeVideo(humanseg_path, frames, fps, size)
# 混流
addMusic('green.mp4', getMusic(video_name))在上面有些变量我们还没有定义,我们在main函数中定义一下:if __name__ == '__main__':
# 当前项目根目录
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "."))
# 每一帧画面保存的地址
frames = BASE_DIR + '\\frames\\'
# 抠好的图片位置
humanseg_path = BASE_DIR + '\\humanseg_output\\'
# 最终视频的保存路径
output_video = BASE_DIR + '\\result.mp4'
# 创建文件夹
if not os.path.exists(frames):
os.makedirs(frames)
if not os.path.exists(background_path):
os.makedirs(background_path)
# 给视频添加特效
changeVideoScene('jntm.mp4')这样就实现了我们完整的特效。
后端小马
有趣的 Python 图像处理
前言图像处理是一门很高的学问,也非常有趣。要掌握好图像处理需要了解很多知识,但是作为娱乐,对这方面知识的要求就低了许多。今天就带大家来了解一下图像处理的知识,Python 图像处理通常使用 Pillow 库,其中提供了大量的操作,像灰度转换、高斯模糊、色道分离等,另外 OpenCV 中也提供了一些图像处理的方法,我们可以结合两者进行有效的图像处理。一、PIL 简介PIL 的全称是(Python Imaging Library)也就是 Python 图像库的意思,实际上 PIL 只支持到 Python 2.7,现在我们大多数人使用的都是 Python 3.x,在 3.x 中有兼容版的 Pillow 模块,有了 PIL 的基本操作,同时也加入了一些新的操作。1.1 安装我们使用 pip 安装:pip install pillow安装完成后我们就可以使用了,虽然在安装是我们时使用 Pillow,但是在实际使用中,我们导入的是 PIL:from PIL import Image其中,Image 类是我们最常使用的一个类。导入模块后,我们来看看最简单的操作:from PIL import Image
# 打开图片
img = Image.open('0_img/q.jpg')
# 显示图片
img.show()这样我们就完成了图片读取和图片显示的操作了。当然,我们获取图像的对象之后,可以获取它相应的属性:from PIL import Image
# 打开图片
img = Image.open('0_img/q.jpg')
print('图像格式:', img.format)
print('图像大小:',img.size)
print('图像宽度:', img.width)
print('图像高度:', img.height)
# 显示图片
img.show()输出如下:图像格式: JPEG
图像大小: (1280, 720)
图像宽度: 1280
图像高度: 720二、使用 PIL 处理图像通常处理图像需要我们掌握许多和图像相关的知识,但是我们使用模块可以很简单地实现图像处理。我们不需要知道底层实现,只需要知道这些处理的作用就可以了。2.1 图像数组我们使用 PIL 读取图片,它的返回值是一个 PIL.JpegImagePlugin.JpegImageFile 对象,而我们在使用 OpenCV 处理时,处理的是 ndarray 对象。我们可以使用如下语句将其转换成 ndarray 对象:from PIL import Image
import numpy as np
# 读取图像
img = Image.open('0_img/q.jpg')
# 使用np将其转换成ndarray对象
img_array = np.array(img)而转换成的 ndarray 对象有 RGB 三个色道,图像的每个像素点都有三个值,值的大小为(0~255)。2.2 灰度转换灰度变换是指根据某种目标条件按一定变换关系逐点改变源图像中每一个像素灰度值的方法,灰度转换可以改善图像的画质。我们通常说的黑白照片就是灰度照片,而真正意义上的黑白图像中的像素值只有 0 和 255,而 PIL 实现灰度转换也非常简单:from PIL import Image
# 读取图像
img = Image.open('0_img/q.jpg')
# 灰度转换
grey_img = img.convert('L')
# 保存图像
grey_img.save('0_img/grey.jpg')原图和灰度图对比如下:2.3 图像混合图像混合就是将两张图片混合在一起,我们可以创建一张单色的图像,然后再进行混合:from PIL import Image
# 读取图像
img1 = Image.open('0_img/q.jpg')
# 创建一个图像
img2 = Image.new('RGB', img1.size, (255, 0, 0))
# 混合图像
Image.blend(img1, img2, 0.5).show()我们使用 Image.new 创建一个图像,该方法传入三个参数,第一个是图像的模式通常有“RGB”、“L”(灰度)等。第二个参数为图像大小,第三个参数则是颜色值。我们可以直接传入颜色字符“red”、“blue”等。混合图像的方法为 Image.blend,该方法接收三个参数,第一个和第二个都是图片对象,第三个为 img2 的透明度。混合后效果如下:2.4 图像缩放图像缩放即按照尺寸放大或缩小图像,通常我们可以使用该方法生成图像的缩略图。图像缩放使用到 thumbnail 方法,该方法接收一个元组,元组包含两个元素,宽和高:from PIL import Image
# 读取图像
img1 = Image.open('0_img/q.jpg')
# 输出元素图像的尺寸
print(img1.size)
# 将img1的宽和高都缩小到原来的1/2
img1.thumbnail((img1.size[0]//2, img1.size[1]//2))
# 输出缩放后的尺寸
print(img1.size)输出结果如下:(1280, 720)
(640, 360)
这里我们需要注意一下,上面接触到的 open、new 和 blend 方法都是 Image 类的静态方法,而 thumbnail 是是需要用实际对象执行的方法。2.5 图像裁剪和粘贴裁剪和粘贴是图像的基本操作,在 PIL 中实现起来非常简单。裁剪使用的是 crop 方法,该方法传入一个元组,该元组有四个参数(左上角的 x,左上角的 y,右下角的 x,右下角的 y),裁剪是在原图的基础上进行的。粘贴同样是在原图的基础上进行的,粘贴是通过 paste 方法实现,该方法传入两个参数,图像对象和左上角坐标元组。代码如下:from PIL import Image
# 读取图像
img = Image.open('0_img/q.jpg')
# 拷贝图片
img1 = img.copy()
img2 = img.copy()
# 裁剪图像,
crop_im = img1.crop((200, 200, 400, 400))
# 粘贴图像
img2.paste(crop_im, (0, 0))
img2.show()效果图如下:2.6 色道分离和合并实现色道分离只需要调用 split 方法,该方法返回 3 个灰度的 Image 对象。色道的合并 Image.merge 方法,该方法传入两个参数,第一个是图像模式,第二个是色道的列表:from PIL import Image
# 读取图像
img1 = Image.open('0_img/q.jpg')
img2 = Image.open('0_img/s.jpg')
# 色道分离
r1, g1, b1 = img1.split()
r2, g2, b2 = img2.split()
# 色道合并
img3 = Image.merge('RGB', [r1, g2, b1])
img3.show()效果图和原图对比如下:左为 s.jpg,中间为 q.jpg,最右边为合并后的图像。2.7 高斯模糊高斯模糊也叫高斯平滑,直观来看的话就是会使图片模糊,但是它实际的作用是对图片降噪。有许多处理需要预先用到高斯模糊,在 PIL 中通过调用 filter 实现:from PIL import Image,ImageFilter
# 读取图像
img = Image.open('0_img/q.jpg')
# 高斯模糊
g_img = img.filter(ImageFilter.GaussianBlur)
g_img.show()filter 方法是给图片添加滤镜,在 ImageFilter 类中提供了许多预设好的滤镜。2.8 色彩亮度调节在 ImageEnhance 中提供了许多调节图像的类,如下:类名方法作用ImageEnhance.Color获取颜色调整器ImageEnhance.Contrast获取对比度调整器ImageEnhance.Brightness获取亮度调整器ImageEnhance.Sharpness获取清晰度调整器要调节图像我们需要先生成相应的调节器,然后再进行调节,下面我们用颜色为例:from PIL import Image,ImageEnhance
# 读取图像
img = Image.open('0_img/q.jpg')
# 获取颜色调节器
color_enhance = ImageEnhance.Color(img)
# 调节颜色,传入值小于1就是减弱,大于1为增强
enhance_img = color_enhance.enhance(0.5)
# 显示图像
enhance_img.show()其它属性的调节同颜色一样。三、OpenCV 简介OpenCV 同 PIL 一样,也可以进行图像处理。但是 OpenCV 读取图像是以 ndarray 形式,且模式为 BGR。我们先安装 OpenCV:pip3 install opencv-python然后我们就可以开始使用了。3.1 OpenCV 读取图像OpenCV 读取图像是通过 cv2.imread 方法实现,该方法返回的是 ndarray 对象,且模式为 BGR。import cv2
# 读取图像
img = cv2.imread('jt.jpg')
# 显示图像,传入两个参数,第一个是窗口名,第二个是ndarray对象
cv2.imshow('img', img)
# 等待键盘输入
cv2.waitKey(0)
# 销毁所有窗口
cv2.destroyAllWindows()调用 imshow 方法图像只会显示一瞬间,所以需要调用 waitkey 方法,该方法接收等待的毫秒数,如果传入 0 则是无限等待。因为 OpenCV 的底层是 C/C++ 所以,需要释放窗口内存。3.1 OpenCV 读取的图像和 PIL 读取的图像互相转换我们上面已经知道如何将 Image 对象转换成 ndarray 对象,但是因为图像模式不同,显示的时候是不一样的,所以我们需要将 RGB 转成 BGR 或者 BRG 转换成 RGB。RGB 转 BGR:import cv2
import numpy as np
from PIL import Image
# 用PIL读取图像
pil_img = Image.open('jt.jpg')
# 转换成ndarray对象,该对象模式为rgb
rgb = np.array(pil_img)
# 将rgb转换成bgr
bgr = rgb[...,::-1]
# 使用opencv显示图像
cv2.imshow('pil_im', bgr)
cv2.waitKey(0)
cv2.destroyAllWindows()BGR 转 RGB:import cv2
from PIL import Image
# 读取图像
bgr = cv2.imread('jt.jpg')
# bgr转rgb
rgb = bgr[...,::-1]
# 将ndarray对象转换成Image对象
cv_im = Image.fromarray(rgb)
cv_im.show()执行后发现显示效果都同原图一样。四、综合操作前面我们一直在讲简单的图像处理,下面我们来一些综合的操作。4.1 边缘检测在 OpenCV 中提供了许多边缘检测的算法,其中 Canny 是当前最优算法,我们可以很快地实现边缘检测,代码如下:import cv2
# 读取图片
img = cv2.imread('jt.jpg')
# 灰度转换
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# canny算法,该算法
canny_im = cv2.Canny(grey, 50, 200)
# 显示图片
cv2.imshow('canny', canny_im)
cv2.waitKey(0)
cv2.destroyAllWindows()我们看看原图和效果图:大致轮廓都出来了,但是浅色部分没有识别出,于是我们就想到颜色增强。修改后代码如下:import cv2
import numpy as np
from PIL import Image, ImageEnhance
# 读取图像
img = Image.open('jt.jpg')
# 增强颜色
color_enhance = ImageEnhance.Color(img)
en_im = color_enhance.enhance(5)
# 转换成ndarray对象
img_array = np.array(en_im)
# 转换成bgr
bgr = img_array[...,::-1]
# canny算法,该算法传入三个值,第一个是ndarray对象,第二个和第三个为阈值
canny_im = cv2.Canny(bgr, 150, 200)
cv2.imshow('canny', canny_im)
cv2.imwrite('canny.jpg', canny_im)
cv2.waitKey(0)
cv2.destroyAllWindows()效果图如下:大家可以根据不同的图片进行调节。4.2 图片文字结合效果我们先准备一张文字图片,尽量小一点,文字图片可以用 Windows 自带的画图工具制作。然后再另外准备一张图片。制作思路如下:准备文字图片和背景图片我们根据背景图片的大小,创建一个白色图片我们将文字图片从左到右,从上到下粘贴到白色图像上让粘贴了问题的图像和背景图像混合代码如下:from PIL import Image
# 读取背景图像和文字图像
im = Image.open('zxz.jpg')
source = Image.open('zack.png')
# 获取背景图像和文字图像的大小
im_size = im.size
source_size = source.size
# 计算一行可以摆放多少文字图片
width_num = im_size[0] // source_size[0]
# 计算一列可以摆放多少文字图片
height_num = im_size[1] // source_size[1]
# 创建一个同背景图片大小一致的白色图片
bg_im = Image.new('RGB', im_size, (255, 255, 255))
# 循环变量
x, y = 0, 0
# 循环,将文字图片从左到右从上到下铺满白色图片
for i in range(width_num+1):
x = i * source_size[0]
for j in range(height_num+1):
y = j * source_size[1]
bg_im.paste(source, (x, y))
# 将粘贴了文字图像的图像和背景图像混合
Image.blend(im, bg_im, 0.5).save('res.png')上述代码实现如下效果:大家可以根据自己学习的知识,制作一些有趣的混合图像。
后端小马
Python自然语言处理只需要5行代码
一、前言人工智能是Python语言的一大应用热门,而自然语言处理又是人工智能的一大方向。 自然语言处理( natural language processing )简称NLP,是研究人同计算机之间用自然语言通信的一种方法。我们都知道,计算机本质上只认识0和1,但是通过编程语言我们可以使用编程语言同计算机交流。这实际上就是程序员同计算机之间的通信,而我们日常生活中使用的是自然语言,是一种带有情感的语言。那么要怎么使计算机理解这种带有情感的语言呢?这就是自然语言处理研究的内容了。语言的情绪识别是自然语言处理的一种操作,如果要我们从0开始实现情绪识别是比较繁琐的。首先我们需要准备好足够的数据,为了让计算机更好的理解,我们还需要对数据进行预处理,之后需要训练数据,有了训练数据我们才能开始情绪识别。识别的准确率在于数据的相关性和数据量,数据相关性越高,数据量越大,识别的准确率就越高。然而,我们使用paddlehub可以很快的实现情绪识别,我们先看看如何安装。二、安装paddlehubpaddlehub是百度飞桨PaddlePaddle中的一个模型库,使用paddlepaddle可以很快的实现多种多样的操作,其中就有我们今天要说到的文字情绪识别,而且代码非常简单。首先我们需要安装paddlepaddle,我们进入官网 https://www.paddlepaddle.org.cn/install/quick ,进入官网后可以看到如下界面:我们可以根据自己Python版本,计算机系统等选择安装方式。关于paddlepaddle支持版本等信息可以在官网中查看,这里就不赘述了。我使用的是Python3.7,这里直接使用pip安装,我们在控制台执行下列语句python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple然后在控制台查看是否安装成功。先输入Python,然后执行import paddle.fluid,再执行 paddle.fluid.install_check.run_check()如下:C:\Users\zaxwz>python
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import paddle.fluid
>>> paddle.fluid.install_check.run_check()如果显示Your Paddle is installed successfully! Let's start deep Learning with Paddle now就表示安装成功了。另外我们还需要安装paddlehub,这里同样是使用pip安装,执行语句如下:pip install -i https://mirror.baidu.com/pypi/simple paddlehub把paddlepaddle和paddlehub安装成功后我们就可以开始写代码了。三、情绪识别(1)情绪识别使用paddlehub完成情绪识别的步骤如下:导入模块加载模型准备句子识别情绪而完成上面的步骤只需要四行代码,另外我们需要输出一下识别的结果,一共就是五行代码,下面让我们看看这五行神奇的代码:# 导入模块
import paddlehub as hub
# 加载模型
senta = hub.Module(name='senta_lstm')
# 准备句子
sentence = ['你真美']
# 情绪识别
result = senta.sentiment_classify(data={"text":sentence})
# 输出识别结果
print(result)识别结果如下:[{'text': '你真美', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.9602, 'negative_probs': 0.0398}]我们可以看到准备的句子是列表类型,识别的结果也是列表类型,结果中列表的元素是一个字典。在这个字典列表中,就包含了我们识别的结果。下面我们来分析一下这个字典。(2)结果分析结果字典中包含了4个字段,我们以表格的形式展示一下:字段名称字段含义解释text识别的源文本识别的源文本sentiment_label分类标签1为积极,0为消极sentiment_key分类结果positive为积极,negative为消极positive_probs积极率情绪为积极的可能性negative_probs消极率情绪为消极的可能性上面的表格是本人编的,用词不当的地方多见谅。我们对照上述表可以分析一下我们上述程序的结果。其含义就是语句“你真美”中包含了积极的情绪。我们再多看几个例子:import paddlehub as hub
senta = hub.Module(name='senta_lstm')
sentence = [
'你真美',
'你真丑',
'我好难过',
'我不开心',
'这个游戏好好玩',
'什么垃圾游戏',
]
results = senta.sentiment_classify(data={"text":sentence})
for result in results:
print(result)识别结果如下:{'text': '你真美', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.9602, 'negative_probs': 0.0398}
{'text': '你真丑', 'sentiment_label': 0, 'sentiment_key': 'negative', 'positive_probs': 0.0033, 'negative_probs': 0.9967}
{'text': '我好难过', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.5324, 'negative_probs': 0.4676}
{'text': '我不开心', 'sentiment_label': 0, 'sentiment_key': 'negative', 'positive_probs': 0.1936, 'negative_probs': 0.8064}
{'text': '这个游戏好好玩', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.9933, 'negative_probs': 0.0067}
{'text': '什么垃圾游戏', 'sentiment_label': 0, 'sentiment_key': 'negative', 'positive_probs': 0.0108, 'negative_probs': 0.9892}上面有6个句子,大多数都成功识别了情绪,但是我好难过识别的结果为积极,很明显是错误的。总体来说,对于简单语句的识别还是比较准的。
后端小马
如何用Python生成一个优雅的二维码
如何用Python生成一个优雅的二维码二维码作为一种信息传递的工具,在当今社会发挥了重要作用。从手机用户登录到手机支付,生活的各个角落都能看到二维码的存在,那么我们如何自己生成一个二维码呢?如果使用Python,我们可以很快的生成一个二维码,我们可以自己定义二维码包含的信息。这些信息可以是文字、图片,也可以是网站。下面我们就来看看如何生成一个二维码。一、使用MyQR生成二维码生成二维码的方式多种多样,我们先来看看使用MyQR模块如何生成一个二维码。(1)模块安装在开始使用之前我们需要先安装该模块。这里使用pip直接下载,这里选用的是国内的源:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ myqr
安装完成后我们就可以开始使用了。先生成一个最简单的二维码:from MyQR import myqr # 注意区分大小写
myqr.run(words='Do not go gentle into that good night!') # 生成二维码
在我们运行这个程序后,py文件同目录下会生成图片qrcode.png,该图片就是我们的二维码图片,扫出来就是我们上面设置的文本信息了。(2)生成一个图像二维码我们日常生活中的二维码都比较单调,有纯二维码,整个二维码只有黑白方块;也有带图片的二维码,通常是在二维码中心放置一个图片,而我们现在要做的是一个整体是一张图片的二维码。也就是将一张图片作为背景。这种二维码实现起来也非常简单:from MyQR import myqr
myqr.run(
words='http://www.baidu.com', # 包含信息
picture='lbxx.jpg', # 背景图片
colorized=True, # 是否有颜色,如果为False则为黑白
save_name='code.png' # 输出文件名
)生成二维码效果如下:可以看到,这里我们二维码包含的信息是一个网址,这个时候我们扫描二维码会直接跳转网页。代码一样我们只需要将picture参数设置为一张动图,另外输出文件后缀改为gif即可:二、使用qrcode生成二维码qrcode同样是一个便捷的工具,使用该模块我们也能够很快的实现二维码的生成。(1)模块安装这里同样使用pip安装,我们在命令行窗口执行下列语句:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ qrcode
安装完成后我们就可以开始生成我们的第一个二维码了:import qrcode
img = qrcode.make('http://www.baidu.com')
img.save('qrcode.jpg')
在我们调用save方法后,项目下就会生成一张qrcode.png图片,该图片就是我们的二维码图片,我们扫出来同样是直接跳转网页。(2)更准确的生成二维码除了上面的方式,我们还可以通过QRCode类来生成二维码,这种方式我们可以控制二维码的更多信息:from qrcode import QRCode
qr = QRCode() # 创建二维码对象
qr.add_data('http://www.baidu.com') # 设置二维码数据
img = qr.make_image() # 创建二维码图片
img.save('qrcode.png') # 保存二维码图片
通过这种方式我们同样可以生成一个二维码,当然我们还可以丰富一下:import qrcode
qr = qrcode.QRCode(
version=5, # 二维码的大小,取值1-40
box_size=10, # 二维码最小正方形的像素数量
error_correction=qrcode.constants.ERROR_CORRECT_H, # 二维码的纠错等级
border=5 # 白色边框的大小
)
qr.add_data('http://www.baidu.com') # 设置二维码数据
img = qr.make_image() # 创建二维码图片
img.save('qrcode.png') # 保存二维码
其中version包含了大小信息,当设置为1时,生成一个12x12大小的二维码,单位为box_size个像素。我们可以将version设置为None,并添加一句qr.make(fit=True),这样程序会自动生成大小合适的二维码。另外error_correction为纠错等级的设置,纠错等级是什么这就是关于二维码本身的知识了。ERROR_CORRECT_L:大约7%或更少的错误能被纠正。ERROR_CORRECT_M(默认):大约15%或更少的错误能被纠正。ROR_CORRECT_H:大约30%或更少的错误能被纠正。上面是可以供我们选择的几个内置常数。(3)读取二维码中的数据上面我们一直在讲如何生成二维码,但是我们人本身是无法读取二维码中的信息,这就要借助我们的设备了。在Python中,我们可以通过pyzbar模块来识别二维码的识别,当然还有其它方法,这里我们使用pyzbar看看应该如何识别二维码,首先我们需要安装模块:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ pyzbar
另外我们需要安装opencv模块:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ opencv-python
之后就可以开始识别二维码了:import cv2
from pyzbar import pyzbar
im = cv2.imread('qrcode.png') # 读取二维码
data = pyzbar.decode(im) # 解析二维码
print(data)我们使用如下图片作为测试:其中包含的信息为http://www.baidu.com,我们看一下输出结果:[Decoded(data=b'http://www.baidu.com', type='QRCODE', rect=Rect(left=5, top=5, width=29, height=29), polygon=[Point(x=5, y=5), Point(x=5, y=34), Point(x=34, y=34), Point(x=34, y=5)])]
显然是我们看不懂的东西,但是我们在里面看到了http://www.baidu.com的字样,我们可以通过如下方式解析出内容:import cv2
from pyzbar import pyzbar
im = cv2.imread('qrcode.png') # 读取二维码
data = pyzbar.decode(im) # 解析二维码
text = data[0].data.decode('utf-8') # 解析数据
print(text)
输出结果如下:http://www.baidu.com
这样我们就算是将内容解析出来了。
后端小马
Python在爬虫、自动化办公、机器学习的实战案例
本专栏专注于分享Python实战文章,读者只需要掌握Python的基础语法就能理解。涉及爬虫、自动化办公、机器学习等方面。