摘要内容……

阅读全文 »
l

一个函数秒杀 2Sum 3Sum 4Sum 问题

经常刷 LeetCode 的读者肯定知道鼎鼎有名的 twoSum 问题,我们的旧文 Two Sum 问题的核心思想 对 twoSum 的几个变种做了解析。

但是除了 twoSum 问题,LeetCode 上面还有 3Sum,4Sum 问题,我估计以后出个 5Sum,6Sum 也不是不可能。

那么,对于这种问题有没有什么好办法用套路解决呢?本文就由浅入深,层层推进,用一个函数来解决所有 nSum 类型的问题。

一、twoSum 问题

[leetcode1](1. 两数之和 - 力扣(LeetCode) (leetcode-cn.com))

力扣上的 twoSum 问题,题目要求返回的是索引,这里我来编一道 twoSum 题目,不要返回索引,返回元素的值:

如果假设输入一个数组 nums 和一个目标和 target,请你返回 nums 中能够凑出 target 的两个元素的值,比如输入 nums = [5,3,1,6], target = 9,那么算法返回两个元素 [3,6]。可以假设只有且仅有一对儿元素可以凑出 target。

我们可以先对 nums 排序,然后利用前文「双指针技巧汇总」写过的左右双指针技巧,从两端相向而行就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vector<int> twoSum(vector<int>& nums, int target) {
// 先对数组排序
sort(nums.begin(), nums.end());
// 左右指针
int lo = 0, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 根据 sum 和 target 的比较,移动左右指针
if (sum < target) {
lo++;
} else if (sum > target) {
hi--;
} else if (sum == target) {
return {nums[lo], nums[hi]};
}
}
return {};
}

这样就可以解决这个问题,不过我们要继续魔改题目,把这个题目变得更泛化,更困难一点:

nums 中可能有多对儿元素之和都等于 target,请你的算法返回所有和为 target 的元素对儿,其中不能出现重复。

函数签名如下:

1
vector<vector<int>> twoSumTarget(vector<int>& nums, int target);

比如说输入为 nums = [1,3,1,2,2,3], target = 4,那么算法返回的结果就是:[[1,3],[2,2]]。

对于修改后的问题,关键难点是现在可能有多个和为 target 的数对儿,还不能重复,比如上述例子中 [1,3] 和 [3,1] 就算重复,只能算一次。

首先,基本思路肯定还是排序加双指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vector<vector<int>> twoSumTarget(vector<int>& nums, int target {
// 先对数组排序
sort(nums.begin(), nums.end());
vector<vector<int>> res;
int lo = 0, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 根据 sum 和 target 的比较,移动左右指针
if (sum < target) lo++;
else if (sum > target) hi--;
else {
res.push_back({lo, hi});
lo++; hi--;
}
}
return res;
}

但是,这样实现会造成重复的结果,比如说 nums = [1,1,1,2,2,3,3], target = 4,得到的结果中 [1,3] 肯定会重复。

出问题的地方在于 sum == target 条件的 if 分支,当给 res 加入一次结果后,lo 和 hi 不应该改变 1 的同时,还应该跳过所有重复的元素:

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 记录索引 lo 和 hi 最初对应的值
int left = nums[lo], right = nums[hi];
if (sum < target) lo++;
else if (sum > target) hi--;
else {
res.push_back({left, right});
// 跳过所有重复的元素
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}

这样就可以保证一个答案只被添加一次,重复的结果都会被跳过,可以得到正确的答案。不过,受这个思路的启发,其实前两个 if 分支也是可以做一点效率优化,跳过相同的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vector<vector<int>> twoSumTarget(vector<int>& nums, int target) {
// nums 数组必须有序
sort(nums.begin(), nums.end());
int lo = 0, hi = nums.size() - 1;
vector<vector<int>> res;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push_back({left, right});
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
return res;
}

这样,一个通用化的 twoSum 函数就写出来了,请确保你理解了该算法的逻辑,我们后面解决 3Sum 和 4Sum 的时候会复用这个函数。

这个函数的时间复杂度非常容易看出来,双指针操作的部分虽然有那么多 while 循环,但是时间复杂度还是 O(N),而排序的时间复杂度是 O(NlogN),所以这个函数的时间复杂度是 O(NlogN)。

二、3Sum 问题

Leetcode15

这是力扣第 15 题「三数之和」:

图片

题目就是让我们找 nums 中和为 0 的三个元素,返回所有可能的三元组(triple),函数签名如下:

1
vector<vector<int>> threeSum(vector<int>& nums);

这样,我们再泛化一下题目,不要光和为 0 的三元组了,计算和为 target 的三元组吧,同上面的 twoSum 一样,也不允许重复的结果:

1
2
3
4
5
6
7
8
vector<vector<int>> threeSum(vector<int>& nums) {
// 求和为 0 的三元组
return threeSumTarget(nums, 0);
}

vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
// 输入数组 nums,返回所有和为 target 的三元组
}

这个问题怎么解决呢?很简单,穷举呗。现在我们想找和为 target 的三个数字,那么对于第一个数字,可能是什么?nums 中的每一个元素 nums[i] 都有可能!

那么,确定了第一个数字之后,剩下的两个数字可以是什么呢?其实就是和为 target - nums[i] 的两个数字呗,那不就是 twoSum 函数解决的问题么🤔

可以直接写代码了,需要把 twoSum 函数稍作修改即可复用:

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
/* 从 nums[start] 开始,计算有序数组
* nums 中所有和为 target 的二元组 */
vector<vector<int>> twoSumTarget(
vector<int>& nums, int start, int target) {
// 左指针改为从 start 开始,其他不变
int lo = start, hi = nums.size() - 1;
vector<vector<int>> res;
while (lo < hi) {
...
}
return res;
}

/* 计算数组 nums 中所有和为 target 的三元组 */
vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
// 数组得排个序
sort(nums.begin(), nums.end());
int n = nums.size();
vector<vector<int>> res;
// 穷举 threeSum 的第一个数
for (int i = 0; i < n; i++) {
// 对 target - nums[i] 计算 twoSum
vector<vector<int>>
tuples = twoSumTarget(nums, i + 1, target - nums[i]);
// 如果存在满足条件的二元组,再加上 nums[i] 就是结果三元组
for (vector<int>& tuple : tuples) {
tuple.push_back(nums[i]);
res.push_back(tuple);
}
// 跳过第一个数字重复的情况,否则会出现重复结果
while (i < n - 1 && nums[i] == nums[i + 1]) i++;
}
return res;
}

需要注意的是,类似 twoSum,3Sum 的结果也可能重复,比如输入是 nums = [1,1,1,2,3], target = 6,结果就会重复。

关键点在于,不能让第一个数重复,至于后面的两个数,我们复用的 twoSum 函数会保证它们不重复。所以代码中必须用一个 while 循环来保证 3Sum 中第一个元素不重复。

至此,3Sum 问题就解决了,时间复杂度不难算,排序的复杂度为 O(NlogN),twoSumTarget 函数中的双指针操作为 O(N),threeSumTarget 函数在 for 循环中调用 twoSumTarget 所以总的时间复杂度就是 O(NlogN + N^2) = O(N^2)。

三、4Sum 问题

Leetcode18

这是力扣第 18 题「四数之和」:

图片

函数签名如下:

1
vector<vector<int>> fourSum(vector<int>& nums, int target);

都到这份上了,4Sum 完全就可以用相同的思路:穷举第一个数字,然后调用 3Sum 函数计算剩下三个数,最后组合出和为 target 的四元组。

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
vector<vector<int>> fourSum(vector<int>& nums, int target) {
// 数组需要排序
sort(nums.begin(), nums.end());
int n = nums.size();
vector<vector<int>> res;
// 穷举 fourSum 的第一个数
for (int i = 0; i < n; i++) {
// 对 target - nums[i] 计算 threeSum
vector<vector<int>>
triples = threeSumTarget(nums, i + 1, target - nums[i]);
// 如果存在满足条件的三元组,再加上 nums[i] 就是结果四元组
for (vector<int>& triple : triples) {
triple.push_back(nums[i]);
res.push_back(triple);
}
// fourSum 的第一个数不能重复
while (i < n - 1 && nums[i] == nums[i + 1]) i++;
}
return res;
}

/* 从 nums[start] 开始,计算有序数组
* nums 中所有和为 target 的三元组 */
vector<vector<int>>
threeSumTarget(vector<int>& nums, int start, int target) {
int n = nums.size();
vector<vector<int>> res;
// i 从 start 开始穷举,其他都不变
for (int i = start; i < n; i++) {
...
}
return res;

这样,按照相同的套路,4Sum 问题就解决了,时间复杂度的分析和之前类似,for 循环中调用了 threeSumTarget 函数,所以总的时间复杂度就是 O(N^3)。

四、100Sum 问题?

在 LeetCode 上,4Sum 就到头了,但是回想刚才写 3Sum 和 4Sum 的过程,实际上是遵循相同的模式的。我相信你只要稍微修改一下 4Sum 的函数就可以复用并解决 5Sum 问题,然后解决 6Sum 问题……

那么,如果我让你求 100Sum 问题,怎么办呢?其实我们可以观察上面这些解法,统一出一个 nSum 函数:

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
/* 注意:调用这个函数之前一定要先给 nums 排序 */
vector<vector<int>> nSumTarget(
vector<int>& nums, int n, int start, int target) {

int sz = nums.size();
vector<vector<int>> res;
// 至少是 2Sum,且数组大小不应该小于 n
if (n < 2 || sz < n) return res;
// 2Sum 是 base case
if (n == 2) {
// 双指针那一套操作
int lo = start, hi = sz - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push_back({left, right});
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// n > 2 时,递归计算 (n-1)Sum 的结果
for (int i = start; i < sz; i++) {
vector<vector<int>>
sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (vector<int>& arr : sub) {
// (n-1)Sum 加上 nums[i] 就是 nSum
arr.push_back(nums[i]);
res.push_back(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}

嗯,看起来很长,实际上就是把之前的题目解法合并起来了,n == 2 时是 twoSum 的双指针解法,n > 2 时就是穷举第一个数字,然后递归调用计算 (n-1)Sum,组装答案。

需要注意的是,调用这个 nSum 函数之前一定要先给 nums 数组排序,因为 nSum 是一个递归函数,如果在 nSum 函数里调用排序函数,那么每次递归都会进行没有必要的排序,效率会非常低。

比如说现在我们写 LeetCode 上的 4Sum 问题:

1
2
3
4
5
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
// n 为 4,从 nums[0] 开始计算和为 target 的四元组
return nSumTarget(nums, 4, 0, target);
}

再比如 LeetCode 的 3Sum 问题,找 target == 0 的三元组:

1
2
3
4
5
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
// n 为 3,从 nums[0] 开始计算和为 0 的三元组
return nSumTarget(nums, 3, 0, 0);
}

那么,如果让你计算 100Sum 问题,直接调用这个函数就完事儿了。

l

1
2
3
4
5
6
7
cmd输入:

reg add HKCU\Software\Microsoft\InputMethod\Settings\CHS /v UserDefinedDoublePinyinScheme0 /t REG_SZ /d "小鹤双拼*2*^*iuvdjhcwfg^xmlnpbksqszxkrltvyovt"

PowerShell输入:

Set-ItemProperty -LiteralPath 'HKCU:\Software\Microsoft\InputMethod\Settings\CHS' -Name 'UserDefinedDoublePinyinScheme0' -Type String -Value '小鹤双拼*2*^*iuvdjhcwfg^xmlnpbksqszxkrltvyovt'
l

MobaXterm破解版下载安装教程

MobaXterm是一款功能十分强大的远程控制的工具,集串口,SSH远程登录和FTP传输功能于一体,通过该软件你可以进行远程计算、显示系统网络设置、进行远程文件传输、执行网络监控,可以同时连接多个终端ip。目前软件采用了scp协议,按照ssh路径调整远程主机区域,帮助您轻松建立多个共享系统,从而可以更加方便的链接远程设备们提高工作效率。

同时MobaXterm还支持多模式连接,你可以选择全局连接,也可以选择分割式连接,方便您跟您自己的工作类型来设置相对应的远程控制模式,从而大大提升您的工作效率。除此之外,软件内建多标签和多终端分屏,采用sftp文件传输,还支持运程运行X窗口程序,以其强大的功能而深受大众的喜爱。

本文提供MobaXterm v24.3专业版下载安装教程,软件已汉化
详细安装教程参考下文操作

1. 下载、解压文件

1.1 下载地址:

点击下载:MobaXterm v24.3软件包和注册机 (访问码:n8lx)

1.2 解压程序包和注册机文件

image

得到2个文件夹
image

2. 注册专业版

2.1 运行注册机

拷贝注册机至MobaXterm目录
image

2.2 获取程序版本

双击运行注册机
MobaXterm_Keygen_v1.5_By_DeltaFoX.exe
image

image

2.3 生成许可文件

image

最后

双击打开MobaXterm.exe
查看 “帮助” - “关于MobaXterm”

image

恭喜你,安装破解完成

高分屏显示少字,错位,不显示的问题解决方法

1、mobexterm安装路径下,鼠标右键Mobexterm选择属性;

2、弹出窗口“兼容性”——“更改高DPI设置”;

3、在高DPI缩放替代栏,勾选“替代高DPI缩放行为。”,在缩放执行下拉列表内勾选“系统”(或系统增强)——点击“确定”完成设置。

4、重新打开Mobexterm显示恢复正常。

l

在Ubuntu下安装CUPS使惠普打印机HP M1005实现局域网手机电脑Airprint无线打印

环境:Ubuntu20
设备:HP M1005
局域网设置静态路由表,使192.168.100.x-192.168.127.x实现跨网互访。

以下操作均使用ROOT用户身份并开启SSH登录选项。如未开启,则先手动执行以下命令开启ROOT身份的SSH选项。

1
2
3
4
5
6
7
8
vim /etc/ssh/sshd_config
找到如下段落修改PermitRootLogin为YES并去除注释
修改完毕后如下图所示,Esc退出:wq!保存。
#LoginGraceTime 2m
PermitRootLogin yes
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10

接下来执行安装CUPS软件

1
2
3
apt-get install hplip
apt-get install hpijs-ppds -y
apt-get install cups -y

手动修改CUPS访问权限

1
cupsctl --remote-admin --remote-any --share-printers

此时,你的CUPS已经可以正常访问,在浏览器输入地址https://ip:631回车访问,**一定使用SSH的root账号密码登录**。

imgCUPS界面

安装hp插件

1
2
hp-plugin
一路YES

img

如下载慢卡在0%,可下载链接内博主已经上传完毕的文件,放入/root根目录,并选择p,手动指定/root路径进行安装。下载地址(访问码:ay6x)
当提示Done并且无报错时,则表示惠普插件安装完毕。

现在,去浏览器界面找到Administration标签,点击add printert添加新的打印机。如果提示已禁止,则证明此时你使用的登录身份不是root,所以无法添加。

img

img

接下来在Local Printers选择你的打印机,并点击Continue。在下一步的页面中自定义打印机的名称,并勾选Share,否则打印机将无法被他人可见。

img

然后再最后这个页面选择和你打印机相同的驱动,一定选择带hpijs后缀的驱动。

img

然后一台打印机就展现在你的打印机列表里了,现在选择Print Test Page,看看打印机是不是响起来了!?

img

接下来在电脑和手机添加打印机地址,就可以实现局域网内无线打印啦!
以上就是在Ubuntu下安装CUPS使惠普打印机HP M1005实现局域网手机电脑Airprint无线打印的方法。

l

使用 Clash 时使用的 Steam 规则

简介

在使用梯子时,Clash Verge 的自定义规则可能时常不好用,经常导致 Steam 连接到国外服务器,导致国内下载很慢 / 耗流量。

本文使用 Clash Verge 2.0.3 来编写 Steam 规则。

配置全局扩展脚本

我们可以通过 Clash 的 全局扩展脚本(通常在配置文件中名为 rule-providers 和 rules 部分)来精细控制 Steam 的走代理行为,避免它连接到国外服务器下载游戏。

在主界面找到【全局扩展脚本】,右键,编辑文件,复制下面内容覆盖原文件

1
2
3
4
5
6
7
8
9
10
11
12
const prependRules = [
// lan ip
"DOMAIN,steamcdn-a.akamaihd.net,DIRECT",
"DOMAIN-SUFFIX,cm.steampowered.com,DIRECT",
"DOMAIN-SUFFIX,steamserver.net,DIRECT",
"DOMAIN-SUFFIX,steamcontent.com,DIRECT"
];

function main(config, profileName) {
config.rules = prependRules.concat(config.rules);
return config;
}

你可以将 Steam 社区和商店页面指定走代理,但下载相关域名设为直连。

这里的 DIRECT,可以替换为你所需要的,如果你想用代理下载,可以替换为你的策略组名字。

验证规则是否生效

在 Clash Verge 中打开「连接」界面,观察访问 store.steampowered.com 时是否走代理,而 content.steampowered.com 是否走直连即可。

你也可以查看 Clash 的日志输出,确认每个域名是否命中预期规则。

小贴士

  • Steam CDN 域名大多为 steamcontent.com 和 steamusercontent.com,这部分建议走直连。
  • steamcommunity.com 等社区功能如果不走代理可能无法加载头像或聊天功能。
l

zsh必备插件

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
#!/bin/bash
if [ "$EUID" -eq 0 ]; then
echo "请勿使用 sudo 或以 root 身份运行此脚本。请以普通用户执行。"
exit 1
fi
set -e # 遇到错误立即退出

# 安装依赖
sudo yum install -y git zsh

# 修改当前用户的登录 shell
chsh -s "$(which zsh)"

# 自动安装 Oh My Zsh(非交互、不自动启动 zsh)
export RUNZSH=no
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended


ZSH_CUSTOM_DIR="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
mkdir -p "$ZSH_CUSTOM_DIR/plugins"
# 克隆插件(浅克隆)
git clone --depth=1 https://github.com/zsh-users/zsh-syntax-highlighting.git "$ZSH_CUSTOM_DIR/plugins/zsh-syntax-highlighting"
git clone --depth=1 https://github.com/paulirish/git-open.git "$ZSH_CUSTOM_DIR/plugins/git-open"
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions.git "$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions"

# 修改插件列表(使用转义后的 sed 模式)
sed -i.bak1 's/plugins=\([^)]*\)/plugins=(cp git zsh-autosuggestions zsh-syntax-highlighting sudo git-open brew extract z)/' ~/.zshrc

# 安装 powerlevel10k 主题
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$ZSH_CUSTOM_DIR/themes/powerlevel10k"
echo 'source $ZSH_CUSTOM_DIR/themes/powerlevel10k/powerlevel10k.zsh-theme' >>~/.zshrc
source ~/.zshrc
# 启动 zsh(替换当前 shell)
exec zsh

l

#Hadoop基础

1. Hadoop 由哪些核心组件组成,每个组件有什么特性?

Hadoop 的核心组件包括 HDFS、MapReduce、YARN 以及 Hadoop Common。

  • HDFS(Hadoop Distributed File System)
    • 特性:
      • 分布式存储,将大文件切分为固定大小的块(默认 128MB)分散存储在多台机器上。
      • 高容错:数据块自动复制多份(默认 3 份),副本分布在不同节点,坏块可自动恢复。
      • 主/从架构:一个 NameNode(管理命名空间和块映射)和多个 DataNode(实际存储数据块)。
      • 一次写入、多次读取(不支持随机修改),适合批处理场景。
      • 适合存储超大文件(TB/PB 级)。
  • MapReduce
    • 特性:
      • 分布式计算框架,将任务分为 Map(映射)和 Reduce(规约)两个阶段。
      • 数据本地性:尽量将计算移动到数据所在节点,减少网络传输。
      • 容错:单个任务失败会自动在其他节点重试。
      • 编程模型简单,但中间结果落盘,不适合低延迟交互式查询。
  • YARN(Yet Another Resource Negotiator)
    • 特性:
      • 资源管理和作业调度平台(Hadoop 2.x 引入)。
      • 将资源管理与作业监控分离:
        • ResourceManager:全局资源调度。
        • NodeManager:每节点资源与任务管理。
        • ApplicationMaster:每个作业一个,负责申请资源、监控任务执行。
      • 支持多种计算框架(MapReduce、Spark、Flink 等)运行在同一集群。
  • Hadoop Common
    • 特性:
      • 提供其他模块公用的工具类、文件系统抽象、RPC 框架、序列化库等。

2. Hadoop 2.x 和 Hadoop 1.x 的架构有什么不同?

方面 Hadoop 1.x Hadoop 2.x
资源管理 JobTracker 同时负责资源管理和作业调度,存在单点故障和扩展性瓶颈 YARN 将资源管理与作业监控分离:ResourceManager + NodeManager + ApplicationMaster
作业跟踪 MapReduce 作业由 JobTracker 统一跟踪,任务过多时压力巨大 ApplicationMaster 负责单个作业的生命周期,ResourceManager 只负责分配资源
支持框架 仅支持 MapReduce 支持 MapReduce、Spark、Flink、Tez 等多种框架
扩展性 集群规模受限(JobTracker 内存压力),通常不超过 4000 节点 可扩展至上万节点,ResourceManager 可水平扩展(HA)
容错 JobTracker 故障导致所有作业失败 ResourceManager 和 ApplicationMaster 均有 HA 方案,单点故障影响小

简单记忆:
1.x = HDFS + MapReduce(紧耦合,资源管理在 MapReduce 内)。
2.x = HDFS + YARN + MapReduce(YARN 独立管理资源,MapReduce 降为运行在 YARN 上的应用)。


3. Hadoop 可以解决什么问题?

Hadoop 主要用于解决海量数据(TB/PB 级)的存储与处理问题,适合以下场景:

  1. 海量日志分析:如网站点击流、服务器日志,通过 MapReduce 进行清洗、聚合、统计。
  2. 数据仓库 ETL:将非结构化/半结构化数据(JSON、CSV、文本)转换加载到数据仓库。
  3. 离线批处理:大型排序、倒排索引、机器学习训练数据预处理等。
  4. 低成本存储:使用普通服务器组成廉价集群,相比 NAS/SAN 成本大幅降低。
  5. 数据归档与备份:利用多副本机制提供高可靠性。
  6. 大规模图计算(配合 Giraph 等)或 NoSQL 数据库(HBase on HDFS)。

不适合:低延迟随机读写(改用 HBase 或 Kudu)、实时流处理(改用 Spark Streaming/Flink)、事务性要求高的场景。


4. 使用一台 Linux 机器实现 Hadoop 伪分布集群安装

伪分布模式:所有守护进程(NameNode、DataNode、ResourceManager、NodeManager)运行在同一台机器上,但每个进程独立 JVM,模拟分布式行为。

步骤概览(以 Ubuntu/CentOS 为例,Hadoop 3.x):

  1. 安装 JDK(1.8 或 11)

    bash

    1
    2
    sudo apt install openjdk-8-jdk   # Ubuntu
    java -version
  2. 设置 SSH 免密登录(本机)

    bash

    1
    2
    3
    4
    ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
    cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
    chmod 600 ~/.ssh/authorized_keys
    ssh localhost # 测试免密
  3. 下载并解压 Hadoop

    bash

    1
    2
    3
    wget https://archive.apache.org/dist/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
    tar -xzf hadoop-3.3.6.tar.gz -C /usr/local/
    cd /usr/local/hadoop-3.3.6
  4. 修改环境变量(~/.bashrc 或 /etc/profile)

    bash

    1
    2
    3
    4
    export HADOOP_HOME=/usr/local/hadoop-3.3.6
    export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
    export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 # 根据实际修改
    source ~/.bashrc
  5. 修改 Hadoop 配置文件(位于 $HADOOP_HOME/etc/hadoop)

    • core-site.xml

      xml

      1
      2
      3
      4
      5
      6
      <configuration>
      <property>
      <name>fs.defaultFS</name>
      <value>hdfs://localhost:9000</value>
      </property>
      </configuration>
    • hdfs-site.xml

      xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <configuration>
      <property>
      <name>dfs.replication</name>
      <value>1</value> <!-- 伪分布只需1个副本 -->
      </property>
      <property>
      <name>dfs.namenode.name.dir</name>
      <value>/usr/local/hadoop-3.3.6/data/namenode</value>
      </property>
      <property>
      <name>dfs.datanode.data.dir</name>
      <value>/usr/local/hadoop-3.3.6/data/datanode</value>
      </property>
      </configuration>
    • mapred-site.xml

      xml

      1
      2
      3
      4
      5
      6
      <configuration>
      <property>
      <name>mapreduce.framework.name</name>
      <value>yarn</value>
      </property>
      </configuration>
    • yarn-site.xml

      xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <configuration>
      <property>
      <name>yarn.nodemanager.aux-services</name>
      <value>mapreduce_shuffle</value>
      </property>
      <property>
      <name>yarn.resourcemanager.hostname</name>
      <value>localhost</value>
      </property>
      </configuration>
  6. 设置 Hadoop 环境变量文件(可选 hadoop-env.sh 中指定 JAVA_HOME)

  7. 格式化 NameNode(仅首次执行)

    bash

    1
    hdfs namenode -format
  8. 启动 Hadoop 服务

    bash

    1
    2
    start-dfs.sh   # 启动 HDFS (NameNode + DataNode)
    start-yarn.sh # 启动 YARN (ResourceManager + NodeManager)

    或一键启动 start-all.sh(已弃用,建议分开)。

  9. 验证进程

    bash

    1
    jps

    应看到:NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager。

  10. 访问 Web 界面

    • HDFS 管理:http://localhost:9870(Hadoop 3.x)
    • YARN 资源管理:http://localhost:8088
  11. 测试运行(例如执行自带 wordcount)

    bash

    1
    2
    3
    4
    hdfs dfs -mkdir /input
    hdfs dfs -put $HADOOP_HOME/etc/hadoop/*.xml /input
    hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar wordcount /input /output
    hdfs dfs -cat /output/part-r-00000

5. 安装 Hadoop 集群需要修改哪几个核心 xml 配置文件?

在一台或多台机器的集群安装中,通常需要修改以下 4 个核心 XML 文件(位于 $HADOOP_HOME/etc/hadoop/):

文件 作用 关键配置示例
core-site.xml Hadoop 核心通用配置 fs.defaultFS(HDFS 访问地址)
hdfs-site.xml HDFS 专属配置 dfs.replication(副本数)、dfs.namenode.name.dir、dfs.datanode.data.dir
mapred-site.xml MapReduce 配置 mapreduce.framework.name(设为 yarn)
yarn-site.xml YARN 资源管理配置 yarn.resourcemanager.hostname、yarn.nodemanager.aux-services

此外,有时还需要修改:

  • **workers**(旧名 slaves):列出所有 DataNode 和 NodeManager 的主机名(每行一个)。
  • **hadoop-env.sh**:设置 JAVA_HOME 环境变量。
  • **yarn-env.sh**:可选的 YARN 环境变量。

对于伪分布模式,workers 文件里只写 localhost 即可。


如果你需要更详细的安装教程(比如完全分布式集群的配置)或对每个配置项的解释,我可以进一步展开说明。

l

Linux 安装 quarkpan v0.4.0

Command-line client for Quark Drive based on libquarkpan

Readme4 VersionsDependenciesDependentsSecurity

quarkpan

quarkpan 是基于 libquarkpan 的夸克网盘命令行工具。

它是面向直接使用终端的场景,负责把底层库能力包装成可恢复、可观察、可取消的命令行体验。

功能概览

当前命令行支持:

  • auth 管理持久化 Cookie
  • list 列出目录内容
  • folder create 创建目录
  • delete 删除一个或多个文件或目录项
  • rename 重命名文件或目录项
  • download 下载文件
  • download-dir 下载整个目录
  • upload 上传文件
  • upload-dir 上传整个目录

同时支持:

  • 仅在交互式 TTY 中显示进度条
  • --color auto|always|never
  • Ctrl+C 取消
  • 中断后保留 .quark.task 任务文件
  • 下载中断后的显式重试与续传
  • 上传分片失败后的显式重试
  • 下次通过 -c, --continue 恢复传输

TLS Features

quarkpan 会把底层 libquarkpan 的 TLS backend feature 透传出来,命名与 reqwest 0.13 对齐,默认使用 default-tls。

示例:

1
2
cargo install quarkpan
cargo install quarkpan --no-default-features --features native-tls

安装

使用 Cargo

1
cargo install quarkpan

从源码运行

1
cargo run --bin quarkpan -- --help

CLI 需要夸克登录后的 Cookie,支持三种方式:

  • --cookie 'k1=v1; k2=v2'
  • --cookie-file ./cookie.txt
  • 环境变量 QUARK_COOKIE
  • auth set-cookie 持久化到系统配置目录

Cookie 内容应为完整的 key=value; key2=value2 格式。 auth set-cookie 需要显式指定来源:

  • --cookie
  • --from-stdin
  • --from-nano
  • --from-vi

使用示例

列出根目录

1
quarkpan --cookie 'k1=v1; k2=v2' list
1
2
quarkpan auth set-cookie --from-stdin
quarkpan auth show-source

也可以显式传入:

1
quarkpan auth set-cookie --cookie 'k1=v1; k2=v2'

也支持通过标准输入:

1
printf 'k1=v1; k2=v2\n' | quarkpan auth set-cookie --from-stdin

如果直接在终端运行:

1
quarkpan auth set-cookie --from-stdin

CLI 会先提示:

1
paste cookie, then press Enter:

也支持通过编辑器:

1
2
quarkpan auth set-cookie --from-nano
quarkpan auth set-cookie --from-vi

逐页查看更多

1
quarkpan --cookie 'k1=v1; k2=v2' list --pdir-fid 0 --more

创建目录

1
quarkpan --cookie 'k1=v1; k2=v2' folder create --pdir-fid 0 --file-name 我的文档

重命名文件或目录项

1
quarkpan --cookie 'k1=v1; k2=v2' rename --fid <fid> --file-name 新名字

删除一个或多个文件或目录项

1
quarkpan --cookie 'k1=v1; k2=v2' delete --fid <fid1> --fid <fid2>

下载文件

1
quarkpan --cookie 'k1=v1; k2=v2' download --fid <fid> --output ./file.bin

恢复下载

1
quarkpan --cookie 'k1=v1; k2=v2' download --fid <fid> --output ./file.bin -c

下载目录

1
quarkpan download-dir --pdir-fid <pdir_fid> --output ./backup

恢复或合并目录下载

1
2
quarkpan download-dir --pdir-fid <pdir_fid> --output ./backup -c
quarkpan download-dir --pdir-fid <pdir_fid> --output ./backup -c -o

上传文件

1
quarkpan --cookie 'k1=v1; k2=v2' upload --file ./file.bin --pdir-fid 0

恢复上传

1
quarkpan --cookie 'k1=v1; k2=v2' upload --file ./file.bin --pdir-fid 0 -c

上传目录

1
quarkpan upload-dir --dir ./photos --pdir-fid 0

恢复或合并目录上传

1
2
quarkpan upload-dir --dir ./photos --pdir-fid 0 -c
quarkpan upload-dir --dir ./photos --pdir-fid 0 -c -o

任务文件说明

下载和上传在中断、报错或收到 Ctrl+C 后,会保留:

1
${filename}.quark.task

目录任务使用与目录同级同名的任务文件,例如:

1
photos.quark.task

用途:

  • 下载时记录远端文件身份信息和目标输出路径
  • 上传时记录预检得到的上传会话信息和已完成的分片状态

成功完成后,任务文件会自动删除。

Ctrl+C 时:

  • 当前传输会尽快停止
  • 已生成的任务文件会保留
  • 不会把取消误当作普通失败继续重试
  • 后续可使用 -c 恢复

进度条显示规则:

  • 只在交互式终端显示
  • 定时任务、管道和重定向默认不显示
  • 下载和上传都会显示当前文件名

当前限制

  • 目录和文件操作当前以 ID 为主
  • 下载当前只支持按文件 ID
  • CLI 当前不负责复杂的路径递归解析
  • 上传恢复依赖本地文件内容未变化,因此继续上传前会重新校验 size/md5/sha1
  • 云端覆盖当前通过删除旧文件后再上传实现
  • CLI 已移除 --json 输出模式

License

1
GPL-3.0-only
l

Hexo博客文章加密

前言

平时开发过程中遇到的一些问题,我都会整理到文档中。有些感觉不错的,会二次整理成文章发布到我的博客中。但是有些文章如果存在隐私内容,或者不打算公开的话,就不能放在博客中了。

我的博客是使用 Hexo 来搭建的,并不能设置某些文章不可见。但如果不在电脑旁或者出门没有带电脑又想要查看一下之前记录的内容,就很不方便了。

我也尝试在 github 上去找一些可以设置账户的开源的博客框架,但测试过一些后发现并没有符合自己需求的,而自己开发却没有时间。

思来想去,就想看看有没有插件能够实现 Hexo 博客的加密操作。最终让我找到了一款名为 Hexo-Blog-Encrypt 的插件。

为了防止以下的修改可能出现版本差异,这里我先声明我使用的 Hexo 版本信息:

1
2
3
4
hexo: 4.2.1
hexo-cli: 3.1.0
next theme version: 7.8.0+a7a948a
hexo-blog-encrypt: "^3.1.6"

插件安装

1
npm install --save hexo-blog-encrypt

快速使用

该插件的使用也很方便,这里我仅作简单介绍,详细的可以查看官方文档。 D0n9X1n/hexo-blog-encrypt: Yet, just another hexo plugin for security.

要为一篇文章添加密码查看功能,只需要在文章信息头部添加 password 字段即可:

1
2
3
4
5
---
title: hello world
date: 2021-04-13 21:18:02
password: hello
---

全局加密配置

分别为每篇文章设置密码,虽然很灵活,但是配置或者修改起来非常麻烦。为此,可以通过设置统一配置来实现全局加密。

通过添加指定 tag 的方式,可以为所有需要加密的文章添加统一加密操作。只需要在需要加密的文章中,添加设置的 tag值 即可。

在Hexo主配置文件 _config.yml 中添加如下配置:

1
2
3
4
5
6
7
8
9
# Security
encrypt: # hexo-blog-encrypt
silent: true
abstract: 这是一篇加密文章,需要密码才能继续阅读。
message: 当前文章暂不对外可见,请输入密码后查看!
tags:
- {name: private, password: hello}
wrong_pass_message: 抱歉,您输入的密码错误,请检查后重新输入。
wrong_hash_message: 抱歉, 当前文章不能被校验, 不过您还是可以看看解密后的内容。

之后,需要清除缓存后重新生成 hexo clean && hexo s -g。

其中的 tag 部分:

1
2
tags:
- {name: private, password: hello}

表示当在文章中指定了 private 这个 tag 后,该文章就会自动加密并使用对应的值 hello 作为密码,输入密码后才可查看。

相应的文章头部设置:

1
2
3
4
5
6
---
title: Password Test
date: 2019-12-21 11:54:07
tags:
- private
---

在全局加密配置下禁用某些文章的加密

可能有这样的情况,属于 private 标签下的某篇文章在一段时间内想要开放访问。如果在描述中加上密码提示: 当前文章密码为xxx,请输入密码后查看 ,来让用户每次查看时都要先输入密码后再查看,这样的操作又会给访客带来不便。

这时可以单独设置允许某篇文章不设置密码。

只需要在使用 加密tag 的前提下,结合 password 来实现即可。在博客文章的头部添加 password 并设置为 "" 就能取消当前文章的 Tag 加密。

相应的设置示例如下:

1
2
3
4
5
6
7
---
title: No Password Test
date: 2019-12-21 11:54:07
tags:
- private
password: ""
---

在全局加密配置下设置非全局密码

在全局加密配置下,我们可以通过设置多个 加密tag 来为多篇不同类型的文章设置相同的查看密码:

1
2
3
4
tags:
- {name: private, password: hello}
- {name: jiami, password: world}
- {name: 加密, password: jiesuo}

那么可能有这样的场景:

属于 private 标签下的某篇文章想要设置成不一样的密码,防止用户恶意通过一个密码来查看同标签下的所有文章。此时,仍可以通过 password 参数来实现:

1
2
3
4
5
6
7
---
title: Password Test
date: 2019-12-21 11:54:07
tags:
- private
password: "buyiyang"
---

说明:

该文章通过tag值 private 做了加密,按说密码应该为 hello ,但是又在信息头中设置了 password ,因为配置的优先级是 文章信息头 > 按标签加密,所以最后的密码为 buyiyang 。


解密后目录不显示

在为某些文章设置了 加密后查看 之后,不经意间发现这些文章的目录在解密后却不显示了。

探究原因

从插件的 github issues 中我找到了相关的讨论:

原因:

加密的时候,post.content 会变成加密后的串,所以原来的 TOC 生成逻辑就会针对加密后的内容。
所以这边我只能把原来的内容存进 post.origin 字段。

找到文件 themes/next/layout/_macro/sidebar.swig ,编辑如下部分:

20210418165143

20210418165143

插件 hexo-blog-encrypt 对文章内容进行加密后,会将原始文章内容保存到字段 origin 中,当生成 TOC 时,我们可以通过 page.origin 来得到原始内容,生成文章目录。

相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<aside class="sidebar">
<div class="sidebar-inner">

{%- set display_toc = page.toc.enable and display_toc %}
{%- if display_toc %}

{%- if (page.encrypt) %}
{%- set toc = toc(page.origin, { class: "nav", list_number: page.toc.number, max_depth: page.toc.max_depth }) %}
{%- else %}
{%- set toc = toc(page.content, { class: "nav", list_number: page.toc.number, max_depth: page.toc.max_depth }) %}
{%- endif %}

{%- set display_toc = toc.length > 1 and display_toc %}
{%- endif %}

<ul class="sidebar-nav motion-element">

修改完成后,执行 hexo clean && hexo s -g 并重新预览。

效果如下:

20210418165529

20210418165529

不过,这样的效果貌似不是我想要的。我理想中的效果应该是:

  • 当文章加密后,访客只能看到侧边栏中的 站点概览 部分,不需要看到 文章目录 部分。
  • 当文章解密后,访客则可以看到 站点概览 和 文章目录 两部分。

而现在加密后的文章未解密之前也可以看到 文章目录 ,虽然该目录不可点击。

当然,如果你不是很介意,那么到这里就可以结束了。如果你和我一样有一些 追求完美的强迫症 的话,我们继续。

如何优化

查看了 hexo-blog-encrypt 相关的 issues ,我找到了一种 折中 的解决方法。

从 issue Archer主题解密后TOC依旧不显示(已按手册修改) 中我们可以知道:

我们可以在文章加密的前提下,通过将目录部分加入到一个 不可见的div 中来实现 隐藏目录 的效果。在源码中的 hexo-blog-encrypt/lib/hbe.js 部分我们也可以看到,解密后通过设置 id 值为 toc-div 的元素为 display:inline 来控制显示隐藏。

1
2
3
4
5
6
7
8
9
{%- if (page.encrypt) %}
<div id="toc-div" style="display:none">
{%- else %}
<div id="toc-div">
{%- endif %}

xxx这里是目录部分xxx

</div>

对文件 themes/next/layout/_macro/sidebar.swig 修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--noindex-->
<div class="post-toc-wrap sidebar-panel">
{%- if (page.encrypt) %}
<div id="toc-div" style="display:none">
{%- else %}
<div id="toc-div">
{%- endif %}

{%- if display_toc %}
<div class="post-toc motion-element">{{ toc }}</div>
{%- endif %}

</div>
</div>
<!--/noindex-->

但这种方法并不是完全的加密,而是采用 障眼法 的方式,通过查看html源文件还是可以看到目录内容的,只是不显示罢了。

对于这个问题,hexo-blog-encrypt 插件的作者也作了说明:next 主题内没有 article.ejs 文件【TOC 相关】 · Issue #162 · D0n9X1n/hexo-blog-encrypt

只好妥协

因为该插件中目前只有一个参数 page.encrypt 可以用来判断当前的文章是否进行了 加密处理 ,而不能获知该文章当前是处于 加密后的锁定 状态,还是处于 加密后的解锁 状态。如果再有一个参数结合起来一起处理就好了。

所以,目前只能在解锁前隐藏目录,解锁后再显示目录。但在解锁前目录区域还是会展开,只是没有内容显示罢了。


让加密文章显示加密提示

类似于我的博客文章列表中的 文章置顶 的提示效果,考虑在文章列表中对加密的文章增加类似的 加密 提示信息。

上面对于文章的加密处理,一方面是在 配置文件 中添加的 tag 全局配置,另一方面是在单个 md源文件 中添加的 password 参数。所以我们需要对这两种情况分别做处理。

对于password参数的情况

针对于 password 字段,参考获取其他字段的方法,比如获取标题用 post.title ,获取置顶用 post.top ,那么获取 password 就是 post.password 了。

可以参考我之前添加置顶提示信息的操作,对文件 themes/next/layout/_macro/post.swig 的修改如下:

1
2
3
4
5
6
7
8
9
10
11
{# 加密文章添加提示信息-for password #}
{%- if post.password %}
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="fas fa-lock"></i>
</span>
<span class="post-meta-item-text">
<font color='#FD7E13'>[加密]</font>
</span>
</span>
{%- endif %}
对于tag标签的情况

针对于 tag 标签的获取,可以从文件 themes/next/layout/_macro/post.swig 中找到类似的处理方法:

1
2
3
{%- for tag in post.tags.toArray() %}
<a href="{{ url_for(tag.path) }}" rel="tag">{{ tag_indicate }} {{ tag.name }}</a>
{%- endfor %}

即可以用最简单的 遍历法 来处理:

我们获取到配置文件中设置的所有 加密tag值 ,再找到文章中的 tag标签 。二者一对比,有匹配的项则说明该文章设置了 tag值 加密。

swig文件

要在 .swig 文件中实现相应的对比逻辑,就需要了解其使用的语法格式。而对于 swig 文件,使用的是 Swig 语法。

Swig 是一个非常棒的、类似 Django/jinja 的 node.js 模板引擎。

不过看到这个代码库 paularmstrong/swig: Take a swig of the best template engine for JavaScript. 已经 归档 了。

但因为 Swig 是类似于 jinja 的模板引擎,那么我们直接去参考 jinja 的语法就可以了。

最终实现

获取全局配置中 encrypt.tags 的值:

1
2
3
4
5
{%- if (config.encrypt) and (config.encrypt.tags) %}
{%- for ctag in config.encrypt.tags %}
<span>{{ ctag.name }}</span>
{%- endfor %}
{%- endif %}

在文章列表中获取当前文章包含的 tags 列表:

1
2
3
4
5
{%- if post.tags %}
{%- for ptag in post.tags.toArray() %}
<span>{{ ptag.name }}</span>
{%- endfor %}
{%- endif %}

对于其中展示的文本格式,可以参考已有的 发表于 更新于 这些副标题的格式来实现。

例如:

1
2
3
4
5
6
7
8
9
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>


<time title="创建时间:2021-02-28 11:18:43 / 修改时间:11:41:19" itemprop="dateCreated datePublished" datetime="2021-02-28T11:18:43+08:00">2021-02-28</time>
</span>

对其进行优化,我们只需要显示提示文字,不需要后面的带下划线部分,最终得到的就是:

1
2
3
4
5
6
7
8
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="fas fa-lock"></i>
</span>
<span class="post-meta-item-text">
<font color='#FD7E13'>[加密]</font>
</span>
</span>

整合上面的代码,对于文章中包含 password 的文档,通过如下方式来显示:

20210418170147

20210418170147

相应代码:

1
2
3
4
5
6
7
8
9
10
11
{# 加密文章添加提示信息-for password #}
{%- if post.password %}
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="fas fa-lock"></i>
</span>
<span class="post-meta-item-text">
<font color='#FD7E13'>[加密]</font>
</span>
</span>
{%- endif %}

对于文章中包含指定加密 tags 的文档,通过如下方式来显示:

20210418170209

20210418170209

相应代码:

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
{# 加密文章添加提示信息-for config tags #}
// 获取全局配置中的加密tag
{%- if (config.encrypt) and (config.encrypt.tags) %}
{%- for ctag in config.encrypt.tags %}
// 判断当前文章中是否包含tags
{%- if post.tags %}
{%- for ptag in post.tags.toArray() %}
// 如果有相同的tag值
{%- if (ctag.name == ptag.name) %}
// 显示加密提示信息
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="fas fa-lock"></i>
</span>
<span class="post-meta-item-text">
<font color='#FD7E13'>[加密]</font>
</span>
</span>

{%- endif %}

{%- endfor %}
{%- endif %}

{%- endfor %}
{%- endif %}

对于两种都有的文档,我们只需要通过一个 判断 来处理就好了:优先判断文档中的 password 字段。当文档中包含 password 时,就说明是加密文章;否则就去判断配置文件看是否为加密文章。

20210418170330

20210418170330

最后的代码为:

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
{# 加密文章添加提示信息-for password #}
{%- if post.password %}
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="fas fa-lock"></i>
</span>
<span class="post-meta-item-text">
<font color='#FD7E13'>[加密]</font>
</span>
</span>
{%- else %}
{# 加密文章添加提示信息-for config tags #}
{%- if (config.encrypt) and (config.encrypt.tags) %}
{%- for ctag in config.encrypt.tags %}

{%- if post.tags %}
{%- for ptag in post.tags.toArray() %}
{%- if (ctag.name == ptag.name) %}
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="fas fa-lock"></i>
</span>
<span class="post-meta-item-text">
<font color='#FD7E13'>[加密]</font>
</span>
</span>
{%- endif %}
{%- endfor %}
{%- endif %}

{%- endfor %}
{%- endif %}
{%- endif %}

稍微不好的一点就是,上面的操作是通过 两个for循环 来处理的,会导致一些性能问题。不过这个操作是在编译过程 hexo g 的时候来处理的,不影响博客浏览,也就可以忽略了。


更换图标

对于需要显示的图标,可以从网站 Icons | Font Awesome 中获取。

例如,我这里选择的是 锁 的icon图标,得到的代码如下:

1
<i class="fas fa-lock"></i>

l