首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2025年5月11日, 农历四月十四, 星期日
2
「今日早报」 2025年5月10日, 农历四月十三, 星期六
3
「今日早报」 2025年5月9日, 农历四月十二, 星期五
4
「今日早报」 2025年5月8日, 农历四月十一, 星期四
5
「今日早报」 2025年5月7日, 农历四月初十, 星期三
沙漠渔
把過去的累積,善用到當下
累计撰写
2,465
篇文章
累计创建
385
个标签
累计收到
994
条评论
栏目
首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
搜索
标签搜索
代理服务
winsw
override
VMware
api
popai
拉取镜像
人工智能
copilot
chatgpt
openai
coze
objdump
ldd
日志
版本
latest
批处理
bat
节能模式
iwconfig
排序
du
设计模式
hostname
面板
cockpit
版本不兼容
npm
统计
烟花
新春
leveldb
Java heap space
堆内存
harbor
utf8mb4
网络聚合
IPV6
nmtui
测速
带宽
千兆
路由器
nmcli
nmlci
orangepi
motd
中文乱码
webdav
香橙派
代码折叠
享元模式
单例模式
解锁
锁定
无法安装
Xshell
并发编程
ScheduledThreadPoolExecutor
ThreadPoolExecutor
线程池
Fock-Join
并发
ExecutorService
nextcloud
alist
panic
hung task
时间戳
ping
tail
dd
嵌入式
点灯
mount
共享
NFS
curl
全屏
ChannelOption
C++
comparator
桥头堡
开源
varchar
char
StringBuilder
StringBuffer
String
命令行工具
网络配置
netsh
建议
专家
离谱
2022
设备管理器
虚拟网卡
环回适配器
安全
攻击
CC
DDOS
跳槽
过年
年假
春节
2023
优站计划
鼻塞
咳嗽
大号流感
阳
新冠病毒
循环冗余校验
CRC-16/XMODEM
crc校验
stream
DMZ主机
域名解析
CDN
七牛云
DDNS
类加载器
双亲委派
加载机制
删除
搜索
变化时间
修改时间
访问时间
响应异常
超时
jsoup
用法详解
压缩命令
打包压缩
zip
优缺点
打包
数据压缩
密码
sudoers
索引量
权重
瞬间
魔鬼洞
沙漠鱼
沙漠渔
面试
引用传递
值传递
即时编译器
机器码
JIT
旗舰版
ISO
原版镜像
win7
参数
配置文件
8小时
时间错误
报告
coverity
jetbrains
idea
谷歌翻译
rest
403
ERR_UNKNOWN_URL_SCHEME
DevTools
优先级
location
FTP
挂载
curlftpfs
提示
自定义参数
springboot
配置
死锁
超级密码
桥接
联通光猫
Dockerfile
构建
命令行
eclipse
光猫
路由模式
桥接模式
bash-4.2
sudo
oracle帐号
JDK下载
百度收录
定时发布
哨兵模式
内存管理
JVM
登录
免密
不一致
服务器时间
BIO,NIO,AIO
ByteBuffer
FileWriter
BufferedOutputStream
流水线
pipeline
环境变量
重启
任务消失
jenkins
卸载
snap
20.04
Ubuntu
容器
大数据传输
InputStream
学习笔记
CSS
最佳实践
DOM操作
原型链
原型
No such file or Directory
SSH配置
SSH免密
tar
命令
网站优化
取消快照
百度快照
全局配置
修改密码
控制台
gitlab
解码器
Netty
文件传输
sz
rz
Jar
打铁花
羊毛沟
容器日志
docker
规范
博客
rejected
Gerrit
wireshark
代理
Go
http-server
nodejs
package-lock.json
996加班
删除标签
钟薛高
60s新闻接口
每日新闻接口
百度福利
自动回复
localstorage
QQ机器人
redis
近乎
客户端
JuiceSSH
乔恩
加菲猫
新浪邮箱
joe
公告
父亲节
图标
每日新闻
51la
服务器
腾讯云
插件
罗永浩
编码
avatar
小虎墩大英雄
端午劫
端午节
中石化
中国石化
儿童节
win10
激活
shell
金门大桥
旧金山
产品经理
免疫力
熬夜
云旅游
云游
招聘
海尔
halo
校友会
创新
哈工大
鸿蒙
王成录
标题
主题
北京
疫情
域名
沙漠渔溏
方法声明
jpa
表情包
emoji
JavaAgent
姓名测试
姓名打分
起名
百度
收录
SEO
group by
distinct
去重
JDBC
validationQuery
兄弟元素
点击事件
$event
9600
传输延时
波特率
串口
站点统计
站长工具
cnzz
生命周期
refs
vue
replaceAll
replace
JavaScript
软件
FRP
内网外入
内网穿透
学习
解题
leetcode
云计算
中国医药
神思电子
股票
Java
报错
数据块
关键词
风筝
清明节
注解
spring
clang-format
格式化
反向代理
nginx
索引
linux
vim
数据库
仓库管理
git
压缩
winrar
mysql
测试
markdown
目 录
CONTENT
以下是
网络聚合
相关的文章
2024-01-19
《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(6)-Charles安卓手机抓包大揭秘
1.简介 Charles和Fiddler一样不但能截获各种浏览器发出的 HTTP 请求,也可以截获各种智能手机发出的HTTP/ HTTPS 请求。 Charles也能截获 Android 和 Windows Phone 等设备发出的 HTTP/HTTPS 请求。 今天宏哥讲解和分享Charles如何截获安卓移动端发出的 HTTP/HTTPS 请求。 2.环境准备 Charles如果想要实现手机抓包,需要先满足下面 3 个条件: (1)电脑上安装有Charles抓包工具。 (2)安装有Charles的电脑必须跟手机处在同一个网络里,并且手机网络代理必须设置为Charles,当我们的手机发送数据时必须经过Charles这一层服务。 (3)在Charles中设置好捕获 HTTPS。 1.宏哥的环境是Windows 10版本 64位系统。如下图所示: 2.夜神模拟器的安卓版本是:5.1.1 。 如下图所示: 3.大致思路步骤 1.电脑本地安装charles证书 2.查看电脑charles的IP和端口号 3.手机连接charles,抓取简单的http 4.charles设置ssl proxy setting 5.手机安装charles证书 4.为什么需要安装Charles的CA证书呢? 1)先理清一些概念的东西: a)简单来说,https是http的安全版本,超文本传输协议http是以明文发送数据,而https是具有安全性的ssl加密传输协议,可以这么认为https=http+ssl。 b)采用https的服务器必须从CA申请一个用于证明服务器用途类型的证书,证书是唯一性,只用于对应的服务器。客户端要认可这个服务器是否是安全的,可以进行访问或者交易等操作,则需要进行对服务端的验证。 下图是客户端对服务器的验证过程: c)ssl证书,遵循了ssl协议,在客户端和服务器之间建立了一条ssl安全通道,一般ssl证书都是在验证服务器身份后颁发给客户端。 d)由于ssl技术已建立在所有主要的浏览器和web服务器程序中,因此,仅需安装服务器证书就可以激活ssl协议,所以客户端通过信任该证书,就相当于信任了该主机(服务器)。 下图是客户端和服务端加密通讯的流程: 2)通过以上一个简单的理顺之后,这也就为什么当我们在使用Charles进行抓包的时候需要安装证书,可以通过ssl数字证书中的私用密钥来解译加密的信息,展示在Charles中,但是Charles有一个特殊的地方,就是实际上客户端安装的是Charles的CA证书,然后Charles安装服务器的CA证书,实际上流程还是一样的。 5.安卓手机抓包配置 5.1Charles PC端http配置 http是默认配置好的,要是默认安装完没配置好,我们就自己配置一下,默认的端口号是“8888”我们后面设置手机代理的的时候也要注意这个端口要保持一致。 Charles的配置:这是要打开代理功能,具体操作步骤如下: 1.启动Charles,点击“Proxy-->Proxy Settings”,然后在Proxy Settings中填好端口(8888),并且勾选上“Enable transparent HTTP proxying”,最后点击“OK”。如下图所示: 2.查看运行Charles的电脑的IP地址,可以在命令行中运行ipconfig或者直接查看网络配置,如下图所示: 3.可以对照一下当前所安装的Charles中的ip地址是否一致,你可以直接在Charles上可以直接查看Charles的IP和端口,点击“Help-->SSL Proxying”,然后点击“Install Charles Root Certificate on a Mobile Device or Remote Browser”,如下图所示: 5.2Charles PC端SSL(https)配置 无论电脑端还是手机端进行抓取Https的包,都需要先安装对应证书才能使用。具体操作步骤如下: 1.打开charles,点击help-->SSL Proxying-->Install Charles root Certificate 安装证书,如下图所示: 2.点击完“Install Charles root Certificate”后,然后点击“安装证书”,如下图所示: 3.点击“安装证书”后,选择存储位置“本地计算机”,点击“下一步”,如下图所示: 4.证书存储位置选择‘将所有的证书都放入下列存储’,然后点击证书存储后的“浏览”,证书存储选择“受信任的根证书颁发机构”,点击“确定”,如下图所示: 5.点击“下一步”,如下图所示: 6.点击“完成”,提示导入成功。如下图所示: 7.点击proxy》 SSL Proxyng Settings。如下图所示: 8.打开界面如下 弄到跟我一样就可以 如果没有*;443自己按add手动添host :*,port:443 。如下图所示: 这样就可以抓取PC端的http和https类型的包了,接下来我们就来进行安卓手机端代理的配置。 5.3安卓手机设置 本节内容适合所有的 Android设备。下面以夜神模拟器为例进行讲解,其他品牌的模拟器和真实的手机操作方法与此差不多。具体操作步骤如下: 1.在确定了手机和Charles在同一局域网下之后, 那么我们来到Android手机的设置选项下,找到夜神模拟器手机当前连接的WLAN(一些 Android 手机是单击右边的箭头,有的是长按弹出对话框),如下图所示: 2.看到有一个wifi信号,长按这个信号,出现修改网络的弹窗。如下图所示: 3.点击修改网络,选中高级选项,打开高级选项,将代理设为手动,代理服务器主机名填写电脑的IP,端口号填写为主机抓包工具的监听端口。如下图所示: 4.点击保存,就成功完成代理的设置了。如下图所示: 5.电脑出现允许代理的提示,点击Allow即可,如下图所示: 那么到此Android手机的网络代理设置就到此为止,其他不同型号的Android模拟器和真机设置大同小异,以此类推,实在不会的自己可以百度一下。到此处表示已经可以抓http的手机包了。 5.4测试Charles捕获手机发出的 HTTP 1.打开手机上的浏览器,在浏览器中输入链接:http://open.vipexam.org。中科VIPExam考试学习资源数据库网站用的是 HTTP 协议而不是 HTTPS 协议,查看 Fiddler 是否捕获到了 HTTP 数据包。如下图所示: 2.打开手机上的 APP,在 APP 中进行一些操作,查看 Fiddler 是否能捕获到 HTTP 数据包。如下图所示: 如果抓不到 HTTP 的包,很可能是 Windows 防火墙的问题,到控制面板中关闭防火墙后再试试。 5.5测试Fiddler捕获手机发出的HTTPS 1.打开手机上的浏览器,在浏览器中输入HTTPS协议,查看Charles是否捕获到了HTTPS数据包。一直在报证书安全警告错误,无法抓取,因此需要我们安装证书,原因宏哥在抓取PC端Web页面包已经说过了,这里就不做赘述了。如下图所示: 2.打开手机上的APP,在APP中进行一些操作,查看Fiddler是否能捕获到HTTPS数据包。又出现了Unknown,如下图所示: 到此,我们知道了要想抓取手机端Https的数据,还的配置证书,证书不用问了,还是Charles下发的。 5.6Android手机配置证书 通过前边宏哥的测试,我们知道在抓取Android手机数据包的时候 跟web端也是一样,都需要配置证书,否则是无法正常进行抓包的。之前已经在我们的android手机上配置好了Charles的代理服务了,那么现在就可以通过ip+port的方式来访问Charles从而下载对应的证书。具体操作步骤如下: 1.在Android手机上打开(自带)的浏览器,输入:http://chls.pro/ssl 来下载证书。如果不出意外的话就会出现如下界面: 2.给证书命名为:CharlesRoot,点击“确定”,如下图所示: 3.点击“确定”后,需要输入凭据存储的密码。如下图所示: 4.再次点击“确定”,提示需要设置锁屏密码(注:选择安装的文件后,需要输入手机的锁屏密码。Android一定要有锁屏密码才能安装证书),如下图所示: 5.点击“确定”。按要求设置一个手机密码,自己设置一个,记住密码就行,最后不用了去系统-安全-密码中去掉即可,如下图所示: 6.完成锁屏密码后,提示证书已安装,证书安装成功后,如果你的手机系统没有设置密码或者锁屏图案,则系统会提示你设置锁屏图案或者密码。如下图所示: 7.证书安装好后,查看已信任证书:具体位置在【设置--->安全--->信任的凭据--->用户】,如下图所示: 6.开始Android抓包 通过前边的配置,我们现在可以开始抓安卓手机的https的包了。 1.打开手机上的浏览器,在浏览器中输入HTTPS协议的网站,例如:百度。如下图所示: 7.小结 Charles和Fiddler一样,一个手机可以安装多个证书,但是每安装的一个证书里面都设置有IP地址,所以:安装的证书和电脑IP是一一对应的,当前的这个证书只能针对某一台电脑使用,更换电脑后,该证书将不能使用,只能重新安装与更换的电脑的IP相同的证书才能使用。 对了,关于Android7.0的版本在Fiddler那里已经详细地介绍了,只不过是工具换了一下,原理都差不多,这里和后边就不再做介绍了。而且这里介绍的和Fiddler抓包安卓手机的设置也基本一致,种种原因这里又啰嗦水了一遍。凑合看吧。
2024-01-19
184
0
0
网络聚合
2024-01-19
SpringBoot项目动态定时任务之 ScheduledTaskRegistrar(解决方案一)
前言 在做SpringBoot项目的过程中,有时客户会提出按照指定时间执行一次业务的需求。 如果客户需要改动业务的执行时间,即动态地调整定时任务的执行时间,那么可以采用SpringBoot自带的ScheduledTaskRegistrar类作为解决方案来实现。 在单一使用ScheduledTaskRegistrar类解决定时任务问题的时候,可能会达不到预期的动态调整定时任务的效果。 如果灵活配合使用对应的工具类(ThreadPoolTaskScheduler类),则可以方便地对动态调整定时任务进行管理。 本文会从问题出发,详细介绍ScheduledTaskRegistrar类是如何解决动态调整定时任务的思路,并给出关键的代码示例,帮助大家快速地上手学习。 目录 一、问题背景 在指定的某一时刻执行业务; 可以手动地更改执行时间。 在实际项目中,很少会有傻瓜式地去指定某一时间就触发某个业务的场景,执行业务的时间不是一成不变的,而是动态地随着客户所指定的时间进行调整的。 二、痛点所在 如果单一地使用SpringBoot自带的ScheduledTaskRegistrar去实现,那么可能会有以下问题: 只能按照指定的时间去执行,更改执行时间需要重启服务; 无法删除该定时任务,或者删除后无法再启动新的定时任务。 业务逻辑与触发器的代码耦合度太高,无法将业务代码从ScheduledTaskRegistrar类中抽离出去。 /** * @author Created by zhuzqc on 2023/1/30 15:28 */ @Slf4j @Component @EnableScheduling public class ScheduleTaskDemo implements SchedulingConfigurer { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { //Runnable线程注册任务 Runnable taskOne = () -> { //需要执行的业务逻辑,一般会在这里封装好 logger.info("----------业务执行结束----------"); }; //任务的触发时间,一般使用 cron 表达式 Trigger triggerOne = triggerContext -> { Date nextExecTime = null; try { // 此处指定 cron 表达式 String cron = "0 00 12 ? * *"; if (StringUtils.isBlank(cron)) { // 提示参数为空 logger.info("trigger定时器的 cron 参数为空!"); // 如果为空则赋默认值,每天中午12点 cron = "0 00 12 ? * *"; } logger.info("---------->定时任务执行中<---------"); CronTrigger cronTrigger = new CronTrigger(cron); nextExecTime = cronTrigger.nextExecutionTime(triggerContext); } catch (Exception e) { e.printStackTrace(); log.info(e.getMessage()); } return nextExecTime; }; taskRegistrar.addTriggerTask(taskOne, triggerOne); } } 上述代码只能实现在指定的时间去触发定时任务,无法对 cron 表达式进行更改,如果更改则需要重新启动服务,非常地“傻瓜”。 而在实际的编码过程中,业务逻辑代码需要单独地剥离开(解耦),如何做到业务逻辑代码和触发器代码都能访问到外部业务数据,是设计过程中需要考虑到的关键。 三、解决思路 //TODO:如果要在此处将业务逻辑和时间触发器进行捆绑,那么在这个实现类中无法获取到来自该类外部的业务数据; //TODO:要解决这样的问题,就要找到一个办法:既能将两者抽离,又能实现灵活触发定时任务。 在这里介绍一个名为ThreadPoolTaskScheduler类,通过源码得知,该类实现了SchedulingTaskExecutor和TaskScheduler接口。 该类中schedule(Runnable task, Trigger trigger)方法,通过分别传入线程任务(业务逻辑)和Trigger触发器对象作为参数,支持动态创建指定 cron 表达式的定时任务。 该方法源码如下: @Override @Nullable public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) { ScheduledExecutorService executor = getScheduledExecutor(); try { ErrorHandler errorHandler = this.errorHandler; if (errorHandler == null) { errorHandler = TaskUtils.getDefaultErrorHandler(true); } return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule(); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); } } 以下部分是对该方法的具体使用,核心思路如下 : 实例化ThreadPoolTaskScheduler类对象; 实例化ScheduledFuture类对象,用于初始化调用schedule()后的值。 将携带有Runnable和Trigger的ScheduledFuture类对象作为Map的value进行装配。 根据Map的key对定时任务进行管理,达到添加和删除的目的。 四、代码示例 代码示例分为两部分: 第一部分是关于ThreadPoolTaskScheduler类和schedule()方法的使用; /** * @author @author Created by zhuzqc on 2023/1/30 15:39 * 任务线程池管理工具 */ public class TaskSchedulerUtil { private static final Logger logger = LoggerFactory.getLogger(TaskSchedulerUtil.class); /** * 线程调度工具对象,作为该类的成员变量 */ private static ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); /** *初始化 map 对象,装配 schedule 方法的返回对象为 value 值 */ private static Map<String, ScheduledFuture<?>> scheduledFutureMap = new HashMap<String, ScheduledFuture<?>>(); static { threadPoolTaskScheduler.initialize(); } /** * 将Runnable对象和Trigger对象作为参数传入该静态方法 * @param runnable * @param trigger * @param 定时任务id */ public static void put(Runnable runnable, Trigger trigger, String id) { // 将携带有Runnable和Trigger的ScheduledFuture类对象作为 Map 的 value 进行装配 ScheduledFuture<?> scheduledFuture = threadPoolTaskScheduler.schedule(runnable, trigger); // 放入 Map 中作为 value scheduledFutureMap.put(id, scheduledFuture); logger.info("---添加定时任务--->" + id); } /** * 通过上述 put 方法的参数id(定时任务id)标识,将定时任务移除出 map * @param id */ public static void delete(String id) { ScheduledFuture<?> scheduledFuture = scheduledFutureMap.get(id); // 条件判断 if (scheduledFuture != null && scheduledFuture.isCancelled()) { scheduledFuture.cancel(true); } scheduledFutureMap.remove(id); logger.info("---取消定时任务--->" + id); } } 第二部分是关于结合实际业务,引入实际业务数据的代码demo。 /** * @author Created by zhuzqc on 2023/1/30 15:58 */ @Slf4j @Component @EnableScheduling public class TaskScheduleDemo{ @Resource private AAAMapper aaaMapper; @Resource private BBBService bbbService; private Logger logger = LoggerFactory.getLogger(this.getClass()); // 引入外部的业务数据对象 public void putHiredTask(CCCEntity cccEntity){ //TODO: 将业务线程和定时触发器交由线程池工具管理:创建业务线程对象,并对属性赋初始化值(有参构造) TaskSchedulerUtil.put(new TaskThreadDemo(cccEntity,aaaMapper,bbbService), new Trigger() { @Override public Date nextExecutionTime(TriggerContext triggerContext) { //获取定时触发器,这里可以获取页面的更新记录,实现定时间隔的动态调整 Date nextExecTime = TaskTransUtils.StringToDateTime(cccEntity.getSendTime()); //cron 表达式转换工具类 String cron = TaskTransUtils.getDateCronTime(nextExecTime); try { if (StringUtils.isBlank(cron)) { // 提示参数为空 logger.info("trackScheduler定时器的 cron 参数为空!"); // 如果为空则赋默认值,每天早上9:00 cron = "0 00 09 ? * *"; } logger.info("-------定时任务执行中:" + cron + "--------"); CronTrigger cronTrigger = new CronTrigger(cron); nextExecTime = cronTrigger.nextExecutionTime(triggerContext); } catch (Exception e) { e.printStackTrace(); log.info(e.getMessage()); } return nextExecTime; } },"该定时任务的id"); } } 五、文章小结 动态定时任务的总结如下: 单一使用ScheduledTaskRegistrar类,无法达到预期动态调整定时任务的效果; 实际的开发场景中,需要业务逻辑代码和触发器代码都能访问到外部业务数据; 配合ThreadPoolTaskScheduler类和该类中的schedule()方法可以达到动态调整定时任务的效果。 如果大家有遇到这样类似的问题,并且为此感到困惑时,希望以上文章的介绍可以帮助到大家。 最后,欢迎大家的指正和交流!
2024-01-19
152
0
0
网络聚合
2024-01-19
前端Linux部署命令与流程记录
以前写过一篇在Linux上从零开始部署前后端分离的Vue+Spring boot项目,但那时候是部署自己的个人项目,磕磕绊绊地把问题解决了,后来在公司有了几次应用到实际生产环境的经验,发现还有很多可以补充的地方,很多指令和下载地址每次用到的时候再找就相对麻烦,通过这篇文章可以做一个记录。 另外,之前漏掉了很重要的Linux版本,因为以前不太了解,一直使用的都是CentOS 7,这次选择系统的时候看到CentOS后续会停止维护,所以决定换一个版本学习一下,Linux版本非常多,通常我们可以选择CentOS、Debian、Ubuntu等,具体的区别可以自己去查下,这里我选择了Debian 11.1作为新的系统环境。 npm和node(通过NVM安装) 前端部署最先想到的就是NPM和Node,但是Node的版本切换可能是个问题,所以使用了NVM,这里之前也写过一篇NVM、NPM、Node.js的安装选择,不过是针对Windows系统的,实际在Linux上运行还有些许不同。 写文档时NVM的最新版本为0.39.3,使用时可以按需要更改版本。 1. 安装 1.1 在线安装 NVM的GitHub地址 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash 1.2 手动安装 也可以下载GitHub的Release版本文件上传到服务器手动安装,安装目录是/root/.nvm。 创建安装目录 mkdir /root/.nvm 解压文件到安装目录 tar -zxvf nvm-0.39.3.tar.gz --strip-components 1 -C /root/.nvm -z:有gzip属性的 -x:解压 -v:显示所有过程 -f: 使用档案名字,切记,这个参数是最后一个参数,后面只能接档案名。 –strip-component=1 代表解压出来的文件,剥离前一个路径 -C, --directory=DIR 改变至目录 DIR 2. 配置环境变量 如果选择了手动安装,需要自己配置一下环境变量才能在全局使用nvm指令。 #编辑文件 vim ~/.bashrc #按“i”进入insert模式,将下面两行代码写入文件,按“esc”退出insert模式,按“:”进入底行模式,输入“wq!”回车,即保存并退出 export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm #刷新配置 source ~/.bashrc 3. 判断是否安装成功 nvm -v 4.安装node和npm #查看线上版本的指令与Windows版不同 nvm ls-remote #选择一个LTS版本,如下图所示18.13.0,关于版本选择的问题以前也说过 nvm install 18.13.0 5.查看和切换版本 #查看npm和node版本 npm -v node -v #如果提示没有找到指令,则查看已安装的node版本 nvm ls #切换到18.13.0版本 nvm use 18.13.0 nrm nrm(npm registry manager)是npm的镜像源管理工具,直接使用连接国外资源下载有时会慢,可以切换至其他镜像源。 #全局安装 npm install -g nrm #查看可选的源 nrm ls #切换至淘宝源 nrm use taobao Nginx Debian安装 如果使用的是Debian的系统,可以通过如下代码直接安装,但实际生产环境多半要添加模块,还是需要手动下载源码编译,参考下文的步骤。 sudo apt update sudo apt install nginx 下载 Nginx下载 选择稳定版本下载,上传到服务器 或者通过远程仓库下载 wget http://nginx.org/download/nginx-1.22.1.tar.gz 安装 # 1.解压文件 tar -zxvf nginx-1.22.1.tar.gz 2.进入目录 cd nginx-1.22.1 按需编译 没有特殊需求的话,在nginx的解压目录里执行 make && make install 就可以编译安装nginx了,但是实际的线上环境还需要添加一些模块来满足线上的业务需求,我们的项目中用到了两个插件: http_realip_module(真实IP) http_ssl_module(SSL协议) 编译前要先安装依赖,虽然两个系统要安装的库不同,但是功能类似,从上到下依次是: gcc编译器:用于 make 编译 正则库:用于在配置文件内进行目录匹配 zlib库:用于对HTTP包的内容做gzip格式的压缩,Nginx编译过程和Http请求过程中需要gzip格式的压缩 OpenSSL库:提供SSL协议的编译环境 # CentOS yum install gcc yum install pcre-devel yum install zlib zlib-devel yum install openssl openssl-devel # Debian apt install -y build-essential apt install -y libpcre3 libpcre3-dev apt install -y zlib1g-dev apt install -y openssl libssl-dev 编译操作如下: #查看可用模块 ./configure --help #配置 ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_realip_module 参数说明: --prefix 用于指定nginx编译后的安装目录 --add-module 为添加的第三方模块 --with..._module 表示启用的nginx模块,如此处启用了两个模块 没有安装依赖的话这里会提示错误,正确执行的结果如下: 接下来就是编译安装: #编译 make #安装 make install #可以简写成 make && make install 创建软链接 安装完成后,通过 nginx -V 查看一下已安装的模块信息,会提示没有这样的指令,这是因为指令只能在对应目录下执行,对于需要频繁使用的工具来说并不方便,想要在全局使用,可以创建一个软链接: sudo ln -s /usr/local/nginx/sbin/nginx /usr/sbin/nginx 之后就可以在任意目录执行nginx指令,执行 nginx -V 可以看到编译安装的模块: 日志 nginx日志默认记录在安装目录的 logs 文件夹下,访问日志全部写入在 access.log 文件中,随着时间推移,日志文件体积会逐渐增加,单个文件过大会导致可读性变差。通过修改配置的方式,我们可以让nginx日志自动按日期分割。 #日志时间变量 map $time_iso8601 $logdate { '~^(?<ymd>\d{4}-\d{2}-\d{2})' $ymd; default 'date-not-found'; } log_format json_log escape=json '{"host":"$http_host $request","time":"$time_local","timestamp":"$msec","from_ip":"$remote_addr","real_ip":"$http_x_forwarded_for","user_agent":"$http_user_agent"}'; access_log logs/access-$logdate.log json_log; 加在下图位置: 在更新配置之前,还要授予文件夹权限,要按日期创建日志文件需要有在目录中写入的权限。 chmod -R 777 /usr/local/nginx/logs 在查找无权限创建文件这个问题时候,看到一些回答是修改运行nginx的用户为root,但是我觉得这样在安全上可能会有问题,虽然777也是文件夹的最高权限,任何用户都可以修改,但相对会好一些,可能还有更好的方法,欢迎评论留言。 需要的话,error.log 也可以以同样的方式按时间分割。 可以创建一个软链接方便查看日志 ln -s /usr/local/nginx/logs /root/logs 启动 无论是启动还是重启,都要先测试配置,没有问题再启动。 #测试配置 nginx -t #配置没有问题后启动 nginx #重启 nginx -s reload pm2 #安装 npm install pm2@latest -g #查看 pm2 -h 如果提示命令未找到,也是与之前一样在bin目录下建立软链接
2024-01-19
138
0
0
网络聚合
2024-01-19
2023年计划,步入工作的第6年
好几年没写计划了,有些浑浑噩噩,过上了一段躺平的生活。 因为上篇博客收到了很多评论,有人评论:想让我写一下躺平后的生活,也有人评论:为啥分手的?(PS上篇博客《工作5年的老程序员的年终总结》地址:https://www.cnblogs.com/liudw-0215/p/16997452.html),所以我准备写了这篇博客。 疫情三年,终于放开了,感觉这三年一直被压抑着,放开之后心情大好,我刚“阳康”,大家也都好了吧?祝大家身体健康、工作顺利! 回复上篇博客的评论 第一个评论:想让我写一下躺平后的生活。也不是完全躺平,但经历上段工作的严重加班,对工作已经提不起兴趣,就是不想工作!但做了很多其他的事,提几个来说说。 读书 内向只是我的一个性格特点,更特别的是我能坐在那一天一句话不说都可以(PS我旁边的同事可以作证,哈哈哈)。 我也是最近两年开始喜欢上读书的,之前完全不知道读书的好处,可能因为中国的应试教育,给中国孩纸太大压力了,所以上大学之后,都逃避学习和读书。 怎么喜欢上读书的 要追溯到2020年11月,那时股市经历19/20年的牛市,市场异常火爆,大量散户跑步进场,我也是作为一颗新鲜的韭菜在2020年11月进场的,正是牛市尾巴,涨了最后3个月,到2021年2月达到最高点,然后一路下跌,今年2022年更不用说了,超级大熊市,作为一颗韭菜当然逃不了被割的命运,到现在还亏1万。 从来不歌颂苦难,但苦难、挫折是最能让人成长的,当暴跌之后,我难受极了,没有刚开始赚点小钱的那种盲目自信了(PS那时心里想着哥就是中国巴菲特),玩基金真的让我体会到很多人性,比如巴特特很经典的一句话:别人贪婪时我恐惧,别人恐惧时我贪婪! 亏钱就要想办法,然后开始去看各种消息、各种大V,发现一点用都没有,然后开始看书吧,看能不能有用,然后慢慢的看了很多书,最后发现也没啥用。但养成我读书的习惯和爱好,之后就开始看很多其他类型的书,心理学、哲学等。 考公 有段时间在准备考公,之前我对体制内很多抵触,想着就是一帮混吃等死之辈,但一句话打破我这种想法:你讨厌里面的人,因为你没在其中! 第二个评论:为啥分手的? 我们是异地恋,因为疫情三年,我见面机会太少了,感情淡了 2023年计划 提高技术 其实还没具体想好提高哪些方面,我是linux C/C++服务器开发,我第一份工作是纯C语言的,没有用到C++,C++都是后来自学的,学的不够深入,很多语言特性没有掌握,而且更新换代太快了,工作中主要还是用c++11,但现在都出C++20了! 所以,第一个计划,就是深入学习一下C++ 第二计划,多学一些python,脚本语言对于做服务端的来说,也算是必需掌握的,什么shell、python,但python只会写一些简单的脚本,学了一点爬虫,并没有更加深入的学习。 第三个计划,学习协程,多掌握一些架构思想。 培养业余技能 1、学习手机摄影,有人可能会想这有啥可以学的?拍出照片和拍出好看的照片是有很大的区别的。 2、学习视频剪辑 3、学习文案,提高自己的文笔,多写一些东西 旅游计划 1、过完年之后,准备带父母来北京溜达一圈,他们还没有来过北京,再让他们坐一回飞机。 2、去北欧或其他国外城市,我还没坐过飞机,推荐一本书《这么慢,那么美》,是讲北欧的,看完之后我就决定要去北欧了,我觉得北欧更像“理想国” 3、国内暂时没有想去的地方,主要人多的地方我不想去,去国内旅游,去哪都排队,就超级烦,说几个还有点兴趣的地方吧,云南、三亚、西藏 希望以上计划能实现吧,2023年底,我再来复盘!
2024-01-19
121
0
0
网络聚合
2024-01-19
[数据结构] 二分查找 (四种写法)
二分查找 二分查找 二分查找(Binary Search)也叫作折半查找,前提是查找的顺序结构是有序的,我们一般在数组上进行二分查找。 二分查找就好像猜数字大小游戏一样。假设要数字目标值属于 [1, 1000] 范围内,当我们猜的数字小于这个目标值时("Too low"),我们需要往大去猜;反之大于这个目标值时("Too high"),我们需要往小去猜。当然这里猜的方式并不是盲目的,我们每次都取中间值去猜,每猜一次可以缩小当前一半的范围,这样可以大大提高效率。二分查找本质上也是这样的过程,时间复杂度为 O(logn) ,在较大数据情况下相比线性查找要快非常多。 我们定义一个左指针 left 标记当前查找区间的左边界,一个右指针 right 标记当前查找范围的右边界。每次取 mid 来判断当前取的值是否等于目标值 target。如果等于 target 就直接返回 mid ;如果小于目标值 target ,那么将左边界变为 mid + 1,缩小区间范围继续在 [mid + 1, right] 范围内进行二分查找,如果大于目标值 target ,那么将右边界变为 mid - 1,缩小区间范围继续在 [left, mid - 1] 范围内进行二分查找。 假如最后出现了 left > right 的情况,说明区间范围大小缩小到 0 都无法找到该目标值,那么很明显数组中不存在这个目标值 target,此时退出循环,返回 -1 。 二分查找图解 (1) (2) (3) 二分查找代码 //二分查找 int BinarySearch(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left <= right){ int mid = (left + right) >> 1; if(v[mid] == target) return mid; if(v[mid] < target) left = mid + 1; else right = mid - 1; } return -1; } 二分查找 递归 上面二分查找也可以写成递归的形式。大致步骤为: (1)当前层 mid 位置元素等于目标值 target,直接 return mid; (2)如果小于目标值,递归搜索 [mid + 1, right] 范围; (3)如果大于目标值,递归搜索 [left, mid - 1] 范围。 //二分查找法递归写法 int BinarySearchRec(vector<int> &a, int left, int right, int target) { int mid = (left + right) >> 1; if(left <= right){ if(a[mid] == target) return mid; if(a[mid] < target) return BinarySearchRec(a, mid + 1, right, target); else return BinarySearchRec(a, left, mid - 1, target); } return -1; } 二分查找 元素起始位置 查找等于目标值的第一个位置 数组中可能存在连续的多个位置元素都等于目标值 target ,当我们要查找第一个出现的位置,我们需要保证查找到位置的左边所有元素都满足小于 target,右边所有元素都满足大于等于 target。 当出现 a[mid] < target 时,说明我们要查找的位置一定在 [mid + 1, right] 范围内,当然也可以写成 (mid, right] ;当出现 a[mid] >= target 时,说明要查找的位置有可能是当前的 mid,也有可能是当前 mid 左边的某个位置,所以此时要查找的位置一定在 [left, mid] 范围内。 因此,当 a[mid] < target,将 left = mid + 1 ;当 a[mid] >= target,将 right = mid。 当最后 left == right 即两个指针相遇时退出循环,最后要判断一下相遇位置处的元素是否等于目标值 target。如果等于目标值,就返回 left 或者 right,如果不等于目标值,说明不存在该元素,那么就返回 -1 。 查找等于目标值起始位置图解 (1) (2) (3) (4) 查找等于目标值起始位置代码 int BinarySearchfirst(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right) >> 1; if(v[mid] < target) left = mid + 1; else right = mid; } return v[left] == target ? left : -1; } 二分查找 元素终止位置 查找等于目标值的最后一个位置 当我们要查找最后一个出现的位置,我们需要保证查找到位置的左边所有元素都满足小于等于 target,右边所有元素都满足大于 target。 当出现 a[mid] > target 时,说明我们要查找的位置一定在 [left, mid - 1] 范围内,当然也可以写成 [left, mid) ;当出现 a[mid] <= target 时,说明要查找的位置有可能是当前的 mid,也有可能是当前 mid 右边的某个位置,所以此时要查找的位置一定在 [mid, right] 范围内。 因此,当 a[mid] > target,将 right = mid - 1 ;当 a[mid] <= target,将 left = mid。 当最后 left == right 即两个指针相遇时退出循环,最后要判断一下相遇位置处的元素是否等于目标值 target。如果等于目标值,就返回 left 或者 right,如果不等于目标值,说明不存在该元素,那么就返回 -1 。 但是要注意的是,在这里取 mid 时,我们不能和之前一样取 (left + right) >> 1,而要采取 (left + right + 1) >> 1 的形式。我们可以假设只有数组两个元素,两个元素都等于目标值,显然此时我们要找的最后一个位置为下标1。我们模拟一下,初始情况下 left 为 0,right 为 1,如果采用 (left + right) >> 1,那么此时的 mid 就等于 0,这个时候出现了 left 依旧等于之前 left 的情况,那么显然这个时候区间无法进行缩小,left 会一直等于 0,这个时候就陷入死循环了。 我们仔细看一下,当前这种情况的特点是 left + 1 == right,那么我们取 mid 时: mid = (left + right) >> 1 = (2 * left + 1) >> 1 = left,很明显left 会一直等于 mid。 如果我们能够让 left 在这种 left + 1 == right 情况下使得 left 取到 right 即往后一位,那么我们的区间范围就得以缩小,也不会陷入死循环。所以我们采用 (left + right + 1) >> 1 取 mid,问题就得以解决了。此时: mid = (left + right + 1) >> 1 = (2 * left + 2) >> 1 = left + 1 = right,可以看出 left 往后了一位。 归根究底还是因为整形数据除 2 会自动进行向下取整的问题,进行 +1 操作后向上取整就可以解决这个问题。 查找等于目标值终止位置图解(取mid向上取整) (1) (2) (3) (4) 查找等于目标值终止位置图解(取mid向下取整)* (1) (2) (3) (4) 查找等于目标值终止位置代码 int BinarySearchlast(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right + 1) >> 1; if(v[mid] > target) right = mid - 1; else left = mid; } return v[left] == target ? left : -1; } 相关试题及完整程序 相关试题 Acwing 789.数的范围 Leetcode 34.在排序数组中查找元素的第一个和最后一个位置 完整程序 #include<iostream> #include<vector> using namespace std; //二分查找 int BinarySearch(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left <= right){ int mid = (left + right) >> 1; if(v[mid] == target) return mid; if(v[mid] < target) left = mid + 1; else right = mid - 1; } return -1; } //二分查找法递归写法 int BinarySearchRec(vector<int> &a, int left, int right, int target) { int mid = (left + right) >> 1; if(left <= right){ if(a[mid] == target) return mid; if(a[mid] < target) return BinarySearchRec(a, mid + 1, right, target); else return BinarySearchRec(a, left, mid - 1, target); } return -1; } //查找元素起始位置 int BinarySearchfirst(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right) >> 1; if(v[mid] < target) left = mid + 1; else right = mid; } return v[left] == target ? left : -1; } //查找元素终止位置 int BinarySearchlast(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right + 1) >> 1; if(v[mid] > target) right = mid - 1; else left = mid; } return v[left] == target ? left : -1; } int main(){ vector<int> v = {7,7,7,7}; cout<<BinarySearchFirst(v, 7)<<endl; cout<<BinarySearchLast(v, 7)<<endl; cout<<BinarySearchRec(v, 0, v.size() - 1, 7)<<endl; cout<<BinarySearch(v, 7)<<endl; }
2024-01-19
250
0
0
网络聚合
2024-01-19
Python绘制神经网络模型图
本文介绍基于Python语言,对神经网络模型的结构进行可视化绘图的方法。 最近需要进行神经网络结构模型的可视化绘图工作。查阅多种方法后,看到很多方法都比较麻烦,例如单纯利用graphviz模块,就需要手动用DOT语言进行图片描述,比较花时间;最终,发现利用第三方的ann_visualizer模块,可以实现对已有神经网络的直接可视化,过程较为方便,本文对此加以详细介绍。 此外,如果需要在MATLAB中实现神经网络构建与简单的可视化,大家可以查看MATLAB人工神经网络ANN代码;如果要借助软件或在线工具进行不需要代码的神经网络可视化,可以查看我们后期的博客。 相关环境的版本信息:Anaconda Navigator:1.10.0;Python:3.8.5。 首先,下载与安装必要的模块ann_visualizer。打开Anaconda Prompt (Soft)。 在弹出的界面中输入: pip install ann_visualizer 即可完成ann_visualizer模块的安装。 接下来,我们就可以借助以下仅仅一句代码对神经网络模型进行可视化了。 ann_viz(DNNModel,view=True,filename='G:/CropYield/02_CodeAndMap/01_SavedPicture/MyANN.gv',title='ANN') 其中,DNNModel就是我们已经建立好的神经网络模型,任意神经网络模型均可——可以是一个简单的浅层人工神经网络,也可以是一个相对复杂的全连接深度神经网络;view表示是否在代码执行后直接显示绘图结果;filename是绘图结果的保存位置,需要以.gv结尾;title就是神经网络图片的名称。 在这里,我就直接以Python TensorFlow深度神经网络回归:keras.Sequential中介绍并建立的深度神经网络加以可视化。 第一次运行代码时发现,出现以下报错: 报错提示我没有安装graphviz模块,但其实之前在进行随机森林决策树的可视化(也就是Python实现随机森林RF并对比自变量的重要性)时,早已经将这一模块安装过了,并且当时用到graphviz这一模块的代码也没有报错。通过查阅,发现这里需要重新安装一下python-graphviz这个新的模块。因此我们打开Anaconda Prompt (Soft),输入代码: conda install python-graphviz 如下图所示: 安装之后这里就不报错啦~ 结果紧接着又报出了新的错误,说我的keras模块没有安装: 这就不对了,明明在进行深度神经网络构建时都没有出现问题,甚至在这一句报错的下方连深度神经网络的误差绘制曲线都能显示(误差曲线的精度的确很差,大家不用在意~因为这里我们仅仅是做一个示范,所以Epoch次数就调得很小),说明keras模块应该是没问题的。 随后考虑到,这里报错的keras是在ann_visualizer的文件环境下,可能是环境不同导致的。打开Anaconda Navigator,在base (root)环境下确实找不到keras: 那么我这里就图方便,直接在base (root)环境下再安装一个keras。安装方法同上,输入代码即可: pip install keras 然后这里就不报错啦~ 接下来,经过多次尝试发现,这一方法进行神经网络可视化时,一是不能存在正则化层与BatchNormalization层;二是LeakyReLU层与Dropout层的总数量不能过多,否则绘图结果会出现问题——这就显得这一可视化方法稍微有点鸡肋了,但是其对于基本的神经网络绘图而言其实也已经很不错了。因此,我就将Python TensorFlow深度神经网络回归:keras.Sequential中的神经网络上述对应的层删除或注释掉。 如下图,首先,将当初我的代码对应的LeakyReLU层与Dropout层注释掉: 然后执行代码,即可进行神经网络的可视化。且绘制出的图将会自动打开在PDF阅读软件中,如下图(版面有限,这里就只是绘图结果的一部分)。 还是很不错的~我们还可以直接将其转换为图片格式,看起来就更直观了: 如果再取消Dropout层的注释,即绘图时加上Dropout层,也还是很不错的: 如果我们再加上LeakyReLU层,就成了这个乱七八糟、不太正确的样子(原图实在太大了,就只给大家截取图片的一部分): 可以看到,这样的话就有些问题了。 最后,我们看一下这个ann_visualizer第三方库的源代码,可以看到该库支持绘图的不同种类神经网络层;如果大家的神经网络包含这些层,就可以用ann_visualizer这一第三方库进行绘图。 至此,大功告成。
2024-01-19
179
0
0
网络聚合
2024-01-19
新项目决定用 JDK 17了
大家好,我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态。 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。 最近在调研 JDK 17,并且试着将之前的一个小项目升级了一下,在测试环境跑了一段时间。最终,决定了,新项目要采用 JDK 17 了。 JDK 1.8:“不是说好了,他发任他发,你用 Java 8 吗?” 不光是我呀,连 Spring Boot 都开始要拥护 JDK 17了,下面这一段是 Spring Boot 3.0 的更新日志。 Spring Boot 3.0 requires Java 17 as a minimum version. If you are currently using Java 8 or Java 11, you'll need to upgrade your JDK before you can develop Spring Boot 3.0 applications. Spring Boot 3.0 需要 JDK 的最低版本就是 JDK 17,如果你想用 Spring Boot 开发应用,你需要将正在使用的 Java 8 或 Java 11升级到 Java 17。 选用 Java 17,概括起来主要有下面几个主要原因: 1、JDK 17 是 LTS (长期支持版),可以免费商用到 2029 年。而且将前面几个过渡版(JDK 9-JDK 16)去其糟粕,取其精华的版本; 2、JDK 17 性能提升不少,比如重写了底层 NIO,至少提升 10% 起步; 3、大多数第三方框架和库都已经支持,不会有什么大坑; 4、准备好了,来吧。 拿几个比较好玩儿的特性来说一下 JDK 17 对比 JDK 8 的改进。 密封类 密封类应用在接口或类上,对接口或类进行继承或实现的约束,约束哪些类型可以继承、实现。例如我们的项目中有个基础服务包,里面有一个父类,但是介于安全性考虑,值允许项目中的某些微服务模块继承使用,就可以用密封类了。 没有密封类之前呢,可以用 final关键字约束,但是这样一来,被修饰的类就变成完全封闭的状态了,所有类都没办法继承。 密封类用关键字 sealed修饰,并且在声明末尾用 permits表示要开放给哪些类型。 下面声明了一个叫做 SealedPlayer的密封类,然后用关键字 permits将集成权限开放给了 MarryPlayer类。 public sealed class SealedPlayer permits MarryPlayer { public void play() { System.out.println("玩儿吧"); } } 之后 MarryPlayer 就可以继承 SealedPlayer了。 public non-sealed class MarryPlayer extends SealedPlayer{ @Override public void play() { System.out.println("不想玩儿了"); } } 继承类也要加上密封限制。比如这个例子中是用的 non-sealed,表示不限制,任何类都可以继承,还可以是 sealed,或者 final。 如果不是 permits 允许的类型,则没办法继承,比如下面这个,编译不过去,会给出提示 "java: 类不得扩展密封类:org.jdk17.SealedPlayer(因为它未列在其 'permits' 子句中)" public non-sealed class TomPlayer extends SealedPlayer { @Override public void play() { } } 空指针异常 String s = null; String s1 = s.toLowerCase(); JDK1.8 的版本下运行: Exception in thread "main" java.lang.NullPointerException at org.jdk8.App.main(App.java:10) JDK17的版本(确切的说是14及以上版本) Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "s" is null at org.jdk17.App.main(App.java:14) 出现异常的具体方法和原因都一目了然。如果你的一行代码中有多个方法、多个变量,可以快速定位问题所在,如果是 JDK1.8,有些情况下真的不太容易看出来。 yield关键字 public static int calc(int a,String operation){ var result = switch (operation) { case "+" -> { yield a + a; } case "*" -> { yield a * a; } default -> a; }; return result; } 换行文本块 如果你用过 Python,一定知道Python 可以用 'hello world'、"hello world"、''' hello world '''、""" hello world """ 四种方式表示一个字符串,其中后两种是可以直接支持换行的。 在 JDK 1.8 中,如果想声明一个字符串,如果字符串是带有格式的,比如回车、单引号、双引号,就只能用转义符号,例如下面这样的 JSON 字符串。 String json = "{\n" + " \"name\": \"古时的风筝\",\n" + " \"age\": 18\n" + "}"; 从 JDK 13开始,也像 Python 那样,支持三引号字符串了,所以再有上面的 JSON 字符串的时候,就可以直接这样声明了。 String json = """ { "name": "古时的风筝", "age": 18 } """; record记录类 类似于 Lombok 。 传统的Java应用程序通过创建一个类,通过该类的构造方法实例化类,并通过getter和setter方法访问成员变量或者设置成员变量的值。有了record关键字,你的代码会变得更加简洁。 之前声明一个实体类。 public class User { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } 使用 Record类之后,就像下面这样。 public record User(String name) { } 调用的时候像下面这样 RecordUser recordUser = new RecordUser("古时的风筝"); System.out.println(recordUser.name()); System.out.println(recordUser.toString()); 输出结果 Record 类更像是一个实体类,直接将构造方法加在类上,并且自动给字段加上了 getter 和 setter。如果一直在用 Lombok 或者觉得还是显式的写上 getter 和 setter 更清晰的话,完全可以不用它。 G1 垃圾收集器 JDK8可以启用G1作为垃圾收集器,JDK9到 JDK 17,G1 垃圾收集器是默认的垃圾收集器,G1是兼顾老年代和年轻代的收集器,并且其内存模型和其他垃圾收集器是不一样的。 G1垃圾收集器在大多数场景下,其性能都好于之前的垃圾收集器,比如CMS。 ZGC 从 JDk 15 开始正式启用 ZGC,并且在 JDK 16后对 ZGC 进行了增强,控制 stop the world 时间不超过10毫秒。但是默认的垃圾收集器仍然是 G1。 配置下面的参数来启用 ZGC 。 -XX:+UseZGC 可以用下面的方法查看当前所用的垃圾收集器 JDK 1.8 的方法 jmap -heap 8877 JDK 1.8以上的版本 jhsdb jmap --heap --pid 8877 例如下面的程序采用 ZGC 垃圾收集器。 其他一些小功能 1、支持 List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法实例化对象; 2、Stream API 有一些改进,比如 .collect(Collectors.toList())可以直接写成 .toList()了,还增加了 Collectors.teeing(),这个挺好玩,有兴趣可以看一下; 3、HttpClient重写了,支持 HTTP2.0,不用再因为嫌弃 HttpClient 而使用第三方网络框架了,比如OKHTTP; 升级 JDK 和 IDEA 安装 JDK 17,这个其实不用说,只是推荐一个网站,这个网站可以下载各种系统、各种版本的 JDK 。地址是 https://adoptium.net/。 还有,如果你想在 IDEA 上使用 JDK 17,可能要升级一下了,只有在 2021.02版本之后才支持 JDK 17。 如果觉得还不错的话,给个推荐吧! 公众号「古时的风筝」,Java 开发者,专注 Java 及周边生态。坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!
2024-01-19
174
0
0
网络聚合
2024-01-19
Rust中的智能指针:Box
Rc
Arc
Cell
RefCell
Weak
Rust中的智能指针是什么 智能指针(smart pointers)是一类数据结构,是拥有数据所有权和额外功能的指针。是指针的进一步发展 指针(pointer)是一个包含内存地址的变量的通用概念。这个地址引用,或 ” 指向”(points at)一些其 他数据 。引用以 & 符号为标志并借用了他们所 指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以在Rust中应用得最多。 智能指针是Rust中一种特殊的数据结构。它与普通指针的本质区别在于普通指针是对值的借用,而智能指针通常拥有对数据的所有权。并且可以实现很多额外的功能。 Rust智能指针有什么用,解决了什么问题 它提供了许多强大的抽象来帮助程序员管理内存和并发。其中一些抽象包括智能指针和内部可变性类型,它们可以帮助你更安全、更高效地管理内存,例如Box<T> 用于在堆上分配值。Rc<T> 是一种引用计数类型,可以实现数据的多重所有权。RefCell<T> 提供内部可变性,可用于实现对同一数据的多个可变引用 它们在标准库中定义,可以用来更灵活地管理内存,智能指针的一个特点就是实现了Drop和Deref这两个trait。其中Drop trait中提供了drop方法,在析构时会去调用。Deref trait提供了自动解引用的能力,让我们在使用智能指针的时候不需要再手动解引用了 Rust有哪些常用智能指针 Box<T>是最简单的智能指针,它允许你在堆上分配值并在离开作用域时自动释放内存。 Rc<T>和Arc<T>是引用计数类型,它们允许多个指针指向同一个值。当最后一个指针离开作用域时,值将被释放。Rc<T>不是线程安全的,而Arc<T>是线程安全的。 内部可变性类型允许你在不可变引用的情况下修改内部值。Rust中有几种内部可变性类型,包括Cell<T>,RefCell<T>和UnsafeCell<T>。 Cell<T>是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。Cell<T>只能用于Copy类型,因为它通过复制值来实现内部可变性。 RefCell<T>也是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>不同,RefCell<T>可以用于非Copy类型。它通过借用检查来确保运行时的安全性。 UnsafeCell<T>是一个底层的内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>和RefCell<T>不同,UnsafeCell<T>不提供任何运行时检查来确保安全性。因此,使用UnsafeCell<T>可能会导致未定义行为。 此外,Rust还提供了一种弱引用类型Weak<T>,它可以与Rc<T>或Arc<T>一起使用来创建循环引用。Weak<T>不会增加引用计数,因此它不会阻止值被释放。 Box<T> Box<T>是最简单的智能指针,它允许你在堆上分配值并在离开作用域时自动释放内存。 Box<T>通常用于以下情况: 当你有一个类型,但不确定它的大小时,可以使用Box<T>来在堆上分配内存。例如,递归类型通常需要使用Box<T>来分配内存。 当你有一个大型数据结构并希望在栈上分配内存时,可以使用Box<T>来在堆上分配内存。这样可以避免栈溢出的问题。 当你希望拥有一个值并只关心它的类型而不是所占用的内存时,可以使用Box<T>。例如,当你需要将一个闭包传递给函数时,可以使用Box<T>来存储闭包。 总之,当你需要在堆上分配内存并管理其生命周期时,可以考虑使用Box<T>。 下面是一个简单的例子: fn main() { let b = Box::new(5); println!("b = {}", b); } 复制代码 这里定义了变量 b,其值是一个指向被分配在堆上的值 5 的 Box。这个程序会打印出 b = 5;在这个例子 中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上) 和它所指向的数据(位于堆上)。 但是Box<T>无法同时在多个地方对同一个值进行引用 enum List { Cons(i32, Box), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); } 复制代码 编译会得出如下错误: error[E0382]: use of moved value: a,因为b和c无法同时拥有a的所有权,这个时候我们要用Rc<T> Rc<T> Reference Counted 引用计数 Rc<T>是一个引用计数类型,它允许多个指针指向同一个值。当最后一个指针离开作用域时,值将被释放。Rc<T>不是线程安全的,因此不能在多线程环境中使用。 Rc<T>通常用于以下情况: 当你希望在多个地方共享数据时,可以使用Rc<T>。解决了使用Box<T>共享数据时出现编译错误 当你希望创建一个循环引用时,可以使用Rc<T>和Weak<T>来实现。 下面是一个简单的例子,演示如何使用Rc<T>来共享数据: use std::rc::Rc; fn main() { let data = Rc::new(vec![<span class="hljs-number">1, <span class="hljs-number">2, <span class="hljs-number">3]); let data1 = data.clone(); let data2 = data.clone(); println!("data: {:?}", data); println!("data1: {:?}", data1); println!("data2: {:?}", data2); } 复制代码 这个例子中,我们使用Rc::new来创建一个新的Rc<T>实例。然后,我们使用clone方法来创建两个新的指针,它们都指向同一个值。由于Rc<T>实现了引用计数,所以当最后一个指针离开作用域时,值将被释放。 但是Rc<T>在多线程中容易引发线程安全问题,为了解决这个问题,又有了Arc<T> Arc<T> Atomically Reference Counted 原子引用计数 Arc<T>是一个线程安全的引用计数类型,它允许多个指针在多个线程之间共享同一个值。当最后一个指针离开作用域时,值将被释放。 Arc<T>通常用于以下情况: 当你希望在多个线程之间共享数据时,可以使用Arc<T>,是Rc<T>的多线程版本。 当你希望在线程之间传递数据时,可以使用Arc<T>来实现。 下面是一个简单的例子,演示如何使用Arc<T>来在线程之间共享数据: use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![<span class="hljs-number">1, <span class="hljs-number">2, <span class="hljs-number">3]); let data1 = data.clone(); let data2 = data.clone(); let handle1 = thread::spawn(move || { println!("data1: {:?}", data1); }); let handle2 = thread::spawn(move || { println!("data2: {:?}", data2); }); handle1.join().unwrap(); handle2.join().unwrap(); } 复制代码 这个例子中,我们使用Arc::new来创建一个新的Arc<T>实例。然后,我们使用clone方法来创建两个新的指针,它们都指向同一个值。接着,我们在线程中使用这些指针来访问共享数据。由于Arc<T>实现了线程安全的引用计数,所以当最后一个指针离开作用域时,值将被释放。 Weak<T> 弱引用类型 Weak<T>是一个弱引用类型,它可以与Rc<T>或Arc<T>一起使用来创建循环引用。Weak<T>不会增加引用计数,因此它不会阻止值被释放。 当你希望创建一个循环引用时,可以使用Rc<T>或Arc<T>和Weak<T>来实现。 Weak<T>通常用于以下情况: 当你希望观察一个值而不拥有它时,可以使用Weak<T>来实现。由于Weak<T>不会增加引用计数,所以它不会影响值的生命周期。 下面是一个简单的例子,演示如何使用Rc<T>和Weak<T>来创建一个循环引用: use std::rc::{Rc, Weak}; struct Node { value: i32, next: Option<Rc<Node>>, prev: Option<Weak<Node>>, } fn main() { let first = Rc::new(Node { value: 1, next: None, prev: None }); let second = Rc::new(Node { value: 2, next: None, prev: Some(Rc::downgrade(&first)) }); first.next = Some(second.clone()); } 复制代码 这个例子中,我们定义了一个Node结构体,它包含一个值、一个指向下一个节点的指针和一个指向前一个节点的弱引用。然后,我们创建了两个节点first和second,并使用Rc::downgrade方法来创建一个弱引用。最后,我们将两个节点连接起来,形成一个循环引用。 需要注意的是,由于Weak<T>不会增加引用计数,所以它不会阻止值被释放。当你需要访问弱引用指向的值时,可以使用upgrade方法来获取一个临时的强引用。如果值已经被释放,则upgrade方法会返回None。 UnsafeCell<T> UnsafeCell<T>是一个底层的内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>和RefCell<T>不同,UnsafeCell<T>不提供任何运行时检查来确保安全性。因此,使用UnsafeCell<T>可能会导致未定义行为。 由于UnsafeCell<T>是一个底层类型,它通常不直接用于应用程序代码。相反,它被用作其他内部可变性类型(如Cell<T>和RefCell<T>)的基础。 下面是一个简单的例子,演示如何使用UnsafeCell<T>来修改内部值: use std::cell::UnsafeCell; fn main() { let x = UnsafeCell::new(1); let y = &x; let z = &x; unsafe { *x.get() = 2; *y.get() = 3; *z.get() = 4; } println!("x: {}", unsafe { *x.get() }); } 复制代码 这个例子中,我们使用UnsafeCell::new来创建一个新的UnsafeCell<T>实例。然后,我们创建了两个不可变引用y和z,它们都指向同一个值。接着,在一个unsafe块中,我们使用get方法来获取一个裸指针,并使用它来修改内部值。由于UnsafeCell<T>实现了内部可变性,所以我们可以在不可变引用的情况下修改内部值。 需要注意的是,由于UnsafeCell<T>不提供任何运行时检查来确保安全性,所以使用它可能会导致未定义行为。因此,在大多数情况下,你应该使用其他内部可变性类型(如Cell<T>和RefCell<T>),而不是直接使用UnsafeCell<T>。 Cell<T> Cell<T>是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。Cell<T>只能用于Copy类型,因为它通过复制值来实现内部可变性。 Cell<T>通常用于以下情况: 当你需要在不可变引用的情况下修改内部值时,可以使用Cell<T>来实现内部可变性。 当你需要在结构体中包含一个可变字段时,可以使用Cell<T>来实现。 下面是一个简单的例子,演示如何使用Cell<T>来修改内部值: use std::cell::Cell; fn main() { let x = Cell::new(1); let y = &x; let z = &x; x.set(2); y.set(3); z.set(4); println!("x: {}", x.get()); } 复制代码 这个例子中,我们使用Cell::new来创建一个新的Cell<T>实例。然后,我们创建了两个不可变引用y和z,它们都指向同一个值。接着,我们使用set方法来修改内部值。由于Cell<T>实现了内部可变性,所以我们可以在不可变引用的情况下修改内部值。 需要注意的是,由于Cell<T>通过复制值来实现内部可变性,所以它只能用于Copy类型。如果你需要在不可变引用的情况下修改非Copy类型的值,可以考虑使用RefCell<T>。 RefCell<T> RefCell<T>是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>不同,RefCell<T>可以用于非Copy类型。 RefCell<T>通过借用检查来确保运行时的安全性。当你尝试获取一个可变引用时,RefCell<T>会检查是否已经有其他可变引用或不可变引用。如果有,则会发生运行时错误。 RefCell<T>通常用于以下情况: 当你需要在不可变引用的情况下修改内部值时,可以使用RefCell<T>来实现内部可变性。 当你需要在结构体中包含一个可变字段时,可以使用RefCell<T>来实现。 下面是一个简单的例子,演示如何使用RefCell<T>来修改内部值: use std::cell::RefCell; fn main() { let x = RefCell::new(vec![<span class="hljs-number">1, <span class="hljs-number">2, <span class="hljs-number">3]); let y = &x; let z = &x; x.borrow_mut().push(4); y.borrow_mut().push(5); z.borrow_mut().push(6); println!("x: {:?}", x.borrow()); } 复制代码 这个例子中,我们使用RefCell::new来创建一个新的RefCell<T>实例。然后,我们创建了两个不可变引用y和z,它们都指向同一个值。接着,我们使用borrow_mut方法来获取一个可变引用,并使用它来修改内部值。由于RefCell<T>实现了内部可变性,所以我们可以在不可变引用的情况下修改内部值。 需要注意的是,由于RefCell<T>通过借用检查来确保运行时的安全性,所以当你尝试获取一个可变引用时,如果已经有其他可变引用或不可变引用,则会发生运行时错误。from刘金,转载请注明原文链接。感谢!
2024-01-19
143
0
0
网络聚合
2024-01-19
Educational Codeforces Round 143 (Rated for Div. 2) A-E
比赛链接 A 题意 有两座塔由红蓝方块组成,分别有 \(n,m\) 个方块,一次操作可以把一座塔塔顶的方块移动到另一座塔的塔顶,问通过操作是否能使每座塔中没有颜色相同的相邻方块。 题解 知识点:贪心。 注意到,操作最多能拆掉一对相邻的方块,因此统计两座塔不合法的对数。 如果超过 \(1\) 对,那么无解。 如果只有 \(1\) 对,那么操作一定使得塔顶相对,塔顶若颜色一样就无解,否则有解。 如果没有不合法的方块,也有解。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { int n, m; cin >> n >> m; string s, t; cin >> s >> t; int mis = 0; for (int i = 1;i < n;i++) if (s[i] == s[i - 1]) { mis++; } for (int i = 1;i < m;i++) if (t[i] == t[i - 1]) { mis++; } if (mis >= 2 || mis == 1 && s.back() == t.back()) return false; else cout << "YES" << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << "NO" << '\n'; } return 0; } B 题意 坐标轴上覆盖了 \(n\) 个线段,指定一个点坐标为 \(k\) ,能否通过删除一些线段使得覆盖指定点的线段严格最多。 题解 知识点:贪心。 直接去除没有覆盖 \(k\) 的线段,此时若 \(k\) 覆盖数严格最多,那么有解;否则,去除任意一条线段,都会使 \(k\) 和一些点同时减 \(1\) ,不会使得 \(k\) 覆盖数严格最多,所以无解。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { int n, k; cin >> n >> k; vector<int> vis(57); for (int i = 1;i <= n;i++) { int l, r; cin >> l >> r; if (l <= k && k <= r) vis[l], vis[r + 1]--; } for (int i = 1;i <= 50;i) vis[i] += vis[i - 1]; bool ok = 1; for (int i = 1;i <= 50;i++) if (i != k) ok &= vis[k] > vis[i]; if (ok) cout << "YES" << '\n'; else return false; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << "NO" << '\n'; } return 0; } C 题意 有 \(n\) 杯茶,第 \(i\) 杯茶有 \(a_i\) 毫升。有 \(n\) 个人,每个人每次最多能喝 \(b_i\) 毫升。 第一轮,第 \(i\) 个人喝第 \(i\) 杯茶,第 \(i\) 杯茶减少 \(\min(a_i,b_i)(1\leq i\leq n)\) 毫升。 第二轮,第 \(i\) 个人喝第 \(i-1\) 杯茶,第 \(1\) 个人不喝,第 \(i\) 杯茶减少 \(\min(a_i,b_{i+1})(1\leq i\leq n-1)\) 毫升。 以此类推,问最后每个人喝了多少毫升茶。 题解 知识点:枚举,前缀和,差分,二分。 考虑枚举每杯茶的贡献。第 \(i\) 杯茶会被 \([i,n]\) 内的人喝,但茶会被喝完,所以设 \(sumb\) 为 \(b\) 的前缀和,找到最大的位置 \(j\) 满足 \(sumb_j - sumb_{i-1}\leq a_i \iff sum_{j} \leq sum_{i-1} + a_i\) ,即 \([i,j]\) 的人能喝到自己的上限,第 \(j+1\) 个人能喝到剩下的 \(a_i - (sum_j - sum_{i-1})\) 。 我们用差分数组 \(cnt\) 完成 \([i,j]\) 的区间加 \(1\) ,最后前缀和就能得到第 \(i\) 个人能喝到自己的上限多少次。再用数组 \(delta\) 记录第 \(i\) 个人喝了多少剩下的茶,最后 \(cnt_ib_i + delta_i\) 即第 \(i\) 个人的毫升数。 时间复杂度 \(O(n\log n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; int a[200007], b[200007]; ll sumb[200007]; int cnt[200007]; ll delta[200007]; bool solve() { int n; cin >> n; for (int i = 1;i <= n;i++) cin >> a[i]; for (int i = 1;i <= n;i++) cin >> b[i]; for (int i = 1;i <= n;i++) sumb[i] = sumb[i - 1] + b[i], delta[i] = 0, cnt[i] = 0; for (int i = 1;i <= n;i++) { int id = upper_bound(sumb + i, sumb + n + 1, a[i] + sumb[i - 1]) - sumb - 1; cnt[i]; cnt[id + 1]--; delta[id + 1] += a[i] - (sumb[id] - sumb[i - 1]); } for (int i = 1;i <= n;i) { cnt[i] += cnt[i - 1]; cout << 1LL * cnt[i] * b[i] + delta[i] << ' '; } cout << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; } D 题意 给定一个有 \(n\) 个点 \(n\) 条边的无向带权图,保证 \(n\) 为 \(6\) 的倍数,组成 \(\dfrac{n}{3}\) 个三元环: \((1,2,3),(4,5,6),\cdots\) 。 现在给每个点染上红或蓝两种颜色,要求红色有 \(\dfrac{n}{2}\) 个点、蓝色也有 \(\dfrac{n}{2}\) 个点 。 定义一种染色方案的价值为,两端颜色不同的边的权值总和。 设所有染色方案种价值最大为 \(W\) ,问有多少种染色方案的价值为 \(W\) 。 题解 知识点:贪心,排列组合。 显然,一个三元环的最多能贡献两条边,即两红一蓝或两蓝一红,刚好我们可以使得所有三元环都能贡献出两条边,我们对每个三元环贪心地选最大的两条边即可达到最大价值。因此,染色方案必然是每个三元环两红一蓝或两蓝一红,且最大的两条边端点颜色不同。 考虑三元环的分配方案,我们需要一半的两红一蓝另一半两蓝一红,因此方案数是 \(\dbinom{n/3}{n/6}\) 。再考虑每一种分配方案的不同染色方案,显然三边权值相同的三元组能贡献三种方案,而两边权值相同的三元组当且仅当三边中不同的权值是最大值时会贡献两种方案,分别记为 \(cnt_1,cnt_2\) ,则总方案数为 \(3^{cnt_1} \cdot 2^{cnt_2}\cdot \dbinom{n/3}{n/6}\) 。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; const int P = 998244353; const int N = 3e5 + 7; namespace CNM { int qpow(int a, ll k) { int ans = 1; while (k) { if (k & 1) ans = 1LL * ans * a % P; k >>= 1; a = 1LL * a * a % P; } return ans; } int fact[N], invfact[N]; void init(int n) { fact[0] = 1; for (int i = 1;i <= n;i++) fact[i] = 1LL * i * fact[i - 1] % P; invfact[n] = qpow(fact[n], P - 2); for (int i = n;i >= 1;i--) invfact[i - 1] = 1LL * invfact[i] * i % P; } int C(int n, int m) { if (n == m && m == -1) return 1; //* 隔板法特判 if (n < m || m < 0) return 0; return 1LL * fact[n] * invfact[n - m] % P * invfact[m] % P; } } int w[300007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; CNM::init(n / 3); for (int i = 1;i <= n;i++) cin >> w[i]; int cnt1 = 0, cnt2 = 0; for (int i = 3;i <= n;i += 3) { if (w[i - 2] == w[i - 1] && w[i - 1] == w[i]) cnt1++; else if (w[i] == w[i - 1] || w[i - 1] == w[i - 2] || w[i - 2] == w[i]) { int mx = max({ w[i - 2],w[i - 1],w[i] }); if (count(w + i - 2, w + i + 1, mx) == 1) cnt2++; } } cout << 1LL * CNM::qpow(3, cnt1) * CNM::qpow(2, cnt2) % P * CNM::C(n / 3, n / 6) % P << '\n'; return 0; } E 题意 有 \(n\) 个怪物,第 \(i\) 个怪物的血量有 \(h_i\) ,血量降为 \(0\) 时立刻死亡,你有两种攻击方式: 指定一个怪物,对其普通攻击,造成 \(1\) 点伤害,消耗 \(1\) 点魔法。普通攻击可以使用无限次。 指定一个怪物,对其释放爆炸魔法,造成的伤害取决于注入的魔法,如果想要消耗 \(x\) 点魔法注入其中,会造成 \(x\) 点伤害。 爆炸魔法具有连锁性,如果被魔法击中的怪物死了,假设是第 \(i\) 个怪物,那么 \(i-1,i+1\) 两个怪物会受到 \(h_i-1\) 的伤害,以此类推,直至没有怪物死掉。这个魔法只能使用一次。 问,最少需要多少魔法能消灭所有怪物。 题解 知识点:枚举,贪心,单调栈。 显然,我们可以枚举释放爆炸魔法的位置,取造成伤害最多的即可。 对于每个位置,都可以向两侧扩展,不妨先考虑左侧部分。 我们设 \(l_i\) 为在第 \(i\) 个位置释放爆炸魔法后,左侧(包括自己)能造成的最大伤害。我们考虑在 \(i\) 处依次向左扩展的两种情况: 对于 \(j<i\) ,若 \(h_j \geq h_i - (i-j)\) ,则说明 \(j\) 不能被消灭,但我们可以在之前通过普通攻击把血量降到可以消灭的血量,因此还是可以造成 \(h_i-(i-j)\) 的伤害。 对于 \(j<i\) ,一旦出现 \(h_j < h_i-(i-j)\) ,则说明 \(j\) 虽然能被消灭,但会降低之后的魔法伤害,后续计算会由 \(h_j\) 直接支配,我们通过 \(l_i\) 直接转移即可,并停止扩展。 注意到,如此操作的复杂度是平方的,我们考虑优化复杂度。我们可以将条件中的变量转换为 \(h_j - j,h_i -i\) ,就可以绑定成一个值了。我们发现,满足情况1造成的伤害都是连续减 \(1\) 的,而满足情况 \(2\) 后直接加 \(f_j\) 后停止,所以我们只要求左侧第一个满足 \(h_j - j < h_i-i\) 的位置 \(j\) ,就可以直接得到造成的伤害 \(l_j + \dfrac{(i-j)(h_i-(i-j)+1 + h_i)}{2}\) 。 显然,这个过程可以用单调递增栈实现的,其可以维护一个字典序最小的极长递增子序列(极长递增子序列指,一个不能继续递增的递增子序列),而子序列相邻两元素之间的在原数组的其他元素,一定都大于等于这两个元素,因此是不需要比较的。于是,对于一个值,我们比较维护的子序列,就可以跳过一些不需要比较的位置。整个过程,每个元素只会出入一次维护的子序列,因此复杂度是线性的。 对于右侧,我们把 \(h\) 反转,重新算一遍 \(h_i-i\) 做相同的事情得到 \(r_i\) ,再将 \(r_i\) 反转,\(h\) 复原。最后,设血量总和为 \(sum\) ,枚举每个位置 \(i\) 得到 \(sum - (l_i + r_i - h_i) + h_i\) 取最小值。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; int n; int h[300007], hi[300007]; ll l[300007], r[300007]; void calc(ll *f) { stack<int> stk; stk.push(0); for (int i = 1;i <= n;i++) { while (stk.size() > 1 && hi[stk.top()] >= hi[i]) stk.pop(); int len = min(h[i], i - stk.top()); f[i] = f[stk.top()] + 1LL * len * (h[i] + (h[i] - len + 1)) / 2; stk.push(i); } } bool solve() { cin >> n; ll sum = 0; for (int i = 1;i <= n;i++) cin >> h[i], sum += h[i]; for (int i = 1;i <= n;i++) hi[i] = h[i] - i; calc(l); reverse(h + 1, h + n + 1); for (int i = 1;i <= n;i++) hi[i] = h[i] - i; calc(r); reverse(h + 1, h + n + 1); reverse(r + 1, r + n + 1); ll ans = 1e18; for (int i = 1;i <= n;i++) ans = min(ans, sum - (l[i] + r[i] - h[i]) + h[i]); cout << ans << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
2024-01-19
201
0
0
网络聚合
2024-01-19
设计模式实践---策略+简单工厂对大量计算公式的处理
业务流程: 1.用户根据需要选择的实验方案,每个实验方案对应一种计算公式,计算公式例如下面这种 2.将带有实验数据的PDF文件上传到特定位置,对PDF文件进行解析后将数据数据保存到数据库。 3.遍历所有方案,对每种方案使用特定的公式对数据库中的数据进行 重构前实现: 遍历方案,使用IF语句对使用的公式进行判断,而后在IF块里对数据进行处理 IF(Formula=='F1'){ //F1的处理... } IF(Formula=='F2'){ //F2的处理... } IF(Formula=='F3'){ //F2的处理... } 这样实现的问题就是程序太过庞大,八十多个公式就要有八十多个判断语句,并且判断内部的处理现也是很庞大的,这就导致了这个程序可读性很差,维护以及调试都十分困难。 重构 这里考虑使用策略模式+简单工厂进行重构 策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化。 使用策略模式重构后的程序结构: 定义一个AbstractFormula抽象类,在抽象类中定义一个Calculation计算方法,这个方法返回经过计算的结果的集合,在各个实现类(即具体公式)中实现Calculation方法 定义上下文类,在上下文类中指定要使用的公式,定义Caclute方法用于返回结果。 实现代码 /// <summary> /// 简易的方案信息 /// </summary> internal class Schemeinformation { //方案ID public string SchemeID { get; set; } //方案名称 public string SchemeName { get; set; } //公式 public string Formula { get; set; } } /// <summary> /// 单个结果 /// </summary> internal class Result { public Result(Schemeinformation schemeinformation, string Result) { Schemeinformation = schemeinformation; FinalResult=Result; } public string FinalResult { get; set; } </span><span style="color: rgba(0, 0, 255, 1)">public</span> Schemeinformation Schemeinformation { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; } }</span></pre> </div> <div class="cnblogs_code code-toolbar" style="overflow-x: scroll;padding: 20px;"> <pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">抽象的公式类 </span> internal abstract class AbstractFormula { //公式计算后的结果集 public List<string>? tempResultList; public abstract List<string> Caclution(Schemeinformation schemeinformation); } //具体实现 internal class Formula1 : AbstractFormula { public override List<string> Caclution(Schemeinformation schemeinformation) { tempResultList = new List<string>(); //计算过程... 对tempResultList赋值 return tempResultList; } } internal class Formula2 : AbstractFormula { public override List<string> Caclution(Schemeinformation schemeinformation) { tempResultList = new List<string>(); //计算过程...其中需要使用到Schemeinformation中的信息 //对tempResultList赋值 return tempResultList; } } /// <summary> /// 上下文类 使用公式,并管理结果集 /// </summary> internal class Context { AbstractFormula formula; public void SetFromula(AbstractFormula formula) { this.formula = formula; } public List<string> Caclute(Schemeinformation schemeinformation) { return formula.Caclution(schemeinformation); } } /// <summary> /// 创建公式的简单工厂 /// </summary> internal class FormulaFactory { public static AbstractFormula GetFormula(string Formula) { if(Formula == "F1") { return new Formula1(); } else if(Formula == "F2") { return new Formula2(); } //以下若干..... <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">找不到这个公式,抛出异常</span> <span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ArgumentNullException(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">value</span><span style="color: rgba(128, 0, 0, 1)">"</span>, $<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">公式{Formula}不存在</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">); ; } } }</span></pre> </div> <div class="cnblogs_code code-toolbar" style="overflow-x: scroll;padding: 20px;"> <pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">实际使用 </span> static void Main(string[] args) { Context context= new Context(); //获取所有方案信息 List<Schemeinformation> schemeinformationList = new List<Schemeinformation>(); //总结果集 List<Result> results= new List<Result>(); </span><span style="color: rgba(0, 0, 255, 1)">foreach</span>(Schemeinformation schemeinformation <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> schemeinformationList) { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">使用简单工厂获得公式对象</span> context.SetFromula(FormulaFactory.GetFormula(schemeinformation.Formula)); //获取当前公式的计算结果集 List<string>tempResults=context.Caclute(schemeinformation); //遍历结果,将所有结果放入到总结果集中 foreach(string tempResult in tempResults) { results.Add(new Result(schemeinformation,tempResult)); } } //下面就可以输出总结果集了 Console.WriteLine(results.Count); }
2024-01-19
172
0
0
网络聚合
2024-01-19
超详细讲解如何搭建自己的文件服务器
Linux上安装文件服务器FTP 由于FTP、HTTP、Telnet等协议的数据都是使用明文进行传输的,因此从设计上就是不可靠的。人们为了满足以密文方式传输文件的需求,发明了vsftpd服务程序。vsftpd(very secure ftp daemon,非常安全的FTP守护进程)是一款运行在Linux操作系统上的FTP服务程序,不仅完全开源而且免费。此外,它还具有很高的安全性、传输速度,以及支持虚拟用户验证等其他FTP服务程序不具备的特点。在不影响使用的前提下,管理者可以自行决定客户端是采用匿名开放、本地用户还是虚拟用户的验证方式来登录vsftpd服务器。这样即便黑客拿到了虚拟用户的账号密码,也不见得能成功登录vsftpd服务器。 安装VSFTP 下载dnf [root@chenstudy ~]# yum install epel-release [root@chenstudy ~]# yum install dnf 下载VSFTP [root@chenstudy ~]# dnf install vsftpd 清除防火墙的iptables缓存 iptables防火墙管理工具默认禁止了FTP协议的端口号,因此在正式配置vsftpd服务程序之前,为了避免这些默认的防火墙策略“捣乱”,还需要清空iptables防火墙的默认策略,并把当前已经被清理的防火墙策略状态保存下来: [root@chenstudy ~]# iptables -F [root@chenstudy ~]# iptables-save 然后再把FTP协议添加到firewalld服务的允许列表中(前期准备工作一定要做充足): [root@chenstudy ~]# firewall-cmd --permanent --zone=public --add-service=ftp success [root@chenstudy ~]# firewall-cmd --reload success 查看vsftpd服务程序的主配置文件(/etc/vsftpd/vsftpd.conf): [root@chenstudy ~]# mv /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd.conf_bak [root@chenstudy ~]# grep -v "#" /etc/vsftpd/vsftpd.conf_bak > /etc/vsftpd/vsftpd.conf [root@chenstudy ~]# cat /etc/vsftpd/vsftpd.conf anonymous_enable=YES local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=NO listen_ipv6=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES [root@chenstudy ~]# **vsftpd服务程序常用的参数以及作用** 参数 作用 listen=[YES|NO] 是否以独立运行的方式监听服务 listen_address=IP地址 设置要监听的IP地址 listen_port=21 设置FTP服务的监听端口 download_enable=[YES|NO] 是否允许下载文件 userlist_enable=[YES|NO] userlist_deny=[YES|NO] 设置用户列表为“允许”还是“禁止”操作 max_clients=0 最大客户端连接数,0为不限制 max_per_ip=0 同一IP地址的最大连接数,0为不限制 anonymous_enable=[YES|NO] 是否允许匿名用户访问 anon_upload_enable=[YES|NO] 是否允许匿名用户上传文件 anon_umask=022 匿名用户上传文件的umask值 anon_root=/var/ftp 匿名用户的FTP根目录 anon_mkdir_write_enable=[YES|NO] 是否允许匿名用户创建目录 anon_other_write_enable=[YES|NO] 是否开放匿名用户的其他写入权限(包括重命名、删除等操作权限) anon_max_rate=0 匿名用户的最大传输速率(字节/秒),0为不限制 local_enable=[YES|NO] 是否允许本地用户登录FTP local_umask=022 本地用户上传文件的umask值 local_root=/var/ftp 本地用户的FTP根目录 chroot_local_user=[YES|NO] 是否将用户权限禁锢在FTP目录,以确保安全 local_max_rate=0 本地用户最大传输速率(字节/秒),0为不限制 下载FTP vsftpd作为更加安全的文件传输协议服务程序,允许用户以3种认证模式登录FTP服务器。 匿名开放模式:是最不安全的一种认证模式,任何人都可以无须密码验证而直接登录到FTP服务器。 本地用户模式:是通过Linux系统本地的账户密码信息进行认证的模式,相较于匿名开放模式更安全,而且配置起来也很简单。但是如果黑客破解了账户的信息,就可以畅通无阻地登录FTP服务器,从而完全控制整台服务器。 虚拟用户模式:更安全的一种认证模式,它需要为FTP服务单独建立用户数据库文件,虚拟出用来进行密码验证的账户信息,而这些账户信息在服务器系统中实际上是不存在的,仅供FTP服务程序进行认证使用。这样,即使黑客破解了账户信息也无法登录服务器,从而有效降低了破坏范围和影响。 ftp是Linux系统中以命令行界面的方式来管理FTP传输服务的客户端工具。我们首先手动安装这个ftp客户端工具: [root@chenstudy ~]# dnf install ftp 匿名访问模式 vsftpd服务程序中,匿名开放模式是最不安全的一种认证模式。任何人都可以无须密码验证而直接登录FTP服务器。这种模式一般用来访问不重要的公开文件(在生产环境中尽量不要存放重要文件)。当然,如果采用第8章中介绍的防火墙管理工具(如TCP Wrapper服务程序)将vsftpd服务程序允许访问的主机范围设置为企业内网,也可以提供基本的安全性。 vsftpd服务程序默认关闭了匿名开放模式,我们需要做的就是开放匿名用户的上传、下载文件的权限,以及让匿名用户创建、删除、更名文件的权限。需要注意的是,针对匿名用户放开这些权限会带来潜在危险,我们只是为了在Linux系统中练习配置vsftpd服务程序而放开了这些权限,不建议在生产环境中如此行事。表11-2罗列了可以向匿名用户开放的权限参数以及作用。 向匿名用户开放的权限参数以及作用 参数 作用 anonymous_enable=YES 允许匿名访问模式 anon_umask=022 匿名用户上传文件的umask值 anon_upload_enable=YES 允许匿名用户上传文件 anon_mkdir_write_enable=YES 允许匿名用户创建目录 anon_other_write_enable=YES 允许匿名用户修改目录名称或删除目录 配置vsftp配置文件: [root@chenstudy ~]# vim /etc/vsftpd/vsftpd.conf # 重启vsftp [root@chenstudy ~]# systemctl restart vsftpd # 把vsftp加入开机自启动 [root@chenstudy ~]# systemctl enable vsftpd Created symlink from /etc/systemd/system/multi-user.target.wants/vsftpd.service to /usr/lib/systemd/system/vsftpd.service. [root@chenstudy ~]# 在linux中采用匿名访问ftp [root@chenstudy ~]# ftp 192.168.200.130 Connected to 192.168.200.130 (192.168.200.130). 220 (vsFTPd 3.0.2) Name (192.168.200.130:root): anonymous 331 Please specify the password. Password: 敲回车 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp> cd pub 250 Directory successfully changed. ftp> mkdir files 550 Create directory operation failed. ftp> # 退出ftp客户端,修改所有者身份 [root@chenstudy ~]# ls -ld /var/ftp/pub drwxr-xr-x. 2 root root 6 Jun 10 2021 /var/ftp/pub [root@chenstudy ~]# chown -R ftp /var/ftp/pub [root@chenstudy ~]# ls -ld /var/ftp/pub drwxr-xr-x. 2 ftp root 6 Jun 10 2021 /var/ftp/pub [root@chenstudy ~]# 系统提示“创建目录的操作失败”(Create directory operation failed),我猜应该是SELinux服务在“捣乱” [root@chenstudy ~]# getsebool -a | grep ftp ftpd_anon_write --> off ftpd_connect_all_unreserved --> off ftpd_connect_db --> off ftpd_full_access --> off ftpd_use_cifs --> off ftpd_use_fusefs --> off ftpd_use_nfs --> off ftpd_use_passive_mode --> off httpd_can_connect_ftp --> off httpd_enable_ftp_server --> off tftp_anon_write --> off tftp_home_dir --> off [root@chenstudy ~]# setsebool -P ftpd_full_access=on SELinux域策略就可以顺利执行文件的创建、修改及删除等操作了: 本地用户模式 本地用户模式要更安全,而且配置起来也很简单 本地用户模式使用的权限参数以及作用 参数 作用 anonymous_enable=NO 禁止匿名访问模式 local_enable=YES 允许本地用户模式 write_enable=YES 设置可写权限 local_umask=022 本地用户模式创建文件的umask值 userlist_deny=YES 启用“禁止用户名单”,名单文件为ftpusers和user_list userlist_enable=YES 开启用户作用名单文件功能 修改vsftp的配置文件: [root@chenstudy ~]# vim /etc/vsftpd/vsftpd.conf anonymous_enable=NO local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=NO listen_ipv6=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES 在我们输入root管理员的密码之前,就已经被系统拒绝访问了。这是因为vsftpd服务程序所在的目录中默认存放着两个名为“用户名单”的文件(ftpusers和user_list):vsftpd服务程序目录中的这两个文件也有类似的功能—只要里面写有某位用户的名字,就不再允许这位用户登录到FTP服务器上。 [root@chenstudy ~]# cat /etc/vsftpd/user_list # vsftpd userlist # If userlist_deny=NO, only allow users in this file # If userlist_deny=YES (default), never allow users in this file, and # do not even prompt for a password. # Note that the default vsftpd pam config also checks /etc/vsftpd/ftpusers # for users that are denied. root bin daemon adm lp sync shutdown halt mail news uucp operator games nobody [root@chenstudy ~]# cat /etc/vsftpd/ftpusers # Users that are not allowed to login via ftp root bin daemon adm lp sync shutdown halt mail news uucp operator games nobody [root@chenstudy ~]# 我们可以使用普通用户登录vsftp服务器: [root@chenstudy ~]# ftp 192.168.200.130 Connected to 192.168.200.130 (192.168.200.130). 220 (vsFTPd 3.0.2) Name (192.168.200.130:root): chen 331 Please specify the password. Password: 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp>
2024-01-19
306
0
0
网络聚合
2024-01-19
手把手教你玩转 Excel 数据透视表
1. 什么是数据透视表 数据透视表是一种可以快速汇总、分析大量数据表格的交互式分析工具。使用数据透视表可以按照数据表格的不同字段从多个角度进行透视,并建立交叉表格,用以查看数据表格不同层面的汇总信息、分析结果以及摘要数据。 使用数据透视表可以深入分析数值数据,以帮助用户发现关键数据,并做出有关企业中关键数据决策。 2. 为什么使用透视表 简单、高效、灵活、出错率低。 3. 什么时候用数据透视表 找出同类数据在不同时期的某种特定关系 以简洁友好的方式,查看大量的表格数据 对数值数据快速分类汇总,按分类和子类查看数据信息 建立交叉表格,将行移动到列或将列移动到行,以查看数据源的不同汇总 快速的计算数值数据的汇总信息、差异、个体占总体的百分比信息等 数据源经常变化 4. 数据透视表的使用方式 4-1 数据源 用于生成透视表的原始数据成为数据源。 数据源需要们组一定的规则才能成为一个合法的数据源,详细规则如下: 每列数据的第一行是该列的标题 数据源不能包含空行和空列 数据源不能包含空单元格 数据源中不能包含合并单元格 数据源中不能包含同类字段 上图中,方框圈出的区域都属于不合法的数据区域。其中,绿色区域为合并单元格,黄色为空白单元格,蓝色为同类字段,均不符合对标准数据源的要求。 4-2 透视表刷新 透视表刷新粉两种方式,手动刷新和自动刷新。 手动刷新下,也分为两种情况,分别是数据源区域未改变及数据源区域发生改变。如果只是单元格数值发生变化,可以选中透视表区域右键刷新或者在透视表分析面板中点击刷新。而如果对数据源进行了删除或新增,则需要手动的去更改数据源。 自动刷新可以在数据透视表选项面板中选择打开文件时刷新数据或使用VBA自动刷新数据表。 5. 切片器 透视表默认提供了筛选、排序等功能,但在需要多维度筛选分析数据或者多个透视表之间共享筛选条件时,默认的筛选按钮操作起来十分繁琐,并且不够直观,在这种情况下,可以使用切片器来达到数据筛选及共享条件效果。 切片器是Office 2013以上版本才有的功能,主要作用就是简化数据筛选,可应用在超级表或者透视表上。给文件中普通的区域套用表格样式之后,该区域会变成超级表,在此选择该区域,就可以插入切片器了,详细操作如下: 切片器同样也可以应用于透视表,在一个Excel文件中,基于相同的数据源,可以生成多个透视表,这些透视表之间会共享数据透视缓存。此时基于某个透视表创建的切片器,选择报表连接,即可与其它透视表共用同一个切片器,实现筛选条件的同步,详细操作如下: 6 透视表的应用场景 6-1 教学管理系统 生成课表是教学管理系统中的一个高频需求点,使用透视表可以十分快捷地生成每个班级的课程表,具体操作如下: 除了制作每个班级的课程表之外,简单的改变透视维度,又可以生成每位老师的课程表: 上边生成的透视表可以让老师们直观的看到自己每天的课程数量,也方便管理者更加直观地了解员工的工作量。 6-2 人事管理系统 人事管理系统中,高频的需求点就是对人员的分类汇总。例如,我们需要对公司所有员工按照性别进行分类,就可以基于人员信息生成透视表,具体操作如下: 该透视表,行维度为部门信息,列维度为性别,最终统计字段为员工姓名。只需简单几步,即可直观的看到人员性别的统计数据,再也不需要我们去做筛选后再统计数据了。初次之外,透视表可以应对复杂多变的统计条件,某一天,你的领导突然想知道公司人员的学历占比,此时你只需要 轻轻调节维度信息,就可以快速交工,又可以快乐的摸鱼了。基于透视表,可以生成更加直观炫酷的透视表,是时候在领导面前秀一波了。 除此之外,透视表也可用于区间数据汇总分析,例如,我们可以分年龄段统计人数。并且统计结果黏贴为普通区域,基于该区域生成一张有对比效果的图表,一起来看看吧~ 6-3 在销售中的应用 在销售管理系统中,一个高频的需求点就是根据销售订单,快速生成月报、季度报告、年报等等。使用透视表,只需要简单几步,即可完成报告的生成,再也不需要苦哈哈的手动去统计数据,之后再制作报表了。首先,我们基于销售历史数据生成一张透视表,并按照销售日期等维度制作一张基础透视表。 接下来,我们对日期创建组,实例中以月维单位,实际项目中,可以根据实际需求,按照季度等其它单位创建组。 透视表在实际业务中应用广泛,也可应用于报表的合并分析及拆分。 拓展阅读 React + Springboot + Quartz,从0实现Excel报表自动化 电子表格也能做购物车?简单三步就能实现 使用纯前端类Excel表格控件SpreadJS构建企业现金流量表
2024-01-19
152
0
0
网络聚合
2024-01-19
服务端技术方案模板参考
如何写好一个技术方案对于开发来说非常关键,这是一个工程类技术方案模板,基于我之前的一些积累,适合相对独立的业务需求承接。 实际开发中可以作为一个模板来套用,在这个基础上进行增加或者删减。 一、概要 很多时候,大家做项目的时候容易陷入为了上线而上线,尤其技术出身的同学,接到一个项目后,会陷入快速了解需求、做方案的圈子里。 通过概要分析,先了解下你要做的项目、在整个业务发展过程中所处的位置及价值,可以简单问几个问题,相信会有所帮助: 1、项目是解决哪个业务的需求?处于什么发展阶段? 2、当前业务有哪些问题/优化空间/增长点,通过这个项目能否解决? 3、参与业务上下游有哪些角色? 4、项目的成本和收益如何评价,有没有可衡量的指标体系? 5、后期业务发展的趋势如何?未来可能的增长规模多大? 基于以上的问题,为后续落地的方案可扩展性及未来业务发展做预判。 1.1 术语 统一术语的目的是让沟通在一个频道上,避免出现鸡同鸭讲,在项目后期出现返工等现象。 把术语放在最开始,以确保本文档的阅读者能够在一篇文档中就可以了解所有的关键术语。 术语将贯穿需求、分析、设计、开发、测试、维护阶段,在整个项目的生命周期中,参与者都将以统一的术语交流。 术语定义要求精确,严格,且勿模糊,含蓄; 寻找术语的方法:从需求文档中寻找那些多次出现的名词,业务名词,系统分析时引入的抽象概念。 术语范围:产品名称相关的名词,业务相关的名词,参与者相关的角色名词,边界、额度、时间相关的名词,系统抽象的名词。 如果在PRD文档中已经定义了某一个术语,建议在此处引用定义。 示例: 优惠券:商品促销手段,通过发放或者领取两种渠道,让用户在下单时抵扣一定的金额。 1.2 背景 背景是STAR法则中情境(situation)的体现,一般来自需求提出者,产研等部门。 背景通常包括项目内外部、团队内外部、组织内外部、公司内外部等。 综合不同维度上的背景,可以综合反映项目当前的价值,帮助下一步统一项目目标。 1.3 目标 基于背景分析,明确项目的目标,项目开发的意义,挖掘用户真实的可评测目标。 1.3.1 商业目标 商业目标一般会在PRD文档中明确,列出商业目标的目的是为了让我们的系统分析不要偏离业务主线。 1.3.2 系统目标 系统目标是为了支持商业目标,是商业目标在系统层面的具体化。 系统目标只是达到商业目标的充分条件,当我们的系统最终达到这几个方面,也意味着系统完全能够支持商业目标,但商业目标能否实现,与系统目标没有必然关系。 系统目标应该是具体的,可以实现的,可以达到的,相比商业目标是可落地的。 对于技术型比较强的系统,一些技术上要达到的指标也是重要的系统目标。比如一些中间件产品的技术指标,RT,SLA等。 系统目标要从当前需求出发,不局限于当前需求。 系统分析不是简单的对PRD当前需求的满足,而是站在整体产品的角度来看本次需求处于产品的那个位置,该位置在产品中的目标就是一个具体的系统目标。 二、总体设计 2.1 业务主流程 “如果你不能用一个过程来描述你正在做的事情,你就不可能真正明白你在做什么。” —— By W.爱德华兹.戴明 (PDCA循环的推广者) 系统的业务流程与具体的一个用例的流程有哪些差异? 业务流程从系统全局来看,用例关注的是自己内部的流程。 系统的业务流程是用户要完成他的业务目的,在系统层面发生着什么样的流程、事件来支撑用户的业务。 此处的流程节点同时是粗粒度的,它关注数据流、事件流在系统里的流转。 对于一些内部系统,如CRM、OA等,系统业务流程可能会成为设计阶段的BPM流程的主要来源。 对于一些小的项目,可能不存在复杂的流程,它们只是简单的请求-处理模式,此时可以不去关注系统业务流程。 对于那些改造型的项目,如果没有流程的变化,可以沿用已有的用例。 如果发生了变化,要把2个版本都画出来,且对差异之处进行重点说明。同时差异之处也是我们系统分析的重点。 2.2 系统整体架构 根据从相似系统或相似问题领域中获取的经验,定义备选架构和模型。 可以结合一些成熟的外部实例,比如电商可以参考淘宝、京东类似业务的设计。 通常需要产品团队和技术同学一起迭代来确定架构和模型,可以分为业务和技术架构。 2.3 业务系统边界 通过梳理业务系统边界,明确系统的上下游关系。 示例: 2.4 关键技术及第三方框架依赖 本系统采用了哪些关键技术,如业务算法、分布式session、分布式事务、分布式cache、分库分表等。 本系统引入了哪些外部框架、jar包。 三、模块实现 3.1 整体模块设计 模块的划分,相关描述 3.2 模块间交互接口 对接口的输入,输出,主要作用,使用场景做描述,输入输出中传递的值对象,也要做描述。不需要描述接口的实现细节。 3.3 状态机设计 对于一些复杂状态的流转,建议通过状态机的方式来描述。 3.4 XX模块功能实现 3.4.1 模块描述 输入输出,功能描述,UC图等 示例,秒杀模块设计: 3.4.2 业务流程 几个图可根据情况选择,但至少要有一个。 例如:状态图,流程度,顺序图 3.4.3 改造点(可选) 如果是改造型功能,列出改造 3.4.4 算法设计(可选) 系统内用到的算法要有技术公式和计算步骤,参数管理方案等。算法要充分考虑性能因素,大批量操作的部分要首先保证性能。需要加密的数据要定义加解密方式。 3.4.5 核心业务逻辑类图(可选) 核心业务逻辑的实现方法可事先定义好,把类图画出来 3.4.6 接口设计详细说明 比如RPC接口,HTTP接口,可以通过单独的文档描述 3.5 YY模块实现设计 同 XX模块设计 3.6 基础模块设计 可以从以下几个角度进行分析: 项目中那些公共组件是很多业务通用的? 改造了那些公共组件是替他业务也在使用的,对其他业务有什么影响? 那些基础设施在本项目中设计,但是以后也打算给其它业务使用? 给其它业务使用过程中是否需要扩展,修改? 本项目中是否已经做了本项目用不到的超前设计? 超前设计是否需要评估和测试? 3.8 关联系统改造 描述关联上下游系统需要的改造。 四、数据存储设计 描述数据库的设计,包括新的设计和原来设计的修改。 4.1 领域模型设计 关注模型,设计ER图等描述数据库设计 示例,权限模型的ER图: 4.2 数据库设计 4.2.1 表结构设计 表中字段长度要留有余量,每个字段都要有详细说明 如果记录可能删除,建议用DELETED表示是否删除(0正常,-1删除)] 预估变化比较多的表,可以增加extend活着feature字段,用于将来可能增加的字段,以键值对形式追加 4.2.2 分库分表设计 影响分表选择的因素是表的业务是否支持水平拆分、表的大小、表的访问量(QPS+TPS)。 拆分的前提是表的业务有一个好的水平拆分维度。如买家id、商家id、订单id,都可以作为拆分的维度。、业务SQL,必须避免跨分表。 表大小上一定规模后,无论是根据pk查询还是根据二级索引查询,sql的rt都会随着表的大小增长而增长,最终超出应用对数据库响应时间的需求。所以,表大了要分表。 表的访问量上一定规模后,对于根据二级索引查询,sql的rt会随着page读写冲突概率的增加而增长,最终超出应用对数据库响应时间的需求。所以,表的访问量高了也要分表。 4.2.3 数据字典设计 数据字典中要定义状态值描述 4.2.4 索引设计 列出使用的主键索引,唯一索引,普通索引。 常用的组合查询条件等 4.2.5 优化设计 注:数据冗余,优化查询方案等 表之间的关系要简单,涉及到多表的复杂查询和跨数据库的操作要合理使用冗余字段。 数据删除一般要采用逻辑删除,逻辑删除的数据要有清理机制。 合理利用事务,误操作后数据如何恢复。 根据数据流量,执行频率,制定分页方案。 要考虑数据库的设计方案对存储成本有多少影响。 4.3 缓存方案设计 缓存选型,本地缓存设计,分布式缓存设计 进一步需要考虑缓存失效等常见问题 4.4 ES索引设计 列出可能需要的索引层设计。 五、数据安全和风控 5.1 业务和数据安全 业务安全可以从如下几个方面考虑: 外部系统提交的数据是完整的,能够满足我方进行数据核对的需求 我方返回的数据是完整的,能够满足外部系统进行数据核对的需求 业务数据的机密性,关键业务数据、敏感业务数据 传输过程中涉及到隐私和机密性,需要找出来,采取对应的措施予以解决 系统支持那些类型的外部访问者,对访问者进行标示,访问者标示和业务数据的关系限制 并发情况下,数据的安全性,主要体现在脏数据,错误数据方面 当存在与外部数据关联的情况下,如果关联失败后,数据不完整的处理方案 5.2 资损风险分析 只要涉及资金(如广告消耗)和电商履约(如商品出库)的项目,都存在资损风险。 5.2.1 风险列表描述 添加资损评估checklist内容。 本项目所有的资损风险点,在此处都要有分析描述。 5.2.2 资损防控方案分析 针对某些资损风险点,需要通过一个完整的方案才能从根源上解决资损风险,具体的方案分析在这里展开。 比如,在做电商退货退款的业务设计,我们要从几个角度进行防资损: 保证退款业务操作的唯一性,针对所有请求都遵循幂等性原则; 保证幂等检查的正确性,如果通过数据库进行幂等控制,要考虑业务重试的风险; 保证退款金额的合理性,退款金额不超出原有的支付金额,在系统模型的设计上要有所体现; 保证并发情况下,不会出现超额退款; 六、性能和可测性 6.1 容量规划 从系统目标和业务的角度,进行合理的容量规划。 6.2 性能分析 从业务的角度分析对性能的需求: • 对事务的响应时间(平均、最长) • 吞吐量(例如每秒处理的事务数) • 容量(例如业务所能容纳的客户或事务数) 在性能分析的时候,通常关注业务链路的起点、连接点、受限制的资源点。 6.2.1 并发访问控制 至少可以从以下方面分析: 有没有资源需要并发访问控制? 有没有资源在并发控制出错的情况下会造成损失? 并发访问控制会不会造成性能瓶颈? 6.2.2 接口性能延时要求 有没有具体的指标或要求? 有没有可能影响现有系统? 避免使用统一的指标要求所有业务,可能各个系统use case,或各个业务有不同的性能要求,应该要分别描述。 6.3 可测性分析 可以从以下方面分析: 系统依赖的外部环境是否可以测试,是否需要开发专门的mock系统 系统依赖的时间因素是否可以测试,是否需要开发专门的配合工具进行测试 系统依赖的时间,环境,权限等因素是否可以预发布确认,是否需要特别的准备才可以预发布确认。 系统依赖的特殊因素如果不能很好的测试或预发布确认,是否需要试运行策略? 七、稳定性设计 7.1 系统降级方案 考虑极端场景下,系统的降级方案。 7.2 系统容错分析 系统的容错能力可以从以下方面进行分析: 一、系统所依赖的外部系统发生一些错误的时候,系统有没有相应的容错机制 二、系统所依赖的硬件网络等基础设施发生故障,系统有没有相应的容错机制 由于所选择的通信,存储等方面的不可确定的因素,以及在某一个临界点处导致的质变,需要我们在此处指出可能存在的风险以及解决方案。 通信层面常出故障:例如带宽太小 文件系统:文件系统损坏、文件系统容量、文件系统每个目录所能容纳的目录、文件数量的限制 文件系统同时打开的句柄数的限制,间接影响并发行、文件自身的损坏 7.3 系统和业务监控 可以从以下方面分析: 系统发布和运行期间是否需要系统日志做监控 系统发布和运行期间是否要对原有业务做监控,以便知道影响 有没有制定新的一些重要的监控指标 系统是否在特定情况下需要一些报警 系统有没有考虑以后方便的扩充监控 监控发现严重问题是否有方法来得及挽回损失 7.4 系统日志 可以从以下方面分析: 日志的写入量有没有明显变化,异步或者同步,会不会影响业务 是否有新的需要长期归档的日志,请遵守命名规范 那些业务需要从日志中进行分析,以方便进行监控或排错 作为外部交互凭据的日志是否有归档,并和其它日志分开 八、数据影响分析 8.1 对已有业务数据分析的影响 可以从以下方面分析: 本项目所作的改造有没有影响现有的数据分析进行 本项目所作的改造有没有影响现有的业务数据分析结果 如果不明确影响,应该主动找数据分析团队沟通 8.2 发布期间影响分析 可以从以下方面分析: 是否有兼容方案,可以做到业务无感知 系统发布期间服务是否正常可用 系统发布期间对用户有没有影响 系统发布期间新老系统共存产生的数据有没有影响 各个系统发布顺序有没有要求 系统发布期间的兼容性是否建议专门测试 8.3 数据归档方案 可以从以下方面分析: 新增的表是否需要回流到HIVE表进行存储 如果表的数量持续增长,是否需要进行数据归档,对历时数据进行定期清理 九、工作量评估 9.1 工作量评估 拆解到人天,可以划分具体里程碑。 十、发布计划 10.1 上线计划 根据具体的业务情况,选择是否需要灰度上线、AB方案等。
2024-01-19
152
0
0
网络聚合
2024-01-19
靶机练习 - 温故知新 - Toppo(sudo 提权)
重新做了一下以前做过的第一个靶机(https://www.cnblogs.com/sallyzhang/p/12792042.html),这个靶机主要是练习sudo提权,当时不会也没理解。 开启靶机,直接告诉了IP地址: 端口扫描: 目录扫描: 一个一个访问,发现密码: 尝试用账户root,admin,ted登录,发现ted登录成功,但不是root权限: 上传扫描文件进行信息搜集,从搜集的信息发现可进行sudo提权: 网上搜一下awk命令咋执行的。。。 查看shadow文件: 执行/bin/bash,不是root。。。 回去翻一下搜集到的信息: 再次尝试,成功: 完成: 又试了下其他几个: 有的是ted权限,有的是root,所以前期的信息搜集还是很有用的,如果只是尝试/bin/bash没有提权到root可能就放弃了。 本文仅用于技术学习和交流,严禁用于非法用途,否则产生的一切后果自行承担。 如需转载,请注明出处,这是对他人劳动成果的尊重。
2024-01-19
89
0
0
网络聚合
2024-01-19
《痞子衡嵌入式半月刊》 第 72 期
痞子衡嵌入式半月刊: 第 72 期 这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻,农历年分二十四节气,希望在每个交节之日准时发布一期。 本期刊是开源项目(GitHub: JayHeng/pzh-mcu-bi-weekly),欢迎提交 issue,投稿或推荐你知道的嵌入式那些事儿。 上期回顾 :《痞子衡嵌入式半月刊: 第 71 期》 唠两句 历史上的今天:1992年2月25日,中国已勘明稀土资源工业储量占世界的80%。 本期共收录 5 个项目、1 个工具,希望对你有帮助! 项目类 1、Newlib - 面向嵌入式系统的开源C运行库 Newlib 是一个面向嵌入式系统的 C 运行库。最初是由 Cygnus Solutions 收集组装的一个源代码集合,取名为 newlib,现在由 Red Hat 维护。 对于与 GNU 兼容的嵌入式C运行库,Newlib 并不是唯一的选择,但是从成熟度来讲,Newlib 是最优秀的。Newlib 具有独特的体系结构,使得它能够非常好地满足深度嵌入式系统的要求。Newlib 可移植性强,具有可重入特性、功能完备等特点,已广泛应用于各种嵌入式系统中。 项目主页:https://sourceware.org/newlib/ 2、AVR LibC - AVR芯片及AVR-GCC工具链下标准库 AVR LibC 库包含了 ISO C 标准所要求的大部分功能,只有 wchar_t 支持是个明显的例外。它还包含许多针对 AVR 控制器家族的辅助功能。此外,它还包括可定制的启动代码,可以与 GNU binutils 提供的链接器脚本一起工作,因此对于大多数 AVR 应用程序来说,通常不需要为此目的提供特定于项目的文件。 项目主页:https://github.com/avrdudes/avr-libc 3、Picolib - 基于newlib和AVR Libc的轻量型C库 Picolibc 是一个提供标准 C 库 api 的库,针对内存有限的小型嵌入式 32bit/64bit 系统。Picolibc 是通过混合 Newlib 和 AVR Libc 的代码而形成的。 项目主页:https://github.com/picolibc/picolibc Picolib 支持的架构如下: 4、Libwebsockets - 轻量级用来开发服务器和客户端的C库 Libwebsockets (LWS)是一个灵活的、轻量级的纯C库,用于使用非阻塞事件循环轻松实现现代网络协议,占用空间很小。自2010年以来,它一直在不断开发,并被世界各地成千上万的开发人员用于数千万台设备中。 项目主页:https://libwebsockets.org/ 5、Flipper Zero - 面向极客的开源多功能工具 Flipper Zero 是一款便携式多功能工具,适合在玩具一样的身体里测试者和极客。它喜欢入侵数字设备,比如无线电协议、访问控制系统、硬件等等。它是完全开源和可定制的,所以你可以以任何你喜欢的方式扩展它。Flipper Zero 主控是 STM32WB55RG。 项目主页:https://github.com/flipperdevices 工具类 1、Kitspace - 入门级PCB项目分享网站 Kitspace 是一个分享现成电子产品设计(PCB)的地方,可以作为 PCB 入门设计参考,通过在线小工具可以看各种参数。 项目主页:https://kitspace.org/ 欢迎订阅 文章会同时发布到我的 博客园主页、CSDN主页、知乎主页、微信公众号 平台上。 微信搜索"痞子衡嵌入式"或者扫描下面二维码,就可以在手机上第一时间看了哦。
2024-01-19
145
0
0
网络聚合
65
66
67
68
69