0%

前言

不知做数仓的各位在工作中是否碰到以下几个现象

  • 业务方问,这个报表中的xx指标和另一个报表中的数据为什么对不上?一查,原来是表报02种的指标不含包了某个条件。
  • 新接了一个需求,是一个从来没有碰到过的需求,于是坑此坑系的去梳理了半天,然后一次偶然的机会,同事告诉你有现成的,某某以前做过。
  • 业务方拿着一个表的某个字段过来问你,这个字段是什么意思?
  • 某业务方开发的脚本,速度奇慢,于是让你帮忙优化,你一看脚本,好家伙,代码惨不忍睹,各种模型表中已有指标全部重新计算。
  • 使用其他同事开发的模型,各种命名全靠猜。
  • 已上线的任务,某天业务方反馈数据重复了,某某字段为空了等等等。

如果你有过以上大多数精力,恭喜你,你的数仓建设尚未体系化,处于百花齐放的各自开发阶段。

海拍客的现状

本人刚来海拍客的时候,上述描述的情况不断在发生,几个比较严重的原因和现象个人总结如下。

1. 需求烟囱化

因海拍客采用的是

前言

KMP(Knuth-Morris-Pratt)算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。

字符串匹配问题

所谓字符串匹配,是这样一种问题:“字符串 P 是否为字符串 S 的子串?如果是,它出现在 S 的哪些位置?” 其中 S 称为主串;P 称为模式串。下面的图片展示了一个例子。

  主串是莎翁那句著名的 “to be or not to be”,这里删去了空格。“no” 这个模式串的匹配结果是“出现了一次,从$S[6]$开始”;“ob”这个模式串的匹配结果是“出现了两次,分别从$S[1]$、$S[10]$开始”。按惯例,主串和模式串都以0开始编号。  字符串匹配是一个非常频繁的任务。例如,今有一份名单,你急切地想知道自己在不在名单上;又如,假设你拿到了一份文献,你希望快速地找到某个关键字(keyword)所在的章节……凡此种种,不胜枚举。  我们先从最朴素的Brute-Force算法开始讲起。

Brute-Force

顾名思义,Brute-Force是一个纯暴力算法。说句题外话,我怀疑,“暴力”一词在算法领域表示“穷举、极低效率的实现”,可能就是源于这个英文词。  

首先,我们应该如何实现两个字符串 A,B 的比较?所谓字符串比较,就是问“两个字符串是否相等”。最朴素的思想,就是从前往后逐字符比较,一旦遇到不相同的字符,就返回False;如果两个字符串都结束了,仍然没有出现不对应的字符,则返回True。实现如下:

  既然我们可以知道“两个字符串是否相等”,那么最朴素的字符串匹配算法 Brute-Force 就呼之欲出了——

  • 枚举 i = 0, 1, 2 … , len(S)-len(P)
  • 将 S[i : i+len(P)] 与 P 作比较。如果一致,则找到了一个匹配。

  现在我们来模拟 Brute-Force 算法,对主串 “AAAAAABC” 和模式串 “AAAB” 做匹配:

  这是一个清晰明了的算法,实现也极其简单。下面给出Python和C++的实现:

  我们成功实现了 Brute-Force 算法。现在,我们需要对它的时间复杂度做一点讨论。按照惯例,记 $n = |S|$ 为串 S 的长度,$m = |P|$ 为串 P 的长度。
  考虑“字符串比较”这个小任务的复杂度。最坏情况发生在:两个字符串唯一的差别在最后一个字符。这种情况下,字符串比较必须走完整个字符串,才能给出结果,因此复杂度是 $O(len)$ 的。
    由此,不难想到 Brute-Force 算法所面对的最坏情况:主串形如“AAAAAAAAAAA…B”,而模式串形如“AAAAA…B”。每次字符串比较都需要付出 $|P|$ 次字符比较的代价,总共需要比较 $|S| - |P| + 1$次,因此总时间复杂度是 $O(|P|⋅(|S|−|P|+1))O(|P|\cdot (|S| - |P| + 1) )O(|P|\cdot (|S| - |P| + 1) )$ . 考虑到主串一般比模式串长很多,故 Brute-Force 的复杂度是 $O(|P|⋅|S|)O(|P| \cdot |S|)O(|P| \cdot |S|)$ ,也就是 $O(nm)的$。这太慢了!

Brute-Force的改进思路

经过刚刚的分析,您已经看到,Brute-Force 慢得像爬一样。它最坏的情况如下图所示:

  我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。  要优化一个算法,首先要回答的问题是“我手上有什么信息?” 我们手上的信息是否足够、是否有效,决定了我们能把算法优化到何种程度。请记住:尽可能利用残余的信息,是KMP算法的思想所在。
  在 Brute-Force 中,如果从 S[i] 开始的那一趟比较失败了,算法会直接开始尝试从 S[i+1] 开始比较。这种行为,属于典型的“没有从之前的错误中学到东西”。我们应当注意到,一次失败的匹配,会给我们提供宝贵的信息——如果 S[i : i+len(P)] 与 P 的匹配是在第 r 个位置失败的,那么从 S[i] 开始的 (r-1) 个连续字符,一定与 P 的前 (r-1) 个字符一模一样!

  需要实现的任务是“字符串匹配”,而每一次失败都会给我们换来一些信息——能告诉我们,主串的某一个子串等于模式串的某一个前缀。但是这又有什么用呢?

跳过不可能成功的字符串比较

  有些趟字符串比较是有可能会成功的;有些则毫无可能。我们刚刚提到过,优化 Brute-Force 的路线是“尽量减少比较的趟数”,而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。  那么,哪些字符串比较是不可能成功的?来看一个例子。已知信息如下:

  • 模式串 P = “abcabd”.
  • 和主串从S[0]开始匹配时,在 P[5] 处失配。

      首先,利用上一节的结论。既然是在 $P[5]$ 失配的,那么说明 $S[0:5]$ 等于 $P[0:5]$,即”abcab”. 现在我们来考虑:从 $S[1]$、$S[2]$、$S[3]$ 开始的匹配尝试,有没有可能成功?
      从 $S[1]$ 开始肯定没办法成功,因为 $S[1] = P[1]$ = ‘b’,和 $P[0]$ 并不相等。从 $S[2]$ 开始也是没戏的,因为 $S[2] = P[2]$ = ‘c’,并不等于$P[0]$. 但是从 $S[3]$ 开始是有可能成功的——至少按照已知的信息,我们推不出矛盾。

      带着“跳过不可能成功的尝试”的思想,我们来看next数组。

next数组

  next数组是对于模式串而言的。P 的 next 数组定义为:$next[i]$ 表示 $P[0]$ ~ $P[i]$ 这一个子串,使得 前k个字符恰等于后k个字符 的最大的k. 特别地,k不能取i+1(因为这个子串一共才 i+1 个字符,自己肯定与自己相等,就没有意义了)。

  上图给出了一个例子。P=”abcabd”时,$next[4]$=2,这是因为$P[0]$ ~ $P[4]$ 这个子串是”abcab”,前两个字符与后两个字符相等,因此$next[4]$取2. 而$next[5]=0$,是因为”abcabd”找不到前缀与后缀相同,因此只能取0.

  如果把模式串视为一把标尺,在主串上移动,那么 Brute-Force 就是每次失配之后只右移一位;改进算法则是每次失配之后,移很多位,跳过那些不可能匹配成功的位置。但是该如何确定要移多少位呢?

  在 $S[0]$ 尝试匹配,失配于 $S[3] <=> P[3]$ 之后,我们直接把模式串往右移了两位,让 $S[3]$ 对准 $P[1]$. 接着继续匹配,失配于 $S[8] <=> P[6]$, 接下来我们把 P 往右平移了三位,把 $S[8]$ 对准 $P[3]$. 此后继续匹配直到成功。
  我们应该如何移动这把标尺?很明显,如图中蓝色箭头所示,旧的后缀要与新的前缀一致(如果不一致,那就肯定没法匹配上了)!

  回忆next数组的性质:$P[0]$ 到 $P[i]$ 这一段子串中,前$next[i]$个字符与后$next[i]$个字符一模一样。既然如此,如果失配在 $P[r]$, 那么$P[0]$~$P[r-1]$这一段里面,前$next[r-1]$个字符恰好和后$next[r-1]$个字符相等——也就是说,我们可以拿长度为 $next[r-1]$ 的那一段前缀,来顶替当前后缀的位置,让匹配继续下去!
  您可以验证一下上面的匹配例子:P[3]失配后,把P[next[3-1]]也就是P[1]对准了主串刚刚失配的那一位;P[6]失配后,把P[next[6-1]]也就是P[3]对准了主串刚刚失配的那一位。

  如上图所示,绿色部分是成功匹配,失配于红色部分。深绿色手绘线条标出了相等的前缀和后缀,其长度为next[右端]. 由于手绘线条部分的字符是一样的,所以直接把前面那条移到后面那条的位置。因此说,next数组为我们如何移动标尺提供了依据。接下来,我们实现这个优化的算法。

利用next数组进行匹配

  了解了利用next数组加速字符串匹配的原理,我们接下来代码实现之。分为两个部分:建立next数组、利用next数组进行匹配。  首先是建立next数组。我们暂且用最朴素的做法,以后再回来优化:

  如上图代码所示,直接根据next数组的定义来建立next数组。不难发现它的复杂度是 $O(m2)O(m^2)O(m^2)$ 的。
  接下来,实现利用next数组加速字符串匹配。代码如下:

  如何分析这个字符串匹配的复杂度呢?乍一看,pos值可能不停地变成next[pos-1],代价会很高;但我们使用摊还分析,显然pos值一共顶多自增len(S)次,因此pos值减少的次数不会高于len(S)次。由此,复杂度是可以接受的,不难分析出整个匹配算法的时间复杂度:O(n+m).

快速求next数组

  终于来到了我们最后一个问题——如何快速构建next数组。
  首先说一句:快速构建next数组,是KMP算法的精髓所在,核心思想是“P自己与自己做匹配”。
  为什么这样说呢?回顾next数组的完整定义:

  • 定义 “k-前缀” 为一个字符串的前 k 个字符; “k-后缀” 为一个字符串的后 k 个字符。k 必须小于字符串长度。
  • next[x] 定义为: P[0]P[x] 这一段字符串,使得k-前缀恰等于k-后缀的最大的k.
      这个定义中,不知不觉地就包含了一个匹配——前缀和后缀相等。接下来,我们考虑采用递推的方式求出next数组。如果next[0], next[1], … next[x-1]均已知,那么如何求出 next[x] 呢?
      来分情况讨论。首先,已经知道了 next[x-1](以下记为now),如果 P[x] 与 P[now] 一样,那最长相等前后缀的长度就可以扩展一位,很明显 next[x] = now + 1. 图示如下。

      刚刚解决了 P[x] = P[now] 的情况。那如果 P[x] 与 P[now] 不一样,又该怎么办?

      如图。长度为 now 的子串 A 和子串 B 是 P[0]
    P[x-1] 中最长的公共前后缀。可惜 A 右边的字符和 B 右边的那个字符不相等,next[x]不能改成 now+1 了。因此,我们应该缩短这个now,把它改成小一点的值,再来试试 P[x] 是否等于 P[now].
      now该缩小到多少呢?显然,我们不想让now缩小太多。因此我们决定,在保持“P[0]P[x-1]的now-前缀仍然等于now-后缀”的前提下,让这个新的now尽可能大一点。 P[0]P[x-1] 的公共前后缀,前缀一定落在串A里面、后缀一定落在串B里面。换句话讲:接下来now应该改成:使得 A的k-前缀等于B的k-后缀 的最大的k.
      您应该已经注意到了一个非常强的性质——串A和串B是相同的!B的后缀等于A的后缀!因此,使得A的k-前缀等于B的k-后缀的最大的k,其实就是串A的最长公共前后缀的长度 —— next[now-1]!

      来看上面的例子。当P[now]与P[x]不相等的时候,我们需要缩小now——把now变成next[now-1],直到P[now]=P[x]为止。P[now]=P[x]时,就可以直接向右扩展了。  代码实现如下:

      应用摊还分析,不难证明构建next数组的时间复杂度是O(m)的。至此,我们以O(n+m)的时间复杂度,实现了构建next数组、利用next数组进行字符串匹配。
      以上就是KMP算法。它于1977年被提出,全称 Knuth–Morris–Pratt 算法。让我们记住前辈们的名字:Donald Knuth(K), James H. Morris(M), Vaughan Pratt(P).

模板代码

  最后附上洛谷P3375 【模板】KMP字符串匹配 的Python和Java版代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Kmp {

//next[i]表示长度为n的多少前缀等于后缀
private int[] next;

private String pattern;

public Kmp(String pattern){
this.pattern=pattern;
buildNext(pattern);
}

private void buildNext(String pattern){
next=new int[pattern.length()];
next[0]=0;
int pos=0;
int i=1;
while (i<pattern.length()){
if(pattern.charAt(i)==pattern.charAt(pos)){
pos++;
next[i]=pos;
i++;
}else if(pos!=0){
pos=next[pos-1];
}else{
next[i]=pos;
i++;
}
}
}

public int search(String word){
int index=-1;
int j=0;
for(int i=0;i<word.length();i++){
while (j<pattern.length()){
if(word.charAt(i)==pattern.charAt(j)){
j++;
i++;
}else if(j==0){//字符不一样时
break;
}else{
j=next[j-1];
}
}
if(j==pattern.length()){
return i-pattern.length();
}
}
return index;
}
}

KMP算法在jdk中的源码应用

学完了这么牛逼的算法,将复杂度从$O(m*n)$降低到了$O(m+n)$,那么来看一下jdk源码String.indexOf和contains是使用kmp算法吗?

1
2
3
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}

contains直接调用indexOf方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}

char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);

for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}

/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);

if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}

indexOf方法采用的是非常朴素的Brute-Force暴力算法。

所以,为什么jdk没有采用kmp算法?

这就不清楚了,一种说法是短字符串的比较,开辟空间消耗的资源远远比计算的大,kmp更适用的场景是,固定的pattern,不断去比较新的字符串,复用next数组。

本文转自 如何更好地理解和掌握 KMP 算法?

Brute Force 算法

用于模式匹配算法

人话就是暴力算法,两层循环。

Brute Force 算法就是穷举 P 相对于 T 产生的所有可能性。

问题

很多人在各种平台开通会员时,经常会选择自动续费,连续包月,毕竟便宜啊。
但是设置后,过个个把月,就不怎么使用功能了,然而钱还是每月哗哗哗的扣款,心疼啊。
本文就描述微信2022新版如何取消自动扣款设置。

怎么取消自动扣款?

大家上网搜的基本都是旧版方案,比如这篇微信怎么样关闭自动扣款,旧版微信在【我的】界面确实有【支付这个选项】,然而在最新的2022版本中已经没有了。

没有了不要慌,这就来说新版怎么取消。

2022微信新版的操作界面在【我的-服务-钱包-支付设置-自动续费中】

1.进入微信,点击【我的】按钮

2.进入【我的】页面,点击【服务】按钮

3.进入【服务】页面,点击【钱包】按钮

4.进入【钱包】页面,点击【支付设置】按钮

5.进入【支付设置】页面,点击【自动续费】按钮

6.进入【自动续费】页面

该页面展示的是你自动续费的选项,选中你要取消的服务,进入详情页

6.进入【详情页】页面,点击【关闭扣费服务】按钮

前言

之前写过一篇ngrok+nginx实现windows远程桌面连接实现了在公司远程连接家里的电脑,最近发现另一个开源网络穿透frp似乎更优秀,其在git的star达到了61.4k,本篇就是用frp搭建网络穿透。

frp 是什么?


官方的解释为

frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the Internet. As of now, it supports TCP and UDP, as well as HTTP and HTTPS protocols, where requests can be forwarded to internal services by domain name.
frp also has a P2P connect mode.

frp 是一种快速反向代理,可帮助您将 NAT 或防火墙后面的本地服务器暴露给 Internet。 目前,它支持 TCP 和 UDP,以及 HTTP 和 HTTPS 协议,可以通过域名将请求转发到内部服务。
frp 还有一个 P2P 连接模式。

简单来说,你个人本地web服务,通过与frp的服务建立隧道连接,然后别人就可以通过访问frp服务来访问你的本地服务了。

frp的git地址: https://github.com/fatedier/frp

frp官方文档: https://gofrp.org/docs/overview/

官方文档罗列了相关概念和操作,要是有能力尽量阅读官方文档。

frp 的下载

frp可以在release页面进行下载,当前的最新版本为0.045,客户端和服务端为同一个,只不过配置和启动命令不同,大家可以根据自己的操作系统进行下载,要是linux不知道下载那个那个文件,可以查看linux cpu 指令集架构 RISC / CISC | arm | amd | X86/i386 | aarch64这篇文章了解cpu对应的指令集架构下载对应的软件包。

frp 服务端

frp 服务端配置


frp的服务端配置文件为frp文件夹下的frps.ini文件,具体完整的配置可以参考frps_full.ini文件.

本人的一个配置为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 模块名
[common]
# 对frp客户端暴露的端口
bind_port = 7000
# 对外暴露的接口
vhost_http_port = 7099
# 服务端统计仪表盘端口
dashboard_port = 7091
# 仪表盘登录用户名
dashboard_user = admin
# 仪表盘登录密码
dashboard_pwd = admin
# 子域名,这里采用子域名模式
subdomain_host = frp.xxx.com

# 和客户端token链接
authentication_method=token
# 认证链接
token=test_token

frp 服务端启动

标准的启动命令为

1
/frp_path/frps -c /frp_path/frps.ini

frp 的部署

只使用命令,当我们推出linux shell后,服务就暂停了,这不是我们想要的结果,部署有两种方式,使用systemd及后天运行脚本。

systemd
systemd为官方推荐的部署方式,参考官方文档 使用 systemd

后台运行
我们也可以使用linux后台运行命令自己部署

1
nohup /frp_path/frps -c /frp_path/frps.ini &

frp 客户端

frp 客户端配置


frp的客户端配置文件为frp文件夹下的frpc.ini文件,具体完整的配置可以参考frpc_full.ini文件.

本人的一个配置为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# frpc.ini
# 对应上文服务器配置的模块名
[common]
server_addr = frp.xxx.com
server_port = 7000

# windows 远程桌面端口
[windows]
# 协议需要是tcp
type = tcp
# 本地的远程服务端口,默认为3389,处于安全考虑,建议修改远程默认端口
local_port = 3389
# frp server暴露连接的端口,远程桌面连接时需要输入端口,因此通过端口暴露,这里也建议修改端口
remote_port = 3389
# 和服务端认证链接
token=test_token

# http web 接口服务
[httpweb01]
# 协议
type = http
# 本地服务端口
local_port = 7102
# 子域名,由于我们服务器的域名subdomain_host配置为frp.xxx.com,因此这里暴露的域名为 web1.frp.xxx.com
subdomain = web1
# http basic auth 基础认证的用户名和密码,不然别人只要知道了你的url就可以访问了
http_user = easytrader
http_pwd = easytrader123
# 和服务端认证链接
token=test_token

# web02
[httpweb02]
type = http
local_port = 7103
subdomain = web2
http_user = easytrader
http_pwd = easytrader123
# 和服务端认证链接
token=test_token


frp 客户端启动

标准的启动命令为

1
./frpc -c ./frpc.ini

frp 的部署

linux 下推荐使用 systemd
windows下推荐自己编写.bat文件,然后配置为开机启动项,windows的开机启动配置可以参考win10 开机启动,无需登录

域名配置

登录你的域名管理,配置frp.xxx.com*.frp.xxx.com指向你的frp server地址

nginx 配置

一个简单的nginx配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
server_name *.frp.xxx.com;

location / {
proxy_pass http://127.0.0.1:7000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

本质上就是当后续使用服务的时候,如访问者访问http://web1.frp.xxx.com 时,将请求转交给frp server处理。

frp 的使用情况

登录 http://server ip:7091 进行访问,端口为之前服务端的dashboard_port配置。

服务的使用

远程桌面连接

通过以下地址 frp.xxx.com:3389 进行连接

http接口或页面

如果是页面,直接访问 web1.frp.xxx.com ,输入用户名密码后即可进行访问

如果是程序调用,需要加上 Authorization 的header,具体可以参考文章 JAVA Http Basic auth

自问自答

frp的客户端和服务端通信安全如何保障?

参考身份认证 ,目前 frpc 和 frps 之间支持两种身份验证方式,token 和 oidc,默认为 token。

我暴露的服务如何保障安全

http相关的可以通过设置 BasicAuth 鉴权实现简单的认证。

复杂可以通过安全地暴露内网服务,这样服务只能被部署了visitor frp client的机器访问,缺点是需要部署frp cli。

完结

至此,使用frp搭建的http穿透及远程桌面连接已顺利实现。

前言

有时候我们希望一些常用程序在开机后就自动运行。
比如笔者就配置了一个远程登录结合frp网络穿透实现windows远程连接,那么就需要frp程序在开机时就运行。
那么如何配置呢?

开机启动配置

上网搜索开机启动,有非常多的答案,但大多方案都有一个弊端,需要登录后才会启动。
比如知乎的这篇Windows10 开机自启动设置 中给的【任务选项->启动】配置及【运行窗口->shell:startup】启动项选项两个方案,都是需要用户登录后才能生效的。

那么有没有不需要启动就能登录的?

有的,可以采用设置定时任务的方式实现。

win10 实现设置定时任务方法

1、在Windows10桌面,右键点击此电脑图标,在弹出菜单中选择“管理”菜单项。

2、然后在打开的计算机管理窗口中,找到“任务计划程序”菜单项。

3、右键,点击创建基本任务
这里创建任务由两种,一种为向导模式的【创建基本任务】,一种为直接模式【创建任务】,我们选择向导模式。

4、进入创建基本任务界面,填入名称和描述,本人的为frp启动,填完后点击下一步。

5、进入触发器选择界面,这里我选择的是计算机启动时,如果你有不同需求,按照自己需求选择。

6、进入操作选择界面,这里我们选择启动程序,因为我们的目标是运行程序。

7、点击下一步,点击界面上的浏览按钮,选择我们期望运行的程序,这里本人选择的是自己编写的start_frp.bat脚本。

8、最后点击完成完成配置。

9、回到之前的【管理->系统工具->任务执行计划->任务执行计划程序库】,此时就能看到自己创建的定时任务了。

此时大家可以重启电脑进行验证。

至此,配置完成

访问者模式简介

访问者模式是一种行为模式。

访问者模式是一种将数据操作和数据结构分离的设计模式。(觉得太抽象,可以看下面的例子)。

使用场景

  1. 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
  2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

优缺点

优点

  1. 各角色职责分离,符合单一职责原则
    通过后面UML类图和示例可以看出来,Visitor、ConcreteVisitor、Element 、ObjectStructure,职责单一,各司其责。
  2. 具有优秀的扩展性
    如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。
  3. 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
    城市经典(数据结构)和跟团游游客、自驾游游客访问者(数据操作)的解耦。
  4. 灵活性

缺点

  1. 具体元素对访问者公布细节,违反了迪米特原则
    团游游客、自驾游游客需要调用具体城市的方法。
  2. 具体元素变更时导致修改成本大
    变更城市属性时,多个访问者都要修改,比如雷峰塔倒了。
  3. 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有以来抽象
    访问者 visit 方法中,依赖了具体城市的具体方法。

通用模板

UML 图

角色介绍

  • Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。
  • ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。
    Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
  • ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。
  • client: 具体的运行代码类

通用代码

Element接口

1
2
3
4
5
6
7
8
/**
* 元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
* */
public interface Element {

public void accept(Visitor visitor);

}

Element Concrete 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

public void operation1(){
System.out.println("ElementA do operation1");
}
}

public class ElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

public void operation2(){
System.out.println("ElementB do operation2");
}
}

visitor接口

1
2
3
4
5
6
7
8
9
10
/**
* 定义了对每个 Element 访问的行为
* */
public interface Visitor {

public void visit(ElementA element1);

public void visit(ElementB element2);

}

visitor concrete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class VisitorA implements Visitor {
@Override
public void visit(ElementA elementA) {
System.out.println("Visitor A start visit ElementA");
elementA.operation1();
}

@Override
public void visit(ElementB elementB) {
System.out.println("Visitor A start visit ElementB");
elementB.operation2();
}
}

public class VisitorB implements Visitor {
@Override
public void visit(ElementA elementA) {
System.out.println("Visitor B start visit ElementA");
elementA.operation1();
}

@Override
public void visit(ElementB elementB) {
System.out.println("Visitor B start visit ElementB");
elementB.operation2();
}
}

ObjectStructure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ObjectStructure {

private List<Element> elementList;

public ObjectStructure(){
elementList=new ArrayList<Element>();
elementList.add(new ElementA());
elementList.add(new ElementB());
}

public void accept(Visitor visitor){
for(Element element:elementList){
element.accept(visitor);
}
}

public void addElement(Element element){
elementList.add(element);
}

public void removeElement(Element element){
elementList.remove(element);
}
}

最后client调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {

private Visitor visitor;

private ObjectStructure objectStructure;

public void doVisit(){
ObjectStructure objectStructure=new ObjectStructure();
objectStructure.accept(new VisitorA());
objectStructure.accept(new VisitorB());
}

public static void main(String[] args){
Client client=new Client();
client.doVisit();
}
}

执行结果

1
2
3
4
5
6
7
8
Visitor A start visit ElementA
ElementA do operation1
Visitor A start visit ElementB
ElementB do operation2
Visitor B start visit ElementA
ElementA do operation1
Visitor B start visit ElementB
ElementB do operation2

依然很抽象,看下面的例子吧。

旅游城市例子

访问者模式,用游客访问城市经典的例子再合适不过了.

不同的游客有不同的访问行为,跟团游和自驾游或休闲游在游览景点是的行为也是完全不相同的,完美符合访问者模式中的访问者这一对象。

UML 图

角色介绍

  • City
    城市充当Element角色,各城市属性非常稳定,但是城市所属的景点可能发生轻微改造或变更,完美符合Element角色的要求。
  • HangzhouCity,HuangshanCity,BeijingCity
    三个城市的具体实现:杭州、黄山、北京,对应的是ElementConcrete
  • Tourists
    游客接口,游客可以访问城市,对应访问者模式中的Visitor角色。
  • WithTheGroupTourists,SelfDriverTourists
    游客的两个具体实现,跟团游游客和自驾游游客,对应VisitorConcrete
  • TravelPlan
    旅游计划抽象,封装了一堆城市的集合。
    对应ObjectStructure
  • TravelClient
    具体的一次旅游,对应 Client

实例代码

City

1
2
3
4
5
6
7
8
/**
* 城市,对应访问者模式中的Element元素,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
* */
public interface City {

public void accept(Tourists visitor);

}

CityConcrete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 北京,city的具体实现
* * 对应visitor中的ElementConcrete
* */
public class BeijingCity implements City {
@Override
public void accept(Tourists tourists) {
tourists.visitBeijing(this);
}

/**
* 旅游长城
* */
public String greatWall(){
return "长城";
}

/**
* 旅游故宫
* */
public String forbiddenCity(){
return "故宫";
}

/**
* 后海酒吧
* */
public String houhaiBar(){
return "后海酒吧";
}
}

/**
* 杭州,city的具体实现
* 对应visitor中的ElementConcrete
* */
public class HangzhouCity implements City {
@Override
public void accept(Tourists tourists) {
tourists.visitHangzhou(this);
}

/**
* 西湖
* */
public String westLake(){
return "西湖";
}

/**
* 浙江大学
* */
public String zhejiangUniversity(){
return "浙江大学";
}

/**
* 雷峰塔
* */
public String leifengPagoda(){
return "雷峰塔";
}
}

/**
* 黄山,city的具体实现
* 对应visitor中的ElementConcrete
* */
public class HuangshanCity implements City {
@Override
public void accept(Tourists tourists) {
tourists.visitHuangshan(this);
}

/**
* 黄山
* */
public String huangshan(){
return "黄山";
}
}

Tourists

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/**
* 游客,对应访问者模式中的Visitor
* 定义了对每个 Element 访问的行为
* */
public interface Tourists {

/**
* 访问北京
* */
public void visitBeijing(BeijingCity beijingCity);

/**
* 访问杭州
* */
public void visitHangzhou(HangzhouCity hangzhouCity);

/**
* 访问黄山
* */
public void visitHuangshan(HuangshanCity huangshanCity);

}

TouristsConcrete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 自驾游,Tourists实现,对应访问者模式中VisitorConcrete
* */
public class SelfDriveTourists implements Tourists {

@Override
public void visitBeijing(BeijingCity beijingCity) {
System.out.println(">>>>>开始自驾旅游北京");
System.out.println("第1站:"+beijingCity.greatWall());
System.out.println("第2站:"+beijingCity.forbiddenCity());
System.out.println("第3站:"+beijingCity.houhaiBar());
System.out.println(">>>>>结束自驾旅游北京");

}

@Override
public void visitHangzhou(HangzhouCity hangzhouCity) {
System.out.println(">>>>>开始自驾旅游杭州");
System.out.println("第1站:"+hangzhouCity.westLake());
System.out.println("第2站:"+hangzhouCity.leifengPagoda());
System.out.println("第3站:"+hangzhouCity.zhejiangUniversity());
System.out.println(">>>>>结束自驾旅游杭州");
}

@Override
public void visitHuangshan(HuangshanCity huangshanCity) {
System.out.println(">>>>>开始自驾旅游黄山");
System.out.println("第1站:"+huangshanCity.huangshan());
System.out.println(">>>>>结束自驾旅游黄山");
}
}

/**
* 跟团游,Tourists的实现
* 对应访问者模式中VisitorConcrete
* */
public class WithTheGroupTourists implements Tourists {
@Override
public void visitBeijing(BeijingCity beijingCity) {
System.out.println(">>>>>开始跟团旅游北京");
System.out.println("第1站:"+beijingCity.houhaiBar());
System.out.println("第2站:"+beijingCity.forbiddenCity());
System.out.println("跟团游时间太紧,无法旅游:"+beijingCity.greatWall());
System.out.println(">>>>>结束跟团旅游北京");

}

@Override
public void visitHangzhou(HangzhouCity hangzhouCity) {
System.out.println(">>>>>开始跟团旅游杭州");
System.out.println("第1站:"+hangzhouCity.westLake());
System.out.println("第2站:"+hangzhouCity.zhejiangUniversity());
System.out.println("跟团游时间太紧,无法旅游:"+hangzhouCity.leifengPagoda());
System.out.println(">>>>>结束跟团旅游杭州");
}

@Override
public void visitHuangshan(HuangshanCity huangshanCity) {
System.out.println(">>>>>开始跟团旅游黄山");
System.out.println("第1站:"+huangshanCity.huangshan());
System.out.println(">>>>>结束跟团旅游黄山");
}
}

TravelPlan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 旅游计划
* 对应访问者模式中的ObjectStructure
* */
public class TravelPlan {

private List<City> cityList;

public TravelPlan(){
cityList=new ArrayList<City>();
cityList.add(new BeijingCity());
cityList.add(new HangzhouCity());
cityList.add(new HuangshanCity());

}

public void travel(Tourists visitor){
for(City city:cityList){
city.accept(visitor);
}
}

public void addCity(City city){
cityList.add(city);
}

public void removeCity(City city){
cityList.remove(city);
}
}

TravelClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 旅游记录
* 对应访问者模式中的Client
* */
public class TravelClient {

private Tourists visitor;

private TravelPlan travelPlan;

public void doTravel(){
TravelPlan travelPlan=new TravelPlan();
//跟团游
travelPlan.travel(new WithTheGroupTourists());
System.out.println();
System.out.println();

//自驾游
travelPlan.travel(new SelfDriveTourists());
}

public static void main(String[] args){
TravelClient travelClient=new TravelClient();
travelClient.doTravel();
}
}

执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>>>>开始跟团旅游北京
第1站:后海酒吧
第2站:故宫
跟团游时间太紧,无法旅游:长城
>>>>>结束跟团旅游北京
>>>>>开始跟团旅游杭州
第1站:西湖
第2站:浙江大学
跟团游时间太紧,无法旅游:雷峰塔
>>>>>结束跟团旅游杭州
>>>>>开始跟团旅游黄山
第1站:黄山
>>>>>结束跟团旅游黄山


>>>>>开始自驾旅游北京
第1站:长城
第2站:故宫
第3站:后海酒吧
>>>>>结束自驾旅游北京
>>>>>开始自驾旅游杭州
第1站:西湖
第2站:雷峰塔
第3站:浙江大学
>>>>>结束自驾旅游杭州
>>>>>开始自驾旅游黄山
第1站:黄山
>>>>>结束自驾旅游黄山

代码

以上例子代码可在个人github项目design-patterns中找到。

参考文章

访问者模式一篇就够了

背景

使用idea阅读spark 源码时,如果是第一次导入,会碰到 org.apache.spark.sql.catalyst.parser.SqlBaseParser包下相关的类无法找到的问题。
AstBuilder 继承 SqlBaseBaseVisitor,SqlBaseBaseVisitor就无法找到。

原因

org.apache.spark.sql.catalyst.parser.SqlBaseParser包下相关的代码并不是人工写的源码,而是antlr4框架自动生成的代码,因为代码没有生成,所以idea无法找到相关代码。

antlr4是什么?

  • antlr github 地址
  • antlr 官网 地址

    ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build parse trees and also generates a listener interface (or visitor) that makes it easy to respond to the recognition of phrases of interest.

ANTLR(另一种语言识别工具)是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件。 它广泛用于构建语言、工具和框架。 从语法中,ANTLR 生成一个可以构建解析树的解析器,还生成一个侦听器接口(或访问者),可以轻松响应对感兴趣短语的识别。

简单来说就是定义语言此法和语法的框架,比如sql的解析。

对应的同类产品有:

  • 在 C/C++ 开发环境下比较有名的 Flex、 Yacc 等;
  • 在 Java 的世 界里,词法分析工具有 JFlex,语法分析工具有 BYACC/J、 Java Cup,兼具两者的有 JavaCC 和 ANTLR等

spark使用antlr4干什么?

spark使用antlr4来解析sql语法,生成语法树,之后应用语法树生成相关逻辑树及最后的物理执行计划。

如果你对sparksql 采用antlr4 解析比较感兴趣,这里推荐
ANTLR4-SqlBase这个开源项目,该作者为 《Spakr SQL内核剖析》 一书作者剥离的Parser模块,用于查看Spark SQL语法解析SQL后生成的语法树的项目。

解决方法

使用antlr4插件生成代码,并将相关代码引入到classpath中。

1、安装antlr4插件

本人mac系统
在 intellij Idea->Preference->Plugins
下搜索 antlr4,安装插件

2、使用antlr4插件生成代码

2.1 使用antlr4语法文件SqlBase.g4生成对应的代码

找到SqlBase.g4文件,该文件在sql/catalyst/src/main/antlr4/org.apache.spark.sql.catalyst.parser下

右键,找到antlr4插件的Generate ANTLR Recognizer选项,点击
如下图所示

会在 sql/catalyst 下创建gen文件夹,生成对应的代码,如果想修改目录,可以点击configure ANTLR修改,这里直接默认的就好

生成的代码结构如下图

2.2 将相关代码导入 classpath

选中再上一步生成的gen文件,右键,找到Mark Directory as -> Sources Root,将该文件夹设置为代码跟路径,这样idea就会将该文件夹添加到classpath中并编译。

再次查看AstBuilder代码,此时红色错误已消失,代码已找到

至此,相关问题已解决。

前言

不知大家是否有这样一些场景:

手头有一些很重要的照片,充满回忆,绝对不能丢失,存到电脑里害怕有一天电脑坏掉导致照片丢失,对丢失数据的恐惧是整天困扰你。

手动把文件存到多台电脑又太麻烦。

数据同步就是解决这种烦恼的答案。本文提供了10个最佳软件选项,你可以从中选择以最小的麻烦创建文件和文件夹的备份。希望本文能帮助到你。

本人推荐Syncthing方案

最强推荐 Syncthing


最好的备份和文件同步软件下载:Syncthing ,它允许在两台或多台计算机之间实时同步文件。它涉及连续的文件同步。Syncthing 提供完全的隐私保护,让你安全地同步文件。你可以选择要存储数据的位置,或者是否要与第三方共享。你还可以决定你的数据在互联网上的传输方式。它使用 TLS 加密为你的文件提供私人存储以保密。

特征:

  • 你可以在自己的设备之间或与使用不同设备的不同人员同步任意数量的文件夹。所以,它很强大
  • Syncthing 允许使用可通过浏览器访问的强大且响应迅速的界面进行简单的配置和监控。在台式计算机上运行 Syncthing 并允许备份后,你可以将文件夹与服务器同步。因此,它是便携式的
  • 当你使用 Syncthing 时,每台机器都有一个单独的 ID。你可以通过简单地共享 ID 与你的朋友共享文件夹。这是可能的,因为 Syncthing 不需要 IP 地址或高级配置,因为它可以在 LAN 和 Internet 上运行。这使它易于使用
    官网
    文件大小: 48.5 MB
    系统支持: Linux、OS X、Windows、Android、BSD、Solaris

GoodSync

GoodSync由 Siber Systems 开发的 ,是一个帮助在两个目录之间创建备份和同步文件的程序。GoodSync 让你可以在多个计算设备上存储和保存相同版本的文件。GoodSync 确保当你在任何单个设备上修改文件时,更改也会在其他设备上同时进行。因此,从哪个设备访问文件并不重要。它还允许你在远程计算机或服务器与你的计算机之间同步文件。

GoodSync 的用途和好处
Goodsync 是一个非常流行的备份和文件同步程序,提供以下好处-

  • AES 256 位加密: 大大提高了关键数据的安全性。
  • 你的数据是 实时 传输的,因此不需要用户交互,因为备份和同步是自动和计划的。
  • 由于你的数据在块级别传播,因此备份时间、网络消耗和存储要求非常低。
  • 版本历史控制保证了最短的数据恢复时间和最高的安全性。

SyncToy


SyncToy 是 PowerToys 系列中的 Microsoft 产品。在使用 Microsoft Sync Framework 时,已使用 .NET 框架编写它。使用它时要遵循的方法是你必须创建一个左侧文件夹,即“源”文件夹。下一步是创建将作为目标的正确文件夹。

你可以选择任何文件夹作为你的源或目标文件夹,这使得产品非常用户友好。你可以自由选择 USB 闪存驱动器、网络驱动器或任何便携式硬盘驱动器上的任何文件夹。你不必只使用内部硬盘驱动器。

特征:
新的 SyncToy 2.1 带有一些改进的功能,如下所示:

  • 现在文件复制速度更快,确保更好的性能
  • 它具有更好的错误报告功能。它清楚地显示了在致命错误期间无法同步哪个文件。新版本对文件系统和瞬态网络错误的抵抗力更强
  • 文件夹对配置的自动备份。这使你可以使用备份副本替换 SyncToyDirPairs.bin。这可以帮助你解决上次保存的配置
    官网
    文件大小: 2.9 MB
    系统支持: Windows

FreeFileSync


FreeFileSync 与 Windows、macOS 和 Linux 兼容,是开源软件。你可以使用此软件创建和管理所有重要文件的备份副本。FreeFileSync 在同步文件时采用了一种聪明的方法。它能够发现源文件夹和目标文件夹之间的差异。这样,FreeFileSync 只传输最少的数据,而忽略目标文件夹中的相同数据。

特征:

有一些功能使 FreeFileSync 与 Goodsync 不同:

  • 同步结果会在你的电子邮件中通知
  • 你可以同步任意数量的文件,因为没有人为限制
  • 你可以使用 SFTP 在线访问文件
  • 你还可以使用 MTP 使用移动设备同步文件
  • 你还可以同步 Google Drive 上的文件
    官网
    文件大小: 14.7 MB
    系统支持: Windows、Linux、macOS

Duplicati


你可以使用 Duplicati 将文件存储在在线云服务上。作为备份客户端,Duplici 通过完全加密确保安全备份。备份经过加密和压缩并存储在云存储和远程文件服务器上。你的备份始终使用内置调度程序定期更新。Duplicati 也使用 AES-256 加密。

特征:

  • 你的文件和文件夹备份带有强大的 AES-256 加密。重复数据删除和增量备份可帮助你节省空间。使用内置调度程序和自动更新程序,你可以更新备份。基于 Web 的界面可帮助你在任何机器上运行备份。
  • Duplicati 甚至可以免费用于商业目的,因为它是开源的免费软件。
    强大的 GPG 或 AES-256 加密保护你的隐私
  • Duplicati 可以很好地处理网络问题。Duplicati 对备份进行定期测试,所有中断的备份都会恢复。损坏的存储系统损坏了备份。Duplicati 的这一功能有助于及时检测
  • Duplicati 可以在网络附加存储 (NAS) 上运行。它可以在任何浏览器上运行,甚至可以在手机上运行,​​因此可以从任何地方访问
    官网
    文件大小: 18.4 MB
    系统支持: Windows、macOS、Linux

Rsync


Rsync 涉及最少的数据复制,仅复制文件中已使用算法更改的那部分。它适用于远程同步,涉及远程和本地以及 Linux 和 Unix 系统上的文件的复制和同步。Rsync 今天被广泛用作一种已大大改进的复制命令。它作为备份和镜像软件非常有用。

特征:

  • 你不需要任何超级用户权限
  • 提供匿名或经过身份验证的 rsync 守护进程,非常适合镜像
  • Rsync 可以与任何透明的远程 shell 一起使用,包括 ssh 或 rsh
  • Rsync 带有 exclude 和 exclude-from 选项,使其与 GNU tar 相似
  • 延迟成本最低,因为它通过流水线进行文件传输
    官网
    系统支持: Linux和Unix

Rclone


作为一个命令行程序,Rclone 可让你在 Amazon S3、Dropbox、Google Cloud Storage 等各种平台之间同步文件和目录。Rclone 作为单个二进制文件出现,是一个 Go 程序。它是免费提供的开源软件。

特征:

  • 你的目录可以与同步模式相同
  • 检查模式可帮助你检查所有 MD5SUM
  • 能够与网络同步,例如两个独立的云端硬盘帐户
  • 你只能使用复制模式复制新的或更改的文件
  • 部分同步支持整个文件
  • 文件保留了时间戳
    官网
    文件大小: 11.2 MB
    系统支持: Windows、macOS、Linux、NetBSD、OpenBSD、Plan9、Solaris

PureSync


PureSync 由 Jumping Bytes 推出。这是一个 Windows 程序,可帮助同步文件和文件夹,同时在后台执行自动备份。它也可以在网络卷、外部硬盘驱动器和数码相机上工作。当你将数码相机中的照片导入计算机时,PureSync 会派上用场。

特征:

  • 并排视图比较
  • 自动进行同步和备份
  • 带有许多选项,但仍然是用户友好的
  • 仅用于个人用途时免费
  • 提供FTP 支持以及 NAS 支持
    你也可以考虑像 Clonezilla这样可以轻松备份 Windows PC 的软件。

官网
文件大小: 10.5 MB
系统支持: Windows

Allway Sync


Allway Sync 的工作效率非常高,允许你在所有设备上同步文件的多个副本,无论是 PC、笔记本电脑还是任何可移动驱动器或更多设备。采用真正的双向同步,Always Sync 的算法可以检测你在文件中所做的最新更改,无论是在哪个设备上。通过将数据记录在本地数据库中来确保隐私。它可以免费供个人使用,但有限制。它具有主要功能,例如-

  • 帮助你同步所有设备
  • 这是一个免费软件
  • 它有一个简单的用户界面
  • 它可以防止数据丢失
  • 它会生成一份关于同步文件夹之间差异的报告
    官网
    文件大小:9.5 MB
    系统支持:Windows 2000 及更高版本

odrive


使用 odrive,你可以像使用本地存储的文件一样使用云文件。如果你要对任何文件进行任何更改,这些更改将在云端自动更新。换句话说,在本地文件中所做的更改会自动与云文件同步。

特征:

  • 自动同步文件和文件夹
  • 对于服务器环境,有一个命令行界面
  • Odrive 可用作 Windows、Linux 和 Mac 的无头同步代理
  • 允许将所有文件和文件夹同步到计算机上的单个驱动器文件夹中
  • 确保桌面和云之间的无缝同步
    官网
    系统支持: 所有操作系统

SyncBack


SyncBack 仅备份基于文件的数据。简而言之,它只备份你创建的文件。但是,你不能克隆包含系统文件的整个驱动器。SyncBack free 是一个 32 位程序,可以使用 2GB 的 RAM。

特征:

  • 它提供对外部和内部存储的支持
  • 支持FTP协议
  • 它提供与网络 PC 或设备之间的备份同步
  • 它提供备份、镜像、基本同步和恢复
    官网
    文件大小: 29.9 MB
    系统支持: Windows

OpenDrive

OpenDrive 为个人、企业和企业使用提供云存储、备份和同步功能。凭借其对各种操作系统的支持,你可以轻松访问、管理和共享你的数据,而无需担心。

特征:

  • 毫不费力地将你的任何文件链接或嵌入云中,以便在电子邮件、网站、消息或社交网站中使用。
  • 高度安全的云存储,具有即时访问、共享和协作功能,界面简单易用。
  • 在任何地方(无论是计算机还是外部存储设备)永久或临时同步文件。
  • 将用户添加到你的 OpenDrive 帐户,让他们直接访问文件。
    官方网站

文件大小: 11 MB

支持的系统: Windows、Mac、Linux、Android、Web、iOS

DirSync Pro

DirSync Pro是一款免费且功能强大的文件和文件夹同步实用程序。DirSync 支持一个或多个文件夹内容的递归同步。此应用程序的 UI 易于使用,同时为你提供大量选项和功能。DirSync Pro 是用 Java 编写的,这就是它快速且健壮的原因。

特征-

  • 你可以通过进行增量备份来节省时间。
  • DirSync Pro 提供镜像、备份、贡献等预配置模式。
  • DirSync Pro 让你轻松安排同步任务。
  • DirSync Pro 完全免费,没有广告和商业文字。你可以不受功能限制地使用它。
    官方网站

文件大小: 2.8 MB

支持的系统: Windows、Mac OS、Linux、DOS